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

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/mobile/screens/billing.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { MobileScreenFrame } from '@withpotter/components/mobile/screen-frame-host';\n\n/**\n * Mobile screen — Billing hub\n *\n * Visual contract: `BillingHub` in mobile-redesign-mockups.tsx\n * Route: /dashboard/settings/billing (mobile)\n */\n\nimport React from 'react';\nimport { pickPlanPrice } from '@withpotter/lib/plan-price';\nimport {\n BOX_EMAIL_ONLY,\n phoneChannelsLabel,\n} from '@withpotter/lib/box-channels';\nimport {\n ChevronRight,\n CreditCard,\n Mail,\n MessageCircle,\n Percent,\n Receipt,\n Star,\n Wallet,\n} from 'lucide-react';\nimport type {\n BillingPlan,\n Subscription,\n PaymentMethod,\n MessagingCreditsInfo,\n EmailCreditsInfo,\n} from '@withpotter/pages-core/dashboard/settings/billing/_components';\n\n/**\n * The screens this drawer can send you to, named as the billing page's mobile\n * router names them.\n *\n * It used to keep its own list, and the two drifted: this file emitted\n * `subscription` and `history` where the page renders `plan` and `usage`, so two\n * of the seven rows led nowhere. Exported, and consumed by the page's own state,\n * so a name can only be wrong in one place.\n */\nexport type MobileBillingScreenKey =\n | 'overview'\n | 'plan'\n | 'messaging'\n | 'email'\n | 'invoices'\n | 'usage'\n | 'payment';\n\ninterface MobileBillingScreenProps {\n subscription: Subscription | null | undefined;\n plans?: BillingPlan[];\n paymentMethods?: PaymentMethod[];\n invoiceCount?: number;\n messagingCredits: MessagingCreditsInfo | null | undefined;\n emailCredits: EmailCreditsInfo | null | undefined;\n formatCurrency: (n: number) => string;\n onSelectTab: (tab: MobileBillingScreenKey) => void;\n onTopUpMessaging?: () => void;\n onManagePlan?: () => void;\n}\n\nexport function MobileBillingScreen({\n subscription,\n paymentMethods,\n invoiceCount,\n messagingCredits,\n emailCredits,\n formatCurrency,\n onSelectTab,\n onTopUpMessaging,\n onManagePlan,\n}: MobileBillingScreenProps) {\n const router = useRouter();\n const plan = subscription?.plan;\n const planPrice = plan ? (pickPlanPrice(plan.prices)?.amount ?? 0) : 0;\n const status = subscription?.status ?? 'inactive';\n const renewsAt = subscription?.currentPeriod?.end ?? null;\n const defaultPM =\n paymentMethods?.find((m) => m.isDefault) ?? paymentMethods?.[0];\n\n // Messaging credits pay for SMS and WhatsApp; an email-only box has neither.\n const totalCredits =\n (BOX_EMAIL_ONLY ? 0 : (messagingCredits?.balance ?? 0)) +\n (emailCredits?.balance ?? 0);\n\n return (\n /* The frame owns the app row, the sheet, the title and the tab bar. Every\n native screen used to draw all four itself — the same root div, the same\n `env(safe-area-inset-top)`, the same title, the same `<BottomTabBar/>`,\n thirteen times and already disagreeing on padding and title size. */\n <MobileScreenFrame title={'Billing'}>\n {/* Plan card */}\n <section\n className=\"mt-6 rounded-3xl border p-4\"\n style={{\n borderColor: 'color-mix(in srgb, var(--mb-accent) 28%, transparent)',\n background: 'color-mix(in srgb, var(--mb-accent) 8%, transparent)',\n }}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p\n className=\"text-3xs font-medium\"\n style={{ color: 'var(--mb-accent)' }}\n >\n {plan?.name ?? 'No active plan'}\n </p>\n <p\n className=\"mt-1 text-4xl font-bold leading-tight tabular-nums\"\n style={{ color: 'var(--mb-text)' }}\n >\n {plan ? formatCurrency(planPrice) : '—'}\n </p>\n <p\n className=\"mt-1 text-xs\"\n style={{ color: 'var(--mb-text-secondary)' }}\n >\n {renewsAt\n ? `Renews ${new Date(renewsAt).toLocaleDateString()}`\n : 'No renewal date'}\n {defaultPM?.card\n ? ` · ${defaultPM.card.brand} ending ${defaultPM.card.last4}`\n : ''}\n </p>\n </div>\n <StatusPill status={status} />\n </div>\n <button\n type=\"button\"\n onClick={onManagePlan ?? (() => onSelectTab('plan'))}\n className=\"mt-4 flex h-11 w-full items-center justify-center gap-2 rounded-2xl text-sm font-semibold\"\n style={{\n background: 'var(--mb-accent)',\n color: 'var(--mb-accent-fg)',\n }}\n >\n Manage plan\n </button>\n </section>\n\n {/* Credits card */}\n <section\n className=\"mt-4 rounded-2xl border p-4\"\n style={{\n borderColor: 'var(--mb-border)',\n background: 'var(--mb-card)',\n }}\n >\n <div className=\"flex items-center justify-between\">\n <div>\n <p\n className=\"text-2xs\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n Credits available\n </p>\n <p\n className=\"mt-1 text-3xl font-bold leading-tight tabular-nums\"\n style={{ color: 'var(--mb-text)' }}\n >\n {totalCredits.toLocaleString()}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={\n BOX_EMAIL_ONLY\n ? () => onSelectTab('email')\n : (onTopUpMessaging ?? (() => onSelectTab('messaging')))\n }\n className=\"rounded-full px-3 py-1.5 text-2xs font-semibold\"\n style={{\n background:\n 'color-mix(in srgb, var(--mb-accent) 15%, transparent)',\n color: 'var(--mb-accent)',\n }}\n >\n Top up\n </button>\n </div>\n <div className=\"mt-4 space-y-3\">\n <CreditMeter\n label=\"Email\"\n info={emailCredits}\n color=\"var(--mb-accent)\"\n />\n {/* Named for the phone channels the box still has; none, no meter. */}\n {phoneChannelsLabel() && (\n <CreditMeter\n label={phoneChannelsLabel()!}\n info={messagingCredits}\n color=\"var(--mb-warn)\"\n />\n )}\n </div>\n </section>\n\n {/* Tools */}\n <section className=\"mt-6\">\n <Caption>Billing tools</Caption>\n <div className=\"mt-2 space-y-2\">\n <ToolRow\n icon={Star}\n label=\"Plan\"\n hint={plan?.name ?? 'Choose a plan'}\n onClick={() => onSelectTab('plan')}\n />\n {!BOX_EMAIL_ONLY && (\n <ToolRow\n icon={MessageCircle}\n label=\"Messaging credits\"\n hint={`${(messagingCredits?.balance ?? 0).toLocaleString()} available`}\n onClick={() => onSelectTab('messaging')}\n />\n )}\n <ToolRow\n icon={Mail}\n label=\"Email credits\"\n hint={`${(emailCredits?.balance ?? 0).toLocaleString()} available`}\n onClick={() => onSelectTab('email')}\n />\n <ToolRow\n icon={Receipt}\n label=\"Statements\"\n hint={`${invoiceCount ?? 0} document${invoiceCount === 1 ? '' : 's'}`}\n onClick={() => onSelectTab('invoices')}\n />\n <ToolRow\n icon={CreditCard}\n label=\"Payment method\"\n hint={\n defaultPM?.card\n ? `${defaultPM.card.brand} ending ${defaultPM.card.last4}`\n : 'Add a card'\n }\n onClick={() => onSelectTab('payment')}\n />\n <ToolRow\n icon={Percent}\n label=\"Rates & fees\"\n hint=\"Policy, preview, ledger\"\n // Rates live on the Payments hub now — what Box charges per\n // transaction belongs beside the money it comes out of, not in\n // the billing drawer.\n onClick={() => router.push('/dashboard/payments')}\n />\n <ToolRow\n icon={Wallet}\n label=\"Usage history\"\n hint=\"Past activity\"\n onClick={() => onSelectTab('usage')}\n />\n </div>\n </section>\n </MobileScreenFrame>\n );\n}\n\n// ─────────────────────────────────────────────────────────────\n\nfunction Caption({ children }: { children: React.ReactNode }) {\n return (\n <p\n className=\"text-3xs font-medium\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n {children}\n </p>\n );\n}\n\nfunction StatusPill({ status }: { status: string }) {\n const isActive = /active|trial/i.test(status);\n const isCancelled = /cancel/i.test(status);\n const color = isActive\n ? 'var(--mb-good)'\n : isCancelled\n ? 'var(--mb-warn)'\n : 'var(--mb-text-tertiary)';\n const bg = isActive\n ? 'color-mix(in srgb, var(--mb-good) 12%, transparent)'\n : isCancelled\n ? 'color-mix(in srgb, var(--mb-warn) 12%, transparent)'\n : 'var(--mb-elevated)';\n const border = isActive\n ? 'color-mix(in srgb, var(--mb-good) 36%, transparent)'\n : isCancelled\n ? 'color-mix(in srgb, var(--mb-warn) 36%, transparent)'\n : 'var(--mb-border)';\n return (\n <span\n className=\"rounded-full border px-2.5 py-1 text-3xs font-semibold capitalize\"\n style={{ borderColor: border, background: bg, color }}\n >\n {status || 'inactive'}\n </span>\n );\n}\n\nfunction CreditMeter({\n label,\n info,\n color,\n}: {\n label: string;\n info: MessagingCreditsInfo | EmailCreditsInfo | null | undefined;\n color: string;\n}) {\n const balance = info?.balance ?? 0;\n const allowance = info?.monthlyAllowance ?? 0;\n const denom = Math.max(allowance, balance, 1);\n const pct = Math.max(0, Math.min(100, Math.round((balance / denom) * 100)));\n return (\n <div>\n <div className=\"flex items-center justify-between text-2xs\">\n <span style={{ color: 'var(--mb-text-secondary)' }}>{label}</span>\n <span\n className=\"tabular-nums\"\n style={{ color: 'var(--mb-text)', fontWeight: 600 }}\n >\n {balance.toLocaleString()}\n </span>\n </div>\n <div\n className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full\"\n style={{ background: 'var(--mb-elevated)' }}\n >\n <div style={{ width: `${pct}%`, background: color, height: '100%' }} />\n </div>\n </div>\n );\n}\n\nfunction ToolRow({\n icon: Icon,\n label,\n hint,\n onClick,\n}: {\n icon: React.ComponentType<{\n className?: string;\n style?: React.CSSProperties;\n }>;\n label: string;\n hint?: string;\n onClick?: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"flex w-full items-center gap-3 rounded-2xl border p-4 text-left\"\n style={{\n borderColor: 'var(--mb-border)',\n background: 'var(--mb-card)',\n minHeight: 60,\n }}\n >\n <div\n className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl\"\n style={{ background: 'var(--mb-elevated)' }}\n >\n <Icon\n className=\"h-4 w-4\"\n style={{ color: 'var(--mb-text-secondary)' }}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p\n className=\"truncate text-sm font-medium\"\n style={{ color: 'var(--mb-text)' }}\n >\n {label}\n </p>\n {hint ? (\n <p\n className=\"mt-0.5 truncate text-2xs\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n {hint}\n </p>\n ) : null}\n </div>\n <ChevronRight\n className=\"size-4\"\n style={{ color: 'var(--mb-text-muted)' }}\n />\n </button>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/mobile/screens/billing.tsx"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { MobileScreenFrame } from '@withpotter/components/mobile/screen-frame-host';\n\n/**\n * Mobile screen — Billing hub\n *\n * Visual contract: `BillingHub` in mobile-redesign-mockups.tsx\n * Route: /dashboard/settings/billing (mobile)\n */\n\nimport React from 'react';\nimport {\n BOX_EMAIL_ONLY,\n phoneChannelsLabel,\n} from '@withpotter/lib/box-channels';\nimport {\n ChevronRight,\n CreditCard,\n Mail,\n MessageCircle,\n Percent,\n Receipt,\n Star,\n Wallet,\n} from 'lucide-react';\nimport type {\n BillingPlan,\n Subscription,\n PaymentMethod,\n MessagingCreditsInfo,\n EmailCreditsInfo,\n} from '@withpotter/pages-core/dashboard/settings/billing/_components';\n\n/**\n * The screens this drawer can send you to, named as the billing page's mobile\n * router names them.\n *\n * It used to keep its own list, and the two drifted: this file emitted\n * `subscription` and `history` where the page renders `plan` and `usage`, so two\n * of the seven rows led nowhere. Exported, and consumed by the page's own state,\n * so a name can only be wrong in one place.\n */\nexport type MobileBillingScreenKey =\n | 'overview'\n | 'plan'\n | 'messaging'\n | 'email'\n | 'invoices'\n | 'usage'\n | 'payment';\n\ninterface MobileBillingScreenProps {\n subscription: Subscription | null | undefined;\n plans?: BillingPlan[];\n paymentMethods?: PaymentMethod[];\n invoiceCount?: number;\n messagingCredits: MessagingCreditsInfo | null | undefined;\n emailCredits: EmailCreditsInfo | null | undefined;\n formatCurrency: (n: number) => string;\n onSelectTab: (tab: MobileBillingScreenKey) => void;\n onTopUpMessaging?: () => void;\n onManagePlan?: () => void;\n}\n\nexport function MobileBillingScreen({\n subscription,\n paymentMethods,\n invoiceCount,\n messagingCredits,\n emailCredits,\n formatCurrency,\n onSelectTab,\n onTopUpMessaging,\n onManagePlan,\n}: MobileBillingScreenProps) {\n const router = useRouter();\n const plan = subscription?.plan;\n const planPriceCurrency = plan ? Object.keys(plan.prices)[0] : null;\n const planPrice =\n plan && planPriceCurrency ? plan.prices[planPriceCurrency] : 0;\n const status = subscription?.status ?? 'inactive';\n const renewsAt = subscription?.currentPeriod?.end ?? null;\n const defaultPM =\n paymentMethods?.find((m) => m.isDefault) ?? paymentMethods?.[0];\n\n // Messaging credits pay for SMS and WhatsApp; an email-only box has neither.\n const totalCredits =\n (BOX_EMAIL_ONLY ? 0 : (messagingCredits?.balance ?? 0)) +\n (emailCredits?.balance ?? 0);\n\n return (\n /* The frame owns the app row, the sheet, the title and the tab bar. Every\n native screen used to draw all four itself — the same root div, the same\n `env(safe-area-inset-top)`, the same title, the same `<BottomTabBar/>`,\n thirteen times and already disagreeing on padding and title size. */\n <MobileScreenFrame title={'Billing'}>\n {/* Plan card */}\n <section\n className=\"mt-6 rounded-3xl border p-4\"\n style={{\n borderColor: 'color-mix(in srgb, var(--mb-accent) 28%, transparent)',\n background: 'color-mix(in srgb, var(--mb-accent) 8%, transparent)',\n }}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p\n className=\"text-3xs font-medium\"\n style={{ color: 'var(--mb-accent)' }}\n >\n {plan?.name ?? 'No active plan'}\n </p>\n <p\n className=\"mt-1 text-4xl font-bold leading-tight tabular-nums\"\n style={{ color: 'var(--mb-text)' }}\n >\n {plan ? formatCurrency(planPrice) : '—'}\n </p>\n <p\n className=\"mt-1 text-xs\"\n style={{ color: 'var(--mb-text-secondary)' }}\n >\n {renewsAt\n ? `Renews ${new Date(renewsAt).toLocaleDateString()}`\n : 'No renewal date'}\n {defaultPM?.card\n ? ` · ${defaultPM.card.brand} ending ${defaultPM.card.last4}`\n : ''}\n </p>\n </div>\n <StatusPill status={status} />\n </div>\n <button\n type=\"button\"\n onClick={onManagePlan ?? (() => onSelectTab('plan'))}\n className=\"mt-4 flex h-11 w-full items-center justify-center gap-2 rounded-2xl text-sm font-semibold\"\n style={{\n background: 'var(--mb-accent)',\n color: 'var(--mb-accent-fg)',\n }}\n >\n Manage plan\n </button>\n </section>\n\n {/* Credits card */}\n <section\n className=\"mt-4 rounded-2xl border p-4\"\n style={{\n borderColor: 'var(--mb-border)',\n background: 'var(--mb-card)',\n }}\n >\n <div className=\"flex items-center justify-between\">\n <div>\n <p\n className=\"text-2xs\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n Credits available\n </p>\n <p\n className=\"mt-1 text-3xl font-bold leading-tight tabular-nums\"\n style={{ color: 'var(--mb-text)' }}\n >\n {totalCredits.toLocaleString()}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={\n BOX_EMAIL_ONLY\n ? () => onSelectTab('email')\n : (onTopUpMessaging ?? (() => onSelectTab('messaging')))\n }\n className=\"rounded-full px-3 py-1.5 text-2xs font-semibold\"\n style={{\n background:\n 'color-mix(in srgb, var(--mb-accent) 15%, transparent)',\n color: 'var(--mb-accent)',\n }}\n >\n Top up\n </button>\n </div>\n <div className=\"mt-4 space-y-3\">\n <CreditMeter\n label=\"Email\"\n info={emailCredits}\n color=\"var(--mb-accent)\"\n />\n {/* Named for the phone channels the box still has; none, no meter. */}\n {phoneChannelsLabel() && (\n <CreditMeter\n label={phoneChannelsLabel()!}\n info={messagingCredits}\n color=\"var(--mb-warn)\"\n />\n )}\n </div>\n </section>\n\n {/* Tools */}\n <section className=\"mt-6\">\n <Caption>Billing tools</Caption>\n <div className=\"mt-2 space-y-2\">\n <ToolRow\n icon={Star}\n label=\"Plan\"\n hint={plan?.name ?? 'Choose a plan'}\n onClick={() => onSelectTab('plan')}\n />\n {!BOX_EMAIL_ONLY && (\n <ToolRow\n icon={MessageCircle}\n label=\"Messaging credits\"\n hint={`${(messagingCredits?.balance ?? 0).toLocaleString()} available`}\n onClick={() => onSelectTab('messaging')}\n />\n )}\n <ToolRow\n icon={Mail}\n label=\"Email credits\"\n hint={`${(emailCredits?.balance ?? 0).toLocaleString()} available`}\n onClick={() => onSelectTab('email')}\n />\n <ToolRow\n icon={Receipt}\n label=\"Statements\"\n hint={`${invoiceCount ?? 0} document${invoiceCount === 1 ? '' : 's'}`}\n onClick={() => onSelectTab('invoices')}\n />\n <ToolRow\n icon={CreditCard}\n label=\"Payment method\"\n hint={\n defaultPM?.card\n ? `${defaultPM.card.brand} ending ${defaultPM.card.last4}`\n : 'Add a card'\n }\n onClick={() => onSelectTab('payment')}\n />\n <ToolRow\n icon={Percent}\n label=\"Rates & fees\"\n hint=\"Policy, preview, ledger\"\n // Rates live on the Payments hub now — what Box charges per\n // transaction belongs beside the money it comes out of, not in\n // the billing drawer.\n onClick={() => router.push('/dashboard/payments')}\n />\n <ToolRow\n icon={Wallet}\n label=\"Usage history\"\n hint=\"Past activity\"\n onClick={() => onSelectTab('usage')}\n />\n </div>\n </section>\n </MobileScreenFrame>\n );\n}\n\n// ─────────────────────────────────────────────────────────────\n\nfunction Caption({ children }: { children: React.ReactNode }) {\n return (\n <p\n className=\"text-3xs font-medium\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n {children}\n </p>\n );\n}\n\nfunction StatusPill({ status }: { status: string }) {\n const isActive = /active|trial/i.test(status);\n const isCancelled = /cancel/i.test(status);\n const color = isActive\n ? 'var(--mb-good)'\n : isCancelled\n ? 'var(--mb-warn)'\n : 'var(--mb-text-tertiary)';\n const bg = isActive\n ? 'color-mix(in srgb, var(--mb-good) 12%, transparent)'\n : isCancelled\n ? 'color-mix(in srgb, var(--mb-warn) 12%, transparent)'\n : 'var(--mb-elevated)';\n const border = isActive\n ? 'color-mix(in srgb, var(--mb-good) 36%, transparent)'\n : isCancelled\n ? 'color-mix(in srgb, var(--mb-warn) 36%, transparent)'\n : 'var(--mb-border)';\n return (\n <span\n className=\"rounded-full border px-2.5 py-1 text-3xs font-semibold capitalize\"\n style={{ borderColor: border, background: bg, color }}\n >\n {status || 'inactive'}\n </span>\n );\n}\n\nfunction CreditMeter({\n label,\n info,\n color,\n}: {\n label: string;\n info: MessagingCreditsInfo | EmailCreditsInfo | null | undefined;\n color: string;\n}) {\n const balance = info?.balance ?? 0;\n const allowance = info?.monthlyAllowance ?? 0;\n const denom = Math.max(allowance, balance, 1);\n const pct = Math.max(0, Math.min(100, Math.round((balance / denom) * 100)));\n return (\n <div>\n <div className=\"flex items-center justify-between text-2xs\">\n <span style={{ color: 'var(--mb-text-secondary)' }}>{label}</span>\n <span\n className=\"tabular-nums\"\n style={{ color: 'var(--mb-text)', fontWeight: 600 }}\n >\n {balance.toLocaleString()}\n </span>\n </div>\n <div\n className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full\"\n style={{ background: 'var(--mb-elevated)' }}\n >\n <div style={{ width: `${pct}%`, background: color, height: '100%' }} />\n </div>\n </div>\n );\n}\n\nfunction ToolRow({\n icon: Icon,\n label,\n hint,\n onClick,\n}: {\n icon: React.ComponentType<{\n className?: string;\n style?: React.CSSProperties;\n }>;\n label: string;\n hint?: string;\n onClick?: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"flex w-full items-center gap-3 rounded-2xl border p-4 text-left\"\n style={{\n borderColor: 'var(--mb-border)',\n background: 'var(--mb-card)',\n minHeight: 60,\n }}\n >\n <div\n className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl\"\n style={{ background: 'var(--mb-elevated)' }}\n >\n <Icon\n className=\"h-4 w-4\"\n style={{ color: 'var(--mb-text-secondary)' }}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p\n className=\"truncate text-sm font-medium\"\n style={{ color: 'var(--mb-text)' }}\n >\n {label}\n </p>\n {hint ? (\n <p\n className=\"mt-0.5 truncate text-2xs\"\n style={{ color: 'var(--mb-text-tertiary)' }}\n >\n {hint}\n </p>\n ) : null}\n </div>\n <ChevronRight\n className=\"size-4\"\n style={{ color: 'var(--mb-text-muted)' }}\n />\n </button>\n );\n}\n"],"mappings":"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","names":[]}
@@ -11,7 +11,6 @@ import { useAuth } from "@withpotter/lib/auth-context";
11
11
  import { merchantNotificationsApi } from "@withpotter/lib/api/merchant-notifications";
12
12
  import { durations, eases, springs } from "@withpotter/lib/motion";
13
13
  import { SwitcherScrim } from "./shell/switcher-scrim";
14
- import { useNotificationActions } from "./shell/notification-actions";
15
14
  import { NotificationRow } from "./shell/notification-row";
16
15
  import { BellMark, bellLabel } from "./shell/bell-mark";
17
16
  function timeAgo(iso) {
@@ -79,14 +78,6 @@ function NotificationsDropdown({
79
78
  }
80
79
  setOpen((v) => !v);
81
80
  };
82
- const act = useNotificationActions();
83
- const doAction = (intent, n) => {
84
- setOpen(false);
85
- if (!n.readAt && slug) {
86
- void merchantNotificationsApi.markRead(slug, n.id).catch(() => null).then(refresh);
87
- }
88
- act?.(intent, n);
89
- };
90
81
  const openItem = (n) => {
91
82
  setOpen(false);
92
83
  if (!n.readAt && slug) {
@@ -160,8 +151,7 @@ function NotificationsDropdown({
160
151
  {
161
152
  notification: n,
162
153
  when: timeAgo(n.createdAt),
163
- onOpen: () => openItem(n),
164
- onAction: act ? doAction : void 0
154
+ onOpen: () => openItem(n)
165
155
  },
166
156
  n.id
167
157
  )) })
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notifications-dropdown.tsx"],"sourcesContent":["'use client';\n\n/**\n * The bell at the foot of the rail: everything that has happened, in one list.\n *\n * Drawn the way the Boxspace dashboard draws it (`shell/notifications-menu`\n * there). The bell always stays in view; when something is unread, a small dot\n * in the brand colour sits on its top-right corner, with a ring of the ground\n * colour so it reads as part of the icon. No count on the bell: it used to\n * carry a 16px pill in a 28px button, which covered the icon and shouted. The\n * number is in the button's label for screen readers, and the list says the\n * rest.\n *\n * The list opens upward from the bell in the same menu skin as Support\n * (`.pv-psw-menu`): a header with Mark all read, then one row per notification\n * (`shell/notification-row`). It reads the same query as the counts on the nav\n * rows (`use-nav-notifications`), so it is one request and the same data.\n * Opening an item marks it read and follows its link.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useRouter } from 'next/navigation';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useDemoMode } from '@withpotter/hooks/use-demo-mode';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { merchantNotificationsApi } from '@withpotter/lib/api/merchant-notifications';\nimport type {\n MerchantNotification,\n MerchantNotificationList,\n} from '@withpotter/lib/api/types';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { SwitcherScrim } from './shell/switcher-scrim';\nimport { useNotificationActions } from './shell/notification-actions';\nimport { NotificationRow } from './shell/notification-row';\nimport { BellMark, bellLabel } from './shell/bell-mark';\n\nfunction timeAgo(iso: string): string {\n const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n return new Date(iso).toLocaleDateString();\n}\n\nconst TRIGGER =\n 'p-2 rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--interactive-active)] transition-colors cursor-pointer';\n\nexport function NotificationsDropdown({\n tenantSlug,\n className = TRIGGER,\n}: {\n /** Defaults to the signed-in business. */\n tenantSlug?: string;\n /** The bell button's classes, so it can match the row it sits in. */\n className?: string;\n}) {\n const router = useRouter();\n const demo = useDemoMode();\n const { tenant } = useAuth();\n const queryClient = useQueryClient();\n const slug = tenantSlug ?? tenant?.slug;\n\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n const { data } = useApiQuery<MerchantNotificationList>(\n ['merchant-notifications'],\n (s) => merchantNotificationsApi.list(s, { limit: 50 }),\n { enabled: !demo, live: true },\n );\n const notifications = data?.notifications ?? [];\n const unread = notifications.filter((n) => !n.readAt).length;\n const unreadCount = Math.max(unread, data?.unreadCount ?? 0);\n\n const refresh = () =>\n queryClient.invalidateQueries({\n queryKey: ['tenant', tenant?.slug, 'merchant-notifications'],\n });\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b) setPos({ bottom: window.innerHeight - b.top + 6, left: b.left });\n }\n setOpen((v) => !v);\n };\n\n const act = useNotificationActions();\n\n // Pressing a pill does the step it names, and the news is read: closing the bell and marking it first, as opening it does.\n const doAction = (intent: string, n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n act?.(intent, n);\n };\n\n const openItem = (n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n if (n.link) router.push(n.link);\n };\n\n const markAll = () => {\n if (!slug) return;\n void merchantNotificationsApi\n .markAllRead(slug)\n .catch(() => null)\n .then(refresh);\n };\n\n const label = bellLabel(unreadCount);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n data-tour=\"notifications\"\n className={`relative ${className}`}\n title={label}\n aria-label={label}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n onClick={toggle}\n >\n <BellMark unreadCount={unreadCount} />\n </button>\n\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n ref={popRef}\n className=\"pv-psw-menu\"\n role=\"dialog\"\n aria-label=\"Notifications\"\n initial={{ opacity: 0, scale: 0.96, y: 4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n exit={{\n opacity: 0,\n scale: 0.98,\n y: 2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n bottom: pos.bottom,\n left: pos.left,\n width: 280,\n maxWidth: 'calc(100vw - 24px)',\n transformOrigin: 'bottom left',\n }}\n >\n <div className=\"flex items-center justify-between px-3 py-2\">\n <span className=\"text-md font-semibold text-[var(--text-primary)]\">\n Notifications\n </span>\n {unread > 0 && (\n <button\n type=\"button\"\n onClick={markAll}\n className=\"text-xs font-medium text-[var(--accent-primary)] transition-opacity hover:opacity-80\"\n >\n Mark all read\n </button>\n )}\n </div>\n <div className=\"pv-psw-sep\" />\n <div className=\"max-h-96 overflow-y-auto\">\n {notifications.length === 0 ? (\n <p className=\"px-3 py-8 text-center text-sm text-[var(--text-muted)]\">\n Nothing yet. Replies from support, new orders and stock\n alerts show up here.\n </p>\n ) : (\n notifications.map((n) => (\n <NotificationRow\n key={n.id}\n notification={n}\n when={timeAgo(n.createdAt)}\n onOpen={() => openItem(n)}\n onAction={act ? doAction : undefined}\n />\n ))\n )}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notifications-dropdown.tsx"],"sourcesContent":["'use client';\n\n/**\n * The bell at the foot of the rail: everything that has happened, in one list.\n *\n * Drawn the way the Boxspace dashboard draws it (`shell/notifications-menu`\n * there). The bell always stays in view; when something is unread, a small dot\n * in the brand colour sits on its top-right corner, with a ring of the ground\n * colour so it reads as part of the icon. No count on the bell: it used to\n * carry a 16px pill in a 28px button, which covered the icon and shouted. The\n * number is in the button's label for screen readers, and the list says the\n * rest.\n *\n * The list opens upward from the bell in the same menu skin as Support\n * (`.pv-psw-menu`): a header with Mark all read, then one row per notification\n * (`shell/notification-row`). It reads the same query as the counts on the nav\n * rows (`use-nav-notifications`), so it is one request and the same data.\n * Opening an item marks it read and follows its link.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useRouter } from 'next/navigation';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useDemoMode } from '@withpotter/hooks/use-demo-mode';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { merchantNotificationsApi } from '@withpotter/lib/api/merchant-notifications';\nimport type {\n MerchantNotification,\n MerchantNotificationList,\n} from '@withpotter/lib/api/types';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { SwitcherScrim } from './shell/switcher-scrim';\nimport { NotificationRow } from './shell/notification-row';\nimport { BellMark, bellLabel } from './shell/bell-mark';\n\nfunction timeAgo(iso: string): string {\n const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n return new Date(iso).toLocaleDateString();\n}\n\nconst TRIGGER =\n 'p-2 rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--interactive-active)] transition-colors cursor-pointer';\n\nexport function NotificationsDropdown({\n tenantSlug,\n className = TRIGGER,\n}: {\n /** Defaults to the signed-in business. */\n tenantSlug?: string;\n /** The bell button's classes, so it can match the row it sits in. */\n className?: string;\n}) {\n const router = useRouter();\n const demo = useDemoMode();\n const { tenant } = useAuth();\n const queryClient = useQueryClient();\n const slug = tenantSlug ?? tenant?.slug;\n\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n const { data } = useApiQuery<MerchantNotificationList>(\n ['merchant-notifications'],\n (s) => merchantNotificationsApi.list(s, { limit: 50 }),\n { enabled: !demo, live: true },\n );\n const notifications = data?.notifications ?? [];\n const unread = notifications.filter((n) => !n.readAt).length;\n const unreadCount = Math.max(unread, data?.unreadCount ?? 0);\n\n const refresh = () =>\n queryClient.invalidateQueries({\n queryKey: ['tenant', tenant?.slug, 'merchant-notifications'],\n });\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b) setPos({ bottom: window.innerHeight - b.top + 6, left: b.left });\n }\n setOpen((v) => !v);\n };\n\n const openItem = (n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n if (n.link) router.push(n.link);\n };\n\n const markAll = () => {\n if (!slug) return;\n void merchantNotificationsApi\n .markAllRead(slug)\n .catch(() => null)\n .then(refresh);\n };\n\n const label = bellLabel(unreadCount);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n data-tour=\"notifications\"\n className={`relative ${className}`}\n title={label}\n aria-label={label}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n onClick={toggle}\n >\n <BellMark unreadCount={unreadCount} />\n </button>\n\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n ref={popRef}\n className=\"pv-psw-menu\"\n role=\"dialog\"\n aria-label=\"Notifications\"\n initial={{ opacity: 0, scale: 0.96, y: 4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n exit={{\n opacity: 0,\n scale: 0.98,\n y: 2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n bottom: pos.bottom,\n left: pos.left,\n width: 280,\n maxWidth: 'calc(100vw - 24px)',\n transformOrigin: 'bottom left',\n }}\n >\n <div className=\"flex items-center justify-between px-3 py-2\">\n <span className=\"text-md font-semibold text-[var(--text-primary)]\">\n Notifications\n </span>\n {unread > 0 && (\n <button\n type=\"button\"\n onClick={markAll}\n className=\"text-xs font-medium text-[var(--accent-primary)] transition-opacity hover:opacity-80\"\n >\n Mark all read\n </button>\n )}\n </div>\n <div className=\"pv-psw-sep\" />\n <div className=\"max-h-96 overflow-y-auto\">\n {notifications.length === 0 ? (\n <p className=\"px-3 py-8 text-center text-sm text-[var(--text-muted)]\">\n Nothing yet. Replies from support, new orders and stock\n alerts show up here.\n </p>\n ) : (\n notifications.map((n) => (\n <NotificationRow\n key={n.id}\n notification={n}\n when={timeAgo(n.createdAt)}\n onOpen={() => openItem(n)}\n />\n ))\n )}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AA+II,mBAYI,KA8BQ,YA1CZ;AA3HJ,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,cAAc;AACxC,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AACxB,SAAS,gCAAgC;AAKzC,SAAS,WAAW,OAAO,eAAe;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,UAAU,iBAAiB;AAEpC,SAAS,QAAQ,KAAqB;AACpC,QAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,EAAE,QAAQ,KAAK,GAAI;AACxE,MAAI,UAAU,GAAI,QAAO;AACzB,QAAM,UAAU,KAAK,MAAM,UAAU,EAAE;AACvC,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,MAAI,QAAQ,GAAI,QAAO,GAAG,KAAK;AAC/B,QAAM,OAAO,KAAK,MAAM,QAAQ,EAAE;AAClC,MAAI,OAAO,EAAG,QAAO,GAAG,IAAI;AAC5B,SAAO,IAAI,KAAK,GAAG,EAAE,mBAAmB;AAC1C;AAEA,MAAM,UACJ;AAEK,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA,YAAY;AACd,GAKG;AACD,QAAM,SAAS,UAAU;AACzB,QAAM,OAAO,YAAY;AACzB,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,cAAc,eAAe;AACnC,QAAM,OAAO,cAAc,QAAQ;AAEnC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,EAAE,QAAQ,GAAG,MAAM,EAAE,CAAC;AACrD,QAAM,SAAS,OAA0B,IAAI;AAC7C,QAAM,SAAS,OAAuB,IAAI;AAE1C,QAAM,EAAE,KAAK,IAAI;AAAA,IACf,CAAC,wBAAwB;AAAA,IACzB,CAAC,MAAM,yBAAyB,KAAK,GAAG,EAAE,OAAO,GAAG,CAAC;AAAA,IACrD,EAAE,SAAS,CAAC,MAAM,MAAM,KAAK;AAAA,EAC/B;AACA,QAAM,gBAAgB,MAAM,iBAAiB,CAAC;AAC9C,QAAM,SAAS,cAAc,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE;AACtD,QAAM,cAAc,KAAK,IAAI,QAAQ,MAAM,eAAe,CAAC;AAE3D,QAAM,UAAU,MACd,YAAY,kBAAkB;AAAA,IAC5B,UAAU,CAAC,UAAU,QAAQ,MAAM,wBAAwB;AAAA,EAC7D,CAAC;AAEH,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,MAAkB;AAC/B,UACE,OAAO,SAAS,SAAS,EAAE,MAAc,KACzC,OAAO,SAAS,SAAS,EAAE,MAAc;AAEzC;AACF,cAAQ,KAAK;AAAA,IACf;AACA,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,cAAQ,KAAK;AACb,aAAO,SAAS,MAAM;AAAA,IACxB;AACA,UAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,aAAS,iBAAiB,aAAa,KAAK;AAC5C,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,iBAAiB,UAAU,OAAO;AACzC,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,KAAK;AAC/C,aAAO,oBAAoB,WAAW,KAAK;AAC3C,aAAO,oBAAoB,UAAU,OAAO;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,OAAO,SAAS,sBAAsB;AAChD,UAAI,EAAG,QAAO,EAAE,QAAQ,OAAO,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,CAAC;AAAA,IACxE;AACA,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAEA,QAAM,WAAW,CAAC,MAA4B;AAC5C,YAAQ,KAAK;AACb,QAAI,CAAC,EAAE,UAAU,MAAM;AACrB,WAAK,yBACF,SAAS,MAAM,EAAE,EAAE,EACnB,MAAM,MAAM,IAAI,EAChB,KAAK,OAAO;AAAA,IACjB;AACA,QAAI,EAAE,KAAM,QAAO,KAAK,EAAE,IAAI;AAAA,EAChC;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,SAAK,yBACF,YAAY,IAAI,EAChB,MAAM,MAAM,IAAI,EAChB,KAAK,OAAO;AAAA,EACjB;AAEA,QAAM,QAAQ,UAAU,WAAW;AAEnC,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,KAAK;AAAA,QACL,aAAU;AAAA,QACV,WAAW,YAAY,SAAS;AAAA,QAChC,OAAO;AAAA,QACP,cAAY;AAAA,QACZ,iBAAc;AAAA,QACd,iBAAe;AAAA,QACf,SAAS;AAAA,QAET,8BAAC,YAAS,aAA0B;AAAA;AAAA,IACtC;AAAA,IAEC,OAAO,aAAa,eACnB;AAAA,MACE,qBAAC,mBACE;AAAA,gBAAQ,oBAAC,mBAAkB,OAAQ;AAAA,QACnC,QACC;AAAA,UAAC,OAAO;AAAA,UAAP;AAAA,YACC,KAAK;AAAA,YACL,WAAU;AAAA,YACV,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS,EAAE,SAAS,GAAG,OAAO,MAAM,GAAG,EAAE;AAAA,YACzC,SAAS,EAAE,SAAS,GAAG,OAAO,GAAG,GAAG,EAAE;AAAA,YACtC,YAAY,QAAQ;AAAA,YACpB,MAAM;AAAA,cACJ,SAAS;AAAA,cACT,OAAO;AAAA,cACP,GAAG;AAAA,cACH,YAAY,EAAE,UAAU,UAAU,MAAM,MAAM,MAAM,IAAI;AAAA,YAC1D;AAAA,YACA,OAAO;AAAA,cACL,QAAQ,IAAI;AAAA,cACZ,MAAM,IAAI;AAAA,cACV,OAAO;AAAA,cACP,UAAU;AAAA,cACV,iBAAiB;AAAA,YACnB;AAAA,YAEA;AAAA,mCAAC,SAAI,WAAU,+CACb;AAAA,oCAAC,UAAK,WAAU,oDAAmD,2BAEnE;AAAA,gBACC,SAAS,KACR;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,WAAU;AAAA,oBACX;AAAA;AAAA,gBAED;AAAA,iBAEJ;AAAA,cACA,oBAAC,SAAI,WAAU,cAAa;AAAA,cAC5B,oBAAC,SAAI,WAAU,4BACZ,wBAAc,WAAW,IACxB,oBAAC,OAAE,WAAU,0DAAyD,0FAGtE,IAEA,cAAc,IAAI,CAAC,MACjB;AAAA,gBAAC;AAAA;AAAA,kBAEC,cAAc;AAAA,kBACd,MAAM,QAAQ,EAAE,SAAS;AAAA,kBACzB,QAAQ,MAAM,SAAS,CAAC;AAAA;AAAA,gBAHnB,EAAE;AAAA,cAIT,CACD,GAEL;AAAA;AAAA;AAAA,QACF;AAAA,SAEJ;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
@@ -7,15 +7,6 @@ export interface NotificationKind {
7
7
  label: string;
8
8
  tone: NotificationTone;
9
9
  }
10
- /**
11
- * How a notification of this type is drawn. A type nobody has styled is still a bell. A plan notice
12
- * (`subscription.*`) carries its own tone, since the same kind can be good news or a warning.
13
- */
14
- export declare function notificationKind(type: string, metadata?: Record<string, unknown> | null): NotificationKind;
15
- /** The next step a notification offers on its row: a label for the pill and the intent it stands for. */
16
- export interface NotificationAction {
17
- label: string;
18
- intent: string;
19
- }
20
- export declare function notificationAction(metadata?: Record<string, unknown> | null): NotificationAction | null;
10
+ /** How a notification of this type is drawn. A type nobody has styled is still a bell. */
11
+ export declare function notificationKind(type: string): NotificationKind;
21
12
  export declare const TONE_DOT: Record<NotificationTone, string>;
@@ -2,7 +2,6 @@ import {
2
2
  Bell,
3
3
  CalendarCheck,
4
4
  Clock,
5
- CreditCard,
6
5
  Inbox,
7
6
  LifeBuoy,
8
7
  Package,
@@ -33,22 +32,9 @@ const KINDS = {
33
32
  "delivery.declined": { Icon: UserX, label: "Delivery", tone: "warning" },
34
33
  "delivery.not_accepted": { Icon: Clock, label: "Delivery", tone: "warning" }
35
34
  };
36
- const TONES = /* @__PURE__ */ new Set(["good", "warning", "info", "neutral"]);
37
- function notificationKind(type, metadata) {
38
- if (type.startsWith("subscription.")) {
39
- const tone = metadata?.tone;
40
- return {
41
- Icon: CreditCard,
42
- label: "Plan",
43
- tone: typeof tone === "string" && TONES.has(tone) ? tone : "neutral"
44
- };
45
- }
35
+ function notificationKind(type) {
46
36
  return KINDS[type] ?? { Icon: Bell, label: "Update", tone: "neutral" };
47
37
  }
48
- function notificationAction(metadata) {
49
- const action = metadata?.action;
50
- return action && typeof action.label === "string" && typeof action.intent === "string" ? { label: action.label, intent: action.intent } : null;
51
- }
52
38
  const TONE_DOT = {
53
39
  good: "var(--success)",
54
40
  warning: "var(--warning)",
@@ -57,7 +43,6 @@ const TONE_DOT = {
57
43
  };
58
44
  export {
59
45
  TONE_DOT,
60
- notificationAction,
61
46
  notificationKind
62
47
  };
63
48
  //# sourceMappingURL=notification-kinds.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notification-kinds.ts"],"sourcesContent":["import {\n Bell,\n CalendarCheck,\n Clock,\n CreditCard,\n Inbox,\n LifeBuoy,\n Package,\n PackageCheck,\n PackageX,\n ShoppingBag,\n UserPlus,\n UserX,\n type LucideIcon,\n} from 'lucide-react';\n\n/** The colour of the dot on a notification's tile: what kind of news it is. */\nexport type NotificationTone = 'good' | 'warning' | 'info' | 'neutral';\n\nexport interface NotificationKind {\n Icon: LucideIcon;\n /** The context line under the title, before the time. */\n label: string;\n tone: NotificationTone;\n}\n\nconst KINDS: Record<string, NotificationKind> = {\n 'merchant.new_order': { Icon: ShoppingBag, label: 'Order', tone: 'good' },\n 'booking.new_alert': { Icon: CalendarCheck, label: 'Booking', tone: 'good' },\n 'merchant.low_stock': { Icon: Package, label: 'Stock', tone: 'warning' },\n 'merchant.support_reply': {\n Icon: LifeBuoy,\n label: 'Support',\n tone: 'info',\n },\n 'merchant.new_inquiry': { Icon: Inbox, label: 'Enquiry', tone: 'info' },\n 'merchant.team_member_joined': {\n Icon: UserPlus,\n label: 'Team',\n tone: 'neutral',\n },\n // A shop's own riders. Each links to the order (`/dashboard/orders/:id`).\n 'delivery.delivered': { Icon: PackageCheck, label: 'Delivery', tone: 'good' },\n 'delivery.failed': { Icon: PackageX, label: 'Delivery', tone: 'warning' },\n 'delivery.declined': { Icon: UserX, label: 'Delivery', tone: 'warning' },\n 'delivery.not_accepted': { Icon: Clock, label: 'Delivery', tone: 'warning' },\n};\n\nconst TONES = new Set<string>(['good', 'warning', 'info', 'neutral']);\n\n/**\n * How a notification of this type is drawn. A type nobody has styled is still a bell. A plan notice\n * (`subscription.*`) carries its own tone, since the same kind can be good news or a warning.\n */\nexport function notificationKind(\n type: string,\n metadata?: Record<string, unknown> | null,\n): NotificationKind {\n if (type.startsWith('subscription.')) {\n const tone = metadata?.tone;\n\n return {\n Icon: CreditCard,\n label: 'Plan',\n tone: typeof tone === 'string' && TONES.has(tone) ? (tone as NotificationTone) : 'neutral',\n };\n }\n\n return KINDS[type] ?? { Icon: Bell, label: 'Update', tone: 'neutral' };\n}\n\n/** The next step a notification offers on its row: a label for the pill and the intent it stands for. */\nexport interface NotificationAction {\n label: string;\n intent: string;\n}\n\nexport function notificationAction(\n metadata?: Record<string, unknown> | null,\n): NotificationAction | null {\n const action = metadata?.action as { label?: unknown; intent?: unknown } | null | undefined;\n\n return action && typeof action.label === 'string' && typeof action.intent === 'string'\n ? { label: action.label, intent: action.intent }\n : null;\n}\n\nexport const TONE_DOT: Record<NotificationTone, string> = {\n good: 'var(--success)',\n warning: 'var(--warning)',\n info: 'var(--brand-500)',\n neutral: 'var(--text-muted)',\n};\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAYP,MAAM,QAA0C;AAAA,EAC9C,sBAAsB,EAAE,MAAM,aAAa,OAAO,SAAS,MAAM,OAAO;AAAA,EACxE,qBAAqB,EAAE,MAAM,eAAe,OAAO,WAAW,MAAM,OAAO;AAAA,EAC3E,sBAAsB,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,UAAU;AAAA,EACvE,0BAA0B;AAAA,IACxB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,wBAAwB,EAAE,MAAM,OAAO,OAAO,WAAW,MAAM,OAAO;AAAA,EACtE,+BAA+B;AAAA,IAC7B,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA;AAAA,EAEA,sBAAsB,EAAE,MAAM,cAAc,OAAO,YAAY,MAAM,OAAO;AAAA,EAC5E,mBAAmB,EAAE,MAAM,UAAU,OAAO,YAAY,MAAM,UAAU;AAAA,EACxE,qBAAqB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAAA,EACvE,yBAAyB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAC7E;AAEA,MAAM,QAAQ,oBAAI,IAAY,CAAC,QAAQ,WAAW,QAAQ,SAAS,CAAC;AAM7D,SAAS,iBACd,MACA,UACkB;AAClB,MAAI,KAAK,WAAW,eAAe,GAAG;AACpC,UAAM,OAAO,UAAU;AAEvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,MAAM,OAAO,SAAS,YAAY,MAAM,IAAI,IAAI,IAAK,OAA4B;AAAA,IACnF;AAAA,EACF;AAEA,SAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,OAAO,UAAU,MAAM,UAAU;AACvE;AAQO,SAAS,mBACd,UAC2B;AAC3B,QAAM,SAAS,UAAU;AAEzB,SAAO,UAAU,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,WAC1E,EAAE,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,IAC7C;AACN;AAEO,MAAM,WAA6C;AAAA,EACxD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AACX;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notification-kinds.ts"],"sourcesContent":["import {\n Bell,\n CalendarCheck,\n Clock,\n Inbox,\n LifeBuoy,\n Package,\n PackageCheck,\n PackageX,\n ShoppingBag,\n UserPlus,\n UserX,\n type LucideIcon,\n} from 'lucide-react';\n\n/** The colour of the dot on a notification's tile: what kind of news it is. */\nexport type NotificationTone = 'good' | 'warning' | 'info' | 'neutral';\n\nexport interface NotificationKind {\n Icon: LucideIcon;\n /** The context line under the title, before the time. */\n label: string;\n tone: NotificationTone;\n}\n\nconst KINDS: Record<string, NotificationKind> = {\n 'merchant.new_order': { Icon: ShoppingBag, label: 'Order', tone: 'good' },\n 'booking.new_alert': { Icon: CalendarCheck, label: 'Booking', tone: 'good' },\n 'merchant.low_stock': { Icon: Package, label: 'Stock', tone: 'warning' },\n 'merchant.support_reply': {\n Icon: LifeBuoy,\n label: 'Support',\n tone: 'info',\n },\n 'merchant.new_inquiry': { Icon: Inbox, label: 'Enquiry', tone: 'info' },\n 'merchant.team_member_joined': {\n Icon: UserPlus,\n label: 'Team',\n tone: 'neutral',\n },\n // A shop's own riders. Each links to the order (`/dashboard/orders/:id`).\n 'delivery.delivered': { Icon: PackageCheck, label: 'Delivery', tone: 'good' },\n 'delivery.failed': { Icon: PackageX, label: 'Delivery', tone: 'warning' },\n 'delivery.declined': { Icon: UserX, label: 'Delivery', tone: 'warning' },\n 'delivery.not_accepted': { Icon: Clock, label: 'Delivery', tone: 'warning' },\n};\n\n/** How a notification of this type is drawn. A type nobody has styled is still a bell. */\nexport function notificationKind(type: string): NotificationKind {\n return KINDS[type] ?? { Icon: Bell, label: 'Update', tone: 'neutral' };\n}\n\nexport const TONE_DOT: Record<NotificationTone, string> = {\n good: 'var(--success)',\n warning: 'var(--warning)',\n info: 'var(--brand-500)',\n neutral: 'var(--text-muted)',\n};\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAYP,MAAM,QAA0C;AAAA,EAC9C,sBAAsB,EAAE,MAAM,aAAa,OAAO,SAAS,MAAM,OAAO;AAAA,EACxE,qBAAqB,EAAE,MAAM,eAAe,OAAO,WAAW,MAAM,OAAO;AAAA,EAC3E,sBAAsB,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,UAAU;AAAA,EACvE,0BAA0B;AAAA,IACxB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,wBAAwB,EAAE,MAAM,OAAO,OAAO,WAAW,MAAM,OAAO;AAAA,EACtE,+BAA+B;AAAA,IAC7B,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA;AAAA,EAEA,sBAAsB,EAAE,MAAM,cAAc,OAAO,YAAY,MAAM,OAAO;AAAA,EAC5E,mBAAmB,EAAE,MAAM,UAAU,OAAO,YAAY,MAAM,UAAU;AAAA,EACxE,qBAAqB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAAA,EACvE,yBAAyB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAC7E;AAGO,SAAS,iBAAiB,MAAgC;AAC/D,SAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,OAAO,UAAU,MAAM,UAAU;AACvE;AAEO,MAAM,WAA6C;AAAA,EACxD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AACX;","names":[]}
@@ -4,15 +4,11 @@ import type { MerchantNotification } from '@withpotter/lib/api/types';
4
4
  * went, the headline, and where and when underneath.
5
5
  *
6
6
  * Unread reads at full strength on a tinted row and read recedes, so a long
7
- * list shows what is waiting without a second marker. When the news has a next
8
- * step (pay, keep, resume) it is a pill under the text, its own button, so
9
- * pressing it does that step and pressing the row opens the news.
7
+ * list shows what is waiting without a second marker.
10
8
  */
11
- export declare function NotificationRow({ notification, when, onOpen, onAction, }: {
9
+ export declare function NotificationRow({ notification, when, onOpen, }: {
12
10
  notification: MerchantNotification;
13
11
  /** The age, already worded ("just now", "5m ago"). */
14
12
  when: string;
15
13
  onOpen: () => void;
16
- /** The pill was pressed. Rows without a next step never call it. */
17
- onAction?: (intent: string, notification: MerchantNotification) => void;
18
14
  }): import("react").JSX.Element;
@@ -1,71 +1,45 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import {
3
- notificationAction,
4
- notificationKind,
5
- TONE_DOT
6
- } from "./notification-kinds";
2
+ import { notificationKind, TONE_DOT } from "./notification-kinds";
7
3
  function NotificationRow({
8
4
  notification,
9
5
  when,
10
- onOpen,
11
- onAction
6
+ onOpen
12
7
  }) {
13
- const { Icon, label, tone } = notificationKind(
14
- notification.type,
15
- notification.metadata
16
- );
17
- const action = onAction ? notificationAction(notification.metadata) : null;
8
+ const { Icon, label, tone } = notificationKind(notification.type);
18
9
  const unread = !notification.readAt;
19
10
  return /* @__PURE__ */ jsxs(
20
- "div",
11
+ "button",
21
12
  {
22
- className: `transition-colors hover:bg-[var(--interactive-hover)] ${unread ? "bg-[var(--bg-subtle)]" : ""}`,
13
+ type: "button",
14
+ onClick: onOpen,
15
+ title: notification.body || void 0,
16
+ className: `flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)] ${unread ? "bg-[var(--bg-subtle)]" : ""}`,
23
17
  children: [
24
- /* @__PURE__ */ jsxs(
25
- "button",
26
- {
27
- type: "button",
28
- onClick: onOpen,
29
- title: notification.body || void 0,
30
- className: "flex w-full items-start gap-2 px-3 py-2 text-left",
31
- children: [
32
- /* @__PURE__ */ jsxs("span", { className: "relative shrink-0", children: [
33
- /* @__PURE__ */ jsx("span", { className: "grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]", children: /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": true }) }),
34
- /* @__PURE__ */ jsx(
35
- "span",
36
- {
37
- "aria-hidden": true,
38
- className: "absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]",
39
- style: { background: TONE_DOT[tone] }
40
- }
41
- )
42
- ] }),
43
- /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
44
- /* @__PURE__ */ jsx(
45
- "span",
46
- {
47
- className: `block truncate text-sm leading-snug ${unread ? "font-medium text-[var(--text-primary)]" : "text-[var(--text-tertiary)]"}`,
48
- children: notification.title
49
- }
50
- ),
51
- /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block truncate text-xs text-[var(--text-tertiary)]", children: [
52
- label,
53
- " \xB7 ",
54
- when
55
- ] })
56
- ] })
57
- ]
58
- }
59
- ),
60
- action && /* @__PURE__ */ jsx("div", { className: "pb-2 pl-14 pr-3", children: /* @__PURE__ */ jsx(
61
- "button",
62
- {
63
- type: "button",
64
- onClick: () => onAction?.(action.intent, notification),
65
- className: "h-7 rounded-full border border-[var(--border-default)] bg-[var(--bg-base)] px-3 text-xs font-medium text-[var(--text-primary)] transition-colors hover:bg-[var(--interactive-hover)]",
66
- children: action.label
67
- }
68
- ) })
18
+ /* @__PURE__ */ jsxs("span", { className: "relative shrink-0", children: [
19
+ /* @__PURE__ */ jsx("span", { className: "grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]", children: /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": true }) }),
20
+ /* @__PURE__ */ jsx(
21
+ "span",
22
+ {
23
+ "aria-hidden": true,
24
+ className: "absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]",
25
+ style: { background: TONE_DOT[tone] }
26
+ }
27
+ )
28
+ ] }),
29
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
30
+ /* @__PURE__ */ jsx(
31
+ "span",
32
+ {
33
+ className: `block truncate text-sm leading-snug ${unread ? "font-medium text-[var(--text-primary)]" : "text-[var(--text-tertiary)]"}`,
34
+ children: notification.title
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block truncate text-xs text-[var(--text-tertiary)]", children: [
38
+ label,
39
+ " \xB7 ",
40
+ when
41
+ ] })
42
+ ] })
69
43
  ]
70
44
  }
71
45
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notification-row.tsx"],"sourcesContent":["import type { MerchantNotification } from '@withpotter/lib/api/types';\nimport {\n notificationAction,\n notificationKind,\n TONE_DOT,\n} from './notification-kinds';\n\n/**\n * One notification: a tile for what kind of news it is, with a dot for how it\n * went, the headline, and where and when underneath.\n *\n * Unread reads at full strength on a tinted row and read recedes, so a long\n * list shows what is waiting without a second marker. When the news has a next\n * step (pay, keep, resume) it is a pill under the text, its own button, so\n * pressing it does that step and pressing the row opens the news.\n */\nexport function NotificationRow({\n notification,\n when,\n onOpen,\n onAction,\n}: {\n notification: MerchantNotification;\n /** The age, already worded (\"just now\", \"5m ago\"). */\n when: string;\n onOpen: () => void;\n /** The pill was pressed. Rows without a next step never call it. */\n onAction?: (intent: string, notification: MerchantNotification) => void;\n}) {\n const { Icon, label, tone } = notificationKind(\n notification.type,\n notification.metadata,\n );\n const action = onAction ? notificationAction(notification.metadata) : null;\n const unread = !notification.readAt;\n\n return (\n <div\n className={`transition-colors hover:bg-[var(--interactive-hover)] ${\n unread ? 'bg-[var(--bg-subtle)]' : ''\n }`}\n >\n <button\n type=\"button\"\n onClick={onOpen}\n title={notification.body || undefined}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <span className=\"relative shrink-0\">\n <span className=\"grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]\">\n <Icon className=\"size-4\" aria-hidden />\n </span>\n <span\n aria-hidden\n className=\"absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]\"\n style={{ background: TONE_DOT[tone] }}\n />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n className={`block truncate text-sm leading-snug ${\n unread\n ? 'font-medium text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)]'\n }`}\n >\n {notification.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-[var(--text-tertiary)]\">\n {label} · {when}\n </span>\n </span>\n </button>\n {action && (\n <div className=\"pb-2 pl-14 pr-3\">\n <button\n type=\"button\"\n onClick={() => onAction?.(action.intent, notification)}\n className=\"h-7 rounded-full border border-[var(--border-default)] bg-[var(--bg-base)] px-3 text-xs font-medium text-[var(--text-primary)] transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {action.label}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"AAgDQ,SAEI,KAFJ;AA/CR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWA,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,EAAE,MAAM,OAAO,KAAK,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,aAAa;AAAA,EACf;AACA,QAAM,SAAS,WAAW,mBAAmB,aAAa,QAAQ,IAAI;AACtE,QAAM,SAAS,CAAC,aAAa;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,yDACT,SAAS,0BAA0B,EACrC;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,OAAO,aAAa,QAAQ;AAAA,YAC5B,WAAU;AAAA,YAEV;AAAA,mCAAC,UAAK,WAAU,qBACd;AAAA,oCAAC,UAAK,WAAU,mIACd,8BAAC,QAAK,WAAU,UAAS,eAAW,MAAC,GACvC;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,eAAW;AAAA,oBACX,WAAU;AAAA,oBACV,OAAO,EAAE,YAAY,SAAS,IAAI,EAAE;AAAA;AAAA,gBACtC;AAAA,iBACF;AAAA,cACA,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,uCACT,SACI,2CACA,6BACN;AAAA,oBAEC,uBAAa;AAAA;AAAA,gBAChB;AAAA,gBACA,qBAAC,UAAK,WAAU,6DACb;AAAA;AAAA,kBAAM;AAAA,kBAAI;AAAA,mBACb;AAAA,iBACF;AAAA;AAAA;AAAA,QACF;AAAA,QACC,UACC,oBAAC,SAAI,WAAU,mBACb;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,WAAW,OAAO,QAAQ,YAAY;AAAA,YACrD,WAAU;AAAA,YAET,iBAAO;AAAA;AAAA,QACV,GACF;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notification-row.tsx"],"sourcesContent":["import type { MerchantNotification } from '@withpotter/lib/api/types';\nimport { notificationKind, TONE_DOT } from './notification-kinds';\n\n/**\n * One notification: a tile for what kind of news it is, with a dot for how it\n * went, the headline, and where and when underneath.\n *\n * Unread reads at full strength on a tinted row and read recedes, so a long\n * list shows what is waiting without a second marker.\n */\nexport function NotificationRow({\n notification,\n when,\n onOpen,\n}: {\n notification: MerchantNotification;\n /** The age, already worded (\"just now\", \"5m ago\"). */\n when: string;\n onOpen: () => void;\n}) {\n const { Icon, label, tone } = notificationKind(notification.type);\n const unread = !notification.readAt;\n\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n title={notification.body || undefined}\n className={`flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)] ${\n unread ? 'bg-[var(--bg-subtle)]' : ''\n }`}\n >\n <span className=\"relative shrink-0\">\n <span className=\"grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]\">\n <Icon className=\"size-4\" aria-hidden />\n </span>\n <span\n aria-hidden\n className=\"absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]\"\n style={{ background: TONE_DOT[tone] }}\n />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n className={`block truncate text-sm leading-snug ${\n unread\n ? 'font-medium text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)]'\n }`}\n >\n {notification.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-[var(--text-tertiary)]\">\n {label} · {when}\n </span>\n </span>\n </button>\n );\n}\n"],"mappings":"AAgCM,SAEI,KAFJ;AA/BN,SAAS,kBAAkB,gBAAgB;AASpC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,MAAM,OAAO,KAAK,IAAI,iBAAiB,aAAa,IAAI;AAChE,QAAM,SAAS,CAAC,aAAa;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO,aAAa,QAAQ;AAAA,MAC5B,WAAW,2GACT,SAAS,0BAA0B,EACrC;AAAA,MAEA;AAAA,6BAAC,UAAK,WAAU,qBACd;AAAA,8BAAC,UAAK,WAAU,mIACd,8BAAC,QAAK,WAAU,UAAS,eAAW,MAAC,GACvC;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA,cACV,OAAO,EAAE,YAAY,SAAS,IAAI,EAAE;AAAA;AAAA,UACtC;AAAA,WACF;AAAA,QACA,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,uCACT,SACI,2CACA,6BACN;AAAA,cAEC,uBAAa;AAAA;AAAA,UAChB;AAAA,UACA,qBAAC,UAAK,WAAU,6DACb;AAAA;AAAA,YAAM;AAAA,YAAI;AAAA,aACb;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -7,7 +7,6 @@ import { durations, eases, springs } from "@withpotter/lib/motion";
7
7
  import { useRouter } from "next/navigation";
8
8
  import { usePermissions } from "@withpotter/lib/use-permissions";
9
9
  import { Check, ChevronDown, Settings, UserPlus } from "lucide-react";
10
- import { useSidebarCards } from "./sidebar-cards";
11
10
  import { useAuth } from "@withpotter/lib/auth-context";
12
11
  import { WorkspaceMark } from "./workspace-mark";
13
12
  import { businessCategoryLabel } from "@withpotter/lib/business-categories";
@@ -29,7 +28,6 @@ function ProductSwitcher({
29
28
  const foot = variant === "foot";
30
29
  const head = variant === "head";
31
30
  const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();
32
- const planLabel = useSidebarCards()?.planLabel;
33
31
  const router = useRouter();
34
32
  const { can, loading: permissionsLoading } = usePermissions();
35
33
  const canManageTeam = can("canManageTeam") && !permissionsLoading;
@@ -206,7 +204,6 @@ function ProductSwitcher({
206
204
  /* @__PURE__ */ jsx("b", { children: name }),
207
205
  activeDescription && /* @__PURE__ */ jsx("span", { className: "cap", children: activeDescription })
208
206
  ] }),
209
- planLabel && /* @__PURE__ */ jsx("span", { className: "pv-psw-plan", children: planLabel }),
210
207
  /* @__PURE__ */ jsx(Check, { className: "pv-psw-check", width: 15, height: 15 })
211
208
  ]
212
209
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/product-switcher.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { useRouter } from 'next/navigation';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { Check, ChevronDown, Settings, UserPlus } from 'lucide-react';\nimport { useSidebarCards } from './sidebar-cards';\nimport { useAuth, type Workspace } from '@withpotter/lib/auth-context';\nimport { WorkspaceMark } from './workspace-mark';\nimport { businessCategoryLabel } from '@withpotter/lib/business-categories';\nimport { byRecentlyUsed, noteWorkspaceUsed } from '@withpotter/lib/workspace-recents';\nimport { SwitcherScrim } from './switcher-scrim';\nimport { SwitcherWorkspaceList } from './switcher-workspace-list';\nimport { CreateBusinessModal } from '@withpotter/components/create-business-modal';\nimport { useSettingsModal } from './settings-modal-context';\nimport { ShellIcon } from './icons';\nimport { TeamModal } from '@withpotter/pages-core/dashboard/settings/team/_components/team-modal';\nimport {\n LocationScopePin,\n SwitcherLocationGroup,\n} from './switcher-location-group';\n\n/**\n * How many other workspaces the menu lists before deferring to the page.\n *\n * Two, and the two most recently opened — John, 2026-08-23. Three rows of\n * somewhere-else was the largest block in a menu whose subject is the\n * workspace you are in, and picking which two by recency is the only ordering\n * that gets better the more the menu is used. See `workspace-recents`.\n */\nconst INLINE_WORKSPACES = 2;\n\n/**\n * The business switcher: one trigger, one scope.\n *\n * Ordered by hierarchy, not by scope: the business you are in (a header block,\n * not a row), then *Switch to* the ones you could be in, then the person they\n * belong to (`switcher-account-group`). Out of this menu on purpose: the theme\n * flip (a preference, it rides the footer), Plan & billing, the team row and\n * business settings (footer rows now), and the plan label under the name. A\n * merchant presses this to change business, so every row between them and that\n * is in the way.\n */\nexport function ProductSwitcher({\n variant = 'top',\n}: {\n /**\n * `head` is the flat row at the top of the rail: mark and name only, with\n * search beside it (see `side-head` in shell.css).\n * `foot` is the same switcher drawn as a flat row at the bottom of the rail\n * (mark, name, chevron) whose menu opens UPWARD, because there is no room\n * under it. Same menu, same state, same keyboard walk.\n */\n variant?: 'top' | 'foot' | 'head';\n} = {}) {\n const foot = variant === 'foot';\n const head = variant === 'head';\n const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const router = useRouter();\n const { can, loading: permissionsLoading } = usePermissions();\n // False while the membership request is in flight, so the row appears when\n // the answer arrives rather than flashing at somebody who turns out to be\n // staff.\n const canManageTeam = can('canManageTeam') && !permissionsLoading;\n const [open, setOpen] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n // The settings window is one shared modal (`SettingsModalProvider`), so closing\n // this menu cannot unmount it.\n const { openSettings } = useSettingsModal();\n // Mounted only while open: the modal reads the whole team on mount.\n const [teamOpen, setTeamOpen] = useState(false);\n const [pos, setPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n }>({ top: 0, left: 0, width: 280 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n /**\n * Stamp the workspace you are in as the most recently used one.\n *\n * Here rather than in `persistActiveWorkspace`, which is the tidier choke\n * point and the wrong place to spend lines: auth-context is on the file-size\n * ledger and must shrink, not grow. This is equivalent — the shell renders\n * for every workspace that becomes active, whatever route made it active —\n * and it keeps the whole feature in the one component that reads it.\n */\n useEffect(() => {\n if (tenant?.slug) noteWorkspaceUsed(tenant.slug);\n }, [tenant?.slug]);\n\n // Measure at click time (not in an effect) so the portaled menu is\n // positioned before it paints.\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b)\n setPos({\n ...(foot\n ? { bottom: window.innerHeight - b.top + 6 }\n : { top: b.bottom + 6 }),\n left: b.left,\n width: Math.max(b.width, 280),\n });\n }\n setOpen((v) => !v);\n };\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n (e.target as Element | null)?.closest?.('[data-pv-psw-flyout]') ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n // This is the app's primary navigation surface, so it has to be usable\n // from the keyboard alone: opening lands on the first control and the\n // arrows walk the list.\n //\n // THE MENU, not its first row. There is nothing to type into any more, so\n // focusing the first control puts a ring on the workspace you are already\n // in the instant the menu opens, which reads as a selected row rather than\n // a landing place. The container takes focus instead and the arrow handler\n // below walks into the list from there — `indexOf` returns -1 for the\n // container, so ArrowDown lands on the first item.\n popRef.current?.focus();\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const onMenuKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Tab') {\n setOpen(false);\n return;\n }\n const step =\n e.key === 'ArrowDown'\n ? 1\n : e.key === 'ArrowUp'\n ? -1\n : e.key === 'Home' || e.key === 'End'\n ? 0\n : null;\n if (step === null) return;\n e.preventDefault();\n // `button, a[href]` — not `button`. Help & support is a real Link so it can\n // be cmd-clicked like any destination, and a selector of `button` alone\n // walked straight past it: the one row in this menu that leaves the app for\n // a page was the one row the arrows could not reach.\n const items = Array.from(\n popRef.current?.querySelectorAll<HTMLElement>('button, a[href]') ?? [],\n );\n if (!items.length) return;\n if (step === 0) {\n (e.key === 'Home' ? items[0] : items[items.length - 1]).focus();\n return;\n }\n const at = items.indexOf(document.activeElement as HTMLElement);\n items[(at + step + items.length) % items.length].focus();\n };\n\n const name = tenant?.businessName ?? 'Your business';\n\n /** Sub-line for a row: what kind of business it is, prefixed by a role worth naming. */\n const describe = (w: Workspace) =>\n [w.role !== 'owner' ? w.role : '', businessCategoryLabel(w.tenant)]\n .filter(Boolean)\n .join(' · ');\n\n /**\n * What the workspace you are in is, in the sub-line the list rows use.\n *\n * Off the workspace ENTRY where there is one, because that is what carries\n * your role here; `tenant` alone can only answer the category half.\n */\n const activeDescription = useMemo(() => {\n const mine = workspaces.find((w) => w.tenant.slug === tenant?.slug);\n if (mine) return describe(mine);\n return tenant ? businessCategoryLabel(tenant) : '';\n }, [workspaces, tenant]);\n\n const go = (href: string) => {\n setOpen(false);\n router.push(href);\n };\n\n const onSwitch = async (slug: string) => {\n setOpen(false);\n if (slug === tenant?.slug) return;\n // Soft swap: switchWorkspace swaps the tenant in context and every\n // useAuth() consumer re-renders. Refresh server segments only — no reload.\n await switchWorkspace(slug);\n router.replace('/dashboard');\n };\n\n /**\n * Everywhere but here. The workspace you are in leads the menu as the first\n * row of its own group, ticked and carrying its picture and its category —\n * so listing it again down here would be the same row twice, which is what\n * the tick had to distinguish it from.\n */\n const listed = useMemo(\n () =>\n byRecentlyUsed(\n workspaces.filter((w) => w.tenant.slug !== tenant?.slug),\n (w) => w.tenant.slug,\n ),\n // Re-read on every switch: the slug changing IS the event that reorders\n // this, and it is the only one that can happen while the menu is mounted.\n [workspaces, tenant?.slug],\n );\n\n // The first few, and a way to the rest. There is no filter here any more:\n // John, 2026-08-23, \"take out the search bar\". It only ever appeared above\n // three workspaces, it was the first thing focus landed on, and the page it\n // defers to can search properly.\n const shown = listed.slice(0, INLINE_WORKSPACES);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n className={\n 'pswitch' +\n (foot ? ' pswitch-foot' : '') +\n (head ? ' pswitch-head' : '') +\n (activeDescription && variant === 'top' ? ' has-sub' : '') +\n (open ? ' open' : '')\n }\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={toggle}\n title={name}\n >\n <WorkspaceMark\n className=\"wmark\"\n logo={tenant?.businessLogo}\n label={name}\n />\n <span className=\"pl\">\n <b>{name}</b>\n {/* What the business IS, where the plan label used to be (John,\n 2026-08-23) — a fact about the workspace rather than a billing\n status, which is what the most permanent line in the app should\n hold. Absent when nothing is stored, and the card drops a step\n rather than holding the space open. Set in Settings → General. */}\n {activeDescription && variant === 'top' && (\n <span>{activeDescription}</span>\n )}\n </span>\n <span className=\"sw\">\n <LocationScopePin />\n {foot || head ? (\n <ChevronDown width={15} height={15} />\n ) : (\n <ShellIcon.updown width={15} height={15} />\n )}\n </span>\n </button>\n\n {/* AnimatePresence INSIDE the portal, and the portal itself is only\n rendered once there is somewhere to put it. `{open && createPortal(…)}`\n ripped the menu out of the tree the moment it closed, so it arrived on\n a spring and vanished on a frame — the exact defect `FormDialog`\n documents for `{editing && <FormDialog/>}`. */}\n {/* `document` rather than a mounted flag: the portal's children are\n `open && …`, which is false on the first client render, so it puts\n nothing in the DOM and there is no markup for the server to disagree\n with. A state-in-effect flag would have been one render pass to learn\n what `typeof` already knows. */}\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n key=\"menu\"\n ref={popRef}\n className=\"pv-psw-menu\"\n tabIndex={-1}\n role=\"menu\"\n aria-label=\"Businesses and account\"\n onKeyDown={onMenuKey}\n /* THE MOTION SYSTEM, not a 140ms CSS keyframe (John, 2026-08-23).\n `springs.gentle` is the surface preset every dialog and sheet\n arrives on; this menu was one of the hand-rolled surfaces that\n never got it. It grows from its top-left, the corner nearest the\n trigger, so it reads as coming OUT of the control. No exit, for\n the reason `PageTransition` gives: a dismissed menu lingering\n delays whatever you pressed it to reach. */\n initial={{ opacity: 0, scale: 0.96, y: foot ? 4 : -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n /* IT LEAVES FASTER THAN IT ARRIVES. The note above is still true —\n a dismissed menu lingering delays whatever you pressed it to\n reach — but \"no delay\" and \"no motion\" are different things, and\n what shipped was the second. So the exit is a tween at\n `--motion-fast`, half the arrival and on the way OUT of the same\n corner: gone before you can want it gone, and still a movement\n rather than a cut. */\n exit={{\n opacity: 0,\n scale: 0.98,\n y: foot ? 2 : -2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n minWidth: pos.width,\n transformOrigin: foot ? 'bottom left' : 'top left',\n }}\n >\n {/* NO ROWS ABOVE THE LIST AT ALL NOW, and that is the end of\n a sequence rather than a fourth opinion. Workspace settings and\n the team went to the rail (John, 2026-09-03); the heading and\n the rule under this group went with them; and Plan & billing\n went last, once we had actually checked what it describes.\n\n WHY PLAN & BILLING WENT. John, 2026-09-03: *\"exactly no plans\n just credits.\"* There is one plan and nothing renews it; what a\n merchant can buy is credits. It is a settings page\n (/dashboard/settings/billing), reached through Workspace\n settings in the rail.\n\n SO THE MENU IS WHAT IT SAYS IT IS — the workspace you are in,\n then the ones you are not, then you.\n\n THE WORKSPACE ITSELF LEADS IT. John, 2026-08-23: *\"the active\n workspace should be on top the workspace settings.\"* It had\n been a row down in the list wearing a tick, which is where you\n look to CHANGE workspace, not to read which one you are in —\n and it left the rows above it acting on a business nothing on\n screen had named. As the group's first row it is both: the\n name, the picture and the kind of business, ticked, with what\n you can do to it directly beneath.\n\n It stays pressable rather than becoming a static block, because\n a row that looks like the rows under it should behave like\n them. `onSwitch` returns early on the slug you are already on,\n so the press is a no-op rather than a reload. */}\n <div role=\"group\" aria-label={`This business: ${name}`}>\n {tenant && (\n <button\n type=\"button\"\n className=\"pv-psw-item current\"\n role=\"menuitem\"\n aria-current=\"true\"\n onClick={() => onSwitch(tenant.slug)}\n >\n <WorkspaceMark\n className=\"pv-psw-ic\"\n logo={tenant.businessLogo}\n label={name}\n />\n <span className=\"pv-psw-txt\">\n <b>{name}</b>\n {/* The SAME sub-line the rows below use, and guarded the\n same way. It was `businessCategoryLabel` written out\n again, rendered unconditionally — so a workspace with\n no category stored (which is most of them until\n somebody picks one) got an empty span holding open a\n line, and the row you are in was the one row in the\n menu wearing a blank where its neighbours wore\n nothing. `describe` also carries the role, which the\n rows below show and this one silently dropped. */}\n {activeDescription && (\n <span className=\"cap\">{activeDescription}</span>\n )}\n </span>\n {planLabel && <span className=\"pv-psw-plan\">{planLabel}</span>}\n <Check className=\"pv-psw-check\" width={15} height={15} />\n </button>\n )}\n {/* What you can do to the business you are in, directly beneath\n it. Back in the menu on John's call, 2026-10-04 (it had\n moved to a rail row on 2026-09-03). */}\n {canManageTeam && (\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n setTeamOpen(true);\n }}\n >\n <UserPlus width={15} height={15} />\n Invite your team\n </button>\n )}\n <SwitcherLocationGroup\n onPicked={() => setOpen(false)}\n onManage={() => go('/dashboard/operations/locations')}\n />\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n openSettings();\n }}\n >\n <Settings width={15} height={15} />\n Settings\n </button>\n </div>\n\n {/* Closes off the active business and its actions — only when\n there is something under it. */}\n <div className=\"pv-psw-sep\" />\n\n <SwitcherWorkspaceList\n shown={shown}\n total={workspaces.length}\n describe={describe}\n onSwitch={onSwitch}\n onAdd={() => {\n setOpen(false);\n setShowCreate(true);\n }}\n onManage={() => go('/dashboard/workspaces')}\n user={user ?? undefined}\n onAccount={() => {\n setOpen(false);\n openSettings('/dashboard/settings/profile');\n }}\n onLogout={() => {\n setOpen(false);\n logout();\n }}\n onClose={() => setOpen(false)}\n />\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n\n <CreateBusinessModal\n open={showCreate}\n onClose={() => setShowCreate(false)}\n />\n\n {teamOpen && (\n <TeamModal open initialInvite onClose={() => setTeamOpen(false)} />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/product-switcher.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { useRouter } from 'next/navigation';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { Check, ChevronDown, Settings, UserPlus } from 'lucide-react';\nimport { useAuth, type Workspace } from '@withpotter/lib/auth-context';\nimport { WorkspaceMark } from './workspace-mark';\nimport { businessCategoryLabel } from '@withpotter/lib/business-categories';\nimport { byRecentlyUsed, noteWorkspaceUsed } from '@withpotter/lib/workspace-recents';\nimport { SwitcherScrim } from './switcher-scrim';\nimport { SwitcherWorkspaceList } from './switcher-workspace-list';\nimport { CreateBusinessModal } from '@withpotter/components/create-business-modal';\nimport { useSettingsModal } from './settings-modal-context';\nimport { ShellIcon } from './icons';\nimport { TeamModal } from '@withpotter/pages-core/dashboard/settings/team/_components/team-modal';\nimport {\n LocationScopePin,\n SwitcherLocationGroup,\n} from './switcher-location-group';\n\n/**\n * How many other workspaces the menu lists before deferring to the page.\n *\n * Two, and the two most recently opened — John, 2026-08-23. Three rows of\n * somewhere-else was the largest block in a menu whose subject is the\n * workspace you are in, and picking which two by recency is the only ordering\n * that gets better the more the menu is used. See `workspace-recents`.\n */\nconst INLINE_WORKSPACES = 2;\n\n/**\n * The business switcher: one trigger, one scope.\n *\n * Ordered by hierarchy, not by scope: the business you are in (a header block,\n * not a row), then *Switch to* the ones you could be in, then the person they\n * belong to (`switcher-account-group`). Out of this menu on purpose: the theme\n * flip (a preference, it rides the footer), Plan & billing, the team row and\n * business settings (footer rows now), and the plan label under the name. A\n * merchant presses this to change business, so every row between them and that\n * is in the way.\n */\nexport function ProductSwitcher({\n variant = 'top',\n}: {\n /**\n * `head` is the flat row at the top of the rail: mark and name only, with\n * search beside it (see `side-head` in shell.css).\n * `foot` is the same switcher drawn as a flat row at the bottom of the rail\n * (mark, name, chevron) whose menu opens UPWARD, because there is no room\n * under it. Same menu, same state, same keyboard walk.\n */\n variant?: 'top' | 'foot' | 'head';\n} = {}) {\n const foot = variant === 'foot';\n const head = variant === 'head';\n const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();\n const router = useRouter();\n const { can, loading: permissionsLoading } = usePermissions();\n // False while the membership request is in flight, so the row appears when\n // the answer arrives rather than flashing at somebody who turns out to be\n // staff.\n const canManageTeam = can('canManageTeam') && !permissionsLoading;\n const [open, setOpen] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n // The settings window is one shared modal (`SettingsModalProvider`), so closing\n // this menu cannot unmount it.\n const { openSettings } = useSettingsModal();\n // Mounted only while open: the modal reads the whole team on mount.\n const [teamOpen, setTeamOpen] = useState(false);\n const [pos, setPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n }>({ top: 0, left: 0, width: 280 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n /**\n * Stamp the workspace you are in as the most recently used one.\n *\n * Here rather than in `persistActiveWorkspace`, which is the tidier choke\n * point and the wrong place to spend lines: auth-context is on the file-size\n * ledger and must shrink, not grow. This is equivalent — the shell renders\n * for every workspace that becomes active, whatever route made it active —\n * and it keeps the whole feature in the one component that reads it.\n */\n useEffect(() => {\n if (tenant?.slug) noteWorkspaceUsed(tenant.slug);\n }, [tenant?.slug]);\n\n // Measure at click time (not in an effect) so the portaled menu is\n // positioned before it paints.\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b)\n setPos({\n ...(foot\n ? { bottom: window.innerHeight - b.top + 6 }\n : { top: b.bottom + 6 }),\n left: b.left,\n width: Math.max(b.width, 280),\n });\n }\n setOpen((v) => !v);\n };\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n (e.target as Element | null)?.closest?.('[data-pv-psw-flyout]') ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n // This is the app's primary navigation surface, so it has to be usable\n // from the keyboard alone: opening lands on the first control and the\n // arrows walk the list.\n //\n // THE MENU, not its first row. There is nothing to type into any more, so\n // focusing the first control puts a ring on the workspace you are already\n // in the instant the menu opens, which reads as a selected row rather than\n // a landing place. The container takes focus instead and the arrow handler\n // below walks into the list from there — `indexOf` returns -1 for the\n // container, so ArrowDown lands on the first item.\n popRef.current?.focus();\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const onMenuKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Tab') {\n setOpen(false);\n return;\n }\n const step =\n e.key === 'ArrowDown'\n ? 1\n : e.key === 'ArrowUp'\n ? -1\n : e.key === 'Home' || e.key === 'End'\n ? 0\n : null;\n if (step === null) return;\n e.preventDefault();\n // `button, a[href]` — not `button`. Help & support is a real Link so it can\n // be cmd-clicked like any destination, and a selector of `button` alone\n // walked straight past it: the one row in this menu that leaves the app for\n // a page was the one row the arrows could not reach.\n const items = Array.from(\n popRef.current?.querySelectorAll<HTMLElement>('button, a[href]') ?? [],\n );\n if (!items.length) return;\n if (step === 0) {\n (e.key === 'Home' ? items[0] : items[items.length - 1]).focus();\n return;\n }\n const at = items.indexOf(document.activeElement as HTMLElement);\n items[(at + step + items.length) % items.length].focus();\n };\n\n const name = tenant?.businessName ?? 'Your business';\n\n /** Sub-line for a row: what kind of business it is, prefixed by a role worth naming. */\n const describe = (w: Workspace) =>\n [w.role !== 'owner' ? w.role : '', businessCategoryLabel(w.tenant)]\n .filter(Boolean)\n .join(' · ');\n\n /**\n * What the workspace you are in is, in the sub-line the list rows use.\n *\n * Off the workspace ENTRY where there is one, because that is what carries\n * your role here; `tenant` alone can only answer the category half.\n */\n const activeDescription = useMemo(() => {\n const mine = workspaces.find((w) => w.tenant.slug === tenant?.slug);\n if (mine) return describe(mine);\n return tenant ? businessCategoryLabel(tenant) : '';\n }, [workspaces, tenant]);\n\n const go = (href: string) => {\n setOpen(false);\n router.push(href);\n };\n\n const onSwitch = async (slug: string) => {\n setOpen(false);\n if (slug === tenant?.slug) return;\n // Soft swap: switchWorkspace swaps the tenant in context and every\n // useAuth() consumer re-renders. Refresh server segments only — no reload.\n await switchWorkspace(slug);\n router.replace('/dashboard');\n };\n\n /**\n * Everywhere but here. The workspace you are in leads the menu as the first\n * row of its own group, ticked and carrying its picture and its category —\n * so listing it again down here would be the same row twice, which is what\n * the tick had to distinguish it from.\n */\n const listed = useMemo(\n () =>\n byRecentlyUsed(\n workspaces.filter((w) => w.tenant.slug !== tenant?.slug),\n (w) => w.tenant.slug,\n ),\n // Re-read on every switch: the slug changing IS the event that reorders\n // this, and it is the only one that can happen while the menu is mounted.\n [workspaces, tenant?.slug],\n );\n\n // The first few, and a way to the rest. There is no filter here any more:\n // John, 2026-08-23, \"take out the search bar\". It only ever appeared above\n // three workspaces, it was the first thing focus landed on, and the page it\n // defers to can search properly.\n const shown = listed.slice(0, INLINE_WORKSPACES);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n className={\n 'pswitch' +\n (foot ? ' pswitch-foot' : '') +\n (head ? ' pswitch-head' : '') +\n (activeDescription && variant === 'top' ? ' has-sub' : '') +\n (open ? ' open' : '')\n }\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={toggle}\n title={name}\n >\n <WorkspaceMark\n className=\"wmark\"\n logo={tenant?.businessLogo}\n label={name}\n />\n <span className=\"pl\">\n <b>{name}</b>\n {/* What the business IS, where the plan label used to be (John,\n 2026-08-23) — a fact about the workspace rather than a billing\n status, which is what the most permanent line in the app should\n hold. Absent when nothing is stored, and the card drops a step\n rather than holding the space open. Set in Settings → General. */}\n {activeDescription && variant === 'top' && (\n <span>{activeDescription}</span>\n )}\n </span>\n <span className=\"sw\">\n <LocationScopePin />\n {foot || head ? (\n <ChevronDown width={15} height={15} />\n ) : (\n <ShellIcon.updown width={15} height={15} />\n )}\n </span>\n </button>\n\n {/* AnimatePresence INSIDE the portal, and the portal itself is only\n rendered once there is somewhere to put it. `{open && createPortal(…)}`\n ripped the menu out of the tree the moment it closed, so it arrived on\n a spring and vanished on a frame — the exact defect `FormDialog`\n documents for `{editing && <FormDialog/>}`. */}\n {/* `document` rather than a mounted flag: the portal's children are\n `open && …`, which is false on the first client render, so it puts\n nothing in the DOM and there is no markup for the server to disagree\n with. A state-in-effect flag would have been one render pass to learn\n what `typeof` already knows. */}\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n key=\"menu\"\n ref={popRef}\n className=\"pv-psw-menu\"\n tabIndex={-1}\n role=\"menu\"\n aria-label=\"Businesses and account\"\n onKeyDown={onMenuKey}\n /* THE MOTION SYSTEM, not a 140ms CSS keyframe (John, 2026-08-23).\n `springs.gentle` is the surface preset every dialog and sheet\n arrives on; this menu was one of the hand-rolled surfaces that\n never got it. It grows from its top-left, the corner nearest the\n trigger, so it reads as coming OUT of the control. No exit, for\n the reason `PageTransition` gives: a dismissed menu lingering\n delays whatever you pressed it to reach. */\n initial={{ opacity: 0, scale: 0.96, y: foot ? 4 : -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n /* IT LEAVES FASTER THAN IT ARRIVES. The note above is still true —\n a dismissed menu lingering delays whatever you pressed it to\n reach — but \"no delay\" and \"no motion\" are different things, and\n what shipped was the second. So the exit is a tween at\n `--motion-fast`, half the arrival and on the way OUT of the same\n corner: gone before you can want it gone, and still a movement\n rather than a cut. */\n exit={{\n opacity: 0,\n scale: 0.98,\n y: foot ? 2 : -2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n minWidth: pos.width,\n transformOrigin: foot ? 'bottom left' : 'top left',\n }}\n >\n {/* NO ROWS ABOVE THE LIST AT ALL NOW, and that is the end of\n a sequence rather than a fourth opinion. Workspace settings and\n the team went to the rail (John, 2026-09-03); the heading and\n the rule under this group went with them; and Plan & billing\n went last, once we had actually checked what it describes.\n\n WHY PLAN & BILLING WENT. John, 2026-09-03: *\"exactly no plans\n just credits.\"* There is one plan and nothing renews it; what a\n merchant can buy is credits. It is a settings page\n (/dashboard/settings/billing), reached through Workspace\n settings in the rail.\n\n SO THE MENU IS WHAT IT SAYS IT IS — the workspace you are in,\n then the ones you are not, then you.\n\n THE WORKSPACE ITSELF LEADS IT. John, 2026-08-23: *\"the active\n workspace should be on top the workspace settings.\"* It had\n been a row down in the list wearing a tick, which is where you\n look to CHANGE workspace, not to read which one you are in —\n and it left the rows above it acting on a business nothing on\n screen had named. As the group's first row it is both: the\n name, the picture and the kind of business, ticked, with what\n you can do to it directly beneath.\n\n It stays pressable rather than becoming a static block, because\n a row that looks like the rows under it should behave like\n them. `onSwitch` returns early on the slug you are already on,\n so the press is a no-op rather than a reload. */}\n <div role=\"group\" aria-label={`This business: ${name}`}>\n {tenant && (\n <button\n type=\"button\"\n className=\"pv-psw-item current\"\n role=\"menuitem\"\n aria-current=\"true\"\n onClick={() => onSwitch(tenant.slug)}\n >\n <WorkspaceMark\n className=\"pv-psw-ic\"\n logo={tenant.businessLogo}\n label={name}\n />\n <span className=\"pv-psw-txt\">\n <b>{name}</b>\n {/* The SAME sub-line the rows below use, and guarded the\n same way. It was `businessCategoryLabel` written out\n again, rendered unconditionally — so a workspace with\n no category stored (which is most of them until\n somebody picks one) got an empty span holding open a\n line, and the row you are in was the one row in the\n menu wearing a blank where its neighbours wore\n nothing. `describe` also carries the role, which the\n rows below show and this one silently dropped. */}\n {activeDescription && (\n <span className=\"cap\">{activeDescription}</span>\n )}\n </span>\n <Check className=\"pv-psw-check\" width={15} height={15} />\n </button>\n )}\n {/* What you can do to the business you are in, directly beneath\n it. Back in the menu on John's call, 2026-10-04 (it had\n moved to a rail row on 2026-09-03). */}\n {canManageTeam && (\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n setTeamOpen(true);\n }}\n >\n <UserPlus width={15} height={15} />\n Invite your team\n </button>\n )}\n <SwitcherLocationGroup\n onPicked={() => setOpen(false)}\n onManage={() => go('/dashboard/operations/locations')}\n />\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n openSettings();\n }}\n >\n <Settings width={15} height={15} />\n Settings\n </button>\n </div>\n\n {/* Closes off the active business and its actions — only when\n there is something under it. */}\n <div className=\"pv-psw-sep\" />\n\n <SwitcherWorkspaceList\n shown={shown}\n total={workspaces.length}\n describe={describe}\n onSwitch={onSwitch}\n onAdd={() => {\n setOpen(false);\n setShowCreate(true);\n }}\n onManage={() => go('/dashboard/workspaces')}\n user={user ?? undefined}\n onAccount={() => {\n setOpen(false);\n openSettings('/dashboard/settings/profile');\n }}\n onLogout={() => {\n setOpen(false);\n logout();\n }}\n onClose={() => setOpen(false)}\n />\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n\n <CreateBusinessModal\n open={showCreate}\n onClose={() => setShowCreate(false)}\n />\n\n {teamOpen && (\n <TeamModal open initialInvite onClose={() => setTeamOpen(false)} />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,8 +2,6 @@ import { type ReactNode } from 'react';
2
2
  interface SettingsModalApi {
3
3
  /** Open the settings window, on a section when given a settings address. */
4
4
  openSettings: (href?: string) => void;
5
- /** Close it, for a screen inside it that leaves for another page. */
6
- closeSettings: () => void;
7
5
  /**
8
6
  * Warm the window before it is opened: a hidden copy of the settings page
9
7
  * loads once so the real one boots with its code fetched and its route