@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/dashboard/home/widgets/money-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUpRight, ArrowDownRight } from 'lucide-react';\nimport {\n computeDelta,\n type DeltaDirection,\n} from '@withpotter/pages-analytics/dashboard/(analytics)/analytics/helpers';\nimport type { TimeSeriesPoint } from '@withpotter/lib/api/types';\nimport { useMoneyBoard, RANGE_LABEL, type MetricsRange } from './money-context';\nimport type { WidgetSize } from './types';\nimport { Panel } from '../panel';\nimport { spreadByDay } from './revenue-series';\n\nconst RANGES: MetricsRange[] = ['7d', '30d', '90d'];\n\nfunction Delta({\n dir,\n pct,\n hasBaseline,\n}: {\n dir: DeltaDirection;\n pct: number;\n hasBaseline: boolean;\n}) {\n if (!hasBaseline || dir === 'flat') return null;\n const good = dir === 'up';\n return (\n <span\n className={`inline-flex items-center gap-0.5 text-sm font-semibold tabular-nums ${\n good ? 'text-[var(--success)]' : 'text-[var(--error)]'\n }`}\n >\n {good ? <ArrowUpRight /> : <ArrowDownRight />}\n {pct.toFixed(pct >= 10 ? 0 : 1)}%\n </span>\n );\n}\n\n/** Bars, because a day is a discrete thing and a line between two invents one. */\nfunction Series({ series }: { series: TimeSeriesPoint[] }) {\n const max = series.reduce((m, p) => Math.max(m, p.value), 0);\n return (\n <div aria-hidden className=\"mt-auto flex h-24 w-full items-end gap-px pt-3\">\n {series.map((p) => (\n <div\n key={p.date}\n style={{ height: `${Math.max(2, (p.value / max) * 100)}%` }}\n className=\"min-w-px flex-1 rounded-t-[1px] bg-[var(--color-brand-500)]/35\"\n />\n ))}\n </div>\n );\n}\n\n/**\n * The range control, rendered by whichever money widget is large enough to hold\n * it. It writes to shared state, so moving it moves every money widget at once.\n * At `sm` the widget states its window as text instead: a segmented control in a\n * 148px box would be most of the box.\n */\nfunction RangeControl() {\n const { range, setRange } = useMoneyBoard();\n return (\n <div className=\"surface-pinned flex items-center gap-0.5 p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r}\n type=\"button\"\n onClick={() => setRange(r)}\n aria-pressed={r === range}\n className={`rounded-md px-2 py-1 text-2xs font-medium transition-colors ${\n r === range\n ? 'bg-[var(--interactive-hover)] text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'\n }`}\n >\n {RANGE_LABEL[r]}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * The shared shell for every money widget.\n *\n * An empty window says so. Cloudflare writes \"Not enough data\" and Square writes\n * \"No data available for timeframe selected\"; both refuse to let nothing\n * masquerade as a measurement, and a bare `₦0` under the word Revenue tells a\n * merchant they sold nothing when the truth is that nobody could have bought\n * anything yet.\n */\nfunction MoneyWidget({\n label,\n value,\n delta,\n series,\n size,\n showRange,\n empty,\n caption,\n}: {\n label: string;\n value: string;\n delta?: { dir: DeltaDirection; pct: number; hasBaseline: boolean };\n series?: TimeSeriesPoint[];\n size: WidgetSize;\n /** Only one widget on the board needs to own the control. */\n showRange?: boolean;\n empty: boolean;\n /** One line under the figure, when the figure alone does not say enough. */\n caption?: string;\n}) {\n const { range, pending, failed, retry } = useMoneyBoard();\n const big = size !== 'narrow';\n\n return (\n <Panel\n title={label}\n control={big && showRange ? <RangeControl /> : undefined}\n action={\n big ? { label: 'Analytics', href: '/dashboard/analytics' } : undefined\n }\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {!big && (\n <p className=\"text-2xs text-[var(--text-muted)]\">\n {RANGE_LABEL[range]}\n </p>\n )}\n\n {pending ? (\n <span\n aria-hidden\n className=\"mt-2 inline-block h-[28px] w-32 animate-pulse rounded bg-[var(--bg-subtle)]\"\n />\n ) : failed ? (\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Couldn’t load this just now.{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"font-medium text-[var(--text-secondary)] underline underline-offset-2 hover:text-[var(--text-primary)]\"\n >\n Try again\n </button>\n </p>\n ) : empty ? (\n /* An empty window is not a measurement. Cloudflare writes \"Not enough\n data\" and Square writes \"No data available for timeframe selected\";\n both refuse to let nothing wear the shape of a result. */\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Not enough data\n </p>\n ) : (\n <>\n <div className=\"mt-1 flex flex-wrap items-baseline gap-x-2\">\n <span\n className={`font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)] ${\n big ? 'text-3xl' : 'text-2xl'\n }`}\n >\n {value}\n </span>\n {delta && <Delta {...delta} />}\n </div>\n {caption && (\n <p className=\"mt-1 text-2xs text-[var(--text-muted)]\">{caption}</p>\n )}\n {size === 'wide' && series?.length ? (\n <Series series={spreadByDay(series, Number.parseInt(range, 10))} />\n ) : null}\n </>\n )}\n </Panel>\n );\n}\n\nexport function RevenueWidget({ size }: { size: WidgetSize }) {\n const { data, prior, range, formatCurrency } = useMoneyBoard();\n const revenue = data?.sales.totalRevenue ?? 0;\n const series = data?.revenueTimeSeries ?? [];\n // With nothing to compare to there is no arrow, and a bare figure says nothing about whether it\n // is good. Say what the comparison is when there is none to draw.\n const caption =\n revenue > 0 && prior != null && (prior.sales.totalRevenue ?? 0) === 0\n ? `Nothing in the previous ${RANGE_LABEL[range]}`\n : undefined;\n return (\n <MoneyWidget\n label=\"Revenue\"\n size={size}\n showRange\n value={formatCurrency(Math.round(revenue))}\n delta={computeDelta(revenue, prior?.sales.totalRevenue ?? 0)}\n caption={caption}\n series={series}\n empty={data != null && revenue === 0 && !series.some((p) => p.value > 0)}\n />\n );\n}\n\nexport function SalesCountWidget({ size }: { size: WidgetSize }) {\n const { data, prior, language } = useMoneyBoard();\n const orders = data?.sales.totalOrders ?? 0;\n return (\n <MoneyWidget\n label={language.countLabel}\n size={size}\n // No chart. `getHomeSummary` returns one series and it is revenue's; a\n // picture of the wrong quantity is worse than no picture.\n value={orders.toLocaleString()}\n delta={computeDelta(orders, prior?.sales.totalOrders ?? 0)}\n empty={data != null && orders === 0}\n />\n );\n}\n\nexport function AvgOrderWidget({ size }: { size: WidgetSize }) {\n const { data, formatCurrency, language } = useMoneyBoard();\n const aov = data?.sales.averageOrderValue ?? 0;\n return (\n <MoneyWidget\n label={language.averageLabel}\n size={size}\n value={formatCurrency(aov)}\n empty={data != null && aov === 0}\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/money-widgets.tsx"],"sourcesContent":["'use client';\n\nimport { ArrowUpRight, ArrowDownRight } from 'lucide-react';\nimport {\n computeDelta,\n type DeltaDirection,\n} from '@withpotter/pages-analytics/dashboard/(analytics)/analytics/helpers';\nimport type { TimeSeriesPoint } from '@withpotter/lib/api/types';\nimport { useMoneyBoard, RANGE_LABEL, type MetricsRange } from './money-context';\nimport type { WidgetSize } from './types';\nimport { Panel } from '../panel';\n\nconst RANGES: MetricsRange[] = ['7d', '30d', '90d'];\n\nfunction Delta({\n dir,\n pct,\n hasBaseline,\n}: {\n dir: DeltaDirection;\n pct: number;\n hasBaseline: boolean;\n}) {\n if (!hasBaseline || dir === 'flat') return null;\n const good = dir === 'up';\n return (\n <span\n className={`inline-flex items-center gap-0.5 text-sm font-semibold tabular-nums ${\n good ? 'text-[var(--success)]' : 'text-[var(--error)]'\n }`}\n >\n {good ? <ArrowUpRight /> : <ArrowDownRight />}\n {pct.toFixed(pct >= 10 ? 0 : 1)}%\n </span>\n );\n}\n\n/** Bars, because a day is a discrete thing and a line between two invents one. */\nfunction Series({ series }: { series: TimeSeriesPoint[] }) {\n const max = series.reduce((m, p) => Math.max(m, p.value), 0);\n return (\n <div aria-hidden className=\"mt-auto flex h-16 w-full items-end gap-px pt-3\">\n {series.map((p) => (\n <div\n key={p.date}\n style={{ height: `${Math.max(2, (p.value / max) * 100)}%` }}\n className=\"min-w-px flex-1 rounded-t-[1px] bg-[var(--color-brand-500)]/35\"\n />\n ))}\n </div>\n );\n}\n\n/**\n * The range control, rendered by whichever money widget is large enough to hold\n * it. It writes to shared state, so moving it moves every money widget at once.\n * At `sm` the widget states its window as text instead: a segmented control in a\n * 148px box would be most of the box.\n */\nfunction RangeControl() {\n const { range, setRange } = useMoneyBoard();\n return (\n <div className=\"surface-pinned flex items-center gap-0.5 p-0.5\">\n {RANGES.map((r) => (\n <button\n key={r}\n type=\"button\"\n onClick={() => setRange(r)}\n aria-pressed={r === range}\n className={`rounded-md px-2 py-1 text-2xs font-medium transition-colors ${\n r === range\n ? 'bg-[var(--interactive-hover)] text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'\n }`}\n >\n {RANGE_LABEL[r]}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * The shared shell for every money widget.\n *\n * An empty window says so. Cloudflare writes \"Not enough data\" and Square writes\n * \"No data available for timeframe selected\"; both refuse to let nothing\n * masquerade as a measurement, and a bare `₦0` under the word Revenue tells a\n * merchant they sold nothing when the truth is that nobody could have bought\n * anything yet.\n */\nfunction MoneyWidget({\n label,\n value,\n delta,\n series,\n size,\n showRange,\n empty,\n}: {\n label: string;\n value: string;\n delta?: { dir: DeltaDirection; pct: number; hasBaseline: boolean };\n series?: TimeSeriesPoint[];\n size: WidgetSize;\n /** Only one widget on the board needs to own the control. */\n showRange?: boolean;\n empty: boolean;\n}) {\n const { range, pending, failed, retry } = useMoneyBoard();\n const big = size !== 'narrow';\n\n return (\n <Panel\n title={label}\n control={big && showRange ? <RangeControl /> : undefined}\n action={\n big ? { label: 'Analytics', href: '/dashboard/analytics' } : undefined\n }\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {!big && (\n <p className=\"text-2xs text-[var(--text-muted)]\">\n {RANGE_LABEL[range]}\n </p>\n )}\n\n {pending ? (\n <span\n aria-hidden\n className=\"mt-2 inline-block h-[28px] w-32 animate-pulse rounded bg-[var(--bg-subtle)]\"\n />\n ) : failed ? (\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Couldn’t load this just now.{' '}\n <button\n type=\"button\"\n onClick={retry}\n className=\"font-medium text-[var(--text-secondary)] underline underline-offset-2 hover:text-[var(--text-primary)]\"\n >\n Try again\n </button>\n </p>\n ) : empty ? (\n /* An empty window is not a measurement. Cloudflare writes \"Not enough\n data\" and Square writes \"No data available for timeframe selected\";\n both refuse to let nothing wear the shape of a result. */\n <p className=\"mt-2 text-sm text-[var(--text-tertiary)]\">\n Not enough data\n </p>\n ) : (\n <>\n <div className=\"mt-1 flex flex-wrap items-baseline gap-x-2\">\n <span\n className={`font-semibold leading-none tracking-[-0.02em] tabular-nums text-[var(--text-primary)] ${\n big ? 'text-3xl' : 'text-2xl'\n }`}\n >\n {value}\n </span>\n {delta && <Delta {...delta} />}\n </div>\n {size === 'wide' && series?.length ? (\n <Series series={series} />\n ) : null}\n </>\n )}\n </Panel>\n );\n}\n\nexport function RevenueWidget({ size }: { size: WidgetSize }) {\n const { data, prior, formatCurrency } = useMoneyBoard();\n const revenue = data?.sales.totalRevenue ?? 0;\n const series = data?.revenueTimeSeries ?? [];\n return (\n <MoneyWidget\n label=\"Revenue\"\n size={size}\n showRange\n value={formatCurrency(Math.round(revenue))}\n delta={computeDelta(revenue, prior?.sales.totalRevenue ?? 0)}\n series={series}\n empty={data != null && revenue === 0 && !series.some((p) => p.value > 0)}\n />\n );\n}\n\nexport function SalesCountWidget({ size }: { size: WidgetSize }) {\n const { data, prior, language } = useMoneyBoard();\n const orders = data?.sales.totalOrders ?? 0;\n return (\n <MoneyWidget\n label={language.countLabel}\n size={size}\n // No chart. `getHomeSummary` returns one series and it is revenue's; a\n // picture of the wrong quantity is worse than no picture.\n value={orders.toLocaleString()}\n delta={computeDelta(orders, prior?.sales.totalOrders ?? 0)}\n empty={data != null && orders === 0}\n />\n );\n}\n\nexport function AvgOrderWidget({ size }: { size: WidgetSize }) {\n const { data, formatCurrency, language } = useMoneyBoard();\n const aov = data?.sales.averageOrderValue ?? 0;\n return (\n <MoneyWidget\n label={language.averageLabel}\n size={size}\n value={formatCurrency(aov)}\n empty={data != null && aov === 0}\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,4 @@
1
- export { registerHostWidget, hostWidgets } from './host-widgets';
2
- import type { BuiltInWidgetId, WidgetDef, WidgetId, WidgetContext } from './types';
1
+ import type { WidgetDef, WidgetId, WidgetContext } from './types';
3
2
  /**
4
3
  * The catalogue.
5
4
  *
@@ -19,8 +18,8 @@ import type { BuiltInWidgetId, WidgetDef, WidgetId, WidgetContext } from './type
19
18
  *
20
19
  * `Blocked` is deliberately NOT here. See `home-widgets.tsx`.
21
20
  */
22
- export declare const WIDGETS: Record<BuiltInWidgetId, WidgetDef>;
23
- export declare const WIDGET_ORDER: BuiltInWidgetId[];
21
+ export declare const WIDGETS: Record<WidgetId, WidgetDef>;
22
+ export declare const WIDGET_ORDER: WidgetId[];
24
23
  /**
25
24
  * Every destination the board is already showing, from placed widgets only.
26
25
  *
@@ -34,6 +33,4 @@ export declare const WIDGET_ORDER: BuiltInWidgetId[];
34
33
  * it would be how the home quietly stops mentioning that you cannot be paid.
35
34
  */
36
35
  export declare function coveredHrefs(ctx: WidgetContext, placed: Iterable<WidgetId>): Set<string>;
37
- /** A widget by id: built in first, then a host's. Undefined for an id nobody defines any more. */
38
- export declare function widgetDef(id: WidgetId): WidgetDef | undefined;
39
36
  export declare function widgetsFor(ctx: WidgetContext): WidgetDef[];
@@ -13,8 +13,6 @@ import { BillWidget } from "./bill-widget";
13
13
  import { WebsiteWidget } from "./website-widget";
14
14
  import { isSettled, orderSteps } from "./setup-steps";
15
15
  import { ThemeMiniature, Pointer } from "./widget-art";
16
- import { hostWidget, hostWidgets } from "./host-widgets";
17
- import { registerHostWidget, hostWidgets as hostWidgets2 } from "./host-widgets";
18
16
  const WIDGETS = {
19
17
  /**
20
18
  * Money, as three widgets rather than one block.
@@ -364,7 +362,7 @@ const WIDGET_ORDER = [
364
362
  function coveredHrefs(ctx, placed) {
365
363
  const out = /* @__PURE__ */ new Set();
366
364
  for (const id of placed) {
367
- const def = widgetDef(id);
365
+ const def = WIDGETS[id];
368
366
  if (!def) continue;
369
367
  for (const href of def.covers?.(ctx) ?? []) out.add(href);
370
368
  const empty = def.empty?.(ctx);
@@ -372,21 +370,13 @@ function coveredHrefs(ctx, placed) {
372
370
  }
373
371
  return out;
374
372
  }
375
- function widgetDef(id) {
376
- return WIDGETS[id] ?? hostWidget(id);
377
- }
378
373
  function widgetsFor(ctx) {
379
- return [...WIDGET_ORDER.map((id) => WIDGETS[id]), ...hostWidgets()].filter(
380
- (w) => w.available(ctx)
381
- );
374
+ return WIDGET_ORDER.map((id) => WIDGETS[id]).filter((w) => w.available(ctx));
382
375
  }
383
376
  export {
384
377
  WIDGETS,
385
378
  WIDGET_ORDER,
386
379
  coveredHrefs,
387
- hostWidgets2 as hostWidgets,
388
- registerHostWidget,
389
- widgetDef,
390
380
  widgetsFor
391
381
  };
392
382
  //# sourceMappingURL=registry.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/dashboard/home/widgets/registry.tsx"],"sourcesContent":["'use client';\n\nimport { RevenueWidget } from './money-widgets';\nimport { ActivityCard } from './activity-card';\nimport { SetupWidget } from './setup-widget';\nimport { HardwareWidget } from './hardware-widget';\nimport { TeamWidget } from './team-widget';\nimport { CreditsWidget } from './credits-widget';\nimport { ReferralWidget } from './referral-widget';\nimport { RegularsWidget } from './regulars-widget';\nimport { OwedWidget } from './owed-widget';\nimport { BillWidget } from './bill-widget';\nimport { WebsiteWidget } from './website-widget';\nimport { isSettled, orderSteps } from './setup-steps';\nimport { ThemeMiniature, Pointer } from './widget-art';\nimport { hostWidget, hostWidgets } from './host-widgets';\n\nexport { registerHostWidget, hostWidgets } from './host-widgets';\nimport type {\n BuiltInWidgetId,\n WidgetDef,\n WidgetId,\n WidgetContext,\n} from './types';\n\n/**\n * The catalogue.\n *\n * Every entry here is a block that already existed on the home; making it a\n * widget is a matter of declaring which sizes it has been drawn for, not of\n * writing a new surface. That is the payoff of having taken the home apart into\n * self-contained blocks first.\n *\n * THREE OF THESE WERE DELETED FROM THE PAGE, AND THAT IS THE POINT.\n * `quick-actions` and `recents` were columns on the old console and were cut\n * for restating the sidebar and for rendering a list of settings screens you\n * had already visited. Neither earns a permanent third of the home. Both are\n * perfectly reasonable things for a particular merchant to CHOOSE, and a widget\n * board is exactly the mechanism that lets something be available without being\n * imposed. Opt-in is a different bar from default, and the two were being\n * argued as though they were one.\n *\n * `Blocked` is deliberately NOT here. See `home-widgets.tsx`.\n */\nexport const WIDGETS: Record<BuiltInWidgetId, WidgetDef> = {\n /**\n * Money, as three widgets rather than one block.\n *\n * It was a single `lg`-only \"Performance\" panel, which left the `sm` and `md`\n * classes with almost nothing to offer and made the gallery look like a\n * feature with one item in it. Split, a merchant can put revenue large and\n * average order small, or drop the count entirely — which is the actual point\n * of a board.\n *\n * All three read ONE fetch and ONE window from `MoneyBoardProvider`. The\n * window is shared on purpose: revenue over thirty days beside a count over\n * seven is two true figures making one false impression.\n */\n revenue: {\n id: 'revenue',\n name: 'Revenue',\n description: 'What came in, and how it moved.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'wide',\n // Money is the one part of this page that is not merely extra detail for\n // the wrong person; it is the part they were deliberately not given.\n available: (ctx) => ctx.canSeeEarnings,\n /**\n * The last of the old \"Finish setting up\" cards, put where its result will\n * appear.\n *\n * This is the whole argument for empty states over a setup rail, and this\n * card is the clearest case of it: a business with no payout account\n * cannot be paid, so a revenue chart over it is a chart of zero drawn with\n * confidence. The card that tells you to connect an account IS the card\n * that will show the money once you have — which no separate rail can ever\n * say, because a rail disappears at exactly the moment it would have\n * become useful.\n *\n * NO DRAWING. This card carried a miniature of a payout landing in a bank,\n * with an amount and the last four digits of an account on it. Neither was\n * the merchant's: on a screen about their money, a figure that is not\n * theirs reads as theirs. The card is its sentence and its button.\n */\n covers: () => ['/dashboard/payments'],\n empty: (ctx) =>\n ctx.hasPayments && ctx.needsIdentity\n ? {\n kind: 'action',\n title: 'Get paid',\n body: 'Your bank account is connected. Verify who you are and payouts can start.',\n action: 'Verify identity',\n href: '/dashboard/payments/payouts',\n }\n : ctx.hasPayments\n ? null\n : {\n kind: 'action',\n title: 'Take payments',\n body: 'Connect a payout account so customers can pay you.',\n action: 'Set up payments',\n href: '/dashboard/payments',\n },\n render: (_ctx, size) => <RevenueWidget size={size} />,\n },\n\n website: {\n id: 'website',\n name: 'Website',\n description: 'Your storefront, and where it lives.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'narrow',\n available: () => true,\n empty: (ctx) =>\n // A shut site outranks both prompts below it: design and a domain are\n // improvements to something nobody can reach yet. When it is shut the\n // card renders itself — the preview, with \"Not open yet\" under the name.\n // It no longer carries a publish control (John, 2026-08-25); opening the\n // site is done from the Website hub the preview links into.\n !ctx.isSiteOpen\n ? null\n : !ctx.hasDesignedSite\n ? {\n kind: 'action',\n title: 'Design your site',\n body: 'Pick a look that fits your brand, then make it yours.',\n action: 'Customize',\n // The Website hub, not the editor: \"design your site\" is an\n // invitation to look at it, and the hub is where templates, domains\n // and pages hang off. Dropping someone into the canvas skips the\n // choice they came to make.\n href: '/dashboard/business',\n art: <ThemeMiniature />,\n }\n : !ctx.storefrontUrl\n ? {\n kind: 'action',\n title: 'Get a domain',\n body: 'A branded URL that’s easy to find, trust, and remember.',\n action: 'Add domain',\n href: '/dashboard/business/domains',\n art: <Pointer className=\"h-[44px] w-[44px] drop-shadow-md\" />,\n }\n : null,\n render: (ctx) => (\n <WebsiteWidget\n storefrontUrl={ctx.storefrontUrl}\n hasCustomDomain={ctx.hasCustomDomain}\n siteImage={ctx.siteImage}\n businessName={ctx.businessName}\n />\n ),\n },\n\n /**\n * REGULARS WHO HAVE GONE QUIET — the marketing card, and the third attempt.\n *\n * A campaign card wanted send and schedule endpoints that do not exist. An\n * attribution card rendered \"Direct · 100%\", a non-answer dressed as a\n * finding. This one cannot degrade that way: it names PEOPLE, and every\n * business that has sold twice to anyone has some.\n *\n * Winning a repeat customer back costs a fraction of finding a new one, and\n * for a business trading over WhatsApp the whole campaign is one message to\n * one person who already likes you.\n */\n regulars: {\n id: 'regulars',\n name: 'Gone quiet',\n description: 'Repeat customers who have stopped ordering.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n // Only once somebody HAS gone quiet. With nobody to name, the card held a\n // drawing of a customer and \"72d ago\" for a person who does not exist, on\n // a board that is meant to say only what is true of this business. Unknown\n // counts as nobody, so a list that is loading or failed draws no card.\n available: (ctx) => ctx.hasQuietRegulars === true,\n covers: () => ['/dashboard/customers'],\n render: () => <RegularsWidget />,\n },\n\n /**\n * WHO IS IN THE BUSINESS.\n *\n * Moved out of the page header and onto the board, swapping places with the\n * \"Add widget\" tile — the faces are content you look at and click into, which\n * is what a card is for, and adding a widget is a control, which is what a\n * header is for.\n */\n /**\n * WHAT IS WRONG WITH THE MACHINES.\n *\n * A till that has stopped reporting, and a drawer that did not balance.\n * Never a count of devices or a total of takings: home answers \"is anything\n * wrong\", and \"you own four tills\" is not an answer to that.\n *\n * Available only once the business owns hardware. A card that spends its life\n * saying \"no devices\" teaches people to skip it, and Hardware's own empty\n * state is where somebody finds out they can have a till.\n */\n hardware: {\n id: 'hardware',\n name: 'Hardware',\n description:\n 'A machine that has gone quiet, a drawer that was short — or a terminal you have not got yet.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n /**\n * Available whether or not the business owns any.\n *\n * It was `ctx.hasHardware`, which made the card reachable only by someone\n * who already had a terminal — and the nav row that used to do the\n * discovering went to Settings on 2026-09-03. A merchant with no hardware\n * now gets the offer here; see `HardwareWidget`, which carries the three\n * states.\n */\n available: () => true,\n covers: () => ['/dashboard/hardware'],\n render: () => <HardwareWidget />,\n },\n\n /**\n * THE TWO CARDS THAT LEFT THE RAIL (John, 2026-09-03).\n *\n * They were a stacked deck in the sidebar's foot, ported off Lovable. The\n * deck's own argument for living there — a rail can hold two standing offers\n * without spending two rows on them — quietly ignored that it spends them on\n * EVERY screen. Here a merchant can size them, move them, or throw them\n * away, which is the whole point of a board.\n *\n * `canSeeEarnings` on both: they are about money leaving the business, and\n * somebody working a shift has no business being sold to. It is the closest\n * flag the board already resolves — the context carries no\n * `canManageSettings`, and adding one for two cards would be a third\n * permission source on this page.\n */\n credits: {\n id: 'credits',\n name: 'Email credits',\n description: 'How much of this period’s email you have used.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/settings/billing/credits'],\n render: () => <CreditsWidget />,\n },\n\n referral: {\n id: 'referral',\n name: 'Refer a business',\n description: 'Your invite link, and what it has earned.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/referrals'],\n render: () => <ReferralWidget />,\n },\n\n team: {\n id: 'team',\n name: 'Team',\n description: 'Who is in the business, and what they can do.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canManageTeam,\n covers: () => ['/dashboard/settings/team'],\n render: () => <TeamWidget />,\n },\n\n /**\n * HOW FAR THROUGH SETTING UP.\n *\n * The one widget that is most useful when the board is emptiest, and the only\n * one that removes ITSELF: `available` goes false the moment every step is\n * settled, which drops it from the board and frees its slot for the add tile.\n * That is the difference between this and the setup RAIL the home was taken\n * apart from — the rail had to be dismissed, so it went on sitting there for\n * people who had already finished.\n */\n /**\n * WHAT IS INVOICED AND HAS NOT ARRIVED.\n *\n * Revenue answers \"what came in\"; this answers \"what is late\", and they are\n * different questions to a business chasing money. Narrow only — it is one\n * figure and an age, not a series.\n *\n * `canSeeEarnings`, not `canSeeAmounts`: this is the business's income, so\n * staff who can read an order total still should not see the shortfall.\n */\n owed: {\n id: 'owed',\n name: 'Money owed',\n description: 'What is invoiced and unpaid, and how old it is.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/invoices'],\n render: () => <OwedWidget />,\n },\n\n /**\n * WHAT POTTER COSTS, which is the only money on this board going the other\n * way. Revenue is what arrived and Money owed is what has not; both are the\n * customer's side. This one is the merchant's own bill, and it was three\n * clicks away behind Settings on a page whose job is \"is anything wrong\".\n *\n * `canSeeEarnings`, like the other two: what the business pays for its own\n * tools is not a staff figure.\n *\n * Opt-in, and not in any archetype default. A bill that is fine says nothing\n * new for twenty-nine days out of thirty, which is a poor claim on a\n * permanent slot but a perfectly good thing to be able to CHOOSE — which is\n * what a board is for.\n */\n bill: {\n id: 'bill',\n name: 'Box bill',\n description: 'What Box costs this period, and when it is charged.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/settings/billing'],\n render: () => <BillWidget />,\n },\n\n setup: {\n id: 'setup',\n name: 'Finish setting up',\n description: 'What is left before the business is fully live.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n // Through `orderSteps`, which is what the card itself lists: a step it\n // drops must not keep an otherwise finished card on the board.\n available: (ctx) => orderSteps(ctx.setupSteps).some((s) => !isSettled(s)),\n // Each step's own destination is already on the board as this card's list,\n // so a blocker pointing at the same place would be the same chore twice.\n covers: (ctx) =>\n orderSteps(ctx.setupSteps)\n .map((s) => s.link)\n .filter((l): l is string => !!l),\n render: (ctx) => (\n <SetupWidget steps={ctx.setupSteps} itemLabel={ctx.itemLabel} />\n ),\n },\n\n /**\n * WHAT HAPPENED, as opposed to what is waiting.\n *\n * Today and Activity are two questions and the home needs both. Merging them\n * is the mistake this avoids: one list with now as the pivot puts a booking\n * at 22:00 next to an invoice paid twenty minutes ago under one heading, so\n * the reader has to check each row's tense before knowing whether it is a\n * task. `buildActivityItems` already assembled this from six sources and the\n * desktop home had nowhere to put it.\n */\n activity: {\n id: 'activity',\n name: 'Recent activity',\n description: 'What has happened, newest first.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'wide',\n available: () => true,\n /**\n * NOTHING HAPPENS UNTIL THERE IS SOMETHING TO SELL. John, 2026-08-23:\n * \"you can't have recent activities without having stuff you are selling\".\n *\n * This card used to answer an empty business with \"Nothing has happened\n * yet · Orders, payments and new customers will appear here\" — true, and a\n * `waiting` state, which is the classification this file warns about\n * getting backwards. Waiting is for a box no button can fill: revenue\n * before the first sale. This one HAS a button. A catalogue with nothing in\n * it is the reason the feed is empty, and adding the first item is the act\n * that starts it.\n *\n * No drawing: the miniature that sat here was two catalogue tiles with a\n * price on one, and a price nobody set has no place on this board.\n *\n * `itemLabel`, so a club whose sidebar says Services is not told to add a\n * product. The href is the catalogue's own new-item route, which every\n * archetype reaching this card resolves through.\n */\n empty: (ctx) =>\n ctx.hasSellable\n ? null\n : {\n kind: 'action',\n title: `Add your first ${ctx.itemLabel.toLowerCase()}`,\n body: 'Orders, payments and new customers show up here once there is something to buy.',\n action: `Add ${ctx.itemLabel.toLowerCase()}`,\n href: '/dashboard/catalog/new',\n },\n render: (ctx, size) => (\n <ActivityCard\n items={ctx.activity}\n /**\n * What is stopping you, and what is late.\n *\n * Blockers plus OVERDUE items, which is the half that was missing.\n * A blocker says \"nothing can happen until this is fixed\"; an overdue\n * invoice says \"this already should have happened\" — both are things\n * the merchant is behind on, and only the first was reaching the card.\n * Blockers still sort first because a broken payout stops every future\n * sale where one late invoice stops one payment.\n *\n * Filtered here rather than in the widget so the widget stays a\n * renderer and the page keeps deciding what counts as urgent.\n */\n blockers={[\n ...ctx.today.filter((i) => i.blocking),\n ...ctx.today.filter((i) => !i.blocking && i.overdue),\n ]}\n limit={size === 'wide' ? 6 : 4}\n />\n ),\n },\n};\n\nexport const WIDGET_ORDER: BuiltInWidgetId[] = [\n 'website',\n 'revenue',\n 'regulars',\n 'team',\n 'setup',\n 'bill',\n /* Below the bill, which is what Box costs; these two are what it costs to\n REACH people and what brings people in. Both are offers rather than\n readings, so they sit under everything a merchant is actually measuring. */\n 'credits',\n 'referral',\n 'activity',\n];\n\n/**\n * Every destination the board is already showing, from placed widgets only.\n *\n * Two sources, and both are needed. A widget's `covers` is what it says when it\n * has data (Low stock lists the items); its `empty` action is what it says when\n * it does not (Revenue's whole card becomes \"Take payments\"). A chore is\n * covered either way, so both count.\n *\n * `placed` rather than every registered widget: a chore covered by a widget the\n * merchant has REMOVED is not covered at all, and suppressing the blocker for\n * it would be how the home quietly stops mentioning that you cannot be paid.\n */\nexport function coveredHrefs(\n ctx: WidgetContext,\n placed: Iterable<WidgetId>,\n): Set<string> {\n const out = new Set<string>();\n for (const id of placed) {\n const def = widgetDef(id);\n if (!def) continue;\n for (const href of def.covers?.(ctx) ?? []) out.add(href);\n const empty = def.empty?.(ctx);\n if (empty && empty.kind === 'action') out.add(empty.href);\n }\n return out;\n}\n\n/** A widget by id: built in first, then a host's. Undefined for an id nobody defines any more. */\nexport function widgetDef(id: WidgetId): WidgetDef | undefined {\n return WIDGETS[id as BuiltInWidgetId] ?? hostWidget(id);\n}\n\nexport function widgetsFor(ctx: WidgetContext): WidgetDef[] {\n return [...WIDGET_ORDER.map((id) => WIDGETS[id]), ...hostWidgets()].filter(\n (w) => w.available(ctx),\n );\n}\n"],"mappings":"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","names":["hostWidgets"]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/registry.tsx"],"sourcesContent":["'use client';\n\nimport { RevenueWidget } from './money-widgets';\nimport { ActivityCard } from './activity-card';\nimport { SetupWidget } from './setup-widget';\nimport { HardwareWidget } from './hardware-widget';\nimport { TeamWidget } from './team-widget';\nimport { CreditsWidget } from './credits-widget';\nimport { ReferralWidget } from './referral-widget';\nimport { RegularsWidget } from './regulars-widget';\nimport { OwedWidget } from './owed-widget';\nimport { BillWidget } from './bill-widget';\nimport { WebsiteWidget } from './website-widget';\nimport { isSettled, orderSteps } from './setup-steps';\nimport { ThemeMiniature, Pointer } from './widget-art';\nimport type { WidgetDef, WidgetId, WidgetContext } from './types';\n\n/**\n * The catalogue.\n *\n * Every entry here is a block that already existed on the home; making it a\n * widget is a matter of declaring which sizes it has been drawn for, not of\n * writing a new surface. That is the payoff of having taken the home apart into\n * self-contained blocks first.\n *\n * THREE OF THESE WERE DELETED FROM THE PAGE, AND THAT IS THE POINT.\n * `quick-actions` and `recents` were columns on the old console and were cut\n * for restating the sidebar and for rendering a list of settings screens you\n * had already visited. Neither earns a permanent third of the home. Both are\n * perfectly reasonable things for a particular merchant to CHOOSE, and a widget\n * board is exactly the mechanism that lets something be available without being\n * imposed. Opt-in is a different bar from default, and the two were being\n * argued as though they were one.\n *\n * `Blocked` is deliberately NOT here. See `home-widgets.tsx`.\n */\nexport const WIDGETS: Record<WidgetId, WidgetDef> = {\n /**\n * Money, as three widgets rather than one block.\n *\n * It was a single `lg`-only \"Performance\" panel, which left the `sm` and `md`\n * classes with almost nothing to offer and made the gallery look like a\n * feature with one item in it. Split, a merchant can put revenue large and\n * average order small, or drop the count entirely — which is the actual point\n * of a board.\n *\n * All three read ONE fetch and ONE window from `MoneyBoardProvider`. The\n * window is shared on purpose: revenue over thirty days beside a count over\n * seven is two true figures making one false impression.\n */\n revenue: {\n id: 'revenue',\n name: 'Revenue',\n description: 'What came in, and how it moved.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'wide',\n // Money is the one part of this page that is not merely extra detail for\n // the wrong person; it is the part they were deliberately not given.\n available: (ctx) => ctx.canSeeEarnings,\n /**\n * The last of the old \"Finish setting up\" cards, put where its result will\n * appear.\n *\n * This is the whole argument for empty states over a setup rail, and this\n * card is the clearest case of it: a business with no payout account\n * cannot be paid, so a revenue chart over it is a chart of zero drawn with\n * confidence. The card that tells you to connect an account IS the card\n * that will show the money once you have — which no separate rail can ever\n * say, because a rail disappears at exactly the moment it would have\n * become useful.\n *\n * NO DRAWING. This card carried a miniature of a payout landing in a bank,\n * with an amount and the last four digits of an account on it. Neither was\n * the merchant's: on a screen about their money, a figure that is not\n * theirs reads as theirs. The card is its sentence and its button.\n */\n covers: () => ['/dashboard/payments'],\n empty: (ctx) =>\n ctx.hasPayments && ctx.needsIdentity\n ? {\n kind: 'action',\n title: 'Get paid',\n body: 'Your bank account is connected. Verify who you are and payouts can start.',\n action: 'Verify identity',\n href: '/dashboard/payments/payouts',\n }\n : ctx.hasPayments\n ? null\n : {\n kind: 'action',\n title: 'Take payments',\n body: 'Connect a payout account so customers can pay you.',\n action: 'Set up payments',\n href: '/dashboard/payments',\n },\n render: (_ctx, size) => <RevenueWidget size={size} />,\n },\n\n website: {\n id: 'website',\n name: 'Website',\n description: 'Your storefront, and where it lives.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'narrow',\n available: () => true,\n empty: (ctx) =>\n // A shut site outranks both prompts below it: design and a domain are\n // improvements to something nobody can reach yet. When it is shut the\n // card renders itself — the preview, with \"Not open yet\" under the name.\n // It no longer carries a publish control (John, 2026-08-25); opening the\n // site is done from the Website hub the preview links into.\n !ctx.isSiteOpen\n ? null\n : !ctx.hasDesignedSite\n ? {\n kind: 'action',\n title: 'Design your site',\n body: 'Pick a look that fits your brand, then make it yours.',\n action: 'Customize',\n // The Website hub, not the editor: \"design your site\" is an\n // invitation to look at it, and the hub is where templates, domains\n // and pages hang off. Dropping someone into the canvas skips the\n // choice they came to make.\n href: '/dashboard/business',\n art: <ThemeMiniature />,\n }\n : !ctx.storefrontUrl\n ? {\n kind: 'action',\n title: 'Get a domain',\n body: 'A branded URL that’s easy to find, trust, and remember.',\n action: 'Add domain',\n href: '/dashboard/business/domains',\n art: <Pointer className=\"h-[44px] w-[44px] drop-shadow-md\" />,\n }\n : null,\n render: (ctx) => (\n <WebsiteWidget\n storefrontUrl={ctx.storefrontUrl}\n hasCustomDomain={ctx.hasCustomDomain}\n siteImage={ctx.siteImage}\n businessName={ctx.businessName}\n />\n ),\n },\n\n /**\n * REGULARS WHO HAVE GONE QUIET — the marketing card, and the third attempt.\n *\n * A campaign card wanted send and schedule endpoints that do not exist. An\n * attribution card rendered \"Direct · 100%\", a non-answer dressed as a\n * finding. This one cannot degrade that way: it names PEOPLE, and every\n * business that has sold twice to anyone has some.\n *\n * Winning a repeat customer back costs a fraction of finding a new one, and\n * for a business trading over WhatsApp the whole campaign is one message to\n * one person who already likes you.\n */\n regulars: {\n id: 'regulars',\n name: 'Gone quiet',\n description: 'Repeat customers who have stopped ordering.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n // Only once somebody HAS gone quiet. With nobody to name, the card held a\n // drawing of a customer and \"72d ago\" for a person who does not exist, on\n // a board that is meant to say only what is true of this business. Unknown\n // counts as nobody, so a list that is loading or failed draws no card.\n available: (ctx) => ctx.hasQuietRegulars === true,\n covers: () => ['/dashboard/customers'],\n render: () => <RegularsWidget />,\n },\n\n /**\n * WHO IS IN THE BUSINESS.\n *\n * Moved out of the page header and onto the board, swapping places with the\n * \"Add widget\" tile — the faces are content you look at and click into, which\n * is what a card is for, and adding a widget is a control, which is what a\n * header is for.\n */\n /**\n * WHAT IS WRONG WITH THE MACHINES.\n *\n * A till that has stopped reporting, and a drawer that did not balance.\n * Never a count of devices or a total of takings: home answers \"is anything\n * wrong\", and \"you own four tills\" is not an answer to that.\n *\n * Available only once the business owns hardware. A card that spends its life\n * saying \"no devices\" teaches people to skip it, and Hardware's own empty\n * state is where somebody finds out they can have a till.\n */\n hardware: {\n id: 'hardware',\n name: 'Hardware',\n description:\n 'A machine that has gone quiet, a drawer that was short — or a terminal you have not got yet.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n /**\n * Available whether or not the business owns any.\n *\n * It was `ctx.hasHardware`, which made the card reachable only by someone\n * who already had a terminal — and the nav row that used to do the\n * discovering went to Settings on 2026-09-03. A merchant with no hardware\n * now gets the offer here; see `HardwareWidget`, which carries the three\n * states.\n */\n available: () => true,\n covers: () => ['/dashboard/hardware'],\n render: () => <HardwareWidget />,\n },\n\n /**\n * THE TWO CARDS THAT LEFT THE RAIL (John, 2026-09-03).\n *\n * They were a stacked deck in the sidebar's foot, ported off Lovable. The\n * deck's own argument for living there — a rail can hold two standing offers\n * without spending two rows on them — quietly ignored that it spends them on\n * EVERY screen. Here a merchant can size them, move them, or throw them\n * away, which is the whole point of a board.\n *\n * `canSeeEarnings` on both: they are about money leaving the business, and\n * somebody working a shift has no business being sold to. It is the closest\n * flag the board already resolves — the context carries no\n * `canManageSettings`, and adding one for two cards would be a third\n * permission source on this page.\n */\n credits: {\n id: 'credits',\n name: 'Email credits',\n description: 'How much of this period’s email you have used.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/settings/billing/credits'],\n render: () => <CreditsWidget />,\n },\n\n referral: {\n id: 'referral',\n name: 'Refer a business',\n description: 'Your invite link, and what it has earned.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/referrals'],\n render: () => <ReferralWidget />,\n },\n\n team: {\n id: 'team',\n name: 'Team',\n description: 'Who is in the business, and what they can do.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canManageTeam,\n covers: () => ['/dashboard/settings/team'],\n render: () => <TeamWidget />,\n },\n\n /**\n * HOW FAR THROUGH SETTING UP.\n *\n * The one widget that is most useful when the board is emptiest, and the only\n * one that removes ITSELF: `available` goes false the moment every step is\n * settled, which drops it from the board and frees its slot for the add tile.\n * That is the difference between this and the setup RAIL the home was taken\n * apart from — the rail had to be dismissed, so it went on sitting there for\n * people who had already finished.\n */\n /**\n * WHAT IS INVOICED AND HAS NOT ARRIVED.\n *\n * Revenue answers \"what came in\"; this answers \"what is late\", and they are\n * different questions to a business chasing money. Narrow only — it is one\n * figure and an age, not a series.\n *\n * `canSeeEarnings`, not `canSeeAmounts`: this is the business's income, so\n * staff who can read an order total still should not see the shortfall.\n */\n owed: {\n id: 'owed',\n name: 'Money owed',\n description: 'What is invoiced and unpaid, and how old it is.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/invoices'],\n render: () => <OwedWidget />,\n },\n\n /**\n * WHAT POTTER COSTS, which is the only money on this board going the other\n * way. Revenue is what arrived and Money owed is what has not; both are the\n * customer's side. This one is the merchant's own bill, and it was three\n * clicks away behind Settings on a page whose job is \"is anything wrong\".\n *\n * `canSeeEarnings`, like the other two: what the business pays for its own\n * tools is not a staff figure.\n *\n * Opt-in, and not in any archetype default. A bill that is fine says nothing\n * new for twenty-nine days out of thirty, which is a poor claim on a\n * permanent slot but a perfectly good thing to be able to CHOOSE — which is\n * what a board is for.\n */\n bill: {\n id: 'bill',\n name: 'Box bill',\n description: 'What Box costs this period, and when it is charged.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n available: (ctx) => ctx.canSeeEarnings,\n covers: () => ['/dashboard/settings/billing'],\n render: () => <BillWidget />,\n },\n\n setup: {\n id: 'setup',\n name: 'Finish setting up',\n description: 'What is left before the business is fully live.',\n sizes: ['narrow'],\n defaultSize: 'narrow',\n // Through `orderSteps`, which is what the card itself lists: a step it\n // drops must not keep an otherwise finished card on the board.\n available: (ctx) => orderSteps(ctx.setupSteps).some((s) => !isSettled(s)),\n // Each step's own destination is already on the board as this card's list,\n // so a blocker pointing at the same place would be the same chore twice.\n covers: (ctx) =>\n orderSteps(ctx.setupSteps)\n .map((s) => s.link)\n .filter((l): l is string => !!l),\n render: (ctx) => (\n <SetupWidget steps={ctx.setupSteps} itemLabel={ctx.itemLabel} />\n ),\n },\n\n /**\n * WHAT HAPPENED, as opposed to what is waiting.\n *\n * Today and Activity are two questions and the home needs both. Merging them\n * is the mistake this avoids: one list with now as the pivot puts a booking\n * at 22:00 next to an invoice paid twenty minutes ago under one heading, so\n * the reader has to check each row's tense before knowing whether it is a\n * task. `buildActivityItems` already assembled this from six sources and the\n * desktop home had nowhere to put it.\n */\n activity: {\n id: 'activity',\n name: 'Recent activity',\n description: 'What has happened, newest first.',\n sizes: ['wide', 'narrow'],\n defaultSize: 'wide',\n available: () => true,\n /**\n * NOTHING HAPPENS UNTIL THERE IS SOMETHING TO SELL. John, 2026-08-23:\n * \"you can't have recent activities without having stuff you are selling\".\n *\n * This card used to answer an empty business with \"Nothing has happened\n * yet · Orders, payments and new customers will appear here\" — true, and a\n * `waiting` state, which is the classification this file warns about\n * getting backwards. Waiting is for a box no button can fill: revenue\n * before the first sale. This one HAS a button. A catalogue with nothing in\n * it is the reason the feed is empty, and adding the first item is the act\n * that starts it.\n *\n * No drawing: the miniature that sat here was two catalogue tiles with a\n * price on one, and a price nobody set has no place on this board.\n *\n * `itemLabel`, so a club whose sidebar says Services is not told to add a\n * product. The href is the catalogue's own new-item route, which every\n * archetype reaching this card resolves through.\n */\n empty: (ctx) =>\n ctx.hasSellable\n ? null\n : {\n kind: 'action',\n title: `Add your first ${ctx.itemLabel.toLowerCase()}`,\n body: 'Orders, payments and new customers show up here once there is something to buy.',\n action: `Add ${ctx.itemLabel.toLowerCase()}`,\n href: '/dashboard/catalog/new',\n },\n render: (ctx, size) => (\n <ActivityCard\n items={ctx.activity}\n /**\n * What is stopping you, and what is late.\n *\n * Blockers plus OVERDUE items, which is the half that was missing.\n * A blocker says \"nothing can happen until this is fixed\"; an overdue\n * invoice says \"this already should have happened\" — both are things\n * the merchant is behind on, and only the first was reaching the card.\n * Blockers still sort first because a broken payout stops every future\n * sale where one late invoice stops one payment.\n *\n * Filtered here rather than in the widget so the widget stays a\n * renderer and the page keeps deciding what counts as urgent.\n */\n blockers={[\n ...ctx.today.filter((i) => i.blocking),\n ...ctx.today.filter((i) => !i.blocking && i.overdue),\n ]}\n limit={size === 'wide' ? 6 : 4}\n />\n ),\n },\n};\n\nexport const WIDGET_ORDER: WidgetId[] = [\n 'website',\n 'revenue',\n 'regulars',\n 'team',\n 'setup',\n 'bill',\n /* Below the bill, which is what Box costs; these two are what it costs to\n REACH people and what brings people in. Both are offers rather than\n readings, so they sit under everything a merchant is actually measuring. */\n 'credits',\n 'referral',\n 'activity',\n];\n\n/**\n * Every destination the board is already showing, from placed widgets only.\n *\n * Two sources, and both are needed. A widget's `covers` is what it says when it\n * has data (Low stock lists the items); its `empty` action is what it says when\n * it does not (Revenue's whole card becomes \"Take payments\"). A chore is\n * covered either way, so both count.\n *\n * `placed` rather than every registered widget: a chore covered by a widget the\n * merchant has REMOVED is not covered at all, and suppressing the blocker for\n * it would be how the home quietly stops mentioning that you cannot be paid.\n */\nexport function coveredHrefs(\n ctx: WidgetContext,\n placed: Iterable<WidgetId>,\n): Set<string> {\n const out = new Set<string>();\n for (const id of placed) {\n const def = WIDGETS[id];\n if (!def) continue;\n for (const href of def.covers?.(ctx) ?? []) out.add(href);\n const empty = def.empty?.(ctx);\n if (empty && empty.kind === 'action') out.add(empty.href);\n }\n return out;\n}\n\nexport function widgetsFor(ctx: WidgetContext): WidgetDef[] {\n return WIDGET_ORDER.map((id) => WIDGETS[id]).filter((w) => w.available(ctx));\n}\n"],"mappings":"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","names":[]}
@@ -9,13 +9,12 @@ import { PersonActivity } from "../person-activity";
9
9
  import { Panel, PanelEmpty } from "../panel";
10
10
  import { useWorkspaceMembers } from "@withpotter/hooks/use-workspace-members";
11
11
  import { TEAM_PAGE } from "@withpotter/lib/team-page";
12
- import { memberLabel } from "./member-label";
13
12
  const ROW_LIMIT = 4;
14
13
  function TeamWidget() {
15
14
  const invite = useWorkspaceInvite();
16
15
  const [inviteOpen, setInviteOpen] = useState(false);
17
16
  const [openMember, setOpenMember] = useState(null);
18
- const { members, isError, refetch } = useWorkspaceMembers();
17
+ const { members } = useWorkspaceMembers();
19
18
  const active = members.filter((m) => m.status !== "removed");
20
19
  const shown = active.slice(0, ROW_LIMIT);
21
20
  const overflow = active.length - shown.length;
@@ -28,18 +27,7 @@ function TeamWidget() {
28
27
  className: "h-full",
29
28
  bodyClassName: "flex flex-col",
30
29
  children: [
31
- isError && active.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-[132px] flex-col items-center justify-center gap-2 px-4 py-6 text-center", children: [
32
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Couldn\u2019t load your team" }),
33
- /* @__PURE__ */ jsx(
34
- "button",
35
- {
36
- type: "button",
37
- onClick: () => void refetch(),
38
- className: "text-sm font-medium text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]",
39
- children: "Retry"
40
- }
41
- )
42
- ] }) : active.length === 0 ? /* @__PURE__ */ jsx(
30
+ active.length === 0 ? /* @__PURE__ */ jsx(
43
31
  PanelEmpty,
44
32
  {
45
33
  title: "Just you so far",
@@ -61,7 +49,7 @@ function TeamWidget() {
61
49
  className: "h-6 w-6 shrink-0 rounded-full object-cover"
62
50
  }
63
51
  ),
64
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]", children: memberLabel(m) }),
52
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]", children: m.name || m.email || "Team member" }),
65
53
  /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm capitalize text-[var(--text-muted)]", children: m.status === "invited" ? "Invited" : m.roleName ?? m.role })
66
54
  ]
67
55
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/dashboard/home/widgets/team-widget.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Plus } from 'lucide-react';\nimport { useWorkspaceInvite } from '@withpotter/lib/use-workspace-invite';\nimport { generatedAvatarUri } from '@withpotter/ui-kit/generated-avatar';\nimport { InviteMemberDialog } from '@withpotter/pages-core/dashboard/settings/team/_components/invite-member-dialog';\nimport { PersonActivity } from '../person-activity';\nimport { Panel, PanelEmpty } from '../panel';\nimport { useWorkspaceMembers } from '@withpotter/hooks/use-workspace-members';\nimport { TEAM_PAGE } from '@withpotter/lib/team-page';\nimport { memberLabel } from './member-label';\n\n/**\n * Who is in the business, on the board rather than in the header.\n *\n * IT SWAPPED PLACES WITH \"Add widget\". The header held a stack of faces and the\n * board's last slot held a dashed \"Add widget\" tile, and both were in the wrong\n * place: the faces are CONTENT — a thing you look at and click into — which is\n * what a card is for, and adding a widget is a CONTROL, which is what a header\n * is for. A control sitting in the grid it edits was always slightly odd.\n *\n * A ROSTER IS NOT A LIST OF NAMES. Each row carries what that person can do,\n * because \"who is in here\" and \"what are they allowed to touch\" are the same\n * question for an owner — and the answer is invisible everywhere else without\n * opening team settings.\n *\n * CLICKING SOMEONE OPENS WHAT THEY HAVE DONE, from the attributed team log.\n * See `PersonActivity` for why that is a different feed from the home's own\n * Activity card and could never have been a filter on it.\n */\n\n/** Rows before the rest becomes a count. Past this the card stops fitting. */\nconst ROW_LIMIT = 4;\n\nexport function TeamWidget() {\n const invite = useWorkspaceInvite();\n const [inviteOpen, setInviteOpen] = useState(false);\n const [openMember, setOpenMember] = useState<string | null>(null);\n\n const { members, isError, refetch } = useWorkspaceMembers();\n const active = members.filter((m) => m.status !== 'removed');\n const shown = active.slice(0, ROW_LIMIT);\n const overflow = active.length - shown.length;\n\n return (\n <>\n <Panel\n title=\"Team\"\n action={{ label: 'Manage', href: TEAM_PAGE }}\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {isError && active.length === 0 ? (\n <div className=\"flex min-h-[132px] flex-col items-center justify-center gap-2 px-4 py-6 text-center\">\n <p className=\"text-sm font-medium text-[var(--text-secondary)]\">\n Couldn’t load your team\n </p>\n <button\n type=\"button\"\n onClick={() => void refetch()}\n className=\"text-sm font-medium text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n Retry\n </button>\n </div>\n ) : active.length === 0 ? (\n <PanelEmpty\n title=\"Just you so far\"\n description=\"Add someone and they can take orders without seeing your takings.\"\n />\n ) : (\n <ul className=\"space-y-1\">\n {shown.map((m) => (\n <li key={m.id}>\n <button\n type=\"button\"\n onClick={() => setOpenMember(m.id)}\n className=\"-mx-2 flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={generatedAvatarUri(m.email || m.id)}\n alt=\"\"\n className=\"h-6 w-6 shrink-0 rounded-full object-cover\"\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]\">\n {memberLabel(m)}\n </span>\n {/* Invited-but-not-joined says so. Showing a role for someone\n who has never arrived implies they are working. */}\n <span className=\"shrink-0 text-sm capitalize text-[var(--text-muted)]\">\n {m.status === 'invited'\n ? 'Invited'\n : (m.roleName ?? m.role)}\n </span>\n </button>\n </li>\n ))}\n {overflow > 0 && (\n <li className=\"px-0 pt-1 text-sm text-[var(--text-muted)]\">\n and {overflow} more\n </li>\n )}\n </ul>\n )}\n\n <div className=\"mt-auto pt-3\">\n <button\n type=\"button\"\n onClick={() => setInviteOpen(true)}\n className=\"inline-flex items-center gap-2 text-sm font-medium text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <Plus />\n Invite someone\n </button>\n </div>\n </Panel>\n\n {openMember && (\n <PersonActivity\n member={active.find((m) => m.id === openMember)!}\n onClose={() => setOpenMember(null)}\n />\n )}\n\n <InviteMemberDialog\n open={inviteOpen}\n onOpenChange={setInviteOpen}\n workspaceRoles={invite.roles}\n wsInviteEmail={invite.email}\n setWsInviteEmail={invite.setEmail}\n wsInvitePhone={invite.phone}\n setWsInvitePhone={invite.setPhone}\n setWsInviteRole={invite.setRole}\n handleWsInvite={invite.invite}\n wsInviting={invite.inviting}\n />\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/team-widget.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { Plus } from 'lucide-react';\nimport { useWorkspaceInvite } from '@withpotter/lib/use-workspace-invite';\nimport { generatedAvatarUri } from '@withpotter/ui-kit/generated-avatar';\nimport { InviteMemberDialog } from '@withpotter/pages-core/dashboard/settings/team/_components/invite-member-dialog';\nimport { PersonActivity } from '../person-activity';\nimport { Panel, PanelEmpty } from '../panel';\nimport { useWorkspaceMembers } from '@withpotter/hooks/use-workspace-members';\nimport { TEAM_PAGE } from '@withpotter/lib/team-page';\n\n/**\n * Who is in the business, on the board rather than in the header.\n *\n * IT SWAPPED PLACES WITH \"Add widget\". The header held a stack of faces and the\n * board's last slot held a dashed \"Add widget\" tile, and both were in the wrong\n * place: the faces are CONTENT — a thing you look at and click into — which is\n * what a card is for, and adding a widget is a CONTROL, which is what a header\n * is for. A control sitting in the grid it edits was always slightly odd.\n *\n * A ROSTER IS NOT A LIST OF NAMES. Each row carries what that person can do,\n * because \"who is in here\" and \"what are they allowed to touch\" are the same\n * question for an owner — and the answer is invisible everywhere else without\n * opening team settings.\n *\n * CLICKING SOMEONE OPENS WHAT THEY HAVE DONE, from the attributed team log.\n * See `PersonActivity` for why that is a different feed from the home's own\n * Activity card and could never have been a filter on it.\n */\n\n/** Rows before the rest becomes a count. Past this the card stops fitting. */\nconst ROW_LIMIT = 4;\n\nexport function TeamWidget() {\n const invite = useWorkspaceInvite();\n const [inviteOpen, setInviteOpen] = useState(false);\n const [openMember, setOpenMember] = useState<string | null>(null);\n\n const { members } = useWorkspaceMembers();\n const active = members.filter((m) => m.status !== 'removed');\n const shown = active.slice(0, ROW_LIMIT);\n const overflow = active.length - shown.length;\n\n return (\n <>\n <Panel\n title=\"Team\"\n action={{ label: 'Manage', href: TEAM_PAGE }}\n className=\"h-full\"\n bodyClassName=\"flex flex-col\"\n >\n {active.length === 0 ? (\n <PanelEmpty\n title=\"Just you so far\"\n description=\"Add someone and they can take orders without seeing your takings.\"\n />\n ) : (\n <ul className=\"space-y-1\">\n {shown.map((m) => (\n <li key={m.id}>\n <button\n type=\"button\"\n onClick={() => setOpenMember(m.id)}\n className=\"-mx-2 flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={generatedAvatarUri(m.email || m.id)}\n alt=\"\"\n className=\"h-6 w-6 shrink-0 rounded-full object-cover\"\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]\">\n {m.name || m.email || 'Team member'}\n </span>\n {/* Invited-but-not-joined says so. Showing a role for someone\n who has never arrived implies they are working. */}\n <span className=\"shrink-0 text-sm capitalize text-[var(--text-muted)]\">\n {m.status === 'invited'\n ? 'Invited'\n : (m.roleName ?? m.role)}\n </span>\n </button>\n </li>\n ))}\n {overflow > 0 && (\n <li className=\"px-0 pt-1 text-sm text-[var(--text-muted)]\">\n and {overflow} more\n </li>\n )}\n </ul>\n )}\n\n <div className=\"mt-auto pt-3\">\n <button\n type=\"button\"\n onClick={() => setInviteOpen(true)}\n className=\"inline-flex items-center gap-2 text-sm font-medium text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <Plus />\n Invite someone\n </button>\n </div>\n </Panel>\n\n {openMember && (\n <PersonActivity\n member={active.find((m) => m.id === openMember)!}\n onClose={() => setOpenMember(null)}\n />\n )}\n\n <InviteMemberDialog\n open={inviteOpen}\n onOpenChange={setInviteOpen}\n workspaceRoles={invite.roles}\n wsInviteEmail={invite.email}\n setWsInviteEmail={invite.setEmail}\n wsInvitePhone={invite.phone}\n setWsInvitePhone={invite.setPhone}\n setWsInviteRole={invite.setRole}\n handleWsInvite={invite.invite}\n wsInviting={invite.inviting}\n />\n </>\n );\n}\n"],"mappings":";AA6CI,mBAQM,KAQM,YAhBZ;AA3CJ,SAAS,gBAAgB;AACzB,SAAS,YAAY;AACrB,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,OAAO,kBAAkB;AAClC,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAsB1B,MAAM,YAAY;AAEX,SAAS,aAAa;AAC3B,QAAM,SAAS,mBAAmB;AAClC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAwB,IAAI;AAEhE,QAAM,EAAE,QAAQ,IAAI,oBAAoB;AACxC,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAC3D,QAAM,QAAQ,OAAO,MAAM,GAAG,SAAS;AACvC,QAAM,WAAW,OAAO,SAAS,MAAM;AAEvC,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,QAAQ,EAAE,OAAO,UAAU,MAAM,UAAU;AAAA,QAC3C,WAAU;AAAA,QACV,eAAc;AAAA,QAEb;AAAA,iBAAO,WAAW,IACjB;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,aAAY;AAAA;AAAA,UACd,IAEA,qBAAC,QAAG,WAAU,aACX;AAAA,kBAAM,IAAI,CAAC,MACV,oBAAC,QACC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,cAAc,EAAE,EAAE;AAAA,gBACjC,WAAU;AAAA,gBAGV;AAAA;AAAA,oBAAC;AAAA;AAAA,sBACC,KAAK,mBAAmB,EAAE,SAAS,EAAE,EAAE;AAAA,sBACvC,KAAI;AAAA,sBACJ,WAAU;AAAA;AAAA,kBACZ;AAAA,kBACA,oBAAC,UAAK,WAAU,0EACb,YAAE,QAAQ,EAAE,SAAS,eACxB;AAAA,kBAGA,oBAAC,UAAK,WAAU,wDACb,YAAE,WAAW,YACV,YACC,EAAE,YAAY,EAAE,MACvB;AAAA;AAAA;AAAA,YACF,KAtBO,EAAE,EAuBX,CACD;AAAA,YACA,WAAW,KACV,qBAAC,QAAG,WAAU,8CAA6C;AAAA;AAAA,cACpD;AAAA,cAAS;AAAA,eAChB;AAAA,aAEJ;AAAA,UAGF,oBAAC,SAAI,WAAU,gBACb;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,cAAc,IAAI;AAAA,cACjC,WAAU;AAAA,cAEV;AAAA,oCAAC,QAAK;AAAA,gBAAE;AAAA;AAAA;AAAA,UAEV,GACF;AAAA;AAAA;AAAA,IACF;AAAA,IAEC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAAA,QAC9C,SAAS,MAAM,cAAc,IAAI;AAAA;AAAA,IACnC;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,gBAAgB,OAAO;AAAA,QACvB,eAAe,OAAO;AAAA,QACtB,kBAAkB,OAAO;AAAA,QACzB,eAAe,OAAO;AAAA,QACtB,kBAAkB,OAAO;AAAA,QACzB,iBAAiB,OAAO;AAAA,QACxB,gBAAgB,OAAO;AAAA,QACvB,YAAY,OAAO;AAAA;AAAA,IACrB;AAAA,KACF;AAEJ;","names":[]}
@@ -35,13 +35,7 @@ export declare const WIDGET_SPAN: Record<WidgetSize, number>;
35
35
  * theirs reads as a designed page rather than as an arrangement.
36
36
  */
37
37
  export declare const WIDGET_HEIGHT = 280;
38
- /** The widgets this package draws. */
39
- export type BuiltInWidgetId = 'website' | 'revenue' | 'regulars' | 'team' | 'hardware' | 'setup' | 'owed' | 'bill' | 'credits' | 'referral' | 'activity';
40
- /**
41
- * Any widget on a board: one of the above, or one a host registered with `registerHostWidget`
42
- * (Olive's Wallet). A saved board only ever holds the id, so a host widget needs no change here.
43
- */
44
- export type WidgetId = BuiltInWidgetId | (string & {});
38
+ export type WidgetId = 'website' | 'revenue' | 'regulars' | 'team' | 'hardware' | 'setup' | 'owed' | 'bill' | 'credits' | 'referral' | 'activity';
45
39
  /** Everything the page has already resolved, handed to every widget. */
46
40
  export interface WidgetContext {
47
41
  today: NextUpItem[];
@@ -178,12 +172,6 @@ export interface WidgetDef {
178
172
  */
179
173
  covers?: (ctx: WidgetContext) => string[];
180
174
  empty?: (ctx: WidgetContext) => WidgetEmpty | null;
181
- /**
182
- * A host's widget that joins a business's board without being asked for: it is added after the
183
- * built-in defaults, and only while the board has room (`MAX_WIDGETS`) and `available` is true.
184
- * A board a merchant has already arranged is left alone; they find it in the gallery.
185
- */
186
- byDefault?: boolean;
187
175
  }
188
176
  /** One placed widget. The saved layout is an ordered list of these. */
189
177
  export interface PlacedWidget {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/dashboard/home/widgets/types.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { WidgetEmpty } from './widget-empty';\nimport type { NextUpItem } from '../next-up-sources';\nimport type { ResolvedIntentConfig } from '@withpotter/lib/intents/types';\nimport type { RecentScreen } from '../use-recent-screens';\nimport type { DashboardStats, OutstandingInvoices } from '@withpotter/lib/api/types';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { OnboardingStep } from '@withpotter/lib/api/types';\n\n/**\n * Two card widths, because Shopify has two, and this board is theirs.\n *\n * Measured off `admin.shopify.com` rather than eyeballed. Their home is a\n * SIX-COLUMN grid with a 20px gap inside a 976px column:\n *\n * row 1 two cards spanning 3 columns each 478px wide\n * row 2 three cards spanning 2 columns each 312px wide\n * every card 320px tall\n *\n * (976 - 5x20) / 6 = 146 per column\n * span 3 -> 3x146 + 2x20 = 478 span 2 -> 2x146 + 20 = 312\n *\n * So a widget does not pick a pixel width, it picks how many of the six\n * columns it takes, and only two answers are allowed. That is the whole\n * discipline: with two widths and one height, a board cannot be arranged into\n * something that looks broken, and any card can sit in either row.\n *\n * `wide` is the 2-up row, `narrow` is the 3-up row.\n */\nexport type WidgetSize = 'wide' | 'narrow';\n\n/** Columns out of six. */\nexport const WIDGET_SPAN: Record<WidgetSize, number> = {\n wide: 3,\n narrow: 2,\n};\n\n/**\n * One height for every card, as Shopify has. A single height is what makes the\n * rows line up no matter which widget a merchant puts where, and it is why\n * theirs reads as a designed page rather than as an arrangement.\n */\nexport const WIDGET_HEIGHT = 280;\n\n/** The widgets this package draws. */\nexport type BuiltInWidgetId =\n | 'website'\n | 'revenue'\n | 'regulars'\n | 'team'\n | 'hardware'\n | 'setup'\n | 'owed'\n | 'bill'\n | 'credits'\n | 'referral'\n | 'activity';\n\n/**\n * Any widget on a board: one of the above, or one a host registered with `registerHostWidget`\n * (Olive's Wallet). A saved board only ever holds the id, so a host widget needs no change here.\n */\nexport type WidgetId = BuiltInWidgetId | (string & {});\n\n/** Everything the page has already resolved, handed to every widget. */\nexport interface WidgetContext {\n today: NextUpItem[];\n nextUpHref: string;\n /** This business's word for its queue: Orders, Bookings, Stays. */\n workLabel: string;\n multiStream: boolean;\n lowStockCount: number;\n /** QuickLinks resolves the archetype's own actions; it takes the config. */\n intentConfig: ResolvedIntentConfig;\n /** The header's primary action, so the launcher never repeats it. */\n primaryActionHref: string;\n /**\n * The archetype's word for a catalogue item, so the \"Add …\" tile is not\n * headed \"Add product\" for a club whose sidebar says Services.\n */\n itemLabel: string;\n /**\n * Take payment, which used to be a button in the page header.\n *\n * It is a callback rather than an href because it opens a flow in place —\n * `QuickPaymentLinkModal` writes a one-line invoice and hands back a link to\n * paste into WhatsApp, which is how most of these actually get collected.\n * Undefined when the viewer cannot manage finance: it creates a payable.\n */\n onTakePayment?: () => void;\n recents: RecentScreen[];\n /** What already happened, newest first. Drives the Activity widget. */\n activity: ActivityItem[];\n /**\n * The business's own image, for the Website card's preview.\n *\n * `tenant.businessLogo`, not a per-page `seo.ogImage`: the OG image lives on\n * `PageResponse` and would cost a second request to fetch a thumbnail with,\n * and a business that has set one has almost always set the other. If a true\n * per-page social image is wanted here, it is a pages fetch away.\n */\n siteImage?: string | null;\n /** Shown as the link preview's title, the way a share card names a site. */\n businessName?: string;\n /**\n * The backend onboarding checklist. Empty once everything is done, which is\n * also how the Setup widget knows to take itself off the board.\n */\n setupSteps: OnboardingStep[];\n /**\n * Whether there is anything to sell. Drives Activity's empty state.\n *\n * False only when the catalogue is KNOWN to be empty. A count that has not\n * arrived, or could not be read, is not zero: see where the home builds it.\n */\n hasSellable: boolean;\n /**\n * Whether any repeat customer has stopped ordering. Gates the Gone quiet\n * card. Filled in by the board itself, from the list that card reads, so the\n * page does not have to ask for it; undefined until that list has answered.\n */\n hasQuietRegulars?: boolean;\n /** The storefront, for the Website widget. */\n storefrontUrl?: string | null;\n hasCustomDomain: boolean;\n /**\n * Whether the storefront is reachable at all.\n *\n * Ahead of \"designed\" and \"has a domain\" in the website card's order,\n * because a shut site cannot be reached however well it is designed or\n * addressed. It used to be nowhere in this context, and the only control\n * that opened a site was a one-time modal — dismiss it and the dashboard\n * offered no route to publishing at all.\n */\n isSiteOpen: boolean;\n hasDesignedSite: boolean;\n /** Money widgets are absent, not empty, for a member without the grant. */\n canSeeEarnings: boolean;\n /**\n * Whether a payout account is connected. Drives Revenue's empty state: a\n * business that cannot be paid has no revenue to report and no chart can\n * say so, but a card can.\n */\n hasPayments: boolean;\n /** A payout account exists but its owner is not yet verified. */\n needsIdentity?: boolean;\n /** Whether this viewer may see the roster. Gates the Team widget. */\n canManageTeam: boolean;\n /**\n * Does this business own any machines?\n *\n * Gates the Hardware card. A card that spends its life saying \"no devices\"\n * teaches people to skip the card, and Hardware's own empty state is where\n * somebody finds out they can have a till in the first place.\n */\n hasHardware: boolean;\n demo?: {\n data: DashboardStats;\n prior: DashboardStats;\n outstanding: OutstandingInvoices;\n };\n}\n\nexport interface WidgetDef {\n id: WidgetId;\n /** Its name in the gallery. A noun, the way the nav names things. */\n name: string;\n /** One line, in the gallery only. Never rendered on the board. */\n description: string;\n /** Which classes this widget has actually been drawn for. */\n sizes: WidgetSize[];\n defaultSize: WidgetSize;\n /**\n * Whether this widget can say anything for this business right now. A\n * widget with nothing to show is kept OUT of the gallery rather than offered\n * and then rendering an apology.\n */\n available: (ctx: WidgetContext) => boolean;\n render: (ctx: WidgetContext, size: WidgetSize) => ReactNode;\n /**\n * What this widget shows before it has anything, or null when it has\n * something. Returning an `action` here is how setup reaches the board:\n * the chore lives in the box that will hold its result. See widget-empty.tsx\n * for why the two kinds must not be confused.\n */\n /**\n * Destinations this widget already puts on the board, so nothing else has to\n * say them again.\n *\n * THIS EXISTS BECAUSE THE HOME KEPT SAYING THINGS TWICE. \"Set up payouts\"\n * was a blocker pinned above Activity AND the Revenue card's whole empty\n * state; \"3 products low on stock\" was a blocker AND the Low stock widget.\n * Both were the same chore, drawn twice, in two different visual languages,\n * on one screen.\n *\n * Deduping by DESTINATION rather than by title is what makes it general: two\n * surfaces describing one chore will word it differently, but they cannot\n * point somewhere different and still be the same chore.\n */\n covers?: (ctx: WidgetContext) => string[];\n empty?: (ctx: WidgetContext) => WidgetEmpty | null;\n /**\n * A host's widget that joins a business's board without being asked for: it is added after the\n * built-in defaults, and only while the board has room (`MAX_WIDGETS`) and `available` is true.\n * A board a merchant has already arranged is left alone; they find it in the gallery.\n */\n byDefault?: boolean;\n}\n\n/** One placed widget. The saved layout is an ordered list of these. */\nexport interface PlacedWidget {\n id: WidgetId;\n size: WidgetSize;\n}\n"],"mappings":"AAgCO,MAAM,cAA0C;AAAA,EACrD,MAAM;AAAA,EACN,QAAQ;AACV;AAOO,MAAM,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/types.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { WidgetEmpty } from './widget-empty';\nimport type { NextUpItem } from '../next-up-sources';\nimport type { ResolvedIntentConfig } from '@withpotter/lib/intents/types';\nimport type { RecentScreen } from '../use-recent-screens';\nimport type { DashboardStats, OutstandingInvoices } from '@withpotter/lib/api/types';\nimport type { ActivityItem } from '@withpotter/components/dashboard/activity-feed';\nimport type { OnboardingStep } from '@withpotter/lib/api/types';\n\n/**\n * Two card widths, because Shopify has two, and this board is theirs.\n *\n * Measured off `admin.shopify.com` rather than eyeballed. Their home is a\n * SIX-COLUMN grid with a 20px gap inside a 976px column:\n *\n * row 1 two cards spanning 3 columns each 478px wide\n * row 2 three cards spanning 2 columns each 312px wide\n * every card 320px tall\n *\n * (976 - 5x20) / 6 = 146 per column\n * span 3 -> 3x146 + 2x20 = 478 span 2 -> 2x146 + 20 = 312\n *\n * So a widget does not pick a pixel width, it picks how many of the six\n * columns it takes, and only two answers are allowed. That is the whole\n * discipline: with two widths and one height, a board cannot be arranged into\n * something that looks broken, and any card can sit in either row.\n *\n * `wide` is the 2-up row, `narrow` is the 3-up row.\n */\nexport type WidgetSize = 'wide' | 'narrow';\n\n/** Columns out of six. */\nexport const WIDGET_SPAN: Record<WidgetSize, number> = {\n wide: 3,\n narrow: 2,\n};\n\n/**\n * One height for every card, as Shopify has. A single height is what makes the\n * rows line up no matter which widget a merchant puts where, and it is why\n * theirs reads as a designed page rather than as an arrangement.\n */\nexport const WIDGET_HEIGHT = 280;\n\nexport type WidgetId =\n | 'website'\n | 'revenue'\n | 'regulars'\n | 'team'\n | 'hardware'\n | 'setup'\n | 'owed'\n | 'bill'\n | 'credits'\n | 'referral'\n | 'activity';\n\n/** Everything the page has already resolved, handed to every widget. */\nexport interface WidgetContext {\n today: NextUpItem[];\n nextUpHref: string;\n /** This business's word for its queue: Orders, Bookings, Stays. */\n workLabel: string;\n multiStream: boolean;\n lowStockCount: number;\n /** QuickLinks resolves the archetype's own actions; it takes the config. */\n intentConfig: ResolvedIntentConfig;\n /** The header's primary action, so the launcher never repeats it. */\n primaryActionHref: string;\n /**\n * The archetype's word for a catalogue item, so the \"Add …\" tile is not\n * headed \"Add product\" for a club whose sidebar says Services.\n */\n itemLabel: string;\n /**\n * Take payment, which used to be a button in the page header.\n *\n * It is a callback rather than an href because it opens a flow in place —\n * `QuickPaymentLinkModal` writes a one-line invoice and hands back a link to\n * paste into WhatsApp, which is how most of these actually get collected.\n * Undefined when the viewer cannot manage finance: it creates a payable.\n */\n onTakePayment?: () => void;\n recents: RecentScreen[];\n /** What already happened, newest first. Drives the Activity widget. */\n activity: ActivityItem[];\n /**\n * The business's own image, for the Website card's preview.\n *\n * `tenant.businessLogo`, not a per-page `seo.ogImage`: the OG image lives on\n * `PageResponse` and would cost a second request to fetch a thumbnail with,\n * and a business that has set one has almost always set the other. If a true\n * per-page social image is wanted here, it is a pages fetch away.\n */\n siteImage?: string | null;\n /** Shown as the link preview's title, the way a share card names a site. */\n businessName?: string;\n /**\n * The backend onboarding checklist. Empty once everything is done, which is\n * also how the Setup widget knows to take itself off the board.\n */\n setupSteps: OnboardingStep[];\n /**\n * Whether there is anything to sell. Drives Activity's empty state.\n *\n * False only when the catalogue is KNOWN to be empty. A count that has not\n * arrived, or could not be read, is not zero: see where the home builds it.\n */\n hasSellable: boolean;\n /**\n * Whether any repeat customer has stopped ordering. Gates the Gone quiet\n * card. Filled in by the board itself, from the list that card reads, so the\n * page does not have to ask for it; undefined until that list has answered.\n */\n hasQuietRegulars?: boolean;\n /** The storefront, for the Website widget. */\n storefrontUrl?: string | null;\n hasCustomDomain: boolean;\n /**\n * Whether the storefront is reachable at all.\n *\n * Ahead of \"designed\" and \"has a domain\" in the website card's order,\n * because a shut site cannot be reached however well it is designed or\n * addressed. It used to be nowhere in this context, and the only control\n * that opened a site was a one-time modal — dismiss it and the dashboard\n * offered no route to publishing at all.\n */\n isSiteOpen: boolean;\n hasDesignedSite: boolean;\n /** Money widgets are absent, not empty, for a member without the grant. */\n canSeeEarnings: boolean;\n /**\n * Whether a payout account is connected. Drives Revenue's empty state: a\n * business that cannot be paid has no revenue to report and no chart can\n * say so, but a card can.\n */\n hasPayments: boolean;\n /** A payout account exists but its owner is not yet verified. */\n needsIdentity?: boolean;\n /** Whether this viewer may see the roster. Gates the Team widget. */\n canManageTeam: boolean;\n /**\n * Does this business own any machines?\n *\n * Gates the Hardware card. A card that spends its life saying \"no devices\"\n * teaches people to skip the card, and Hardware's own empty state is where\n * somebody finds out they can have a till in the first place.\n */\n hasHardware: boolean;\n demo?: {\n data: DashboardStats;\n prior: DashboardStats;\n outstanding: OutstandingInvoices;\n };\n}\n\nexport interface WidgetDef {\n id: WidgetId;\n /** Its name in the gallery. A noun, the way the nav names things. */\n name: string;\n /** One line, in the gallery only. Never rendered on the board. */\n description: string;\n /** Which classes this widget has actually been drawn for. */\n sizes: WidgetSize[];\n defaultSize: WidgetSize;\n /**\n * Whether this widget can say anything for this business right now. A\n * widget with nothing to show is kept OUT of the gallery rather than offered\n * and then rendering an apology.\n */\n available: (ctx: WidgetContext) => boolean;\n render: (ctx: WidgetContext, size: WidgetSize) => ReactNode;\n /**\n * What this widget shows before it has anything, or null when it has\n * something. Returning an `action` here is how setup reaches the board:\n * the chore lives in the box that will hold its result. See widget-empty.tsx\n * for why the two kinds must not be confused.\n */\n /**\n * Destinations this widget already puts on the board, so nothing else has to\n * say them again.\n *\n * THIS EXISTS BECAUSE THE HOME KEPT SAYING THINGS TWICE. \"Set up payouts\"\n * was a blocker pinned above Activity AND the Revenue card's whole empty\n * state; \"3 products low on stock\" was a blocker AND the Low stock widget.\n * Both were the same chore, drawn twice, in two different visual languages,\n * on one screen.\n *\n * Deduping by DESTINATION rather than by title is what makes it general: two\n * surfaces describing one chore will word it differently, but they cannot\n * point somewhere different and still be the same chore.\n */\n covers?: (ctx: WidgetContext) => string[];\n empty?: (ctx: WidgetContext) => WidgetEmpty | null;\n}\n\n/** One placed widget. The saved layout is an ordered list of these. */\nexport interface PlacedWidget {\n id: WidgetId;\n size: WidgetSize;\n}\n"],"mappings":"AAgCO,MAAM,cAA0C;AAAA,EACrD,MAAM;AAAA,EACN,QAAQ;AACV;AAOO,MAAM,gBAAgB;","names":[]}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useCallback, useMemo, useSyncExternalStore } from "react";
3
- import { hostWidgets, widgetDef } from "./registry";
3
+ import { WIDGETS } from "./registry";
4
4
  const VERSION = 8;
5
5
  function storageKey(accountId, tenantSlug) {
6
6
  return `pb_home_layout:${accountId}:${tenantSlug}`;
@@ -113,12 +113,7 @@ function defaultLayout(ctx) {
113
113
  }
114
114
  }
115
115
  if (out.length === 0) out.push(...FALLBACK);
116
- for (const h of hostWidgets()) {
117
- if (h.byDefault && !out.some((o) => o.id === h.id)) {
118
- out.push({ id: h.id, size: h.defaultSize });
119
- }
120
- }
121
- return out.filter((w) => widgetDef(w.id)?.available(ctx)).slice(0, MAX_WIDGETS);
116
+ return out.filter((w) => WIDGETS[w.id].available(ctx)).slice(0, MAX_WIDGETS);
122
117
  }
123
118
  const listeners = /* @__PURE__ */ new Set();
124
119
  function emit() {
@@ -145,7 +140,7 @@ function parseLayout(raw) {
145
140
  try {
146
141
  const parsed = JSON.parse(raw);
147
142
  if (parsed?.v !== VERSION || !Array.isArray(parsed.widgets)) return null;
148
- return parsed.widgets.filter((w) => !!widgetDef(w.id));
143
+ return parsed.widgets.filter((w) => !!WIDGETS[w.id]);
149
144
  } catch {
150
145
  return null;
151
146
  }
@@ -173,7 +168,7 @@ function useHomeLayout(accountId, tenantSlug, ctx) {
173
168
  [key]
174
169
  );
175
170
  const base = layout ?? defaultLayout(ctx);
176
- const resolved = base.filter((w) => widgetDef(w.id)?.available(ctx)).map((w, i) => ({ ...w, size: sizeForIndex(i) }));
171
+ const resolved = base.filter((w) => WIDGETS[w.id].available(ctx)).map((w, i) => ({ ...w, size: sizeForIndex(i) }));
177
172
  const add = (id, size) => {
178
173
  if (base.length >= MAX_WIDGETS || base.some((w) => w.id === id)) return;
179
174
  persist([...base, { id, size }]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/dashboard/home/widgets/use-home-layout.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useMemo, useSyncExternalStore } from 'react';\nimport type {\n PlacedWidget,\n WidgetId,\n WidgetSize,\n WidgetContext,\n} from './types';\nimport { hostWidgets, widgetDef } from './registry';\n\n/**\n * The saved board.\n *\n * \"HOW WILL THEY KNOW WHAT THEY NEED?\" — Prince Abraham, and it is the question\n * that kills most widget dashboards. The answer this file implements is that\n * MOST MERCHANTS NEVER TOUCH IT. The default has to be right on its own, and\n * configuration exists for the minority who outgrow it, exactly as on a phone\n * where most people never add a widget and the home screen is still good.\n *\n * So the default is a function of the ARCHETYPE, not a fixed list: a club\n * running courts and a shop selling things do not open the same business, and\n * `intentConfig` already knows which is which.\n *\n * WHERE THIS IS STORED, and why it is honest. Per account AND per tenant, in\n * localStorage. That is deliberate for the first cut: arranging your own home\n * is a per-person preference (an owner and a front-desk hire want different\n * boards, and they share a workspace), and no endpoint exists for per-person\n * preferences yet. The shape below is what a `PUT /me/preferences` would take\n * verbatim, so moving it server-side later changes this file and nothing else.\n *\n * The known cost, stated rather than hidden: a board arranged on a laptop does\n * not follow you to a phone. That is acceptable for an arrangement preference\n * and would not be for anything a merchant could lose work over.\n */\n\n/**\n * Bump this whenever the SHAPE of a saved board changes.\n *\n * v2: the size vocabulary went from three classes (sm/md/lg) to Shopify's two\n * (wide/narrow). A board saved under v1 carries sizes that no longer exist, and\n * because an unknown size matches neither span it silently rendered every\n * widget one column wide. A stale board must be discarded, not reinterpreted.\n *\n * v3: `setup` was removed and `website` added. A v2 board would keep a widget\n * the registry no longer defines and miss the only one that can carry the two\n * chores the setup rail used to hold.\n *\n * v4: the board became five cards and stopped defaulting to three order\n * metrics. Every v3 board was saved from a default that appended `website` to\n * an already-full five, so every one of them is six cards long with `avg-order`\n * — revenue ÷ sales, arithmetic on its own neighbours — sitting in it. That is\n * not a layout anyone chose, it is the bug persisted, so it is discarded\n * rather than trimmed: trimming would have to guess which card the merchant\n * meant to keep, and the answer for a board nobody arranged is \"the default\".\n *\n * v5: Today gave its slot to Activity, Quick actions and the order count came\n * off the defaults entirely, and the board's fifth cell became the widget\n * library rather than a widget. A v4 board leads with a card that is no longer\n * the default and carries two that were removed for saying what other cards\n * already said.\n *\n * v6: five widgets were deleted outright — Today, the order count, Average\n * order, Quick actions and Recents — so a v5 board can hold ids the registry no\n * longer defines. Revenue also became wide everywhere, and a saved board would\n * keep it narrow beside a wide Activity.\n *\n * v7: Low on stock and Money owed were deleted, and Where sales come from\n * became a default — so a v6 board can hold two ids the registry no longer\n * defines and is missing the card that fills the trio row.\n *\n * v8: Where sales come from was deleted in turn — it could only ever report\n * \"Direct · 100%\" for a business whose customers arrive by WhatsApp — and Gone\n * quiet took the marketing slot. A v7 board holds a dead id and, worse, sits at\n * four cards on a board specified to hold five.\n */\nconst VERSION = 8;\n\ninterface StoredLayout {\n v: number;\n widgets: PlacedWidget[];\n}\n\nfunction storageKey(accountId: string, tenantSlug: string) {\n return `pb_home_layout:${accountId}:${tenantSlug}`;\n}\n\n/**\n * Defaults per archetype. Five each, and never six — see `defaultLayout`.\n *\n * Keyed on the archetype keys a business actually runs, not on\n * `intentConfig.primary`, which is whichever was ticked first and not what the\n * screen is about. A business that is several things gets the union, deduped,\n * in the order below.\n *\n * SETUP IS THE FOURTH CARD, and it is the only default that removes itself:\n * `available` goes false once every step is settled, so a running business\n * never sees it and a new one opens on the thing it most needs. That frees the\n * slot for the add tile rather than leaving a hole.\n *\n * THE ORDER COUNT IS NOT A DEFAULT EITHER. Activity already lists the orders\n * themselves, by name and amount, so a card reading \"Orders 148\" beside it is\n * the same fact with the detail removed. It stays in the gallery.\n *\n * QUICK ACTIONS IS NOT A DEFAULT. Its slot is the add tile now. A launcher of\n * four links restates a sidebar that already lists every one of them, and the\n * one action that genuinely was not on the sidebar — Take payment — is pinned\n * to the rail on every screen. It stays in the gallery for anyone who wants it.\n *\n * SIX LISTED, FIVE PLACED, and the extra one is not a mistake. `defaultLayout`\n * FILTERS before it slices, so a widget that has nothing to say drops out and\n * the next in line takes its place — Setup disappears the moment a business\n * finishes setting up, and without a spare the board would quietly fall to\n * four and leave the trio row short. Team is that spare: always available,\n * never urgent, and exactly the card you want back once the chores are done.\n *\n * FIVE, AND THE SHAPE IS THE POINT: two wide over three narrow, which is the\n * grid Shopify's home uses and the one `sizeForIndex` encodes. Four widgets\n * left the second row a card short and the board looked truncated rather than\n * arranged — the trio row is only a trio if something fills it.\n *\n * THE TOP ROW IS ALWAYS ACTIVITY AND MONEY, both wide, side by side. Two\n * archetypes had Revenue narrow, which put the money card in the trio row\n * under a wide Activity — so the two things a merchant opens the home to see\n * were in different rows at different sizes for no reason a merchant could\n * name. The board's first row answers \"what happened\" and \"what did it earn\",\n * and those are a pair.\n *\n * ACTIVITY LEADS, NOT TODAY. Today answered \"what is waiting on you\"; Activity\n * answers \"what has happened\", and the second turned out to be the one a\n * merchant actually opens the home for — the waiting work is already on the\n * screen that owns it, one click away, where the history is nowhere else at\n * all. Today stays in the gallery for anyone whose day genuinely is a queue.\n *\n * The blockers Today was carrying moved WITH the slot rather than being\n * dropped: they are pinned above Activity's timeline. See `activity-widget`.\n *\n * NO BOARD LEADS WITH THREE ORDER METRICS ANY MORE. Every default used to be\n * Today + Revenue + Sales count + Average order, which is one clock and three\n * numbers all derived from the same table — and `avg-order` is the worst of\n * them, because it is literally revenue ÷ sales, arithmetic performed on the\n * two cards sitting next to it. A card whose value you can compute by looking\n * at its neighbours is not a fifth thing to know, it is the same thing again.\n * It stays in the gallery for anyone who wants it; it stops being the default.\n *\n * Website takes the freed slot, which also fixes the sixth card: it is the one\n * widget that can carry the \"design your site\" and \"get a domain\" chores, so it\n * was being APPENDED to five and making six. Now it is one of the five.\n */\nconst ARCHETYPE_DEFAULTS: Record<string, PlacedWidget[]> = {\n // Time-shaped businesses. The day IS the job, so it takes a wide card.\n 'take-bookings': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'event-tickets': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'rent-property': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'dine-in': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'food-orders': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n\n // Stock-shaped. Running out is what stops the day, so the count is on the\n // board rather than one click away in Inventory.\n 'sell-products': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n\n // Money-shaped. Nothing sits on a clock; what matters is size and mix.\n 'send-invoices': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n subscriptions: [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'digital-products': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'capture-leads': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n default: [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n};\n\n/**\n * TWO WIDE, THEN THREE NARROW. That shape is not incidental, it is the layout:\n * Shopify's home is a pair of 478px cards over a trio of 312px ones, and every\n * default here fills exactly those five slots. A merchant who never touches the\n * board gets that page; the board exists so the ones who care can change which\n * five.\n *\n * Sizes are stated per placement rather than taken from each widget's own\n * default, because the same widget belongs in a different row depending on the\n * business — a restaurant's Quick actions earn a wide card, a shop's do not.\n */\nconst FALLBACK: PlacedWidget[] = ARCHETYPE_DEFAULTS.default;\n\n/**\n * FIVE. Two wide over three narrow, and the cap is real rather than implied.\n *\n * It used to be implied, and the implication broke twice over: a multi-model\n * business takes the UNION of its archetypes' defaults, which can run past\n * five on its own, and Website was then appended on top of whatever that came\n * to. A shop that also takes bookings opened on six or seven cards — a fourth\n * card orphaned on the second row, which reads as a layout bug rather than as\n * a board.\n *\n * So the union is taken, then sliced. Order matters because the slice is what\n * enforces the shape: each archetype's list is already written most-important\n * first, so the widgets that survive are the ones that businesses running that\n * combination need most.\n */\n/**\n * FIVE widgets, and the add tile lives in whatever cell is still free.\n *\n * The board is two wide over three narrow. Defaults fill four of those five, so\n * the last narrow cell is the entry to the widget library — put a fifth widget\n * there and the tile steps aside, because at that point the board is what the\n * merchant asked for and an empty invitation beside it is clutter.\n *\n * This is a CAP, not a target: `defaultLayout` slices to it, and the gallery\n * stops offering once five are placed. See `home-widgets.tsx`.\n */\nexport const MAX_WIDGETS = 5;\n\n/** Two wide, then narrow. The grid's shape, as a rule. */\nexport function sizeForIndex(index: number): WidgetSize {\n return index < 2 ? 'wide' : 'narrow';\n}\n\nexport function defaultLayout(ctx: WidgetContext): PlacedWidget[] {\n const keys = ctx.intentConfig.all.map((c) => c.key);\n const out: PlacedWidget[] = [];\n for (const k of keys) {\n for (const w of ARCHETYPE_DEFAULTS[k] ?? []) {\n if (!out.some((o) => o.id === w.id)) out.push(w);\n }\n }\n if (out.length === 0) out.push(...FALLBACK);\n // A host's own widgets (the wallet) come after the built-in ones, so they only ever fill room\n // the defaults left, never take a place from them.\n for (const h of hostWidgets()) {\n if (h.byDefault && !out.some((o) => o.id === h.id)) {\n out.push({ id: h.id, size: h.defaultSize });\n }\n }\n // Filter BEFORE slicing: a widget the business cannot use must not consume\n // one of the five and leave the board four long.\n return out\n .filter((w) => widgetDef(w.id)?.available(ctx))\n .slice(0, MAX_WIDGETS);\n}\n\n/**\n * localStorage is an external store, so it is read through the API React\n * provides for external stores rather than mirrored into state by an effect.\n * That is not pedantry: the effect version renders the default board first and\n * replaces it a frame later, a visible flicker on every load. The server\n * snapshot is `null`, so SSR renders the archetype default and hydration swaps\n * in the saved board without a mismatch.\n */\nconst listeners = new Set<() => void>();\n\nfunction emit() {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void) {\n listeners.add(cb);\n // Another tab arranging the same board is the same event as this one doing it.\n window.addEventListener('storage', cb);\n return () => {\n listeners.delete(cb);\n window.removeEventListener('storage', cb);\n };\n}\n\nfunction readRaw(key: string | null): string | null {\n if (!key) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction parseLayout(raw: string | null): PlacedWidget[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as StoredLayout;\n if (parsed?.v !== VERSION || !Array.isArray(parsed.widgets)) return null;\n // Drop anything the registry no longer defines, so a widget deleted from\n // the catalogue cannot wedge a saved board.\n return parsed.widgets.filter((w) => !!widgetDef(w.id));\n } catch {\n // A corrupt board is not worth a broken home.\n return null;\n }\n}\n\nexport function useHomeLayout(\n accountId: string | undefined,\n tenantSlug: string | undefined,\n ctx: WidgetContext,\n) {\n const key =\n accountId && tenantSlug ? storageKey(accountId, tenantSlug) : null;\n\n // A string snapshot, so referential stability is free.\n const raw = useSyncExternalStore(\n subscribe,\n () => readRaw(key),\n () => null,\n );\n const layout = useMemo(() => parseLayout(raw), [raw]);\n\n const persist = useCallback(\n (widgets: PlacedWidget[]) => {\n if (!key) return;\n try {\n window.localStorage.setItem(\n key,\n JSON.stringify({ v: VERSION, widgets } satisfies StoredLayout),\n );\n } catch {\n // Private mode, or a full quota. Nothing is lost that matters.\n }\n emit();\n },\n [key],\n );\n\n // The board actually rendered: the saved one, or the archetype's default.\n // Widgets that have stopped having anything to say (setup finished, stock\n // replenished) fall out HERE rather than being deleted from the save, so a\n // merchant who placed one gets it back when it matters again.\n const base = layout ?? defaultLayout(ctx);\n\n /**\n * SIZE IS A FUNCTION OF POSITION, not a property a widget carries.\n *\n * The grid is two wide over three narrow, and that shape IS the layout — so\n * the first two slots are wide and everything after them is narrow, always.\n * Storing a size per widget let the two disagree: a card set narrow and then\n * dragged into the top row made that row 3 + 2 with a hole in it. Only one of\n * position and size can be authoritative, and position is the one being\n * dragged.\n *\n * It also removes a control. There is nothing to resize any more — you choose\n * where a card goes and the board decides how big that place is, which is one\n * decision instead of two that could contradict each other.\n *\n * The stored `size` is left alone rather than migrated out: it is harmless,\n * and rewriting every saved board to drop a field nothing reads is a\n * migration with no user-visible payoff.\n */\n const resolved = base\n .filter((w) => widgetDef(w.id)?.available(ctx))\n .map((w, i) => ({ ...w, size: sizeForIndex(i) }));\n\n // Plain functions, not `useCallback`. `base` is rebuilt every render by\n // design (it folds in whatever the context now makes available), so memoising\n // on it buys nothing and only tells the compiler a lie about stability.\n /**\n * Capped, and enforced HERE rather than only in the gallery's UI. A cap that\n * only exists as a disabled button is a cap a second entry point walks past\n * — and the board already has two ways in (the add tile and the gallery).\n */\n const add = (id: WidgetId, size: WidgetSize) => {\n if (base.length >= MAX_WIDGETS || base.some((w) => w.id === id)) return;\n persist([...base, { id, size }]);\n };\n\n const remove = (id: WidgetId) => persist(base.filter((w) => w.id !== id));\n\n const resize = (id: WidgetId, size: WidgetSize) =>\n persist(base.map((w) => (w.id === id ? { ...w, size } : w)));\n\n /**\n * Put one widget where another is, sliding the rest along.\n *\n * A SPLICE, NOT A SWAP, and the difference is visible. Swapping two cards\n * moves the one you dropped on all the way back to where you started, which\n * nobody asks for: dragging the last card to the front should push everything\n * down one, not fling the first card to the end. This is the same reordering\n * every list with a handle has taught people to expect.\n */\n const move = (id: WidgetId, toIndex: number) => {\n const from = base.findIndex((w) => w.id === id);\n if (from === -1) return;\n const to = Math.max(0, Math.min(base.length - 1, toIndex));\n if (from === to) return;\n const next = [...base];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n persist(next);\n };\n\n const reset = useCallback(() => {\n if (key) {\n try {\n window.localStorage.removeItem(key);\n } catch {\n /* nothing to undo */\n }\n }\n emit();\n }, [key]);\n\n return {\n widgets: resolved,\n placed: new Set(resolved.map((w) => w.id)),\n add,\n remove,\n resize,\n move,\n reset,\n /** True once this merchant has arranged their own board. */\n customised: layout != null,\n };\n}\n"],"mappings":";AAEA,SAAS,aAAa,SAAS,4BAA4B;AAO3D,SAAS,aAAa,iBAAiB;AAmEvC,MAAM,UAAU;AAOhB,SAAS,WAAW,WAAmB,YAAoB;AACzD,SAAO,kBAAkB,SAAS,IAAI,UAAU;AAClD;AAgEA,MAAM,qBAAqD;AAAA;AAAA,EAEzD,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,WAAW;AAAA,IACT,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,eAAe;AAAA,IACb,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA;AAAA;AAAA,EAIA,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA;AAAA,EAGA,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,eAAe;AAAA,IACb,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,oBAAoB;AAAA,IAClB,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,iBAAiB;AAAA,IACf,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AAAA,EACA,SAAS;AAAA,IACP,EAAE,IAAI,YAAY,MAAM,OAAO;AAAA,IAC/B,EAAE,IAAI,WAAW,MAAM,OAAO;AAAA,IAC9B,EAAE,IAAI,WAAW,MAAM,SAAS;AAAA,IAChC,EAAE,IAAI,SAAS,MAAM,SAAS;AAAA,IAC9B,EAAE,IAAI,YAAY,MAAM,SAAS;AAAA,IACjC,EAAE,IAAI,QAAQ,MAAM,SAAS;AAAA,EAC/B;AACF;AAaA,MAAM,WAA2B,mBAAmB;AA4B7C,MAAM,cAAc;AAGpB,SAAS,aAAa,OAA2B;AACtD,SAAO,QAAQ,IAAI,SAAS;AAC9B;AAEO,SAAS,cAAc,KAAoC;AAChE,QAAM,OAAO,IAAI,aAAa,IAAI,IAAI,CAAC,MAAM,EAAE,GAAG;AAClD,QAAM,MAAsB,CAAC;AAC7B,aAAW,KAAK,MAAM;AACpB,eAAW,KAAK,mBAAmB,CAAC,KAAK,CAAC,GAAG;AAC3C,UAAI,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,EAAG,KAAI,KAAK,CAAC;AAAA,IACjD;AAAA,EACF;AACA,MAAI,IAAI,WAAW,EAAG,KAAI,KAAK,GAAG,QAAQ;AAG1C,aAAW,KAAK,YAAY,GAAG;AAC7B,QAAI,EAAE,aAAa,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,GAAG;AAClD,UAAI,KAAK,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,YAAY,CAAC;AAAA,IAC5C;AAAA,EACF;AAGA,SAAO,IACJ,OAAO,CAAC,MAAM,UAAU,EAAE,EAAE,GAAG,UAAU,GAAG,CAAC,EAC7C,MAAM,GAAG,WAAW;AACzB;AAUA,MAAM,YAAY,oBAAI,IAAgB;AAEtC,SAAS,OAAO;AACd,aAAW,KAAK,UAAW,GAAE;AAC/B;AAEA,SAAS,UAAU,IAAgB;AACjC,YAAU,IAAI,EAAE;AAEhB,SAAO,iBAAiB,WAAW,EAAE;AACrC,SAAO,MAAM;AACX,cAAU,OAAO,EAAE;AACnB,WAAO,oBAAoB,WAAW,EAAE;AAAA,EAC1C;AACF;AAEA,SAAS,QAAQ,KAAmC;AAClD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,GAAG;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAY,KAA2C;AAC9D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,QAAQ,MAAM,WAAW,CAAC,MAAM,QAAQ,OAAO,OAAO,EAAG,QAAO;AAGpE,WAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC;AAAA,EACvD,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cACd,WACA,YACA,KACA;AACA,QAAM,MACJ,aAAa,aAAa,WAAW,WAAW,UAAU,IAAI;AAGhE,QAAM,MAAM;AAAA,IACV;AAAA,IACA,MAAM,QAAQ,GAAG;AAAA,IACjB,MAAM;AAAA,EACR;AACA,QAAM,SAAS,QAAQ,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC;AAEpD,QAAM,UAAU;AAAA,IACd,CAAC,YAA4B;AAC3B,UAAI,CAAC,IAAK;AACV,UAAI;AACF,eAAO,aAAa;AAAA,UAClB;AAAA,UACA,KAAK,UAAU,EAAE,GAAG,SAAS,QAAQ,CAAwB;AAAA,QAC/D;AAAA,MACF,QAAQ;AAAA,MAER;AACA,WAAK;AAAA,IACP;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAMA,QAAM,OAAO,UAAU,cAAc,GAAG;AAoBxC,QAAM,WAAW,KACd,OAAO,CAAC,MAAM,UAAU,EAAE,EAAE,GAAG,UAAU,GAAG,CAAC,EAC7C,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,GAAG,MAAM,aAAa,CAAC,EAAE,EAAE;AAUlD,QAAM,MAAM,CAAC,IAAc,SAAqB;AAC9C,QAAI,KAAK,UAAU,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAG;AACjE,YAAQ,CAAC,GAAG,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC;AAAA,EACjC;AAEA,QAAM,SAAS,CAAC,OAAiB,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAExE,QAAM,SAAS,CAAC,IAAc,SAC5B,QAAQ,KAAK,IAAI,CAAC,MAAO,EAAE,OAAO,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,CAAE,CAAC;AAW7D,QAAM,OAAO,CAAC,IAAc,YAAoB;AAC9C,UAAM,OAAO,KAAK,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE;AAC9C,QAAI,SAAS,GAAI;AACjB,UAAM,KAAK,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,GAAG,OAAO,CAAC;AACzD,QAAI,SAAS,GAAI;AACjB,UAAM,OAAO,CAAC,GAAG,IAAI;AACrB,UAAM,CAAC,KAAK,IAAI,KAAK,OAAO,MAAM,CAAC;AACnC,SAAK,OAAO,IAAI,GAAG,KAAK;AACxB,YAAQ,IAAI;AAAA,EACd;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,QAAI,KAAK;AACP,UAAI;AACF,eAAO,aAAa,WAAW,GAAG;AAAA,MACpC,QAAQ;AAAA,MAER;AAAA,IACF;AACA,SAAK;AAAA,EACP,GAAG,CAAC,GAAG,CAAC;AAER,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,IACzC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA,YAAY,UAAU;AAAA,EACxB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/dashboard/home/widgets/use-home-layout.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useMemo, useSyncExternalStore } from 'react';\nimport type {\n PlacedWidget,\n WidgetId,\n WidgetSize,\n WidgetContext,\n} from './types';\nimport { WIDGETS } from './registry';\n\n/**\n * The saved board.\n *\n * \"HOW WILL THEY KNOW WHAT THEY NEED?\" — Prince Abraham, and it is the question\n * that kills most widget dashboards. The answer this file implements is that\n * MOST MERCHANTS NEVER TOUCH IT. The default has to be right on its own, and\n * configuration exists for the minority who outgrow it, exactly as on a phone\n * where most people never add a widget and the home screen is still good.\n *\n * So the default is a function of the ARCHETYPE, not a fixed list: a club\n * running courts and a shop selling things do not open the same business, and\n * `intentConfig` already knows which is which.\n *\n * WHERE THIS IS STORED, and why it is honest. Per account AND per tenant, in\n * localStorage. That is deliberate for the first cut: arranging your own home\n * is a per-person preference (an owner and a front-desk hire want different\n * boards, and they share a workspace), and no endpoint exists for per-person\n * preferences yet. The shape below is what a `PUT /me/preferences` would take\n * verbatim, so moving it server-side later changes this file and nothing else.\n *\n * The known cost, stated rather than hidden: a board arranged on a laptop does\n * not follow you to a phone. That is acceptable for an arrangement preference\n * and would not be for anything a merchant could lose work over.\n */\n\n/**\n * Bump this whenever the SHAPE of a saved board changes.\n *\n * v2: the size vocabulary went from three classes (sm/md/lg) to Shopify's two\n * (wide/narrow). A board saved under v1 carries sizes that no longer exist, and\n * because an unknown size matches neither span it silently rendered every\n * widget one column wide. A stale board must be discarded, not reinterpreted.\n *\n * v3: `setup` was removed and `website` added. A v2 board would keep a widget\n * the registry no longer defines and miss the only one that can carry the two\n * chores the setup rail used to hold.\n *\n * v4: the board became five cards and stopped defaulting to three order\n * metrics. Every v3 board was saved from a default that appended `website` to\n * an already-full five, so every one of them is six cards long with `avg-order`\n * — revenue ÷ sales, arithmetic on its own neighbours — sitting in it. That is\n * not a layout anyone chose, it is the bug persisted, so it is discarded\n * rather than trimmed: trimming would have to guess which card the merchant\n * meant to keep, and the answer for a board nobody arranged is \"the default\".\n *\n * v5: Today gave its slot to Activity, Quick actions and the order count came\n * off the defaults entirely, and the board's fifth cell became the widget\n * library rather than a widget. A v4 board leads with a card that is no longer\n * the default and carries two that were removed for saying what other cards\n * already said.\n *\n * v6: five widgets were deleted outright — Today, the order count, Average\n * order, Quick actions and Recents — so a v5 board can hold ids the registry no\n * longer defines. Revenue also became wide everywhere, and a saved board would\n * keep it narrow beside a wide Activity.\n *\n * v7: Low on stock and Money owed were deleted, and Where sales come from\n * became a default — so a v6 board can hold two ids the registry no longer\n * defines and is missing the card that fills the trio row.\n *\n * v8: Where sales come from was deleted in turn — it could only ever report\n * \"Direct · 100%\" for a business whose customers arrive by WhatsApp — and Gone\n * quiet took the marketing slot. A v7 board holds a dead id and, worse, sits at\n * four cards on a board specified to hold five.\n */\nconst VERSION = 8;\n\ninterface StoredLayout {\n v: number;\n widgets: PlacedWidget[];\n}\n\nfunction storageKey(accountId: string, tenantSlug: string) {\n return `pb_home_layout:${accountId}:${tenantSlug}`;\n}\n\n/**\n * Defaults per archetype. Five each, and never six — see `defaultLayout`.\n *\n * Keyed on the archetype keys a business actually runs, not on\n * `intentConfig.primary`, which is whichever was ticked first and not what the\n * screen is about. A business that is several things gets the union, deduped,\n * in the order below.\n *\n * SETUP IS THE FOURTH CARD, and it is the only default that removes itself:\n * `available` goes false once every step is settled, so a running business\n * never sees it and a new one opens on the thing it most needs. That frees the\n * slot for the add tile rather than leaving a hole.\n *\n * THE ORDER COUNT IS NOT A DEFAULT EITHER. Activity already lists the orders\n * themselves, by name and amount, so a card reading \"Orders 148\" beside it is\n * the same fact with the detail removed. It stays in the gallery.\n *\n * QUICK ACTIONS IS NOT A DEFAULT. Its slot is the add tile now. A launcher of\n * four links restates a sidebar that already lists every one of them, and the\n * one action that genuinely was not on the sidebar — Take payment — is pinned\n * to the rail on every screen. It stays in the gallery for anyone who wants it.\n *\n * SIX LISTED, FIVE PLACED, and the extra one is not a mistake. `defaultLayout`\n * FILTERS before it slices, so a widget that has nothing to say drops out and\n * the next in line takes its place — Setup disappears the moment a business\n * finishes setting up, and without a spare the board would quietly fall to\n * four and leave the trio row short. Team is that spare: always available,\n * never urgent, and exactly the card you want back once the chores are done.\n *\n * FIVE, AND THE SHAPE IS THE POINT: two wide over three narrow, which is the\n * grid Shopify's home uses and the one `sizeForIndex` encodes. Four widgets\n * left the second row a card short and the board looked truncated rather than\n * arranged — the trio row is only a trio if something fills it.\n *\n * THE TOP ROW IS ALWAYS ACTIVITY AND MONEY, both wide, side by side. Two\n * archetypes had Revenue narrow, which put the money card in the trio row\n * under a wide Activity — so the two things a merchant opens the home to see\n * were in different rows at different sizes for no reason a merchant could\n * name. The board's first row answers \"what happened\" and \"what did it earn\",\n * and those are a pair.\n *\n * ACTIVITY LEADS, NOT TODAY. Today answered \"what is waiting on you\"; Activity\n * answers \"what has happened\", and the second turned out to be the one a\n * merchant actually opens the home for — the waiting work is already on the\n * screen that owns it, one click away, where the history is nowhere else at\n * all. Today stays in the gallery for anyone whose day genuinely is a queue.\n *\n * The blockers Today was carrying moved WITH the slot rather than being\n * dropped: they are pinned above Activity's timeline. See `activity-widget`.\n *\n * NO BOARD LEADS WITH THREE ORDER METRICS ANY MORE. Every default used to be\n * Today + Revenue + Sales count + Average order, which is one clock and three\n * numbers all derived from the same table — and `avg-order` is the worst of\n * them, because it is literally revenue ÷ sales, arithmetic performed on the\n * two cards sitting next to it. A card whose value you can compute by looking\n * at its neighbours is not a fifth thing to know, it is the same thing again.\n * It stays in the gallery for anyone who wants it; it stops being the default.\n *\n * Website takes the freed slot, which also fixes the sixth card: it is the one\n * widget that can carry the \"design your site\" and \"get a domain\" chores, so it\n * was being APPENDED to five and making six. Now it is one of the five.\n */\nconst ARCHETYPE_DEFAULTS: Record<string, PlacedWidget[]> = {\n // Time-shaped businesses. The day IS the job, so it takes a wide card.\n 'take-bookings': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'event-tickets': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'rent-property': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'dine-in': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'food-orders': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n\n // Stock-shaped. Running out is what stops the day, so the count is on the\n // board rather than one click away in Inventory.\n 'sell-products': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n\n // Money-shaped. Nothing sits on a clock; what matters is size and mix.\n 'send-invoices': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n subscriptions: [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'digital-products': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n 'capture-leads': [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n default: [\n { id: 'activity', size: 'wide' },\n { id: 'revenue', size: 'wide' },\n { id: 'website', size: 'narrow' },\n { id: 'setup', size: 'narrow' },\n { id: 'regulars', size: 'narrow' },\n { id: 'team', size: 'narrow' },\n ],\n};\n\n/**\n * TWO WIDE, THEN THREE NARROW. That shape is not incidental, it is the layout:\n * Shopify's home is a pair of 478px cards over a trio of 312px ones, and every\n * default here fills exactly those five slots. A merchant who never touches the\n * board gets that page; the board exists so the ones who care can change which\n * five.\n *\n * Sizes are stated per placement rather than taken from each widget's own\n * default, because the same widget belongs in a different row depending on the\n * business — a restaurant's Quick actions earn a wide card, a shop's do not.\n */\nconst FALLBACK: PlacedWidget[] = ARCHETYPE_DEFAULTS.default;\n\n/**\n * FIVE. Two wide over three narrow, and the cap is real rather than implied.\n *\n * It used to be implied, and the implication broke twice over: a multi-model\n * business takes the UNION of its archetypes' defaults, which can run past\n * five on its own, and Website was then appended on top of whatever that came\n * to. A shop that also takes bookings opened on six or seven cards — a fourth\n * card orphaned on the second row, which reads as a layout bug rather than as\n * a board.\n *\n * So the union is taken, then sliced. Order matters because the slice is what\n * enforces the shape: each archetype's list is already written most-important\n * first, so the widgets that survive are the ones that businesses running that\n * combination need most.\n */\n/**\n * FIVE widgets, and the add tile lives in whatever cell is still free.\n *\n * The board is two wide over three narrow. Defaults fill four of those five, so\n * the last narrow cell is the entry to the widget library — put a fifth widget\n * there and the tile steps aside, because at that point the board is what the\n * merchant asked for and an empty invitation beside it is clutter.\n *\n * This is a CAP, not a target: `defaultLayout` slices to it, and the gallery\n * stops offering once five are placed. See `home-widgets.tsx`.\n */\nexport const MAX_WIDGETS = 5;\n\n/** Two wide, then narrow. The grid's shape, as a rule. */\nexport function sizeForIndex(index: number): WidgetSize {\n return index < 2 ? 'wide' : 'narrow';\n}\n\nexport function defaultLayout(ctx: WidgetContext): PlacedWidget[] {\n const keys = ctx.intentConfig.all.map((c) => c.key);\n const out: PlacedWidget[] = [];\n for (const k of keys) {\n for (const w of ARCHETYPE_DEFAULTS[k] ?? []) {\n if (!out.some((o) => o.id === w.id)) out.push(w);\n }\n }\n if (out.length === 0) out.push(...FALLBACK);\n // Filter BEFORE slicing: a widget the business cannot use must not consume\n // one of the five and leave the board four long.\n return out.filter((w) => WIDGETS[w.id].available(ctx)).slice(0, MAX_WIDGETS);\n}\n\n/**\n * localStorage is an external store, so it is read through the API React\n * provides for external stores rather than mirrored into state by an effect.\n * That is not pedantry: the effect version renders the default board first and\n * replaces it a frame later, a visible flicker on every load. The server\n * snapshot is `null`, so SSR renders the archetype default and hydration swaps\n * in the saved board without a mismatch.\n */\nconst listeners = new Set<() => void>();\n\nfunction emit() {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void) {\n listeners.add(cb);\n // Another tab arranging the same board is the same event as this one doing it.\n window.addEventListener('storage', cb);\n return () => {\n listeners.delete(cb);\n window.removeEventListener('storage', cb);\n };\n}\n\nfunction readRaw(key: string | null): string | null {\n if (!key) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction parseLayout(raw: string | null): PlacedWidget[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as StoredLayout;\n if (parsed?.v !== VERSION || !Array.isArray(parsed.widgets)) return null;\n // Drop anything the registry no longer defines, so a widget deleted from\n // the catalogue cannot wedge a saved board.\n return parsed.widgets.filter((w) => !!WIDGETS[w.id]);\n } catch {\n // A corrupt board is not worth a broken home.\n return null;\n }\n}\n\nexport function useHomeLayout(\n accountId: string | undefined,\n tenantSlug: string | undefined,\n ctx: WidgetContext,\n) {\n const key =\n accountId && tenantSlug ? storageKey(accountId, tenantSlug) : null;\n\n // A string snapshot, so referential stability is free.\n const raw = useSyncExternalStore(\n subscribe,\n () => readRaw(key),\n () => null,\n );\n const layout = useMemo(() => parseLayout(raw), [raw]);\n\n const persist = useCallback(\n (widgets: PlacedWidget[]) => {\n if (!key) return;\n try {\n window.localStorage.setItem(\n key,\n JSON.stringify({ v: VERSION, widgets } satisfies StoredLayout),\n );\n } catch {\n // Private mode, or a full quota. Nothing is lost that matters.\n }\n emit();\n },\n [key],\n );\n\n // The board actually rendered: the saved one, or the archetype's default.\n // Widgets that have stopped having anything to say (setup finished, stock\n // replenished) fall out HERE rather than being deleted from the save, so a\n // merchant who placed one gets it back when it matters again.\n const base = layout ?? defaultLayout(ctx);\n\n /**\n * SIZE IS A FUNCTION OF POSITION, not a property a widget carries.\n *\n * The grid is two wide over three narrow, and that shape IS the layout — so\n * the first two slots are wide and everything after them is narrow, always.\n * Storing a size per widget let the two disagree: a card set narrow and then\n * dragged into the top row made that row 3 + 2 with a hole in it. Only one of\n * position and size can be authoritative, and position is the one being\n * dragged.\n *\n * It also removes a control. There is nothing to resize any more — you choose\n * where a card goes and the board decides how big that place is, which is one\n * decision instead of two that could contradict each other.\n *\n * The stored `size` is left alone rather than migrated out: it is harmless,\n * and rewriting every saved board to drop a field nothing reads is a\n * migration with no user-visible payoff.\n */\n const resolved = base\n .filter((w) => WIDGETS[w.id].available(ctx))\n .map((w, i) => ({ ...w, size: sizeForIndex(i) }));\n\n // Plain functions, not `useCallback`. `base` is rebuilt every render by\n // design (it folds in whatever the context now makes available), so memoising\n // on it buys nothing and only tells the compiler a lie about stability.\n /**\n * Capped, and enforced HERE rather than only in the gallery's UI. A cap that\n * only exists as a disabled button is a cap a second entry point walks past\n * — and the board already has two ways in (the add tile and the gallery).\n */\n const add = (id: WidgetId, size: WidgetSize) => {\n if (base.length >= MAX_WIDGETS || base.some((w) => w.id === id)) return;\n persist([...base, { id, size }]);\n };\n\n const remove = (id: WidgetId) => persist(base.filter((w) => w.id !== id));\n\n const resize = (id: WidgetId, size: WidgetSize) =>\n persist(base.map((w) => (w.id === id ? { ...w, size } : w)));\n\n /**\n * Put one widget where another is, sliding the rest along.\n *\n * A SPLICE, NOT A SWAP, and the difference is visible. Swapping two cards\n * moves the one you dropped on all the way back to where you started, which\n * nobody asks for: dragging the last card to the front should push everything\n * down one, not fling the first card to the end. This is the same reordering\n * every list with a handle has taught people to expect.\n */\n const move = (id: WidgetId, toIndex: number) => {\n const from = base.findIndex((w) => w.id === id);\n if (from === -1) return;\n const to = Math.max(0, Math.min(base.length - 1, toIndex));\n if (from === to) return;\n const next = [...base];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n persist(next);\n };\n\n const reset = useCallback(() => {\n if (key) {\n try {\n window.localStorage.removeItem(key);\n } catch {\n /* nothing to undo */\n }\n }\n emit();\n }, [key]);\n\n return {\n widgets: resolved,\n placed: new Set(resolved.map((w) => w.id)),\n add,\n remove,\n resize,\n move,\n reset,\n /** True once this merchant has arranged their own board. */\n customised: layout != null,\n };\n}\n"],"mappings":"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","names":[]}
@@ -14,7 +14,6 @@ import {
14
14
  import { useAuth } from "@withpotter/lib/auth-context";
15
15
  import { analyticsApi } from "@withpotter/lib/api/analytics";
16
16
  import { useApiQuery } from "@withpotter/hooks/use-api-query";
17
- import { middleEllipsis } from "./middle-ellipsis";
18
17
  import { Panel, PanelAction } from "../panel";
19
18
  function useWeeklyTraffic(enabled) {
20
19
  const iso = (d) => d.toISOString().slice(0, 10);
@@ -146,14 +145,7 @@ function WebsiteWidget({
146
145
  className: "h-[9px] w-[9px] shrink-0 text-[var(--success)]"
147
146
  }
148
147
  ),
149
- /* @__PURE__ */ jsx(
150
- "span",
151
- {
152
- title: host,
153
- className: "min-w-0 truncate text-sm text-[var(--text-tertiary)]",
154
- children: middleEllipsis(host)
155
- }
156
- )
148
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-sm text-[var(--text-tertiary)]", children: host })
157
149
  ] }) : (
158
150
  /* "Not published yet" was a FACT with no way to act on it, and
159
151
  the only control that could open the site was a modal that