@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/sidebar/setup-progress.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useRef, useState } from 'react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { DeliveryStep, PayoutStep } from '@withpotter/components/dashboard/go-live-steps';\nimport { SetupContactDialog } from './setup-contact-dialog';\nimport { taxAndPolicySteps } from './setup-extra-steps';\nimport { SetupStepSections } from './setup-step-sections';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { onboardingApi } from '@withpotter/lib/api/onboarding';\nimport { tenantApi } from '@withpotter/lib/api/tenant';\nimport { WALLET_SETTLEMENT } from '@withpotter/lib/wallet-settlement';\nimport { isHostCheck } from '@withpotter/lib/go-live-gate';\nimport type {\n CoreReadinessKey,\n OnboardingProgress,\n OnboardingStep,\n ReadinessBlocker,\n StorefrontReadiness,\n} from '@withpotter/lib/api/types';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n} from '@withpotter/ui-kit/dialog';\nimport {\n SetupRing,\n isSettled,\n orderSteps,\n relabelStep,\n} from '@withpotter/components/dashboard/home/widgets/setup-steps';\n\n/** A row the checklist API does not send, in the shape its rows are drawn in. */\nfunction row(\n stepKey: string,\n label: string,\n done: boolean,\n link: string,\n): OnboardingStep {\n return {\n id: stepKey,\n stepKey,\n label,\n status: done ? 'completed' : 'pending',\n link,\n };\n}\n\n/** Checklist steps that already ask for what each go-live check asks for. */\nconst CHECKLIST_COVERS: Record<CoreReadinessKey, string[]> = {\n payable: ['payments'],\n sellable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n bookable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n schedulable: ['availability', 'schedule'],\n reachable: ['shipping'],\n // The two profile rows below ask for exactly this.\n contactable: ['profile_email', 'profile_phone'],\n};\n\n/** The checklist steps a check covers. A host check is covered by the step with its own key. */\nfunction coversOf(key: ReadinessBlocker['key']): string[] {\n return isHostCheck(key) ? [key] : CHECKLIST_COVERS[key];\n}\n\n/**\n * The address of a row that is finished in a dialog and never navigates.\n *\n * NOT a settings path. The shell opens the settings window on any click on a\n * link into `/dashboard/settings`, in the capture phase, before the row's own\n * handler can cancel it — so these rows opened their dialog AND the settings\n * window behind it. The press is always cancelled, so the address is only what\n * the row is drawn as.\n */\nconst INLINE_HREF = '/dashboard';\n\n/** Rows finished in a dialog over the checklist instead of on a page. */\nconst INLINE_STEPS: Record<string, 'email' | 'phone' | 'delivery' | 'payout'> =\n {\n profile_email: 'email',\n profile_phone: 'phone',\n live_reachable: 'delivery',\n shipping: 'delivery',\n // A bank account is finished here too, as it was in the old go-live gate.\n live_payable: 'payout',\n payments: 'payout',\n };\n\n/** Steps that put a logo and colours on the business, so a logo row is not\n * asked for twice. */\nconst BRANDING_KEYS = ['branding', 'customize_theme', 'add_logo'];\n\n/**\n * How far through setting up this business is, as a bar and a percentage in the\n * foot of the rail, on every screen. Pressing it opens the whole checklist.\n *\n * It reads the same checklist as the home's \"Finish setting up\" card (the same\n * query key and the same rows from `setup-steps`), and that card draws its ring\n * from the count made here, through `useSetupProgress`, so the two can never\n * show different numbers. In the window each step is a row that goes to where\n * it is done, and the window closes as you leave for it.\n *\n * It leaves when the checklist is done, as that card does, and draws nothing\n * while the answer is loading or when there is no checklist. The caller hides it\n * on the collapsed rail, which has no room for a sentence and a bar.\n */\nfunction useSetupData() {\n const { tenant, refreshTenant } = useAuth();\n const item = useEntityLabel('item');\n /** The step being finished in a dialog over this one, rather than on a page. */\n const progressQuery = useApiQuery<OnboardingProgress>(\n ['onboarding-progress'],\n () => onboardingApi.getProgress(tenant!.id),\n // A refusal is not asked again each time another reader of this mounts.\n { enabled: !!tenant?.id, retryOnMount: false },\n );\n const { data, refetch: refetchProgress } = progressQuery;\n\n const readinessQuery = useApiQuery<StorefrontReadiness>(\n ['dashboard-storefront-readiness'],\n (slug) => tenantApi.readiness(slug),\n { retryOnMount: false },\n );\n const { data: readinessRaw, refetch: refetchReadiness } = readinessQuery;\n\n // A wallet box has no payout account, so \"payable\" is neither a thing to\n // finish nor a thing that was finished. Dropped from both lists here, which\n // is what every row below is built from.\n // Memoised: effects further down watch `readiness`, and a fresh object every\n // render made them refetch on every render, which never ends.\n const readiness: StorefrontReadiness | undefined = useMemo(\n () =>\n readinessRaw && WALLET_SETTLEMENT\n ? {\n ...readinessRaw,\n blockers: (readinessRaw.blockers ?? []).filter(\n (b) => b.key !== 'payable',\n ),\n satisfied: (readinessRaw.satisfied ?? []).filter(\n (key) => key !== 'payable',\n ),\n }\n : readinessRaw,\n [readinessRaw],\n );\n\n // NO NUMBER UNTIL BOTH HAVE ANSWERED. The percentage is counted over three\n // lists: the profile rows, which are known at once, and the checklist and the\n // go-live checks, which each arrive on a request of their own. Counted before\n // those landed it was the profile rows alone, so the rail read 33%, then 43%\n // a moment later, for a business that had changed nothing.\n //\n // \"Answered\" includes a first refusal. A backend that does not serve one of\n // these routes will not serve it on the retry either, and holding the bar\n // back for the whole retry window would hide it for no better number.\n //\n // ONCE ANSWERED, ALWAYS ANSWERED. A refused query asks again when something\n // new starts reading it, and for that moment it is pending with no failures\n // counted, exactly as it was the first time. Deciding from that alone hid the\n // card, which unmounted its readers, which showed it again when the request\n // was refused, which mounted them, which asked again: thousands of requests\n // a minute against a backend that had already said no. `isFetched` does not\n // go back, and the ref keeps the card up through any later refetch.\n const firstAnswerPending =\n (!progressQuery.isFetched && progressQuery.failureCount === 0) ||\n (!readinessQuery.isFetched && readinessQuery.failureCount === 0);\n const answered = useRef(false);\n if (!firstAnswerPending) answered.current = true;\n const asking = !answered.current;\n\n // Two sources say what is finished: the onboarding checklist and the\n // readiness check that gates going live. They are read at different times and\n // can disagree, and the list should never show something as missing that the\n // check has already passed. A step the readiness check covers is done when\n // that check is. `contactable` is left out: it passes on an email OR a phone,\n // so it cannot say both are done.\n const satisfiedCovers = new Set(\n (readiness?.satisfied ?? [])\n .filter((key) => key !== 'contactable')\n .flatMap((key) => coversOf(key)),\n );\n const checklist = orderSteps(data?.checklist?.steps ?? []).map((s) => {\n const step = relabelStep(s, item.singular);\n return satisfiedCovers.has(step.stepKey) && !isSettled(step)\n ? { ...step, status: 'completed' as const }\n : step;\n });\n const checklistKeys = new Set(checklist.map((s) => s.stepKey));\n\n // The business's own details: how a customer reaches it, and how it looks.\n const profile = [\n row(\n 'profile_email',\n 'Add a contact email',\n !!tenant?.contactEmail,\n INLINE_HREF,\n ),\n row(\n 'profile_phone',\n 'Add a phone number',\n !!tenant?.contactPhone,\n INLINE_HREF,\n ),\n ...(BRANDING_KEYS.some((k) => checklistKeys.has(k))\n ? []\n : [\n row(\n 'profile_logo',\n 'Add your logo',\n !!tenant?.businessLogo,\n '/dashboard/settings#branding',\n ),\n ]),\n ...taxAndPolicySteps(tenant?.settings),\n ];\n\n // What still stops it opening, that the checklist above does not already say.\n const goLiveLabel: Record<CoreReadinessKey, string> = {\n sellable: `Add your first ${item.singular.toLowerCase()}`,\n bookable: `Add a ${item.singular.toLowerCase()} people can book`,\n schedulable: 'Set the hours you take bookings',\n payable: 'Add a bank account',\n reachable: 'Add a delivery area',\n contactable: 'Add an email or phone number',\n };\n const goLiveLink: Record<CoreReadinessKey, string> = {\n sellable: '/dashboard/catalog/new',\n bookable: '/dashboard/catalog/new',\n schedulable: '/dashboard/bookings?view=availability',\n payable: '/dashboard/payments/payouts',\n reachable: '/dashboard/business/shipping',\n contactable: '/dashboard/settings#workspace-email',\n };\n const live = (readiness?.blockers ?? [])\n .filter(\n (b) =>\n b.key !== 'contactable' &&\n !coversOf(b.key).some((k) => checklistKeys.has(k)),\n )\n .map((b) =>\n isHostCheck(b.key)\n ? row(`live_${b.key}`, b.title, false, b.dashboardPath)\n : row(`live_${b.key}`, goLiveLabel[b.key], false, goLiveLink[b.key]),\n );\n\n /**\n * THE ONES THAT STAND BETWEEN THE BUSINESS AND GOING LIVE, marked with a red\n * asterisk and listed first. A row is required when the server still reports\n * the go-live check it answers: a delivery area, a payout account, something\n * to sell, a way to be reached. Everything else is worth doing and does not\n * stop the site opening.\n */\n // What is already satisfied is a done row, not a missing one, so the list\n // shows what the business has finished as well as what is left.\n // A host check that passed has no label of its own here; its checklist step\n // (same key) already shows it done, through `satisfiedCovers` above.\n const liveDone = (readiness?.satisfied ?? [])\n .filter(\n (key): key is CoreReadinessKey =>\n key !== 'contactable' &&\n !isHostCheck(key) &&\n !CHECKLIST_COVERS[key].some((k) => checklistKeys.has(k)),\n )\n .map((key) => row(`live_${key}`, goLiveLabel[key], true, goLiveLink[key]));\n\n const outstanding = readiness?.blockers ?? [];\n const requiredKeys = new Set<string>([\n ...live.map((st) => st.stepKey),\n ...liveDone.map((st) => st.stepKey),\n ...outstanding.flatMap((b) => coversOf(b.key)),\n ...(outstanding.some((b) => b.key === 'contactable')\n ? ['profile_email', 'profile_phone']\n : []),\n ]);\n const isRequired = (st: OnboardingStep) => requiredKeys.has(st.stepKey);\n const rest = [...profile, ...checklist, ...live, ...liveDone];\n\n const sections = [\n { title: 'Needed to go live', steps: rest.filter(isRequired) },\n {\n title: 'Your business',\n steps: profile.filter((st) => !isRequired(st)),\n },\n {\n title: 'Setting up',\n steps: checklist.filter((st) => !isRequired(st)),\n },\n ].filter((sec) => sec.steps.length > 0);\n const all = sections.flatMap((sec) => sec.steps);\n if (asking || all.length === 0) return null;\n\n const done = all.filter(isSettled).length;\n const total = all.length;\n const next = all.find((st) => !isSettled(st));\n if (done >= total) return null;\n const pct = Math.round((done / total) * 100);\n\n return {\n tenant,\n refreshTenant,\n sections,\n all,\n done,\n total,\n next,\n pct,\n isRequired,\n refetchProgress,\n refetchReadiness,\n };\n}\n\n/**\n * The Complete your setup list, as a dialog anything can open: the sidebar's\n * progress pill, and Publish when something still stands in the way.\n *\n * Publish used to open its own, older dialog with a list of its own. Two lists\n * for one question drift, and the second never showed what had already been\n * done. There is one now. `mode=\"go-live\"` only changes the line under the\n * title, to how many of the needed items are left.\n */\nexport function SetupDialog({\n open,\n onOpenChange,\n mode = 'setup',\n onFixed,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n mode?: 'setup' | 'go-live';\n /** Re-run whatever opened this once something was set up from in here. */\n onFixed?: () => void | Promise<void>;\n}) {\n const [inline, setInline] = useState<\n 'email' | 'phone' | 'delivery' | 'payout' | null\n >(null);\n const setup = useSetupData();\n if (!setup) return null;\n const {\n tenant,\n refreshTenant,\n sections,\n done,\n total,\n next,\n pct,\n isRequired,\n all,\n refetchProgress,\n refetchReadiness,\n } = setup;\n const needed = all.filter((st) => isRequired(st) && !isSettled(st)).length;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-label=\"Complete your setup\"\n className=\"max-h-[85vh] max-w-[440px] overflow-y-auto p-6\"\n >\n <div className=\"flex items-center gap-4\">\n {/* The ring draws the count; this says it. */}\n <span role=\"img\" aria-label={`${done} of ${total} steps done`}>\n <SetupRing done={done} total={total} showCount={false} />\n </span>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-semibold text-[var(--text-primary)]\">\n Complete your setup\n </DialogTitle>\n <DialogDescription className=\"mt-0.5 text-md text-[var(--text-tertiary)]\">\n {mode === 'go-live' && needed > 0\n ? `${needed} left to go live`\n : `${done} of ${total} done · ${pct}%`}\n {next ? ` · Next: ${next.label}` : ''}\n </DialogDescription>\n </div>\n </div>\n\n <SetupStepSections\n sections={sections}\n isRequired={isRequired}\n onActivate={(step) => {\n // These are finished here, in a dialog over the list, so the\n // list is still there when they are done.\n const inlineKey = INLINE_STEPS[step.stepKey];\n if (inlineKey) {\n setInline(inlineKey);\n return true;\n }\n onOpenChange(false);\n }}\n />\n </DialogContent>\n </Dialog>\n\n <SetupContactDialog\n field={inline === 'email' || inline === 'phone' ? inline : null}\n tenantSlug={tenant?.slug ?? ''}\n onClose={() => setInline(null)}\n onSaved={async () => {\n await refreshTenant();\n await refetchReadiness();\n await onFixed?.();\n }}\n />\n <DeliveryStep\n open={inline === 'delivery'}\n onOpenChange={(v) => setInline(v ? 'delivery' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n <PayoutStep\n open={inline === 'payout'}\n onOpenChange={(v) => setInline(v ? 'payout' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n </>\n );\n}\n\n/**\n * The one count of how far through setup this business is, for a surface that\n * draws it without the list. Null while it is being worked out and once\n * everything is done.\n */\nexport function useSetupProgress(): { done: number; total: number } | null {\n const setup = useSetupData();\n return setup ? { done: setup.done, total: setup.total } : null;\n}\n\nexport function SetupProgress({ stacked = false }: { stacked?: boolean } = {}) {\n const [open, setOpen] = useState(false);\n const setup = useSetupData();\n if (!setup) return null;\n const { pct } = setup;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={stacked ? 'side-setup side-stack-card side-stack-back' : 'side-setup'}\n aria-label={`Setup ${pct}% complete. See what is left`}\n >\n <span className=\"side-setup-row\">\n <span>Complete your setup</span>\n <b>{pct}%</b>\n </span>\n <span\n className=\"side-setup-track\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n >\n <span className=\"side-setup-fill\" style={{ width: `${pct}%` }} />\n </span>\n </button>\n\n <SetupDialog open={open} onOpenChange={setOpen} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/setup-progress.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useRef, useState } from 'react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { DeliveryStep, PayoutStep } from '@withpotter/components/dashboard/go-live-steps';\nimport { SetupContactDialog } from './setup-contact-dialog';\nimport { taxAndPolicySteps } from './setup-extra-steps';\nimport { SetupStepSections } from './setup-step-sections';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { onboardingApi } from '@withpotter/lib/api/onboarding';\nimport { tenantApi } from '@withpotter/lib/api/tenant';\nimport { WALLET_SETTLEMENT } from '@withpotter/lib/wallet-settlement';\nimport { isHostCheck } from '@withpotter/lib/go-live-gate';\nimport type {\n CoreReadinessKey,\n OnboardingProgress,\n OnboardingStep,\n ReadinessBlocker,\n StorefrontReadiness,\n} from '@withpotter/lib/api/types';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n} from '@withpotter/ui-kit/dialog';\nimport {\n SetupRing,\n isSettled,\n orderSteps,\n relabelStep,\n} from '@withpotter/components/dashboard/home/widgets/setup-steps';\n\n/** A row the checklist API does not send, in the shape its rows are drawn in. */\nfunction row(\n stepKey: string,\n label: string,\n done: boolean,\n link: string,\n): OnboardingStep {\n return {\n id: stepKey,\n stepKey,\n label,\n status: done ? 'completed' : 'pending',\n link,\n };\n}\n\n/** Checklist steps that already ask for what each go-live check asks for. */\nconst CHECKLIST_COVERS: Record<CoreReadinessKey, string[]> = {\n payable: ['payments'],\n sellable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n bookable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n schedulable: ['availability', 'schedule'],\n reachable: ['shipping'],\n // The two profile rows below ask for exactly this.\n contactable: ['profile_email', 'profile_phone'],\n};\n\n/** The checklist steps a check covers. A host check is covered by the step with its own key. */\nfunction coversOf(key: ReadinessBlocker['key']): string[] {\n return isHostCheck(key) ? [key] : CHECKLIST_COVERS[key];\n}\n\n/**\n * The address of a row that is finished in a dialog and never navigates.\n *\n * NOT a settings path. The shell opens the settings window on any click on a\n * link into `/dashboard/settings`, in the capture phase, before the row's own\n * handler can cancel it — so these rows opened their dialog AND the settings\n * window behind it. The press is always cancelled, so the address is only what\n * the row is drawn as.\n */\nconst INLINE_HREF = '/dashboard';\n\n/** Rows finished in a dialog over the checklist instead of on a page. */\nconst INLINE_STEPS: Record<string, 'email' | 'phone' | 'delivery' | 'payout'> =\n {\n profile_email: 'email',\n profile_phone: 'phone',\n live_reachable: 'delivery',\n shipping: 'delivery',\n // A bank account is finished here too, as it was in the old go-live gate.\n live_payable: 'payout',\n payments: 'payout',\n };\n\n/** Steps that put a logo and colours on the business, so a logo row is not\n * asked for twice. */\nconst BRANDING_KEYS = ['branding', 'customize_theme', 'add_logo'];\n\n/**\n * How far through setting up this business is, as a bar and a percentage in the\n * foot of the rail, on every screen. Pressing it opens the whole checklist.\n *\n * It reads the same checklist as the home's \"Finish setting up\" card (the same\n * query key and the same rows from `setup-steps`), and that card draws its ring\n * from the count made here, through `useSetupProgress`, so the two can never\n * show different numbers. In the window each step is a row that goes to where\n * it is done, and the window closes as you leave for it.\n *\n * It leaves when the checklist is done, as that card does, and draws nothing\n * while the answer is loading or when there is no checklist. The caller hides it\n * on the collapsed rail, which has no room for a sentence and a bar.\n */\nfunction useSetupData() {\n const { tenant, refreshTenant } = useAuth();\n const item = useEntityLabel('item');\n /** The step being finished in a dialog over this one, rather than on a page. */\n const progressQuery = useApiQuery<OnboardingProgress>(\n ['onboarding-progress'],\n () => onboardingApi.getProgress(tenant!.id),\n // A refusal is not asked again each time another reader of this mounts.\n { enabled: !!tenant?.id, retryOnMount: false },\n );\n const { data, refetch: refetchProgress } = progressQuery;\n\n const readinessQuery = useApiQuery<StorefrontReadiness>(\n ['dashboard-storefront-readiness'],\n (slug) => tenantApi.readiness(slug),\n { retryOnMount: false },\n );\n const { data: readinessRaw, refetch: refetchReadiness } = readinessQuery;\n\n // A wallet box has no payout account, so \"payable\" is neither a thing to\n // finish nor a thing that was finished. Dropped from both lists here, which\n // is what every row below is built from.\n // Memoised: effects further down watch `readiness`, and a fresh object every\n // render made them refetch on every render, which never ends.\n const readiness: StorefrontReadiness | undefined = useMemo(\n () =>\n readinessRaw && WALLET_SETTLEMENT\n ? {\n ...readinessRaw,\n blockers: (readinessRaw.blockers ?? []).filter(\n (b) => b.key !== 'payable',\n ),\n satisfied: (readinessRaw.satisfied ?? []).filter(\n (key) => key !== 'payable',\n ),\n }\n : readinessRaw,\n [readinessRaw],\n );\n\n // NO NUMBER UNTIL BOTH HAVE ANSWERED. The percentage is counted over three\n // lists: the profile rows, which are known at once, and the checklist and the\n // go-live checks, which each arrive on a request of their own. Counted before\n // those landed it was the profile rows alone, so the rail read 33%, then 43%\n // a moment later, for a business that had changed nothing.\n //\n // \"Answered\" includes a first refusal. A backend that does not serve one of\n // these routes will not serve it on the retry either, and holding the bar\n // back for the whole retry window would hide it for no better number.\n //\n // ONCE ANSWERED, ALWAYS ANSWERED. A refused query asks again when something\n // new starts reading it, and for that moment it is pending with no failures\n // counted, exactly as it was the first time. Deciding from that alone hid the\n // card, which unmounted its readers, which showed it again when the request\n // was refused, which mounted them, which asked again: thousands of requests\n // a minute against a backend that had already said no. `isFetched` does not\n // go back, and the ref keeps the card up through any later refetch.\n const firstAnswerPending =\n (!progressQuery.isFetched && progressQuery.failureCount === 0) ||\n (!readinessQuery.isFetched && readinessQuery.failureCount === 0);\n const answered = useRef(false);\n if (!firstAnswerPending) answered.current = true;\n const asking = !answered.current;\n\n // Two sources say what is finished: the onboarding checklist and the\n // readiness check that gates going live. They are read at different times and\n // can disagree, and the list should never show something as missing that the\n // check has already passed. A step the readiness check covers is done when\n // that check is. `contactable` is left out: it passes on an email OR a phone,\n // so it cannot say both are done.\n const satisfiedCovers = new Set(\n (readiness?.satisfied ?? [])\n .filter((key) => key !== 'contactable')\n .flatMap((key) => coversOf(key)),\n );\n const checklist = orderSteps(data?.checklist?.steps ?? []).map((s) => {\n const step = relabelStep(s, item.singular);\n return satisfiedCovers.has(step.stepKey) && !isSettled(step)\n ? { ...step, status: 'completed' as const }\n : step;\n });\n const checklistKeys = new Set(checklist.map((s) => s.stepKey));\n\n // The business's own details: how a customer reaches it, and how it looks.\n const profile = [\n row(\n 'profile_email',\n 'Add a contact email',\n !!tenant?.contactEmail,\n INLINE_HREF,\n ),\n row(\n 'profile_phone',\n 'Add a phone number',\n !!tenant?.contactPhone,\n INLINE_HREF,\n ),\n ...(BRANDING_KEYS.some((k) => checklistKeys.has(k))\n ? []\n : [\n row(\n 'profile_logo',\n 'Add your logo',\n !!tenant?.businessLogo,\n '/dashboard/settings#branding',\n ),\n ]),\n ...taxAndPolicySteps(tenant?.settings),\n ];\n\n // What still stops it opening, that the checklist above does not already say.\n const goLiveLabel: Record<CoreReadinessKey, string> = {\n sellable: `Add your first ${item.singular.toLowerCase()}`,\n bookable: `Add a ${item.singular.toLowerCase()} people can book`,\n schedulable: 'Set the hours you take bookings',\n payable: 'Add a bank account',\n reachable: 'Add a delivery area',\n contactable: 'Add an email or phone number',\n };\n const goLiveLink: Record<CoreReadinessKey, string> = {\n sellable: '/dashboard/catalog/new',\n bookable: '/dashboard/catalog/new',\n schedulable: '/dashboard/bookings?view=availability',\n payable: '/dashboard/payments/payouts',\n reachable: '/dashboard/business/shipping',\n contactable: '/dashboard/settings#workspace-email',\n };\n const live = (readiness?.blockers ?? [])\n .filter(\n (b) =>\n b.key !== 'contactable' &&\n !coversOf(b.key).some((k) => checklistKeys.has(k)),\n )\n .map((b) =>\n isHostCheck(b.key)\n ? row(`live_${b.key}`, b.title, false, b.dashboardPath)\n : row(`live_${b.key}`, goLiveLabel[b.key], false, goLiveLink[b.key]),\n );\n\n /**\n * THE ONES THAT STAND BETWEEN THE BUSINESS AND GOING LIVE, marked with a red\n * asterisk and listed first. A row is required when the server still reports\n * the go-live check it answers: a delivery area, a payout account, something\n * to sell, a way to be reached. Everything else is worth doing and does not\n * stop the site opening.\n */\n // What is already satisfied is a done row, not a missing one, so the list\n // shows what the business has finished as well as what is left.\n // A host check that passed has no label of its own here; its checklist step\n // (same key) already shows it done, through `satisfiedCovers` above.\n const liveDone = (readiness?.satisfied ?? [])\n .filter(\n (key): key is CoreReadinessKey =>\n key !== 'contactable' &&\n !isHostCheck(key) &&\n !CHECKLIST_COVERS[key].some((k) => checklistKeys.has(k)),\n )\n .map((key) => row(`live_${key}`, goLiveLabel[key], true, goLiveLink[key]));\n\n const outstanding = readiness?.blockers ?? [];\n const requiredKeys = new Set<string>([\n ...live.map((st) => st.stepKey),\n ...liveDone.map((st) => st.stepKey),\n ...outstanding.flatMap((b) => coversOf(b.key)),\n ...(outstanding.some((b) => b.key === 'contactable')\n ? ['profile_email', 'profile_phone']\n : []),\n ]);\n const isRequired = (st: OnboardingStep) => requiredKeys.has(st.stepKey);\n const rest = [...profile, ...checklist, ...live, ...liveDone];\n\n const sections = [\n { title: 'Needed to go live', steps: rest.filter(isRequired) },\n {\n title: 'Your business',\n steps: profile.filter((st) => !isRequired(st)),\n },\n {\n title: 'Setting up',\n steps: checklist.filter((st) => !isRequired(st)),\n },\n ].filter((sec) => sec.steps.length > 0);\n const all = sections.flatMap((sec) => sec.steps);\n if (asking || all.length === 0) return null;\n\n const done = all.filter(isSettled).length;\n const total = all.length;\n const next = all.find((st) => !isSettled(st));\n if (done >= total) return null;\n const pct = Math.round((done / total) * 100);\n\n return {\n tenant,\n refreshTenant,\n sections,\n all,\n done,\n total,\n next,\n pct,\n isRequired,\n refetchProgress,\n refetchReadiness,\n };\n}\n\n/**\n * The Complete your setup list, as a dialog anything can open: the sidebar's\n * progress pill, and Publish when something still stands in the way.\n *\n * Publish used to open its own, older dialog with a list of its own. Two lists\n * for one question drift, and the second never showed what had already been\n * done. There is one now. `mode=\"go-live\"` only changes the line under the\n * title, to how many of the needed items are left.\n */\nexport function SetupDialog({\n open,\n onOpenChange,\n mode = 'setup',\n onFixed,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n mode?: 'setup' | 'go-live';\n /** Re-run whatever opened this once something was set up from in here. */\n onFixed?: () => void | Promise<void>;\n}) {\n const [inline, setInline] = useState<\n 'email' | 'phone' | 'delivery' | 'payout' | null\n >(null);\n const setup = useSetupData();\n if (!setup) return null;\n const {\n tenant,\n refreshTenant,\n sections,\n done,\n total,\n next,\n pct,\n isRequired,\n all,\n refetchProgress,\n refetchReadiness,\n } = setup;\n const needed = all.filter((st) => isRequired(st) && !isSettled(st)).length;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-label=\"Complete your setup\"\n className=\"max-h-[85vh] max-w-[440px] overflow-y-auto p-6\"\n >\n <div className=\"flex items-center gap-4\">\n {/* The ring draws the count; this says it. */}\n <span role=\"img\" aria-label={`${done} of ${total} steps done`}>\n <SetupRing done={done} total={total} showCount={false} />\n </span>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-semibold text-[var(--text-primary)]\">\n Complete your setup\n </DialogTitle>\n <DialogDescription className=\"mt-0.5 text-md text-[var(--text-tertiary)]\">\n {mode === 'go-live' && needed > 0\n ? `${needed} left to go live`\n : `${done} of ${total} done · ${pct}%`}\n {next ? ` · Next: ${next.label}` : ''}\n </DialogDescription>\n </div>\n </div>\n\n <SetupStepSections\n sections={sections}\n isRequired={isRequired}\n onActivate={(step) => {\n // These are finished here, in a dialog over the list, so the\n // list is still there when they are done.\n const inlineKey = INLINE_STEPS[step.stepKey];\n if (inlineKey) {\n setInline(inlineKey);\n return true;\n }\n onOpenChange(false);\n }}\n />\n </DialogContent>\n </Dialog>\n\n <SetupContactDialog\n field={inline === 'email' || inline === 'phone' ? inline : null}\n tenantSlug={tenant?.slug ?? ''}\n onClose={() => setInline(null)}\n onSaved={async () => {\n await refreshTenant();\n await refetchReadiness();\n await onFixed?.();\n }}\n />\n <DeliveryStep\n open={inline === 'delivery'}\n onOpenChange={(v) => setInline(v ? 'delivery' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n <PayoutStep\n open={inline === 'payout'}\n onOpenChange={(v) => setInline(v ? 'payout' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n </>\n );\n}\n\n/**\n * The one count of how far through setup this business is, for a surface that\n * draws it without the list. Null while it is being worked out and once\n * everything is done.\n */\nexport function useSetupProgress(): { done: number; total: number } | null {\n const setup = useSetupData();\n return setup ? { done: setup.done, total: setup.total } : null;\n}\n\nexport function SetupProgress() {\n const [open, setOpen] = useState(false);\n const setup = useSetupData();\n if (!setup) return null;\n const { pct } = setup;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className=\"side-setup\"\n aria-label={`Setup ${pct}% complete. See what is left`}\n >\n <span className=\"side-setup-row\">\n <span>Complete your setup</span>\n <b>{pct}%</b>\n </span>\n <span\n className=\"side-setup-track\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n >\n <span className=\"side-setup-fill\" style={{ width: `${pct}%` }} />\n </span>\n </button>\n\n <SetupDialog open={open} onOpenChange={setOpen} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -18,7 +18,6 @@ import { buildMoneyGroup } from "./spine-money";
18
18
  import { buildCustomersGroup } from "./spine-customers";
19
19
  import { shellTakesMoney } from "./takes-money";
20
20
  import { groupItems } from "./flatten-work-groups";
21
- import { makeFlatGroup } from "./nav-gates";
22
21
  function buildSpineGroups({
23
22
  filterItems,
24
23
  coreItem,
@@ -275,12 +274,7 @@ function buildSpineGroups({
275
274
  ...group(analyticsGroup),
276
275
  ...group(marketingGroup),
277
276
  ...group(operationsGroup),
278
- // Website is gated on its own permission, not on its children: every row
279
- // inside needs a manage grant, so a member who may only LOOK at the site
280
- // (canViewWebsite) has none left, and `group` dropped the hub for them. The
281
- // hub's landing page ("Your site") is theirs to open, and a childless hub
282
- // draws as a plain link.
283
- ...makeFlatGroup(can)(websiteGroup),
277
+ ...group(websiteGroup),
284
278
  ...group(settingsGroup)
285
279
  ];
286
280
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/spine-groups.ts"],"sourcesContent":["/**\n * The fixed spine of the sidebar: Catalog, Customers, Payments, Analytics,\n * Marketing, Website and Settings.\n *\n * Split out of build-nav-groups.ts to keep both files under the 500-line house\n * limit. It reads nothing from component state — the four helpers it needs are\n * passed in, so the rules about what a member may see live in ONE place rather\n * than being re-derived here.\n */\nimport {\n Route,\n ChartNoAxesCombined,\n Users,\n Building2,\n ToggleLeft,\n Mail,\n Bell,\n Percent,\n Scale,\n Settings,\n} from 'lucide-react';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { resolveAnalyticsViews } from '@withpotter/lib/intents/analytics-labels';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { type NavItem, type NavGroup } from './config';\nimport { buildWebsiteGroup } from './spine-website';\nimport { buildOperationsGroup } from './spine-operations';\nimport { buildMarketingGroup } from './spine-marketing';\nimport { buildMoneyGroup } from './spine-money';\nimport { buildCustomersGroup } from './spine-customers';\nimport { shellTakesMoney } from './takes-money';\nimport { groupItems } from './flatten-work-groups';\nimport { makeFlatGroup } from './nav-gates';\n\nexport interface SpineDeps {\n filterItems: (items: NavItem[]) => NavItem[];\n coreItem: (id: string, labelOverride?: string) => NavItem | null;\n group: (g: NavGroup) => NavGroup[];\n /**\n * Rows several archetypes share — one Catalog, one Orders — as their own\n * links above the archetype groups. They belong to no single archetype, so\n * they are not filed under one.\n */\n sharedGroups: NavGroup[];\n intentGroups: NavGroup[];\n catalogToolsGroup: NavGroup[];\n can: ReturnType<typeof usePermissions>['can'];\n enabledBehaviors: ReturnType<typeof resolveEnabledBehaviors>;\n intentConfig: ReturnType<typeof useIntentConfig>;\n template: ReturnType<typeof useCurrentTemplate>['template'];\n}\n\nexport function buildSpineGroups({\n filterItems,\n coreItem,\n group,\n sharedGroups,\n intentGroups,\n catalogToolsGroup,\n can,\n enabledBehaviors,\n intentConfig,\n}: SpineDeps): NavGroup[] {\n // ── Spine groups ──────────────────────────────────────────────────────\n // Home is rendered separately (a fixed link). Everything below is the\n // adaptive money-model work surfaces, then the fixed spine.\n\n const takesMoney = shellTakesMoney(enabledBehaviors);\n // Customers is the relationship hub: the directory (header link), segments,\n // loyalty. The Inbox is not in it any more; it is a row of its own under\n // Home (`inbox-row.ts`).\n const owns = (id: string) =>\n intentGroups.some((group) =>\n group.items?.some(\n (item) =>\n item.id === id || item.children?.some((child) => child.id === id),\n ),\n );\n\n // Same question for invoices, and for the same reason: `shares` collapses one\n // archetype against another, never an archetype row against a spine child.\n const intentOwnsInvoices = owns('invoices');\n\n // The archetype OR the behaviour, because they come apart: a shop can switch\n // `subscribable` on in Settings → Business model without picking the\n // membership archetype, and it has subscribers all the same.\n const runsMembershipArchetype = intentConfig.all.some(\n (config) => config.key === 'subscriptions',\n );\n const runsSubscriptions =\n runsMembershipArchetype || enabledBehaviors.includes('subscribable');\n\n const customersGroup = buildCustomersGroup({\n filterItems,\n takesMoney,\n runsSubscriptions,\n runsMembershipArchetype,\n });\n\n const paymentsGroup = buildMoneyGroup({\n filterItems,\n intentOwnsInvoices,\n canSeeFinance: can('canViewFinance') || can('canManageFinance'),\n });\n\n // Every active archetype, through the same resolver the Analytics provider\n // uses — the row and the view have to agree about which views exist, and they\n // cannot if each works it out for itself.\n const analyticsViews = resolveAnalyticsViews(intentConfig.all);\n\n // Analytics measures; it never owns records. Every child here answers a\n // question the merchant asks about the business, and links out to the surface\n // that owns the thing being measured.\n //\n // The children are deliberately NOT the nouns of the other spine rows.\n // `analytics-retention` carries its own id because an item id does not\n // identify a screen: reusing `customers` here would have collided with the\n // Customers hub in three separate places (icon lookup by id, destination\n // sharing, and label renaming by module).\n //\n // Traffic IS here, last. It lived under Website beside the storefront it\n // measures, and was moved in 2026-10-06: it is a report, and reports are\n // found under Analytics. The screen is still the Website's\n // (`/dashboard/business/traffic`); only the way in moved. It sits outside\n // the archetype filter below because that filter is about which BUSINESS\n // views a model has, and every site has visitors.\n const analyticsGroup: NavGroup = {\n id: 'analytics',\n label: 'Analytics',\n icon: ChartNoAxesCombined,\n href: '/dashboard/analytics',\n /**\n * The landing view IS the sales view — there is no \"Overview\". An overview\n * of a two-page hub is a summary of what is already on the next screen, and\n * the word names nothing: every hub has one, so it distinguishes nothing\n * about this one.\n *\n * `Sales summary`, not `Sales`, and that is a correction. Money already\n * carries a row called Sales — the cross-rail register of everything sold,\n * where each row opens the record to act on it — so a multi-model merchant\n * saw the word twice in one sidebar, pointing at two different screens. It\n * went unseen for the usual reason: the `sales` row is gated on `isMulti`,\n * so the collision only exists on the businesses that run more than one\n * model, which is 16 of the walkthrough set.\n *\n * The hub had already made this exact call one row down: Retention is named\n * Retention BECAUSE \"Customers\" was taken by the spine row above it. Same\n * rule, applied to the row that was missed.\n *\n * The report takes the qualifier rather than the register, because the\n * register is the place and this is the measurement of it — and because\n * `VIEW_TITLES` in the hub's layout has titled this document \"Sales\n * summary\" all along. The row and the title agreeing costs nothing here;\n * they were allowed to differ, not required to.\n */\n landingLabel: 'Sales overview',\n requiredPermission: ['canViewAnalytics'],\n // One child plus the landing page. Without this, `renderHub` collapses a\n // lone child into a flat link and Retention would never appear.\n alwaysHub: true,\n items: filterItems(\n (\n [\n // No `Orders` row. The order report is an INSTRUMENT, not a subject:\n // it is one object — a measure crossed with a dimension over a\n // window — and Square's seven report rows are that same object with\n // different `Group metric by` presets, not seven different things.\n // Listing it beside Sales and Retention put a tool in a list of\n // topics, which is why the hub read as three unrelated pages.\n //\n // It is reached by drilling instead: every panel that has a cut\n // carries a link into `/dashboard/analytics/orders` with its own\n // dimension already set, the way Shopify's overview cards each open\n // the report that owns them. The route still exists and is still\n // addressable — it just is not a place you navigate to cold, because\n // arriving with no question is what made it a wall of controls.\n {\n /**\n * Attribution, which was a fourth row under Marketing until\n * 2026-08-27.\n *\n * It always answered to `canViewAnalytics` — the old row said so in\n * its own comment — which is the tell that it was filed under the\n * wrong parent: a row whose permission belongs to another group is\n * a report that has been shelved with the tools.\n *\n * The campaign-level answer never lived on it anyway. What a send\n * earned is the Attributable sales column on the campaigns table,\n * and `useCampaignRevenue` gets it from the SAME endpoint this page\n * calls. What is left — Sources, Channels, Promotions — is not\n * campaign-scoped at all: it is where the money came from, which is\n * the same kind of question as Sales and Retention beside it.\n *\n * Checked live in Square (2026-08-27): they have no attribution\n * page. It is a column called \"Attributable Sales\" on the campaign\n * row and nothing more. We keep the report because Sources and\n * Channels answer what a column cannot — an order that carried no\n * campaign still came from somewhere — but not as a marketing row.\n */\n id: 'analytics-attribution',\n name: 'Acquisition',\n href: '/dashboard/analytics/acquisition',\n icon: Route,\n requiredPermission: ['canViewAnalytics'],\n },\n {\n // Not `Customers`. That word is already a spine row directly above\n // this one, and two rows reading the same noun at different depths\n // is the confusion this hub exists to avoid. It is also not the\n // right word: the Customers hub owns the PEOPLE — a list you open\n // and edit — while this measures their BEHAVIOUR, which is a thing\n // you cannot click into one of.\n //\n // Renaming would not have fixed it either: a hotel that relabels\n // the hub to \"Guests\" would simply have read \"Guests\" twice.\n id: 'analytics-retention',\n name: 'Retention',\n href: '/dashboard/analytics/retention',\n icon: Users,\n requiredPermission: ['canViewAnalytics'],\n },\n ] as NavItem[]\n ).filter(\n // An archetype that names its analytics views gets only those. The\n // row and the page have to agree: the view decides its own sections\n // from the same declaration, so a nav child pointing at a view that\n // renders nothing would be a link to an empty screen.\n (item) =>\n !analyticsViews ||\n analyticsViews.includes(item.id.replace('analytics-', '')),\n ),\n ),\n };\n\n const marketingGroup = buildMarketingGroup({\n filterItems,\n enabledBehaviors,\n });\n\n const websiteGroup = buildWebsiteGroup({ filterItems, coreItem });\n const staffTaken = [...sharedGroups, ...intentGroups].some((g) =>\n groupItems(g).some(\n (item) =>\n item.name === 'Staff' ||\n item.children?.some((child) => child.name === 'Staff'),\n ),\n );\n const operationsGroup = buildOperationsGroup(filterItems, { staffTaken });\n\n // Settings is scoped to THIS business (the current tenant). Account-level\n // concerns — your personal profile/login, and switching between the\n // businesses in your account — are a separate tier that belongs in the\n // account menu, not here; they're intentionally left out of this group.\n const settingsGroup: NavGroup = {\n id: 'settings',\n label: 'Settings',\n icon: Settings,\n sections: [\n {\n id: 'settings-business',\n // Same three buckets, same names as the settings rail (BUSINESS_GROUPS)\n // and the workspace hub. They used to read Workspace / Operations /\n // Compliance here and Setup / Selling / Records there, for the same\n // destinations — so whichever a merchant learned first was wrong in\n // the other place.\n label: 'Setup',\n items: filterItems([\n {\n id: 'business-profile',\n // 'General' everywhere. The rail and the hub already call it\n // that; only this list said 'Business profile'.\n name: 'General',\n href: '/dashboard/settings',\n icon: Building2,\n requiredPermission: ['canManageBusinessProfile'],\n },\n {\n id: 'features',\n name: 'Features',\n href: '/dashboard/settings/features',\n icon: ToggleLeft,\n requiredPermission: ['canManageSettings'],\n },\n // Configuration, not history — it was filed under Records beside\n // Activity, which is the log group. No requiredPermission: every\n // active member has a personal notification section on this page,\n // and the tenant-wide config is gated inside it.\n {\n id: 'notifications',\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n // Connectors lives in Customize now. See config.ts.\n ]),\n },\n {\n id: 'settings-operations',\n label: 'Selling',\n items: filterItems([\n {\n id: 'taxes',\n name: 'Taxes',\n href: '/dashboard/business/checkout?tab=taxes',\n icon: Percent,\n requiredPermission: ['canManageSettings'],\n },\n {\n id: 'policies',\n name: 'Policies',\n href: '/dashboard/settings/policies',\n icon: Scale,\n requiredPermission: ['canManageSettings'],\n },\n // Plan & billing is per-WORKSPACE. This comment used to claim the\n // opposite while `config.ts` two files away claimed the truth, and\n // the schema settles it: `billing_subscriptions.tenantId` is unique,\n // and every credit table is `tenant_*` keyed on `tenantId`. It is\n // listed in the business-scoped Setup group, not the account menu.\n ]),\n },\n {\n id: 'settings-compliance',\n label: 'Records',\n items: filterItems([\n {\n id: 'email',\n name: 'Email',\n href: '/dashboard/settings/email',\n icon: Mail,\n requiredPermission: ['canManageCommunications'],\n },\n ]),\n },\n ].filter((section) => section.items.length > 0),\n };\n\n // No Today row anywhere any more. It and the Appointments calendar drew the\n // same day over the same records and only one of them could act on it, so\n // they are one surface with a view switch — /dashboard/bookings?view=today —\n // and /dashboard/today redirects into it.\n //\n // No Sales row here either. It is a child of Money now: it stood beside\n // Orders as a peer and the two words do not carry their difference, so the\n // read moved to the hub whose question it answers. See `spine-money.ts`.\n\n return [\n ...sharedGroups,\n ...intentGroups,\n ...catalogToolsGroup,\n // Hubs, not flat links: Customers expands to Reviews, Money to its three\n // logs, Analytics to its Sales and Customers views. `group` keeps them\n // collapsible. Analytics was the last flat row in the spine — its views\n // existed only as an in-page tab strip, which advertised them to nobody\n // who hadn't already opened the page.\n ...group(customersGroup),\n // Only for a shell that charges somebody. See `takes-money.ts`.\n ...(takesMoney ? group(paymentsGroup) : []),\n /**\n * STILL NO TOP-LEVEL HARDWARE ROW. John, 2026-09-03, moved it to Settings\n * because hardware is infrastructure a business sets up and then operates,\n * not a place it visits; and on 2026-10-05 moved it again, to Money, where\n * the machines that take the money sit beside the money they take. It is a\n * row inside that group (see `spine-money.ts`), not a rail row of its own.\n *\n * `buildHardwareGroup` is unchanged and still exported, so the machines, the\n * requests and their permissions keep one definition.\n */\n ...group(analyticsGroup),\n ...group(marketingGroup),\n ...group(operationsGroup),\n // Website is gated on its own permission, not on its children: every row\n // inside needs a manage grant, so a member who may only LOOK at the site\n // (canViewWebsite) has none left, and `group` dropped the hub for them. The\n // hub's landing page (\"Your site\") is theirs to open, and a childless hub\n // draws as a plain link.\n ...makeFlatGroup(can)(websiteGroup),\n ...group(settingsGroup),\n ];\n}\n"],"mappings":"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","names":["group"]}
1
+ {"version":3,"sources":["../../src/sidebar/spine-groups.ts"],"sourcesContent":["/**\n * The fixed spine of the sidebar: Catalog, Customers, Payments, Analytics,\n * Marketing, Website and Settings.\n *\n * Split out of build-nav-groups.ts to keep both files under the 500-line house\n * limit. It reads nothing from component state — the four helpers it needs are\n * passed in, so the rules about what a member may see live in ONE place rather\n * than being re-derived here.\n */\nimport {\n Route,\n ChartNoAxesCombined,\n Users,\n Building2,\n ToggleLeft,\n Mail,\n Bell,\n Percent,\n Scale,\n Settings,\n} from 'lucide-react';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { resolveAnalyticsViews } from '@withpotter/lib/intents/analytics-labels';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { type NavItem, type NavGroup } from './config';\nimport { buildWebsiteGroup } from './spine-website';\nimport { buildOperationsGroup } from './spine-operations';\nimport { buildMarketingGroup } from './spine-marketing';\nimport { buildMoneyGroup } from './spine-money';\nimport { buildCustomersGroup } from './spine-customers';\nimport { shellTakesMoney } from './takes-money';\nimport { groupItems } from './flatten-work-groups';\n\nexport interface SpineDeps {\n filterItems: (items: NavItem[]) => NavItem[];\n coreItem: (id: string, labelOverride?: string) => NavItem | null;\n group: (g: NavGroup) => NavGroup[];\n /**\n * Rows several archetypes share — one Catalog, one Orders — as their own\n * links above the archetype groups. They belong to no single archetype, so\n * they are not filed under one.\n */\n sharedGroups: NavGroup[];\n intentGroups: NavGroup[];\n catalogToolsGroup: NavGroup[];\n can: ReturnType<typeof usePermissions>['can'];\n enabledBehaviors: ReturnType<typeof resolveEnabledBehaviors>;\n intentConfig: ReturnType<typeof useIntentConfig>;\n template: ReturnType<typeof useCurrentTemplate>['template'];\n}\n\nexport function buildSpineGroups({\n filterItems,\n coreItem,\n group,\n sharedGroups,\n intentGroups,\n catalogToolsGroup,\n can,\n enabledBehaviors,\n intentConfig,\n}: SpineDeps): NavGroup[] {\n // ── Spine groups ──────────────────────────────────────────────────────\n // Home is rendered separately (a fixed link). Everything below is the\n // adaptive money-model work surfaces, then the fixed spine.\n\n const takesMoney = shellTakesMoney(enabledBehaviors);\n // Customers is the relationship hub: the directory (header link), segments,\n // loyalty. The Inbox is not in it any more; it is a row of its own under\n // Home (`inbox-row.ts`).\n const owns = (id: string) =>\n intentGroups.some((group) =>\n group.items?.some(\n (item) =>\n item.id === id || item.children?.some((child) => child.id === id),\n ),\n );\n\n // Same question for invoices, and for the same reason: `shares` collapses one\n // archetype against another, never an archetype row against a spine child.\n const intentOwnsInvoices = owns('invoices');\n\n // The archetype OR the behaviour, because they come apart: a shop can switch\n // `subscribable` on in Settings → Business model without picking the\n // membership archetype, and it has subscribers all the same.\n const runsMembershipArchetype = intentConfig.all.some(\n (config) => config.key === 'subscriptions',\n );\n const runsSubscriptions =\n runsMembershipArchetype || enabledBehaviors.includes('subscribable');\n\n const customersGroup = buildCustomersGroup({\n filterItems,\n takesMoney,\n runsSubscriptions,\n runsMembershipArchetype,\n });\n\n const paymentsGroup = buildMoneyGroup({\n filterItems,\n intentOwnsInvoices,\n canSeeFinance: can('canViewFinance') || can('canManageFinance'),\n });\n\n // Every active archetype, through the same resolver the Analytics provider\n // uses — the row and the view have to agree about which views exist, and they\n // cannot if each works it out for itself.\n const analyticsViews = resolveAnalyticsViews(intentConfig.all);\n\n // Analytics measures; it never owns records. Every child here answers a\n // question the merchant asks about the business, and links out to the surface\n // that owns the thing being measured.\n //\n // The children are deliberately NOT the nouns of the other spine rows.\n // `analytics-retention` carries its own id because an item id does not\n // identify a screen: reusing `customers` here would have collided with the\n // Customers hub in three separate places (icon lookup by id, destination\n // sharing, and label renaming by module).\n //\n // Traffic IS here, last. It lived under Website beside the storefront it\n // measures, and was moved in 2026-10-06: it is a report, and reports are\n // found under Analytics. The screen is still the Website's\n // (`/dashboard/business/traffic`); only the way in moved. It sits outside\n // the archetype filter below because that filter is about which BUSINESS\n // views a model has, and every site has visitors.\n const analyticsGroup: NavGroup = {\n id: 'analytics',\n label: 'Analytics',\n icon: ChartNoAxesCombined,\n href: '/dashboard/analytics',\n /**\n * The landing view IS the sales view — there is no \"Overview\". An overview\n * of a two-page hub is a summary of what is already on the next screen, and\n * the word names nothing: every hub has one, so it distinguishes nothing\n * about this one.\n *\n * `Sales summary`, not `Sales`, and that is a correction. Money already\n * carries a row called Sales — the cross-rail register of everything sold,\n * where each row opens the record to act on it — so a multi-model merchant\n * saw the word twice in one sidebar, pointing at two different screens. It\n * went unseen for the usual reason: the `sales` row is gated on `isMulti`,\n * so the collision only exists on the businesses that run more than one\n * model, which is 16 of the walkthrough set.\n *\n * The hub had already made this exact call one row down: Retention is named\n * Retention BECAUSE \"Customers\" was taken by the spine row above it. Same\n * rule, applied to the row that was missed.\n *\n * The report takes the qualifier rather than the register, because the\n * register is the place and this is the measurement of it — and because\n * `VIEW_TITLES` in the hub's layout has titled this document \"Sales\n * summary\" all along. The row and the title agreeing costs nothing here;\n * they were allowed to differ, not required to.\n */\n landingLabel: 'Sales overview',\n requiredPermission: ['canViewAnalytics'],\n // One child plus the landing page. Without this, `renderHub` collapses a\n // lone child into a flat link and Retention would never appear.\n alwaysHub: true,\n items: filterItems(\n (\n [\n // No `Orders` row. The order report is an INSTRUMENT, not a subject:\n // it is one object — a measure crossed with a dimension over a\n // window — and Square's seven report rows are that same object with\n // different `Group metric by` presets, not seven different things.\n // Listing it beside Sales and Retention put a tool in a list of\n // topics, which is why the hub read as three unrelated pages.\n //\n // It is reached by drilling instead: every panel that has a cut\n // carries a link into `/dashboard/analytics/orders` with its own\n // dimension already set, the way Shopify's overview cards each open\n // the report that owns them. The route still exists and is still\n // addressable — it just is not a place you navigate to cold, because\n // arriving with no question is what made it a wall of controls.\n {\n /**\n * Attribution, which was a fourth row under Marketing until\n * 2026-08-27.\n *\n * It always answered to `canViewAnalytics` — the old row said so in\n * its own comment — which is the tell that it was filed under the\n * wrong parent: a row whose permission belongs to another group is\n * a report that has been shelved with the tools.\n *\n * The campaign-level answer never lived on it anyway. What a send\n * earned is the Attributable sales column on the campaigns table,\n * and `useCampaignRevenue` gets it from the SAME endpoint this page\n * calls. What is left — Sources, Channels, Promotions — is not\n * campaign-scoped at all: it is where the money came from, which is\n * the same kind of question as Sales and Retention beside it.\n *\n * Checked live in Square (2026-08-27): they have no attribution\n * page. It is a column called \"Attributable Sales\" on the campaign\n * row and nothing more. We keep the report because Sources and\n * Channels answer what a column cannot — an order that carried no\n * campaign still came from somewhere — but not as a marketing row.\n */\n id: 'analytics-attribution',\n name: 'Acquisition',\n href: '/dashboard/analytics/acquisition',\n icon: Route,\n requiredPermission: ['canViewAnalytics'],\n },\n {\n // Not `Customers`. That word is already a spine row directly above\n // this one, and two rows reading the same noun at different depths\n // is the confusion this hub exists to avoid. It is also not the\n // right word: the Customers hub owns the PEOPLE — a list you open\n // and edit — while this measures their BEHAVIOUR, which is a thing\n // you cannot click into one of.\n //\n // Renaming would not have fixed it either: a hotel that relabels\n // the hub to \"Guests\" would simply have read \"Guests\" twice.\n id: 'analytics-retention',\n name: 'Retention',\n href: '/dashboard/analytics/retention',\n icon: Users,\n requiredPermission: ['canViewAnalytics'],\n },\n ] as NavItem[]\n ).filter(\n // An archetype that names its analytics views gets only those. The\n // row and the page have to agree: the view decides its own sections\n // from the same declaration, so a nav child pointing at a view that\n // renders nothing would be a link to an empty screen.\n (item) =>\n !analyticsViews ||\n analyticsViews.includes(item.id.replace('analytics-', '')),\n ),\n ),\n };\n\n const marketingGroup = buildMarketingGroup({\n filterItems,\n enabledBehaviors,\n });\n\n const websiteGroup = buildWebsiteGroup({ filterItems, coreItem });\n const staffTaken = [...sharedGroups, ...intentGroups].some((g) =>\n groupItems(g).some(\n (item) =>\n item.name === 'Staff' ||\n item.children?.some((child) => child.name === 'Staff'),\n ),\n );\n const operationsGroup = buildOperationsGroup(filterItems, { staffTaken });\n\n // Settings is scoped to THIS business (the current tenant). Account-level\n // concerns — your personal profile/login, and switching between the\n // businesses in your account — are a separate tier that belongs in the\n // account menu, not here; they're intentionally left out of this group.\n const settingsGroup: NavGroup = {\n id: 'settings',\n label: 'Settings',\n icon: Settings,\n sections: [\n {\n id: 'settings-business',\n // Same three buckets, same names as the settings rail (BUSINESS_GROUPS)\n // and the workspace hub. They used to read Workspace / Operations /\n // Compliance here and Setup / Selling / Records there, for the same\n // destinations — so whichever a merchant learned first was wrong in\n // the other place.\n label: 'Setup',\n items: filterItems([\n {\n id: 'business-profile',\n // 'General' everywhere. The rail and the hub already call it\n // that; only this list said 'Business profile'.\n name: 'General',\n href: '/dashboard/settings',\n icon: Building2,\n requiredPermission: ['canManageBusinessProfile'],\n },\n {\n id: 'features',\n name: 'Features',\n href: '/dashboard/settings/features',\n icon: ToggleLeft,\n requiredPermission: ['canManageSettings'],\n },\n // Configuration, not history — it was filed under Records beside\n // Activity, which is the log group. No requiredPermission: every\n // active member has a personal notification section on this page,\n // and the tenant-wide config is gated inside it.\n {\n id: 'notifications',\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n // Connectors lives in Customize now. See config.ts.\n ]),\n },\n {\n id: 'settings-operations',\n label: 'Selling',\n items: filterItems([\n {\n id: 'taxes',\n name: 'Taxes',\n href: '/dashboard/business/checkout?tab=taxes',\n icon: Percent,\n requiredPermission: ['canManageSettings'],\n },\n {\n id: 'policies',\n name: 'Policies',\n href: '/dashboard/settings/policies',\n icon: Scale,\n requiredPermission: ['canManageSettings'],\n },\n // Plan & billing is per-WORKSPACE. This comment used to claim the\n // opposite while `config.ts` two files away claimed the truth, and\n // the schema settles it: `billing_subscriptions.tenantId` is unique,\n // and every credit table is `tenant_*` keyed on `tenantId`. It is\n // listed in the business-scoped Setup group, not the account menu.\n ]),\n },\n {\n id: 'settings-compliance',\n label: 'Records',\n items: filterItems([\n {\n id: 'email',\n name: 'Email',\n href: '/dashboard/settings/email',\n icon: Mail,\n requiredPermission: ['canManageCommunications'],\n },\n ]),\n },\n ].filter((section) => section.items.length > 0),\n };\n\n // No Today row anywhere any more. It and the Appointments calendar drew the\n // same day over the same records and only one of them could act on it, so\n // they are one surface with a view switch — /dashboard/bookings?view=today —\n // and /dashboard/today redirects into it.\n //\n // No Sales row here either. It is a child of Money now: it stood beside\n // Orders as a peer and the two words do not carry their difference, so the\n // read moved to the hub whose question it answers. See `spine-money.ts`.\n\n return [\n ...sharedGroups,\n ...intentGroups,\n ...catalogToolsGroup,\n // Hubs, not flat links: Customers expands to Reviews, Money to its three\n // logs, Analytics to its Sales and Customers views. `group` keeps them\n // collapsible. Analytics was the last flat row in the spine — its views\n // existed only as an in-page tab strip, which advertised them to nobody\n // who hadn't already opened the page.\n ...group(customersGroup),\n // Only for a shell that charges somebody. See `takes-money.ts`.\n ...(takesMoney ? group(paymentsGroup) : []),\n /**\n * STILL NO TOP-LEVEL HARDWARE ROW. John, 2026-09-03, moved it to Settings\n * because hardware is infrastructure a business sets up and then operates,\n * not a place it visits; and on 2026-10-05 moved it again, to Money, where\n * the machines that take the money sit beside the money they take. It is a\n * row inside that group (see `spine-money.ts`), not a rail row of its own.\n *\n * `buildHardwareGroup` is unchanged and still exported, so the machines, the\n * requests and their permissions keep one definition.\n */\n ...group(analyticsGroup),\n ...group(marketingGroup),\n ...group(operationsGroup),\n ...group(websiteGroup),\n ...group(settingsGroup),\n ];\n}\n"],"mappings":"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","names":["group"]}
package/dist/sidebar.js CHANGED
@@ -82,18 +82,18 @@ function Sidebar({
82
82
  );
83
83
  const salesChannel = isSalesChannel(tenantSettings?.salesChannel) ? tenantSettings.salesChannel : null;
84
84
  const inPersonOnly = salesChannel === "in_person";
85
- const enabledBehaviors = useMemo(
86
- () => resolveEnabledBehaviors(tenantSettings, template),
87
- [tenantSettings, template]
88
- );
89
85
  const { coreNavigation } = usePlatformModules({
90
86
  template,
91
87
  dashboardState,
92
- enabledBehaviors,
88
+ enabledBehaviors: tenantSettings?.enabledBehaviors,
93
89
  hiddenModules: tenantSettings?.hiddenModules,
94
90
  moduleLabels,
95
91
  moduleOrder: tenantSettings?.moduleOrder
96
92
  });
93
+ const enabledBehaviors = useMemo(
94
+ () => resolveEnabledBehaviors(tenantSettings, template),
95
+ [tenantSettings, template]
96
+ );
97
97
  const intentConfig = useIntentConfig();
98
98
  const { can, isOwner, loading: permissionsLoading } = usePermissions();
99
99
  const capabilities = useCapabilities();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { PanelLeftClose, PanelLeftOpen } from 'lucide-react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { ProductSwitcher } from './shell/product-switcher';\nimport { Logo } from '@withpotter/ui-kit/logo';\nimport './shell/shell.css';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCapabilities } from '@withpotter/lib/capability-context';\nimport { effectiveModuleLabels } from '@withpotter/lib/effective-module-labels';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { SidebarFooter } from '@withpotter/components/shell/sidebar-footer';\nimport { usePlatformModules } from '@withpotter/lib/use-platform-modules';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { isSalesChannel } from '@withpotter/hooks/use-sales-channel';\nimport {\n ACCOUNT_ROUTE_PREFIXES,\n intentKeySet,\n type NavGroup,\n type NavItem,\n} from './sidebar/config';\nimport {\n isActive as isActiveFor,\n resolveActiveHref,\n type SidebarLocation,\n} from './sidebar/active-href';\nimport { buildInboxRow } from './sidebar/inbox-row';\nimport { RailBody } from './sidebar/rail-body';\nimport { EditableRailBody } from './sidebar/rail-editor';\nimport { usableHidden, type SectionId } from './sidebar/rail-edits';\nimport { buildNavGroups } from './sidebar/build-nav-groups';\nimport { buildRailRows } from './sidebar/rail-rows';\nimport {\n buildOrdersPaymentsHub,\n findOrdersRow,\n} from './sidebar/orders-payments-hub';\nimport { splitWorkRows } from './sidebar/work-rows';\nimport { buildMoreGroup } from './sidebar/more-group';\nimport { buildInventoryHub } from './sidebar/inventory-hub';\nimport { SidebarAsk } from './shell/sidebar-ask';\nimport { railContextFor } from './sidebar/route-context';\nimport { useNewNavRows } from './sidebar/use-new-nav-rows';\nimport { useResourceRows } from './sidebar/use-resource-rows';\nimport {\n FIXED_GROUP_IDS,\n flattenWorkGroups,\n groupItems,\n} from './sidebar/flatten-work-groups';\nimport { SettingsNav } from './sidebar/settings-nav';\nimport { createNavRenderers } from './sidebar/nav-renderers';\nimport { useNavBadges } from './sidebar/use-nav-badges';\nimport { useOpenHub } from './sidebar/use-open-hub';\n\nexport function Sidebar({\n collapsed = false,\n onToggleCollapse,\n}: {\n collapsed?: boolean;\n /** Collapses the rail; the button for it sits beside Search in the head. */\n onToggleCollapse?: () => void;\n}) {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const router = useRouter();\n /** \"Customize sidebar\" is on: the rail itself is editable in place. */\n const [editing, setEditing] = useState<SectionId | null>(null);\n const startEditing = useCallback((s: SectionId) => setEditing(s), []);\n const stopEditing = useCallback(() => setEditing(null), []);\n\n /**\n * Warm a route when the pointer arrives, not when it clicks: every link here\n * is `prefetch={false}`, so a click paid the full cost with nothing ready.\n * Prefetches the URL actually navigated to, query and all, because the\n * adaptive rows are intent-scoped (`/dashboard/catalog?intent=...`).\n */\n const warm = useCallback(\n (href: string) => {\n if (!href || href.startsWith('http')) return;\n router.prefetch(href);\n },\n [router],\n );\n const { tenant } = useAuth();\n const { inSettings, isAccountContext, businessContext } =\n railContextFor(pathname);\n // Editing in place needs the full rail, so it ends when the rail collapses or you\n // go into settings (and does not come back on its own when you return).\n if (editing && (collapsed || inSettings)) setEditing(null);\n // \"Back home\" means home: the dashboard, not wherever you last were.\n const settingsBack = () => router.push('/dashboard');\n const { hubChoice, setHubOpen } = useOpenHub(pathname);\n\n const { template, dashboardState } = useCurrentTemplate(tenant?.templateId);\n\n const tenantSettings = tenant?.settings as\n Record<string, unknown> | undefined;\n const moduleLabels = effectiveModuleLabels(tenantSettings);\n const hiddenModules = tenantSettings?.hiddenModules as string[] | undefined;\n /**\n * What the groups are built without. While editing, nothing is hidden, so a\n * hidden row is still on the rail (dimmed, to be shown again); the rail\n * itself drops hidden rows otherwise. Essential rows are never hidden, so a\n * stale `inbox` entry in the list does not hide the Inbox.\n */\n const navHidden = useMemo(\n () => (editing ? undefined : usableHidden(hiddenModules)),\n [editing, hiddenModules],\n );\n\n // How the tenant takes money (from onboarding). Refines the nav: an\n // in-person-only business doesn't ship, so fulfillment surfaces are hidden.\n // `null` (unset) never hides anything — this only ever removes what genuinely\n // doesn't apply.\n const salesChannel = isSalesChannel(tenantSettings?.salesChannel)\n ? tenantSettings.salesChannel\n : null;\n const inPersonOnly = salesChannel === 'in_person';\n\n // The behaviors the business really has: its model, the merchant's own\n // switches (Settings > Features) and the template's defaults. Resolved BEFORE\n // the core list is built, because that list gates its modules on them. It was\n // handed the raw stored `enabledBehaviors` instead, which knows nothing of an\n // override, so a Blog switched on in Features never reached Website's Blog\n // row and showed only in More.\n const enabledBehaviors = useMemo(\n () => resolveEnabledBehaviors(tenantSettings, template),\n [tenantSettings, template],\n );\n\n // Still use usePlatformModules for behavior resolution and resource sidebar\n const { coreNavigation } = usePlatformModules({\n template,\n dashboardState,\n enabledBehaviors,\n hiddenModules: tenantSettings?.hiddenModules as string[] | undefined,\n moduleLabels,\n moduleOrder: tenantSettings?.moduleOrder as string[] | undefined,\n });\n\n const intentConfig = useIntentConfig();\n const { can, isOwner, loading: permissionsLoading } = usePermissions();\n const capabilities = useCapabilities();\n\n /**\n * Which archetypes the CURRENT URL is narrowed to, verbatim.\n *\n * Not `intentConfig.primary.key`: that falls back to sessionStorage when the\n * URL carries no `?intent=`, so which row lit up depended on where you had\n * last been rather than where you are. And not a single key either — a\n * collapsed row carries every archetype behind it.\n */\n const currentIntent =\n intentConfig.mode === 'multi'\n ? intentKeySet(searchParams.get('intent'))\n : null;\n\n const location: SidebarLocation = {\n pathname,\n params: searchParams,\n currentIntent,\n };\n const isActive = (href: string) => isActiveFor(href, location);\n\n const getGroupItems = groupItems;\n\n // Build navigation groups. Memoized on its real inputs (NOT pathname) so the\n // whole tree — intent resolution, permission filtering, label overrides —\n // doesn't rebuild on every navigation. Only activeHref/isActive depend on the\n // route, and they're cheap.\n const declaredGroups: NavGroup[] = useMemo(\n () =>\n buildNavGroups({\n hiddenModules: navHidden,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly,\n intentConfig,\n coreNavigation,\n template,\n can,\n isOwner,\n permissionsLoading,\n hasCapability: capabilities.has,\n }),\n [\n navHidden,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly,\n intentConfig,\n coreNavigation,\n template,\n can,\n isOwner,\n permissionsLoading,\n capabilities,\n ],\n );\n\n /**\n * The bookable-things row, told apart: Courts and Coaches rather than one\n * row named after whichever shape the archetype guessed. Only expands when\n * this business actually has groups; see `use-resource-rows.ts`.\n */\n const groups = useResourceRows(declaredGroups);\n\n // Flat render model (console-style): work-surface items sit unlabeled with\n // Home; each fixed spine group is either a single link, or a hub that reveals\n // its children inline (disclosure, no chevron); Settings is pinned in the foot.\n const FIXED_IDS = FIXED_GROUP_IDS;\n /**\n * A flat group is a row, not just a container for rows.\n *\n * This took each non-fixed group's ITEMS and dropped the group itself, which\n * is right for an archetype group — Dine-in and Events are labels, and their\n * children are the destinations. It is wrong for the hoisted shared rows.\n *\n * `Catalog` and `Orders` come out of the collapse as groups carrying their\n * own `href`, because they belong to no archetype: Orders has no children at\n * all, and Catalog has exactly one (Collections). So this rendered **nothing**\n * for Orders and **only Collections** for Catalog — the two most-used\n * destinations in the app were absent from the sidebar of every\n * multi-archetype business, and the archetype rows beside them made it look\n * intentional.\n *\n * Every test in the suite asserts on what `buildNavGroups` returns, which was\n * correct the whole time. Nothing looked at what the sidebar drew.\n */\n // Shared, so the test and `scripts/archetype-shape.mts` see what is drawn\n // here rather than each modelling it. See `flatten-work-groups.ts`.\n const workItems = flattenWorkGroups(groups);\n const spineGroups = groups.filter(\n (g) => FIXED_IDS.has(g.id) && g.id !== 'settings',\n );\n\n // More is built here, ahead of the active-row list, because that list has to\n // include its rows: built after it they are never candidates, so nothing lights\n // on their pages and the hub never opens itself.\n const moduleOrder = tenantSettings?.moduleOrder as string[] | undefined;\n const moreGroup = useMemo(\n () =>\n buildMoreGroup(enabledBehaviors, {\n hiddenModules: navHidden,\n moduleLabels,\n moduleOrder,\n }),\n [enabledBehaviors, navHidden, moduleLabels, moduleOrder],\n );\n\n // Ahead of the active-row list for the same reason as More: its href has to\n // be a candidate or nothing lights on /dashboard/inbox.\n const inboxRow = buildInboxRow({\n hiddenModules: navHidden,\n moduleLabels,\n can,\n isOwner,\n permissionsLoading,\n });\n\n // Exactly one row carries the active highlight. If the path is inside a hub,\n // the hub's landing row lights up (its sub-pages are tabs, not sidebar rows);\n // otherwise the deepest matching flat row wins. Keeping it unique lets the\n // shared motion indicator spring cleanly between rows.\n // The deepest matching row wins — including hub CHILDREN, now that sub-pages\n // render inline in the sidebar rather than only as HubTabs. So the actual\n // current sub-page carries the highlight, not just the hub landing.\n // Exactly one row carries the active highlight — see `active-href.ts` for\n // how it is chosen. The list is every href the sidebar draws, hub children\n // included: Collections is attached as a child of the catalogue row, so when\n // children were missing from this list nothing lit on /dashboard/collections\n // and the hub holding the only row that leads there stayed shut.\n const activeHref = useMemo(\n () =>\n resolveActiveHref(\n [\n '/dashboard',\n ...(inboxRow ? [inboxRow.href] : []),\n ...workItems.flatMap((i) => [\n i.href,\n ...(i.children ?? []).map((c) => c.href),\n ]),\n ...spineGroups.flatMap((g) => [\n g.href,\n ...getGroupItems(g).map((i) => i.href),\n ]),\n ...(moreGroup ? getGroupItems(moreGroup).map((i) => i.href) : []),\n ].filter((h): h is string => !!h),\n location,\n ),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [pathname, searchParams, groups, moreGroup, inboxRow?.href],\n );\n\n const { inventoryHub, catalogMatches, collectionsItem, collectionsNested } =\n buildInventoryHub(workItems);\n\n /**\n * When the Inventory hub exists, catalog + collections move inside it; else\n * both still render flat, right where they were.\n *\n * THE ROWS THEMSELVES, not their ids. An id says what a row is called by its\n * archetype, not which screen it opens, and `subscriptions` calls its Plans\n * row `catalog` too — so a business running memberships beside anything with\n * a real catalogue had **Plans deleted from its sidebar**, in all nine\n * two-archetype combinations. Collections is consumed only when the hub took\n * it: dropping it always cost the row to a business with no catalogue row.\n */\n const ordersItem = findOrdersRow(workItems);\n const ordersPaymentsHub = buildOrdersPaymentsHub({\n ordersItem,\n moneyGroup: spineGroups.find((g) => g.id === 'payments') ?? null,\n can,\n });\n\n const consumed = new Set<unknown>([\n ...(inventoryHub\n ? [...catalogMatches, ...(collectionsItem ? [collectionsItem] : [])]\n : []),\n // With no catalogue row to hold it, Collections is drawn as a row of its\n // own by the rail; it must not also be drawn as a work row.\n ...(!inventoryHub && collectionsItem && !collectionsNested\n ? [collectionsItem]\n : []),\n ...(ordersPaymentsHub && ordersItem ? [ordersItem] : []),\n ]);\n /* General (unlabeled) leads; the rest drop into Operations. The rule is in\n `work-rows`. */\n const { primaryWork, extraWork } = splitWorkRows({\n workItems,\n consumed,\n hasInventoryHub: !!inventoryHub,\n });\n\n const hubById = new Map(spineGroups.map((g) => [g.id, g]));\n const operationsHub = hubById.get('operations');\n\n const badges = useNavBadges();\n /* Rows that appeared because the business added something this week — see\n `use-new-nav-rows`. */\n const newRows = useNewNavRows();\n\n // Plain render helpers (NOT inner components) — defining components inside\n // the render remounts them every pass, which breaks motion's shared-layout\n // highlight (it would jump from stale positions). Functions returning JSX\n // keep the elements stable under the parent, so the spring animates cleanly.\n const {\n renderHighlight,\n renderIcon,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n } = createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew: (id: string) => newRows.has(id),\n });\n\n const { moneyRow, manageRows, growRows } = buildRailRows({\n inventoryHub,\n payHub: ordersPaymentsHub ?? hubById.get('payments'),\n primaryWork,\n extraWork,\n collectionsItem,\n collectionsNested,\n customersHub: hubById.get('customers'),\n hardwareHub: hubById.get('hardware'),\n operationsHub,\n growHubs: [\n hubById.get('marketing'),\n hubById.get('analytics'),\n hubById.get('website'),\n ],\n renderHub,\n renderRow,\n renderWorkRow,\n });\n\n /** What the rail is drawn from, in both modes. See `rail-body.tsx`. */\n const railProps = {\n collapsed,\n homeActive: activeHref === '/dashboard',\n toolkitActive: pathname.startsWith('/dashboard/toolkit'),\n highlight: renderHighlight,\n inboxRow,\n renderRow,\n renderHub,\n renderIcon,\n ownsRoute,\n moneyRow,\n manageRows,\n growRows,\n moreGroup,\n };\n\n return (\n <aside\n className={`pv-shell sidebar${collapsed ? ' collapsed' : ''} hidden lg:flex`}\n >\n {/* THE HEAD OF THE RAIL, one flat row (John, 2026-10-04): the business's\n mark and name at the left, the Search and Ask icons at the right. It\n was a bordered card with a second line, and Search a row of its own\n beneath it. */}\n {/* A branded box can put its own logo at the top of the rail\n (NEXT_PUBLIC_BOX_LOGO_IN_SHELL). Left out when the rail is collapsed,\n where there is no room for a wordmark. */}\n {process.env.NEXT_PUBLIC_BOX_LOGO_IN_SHELL === 'true' &&\n process.env.NEXT_PUBLIC_BOX_LOGO &&\n !collapsed && (\n <div className=\"px-3 pb-1 pt-3\">\n <Logo size=\"sm\" theme=\"light\" />\n </div>\n )}\n <div className=\"side-head\">\n {collapsed && onToggleCollapse && (\n /* The way back out of a collapsed rail: laid over the business\n mark, invisible until the pointer is on it, then the expand icon\n (John, 2026-10-11). */\n <button\n type=\"button\"\n onClick={onToggleCollapse}\n className=\"side-head-expand\"\n title=\"Expand sidebar (⌘\\)\"\n aria-label=\"Expand sidebar\"\n >\n <PanelLeftOpen width={18} height={18} />\n </button>\n )}\n {!businessContext ? (\n <ProductSwitcher variant=\"head\" />\n ) : (\n <div className=\"flex-1\" />\n )}\n {/* Search, at the right of the head, after Linear (John, 2026-10-11).\n It was a row under Home; it is an action, not a place, so it leaves\n the list. */}\n <div className=\"side-head-actions\">\n <SidebarAsk collapsed={collapsed} variant=\"icon\" />\n {onToggleCollapse && !collapsed && (\n <button\n type=\"button\"\n onClick={onToggleCollapse}\n className=\"side-head-icon\"\n title=\"Collapse sidebar (⌘\\)\"\n aria-label=\"Collapse sidebar\"\n >\n <PanelLeftClose width={16} height={16} />\n </button>\n )}\n </div>\n </div>\n\n {/* Navigation */}\n <nav className=\"nav\" aria-label=\"Main navigation\">\n {/* No loading skeleton: the general nav renders without the template. */}\n\n {/* General nav ⇄ Settings nav: settings drills in place. The pane is\n keyed on the boundary so it remounts and slides in (from the right\n going into settings, from the left coming back). Enter-only — no\n exit — so the nav never collapses and re-flows from the bottom. */}\n <div\n key={inSettings ? 'settings' : 'main'}\n className={'nav-swap ' + (inSettings ? 'from-right' : 'from-left')}\n >\n {inSettings ? (\n <SettingsNav\n businessContext={businessContext}\n pathname={pathname}\n collapsed={collapsed}\n enabledBehaviors={enabledBehaviors}\n tenant={tenant}\n onBack={settingsBack}\n />\n ) : editing ? (\n <EditableRailBody\n {...railProps}\n onDone={stopEditing}\n editSection={editing}\n savedLabels={moduleLabels}\n storedLabels={\n tenantSettings?.moduleLabels as\n Record<string, string> | undefined\n }\n />\n ) : (\n <RailBody\n {...railProps}\n onConfigure={startEditing}\n edits={{\n hidden: hiddenModules,\n order: moduleOrder,\n labels: moduleLabels,\n sectionLabels: tenantSettings?.sectionLabels as\n Record<string, string> | undefined,\n }}\n />\n )}\n </div>\n </nav>\n\n <SidebarFooter collapsed={collapsed} />\n </aside>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sidebar.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { PanelLeftClose, PanelLeftOpen } from 'lucide-react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { ProductSwitcher } from './shell/product-switcher';\nimport { Logo } from '@withpotter/ui-kit/logo';\nimport './shell/shell.css';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCapabilities } from '@withpotter/lib/capability-context';\nimport { effectiveModuleLabels } from '@withpotter/lib/effective-module-labels';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { SidebarFooter } from '@withpotter/components/shell/sidebar-footer';\nimport { usePlatformModules } from '@withpotter/lib/use-platform-modules';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { isSalesChannel } from '@withpotter/hooks/use-sales-channel';\nimport {\n ACCOUNT_ROUTE_PREFIXES,\n intentKeySet,\n type NavGroup,\n type NavItem,\n} from './sidebar/config';\nimport {\n isActive as isActiveFor,\n resolveActiveHref,\n type SidebarLocation,\n} from './sidebar/active-href';\nimport { buildInboxRow } from './sidebar/inbox-row';\nimport { RailBody } from './sidebar/rail-body';\nimport { EditableRailBody } from './sidebar/rail-editor';\nimport { usableHidden, type SectionId } from './sidebar/rail-edits';\nimport { buildNavGroups } from './sidebar/build-nav-groups';\nimport { buildRailRows } from './sidebar/rail-rows';\nimport {\n buildOrdersPaymentsHub,\n findOrdersRow,\n} from './sidebar/orders-payments-hub';\nimport { splitWorkRows } from './sidebar/work-rows';\nimport { buildMoreGroup } from './sidebar/more-group';\nimport { buildInventoryHub } from './sidebar/inventory-hub';\nimport { SidebarAsk } from './shell/sidebar-ask';\nimport { railContextFor } from './sidebar/route-context';\nimport { useNewNavRows } from './sidebar/use-new-nav-rows';\nimport { useResourceRows } from './sidebar/use-resource-rows';\nimport {\n FIXED_GROUP_IDS,\n flattenWorkGroups,\n groupItems,\n} from './sidebar/flatten-work-groups';\nimport { SettingsNav } from './sidebar/settings-nav';\nimport { createNavRenderers } from './sidebar/nav-renderers';\nimport { useNavBadges } from './sidebar/use-nav-badges';\nimport { useOpenHub } from './sidebar/use-open-hub';\n\nexport function Sidebar({\n collapsed = false,\n onToggleCollapse,\n}: {\n collapsed?: boolean;\n /** Collapses the rail; the button for it sits beside Search in the head. */\n onToggleCollapse?: () => void;\n}) {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const router = useRouter();\n /** \"Customize sidebar\" is on: the rail itself is editable in place. */\n const [editing, setEditing] = useState<SectionId | null>(null);\n const startEditing = useCallback((s: SectionId) => setEditing(s), []);\n const stopEditing = useCallback(() => setEditing(null), []);\n\n /**\n * Warm a route when the pointer arrives, not when it clicks: every link here\n * is `prefetch={false}`, so a click paid the full cost with nothing ready.\n * Prefetches the URL actually navigated to, query and all, because the\n * adaptive rows are intent-scoped (`/dashboard/catalog?intent=...`).\n */\n const warm = useCallback(\n (href: string) => {\n if (!href || href.startsWith('http')) return;\n router.prefetch(href);\n },\n [router],\n );\n const { tenant } = useAuth();\n const { inSettings, isAccountContext, businessContext } =\n railContextFor(pathname);\n // Editing in place needs the full rail, so it ends when the rail collapses or you\n // go into settings (and does not come back on its own when you return).\n if (editing && (collapsed || inSettings)) setEditing(null);\n // \"Back home\" means home: the dashboard, not wherever you last were.\n const settingsBack = () => router.push('/dashboard');\n const { hubChoice, setHubOpen } = useOpenHub(pathname);\n\n const { template, dashboardState } = useCurrentTemplate(tenant?.templateId);\n\n const tenantSettings = tenant?.settings as\n Record<string, unknown> | undefined;\n const moduleLabels = effectiveModuleLabels(tenantSettings);\n const hiddenModules = tenantSettings?.hiddenModules as string[] | undefined;\n /**\n * What the groups are built without. While editing, nothing is hidden, so a\n * hidden row is still on the rail (dimmed, to be shown again); the rail\n * itself drops hidden rows otherwise. Essential rows are never hidden, so a\n * stale `inbox` entry in the list does not hide the Inbox.\n */\n const navHidden = useMemo(\n () => (editing ? undefined : usableHidden(hiddenModules)),\n [editing, hiddenModules],\n );\n\n // How the tenant takes money (from onboarding). Refines the nav: an\n // in-person-only business doesn't ship, so fulfillment surfaces are hidden.\n // `null` (unset) never hides anything — this only ever removes what genuinely\n // doesn't apply.\n const salesChannel = isSalesChannel(tenantSettings?.salesChannel)\n ? tenantSettings.salesChannel\n : null;\n const inPersonOnly = salesChannel === 'in_person';\n\n // Still use usePlatformModules for behavior resolution and resource sidebar\n const { coreNavigation } = usePlatformModules({\n template,\n dashboardState,\n enabledBehaviors: tenantSettings?.enabledBehaviors as string[] | undefined,\n hiddenModules: tenantSettings?.hiddenModules as string[] | undefined,\n moduleLabels,\n moduleOrder: tenantSettings?.moduleOrder as string[] | undefined,\n });\n\n const enabledBehaviors = useMemo(\n () => resolveEnabledBehaviors(tenantSettings, template),\n [tenantSettings, template],\n );\n\n const intentConfig = useIntentConfig();\n const { can, isOwner, loading: permissionsLoading } = usePermissions();\n const capabilities = useCapabilities();\n\n /**\n * Which archetypes the CURRENT URL is narrowed to, verbatim.\n *\n * Not `intentConfig.primary.key`: that falls back to sessionStorage when the\n * URL carries no `?intent=`, so which row lit up depended on where you had\n * last been rather than where you are. And not a single key either — a\n * collapsed row carries every archetype behind it.\n */\n const currentIntent =\n intentConfig.mode === 'multi'\n ? intentKeySet(searchParams.get('intent'))\n : null;\n\n const location: SidebarLocation = {\n pathname,\n params: searchParams,\n currentIntent,\n };\n const isActive = (href: string) => isActiveFor(href, location);\n\n const getGroupItems = groupItems;\n\n // Build navigation groups. Memoized on its real inputs (NOT pathname) so the\n // whole tree — intent resolution, permission filtering, label overrides —\n // doesn't rebuild on every navigation. Only activeHref/isActive depend on the\n // route, and they're cheap.\n const declaredGroups: NavGroup[] = useMemo(\n () =>\n buildNavGroups({\n hiddenModules: navHidden,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly,\n intentConfig,\n coreNavigation,\n template,\n can,\n isOwner,\n permissionsLoading,\n hasCapability: capabilities.has,\n }),\n [\n navHidden,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly,\n intentConfig,\n coreNavigation,\n template,\n can,\n isOwner,\n permissionsLoading,\n capabilities,\n ],\n );\n\n /**\n * The bookable-things row, told apart: Courts and Coaches rather than one\n * row named after whichever shape the archetype guessed. Only expands when\n * this business actually has groups; see `use-resource-rows.ts`.\n */\n const groups = useResourceRows(declaredGroups);\n\n // Flat render model (console-style): work-surface items sit unlabeled with\n // Home; each fixed spine group is either a single link, or a hub that reveals\n // its children inline (disclosure, no chevron); Settings is pinned in the foot.\n const FIXED_IDS = FIXED_GROUP_IDS;\n /**\n * A flat group is a row, not just a container for rows.\n *\n * This took each non-fixed group's ITEMS and dropped the group itself, which\n * is right for an archetype group — Dine-in and Events are labels, and their\n * children are the destinations. It is wrong for the hoisted shared rows.\n *\n * `Catalog` and `Orders` come out of the collapse as groups carrying their\n * own `href`, because they belong to no archetype: Orders has no children at\n * all, and Catalog has exactly one (Collections). So this rendered **nothing**\n * for Orders and **only Collections** for Catalog — the two most-used\n * destinations in the app were absent from the sidebar of every\n * multi-archetype business, and the archetype rows beside them made it look\n * intentional.\n *\n * Every test in the suite asserts on what `buildNavGroups` returns, which was\n * correct the whole time. Nothing looked at what the sidebar drew.\n */\n // Shared, so the test and `scripts/archetype-shape.mts` see what is drawn\n // here rather than each modelling it. See `flatten-work-groups.ts`.\n const workItems = flattenWorkGroups(groups);\n const spineGroups = groups.filter(\n (g) => FIXED_IDS.has(g.id) && g.id !== 'settings',\n );\n\n // More is built here, ahead of the active-row list, because that list has to\n // include its rows: built after it they are never candidates, so nothing lights\n // on their pages and the hub never opens itself.\n const moduleOrder = tenantSettings?.moduleOrder as string[] | undefined;\n const moreGroup = useMemo(\n () =>\n buildMoreGroup(enabledBehaviors, {\n hiddenModules: navHidden,\n moduleLabels,\n moduleOrder,\n }),\n [enabledBehaviors, navHidden, moduleLabels, moduleOrder],\n );\n\n // Ahead of the active-row list for the same reason as More: its href has to\n // be a candidate or nothing lights on /dashboard/inbox.\n const inboxRow = buildInboxRow({\n hiddenModules: navHidden,\n moduleLabels,\n can,\n isOwner,\n permissionsLoading,\n });\n\n // Exactly one row carries the active highlight. If the path is inside a hub,\n // the hub's landing row lights up (its sub-pages are tabs, not sidebar rows);\n // otherwise the deepest matching flat row wins. Keeping it unique lets the\n // shared motion indicator spring cleanly between rows.\n // The deepest matching row wins — including hub CHILDREN, now that sub-pages\n // render inline in the sidebar rather than only as HubTabs. So the actual\n // current sub-page carries the highlight, not just the hub landing.\n // Exactly one row carries the active highlight — see `active-href.ts` for\n // how it is chosen. The list is every href the sidebar draws, hub children\n // included: Collections is attached as a child of the catalogue row, so when\n // children were missing from this list nothing lit on /dashboard/collections\n // and the hub holding the only row that leads there stayed shut.\n const activeHref = useMemo(\n () =>\n resolveActiveHref(\n [\n '/dashboard',\n ...(inboxRow ? [inboxRow.href] : []),\n ...workItems.flatMap((i) => [\n i.href,\n ...(i.children ?? []).map((c) => c.href),\n ]),\n ...spineGroups.flatMap((g) => [\n g.href,\n ...getGroupItems(g).map((i) => i.href),\n ]),\n ...(moreGroup ? getGroupItems(moreGroup).map((i) => i.href) : []),\n ].filter((h): h is string => !!h),\n location,\n ),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [pathname, searchParams, groups, moreGroup, inboxRow?.href],\n );\n\n const { inventoryHub, catalogMatches, collectionsItem, collectionsNested } =\n buildInventoryHub(workItems);\n\n /**\n * When the Inventory hub exists, catalog + collections move inside it; else\n * both still render flat, right where they were.\n *\n * THE ROWS THEMSELVES, not their ids. An id says what a row is called by its\n * archetype, not which screen it opens, and `subscriptions` calls its Plans\n * row `catalog` too — so a business running memberships beside anything with\n * a real catalogue had **Plans deleted from its sidebar**, in all nine\n * two-archetype combinations. Collections is consumed only when the hub took\n * it: dropping it always cost the row to a business with no catalogue row.\n */\n const ordersItem = findOrdersRow(workItems);\n const ordersPaymentsHub = buildOrdersPaymentsHub({\n ordersItem,\n moneyGroup: spineGroups.find((g) => g.id === 'payments') ?? null,\n can,\n });\n\n const consumed = new Set<unknown>([\n ...(inventoryHub\n ? [...catalogMatches, ...(collectionsItem ? [collectionsItem] : [])]\n : []),\n // With no catalogue row to hold it, Collections is drawn as a row of its\n // own by the rail; it must not also be drawn as a work row.\n ...(!inventoryHub && collectionsItem && !collectionsNested\n ? [collectionsItem]\n : []),\n ...(ordersPaymentsHub && ordersItem ? [ordersItem] : []),\n ]);\n /* General (unlabeled) leads; the rest drop into Operations. The rule is in\n `work-rows`. */\n const { primaryWork, extraWork } = splitWorkRows({\n workItems,\n consumed,\n hasInventoryHub: !!inventoryHub,\n });\n\n const hubById = new Map(spineGroups.map((g) => [g.id, g]));\n const operationsHub = hubById.get('operations');\n\n const badges = useNavBadges();\n /* Rows that appeared because the business added something this week — see\n `use-new-nav-rows`. */\n const newRows = useNewNavRows();\n\n // Plain render helpers (NOT inner components) — defining components inside\n // the render remounts them every pass, which breaks motion's shared-layout\n // highlight (it would jump from stale positions). Functions returning JSX\n // keep the elements stable under the parent, so the spring animates cleanly.\n const {\n renderHighlight,\n renderIcon,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n } = createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew: (id: string) => newRows.has(id),\n });\n\n const { moneyRow, manageRows, growRows } = buildRailRows({\n inventoryHub,\n payHub: ordersPaymentsHub ?? hubById.get('payments'),\n primaryWork,\n extraWork,\n collectionsItem,\n collectionsNested,\n customersHub: hubById.get('customers'),\n hardwareHub: hubById.get('hardware'),\n operationsHub,\n growHubs: [\n hubById.get('marketing'),\n hubById.get('analytics'),\n hubById.get('website'),\n ],\n renderHub,\n renderRow,\n renderWorkRow,\n });\n\n /** What the rail is drawn from, in both modes. See `rail-body.tsx`. */\n const railProps = {\n collapsed,\n homeActive: activeHref === '/dashboard',\n toolkitActive: pathname.startsWith('/dashboard/toolkit'),\n highlight: renderHighlight,\n inboxRow,\n renderRow,\n renderHub,\n renderIcon,\n ownsRoute,\n moneyRow,\n manageRows,\n growRows,\n moreGroup,\n };\n\n return (\n <aside\n className={`pv-shell sidebar${collapsed ? ' collapsed' : ''} hidden lg:flex`}\n >\n {/* THE HEAD OF THE RAIL, one flat row (John, 2026-10-04): the business's\n mark and name at the left, the Search and Ask icons at the right. It\n was a bordered card with a second line, and Search a row of its own\n beneath it. */}\n {/* A branded box can put its own logo at the top of the rail\n (NEXT_PUBLIC_BOX_LOGO_IN_SHELL). Left out when the rail is collapsed,\n where there is no room for a wordmark. */}\n {process.env.NEXT_PUBLIC_BOX_LOGO_IN_SHELL === 'true' &&\n process.env.NEXT_PUBLIC_BOX_LOGO &&\n !collapsed && (\n <div className=\"px-3 pb-1 pt-3\">\n <Logo size=\"sm\" theme=\"light\" />\n </div>\n )}\n <div className=\"side-head\">\n {collapsed && onToggleCollapse && (\n /* The way back out of a collapsed rail: laid over the business\n mark, invisible until the pointer is on it, then the expand icon\n (John, 2026-10-11). */\n <button\n type=\"button\"\n onClick={onToggleCollapse}\n className=\"side-head-expand\"\n title=\"Expand sidebar (⌘\\)\"\n aria-label=\"Expand sidebar\"\n >\n <PanelLeftOpen width={18} height={18} />\n </button>\n )}\n {!businessContext ? (\n <ProductSwitcher variant=\"head\" />\n ) : (\n <div className=\"flex-1\" />\n )}\n {/* Search, at the right of the head, after Linear (John, 2026-10-11).\n It was a row under Home; it is an action, not a place, so it leaves\n the list. */}\n <div className=\"side-head-actions\">\n <SidebarAsk collapsed={collapsed} variant=\"icon\" />\n {onToggleCollapse && !collapsed && (\n <button\n type=\"button\"\n onClick={onToggleCollapse}\n className=\"side-head-icon\"\n title=\"Collapse sidebar (⌘\\)\"\n aria-label=\"Collapse sidebar\"\n >\n <PanelLeftClose width={16} height={16} />\n </button>\n )}\n </div>\n </div>\n\n {/* Navigation */}\n <nav className=\"nav\" aria-label=\"Main navigation\">\n {/* No loading skeleton: the general nav renders without the template. */}\n\n {/* General nav ⇄ Settings nav: settings drills in place. The pane is\n keyed on the boundary so it remounts and slides in (from the right\n going into settings, from the left coming back). Enter-only — no\n exit — so the nav never collapses and re-flows from the bottom. */}\n <div\n key={inSettings ? 'settings' : 'main'}\n className={'nav-swap ' + (inSettings ? 'from-right' : 'from-left')}\n >\n {inSettings ? (\n <SettingsNav\n businessContext={businessContext}\n pathname={pathname}\n collapsed={collapsed}\n enabledBehaviors={enabledBehaviors}\n tenant={tenant}\n onBack={settingsBack}\n />\n ) : editing ? (\n <EditableRailBody\n {...railProps}\n onDone={stopEditing}\n editSection={editing}\n savedLabels={moduleLabels}\n storedLabels={\n tenantSettings?.moduleLabels as\n Record<string, string> | undefined\n }\n />\n ) : (\n <RailBody\n {...railProps}\n onConfigure={startEditing}\n edits={{\n hidden: hiddenModules,\n order: moduleOrder,\n labels: moduleLabels,\n sectionLabels: tenantSettings?.sectionLabels as\n Record<string, string> | undefined,\n }}\n />\n )}\n </div>\n </nav>\n\n <SidebarFooter collapsed={collapsed} />\n </aside>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,6 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
4
  import { resolveWebUrl } from "@withpotter/lib/web-url";
5
- import { tileSource } from "./template-tile-source";
6
5
  const BASE_WIDTH = 1280;
7
6
  const RENDER_GRACE_MS = 6e3;
8
7
  function TemplateLiveThumb({
@@ -17,7 +16,6 @@ function TemplateLiveThumb({
17
16
  const [shouldLoadVideo, setShouldLoadVideo] = useState(false);
18
17
  const [isVisible, setIsVisible] = useState(false);
19
18
  const [videoFailed, setVideoFailed] = useState(false);
20
- const [posterFailed, setPosterFailed] = useState(false);
21
19
  const [rendered, setRendered] = useState(null);
22
20
  const candidates = (Array.isArray(image) ? image : [image]).filter(
23
21
  (src) => !!src
@@ -92,21 +90,12 @@ function TemplateLiveThumb({
92
90
  );
93
91
  return () => clearTimeout(t);
94
92
  }, [still]);
95
- const source = tileSource({
96
- hasVideo: !!media && (!!media.mp4 || !!media.webm),
97
- hasPoster: !!media?.poster,
98
- videoWanted: !!media && shouldLoadVideo,
99
- videoFailed,
100
- posterFailed,
101
- stillAvailable: !!still,
102
- renderGaveUp: rendered === false
103
- });
104
93
  return /* @__PURE__ */ jsx(
105
94
  "div",
106
95
  {
107
96
  ref,
108
97
  className: "relative aspect-[16/9] w-full overflow-hidden bg-white",
109
- children: source === "video" && media ? /* @__PURE__ */ jsxs(
98
+ children: media && shouldLoadVideo && !videoFailed ? /* @__PURE__ */ jsxs(
110
99
  "video",
111
100
  {
112
101
  ref: videoRef,
@@ -132,7 +121,7 @@ function TemplateLiveThumb({
132
121
  media.webm ? /* @__PURE__ */ jsx("source", { src: media.webm, type: "video/webm" }) : null
133
122
  ]
134
123
  }
135
- ) : source === "poster" && media ? (
124
+ ) : media?.poster && !videoFailed ? (
136
125
  // eslint-disable-next-line @next/next/no-img-element
137
126
  /* @__PURE__ */ jsx(
138
127
  "img",
@@ -141,11 +130,11 @@ function TemplateLiveThumb({
141
130
  alt: "",
142
131
  "aria-hidden": true,
143
132
  loading: "lazy",
144
- onError: () => setPosterFailed(true),
133
+ onError: () => setVideoFailed(true),
145
134
  className: "absolute inset-0 h-full w-full object-cover object-top"
146
135
  }
147
136
  )
148
- ) : source === "still" && still ? (
137
+ ) : still ? (
149
138
  // eslint-disable-next-line @next/next/no-img-element
150
139
  /* @__PURE__ */ jsx(
151
140
  "img",
@@ -158,7 +147,7 @@ function TemplateLiveThumb({
158
147
  className: "absolute inset-0 h-full w-full object-cover object-top"
159
148
  }
160
149
  )
161
- ) : source === "unavailable" ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center", children: /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-tertiary)]", children: [
150
+ ) : rendered === false ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center", children: /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-tertiary)]", children: [
162
151
  name ?? "Preview",
163
152
  " unavailable"
164
153
  ] }) }) : /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/templates/template-live-thumb.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { resolveWebUrl } from '@withpotter/lib/web-url';\nimport { tileSource } from './template-tile-source';\n\nconst BASE_WIDTH = 1280;\n/** Long enough for a cold render, short enough not to hold a blank tile. */\nconst RENDER_GRACE_MS = 6000;\n\nexport interface TemplatePreviewMedia {\n poster: string;\n webm?: string;\n mp4?: string;\n width: number;\n height: number;\n durationMs: number;\n sourceHash: string;\n}\n\n/**\n * A template's picture.\n *\n * PREFERS A STILL, FALLS BACK TO A LIVE RENDER. It used to be a live\n * `/preview/{id}` iframe unconditionally, which meant a gallery booted a full\n * storefront render per tile just to show a thumbnail. Slow on a Lagos\n * connection, uncacheable, and — the part that actually bit — a hard runtime\n * dependency on the renderer for a static need. With web-render down or\n * mid-deploy, every tile in the product was a blank white box.\n *\n * So: a real image when one exists, the live render only when it does not, and\n * a legible placeholder when neither answers. The live path stays because it is\n * genuinely useful (it can never be stale, and it is right for the preview\n * panel where someone asked to see the thing work), but it is no longer the\n * only way a template can be seen.\n */\nexport function TemplateLiveThumb({\n url,\n preview,\n image,\n name,\n}: {\n url: string;\n preview?: TemplatePreviewMedia | null;\n /**\n * Pictures to try, best first. When one loads, nothing is rendered live.\n *\n * A LIST, not one path, because there are two places a real picture can come\n * from and only one of them was being read. `stillFor` points at a still\n * generated into this app's own `public/templates/`, which has to be\n * regenerated by hand whenever a template is added — so the three most recent\n * templates had no still, fell straight through to the live iframe, and every\n * one of them showed as a blank white box. Meanwhile the template package\n * itself ships `thumbnail.png`, a clean full-page capture, and nothing in this\n * app ever asked for it.\n */\n image?: string | (string | null | undefined)[] | null;\n /** Named in the placeholder, so a failed tile is still identifiable. */\n name?: string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const [scale, setScale] = useState(0.3);\n const [shouldLoadVideo, setShouldLoadVideo] = useState(false);\n const [isVisible, setIsVisible] = useState(false);\n const [videoFailed, setVideoFailed] = useState(false);\n /** The poster is its own failure: a video that is not there must not take a working poster with it. */\n const [posterFailed, setPosterFailed] = useState(false);\n /** null while the frame is given a chance, false once it is not coming. */\n const [rendered, setRendered] = useState<boolean | null>(null);\n /**\n * A picture that 404s must fall through to the next one, not sit there broken.\n *\n * `stillFor` always returns a path — it cannot know whether the file exists —\n * so the `image` prop is never falsy and the stated precedence (picture, then\n * live render, then placeholder) silently collapsed to its first step.\n */\n const candidates = (Array.isArray(image) ? image : [image]).filter(\n (src): src is string => !!src,\n );\n const [failedCount, setFailedCount] = useState(0);\n const still = candidates[failedCount] ?? null;\n const media = preview\n ? {\n poster: `${resolveWebUrl()}${preview.poster}`,\n webm: preview.webm ? `${resolveWebUrl()}${preview.webm}` : undefined,\n mp4: preview.mp4 ? `${resolveWebUrl()}${preview.mp4}` : undefined,\n }\n : null;\n\n useEffect(() => {\n if (!preview) return;\n const connection = (\n navigator as Navigator & { connection?: { saveData?: boolean } }\n ).connection;\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n connection?.saveData === true\n ) {\n return;\n }\n const host = ref.current;\n if (!host) return;\n const loadObserver = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n setShouldLoadVideo(true);\n loadObserver.disconnect();\n },\n { rootMargin: '600px 0px' },\n );\n const visibilityObserver = new IntersectionObserver(\n ([entry]) => setIsVisible(entry.isIntersecting),\n { threshold: 0.01 },\n );\n loadObserver.observe(host);\n visibilityObserver.observe(host);\n return () => {\n loadObserver.disconnect();\n visibilityObserver.disconnect();\n };\n }, [preview]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n const syncPlayback = () => {\n if (isVisible && document.visibilityState === 'visible') {\n void video.play().catch((error: unknown) => {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n setVideoFailed(true);\n });\n } else {\n video.pause();\n }\n };\n syncPlayback();\n document.addEventListener('visibilitychange', syncPlayback);\n return () => document.removeEventListener('visibilitychange', syncPlayback);\n }, [isVisible, shouldLoadVideo]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) setScale(entry.contentRect.width / BASE_WIDTH);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n useEffect(() => {\n if (still) return;\n // A refused connection never fires onLoad, so the only way to know the\n // render is not coming is to stop waiting for it.\n const t = setTimeout(\n () => setRendered((r) => (r === null ? false : r)),\n RENDER_GRACE_MS,\n );\n return () => clearTimeout(t);\n }, [still]);\n\n const source = tileSource({\n hasVideo: !!media && (!!media.mp4 || !!media.webm),\n hasPoster: !!media?.poster,\n videoWanted: !!media && shouldLoadVideo,\n videoFailed,\n posterFailed,\n stillAvailable: !!still,\n renderGaveUp: rendered === false,\n });\n\n return (\n <div\n ref={ref}\n className=\"relative aspect-[16/9] w-full overflow-hidden bg-white\"\n >\n {source === 'video' && media ? (\n <video\n ref={videoRef}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n muted\n loop\n playsInline\n preload=\"metadata\"\n poster={media.poster}\n aria-hidden\n onError={() => setVideoFailed(true)}\n onCanPlay={() => {\n if (!isVisible || document.visibilityState !== 'visible') return;\n void videoRef.current?.play().catch((error: unknown) => {\n if (\n error instanceof DOMException &&\n error.name === 'AbortError'\n ) {\n return;\n }\n setVideoFailed(true);\n });\n }}\n >\n {media.mp4 ? <source src={media.mp4} type=\"video/mp4\" /> : null}\n {media.webm ? <source src={media.webm} type=\"video/webm\" /> : null}\n </video>\n ) : source === 'poster' && media ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={media.poster}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n onError={() => setPosterFailed(true)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : source === 'still' && still ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={still}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n // Try the next candidate, then the live render, then the placeholder.\n onError={() => setFailedCount((n) => n + 1)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : source === 'unavailable' ? (\n <div className=\"absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center\">\n <span className=\"text-xs text-[var(--text-tertiary)]\">\n {name ?? 'Preview'} unavailable\n </span>\n </div>\n ) : (\n <iframe\n src={url}\n tabIndex={-1}\n aria-hidden\n loading=\"lazy\"\n onLoad={() => setRendered(true)}\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left border-0\"\n style={{\n width: BASE_WIDTH,\n height: BASE_WIDTH * (5 / 3),\n transform: `scale(${scale})`,\n }}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/templates/template-live-thumb.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { resolveWebUrl } from '@withpotter/lib/web-url';\n\nconst BASE_WIDTH = 1280;\n/** Long enough for a cold render, short enough not to hold a blank tile. */\nconst RENDER_GRACE_MS = 6000;\n\nexport interface TemplatePreviewMedia {\n poster: string;\n webm?: string;\n mp4?: string;\n width: number;\n height: number;\n durationMs: number;\n sourceHash: string;\n}\n\n/**\n * A template's picture.\n *\n * PREFERS A STILL, FALLS BACK TO A LIVE RENDER. It used to be a live\n * `/preview/{id}` iframe unconditionally, which meant a gallery booted a full\n * storefront render per tile just to show a thumbnail. Slow on a Lagos\n * connection, uncacheable, and — the part that actually bit — a hard runtime\n * dependency on the renderer for a static need. With web-render down or\n * mid-deploy, every tile in the product was a blank white box.\n *\n * So: a real image when one exists, the live render only when it does not, and\n * a legible placeholder when neither answers. The live path stays because it is\n * genuinely useful (it can never be stale, and it is right for the preview\n * panel where someone asked to see the thing work), but it is no longer the\n * only way a template can be seen.\n */\nexport function TemplateLiveThumb({\n url,\n preview,\n image,\n name,\n}: {\n url: string;\n preview?: TemplatePreviewMedia | null;\n /**\n * Pictures to try, best first. When one loads, nothing is rendered live.\n *\n * A LIST, not one path, because there are two places a real picture can come\n * from and only one of them was being read. `stillFor` points at a still\n * generated into this app's own `public/templates/`, which has to be\n * regenerated by hand whenever a template is added — so the three most recent\n * templates had no still, fell straight through to the live iframe, and every\n * one of them showed as a blank white box. Meanwhile the template package\n * itself ships `thumbnail.png`, a clean full-page capture, and nothing in this\n * app ever asked for it.\n */\n image?: string | (string | null | undefined)[] | null;\n /** Named in the placeholder, so a failed tile is still identifiable. */\n name?: string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const [scale, setScale] = useState(0.3);\n const [shouldLoadVideo, setShouldLoadVideo] = useState(false);\n const [isVisible, setIsVisible] = useState(false);\n const [videoFailed, setVideoFailed] = useState(false);\n /** null while the frame is given a chance, false once it is not coming. */\n const [rendered, setRendered] = useState<boolean | null>(null);\n /**\n * A picture that 404s must fall through to the next one, not sit there broken.\n *\n * `stillFor` always returns a path — it cannot know whether the file exists —\n * so the `image` prop is never falsy and the stated precedence (picture, then\n * live render, then placeholder) silently collapsed to its first step.\n */\n const candidates = (Array.isArray(image) ? image : [image]).filter(\n (src): src is string => !!src,\n );\n const [failedCount, setFailedCount] = useState(0);\n const still = candidates[failedCount] ?? null;\n const media = preview\n ? {\n poster: `${resolveWebUrl()}${preview.poster}`,\n webm: preview.webm ? `${resolveWebUrl()}${preview.webm}` : undefined,\n mp4: preview.mp4 ? `${resolveWebUrl()}${preview.mp4}` : undefined,\n }\n : null;\n\n useEffect(() => {\n if (!preview) return;\n const connection = (\n navigator as Navigator & { connection?: { saveData?: boolean } }\n ).connection;\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n connection?.saveData === true\n ) {\n return;\n }\n const host = ref.current;\n if (!host) return;\n const loadObserver = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n setShouldLoadVideo(true);\n loadObserver.disconnect();\n },\n { rootMargin: '600px 0px' },\n );\n const visibilityObserver = new IntersectionObserver(\n ([entry]) => setIsVisible(entry.isIntersecting),\n { threshold: 0.01 },\n );\n loadObserver.observe(host);\n visibilityObserver.observe(host);\n return () => {\n loadObserver.disconnect();\n visibilityObserver.disconnect();\n };\n }, [preview]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n const syncPlayback = () => {\n if (isVisible && document.visibilityState === 'visible') {\n void video.play().catch((error: unknown) => {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n setVideoFailed(true);\n });\n } else {\n video.pause();\n }\n };\n syncPlayback();\n document.addEventListener('visibilitychange', syncPlayback);\n return () => document.removeEventListener('visibilitychange', syncPlayback);\n }, [isVisible, shouldLoadVideo]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) setScale(entry.contentRect.width / BASE_WIDTH);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n useEffect(() => {\n if (still) return;\n // A refused connection never fires onLoad, so the only way to know the\n // render is not coming is to stop waiting for it.\n const t = setTimeout(\n () => setRendered((r) => (r === null ? false : r)),\n RENDER_GRACE_MS,\n );\n return () => clearTimeout(t);\n }, [still]);\n\n return (\n <div\n ref={ref}\n className=\"relative aspect-[16/9] w-full overflow-hidden bg-white\"\n >\n {media && shouldLoadVideo && !videoFailed ? (\n <video\n ref={videoRef}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n muted\n loop\n playsInline\n preload=\"metadata\"\n poster={media.poster}\n aria-hidden\n onError={() => setVideoFailed(true)}\n onCanPlay={() => {\n if (!isVisible || document.visibilityState !== 'visible') return;\n void videoRef.current?.play().catch((error: unknown) => {\n if (\n error instanceof DOMException &&\n error.name === 'AbortError'\n ) {\n return;\n }\n setVideoFailed(true);\n });\n }}\n >\n {media.mp4 ? <source src={media.mp4} type=\"video/mp4\" /> : null}\n {media.webm ? <source src={media.webm} type=\"video/webm\" /> : null}\n </video>\n ) : media?.poster && !videoFailed ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={media.poster}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n onError={() => setVideoFailed(true)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : still ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={still}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n // Try the next candidate, then the live render, then the placeholder.\n onError={() => setFailedCount((n) => n + 1)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : rendered === false ? (\n <div className=\"absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center\">\n <span className=\"text-xs text-[var(--text-tertiary)]\">\n {name ?? 'Preview'} unavailable\n </span>\n </div>\n ) : (\n <iframe\n src={url}\n tabIndex={-1}\n aria-hidden\n loading=\"lazy\"\n onLoad={() => setRendered(true)}\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left border-0\"\n style={{\n width: BASE_WIDTH,\n height: BASE_WIDTH * (5 / 3),\n transform: `scale(${scale})`,\n }}\n />\n )}\n </div>\n );\n}\n"],"mappings":";AAuKQ,SAuBe,KAvBf;AArKR,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,MAAM,aAAa;AAEnB,MAAM,kBAAkB;AA4BjB,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAkBG;AACD,QAAM,MAAM,OAAuB,IAAI;AACvC,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,GAAG;AACtC,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAC5D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAyB,IAAI;AAQ7D,QAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG;AAAA,IAC1D,CAAC,QAAuB,CAAC,CAAC;AAAA,EAC5B;AACA,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,QAAQ,WAAW,WAAW,KAAK;AACzC,QAAM,QAAQ,UACV;AAAA,IACE,QAAQ,GAAG,cAAc,CAAC,GAAG,QAAQ,MAAM;AAAA,IAC3C,MAAM,QAAQ,OAAO,GAAG,cAAc,CAAC,GAAG,QAAQ,IAAI,KAAK;AAAA,IAC3D,KAAK,QAAQ,MAAM,GAAG,cAAc,CAAC,GAAG,QAAQ,GAAG,KAAK;AAAA,EAC1D,IACA;AAEJ,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,aACJ,UACA;AACF,QACE,OAAO,WAAW,kCAAkC,EAAE,WACtD,YAAY,aAAa,MACzB;AACA;AAAA,IACF;AACA,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,KAAM;AACX,UAAM,eAAe,IAAI;AAAA,MACvB,CAAC,CAAC,KAAK,MAAM;AACX,YAAI,CAAC,MAAM,eAAgB;AAC3B,2BAAmB,IAAI;AACvB,qBAAa,WAAW;AAAA,MAC1B;AAAA,MACA,EAAE,YAAY,YAAY;AAAA,IAC5B;AACA,UAAM,qBAAqB,IAAI;AAAA,MAC7B,CAAC,CAAC,KAAK,MAAM,aAAa,MAAM,cAAc;AAAA,MAC9C,EAAE,WAAW,KAAK;AAAA,IACpB;AACA,iBAAa,QAAQ,IAAI;AACzB,uBAAmB,QAAQ,IAAI;AAC/B,WAAO,MAAM;AACX,mBAAa,WAAW;AACxB,yBAAmB,WAAW;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,YAAU,MAAM;AACd,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AACZ,UAAM,eAAe,MAAM;AACzB,UAAI,aAAa,SAAS,oBAAoB,WAAW;AACvD,aAAK,MAAM,KAAK,EAAE,MAAM,CAAC,UAAmB;AAC1C,cAAI,iBAAiB,gBAAgB,MAAM,SAAS,cAAc;AAChE;AAAA,UACF;AACA,yBAAe,IAAI;AAAA,QACrB,CAAC;AAAA,MACH,OAAO;AACL,cAAM,MAAM;AAAA,MACd;AAAA,IACF;AACA,iBAAa;AACb,aAAS,iBAAiB,oBAAoB,YAAY;AAC1D,WAAO,MAAM,SAAS,oBAAoB,oBAAoB,YAAY;AAAA,EAC5E,GAAG,CAAC,WAAW,eAAe,CAAC;AAE/B,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,KAAK,IAAI,eAAe,CAAC,CAAC,KAAK,MAAM;AACzC,UAAI,MAAO,UAAS,MAAM,YAAY,QAAQ,UAAU;AAAA,IAC1D,CAAC;AACD,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,MAAO;AAGX,UAAM,IAAI;AAAA,MACR,MAAM,YAAY,CAAC,MAAO,MAAM,OAAO,QAAQ,CAAE;AAAA,MACjD;AAAA,IACF;AACA,WAAO,MAAM,aAAa,CAAC;AAAA,EAC7B,GAAG,CAAC,KAAK,CAAC;AAEV,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MAET,mBAAS,mBAAmB,CAAC,cAC5B;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,WAAU;AAAA,UACV,OAAK;AAAA,UACL,MAAI;AAAA,UACJ,aAAW;AAAA,UACX,SAAQ;AAAA,UACR,QAAQ,MAAM;AAAA,UACd,eAAW;AAAA,UACX,SAAS,MAAM,eAAe,IAAI;AAAA,UAClC,WAAW,MAAM;AACf,gBAAI,CAAC,aAAa,SAAS,oBAAoB,UAAW;AAC1D,iBAAK,SAAS,SAAS,KAAK,EAAE,MAAM,CAAC,UAAmB;AACtD,kBACE,iBAAiB,gBACjB,MAAM,SAAS,cACf;AACA;AAAA,cACF;AACA,6BAAe,IAAI;AAAA,YACrB,CAAC;AAAA,UACH;AAAA,UAEC;AAAA,kBAAM,MAAM,oBAAC,YAAO,KAAK,MAAM,KAAK,MAAK,aAAY,IAAK;AAAA,YAC1D,MAAM,OAAO,oBAAC,YAAO,KAAK,MAAM,MAAM,MAAK,cAAa,IAAK;AAAA;AAAA;AAAA,MAChE,IACE,OAAO,UAAU,CAAC;AAAA;AAAA,QAEpB;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM;AAAA,YACX,KAAI;AAAA,YACJ,eAAW;AAAA,YACX,SAAQ;AAAA,YACR,SAAS,MAAM,eAAe,IAAI;AAAA,YAClC,WAAU;AAAA;AAAA,QACZ;AAAA,UACE;AAAA;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,KAAI;AAAA,YACJ,eAAW;AAAA,YACX,SAAQ;AAAA,YAER,SAAS,MAAM,eAAe,CAAC,MAAM,IAAI,CAAC;AAAA,YAC1C,WAAU;AAAA;AAAA,QACZ;AAAA,UACE,aAAa,QACf,oBAAC,SAAI,WAAU,8FACb,+BAAC,UAAK,WAAU,uCACb;AAAA,gBAAQ;AAAA,QAAU;AAAA,SACrB,GACF,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,UAAU;AAAA,UACV,eAAW;AAAA,UACX,SAAQ;AAAA,UACR,QAAQ,MAAM,YAAY,IAAI;AAAA,UAC9B,WAAU;AAAA,UACV,OAAO;AAAA,YACL,OAAO;AAAA,YACP,QAAQ,cAAc,IAAI;AAAA,YAC1B,WAAW,SAAS,KAAK;AAAA,UAC3B;AAAA;AAAA,MACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.124",
3
+ "version": "0.13.0-next.58",
4
4
  "description": "Box dashboard feature components: editors, mobile screens, pickers, and composite UI built on @withpotter/ui-kit.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [
@@ -50,9 +50,9 @@
50
50
  "@tiptap/react": "^3.22.5",
51
51
  "@tiptap/starter-kit": "^3.22.5",
52
52
  "@tiptap/suggestion": "^3.22.5",
53
- "@withpotter/hooks": "^0.13.0-next.124",
54
- "@withpotter/lib": "^0.13.0-next.124",
55
- "@withpotter/ui-kit": "^0.13.0-next.124",
53
+ "@withpotter/hooks": "^0.13.0-next.58",
54
+ "@withpotter/lib": "^0.13.0-next.58",
55
+ "@withpotter/ui-kit": "^0.13.0-next.58",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",
@@ -77,7 +77,7 @@
77
77
  "typescript": "^5"
78
78
  },
79
79
  "publishConfig": {
80
- "registry": "https://registry.npmjs.org",
80
+ "registry": "https://registry.npmjs.org/",
81
81
  "access": "restricted"
82
82
  }
83
83
  }
@@ -1,10 +0,0 @@
1
- /**
2
- * Whether the money for an order has actually arrived.
3
- *
4
- * One answer for the activity feed's wording ("started a ticket order" versus
5
- * "bought a ticket") and for how quiet its row looks, so the two cannot drift.
6
- * The signal is the order's own `paymentStatus`, never the wording of a title.
7
- * An order with no payment status is treated as unpaid, which is what the
8
- * wording has always done.
9
- */
10
- export declare function isOrderPaid(paymentStatus: string | null | undefined): boolean;
@@ -1,7 +0,0 @@
1
- function isOrderPaid(paymentStatus) {
2
- return paymentStatus === "paid" || paymentStatus === "partially_refunded" || paymentStatus === "refunded";
3
- }
4
- export {
5
- isOrderPaid
6
- };
7
- //# sourceMappingURL=activity-paid.js.map