From 7dae4be61d3ee06e8c23fd8366f4c46c9511738e Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 16 May 2026 13:11:35 +0700 Subject: [PATCH] refactor(frontend): headless controller for DetailNavigation Detail pages duplicated DetailNavigationToolbar's internal navigation state (prev/next, sheet open, position label) because mobile chrome lives inside a DropdownMenuItem and could not reuse the toolbar component. Promote useDetailNavigation to return a full DetailNavigationController, have the leaf components (Buttons / Sheet / Toolbar) read from it, and drop the ~40 LOC mobile mirror block on each of three pages. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../detail-navigation-buttons.tsx | 73 ++-- .../detail-navigation-sheet.test.tsx | 241 ++++++++++++ .../detail-navigation-sheet.tsx | 54 ++- .../detail-navigation-toolbar.test.tsx | 306 +++------------ .../detail-navigation-toolbar.tsx | 172 +-------- .../shared/detail-navigation/index.ts | 5 +- .../use-detail-navigation.test.tsx | 362 +++++++++++++++--- .../use-detail-navigation.ts | 298 +++++++++++--- .../flows/use-flow-detail-navigation.ts | 6 +- .../features/knowledges/knowledge-header.tsx | 154 ++------ .../use-knowledge-detail-navigation.ts | 4 +- .../use-template-detail-navigation.ts | 9 +- frontend/src/pages/flows/flow.tsx | 176 ++------- frontend/src/pages/templates/template.tsx | 118 ++---- 14 files changed, 1045 insertions(+), 933 deletions(-) create mode 100644 frontend/src/components/shared/detail-navigation/detail-navigation-sheet.test.tsx diff --git a/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx b/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx index 323a601c..04608860 100644 --- a/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx +++ b/frontend/src/components/shared/detail-navigation/detail-navigation-buttons.tsx @@ -2,43 +2,40 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; -interface DetailNavigationButtonsProps { - /** Disable the position-button when the filtered subset is empty. */ - hasEntries: boolean; - /** No next sibling — disable the right chevron. */ - nextDisabled: boolean; - onNext: () => void; - onOpen: () => void; - onPrev: () => void; - /** Pre-formatted `"3/10"` (or `"–/0"` when no current). */ - positionLabel: string; - /** No previous sibling — disable the left chevron. */ - prevDisabled: boolean; +import type { DetailNavigationController } from './use-detail-navigation'; + +interface DetailNavigationButtonsProps { + controller: DetailNavigationController; /** Lowercased plural used in the aria-label / tooltip ("flows", "templates"). */ sheetTitle: string; + /** + * Size variant. `'default'` is the desktop toolbar's `size-8` cluster; + * `'sm'` shrinks the cluster to `size-7` for embedding inside a + * `` on mobile, where the host row is already padded. + */ + size?: 'default' | 'sm'; } /** - * Prev / Position / Next button cluster for a detail page. Stateless and - * presentation-only — `DetailNavigationToolbar` owns the navigation logic - * and feeds the resolved indices, labels, and click handlers down. + * Prev / Position / Next button cluster bound to a `DetailNavigationController`. + * Stateless: the controller owns navigation, `isSheetOpen`, and the + * pre-formatted `positionLabel`. * - * Kept separate from `DetailNavigationSheet` so the same buttons could in - * principle be reused without the sheet (e.g. a future variant that ships - * keyboard-only navigation without an overlay). + * Reused in both the desktop toolbar (`size="default"`) and the mobile + * dropdown row (`size="sm"`) — same a11y contract, same tooltips, same + * keyboard semantics in both places. */ -export const DetailNavigationButtons = ({ - hasEntries, - nextDisabled, - onNext, - onOpen, - onPrev, - positionLabel, - prevDisabled, +export const DetailNavigationButtons = ({ + controller, sheetTitle, -}: DetailNavigationButtonsProps) => { + size = 'default', +}: DetailNavigationButtonsProps) => { const lowerTitle = sheetTitle.toLowerCase(); + const isSm = size === 'sm'; + const sideButtonSize = isSm ? 'size-7' : 'size-8'; + const middleHeight = isSm ? 'h-7' : 'h-8'; return (
@@ -46,9 +43,9 @@ export const DetailNavigationButtons = ({ Show all matching {lowerTitle} @@ -75,9 +72,9 @@ export const DetailNavigationButtons = ({ - - + + controller={knowledgeNav} + sheetTitle="Knowledges" + size="sm" + />
@@ -326,28 +256,10 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu {isMobile && canShowActions && ( - currentId={knowledgeToolbarProps.currentId} - currentIndex={mobileNav.currentIndex} - getId={knowledgeToolbarProps.getId} - getLabel={knowledgeToolbarProps.getLabel} - items={mobileNav.filteredItems} - onItemSelect={mobileNavSelectItem} - onOpenChange={setIsMobileNavSheetOpen} - open={isMobileNavSheetOpen} - renderItem={(item, isCurrent) => ( - <> - - {item.docType} - - {item.question} - - )} + controller={knowledgeNav} + renderItem={renderKnowledgeItem} sheetIcon={} sheetTitle="Knowledges" - total={mobileNav.total} /> )} item.question; const getHref = (item: Knowledge) => `/knowledges/${item.id}`; /** - * Detail-page navigation wired up for knowledge documents. The list page + * Detail-page navigation wired up for knowledge documents. Returns a + * `DetailNavigationController` for `` / + * `` / ``. The list page * filters on `question` and the header shows the same, so `getLabel` * doubles as the default searchable text. */ diff --git a/frontend/src/features/templates/use-template-detail-navigation.ts b/frontend/src/features/templates/use-template-detail-navigation.ts index 56f7bd7d..9e93c193 100644 --- a/frontend/src/features/templates/use-template-detail-navigation.ts +++ b/frontend/src/features/templates/use-template-detail-navigation.ts @@ -6,9 +6,12 @@ const getId = (item: Template) => String(item.id); const getHref = (item: Template) => `/templates/${item.id}`; /** - * Detail-page navigation wired up for templates. The list page filters on - * `title` and the breadcrumb shows the same, so `getLabel` doubles as the - * default searchable text (no explicit `getSearchableText` needed). + * Detail-page navigation wired up for templates. Returns a + * `DetailNavigationController