From 3bb55fbae0b13291cd15d58cedc5b6c11d62f9b2 Mon Sep 17 00:00:00 2001 From: ThishakaM Date: Wed, 14 Oct 2026 09:29:30 +0530 Subject: [PATCH 1/7] style(nav-rail): improve reorder handle visibility in light mode --- worklenz-frontend/src/components/nav-rail/NavRailItem.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx index 70071e3e9..99ec19e0e 100644 --- a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx +++ b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx @@ -242,11 +242,12 @@ const NavRailItem: React.FC = ({ aria-label={`Reorder ${typeof label === 'string' ? label : item.key}`} style={{ position: 'absolute', - bottom: 2, + top: 2, right: 2, fontSize: 14, color: token.colorPrimary, - background: token.colorBgContainer, + background: isDark ? token.colorBgContainer : token.colorPrimaryBg, + border: `1px solid ${token.colorBorder}`, borderRadius: token.borderRadiusSM, padding: '2px', cursor: 'grab', From e73cc844151758cfdf33e565ac5ef6905c8c8fc3 Mon Sep 17 00:00:00 2001 From: ThishakaM Date: Wed, 14 Oct 2026 09:46:51 +0530 Subject: [PATCH 2/7] style(nav-rail): reduce font size of reorder icon for improved visibility --- worklenz-frontend/src/components/nav-rail/NavRailItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx index 99ec19e0e..958d84ada 100644 --- a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx +++ b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx @@ -244,7 +244,7 @@ const NavRailItem: React.FC = ({ position: 'absolute', top: 2, right: 2, - fontSize: 14, + fontSize: 12, color: token.colorPrimary, background: isDark ? token.colorBgContainer : token.colorPrimaryBg, border: `1px solid ${token.colorBorder}`, From 711cd83615b5ab56566d145ac26e30c6f8066412 Mon Sep 17 00:00:00 2001 From: ThishakaM Date: Wed, 14 Oct 2026 14:03:17 +0530 Subject: [PATCH 3/7] feat(nav-rail): optimize item interactions, fix pin toggle, and add soft icon glow - Enable full-item dragging by binding dnd-kit attributes directly to the item button, removing the separate drag handle. - Fix pin toggle affordance: - Reposition to top-left (top: 4, left: 4) to prevent overlap with unread badges and Soon tags. - Intercept pointer-down events on the pin to prevent dnd-kit from capturing clicks as drag initiations. - Show pin affordance when an item is active, pinned, or hovered. - Replace blocky background hover highlight with a subtle, soft icon-only glow using CSS drop-shadow (3px radius, 45% opacity). - Optimize performance by delegating all hover states, transitions, and glow effects to CSS, eliminating React hover states and preventing unnecessary re-renders. - Reposition indicators (badge at top: 4, right: 5; Soon tag at top: 22, right: 4) with pointer-events: none to maintain hit-test integrity. --- .../src/components/nav-rail/NavRailItem.tsx | 288 ++++++++++-------- .../src/components/nav-rail/nav-rail.css | 67 +++- 2 files changed, 212 insertions(+), 143 deletions(-) diff --git a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx index 958d84ada..20f5ac635 100644 --- a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx +++ b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useMemo, useCallback, useRef } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Tooltip, theme } from '@/shared/antd-imports'; -import { HolderOutlined, PushpinFilled, PushpinOutlined } from '@/shared/antd-imports'; +import { PushpinFilled, PushpinOutlined } from '@/shared/antd-imports'; import type { NavItem } from '@/features/navigation/nav-registry.types'; import './nav-rail.css'; @@ -48,112 +48,176 @@ const NavRailItem: React.FC = ({ disabled: !draggable, }); - // Matches PlannerLeftSidebar.tsx's existing fix for antd Tooltip's dark - // chip reading as a mismatched box against a light-mode rail — reused here - // so every surface gets the same theme-matched tooltip instead of each - // rewriting the same workaround. - const tooltipProps = { - color: isDark ? undefined : '#fff', - styles: { body: isDark ? undefined : { color: token.colorText, boxShadow: '0 2px 8px rgba(0,0,0,.15)' } }, - }; + // Track whether we're interacting with the pin so we can suppress the + // item's own click/drag when the user intends to click the pin. + const pinInteracting = useRef(false); - // `soon` items are non-clickable by default; `soonClickable` opts a - // specific item (e.g. Client Portal's Ticketing) out of that while - // keeping the dimmed styling and "Soon" tag below. const clickDisabled = item.soon && !item.soonClickable; const tooltipTitle = isPinned ? `${label} (Pinned)` : label; + const iconColor = active + ? token.colorPrimary + : isDark + ? 'rgba(255,255,255,.45)' + : 'rgba(0,0,0,.45)'; - const iconColor = active ? token.colorPrimary : isDark ? 'rgba(255,255,255,.45)' : 'rgba(0,0,0,.45)'; + // ─── Memoized styles ──────────────────────────────────────────────────────── - const wrapperStyle: React.CSSProperties = { - position: 'relative', - width: '100%', - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - zIndex: isDragging ? 1 : undefined, - }; + const wrapperStyle = useMemo( + () => ({ + position: 'relative' as const, + width: '100%', + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + zIndex: isDragging ? 1 : undefined, + // Pass token colors to CSS for pure-CSS hover glow (zero JS re-renders on hover) + '--nav-rail-color-primary': token.colorPrimary, + '--nav-rail-glow': token.colorPrimary, + } as React.CSSProperties), + [transform, transition, isDragging, token.colorPrimary], + ); + + const buttonStyle = useMemo( + () => ({ + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + justifyContent: 'center', + gap: 3, + width: '100%', + padding: collapsed ? '3px 2px' : '6px 4px 6px', + cursor: clickDisabled ? 'default' : 'pointer', + opacity: item.soon ? 0.45 : 1, + position: 'relative' as const, + background: 'transparent', + }), + [collapsed, clickDisabled, item.soon], + ); + + // Pin icon colour: primary when already pinned, muted otherwise. + // The :hover glow and color are handled in CSS without re-renders. + const pinIconColor = useMemo( + () => (isPinned ? token.colorPrimary : isDark ? 'rgba(255,255,255,.40)' : 'rgba(0,0,0,.35)'), + [isPinned, isDark, token.colorPrimary], + ); + + const pinIconStyle = useMemo( + () => ({ + fontSize: 10, + color: pinIconColor, + transition: 'color .15s', + }), + [pinIconColor], + ); + + const pinWrapperStyle = useMemo( + () => ({ + position: 'absolute' as const, + // Top-left corner — clear of badge (top-right) and Soon tag (right side) + top: 4, + left: 4, + width: 16, + height: 16, + borderRadius: token.borderRadiusSM, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 3, + cursor: 'pointer', + }), + [token.borderRadiusSM], + ); + + const tooltipProps = useMemo( + () => ({ + color: isDark ? undefined : '#fff', + styles: { + body: isDark ? undefined : { color: token.colorText, boxShadow: '0 2px 8px rgba(0,0,0,.15)' }, + }, + }), + [isDark, token.colorText], + ); + + // ─── Stable callbacks ──────────────────────────────────────────────────────── + + const handleSelect = useCallback(() => { + if (!clickDisabled) onSelect(); + }, [clickDisabled, onSelect]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => activateOnKey(handleSelect)(e), + [handleSelect], + ); - // Affordances (pin, drag handle) are always in the DOM — visibility is - // driven by CSS :hover/:focus-within (see nav-rail.css) so a keyboard user - // tabbing to them can actually find and use them, not just a mouse user. - const affordanceClassName = `nav-rail-affordance${isPinned ? ' nav-rail-affordance-visible' : ''}`; + // Pin click: stop propagation so the item click / DnD listeners don't fire. + const handlePinClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + pinInteracting.current = false; + onTogglePin(); + }, + [onTogglePin], + ); + + const handlePinKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + activateOnKey(onTogglePin)(e); + }, + [onTogglePin], + ); - const togglePin = () => onTogglePin(); + // Intercept DnD pointer events on the pin so they don't start a drag. + const handlePinPointerDown = useCallback((e: React.PointerEvent) => { + e.stopPropagation(); + pinInteracting.current = true; + }, []); return (
- - {/* A
with button semantics, not a real
); }; -export default NavRailItem; +export default React.memo(NavRailItem); diff --git a/worklenz-frontend/src/components/nav-rail/nav-rail.css b/worklenz-frontend/src/components/nav-rail/nav-rail.css index 51e41d0a3..79100cf71 100644 --- a/worklenz-frontend/src/components/nav-rail/nav-rail.css +++ b/worklenz-frontend/src/components/nav-rail/nav-rail.css @@ -10,19 +10,68 @@ } } -/* Pin + drag-handle affordances on a NavRailItem: always in the DOM (so a - keyboard user tabbing through can reach them), hidden until the item is - hovered or something inside it has focus, or the affordance is forced - visible (a pinned item's pin icon stays showing even without hover). */ -.nav-rail-affordance { +/* ── Main nav icon & label glow (icon glows on hover, NO background highlight) ── */ +.nav-rail-item-icon { + width: 24px; + height: 24px; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.18s ease, filter 0.18s ease; + overflow: visible; + filter: none; +} + +.nav-rail-item-label { + font-size: 11px; + text-align: center; + line-height: 1.2; + white-space: pre-line; + transition: color 0.18s ease; +} + +/* On item hover / focus: subtle, soft glow on the icon only, never the background */ +.nav-rail-item-wrapper:hover .nav-rail-item-icon, +.nav-rail-item-wrapper:focus-within .nav-rail-item-icon { + color: var(--nav-rail-color-primary, #1890ff) !important; + filter: drop-shadow(0 0 3px rgba(24, 144, 255, 0.4)); + filter: drop-shadow(0 0 3px color-mix(in srgb, var(--nav-rail-glow, #1890ff) 45%, transparent)); +} + +.nav-rail-item-wrapper:hover .nav-rail-item-label, +.nav-rail-item-wrapper:focus-within .nav-rail-item-label { + color: var(--nav-rail-color-primary, #1890ff) !important; +} + +/* ── Pin affordance ──────────────────────────────────────────────────────── + Always in DOM for accessibility. Shown when: + - item is active or pinned (.nav-rail-pin-visible) + - item is hovered or focused (.nav-rail-item-wrapper:hover / :focus-within) +*/ +.nav-rail-pin-affordance { opacity: 0; pointer-events: none; - transition: opacity 0.15s; + transition: opacity 0.15s ease, filter 0.15s ease, color 0.15s ease; + overflow: visible; + filter: none; } -.nav-rail-item-wrapper:hover .nav-rail-affordance, -.nav-rail-item-wrapper:focus-within .nav-rail-affordance, -.nav-rail-affordance.nav-rail-affordance-visible { +.nav-rail-item-wrapper:hover .nav-rail-pin-affordance, +.nav-rail-item-wrapper:focus-within .nav-rail-pin-affordance, +.nav-rail-pin-affordance.nav-rail-pin-visible { opacity: 1; pointer-events: auto; } + +/* Soft glow and highlight on pin icon when hovered directly */ +.nav-rail-pin-affordance:hover, +.nav-rail-pin-affordance:focus-visible { + filter: drop-shadow(0 0 2.5px rgba(24, 144, 255, 0.4)); + filter: drop-shadow(0 0 2.5px color-mix(in srgb, var(--nav-rail-glow, #1890ff) 45%, transparent)); +} + +.nav-rail-pin-affordance:hover .anticon, +.nav-rail-pin-affordance:focus-visible .anticon { + color: var(--nav-rail-color-primary, #1890ff) !important; +} From ddff1bcd56469c9f08a5f3fd6a25ce5ae2224607 Mon Sep 17 00:00:00 2001 From: ThishakaM Date: Wed, 14 Oct 2026 15:08:08 +0530 Subject: [PATCH 4/7] fix(nav-rail): un-nest pin control and preserve dnd-kit keyboard listeners - Move the pin affordance out of the sortable item element to avoid nested interactive controls while preserving its styling, visual layout, and keyboard accessibility. - Update handleKeyDown to invoke listeners.onKeyDown from dnd-kit and check defaultPrevented to prevent keyboard drag activations from triggering item selection. - Remove the unused pinInteracting ref and unused useRef import while retaining event propagation stops on the pin control. --- .../src/components/nav-rail/NavRailItem.tsx | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx index 20f5ac635..4d127d6bc 100644 --- a/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx +++ b/worklenz-frontend/src/components/nav-rail/NavRailItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback, useRef } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Tooltip, theme } from '@/shared/antd-imports'; @@ -20,9 +20,7 @@ interface NavRailItemProps { } // Fires `action` on Enter/Space, mirroring a native