@withpotter/components 0.13.0-next.124 → 0.13.0-next.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/billing/overview-tab.js +3 -4
  2. package/dist/billing/overview-tab.js.map +1 -1
  3. package/dist/billing/plans-tab.js +12 -6
  4. package/dist/billing/plans-tab.js.map +1 -1
  5. package/dist/charts/index.d.ts +1 -1
  6. package/dist/charts/index.js +1 -3
  7. package/dist/charts/index.js.map +1 -1
  8. package/dist/dashboard/activity-feed.d.ts +0 -2
  9. package/dist/dashboard/activity-feed.js +1 -4
  10. package/dist/dashboard/activity-feed.js.map +1 -1
  11. package/dist/dashboard/home/ask-controls.js +0 -1
  12. package/dist/dashboard/home/ask-controls.js.map +1 -1
  13. package/dist/dashboard/home/ask-mode-chip.js +4 -4
  14. package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
  15. package/dist/dashboard/home/home-ask.js +3 -3
  16. package/dist/dashboard/home/home-ask.js.map +1 -1
  17. package/dist/dashboard/home/home-quick-actions.js +1 -9
  18. package/dist/dashboard/home/home-quick-actions.js.map +1 -1
  19. package/dist/dashboard/home/widgets/activity-widget.js +43 -56
  20. package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
  21. package/dist/dashboard/home/widgets/home-widgets.js +2 -3
  22. package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
  23. package/dist/dashboard/home/widgets/money-widgets.js +4 -9
  24. package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
  25. package/dist/dashboard/home/widgets/registry.d.ts +3 -6
  26. package/dist/dashboard/home/widgets/registry.js +2 -12
  27. package/dist/dashboard/home/widgets/registry.js.map +1 -1
  28. package/dist/dashboard/home/widgets/team-widget.js +3 -15
  29. package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
  30. package/dist/dashboard/home/widgets/types.d.ts +1 -13
  31. package/dist/dashboard/home/widgets/types.js.map +1 -1
  32. package/dist/dashboard/home/widgets/use-home-layout.js +4 -9
  33. package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
  34. package/dist/dashboard/home/widgets/website-widget.js +1 -9
  35. package/dist/dashboard/home/widgets/website-widget.js.map +1 -1
  36. package/dist/dashboard/page-transition.d.ts +0 -11
  37. package/dist/dashboard/page-transition.js +2 -9
  38. package/dist/dashboard/page-transition.js.map +1 -1
  39. package/dist/features/gift-card-artwork.js +1 -1
  40. package/dist/features/gift-card-artwork.js.map +1 -1
  41. package/dist/features/gift-cards/gift-cards-feature.js +518 -656
  42. package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
  43. package/dist/features/product-form.js +1 -1
  44. package/dist/features/product-form.js.map +1 -1
  45. package/dist/mobile/screens/billing-flows.js +6 -7
  46. package/dist/mobile/screens/billing-flows.js.map +1 -1
  47. package/dist/mobile/screens/billing.js +2 -2
  48. package/dist/mobile/screens/billing.js.map +1 -1
  49. package/dist/notifications-dropdown.js +1 -11
  50. package/dist/notifications-dropdown.js.map +1 -1
  51. package/dist/shell/notification-kinds.d.ts +2 -11
  52. package/dist/shell/notification-kinds.js +1 -16
  53. package/dist/shell/notification-kinds.js.map +1 -1
  54. package/dist/shell/notification-row.d.ts +2 -6
  55. package/dist/shell/notification-row.js +33 -59
  56. package/dist/shell/notification-row.js.map +1 -1
  57. package/dist/shell/product-switcher.js +0 -3
  58. package/dist/shell/product-switcher.js.map +1 -1
  59. package/dist/shell/settings-modal-context.d.ts +0 -2
  60. package/dist/shell/settings-modal-context.js +2 -4
  61. package/dist/shell/settings-modal-context.js.map +1 -1
  62. package/dist/shell/settings-panels.js +9 -29
  63. package/dist/shell/settings-panels.js.map +1 -1
  64. package/dist/shell/settings-rows.d.ts +2 -7
  65. package/dist/shell/settings-rows.js +36 -93
  66. package/dist/shell/settings-rows.js.map +1 -1
  67. package/dist/shell/shell.css +0 -192
  68. package/dist/shell/sidebar-footer.js +2 -6
  69. package/dist/shell/sidebar-footer.js.map +1 -1
  70. package/dist/shell/unified-settings-modal.d.ts +2 -3
  71. package/dist/shell/unified-settings-modal.js +74 -151
  72. package/dist/shell/unified-settings-modal.js.map +1 -1
  73. package/dist/sidebar/config.js +3 -19
  74. package/dist/sidebar/config.js.map +1 -1
  75. package/dist/sidebar/more-group.js +1 -1
  76. package/dist/sidebar/more-group.js.map +1 -1
  77. package/dist/sidebar/nav-renderers.js +0 -2
  78. package/dist/sidebar/nav-renderers.js.map +1 -1
  79. package/dist/sidebar/nav-section.js +3 -5
  80. package/dist/sidebar/nav-section.js.map +1 -1
  81. package/dist/sidebar/setup-progress.d.ts +1 -3
  82. package/dist/sidebar/setup-progress.js +2 -2
  83. package/dist/sidebar/setup-progress.js.map +1 -1
  84. package/dist/sidebar/spine-groups.js +1 -7
  85. package/dist/sidebar/spine-groups.js.map +1 -1
  86. package/dist/sidebar.js +5 -5
  87. package/dist/sidebar.js.map +1 -1
  88. package/dist/templates/template-live-thumb.js +5 -16
  89. package/dist/templates/template-live-thumb.js.map +1 -1
  90. package/package.json +5 -5
  91. package/dist/dashboard/activity-paid.d.ts +0 -10
  92. package/dist/dashboard/activity-paid.js +0 -7
  93. package/dist/dashboard/activity-paid.js.map +0 -1
  94. package/dist/dashboard/home/host-quick-actions.d.ts +0 -19
  95. package/dist/dashboard/home/host-quick-actions.js +0 -12
  96. package/dist/dashboard/home/host-quick-actions.js.map +0 -1
  97. package/dist/dashboard/home/widgets/host-widgets.d.ts +0 -6
  98. package/dist/dashboard/home/widgets/host-widgets.js +0 -16
  99. package/dist/dashboard/home/widgets/host-widgets.js.map +0 -1
  100. package/dist/dashboard/home/widgets/member-label.d.ts +0 -11
  101. package/dist/dashboard/home/widgets/member-label.js +0 -10
  102. package/dist/dashboard/home/widgets/member-label.js.map +0 -1
  103. package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +0 -5
  104. package/dist/dashboard/home/widgets/middle-ellipsis.js +0 -11
  105. package/dist/dashboard/home/widgets/middle-ellipsis.js.map +0 -1
  106. package/dist/dashboard/home/widgets/revenue-series.d.ts +0 -12
  107. package/dist/dashboard/home/widgets/revenue-series.js +0 -26
  108. package/dist/dashboard/home/widgets/revenue-series.js.map +0 -1
  109. package/dist/dashboard/page-transition-key.d.ts +0 -7
  110. package/dist/dashboard/page-transition-key.js +0 -11
  111. package/dist/dashboard/page-transition-key.js.map +0 -1
  112. package/dist/features/gift-cards/gift-card-logic.d.ts +0 -41
  113. package/dist/features/gift-cards/gift-card-logic.js +0 -65
  114. package/dist/features/gift-cards/gift-card-logic.js.map +0 -1
  115. package/dist/features/gift-cards/hint.d.ts +0 -9
  116. package/dist/features/gift-cards/hint.js +0 -25
  117. package/dist/features/gift-cards/hint.js.map +0 -1
  118. package/dist/shell/notification-actions.d.ts +0 -12
  119. package/dist/shell/notification-actions.js +0 -18
  120. package/dist/shell/notification-actions.js.map +0 -1
  121. package/dist/shell/settings-groups.d.ts +0 -23
  122. package/dist/shell/settings-groups.js +0 -22
  123. package/dist/shell/settings-groups.js.map +0 -1
  124. package/dist/shell/settings-host.d.ts +0 -39
  125. package/dist/shell/settings-host.js +0 -37
  126. package/dist/shell/settings-host.js.map +0 -1
  127. package/dist/shell/sidebar-cards.d.ts +0 -32
  128. package/dist/shell/sidebar-cards.js +0 -37
  129. package/dist/shell/sidebar-cards.js.map +0 -1
  130. package/dist/templates/template-tile-source.d.ts +0 -24
  131. package/dist/templates/template-tile-source.js +0 -18
  132. package/dist/templates/template-tile-source.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowUpRight, ChevronLeft, Search, X } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\nimport { Dialog, DialogClose, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\n/**\n * A row of the rail: 34px tall, 13.5px, an icon 16px wide at 1.75 stroke. The values are the settings prototype's; the\n * colours come from the window's own palette (`.st-modal` in shell.css), which falls back to the theme in dark.\n */\nconst ROW_CLASS =\n 'mb-px flex h-[34px] w-full items-center gap-[11px] rounded-[9px] px-2 text-left text-[13.5px] font-medium leading-5 transition-colors';\nconst ROW_IDLE =\n 'text-[var(--st-ink-2)] hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)]';\nconst ROW_ON =\n 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-[var(--st-row-on-ring)]';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right):\n * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n // Legal is not on the rail: it sits at the foot, beside Get help.\n const railRows = shown.filter((r) => r.id !== 'legal');\n const legalRow = shown.find((r) => r.id === 'legal');\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n showClose={false}\n className=\"st-modal flex h-[min(640px,calc(100dvh-2.5rem))] w-[calc(100%-2.5rem)] max-w-[920px] gap-0 overflow-hidden rounded-2xl border-0 p-0 text-[13.5px] shadow-[0_24px_60px_-20px_rgba(18,18,34,0.45),0_0_0_1px_rgba(0,0,0,0.04)]\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[248px] shrink-0 flex-col overflow-hidden border-r border-[var(--st-rail-line)] bg-[var(--st-rail)] px-3 pb-2.5 pt-4\"\n >\n {name && (\n <div className=\"flex items-center gap-2.5 px-1.5 pb-3.5 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"max-w-[150px] truncate text-[14px] font-semibold leading-[18px] text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-[12px] leading-4 text-[var(--st-ink-3)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\n )}\n <div className=\"mb-1.5 flex h-8 shrink-0 items-center gap-2 rounded-[10px] bg-[var(--bg-base)] px-2.5 text-[var(--st-ink-3)] shadow-[var(--st-search-ring)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-[15px] shrink-0\" strokeWidth={1.8} />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <div className=\"-mx-1 min-h-0 flex-1 overflow-y-auto px-1\">\n {shown.length === 0 && (\n <p className=\"px-2 py-2 text-[13px] text-[var(--st-ink-3)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(railRows).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-2 pb-1.5 pt-3.5 text-[12px] font-medium leading-5 text-[var(--st-ink-4)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0 opacity-[.55]\" />\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${selected ? ROW_ON : ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n </Fragment>\n ))}\n </div>\n {/* The foot is one row, as in the prototype: Get help, with Legal beside it so it stays one press away without\n taking a row of its own (a second row pushed the last rail rows out of view). */}\n <div className=\"mt-1.5 flex shrink-0 items-center gap-1 border-t border-[var(--st-rail-rule)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE} min-w-0 flex-1`}\n >\n <HelpIcon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span>Get help</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0\" />\n </Link>\n {legalRow && (\n <button\n type=\"button\"\n data-row\n aria-current={legalRow.id === current?.id ? 'page' : undefined}\n onClick={() => {\n setPicked(legalRow.id);\n setView(null);\n }}\n className={`flex h-[34px] shrink-0 items-center rounded-[9px] px-2.5 text-[13.5px] font-medium transition-colors ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`}\n >\n {legalRow.name}\n </button>\n )}\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex flex-none items-start justify-between pb-4 pl-10 pr-6 pt-5\">\n <div className=\"min-w-0\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"-ml-0.5 mb-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-[var(--st-ink-2)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <ChevronLeft className=\"size-[15px]\" strokeWidth={2} />\n {current?.name ?? 'Settings'}\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-[18px] font-semibold leading-6 tracking-[-0.01em] text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n {!view && current?.subtitle && (\n <p className=\"mt-0.5 text-[13px] leading-[18px] text-[var(--st-ink-3)]\">\n {current.subtitle}\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-2.5\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n <DialogClose\n aria-label=\"Close\"\n className=\"grid size-8 place-items-center rounded-[9px] text-[var(--st-ink-2)] transition-colors hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]\"\n >\n <X className=\"size-[18px]\" strokeWidth={2} />\n </DialogClose>\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-6 pb-7 outline-none\">\n <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { ArrowLeft, ArrowUpRight, Search } from 'lucide-react';\nimport { Dialog, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\nconst ROW_CLASS =\n 'flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm font-medium transition-colors';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right)\n * with every row in one list. A search at the top matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<SettingsRowId | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const rows = useMemo(\n () =>\n SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n ),\n [can, loading],\n );\n const shown = matchRows(rows, query);\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n className=\"flex h-[min(78vh,640px)] max-w-[860px] gap-0 overflow-hidden p-0\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[224px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--border-subtle)] bg-[var(--bg-muted)] p-3\"\n >\n <div className=\"mb-2 flex items-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 text-[var(--text-tertiary)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-4 shrink-0\" />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <p className=\"px-3 pb-1 pt-1 text-xs font-medium text-[var(--text-tertiary)]\">\n Settings\n </p>\n {shown.length === 0 && (\n <p className=\"px-3 py-2 text-sm text-[var(--text-tertiary)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {shown.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${\n selected\n ? 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm'\n : 'text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n <div className=\"mt-auto border-t border-[var(--border-subtle)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <HelpIcon className=\"size-[18px] shrink-0\" />\n <span className=\"flex-1\">Get help</span>\n <ArrowUpRight className=\"size-4 shrink-0 text-[var(--text-tertiary)]\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"grid size-7 shrink-0 place-items-center rounded-lg text-[var(--text-secondary)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]\"\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-md font-semibold text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n </div>\n <div className=\"mr-7 flex items-center gap-2\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none\">\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n <SettingsPanel key={current.id} id={current.id} />\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
@@ -6,12 +6,10 @@ import {
6
6
  UserRound,
7
7
  LayoutGrid,
8
8
  SlidersHorizontal,
9
- ToggleLeft,
10
9
  Database,
11
10
  ShieldCheck,
12
11
  Accessibility,
13
- ChartColumn,
14
- Sparkles
12
+ ChartColumn
15
13
  } from "lucide-react";
16
14
  import { INTENT_ICON_MAP } from "@withpotter/lib/intents/icon-map";
17
15
  const ICON_MAP = INTENT_ICON_MAP;
@@ -73,13 +71,6 @@ const BUSINESS_GROUPS = [
73
71
  match: ["/dashboard/settings/branding", "/dashboard/settings/regional"],
74
72
  icon: SlidersHorizontal
75
73
  },
76
- // FEATURES — the switches no business type implies (Blog today).
77
- // Business types themselves are Explore's, see below.
78
- {
79
- name: "Features",
80
- href: "/dashboard/settings/features",
81
- icon: ToggleLeft
82
- },
83
74
  // NO BUSINESS TYPE ROW. It moved out of Settings entirely on
84
75
  // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.
85
76
  //
@@ -91,8 +82,8 @@ const BUSINESS_GROUPS = [
91
82
  // something you already have, and this is where you go to get something
92
83
  // you do not.
93
84
  //
94
- // `/dashboard/settings/features` is Features again (above): it no longer
95
- // holds the business model, only the switches no model implies.
85
+ // `/dashboard/settings/features` redirects, so nothing that linked to it
86
+ // breaks.
96
87
  // MONEY, then the PEOPLE who can spend it.
97
88
  // A licensed box that bills outside the console hides the row.
98
89
  ...process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === "true" ? [] : [
@@ -126,13 +117,6 @@ const BUSINESS_GROUPS = [
126
117
  name: "Notifications",
127
118
  href: "/dashboard/settings/notifications",
128
119
  icon: Bell
129
- },
130
- {
131
- // Sample products, pages and pictures a business can start with, clear
132
- // in one go, or add again. Owner and admins act; others see the state.
133
- name: "Demo content",
134
- href: "/dashboard/settings/demo-content",
135
- icon: Sparkles
136
120
  }
137
121
  // Connectors moved to Customize (2026-10-03), beside Add more: both are
138
122
  // "what is in this workspace". `/dashboard/settings/integrations` now
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/config.ts"],"sourcesContent":["/**\n * The sidebar's static shape: icon and permission maps, the nav types, the\n * route sets that decide what counts as active, and the settings drill-in\n * groups.\n *\n * Split out of sidebar.tsx, which had grown to 1,510 lines — three times the\n * house limit. None of this depends on component state, so it does not belong\n * inside the component that renders it.\n */\nimport {\n CreditCard,\n Users,\n Bell,\n Scale,\n UserRound,\n LayoutGrid,\n SlidersHorizontal,\n ToggleLeft,\n Database,\n ShieldCheck,\n Accessibility,\n Globe2,\n Palette,\n ChartColumn,\n Sparkles,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { INTENT_ICON_MAP } from '@withpotter/lib/intents/icon-map';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/** Map string icon names from IntentConfig to Lucide components */\nexport const ICON_MAP = INTENT_ICON_MAP;\n\n// The nav permission maps are data and live in ./nav-permissions, so the\n// coverage test can read them without pulling this file's icon imports in.\n// Re-exported here because every existing importer reaches for them via config.\nexport {\n INTENT_ITEM_PERMISSIONS,\n CORE_TOOL_PERMISSIONS,\n} from './nav-permissions';\n\nexport interface NavItem {\n id: string;\n name: string;\n href: string;\n icon: LucideIcon;\n requiredBehavior?: string[];\n /**\n * The row is only drawn on a box whose capability list carries this key. A\n * box that published no list is unchanged, so the main app keeps every row.\n * For a screen whose backend only some engines have: without it the row led\n * to \"Cannot GET /referrals/stats\" on a box that never had that module.\n */\n requiredCapability?: string;\n /**\n * The item is only rendered when the current team member has AT LEAST ONE of\n * these permissions (OWNER always passes). Undefined or empty means the item\n * is hidden from every non-owner — see INTENT_ITEM_PERMISSIONS for why the\n * default is closed rather than open.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Rows revealed under this one. An item with children renders with the same\n * disclosure the fixed spine hubs use, so an Operations surface can carry a\n * smaller surface (Appointments → Waitlist) without claiming a second\n * top-level row for it.\n */\n children?: NavItem[];\n /**\n * Names this row's own landing page once it carries children.\n *\n * A work row with children renders through `renderHub`, which moves the\n * row's own screen into the group as its first child — a row is a place or a\n * container, never both. Without this the child falls back to \"Overview\",\n * which the hub renderer says outright no new hub should rely on: every hub\n * could carry one, so the word tells the merchant nothing about which hub\n * they are in. `{label}` interpolates the row's post-rename label, so a\n * tenant calling them Bookings gets \"All bookings\" rather than a literal.\n */\n landingLabel?: string;\n}\n\nexport interface NavGroupSection {\n id: string;\n label: string;\n items: NavItem[];\n}\n\nexport interface NavGroup {\n id: string;\n label: string;\n icon: LucideIcon;\n items?: NavItem[];\n sections?: NavGroupSection[];\n /**\n * With no `items`, the group is a flat top-level destination (rendered like\n * the Home link — no chevron, no children).\n *\n * With `items`, this is the group's landing page, and it is rendered as the\n * FIRST CHILD rather than on the group's own row. A row is either a place or\n * a container, never both: a row that navigated *and* disclosed gave two\n * answers to one click, and the highlight landed on the parent while the\n * list that had just opened underneath it looked like a menu you had yet to\n * choose from. See `landingLabel` for what the child is called.\n */\n href?: string;\n /**\n * What this group's landing page is called once it sits among its own\n * children. Defaults to \"Overview\". Ignored unless the group has both an\n * `href` and `items`.\n *\n * `{label}` interpolates the group's OWN label, after renaming. A hub named\n * for a merchant-facing noun is renamed per tenant — Customers becomes\n * Guests, Clients, Members — and a literal \"All customers\" would have sat\n * under a hub reading \"Guests\", which is the exact mismatch `renameGroup`\n * exists to prevent. Write `All {label}`, not `All customers`.\n */\n landingLabel?: string;\n /**\n * Where the landing page sits among the children, counting from 0. Absent, it\n * leads, which every hub but Money wants. Money puts the wallet above its\n * Transactions page (John, 2026-10-11): the balance a business holds is the\n * first question, the log of what moved it the second.\n */\n landingAt?: number;\n /**\n * Only meaningful for flat (href) groups: gate visibility on AT LEAST ONE\n * of these permissions (OWNER always passes). Collapsible groups gate at\n * the item level instead.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Render as a disclosure hub even with a single child (normally a 1-child\n * group collapses to a flat link). Used by Inventory so Collections shows\n * inside it.\n */\n alwaysHub?: boolean;\n}\n\nexport const EXACT_MATCH_ROUTES = new Set([\n '/dashboard',\n '/dashboard/settings',\n]);\nexport const PLATFORM_CONTENT_TYPES = new Set(['product', 'products']);\nexport const PLATFORM_CONTENT_ROUTES = new Set([\n '/dashboard/content/product',\n '/dashboard/content/products',\n]);\n\n/** Settings sub-nav — shown in the sidebar (drill-in) when on a settings route. */\nexport type SettingsItem = {\n name: string;\n href: string;\n requires?: string;\n requiresAny?: string[];\n /** Extra routes that also mark this (collapsed) entry active, e.g. tabs. */\n match?: string[];\n /** Rail glyph. The settings rail had labels only, so it was the one nav in\n * the app you had to read rather than recognise — and it's the nav people\n * visit least often, which is exactly where recognition helps most. */\n icon: LucideIcon;\n};\n// The settings drill-in is context-aware. At the ACCOUNT level (account,\n// security, the Workspaces hub) it shows the account's own settings. When you\n// drill into a specific workspace's setting, it becomes THAT workspace's rail.\n// BUSINESS_GROUPS is now the only place these sections are listed — the\n// per-workspace hub page that used to mirror them was a screen whose only job\n// was linking to what this rail already shows, so it was deleted.\n// Login & security is its OWN rail item, not folded into Account.\n//\n// It was meant to be folded — the note here said so — but the fold was never\n// finished: the standalone page kept passkeys, connected sign-in and the device\n// list, gained no row, and was linked from nowhere, so the richest security\n// surface in the app could only be reached by typing the URL and nothing lit up\n// when you got there. Worse, the half that DID move was duplicated rather than\n// moved: two-factor and sessions rendered on both pages, so switching 2FA on in\n// one left the other showing it off until a reload.\n//\n// One page owns security now. Account keeps who you are and the door out.\nexport const ACCOUNT_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n label: 'Account',\n items: [\n { name: 'Account', href: '/dashboard/settings/profile', icon: UserRound },\n {\n name: 'Login & security',\n href: '/dashboard/settings/devices',\n icon: ShieldCheck,\n },\n { name: 'Businesses', href: '/dashboard/workspaces', icon: LayoutGrid },\n {\n name: 'Preferences',\n href: '/dashboard/settings/language',\n icon: SlidersHorizontal,\n },\n // Reachable from the mobile Me screen and from nowhere on desktop, so\n // the setting a merchant needs in order to USE the product was the one\n // they could not navigate to.\n {\n name: 'Accessibility',\n href: '/dashboard/settings/accessibility',\n icon: Accessibility,\n },\n {\n name: 'Data & privacy',\n href: '/dashboard/settings/data-privacy',\n icon: Database,\n },\n { name: 'Legal', href: '/dashboard/settings/legal', icon: Scale },\n ],\n },\n];\n\n/**\n * ONE ORDERED LIST, NO GROUP HEADINGS.\n *\n * These rows sat under `Setup` / `Selling` / `Records` and the headings were\n * doing no work. `Setup` held Billing and Notifications, which you visit for\n * years after setting up. `Records` held exactly one row. And a merchant\n * looking for Taxes does not first decide which of the three thirds it is in —\n * they scan for the word \"Taxes\".\n *\n * Shopify carries nineteen settings rows with no headings at all and it reads\n * fine, because every label is a noun the merchant already owns. What does the\n * work there is the ORDER, and it is not alphabetical: identity, then money,\n * then people, then selling, then places, then the storefront, then the log.\n * That is the order below.\n *\n * ON THE LABELS: an ampersand is fine when the second half says what else is\n * inside — `Team & permissions`, `Billing & plan`, `Login & security`,\n * `Data & privacy` each tell you something the first word alone does not. What\n * is not fine is a word from our side of the fence:\n *\n * `Operations` → **Locations**. An umbrella nobody says out loud; the row lands\n * on the locations list, hours and booking policy are tabs of a location, and\n * Locations is what both Shopify and Square call it.\n *\n * `Integrations` → **Connections**. Integration is a developer's word for what\n * a merchant experiences as connecting their WhatsApp or their Google Calendar.\n * Not `Apps` — that is Shopify's word for a marketplace we do not have, and the\n * label would promise a store that isn't there.\n *\n * `Tracking`, not Shopify's `Customer events`: a merchant running Meta ads\n * already calls it tracking, and \"customer events\" is a platform's internal\n * noun leaking into the nav.\n */\nexport const BUSINESS_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n // No heading. See the note above — the order is the structure.\n label: '',\n items: [\n // IDENTITY — who this workspace is.\n {\n name: 'General',\n href: '/dashboard/settings',\n match: ['/dashboard/settings/branding', '/dashboard/settings/regional'],\n icon: SlidersHorizontal,\n },\n // FEATURES — the switches no business type implies (Blog today).\n // Business types themselves are Explore's, see below.\n {\n name: 'Features',\n href: '/dashboard/settings/features',\n icon: ToggleLeft,\n },\n // NO BUSINESS TYPE ROW. It moved out of Settings entirely on\n // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.\n //\n // The page always DID the right thing — picking a model derives the\n // behaviour set that builds the sidebar — and nobody could find it. It\n // was three names deep in the wrong place: behind Workspace settings,\n // under a heading a merchant does not type when the thought is \"can\n // Box do events?\". A settings list is where you go to change\n // something you already have, and this is where you go to get something\n // you do not.\n //\n // `/dashboard/settings/features` is Features again (above): it no longer\n // holds the business model, only the switches no model implies.\n\n // MONEY, then the PEOPLE who can spend it.\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true'\n ? []\n : [\n {\n // Per business: each tenant has its own subscription.\n name: 'Billing & plan',\n href: '/dashboard/settings/billing',\n icon: CreditCard,\n },\n ]),\n {\n // What the plan is being spent on: fees, credits, delivery. Billing is\n // the bill; this is the meter behind it. Credits are bought from here.\n name: 'Usage',\n href: '/dashboard/settings/usage',\n match: ['/dashboard/settings/billing/credits'],\n icon: ChartColumn,\n },\n {\n name: 'Team & permissions',\n href: '/dashboard/settings/team',\n match: ['/dashboard/settings/roles', '/dashboard/settings/activity'],\n icon: Users,\n },\n\n // THE TERMS, and who gets told.\n { name: 'Policies', href: '/dashboard/settings/policies', icon: Scale },\n {\n // Configuration, not history. This sat beside Activity, which is the\n // log — so the one screen that decides whether a customer is told\n // anything was filed with the screen reporting what already happened.\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n {\n // Sample products, pages and pictures a business can start with, clear\n // in one go, or add again. Owner and admins act; others see the state.\n name: 'Demo content',\n href: '/dashboard/settings/demo-content',\n icon: Sparkles,\n },\n\n // Connectors moved to Customize (2026-10-03), beside Add more: both are\n // \"what is in this workspace\". `/dashboard/settings/integrations` now\n // redirects there; its sub-routes (calendar-sync, sales-channels,\n // tracking, a single connection) still live under Settings.\n ],\n },\n];\n\n// Settings routes that belong to the PERSON, not a business. Everything else\n// under /dashboard/settings is a business setting and gets the business rail.\nexport const ACCOUNT_ROUTE_PREFIXES = [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/language',\n '/dashboard/settings/data-privacy',\n '/dashboard/settings/legal',\n '/dashboard/settings/accessibility',\n];\n\nexport const parseSidebarHref = (href: string) => {\n const [path, query = ''] = href.split('?');\n const params = new URLSearchParams(query);\n return {\n path,\n intent: params.get('intent'),\n /**\n * Everything else the row narrows by — `group`, `tab`, `view`.\n *\n * This was `group` alone, added because a business with more than one group\n * of bookable things reaches the roster through rows that all lead to the\n * same PATH: without it every row lit and the tie-break picked whichever\n * href was longer. Every OTHER param is the same row on the same path, and\n * was still invisible — see `active-href.ts`.\n */\n params,\n };\n};\n\n/**\n * The archetypes an `?intent=` narrows to.\n *\n * A comma list, because a collapsed row stands for every archetype behind it —\n * a restaurant's Menu is `dine-in,food-orders`, which is neither one archetype\n * nor all of them. One key is still the common case; empty means not narrowed.\n */\nexport const intentKeySet = (intent: string | null | undefined): Set<string> =>\n new Set(\n (intent ?? '')\n .split(',')\n .map((key) => key.trim())\n .filter(Boolean),\n );\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/config.ts"],"sourcesContent":["/**\n * The sidebar's static shape: icon and permission maps, the nav types, the\n * route sets that decide what counts as active, and the settings drill-in\n * groups.\n *\n * Split out of sidebar.tsx, which had grown to 1,510 lines — three times the\n * house limit. None of this depends on component state, so it does not belong\n * inside the component that renders it.\n */\nimport {\n CreditCard,\n Users,\n Bell,\n Scale,\n UserRound,\n LayoutGrid,\n SlidersHorizontal,\n Database,\n ShieldCheck,\n Accessibility,\n Globe2,\n Palette,\n ChartColumn,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { INTENT_ICON_MAP } from '@withpotter/lib/intents/icon-map';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/** Map string icon names from IntentConfig to Lucide components */\nexport const ICON_MAP = INTENT_ICON_MAP;\n\n// The nav permission maps are data and live in ./nav-permissions, so the\n// coverage test can read them without pulling this file's icon imports in.\n// Re-exported here because every existing importer reaches for them via config.\nexport {\n INTENT_ITEM_PERMISSIONS,\n CORE_TOOL_PERMISSIONS,\n} from './nav-permissions';\n\nexport interface NavItem {\n id: string;\n name: string;\n href: string;\n icon: LucideIcon;\n requiredBehavior?: string[];\n /**\n * The row is only drawn on a box whose capability list carries this key. A\n * box that published no list is unchanged, so the main app keeps every row.\n * For a screen whose backend only some engines have: without it the row led\n * to \"Cannot GET /referrals/stats\" on a box that never had that module.\n */\n requiredCapability?: string;\n /**\n * The item is only rendered when the current team member has AT LEAST ONE of\n * these permissions (OWNER always passes). Undefined or empty means the item\n * is hidden from every non-owner — see INTENT_ITEM_PERMISSIONS for why the\n * default is closed rather than open.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Rows revealed under this one. An item with children renders with the same\n * disclosure the fixed spine hubs use, so an Operations surface can carry a\n * smaller surface (Appointments → Waitlist) without claiming a second\n * top-level row for it.\n */\n children?: NavItem[];\n /**\n * Names this row's own landing page once it carries children.\n *\n * A work row with children renders through `renderHub`, which moves the\n * row's own screen into the group as its first child — a row is a place or a\n * container, never both. Without this the child falls back to \"Overview\",\n * which the hub renderer says outright no new hub should rely on: every hub\n * could carry one, so the word tells the merchant nothing about which hub\n * they are in. `{label}` interpolates the row's post-rename label, so a\n * tenant calling them Bookings gets \"All bookings\" rather than a literal.\n */\n landingLabel?: string;\n}\n\nexport interface NavGroupSection {\n id: string;\n label: string;\n items: NavItem[];\n}\n\nexport interface NavGroup {\n id: string;\n label: string;\n icon: LucideIcon;\n items?: NavItem[];\n sections?: NavGroupSection[];\n /**\n * With no `items`, the group is a flat top-level destination (rendered like\n * the Home link — no chevron, no children).\n *\n * With `items`, this is the group's landing page, and it is rendered as the\n * FIRST CHILD rather than on the group's own row. A row is either a place or\n * a container, never both: a row that navigated *and* disclosed gave two\n * answers to one click, and the highlight landed on the parent while the\n * list that had just opened underneath it looked like a menu you had yet to\n * choose from. See `landingLabel` for what the child is called.\n */\n href?: string;\n /**\n * What this group's landing page is called once it sits among its own\n * children. Defaults to \"Overview\". Ignored unless the group has both an\n * `href` and `items`.\n *\n * `{label}` interpolates the group's OWN label, after renaming. A hub named\n * for a merchant-facing noun is renamed per tenant — Customers becomes\n * Guests, Clients, Members — and a literal \"All customers\" would have sat\n * under a hub reading \"Guests\", which is the exact mismatch `renameGroup`\n * exists to prevent. Write `All {label}`, not `All customers`.\n */\n landingLabel?: string;\n /**\n * Where the landing page sits among the children, counting from 0. Absent, it\n * leads, which every hub but Money wants. Money puts the wallet above its\n * Transactions page (John, 2026-10-11): the balance a business holds is the\n * first question, the log of what moved it the second.\n */\n landingAt?: number;\n /**\n * Only meaningful for flat (href) groups: gate visibility on AT LEAST ONE\n * of these permissions (OWNER always passes). Collapsible groups gate at\n * the item level instead.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Render as a disclosure hub even with a single child (normally a 1-child\n * group collapses to a flat link). Used by Inventory so Collections shows\n * inside it.\n */\n alwaysHub?: boolean;\n}\n\nexport const EXACT_MATCH_ROUTES = new Set([\n '/dashboard',\n '/dashboard/settings',\n]);\nexport const PLATFORM_CONTENT_TYPES = new Set(['product', 'products']);\nexport const PLATFORM_CONTENT_ROUTES = new Set([\n '/dashboard/content/product',\n '/dashboard/content/products',\n]);\n\n/** Settings sub-nav — shown in the sidebar (drill-in) when on a settings route. */\nexport type SettingsItem = {\n name: string;\n href: string;\n requires?: string;\n requiresAny?: string[];\n /** Extra routes that also mark this (collapsed) entry active, e.g. tabs. */\n match?: string[];\n /** Rail glyph. The settings rail had labels only, so it was the one nav in\n * the app you had to read rather than recognise — and it's the nav people\n * visit least often, which is exactly where recognition helps most. */\n icon: LucideIcon;\n};\n// The settings drill-in is context-aware. At the ACCOUNT level (account,\n// security, the Workspaces hub) it shows the account's own settings. When you\n// drill into a specific workspace's setting, it becomes THAT workspace's rail.\n// BUSINESS_GROUPS is now the only place these sections are listed — the\n// per-workspace hub page that used to mirror them was a screen whose only job\n// was linking to what this rail already shows, so it was deleted.\n// Login & security is its OWN rail item, not folded into Account.\n//\n// It was meant to be folded — the note here said so — but the fold was never\n// finished: the standalone page kept passkeys, connected sign-in and the device\n// list, gained no row, and was linked from nowhere, so the richest security\n// surface in the app could only be reached by typing the URL and nothing lit up\n// when you got there. Worse, the half that DID move was duplicated rather than\n// moved: two-factor and sessions rendered on both pages, so switching 2FA on in\n// one left the other showing it off until a reload.\n//\n// One page owns security now. Account keeps who you are and the door out.\nexport const ACCOUNT_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n label: 'Account',\n items: [\n { name: 'Account', href: '/dashboard/settings/profile', icon: UserRound },\n {\n name: 'Login & security',\n href: '/dashboard/settings/devices',\n icon: ShieldCheck,\n },\n { name: 'Businesses', href: '/dashboard/workspaces', icon: LayoutGrid },\n {\n name: 'Preferences',\n href: '/dashboard/settings/language',\n icon: SlidersHorizontal,\n },\n // Reachable from the mobile Me screen and from nowhere on desktop, so\n // the setting a merchant needs in order to USE the product was the one\n // they could not navigate to.\n {\n name: 'Accessibility',\n href: '/dashboard/settings/accessibility',\n icon: Accessibility,\n },\n {\n name: 'Data & privacy',\n href: '/dashboard/settings/data-privacy',\n icon: Database,\n },\n { name: 'Legal', href: '/dashboard/settings/legal', icon: Scale },\n ],\n },\n];\n\n/**\n * ONE ORDERED LIST, NO GROUP HEADINGS.\n *\n * These rows sat under `Setup` / `Selling` / `Records` and the headings were\n * doing no work. `Setup` held Billing and Notifications, which you visit for\n * years after setting up. `Records` held exactly one row. And a merchant\n * looking for Taxes does not first decide which of the three thirds it is in —\n * they scan for the word \"Taxes\".\n *\n * Shopify carries nineteen settings rows with no headings at all and it reads\n * fine, because every label is a noun the merchant already owns. What does the\n * work there is the ORDER, and it is not alphabetical: identity, then money,\n * then people, then selling, then places, then the storefront, then the log.\n * That is the order below.\n *\n * ON THE LABELS: an ampersand is fine when the second half says what else is\n * inside — `Team & permissions`, `Billing & plan`, `Login & security`,\n * `Data & privacy` each tell you something the first word alone does not. What\n * is not fine is a word from our side of the fence:\n *\n * `Operations` → **Locations**. An umbrella nobody says out loud; the row lands\n * on the locations list, hours and booking policy are tabs of a location, and\n * Locations is what both Shopify and Square call it.\n *\n * `Integrations` → **Connections**. Integration is a developer's word for what\n * a merchant experiences as connecting their WhatsApp or their Google Calendar.\n * Not `Apps` — that is Shopify's word for a marketplace we do not have, and the\n * label would promise a store that isn't there.\n *\n * `Tracking`, not Shopify's `Customer events`: a merchant running Meta ads\n * already calls it tracking, and \"customer events\" is a platform's internal\n * noun leaking into the nav.\n */\nexport const BUSINESS_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n // No heading. See the note above — the order is the structure.\n label: '',\n items: [\n // IDENTITY — who this workspace is.\n {\n name: 'General',\n href: '/dashboard/settings',\n match: ['/dashboard/settings/branding', '/dashboard/settings/regional'],\n icon: SlidersHorizontal,\n },\n // NO BUSINESS TYPE ROW. It moved out of Settings entirely on\n // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.\n //\n // The page always DID the right thing — picking a model derives the\n // behaviour set that builds the sidebar — and nobody could find it. It\n // was three names deep in the wrong place: behind Workspace settings,\n // under a heading a merchant does not type when the thought is \"can\n // Box do events?\". A settings list is where you go to change\n // something you already have, and this is where you go to get something\n // you do not.\n //\n // `/dashboard/settings/features` redirects, so nothing that linked to it\n // breaks.\n\n // MONEY, then the PEOPLE who can spend it.\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true'\n ? []\n : [\n {\n // Per business: each tenant has its own subscription.\n name: 'Billing & plan',\n href: '/dashboard/settings/billing',\n icon: CreditCard,\n },\n ]),\n {\n // What the plan is being spent on: fees, credits, delivery. Billing is\n // the bill; this is the meter behind it. Credits are bought from here.\n name: 'Usage',\n href: '/dashboard/settings/usage',\n match: ['/dashboard/settings/billing/credits'],\n icon: ChartColumn,\n },\n {\n name: 'Team & permissions',\n href: '/dashboard/settings/team',\n match: ['/dashboard/settings/roles', '/dashboard/settings/activity'],\n icon: Users,\n },\n\n // THE TERMS, and who gets told.\n { name: 'Policies', href: '/dashboard/settings/policies', icon: Scale },\n {\n // Configuration, not history. This sat beside Activity, which is the\n // log — so the one screen that decides whether a customer is told\n // anything was filed with the screen reporting what already happened.\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n\n // Connectors moved to Customize (2026-10-03), beside Add more: both are\n // \"what is in this workspace\". `/dashboard/settings/integrations` now\n // redirects there; its sub-routes (calendar-sync, sales-channels,\n // tracking, a single connection) still live under Settings.\n ],\n },\n];\n\n// Settings routes that belong to the PERSON, not a business. Everything else\n// under /dashboard/settings is a business setting and gets the business rail.\nexport const ACCOUNT_ROUTE_PREFIXES = [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/language',\n '/dashboard/settings/data-privacy',\n '/dashboard/settings/legal',\n '/dashboard/settings/accessibility',\n];\n\nexport const parseSidebarHref = (href: string) => {\n const [path, query = ''] = href.split('?');\n const params = new URLSearchParams(query);\n return {\n path,\n intent: params.get('intent'),\n /**\n * Everything else the row narrows by — `group`, `tab`, `view`.\n *\n * This was `group` alone, added because a business with more than one group\n * of bookable things reaches the roster through rows that all lead to the\n * same PATH: without it every row lit and the tie-break picked whichever\n * href was longer. Every OTHER param is the same row on the same path, and\n * was still invisible — see `active-href.ts`.\n */\n params,\n };\n};\n\n/**\n * The archetypes an `?intent=` narrows to.\n *\n * A comma list, because a collapsed row stands for every archetype behind it —\n * a restaurant's Menu is `dine-in,food-orders`, which is neither one archetype\n * nor all of them. One key is still the common case; empty means not narrowed.\n */\nexport const intentKeySet = (intent: string | null | undefined): Set<string> =>\n new Set(\n (intent ?? '')\n .split(',')\n .map((key) => key.trim())\n .filter(Boolean),\n );\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { MoreHorizontal } from "lucide-react";
2
2
  import { universalCoreModules } from "@withpotter/lib/use-platform-modules";
3
- const MORE_IDS = [];
3
+ const MORE_IDS = ["blog"];
4
4
  function buildMoreGroup(enabledBehaviors, edits = {}) {
5
5
  const items = [];
6
6
  const hidden = new Set(edits.hiddenModules ?? []);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/more-group.ts"],"sourcesContent":["import { MoreHorizontal } from 'lucide-react';\nimport { universalCoreModules } from '@withpotter/lib/use-platform-modules';\nimport type { NavGroup, NavItem } from './config';\n\n/**\n * What More holds, in order: the places a business uses now and then that have\n * no row of their own in the rail. Each keeps its behaviour gate from\n * `universalCoreModules`.\n *\n * Empty since Blog moved under Website (it was listed here AND as the last\n * child of Website, so a business with a blog got it twice). The hub is kept,\n * rather than deleted, because the rail's edit list, its row id and its place\n * at the bottom of the rail are all wired to it; with nothing to hold,\n * `buildMoreGroup` returns null and no row is drawn. A place that belongs to\n * no group goes here.\n */\nconst MORE_IDS: readonly string[] = [];\n\n/**\n * MORE — the last row of the general list, as an ordinary hub.\n *\n * It is a `NavGroup` like Marketing or Appointments and is drawn by the same\n * `renderHub`, so it opens in place with the same chevron, the same growing\n * list and the same highlight on the child you are on, and it shares their\n * one-open-at-a-time rule. There is no menu or pop-up of its own to keep in\n * step with them. `alwaysHub` keeps it a dropdown even when a business is left\n * with a single place to show.\n *\n * It honours the same hide, rename and order settings as the rest of the rail\n * (`hiddenModules`, `moduleLabels`, `moduleOrder`), so editing the sidebar\n * reaches it too.\n *\n * Null when none of its places apply, so the row is not drawn empty.\n */\nexport function buildMoreGroup(\n enabledBehaviors: readonly string[] | null | undefined,\n edits: {\n hiddenModules?: readonly string[];\n moduleLabels?: Record<string, string>;\n moduleOrder?: readonly string[];\n } = {},\n): NavGroup | null {\n const items: NavItem[] = [];\n const hidden = new Set(edits.hiddenModules ?? []);\n // The merchant's own order, where they gave one; otherwise ours.\n const order = [...MORE_IDS].sort((a, b) => {\n const at = edits.moduleOrder?.indexOf(a) ?? -1;\n const bt = edits.moduleOrder?.indexOf(b) ?? -1;\n return (at === -1 ? 999 : at) - (bt === -1 ? 999 : bt);\n });\n for (const id of order) {\n if (hidden.has(id)) continue;\n const m = universalCoreModules.find((x) => x.id === id);\n if (!m) continue;\n const need = m.requiredBehavior;\n if (need && enabledBehaviors) {\n const any = Array.isArray(need) ? need : [need];\n if (!any.some((b) => enabledBehaviors.includes(b))) continue;\n }\n items.push({\n id: m.id,\n name: edits.moduleLabels?.[m.id]?.trim() || m.name,\n href: m.href,\n icon: m.icon,\n });\n }\n if (items.length === 0) return null;\n return {\n id: 'more',\n label: 'More',\n icon: MoreHorizontal,\n items,\n alwaysHub: true,\n };\n}\n"],"mappings":"AAAA,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AAerC,MAAM,WAA8B,CAAC;AAkB9B,SAAS,eACd,kBACA,QAII,CAAC,GACY;AACjB,QAAM,QAAmB,CAAC;AAC1B,QAAM,SAAS,IAAI,IAAI,MAAM,iBAAiB,CAAC,CAAC;AAEhD,QAAM,QAAQ,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM;AACzC,UAAM,KAAK,MAAM,aAAa,QAAQ,CAAC,KAAK;AAC5C,UAAM,KAAK,MAAM,aAAa,QAAQ,CAAC,KAAK;AAC5C,YAAQ,OAAO,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM;AAAA,EACrD,CAAC;AACD,aAAW,MAAM,OAAO;AACtB,QAAI,OAAO,IAAI,EAAE,EAAG;AACpB,UAAM,IAAI,qBAAqB,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AACtD,QAAI,CAAC,EAAG;AACR,UAAM,OAAO,EAAE;AACf,QAAI,QAAQ,kBAAkB;AAC5B,YAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI;AAC9C,UAAI,CAAC,IAAI,KAAK,CAAC,MAAM,iBAAiB,SAAS,CAAC,CAAC,EAAG;AAAA,IACtD;AACA,UAAM,KAAK;AAAA,MACT,IAAI,EAAE;AAAA,MACN,MAAM,MAAM,eAAe,EAAE,EAAE,GAAG,KAAK,KAAK,EAAE;AAAA,MAC9C,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,EACb;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/more-group.ts"],"sourcesContent":["import { MoreHorizontal } from 'lucide-react';\nimport { universalCoreModules } from '@withpotter/lib/use-platform-modules';\nimport type { NavGroup, NavItem } from './config';\n\n/**\n * What More holds, in order: the places a business uses now and then that have\n * no row of their own in the rail. Each keeps its behaviour gate from\n * `universalCoreModules`, so a business that never publishes does not see Blog.\n */\nconst MORE_IDS = ['blog'] as const;\n\n/**\n * MORE — the last row of the general list, as an ordinary hub.\n *\n * It is a `NavGroup` like Marketing or Appointments and is drawn by the same\n * `renderHub`, so it opens in place with the same chevron, the same growing\n * list and the same highlight on the child you are on, and it shares their\n * one-open-at-a-time rule. There is no menu or pop-up of its own to keep in\n * step with them. `alwaysHub` keeps it a dropdown even when a business is left\n * with a single place to show.\n *\n * It honours the same hide, rename and order settings as the rest of the rail\n * (`hiddenModules`, `moduleLabels`, `moduleOrder`), so editing the sidebar\n * reaches it too.\n *\n * Null when none of its places apply, so the row is not drawn empty.\n */\nexport function buildMoreGroup(\n enabledBehaviors: readonly string[] | null | undefined,\n edits: {\n hiddenModules?: readonly string[];\n moduleLabels?: Record<string, string>;\n moduleOrder?: readonly string[];\n } = {},\n): NavGroup | null {\n const items: NavItem[] = [];\n const hidden = new Set(edits.hiddenModules ?? []);\n // The merchant's own order, where they gave one; otherwise ours.\n const order = [...MORE_IDS].sort((a, b) => {\n const at = edits.moduleOrder?.indexOf(a) ?? -1;\n const bt = edits.moduleOrder?.indexOf(b) ?? -1;\n return (at === -1 ? 999 : at) - (bt === -1 ? 999 : bt);\n });\n for (const id of order) {\n if (hidden.has(id)) continue;\n const m = universalCoreModules.find((x) => x.id === id);\n if (!m) continue;\n const need = m.requiredBehavior;\n if (need && enabledBehaviors) {\n const any = Array.isArray(need) ? need : [need];\n if (!any.some((b) => enabledBehaviors.includes(b))) continue;\n }\n items.push({\n id: m.id,\n name: edits.moduleLabels?.[m.id]?.trim() || m.name,\n href: m.href,\n icon: m.icon,\n });\n }\n if (items.length === 0) return null;\n return {\n id: 'more',\n label: 'More',\n icon: MoreHorizontal,\n items,\n alwaysHub: true,\n };\n}\n"],"mappings":"AAAA,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AAQrC,MAAM,WAAW,CAAC,MAAM;AAkBjB,SAAS,eACd,kBACA,QAII,CAAC,GACY;AACjB,QAAM,QAAmB,CAAC;AAC1B,QAAM,SAAS,IAAI,IAAI,MAAM,iBAAiB,CAAC,CAAC;AAEhD,QAAM,QAAQ,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM;AACzC,UAAM,KAAK,MAAM,aAAa,QAAQ,CAAC,KAAK;AAC5C,UAAM,KAAK,MAAM,aAAa,QAAQ,CAAC,KAAK;AAC5C,YAAQ,OAAO,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM;AAAA,EACrD,CAAC;AACD,aAAW,MAAM,OAAO;AACtB,QAAI,OAAO,IAAI,EAAE,EAAG;AACpB,UAAM,IAAI,qBAAqB,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AACtD,QAAI,CAAC,EAAG;AACR,UAAM,OAAO,EAAE;AACf,QAAI,QAAQ,kBAAkB;AAC5B,YAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI;AAC9C,UAAI,CAAC,IAAI,KAAK,CAAC,MAAM,iBAAiB,SAAS,CAAC,CAAC,EAAG;AAAA,IACtD;AACA,UAAM,KAAK;AAAA,MACT,IAAI,EAAE;AAAA,MACN,MAAM,MAAM,eAAe,EAAE,EAAE,GAAG,KAAK,KAAK,EAAE;AAAA,MAC9C,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,EACb;AACF;","names":[]}
@@ -146,7 +146,6 @@ function createNavRenderers({
146
146
  onMouseEnter: () => rows.forEach((c) => warm(c.href)),
147
147
  onFocus: () => rows.forEach((c) => warm(c.href)),
148
148
  "aria-expanded": open,
149
- "aria-controls": open ? `navsub-${group.id}` : void 0,
150
149
  children: [
151
150
  /* @__PURE__ */ jsx("span", { className: "ic", children: renderIcon(group.id, group.icon) }),
152
151
  /* @__PURE__ */ jsx("span", { className: "lbl", children: group.label }),
@@ -158,7 +157,6 @@ function createNavRenderers({
158
157
  /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: open && /* @__PURE__ */ jsxs(
159
158
  motion.ul,
160
159
  {
161
- id: `navsub-${group.id}`,
162
160
  className: "navsub",
163
161
  initial: { height: 0, opacity: 0, marginBottom: 0 },
164
162
  animate: { height: "auto", opacity: 1, marginBottom: SUB_GAP },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n aria-controls={open ? `navsub-${group.id}` : undefined}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n id={`navsub-${group.id}`}\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useId, useSyncExternalStore } from "react";
3
+ import { useCallback, useSyncExternalStore } from "react";
4
4
  import { Check, ChevronDown, Settings2 } from "lucide-react";
5
5
  import { EditableName } from "./rail-editable";
6
6
  const STORAGE_KEY = "pb_nav_sections_closed";
@@ -47,7 +47,6 @@ function NavSection({
47
47
  onDone
48
48
  }) {
49
49
  const key = foldKey ?? label;
50
- const listId = useId();
51
50
  const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);
52
51
  const items = children.filter(Boolean);
53
52
  const isClosed = !containsActive && closedMap[key] === true;
@@ -92,7 +91,7 @@ function NavSection({
92
91
  type: "button",
93
92
  onClick: onConfigure,
94
93
  className: "navheading-gear",
95
- "aria-label": `Customize sidebar, ${label} section`,
94
+ "aria-label": "Customize sidebar",
96
95
  title: "Customize sidebar",
97
96
  children: /* @__PURE__ */ jsx(Settings2, { "aria-hidden": "true" })
98
97
  }
@@ -104,7 +103,6 @@ function NavSection({
104
103
  onClick: toggle,
105
104
  className: "navheading navheading-toggle",
106
105
  "aria-expanded": !isClosed,
107
- "aria-controls": isClosed ? void 0 : listId,
108
106
  children: [
109
107
  /* @__PURE__ */ jsx("span", { children: label }),
110
108
  /* @__PURE__ */ jsx(
@@ -117,7 +115,7 @@ function NavSection({
117
115
  ]
118
116
  }
119
117
  ),
120
- !isClosed && /* @__PURE__ */ jsx("ul", { id: listId, className: "navgroup", children: items })
118
+ !isClosed && /* @__PURE__ */ jsx("ul", { className: "navgroup", children: items })
121
119
  ] });
122
120
  }
123
121
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useId, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const listId = useId();\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label={`Customize sidebar, ${label} section`}\n title=\"Customize sidebar\"\n >\n <Settings2 aria-hidden=\"true\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={toggle}\n className=\"navheading navheading-toggle\"\n aria-expanded={!isClosed}\n aria-controls={isClosed ? undefined : listId}\n >\n <span>{label}</span>\n <ChevronDown\n className={'navheading-chevron' + (isClosed ? ' closed' : '')}\n aria-hidden=\"true\"\n />\n </button>\n {!isClosed && (\n <ul id={listId} className=\"navgroup\">\n {items}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label=\"Customize sidebar\"\n title=\"Customize sidebar\"\n >\n <Settings2 aria-hidden=\"true\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={toggle}\n className=\"navheading navheading-toggle\"\n aria-expanded={!isClosed}\n >\n <span>{label}</span>\n <ChevronDown\n className={'navheading-chevron' + (isClosed ? ' closed' : '')}\n aria-hidden=\"true\"\n />\n </button>\n {!isClosed && <ul className=\"navgroup\">{items}</ul>}\n </div>\n );\n}\n"],"mappings":";AAiJQ,cAQA,YARA;AA/IR,SAAS,aAAa,4BAA4B;AAClD,SAAS,OAAO,aAAa,iBAAiB;AAC9C,SAAS,oBAAoB;AAE7B,MAAM,cAAc;AAWpB,IAAI,cAAuC,CAAC;AAC5C,IAAI,YAA2B;AAC/B,MAAM,YAAY,oBAAI,IAAgB;AAEtC,SAAS,aAAsC;AAC7C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,MAAM,aAAa,QAAQ,WAAW;AAG5C,MAAI,QAAQ,WAAW;AACrB,gBAAY;AACZ,QAAI;AACF,oBAAc,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,IACzC,QAAQ;AACN,oBAAc,CAAC;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAGA,MAAM,iBAAiB,MAAM;AAE7B,SAAS,UAAU,IAAgB;AACjC,YAAU,IAAI,EAAE;AAChB,SAAO,MAAM,UAAU,OAAO,EAAE;AAClC;AAEA,SAAS,iBAAiB,OAAe,QAAiB;AACxD,QAAM,OAAO,EAAE,GAAG,WAAW,GAAG,CAAC,KAAK,GAAG,OAAO;AAChD,MAAI;AACF,iBAAa,QAAQ,aAAa,KAAK,UAAU,IAAI,CAAC;AAAA,EACxD,QAAQ;AAAA,EAER;AACA,cAAY,KAAK,UAAU,IAAI;AAC/B,gBAAc;AACd,YAAU,QAAQ,CAAC,OAAO,GAAG,CAAC;AAChC;AAkBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GA6CG;AACD,QAAM,MAAM,WAAW;AACvB,QAAM,YAAY,qBAAqB,WAAW,YAAY,cAAc;AAC5E,QAAM,QAAQ,SAAS,OAAO,OAAO;AACrC,QAAM,WAAW,CAAC,kBAAkB,UAAU,GAAG,MAAM;AACvD,QAAM,SAAS;AAAA,IACb,MAAM,iBAAiB,KAAK,CAAC,QAAQ;AAAA,IACrC,CAAC,KAAK,QAAQ;AAAA,EAChB;AAEA,MAAI,CAAC,MAAM,OAAQ,QAAO;AAI1B,MAAK,MAAM,WAAW,KAAK,CAAC,aAAc,WAAW;AACnD,WACE,oBAAC,SAAI,WAAU,oBACb,8BAAC,QAAG,WAAU,YAAY,iBAAM,GAClC;AAAA,EAEJ;AAEA,MAAI,eAAe;AACjB,WACE,qBAAC,SAAI,WAAU,cACb;AAAA,2BAAC,SAAI,WAAU,+BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,OAAO,kBAAkB,KAAK;AAAA,YAC9B,UAAU;AAAA;AAAA,QACZ;AAAA,QACC,UACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YACV,cAAW;AAAA,YACX,OAAM;AAAA,YAEN,8BAAC,SAAM,eAAY,QAAO;AAAA;AAAA,QAC5B;AAAA,SAEJ;AAAA,MACA,oBAAC,QAAG,WAAU,YAAY,iBAAM;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,cACZ;AAAA,mBACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACV,cAAW;AAAA,QACX,OAAM;AAAA,QAEN,8BAAC,aAAU,eAAY,QAAO;AAAA;AAAA,IAChC;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACV,iBAAe,CAAC;AAAA,QAEhB;AAAA,8BAAC,UAAM,iBAAM;AAAA,UACb;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,wBAAwB,WAAW,YAAY;AAAA,cAC1D,eAAY;AAAA;AAAA,UACd;AAAA;AAAA;AAAA,IACF;AAAA,IACC,CAAC,YAAY,oBAAC,QAAG,WAAU,YAAY,iBAAM;AAAA,KAChD;AAEJ;","names":[]}
@@ -23,6 +23,4 @@ export declare function useSetupProgress(): {
23
23
  done: number;
24
24
  total: number;
25
25
  } | null;
26
- export declare function SetupProgress({ stacked }?: {
27
- stacked?: boolean;
28
- }): import("react").JSX.Element | null;
26
+ export declare function SetupProgress(): import("react").JSX.Element | null;
@@ -301,7 +301,7 @@ function useSetupProgress() {
301
301
  const setup = useSetupData();
302
302
  return setup ? { done: setup.done, total: setup.total } : null;
303
303
  }
304
- function SetupProgress({ stacked = false } = {}) {
304
+ function SetupProgress() {
305
305
  const [open, setOpen] = useState(false);
306
306
  const setup = useSetupData();
307
307
  if (!setup) return null;
@@ -314,7 +314,7 @@ function SetupProgress({ stacked = false } = {}) {
314
314
  onClick: () => setOpen(true),
315
315
  "aria-haspopup": "dialog",
316
316
  "aria-expanded": open,
317
- className: stacked ? "side-setup side-stack-card side-stack-back" : "side-setup",
317
+ className: "side-setup",
318
318
  "aria-label": `Setup ${pct}% complete. See what is left`,
319
319
  children: [
320
320
  /* @__PURE__ */ jsxs("span", { className: "side-setup-row", children: [