@withpotter/components 0.13.0-next.84 → 0.13.0-next.86
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.
- package/dist/billing/overview-tab.js +4 -3
- package/dist/billing/overview-tab.js.map +1 -1
- package/dist/billing/plans-tab.js +6 -12
- package/dist/billing/plans-tab.js.map +1 -1
- package/dist/dashboard/activity-feed.d.ts +2 -0
- package/dist/dashboard/activity-feed.js +3 -1
- package/dist/dashboard/activity-feed.js.map +1 -1
- package/dist/dashboard/activity-paid.d.ts +10 -0
- package/dist/dashboard/activity-paid.js +7 -0
- package/dist/dashboard/activity-paid.js.map +1 -0
- package/dist/dashboard/home/ask-controls.js +1 -0
- package/dist/dashboard/home/ask-controls.js.map +1 -1
- package/dist/dashboard/home/ask-mode-chip.js +4 -4
- package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
- package/dist/dashboard/home/home-ask.js +3 -3
- package/dist/dashboard/home/home-ask.js.map +1 -1
- package/dist/dashboard/home/home-quick-actions.js +9 -1
- package/dist/dashboard/home/home-quick-actions.js.map +1 -1
- package/dist/dashboard/home/host-quick-actions.d.ts +19 -0
- package/dist/dashboard/home/host-quick-actions.js +12 -0
- package/dist/dashboard/home/host-quick-actions.js.map +1 -0
- package/dist/dashboard/home/widgets/activity-widget.js +7 -1
- package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
- package/dist/features/gift-card-artwork.js +1 -1
- package/dist/features/gift-card-artwork.js.map +1 -1
- package/dist/features/gift-cards/gift-card-logic.d.ts +41 -0
- package/dist/features/gift-cards/gift-card-logic.js +65 -0
- package/dist/features/gift-cards/gift-card-logic.js.map +1 -0
- package/dist/features/gift-cards/gift-cards-feature.js +656 -518
- package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
- package/dist/features/gift-cards/hint.d.ts +9 -0
- package/dist/features/gift-cards/hint.js +25 -0
- package/dist/features/gift-cards/hint.js.map +1 -0
- package/dist/features/product-form.js +1 -1
- package/dist/features/product-form.js.map +1 -1
- package/dist/mobile/screens/billing-flows.js +7 -6
- package/dist/mobile/screens/billing-flows.js.map +1 -1
- package/dist/mobile/screens/billing.js +2 -2
- package/dist/mobile/screens/billing.js.map +1 -1
- package/dist/sidebar/nav-renderers.js +2 -0
- package/dist/sidebar/nav-renderers.js.map +1 -1
- package/dist/sidebar/nav-section.js +5 -3
- package/dist/sidebar/nav-section.js.map +1 -1
- package/package.json +5 -5
|
@@ -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 {\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":[]}
|
|
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":[]}
|
|
@@ -146,6 +146,7 @@ function createNavRenderers({
|
|
|
146
146
|
onMouseEnter: () => rows.forEach((c) => warm(c.href)),
|
|
147
147
|
onFocus: () => rows.forEach((c) => warm(c.href)),
|
|
148
148
|
"aria-expanded": open,
|
|
149
|
+
"aria-controls": open ? `navsub-${group.id}` : void 0,
|
|
149
150
|
children: [
|
|
150
151
|
/* @__PURE__ */ jsx("span", { className: "ic", children: renderIcon(group.id, group.icon) }),
|
|
151
152
|
/* @__PURE__ */ jsx("span", { className: "lbl", children: group.label }),
|
|
@@ -157,6 +158,7 @@ function createNavRenderers({
|
|
|
157
158
|
/* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: open && /* @__PURE__ */ jsxs(
|
|
158
159
|
motion.ul,
|
|
159
160
|
{
|
|
161
|
+
id: `navsub-${group.id}`,
|
|
160
162
|
className: "navsub",
|
|
161
163
|
initial: { height: 0, opacity: 0, marginBottom: 0 },
|
|
162
164
|
animate: { height: "auto", opacity: 1, marginBottom: SUB_GAP },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/sidebar/nav-renderers.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { ChevronDown } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { eases } from '@withpotter/lib/motion';\nimport { prefersReducedMotion } from './nav-highlights';\nimport { navIcon } from '../shell/icons';\nimport { RailLabel } from './rail-label';\nimport { type NavItem, type NavGroup } from './config';\nimport type { NavRendererDeps } from './nav-renderer-deps';\nimport { NavHighlight, NavPending, SubHighlight } from './nav-highlights';\n\n/**\n * The air under an open hub, before the next top-level row.\n *\n * The rail's gaps are what say where one thing stops and the next begins, and\n * an open hub had nothing to say it: its last child sat 2px above the next\n * top-level row — the same 2px that separates two children — so the row after\n * an open hub read as the last item IN it.\n *\n * 6px, not 30. With the group's own 2px that is 8px, four times a sibling gap\n * and unmistakable, and still well under the 20px that opens a new SECTION. A\n * gap bigger than the section break would rank an open hub above a heading,\n * which is backwards: the hub is one row inside the section it sits in.\n */\nconst SUB_GAP = 6;\n\nexport type { NavRendererDeps } from './nav-renderer-deps';\n\n/**\n * One highlight per open hub, MOVED between its children rather than\n * replaced on each of them.\n *\n * The obvious version — a `layoutId` on a pill that unmounts from one row and\n * mounts on the next — is what this replaces. Framer took ownership of the\n * element (it carried the projection styles) but resolved no delta and put it\n * down on the new row with the frames in between missing, and chasing that\n * through framer's projection internals is not worth what it buys. A pill\n * that never unmounts cannot fail to match itself: `y` is an ordinary\n * animated value on one persistent element, so there is nothing to hand off.\n *\n * `offsetTop` off the row rather than index × pitch, so the row height, the\n * gap and the group's padding stay facts of the stylesheet. Read in a layout\n * effect, before paint, so the pill is never seen at the wrong row.\n *\n * A spring, not a curve: this is the one thing in the nav that genuinely\n * moves, and clicking a third row mid-flight has to reverse from wherever it\n * got to, carrying its velocity.\n */\n/**\n * Read at render rather than through `useReducedMotion`, because\n * `createNavRenderers` is a plain factory and not a hook — it cannot hold one.\n * Non-reactive is correct here anyway: the transition config is consulted when\n * an animation starts, so a preference changed mid-session applies to the next\n * open rather than to one already in flight.\n */\n/**\n * The sidebar's row, hub and section renderers.\n *\n * A factory rather than components: each one reads seven values that belong to\n * the sidebar's render pass (what is active, what the merchant has toggled\n * open), and threading those through props one component at a time was what\n * kept them inside a 1,510-line file in the first place.\n *\n * Anything with STATE must stay outside it. This runs on every render of the\n * sidebar, so a component declared in here is a new function identity each\n * time, and React unmounts and remounts it on every pass — which silently\n * destroys any hook state it was holding. That is what defeated the first two\n * attempts at a sliding highlight: `SubHighlight` was resetting its motion\n * values before it could ever animate them, and it looked exactly like a\n * framer bug. `SubHighlight` and `NavPending` live above for that reason.\n */\nexport function createNavRenderers({\n collapsed,\n warm,\n activeHref,\n isActive,\n hubChoice,\n setHubOpen,\n getGroupItems,\n badges,\n isNew,\n}: NavRendererDeps) {\n const renderIcon = (\n id: string | undefined,\n Fallback: LucideIcon,\n size = 18,\n ) => {\n const Console = id ? navIcon(id) : null;\n if (Console) return <Console width={size} height={size} />;\n return <Fallback width={size} height={size} />;\n };\n\n // The highlight on a top-level row.\n //\n // It still does not TRAVEL: there is no \"between\" for a row whose neighbours\n // are unrelated destinations, only a jump the length of the whole sidebar,\n // which is what got the old shared-layout pill torn out. But it no longer\n // simply appears either. Coming back up out of a hub, the pill rises the\n // same eight pixels the sub-pill drops, so the two halves of that gesture\n // are the same gesture seen from either end.\n const renderHighlight = (active: boolean) =>\n active ? <NavHighlight /> : null;\n\n /**\n * A nav row. Sub-rows carry no icon.\n *\n * A glyph beside a child was doing no work the layout wasn't already doing.\n * A child is identified by the parent it sits under and the indent it sits\n * at, and a second column of icons only made the tree read as two lists of\n * peers stacked on each other. Dropping it also lands the child's label\n * directly under the parent's, so the labels form one column rather than two\n * ragged ones. Icons stay where they still distinguish: the top level.\n */\n /**\n * The count a row wears, or nothing.\n *\n * Hidden when the rail is collapsed: at that width the label is gone too,\n * and a bare digit beside an icon names nothing.\n */\n const renderBadge = (n: number | undefined) =>\n !collapsed && !!n ? (\n <span className=\"navbadge squircle\" aria-label={`${n} need attention`}>\n {/* Capped, because the badge is a fixed square: `.squircle` is a mask\n stretched to the box, so a third digit would widen it and distort the\n corners away from every other squircle in the app. Past 99 the exact\n number has stopped being the point. */}\n {n > 99 ? '99+' : n}\n </span>\n ) : null;\n\n /**\n * A CLOSED HUB CARRIES WHAT IS INSIDE IT.\n *\n * Notifications land on the row their link names, and most of those rows are\n * a hub's CHILD — `/dashboard/inventory` is inside Inventory, which is shut\n * on most screens. Counting only the hub's own id meant the badge existed\n * and was invisible on every business that had one, which is the exact bug\n * this whole feature was replacing the bell to avoid.\n *\n * Only while closed. Open, the children are on screen wearing their own\n * numbers, and a total above them would be the same work counted twice.\n */\n const hubTotal = (group: NavGroup) =>\n (badges?.[group.id] ?? 0) +\n (group.items ?? []).reduce((sum, c) => sum + (badges?.[c.id] ?? 0), 0);\n\n const renderRow = (item: NavItem, sub = false, count?: number) => {\n const active = item.href === activeHref;\n return (\n // Keyed by href, not id. A business running several archetypes gets one\n // `catalog` row per archetype — Services, Properties, Menu — all sharing\n // the id and differing only by their intent-scoped href, which React saw\n // as duplicate keys and warned it may drop rows for.\n <li className=\"navitem\" key={item.href}>\n <RailLabel label={item.name} collapsed={collapsed}>\n <Link\n href={item.href}\n prefetch={false}\n onMouseEnter={() => warm(item.href)}\n onFocus={() => warm(item.href)}\n // Touch has no hover, so a tap used to hit a stone-cold route.\n // pointerdown lands before click and buys the prefetch a head start.\n onPointerDown={() => warm(item.href)}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? item.name : undefined}\n >\n {/* A sub-row's highlight belongs to its GROUP, which owns one and\n slides it. Rendering a second one here would put a static pill\n under the moving one on the row it just left. */}\n {!sub && renderHighlight(active)}\n <NavPending />\n {!sub && (\n <span className=\"ic\">{renderIcon(item.id, item.icon)}</span>\n )}\n <span className=\"lbl\">{item.name}</span>\n {/* Work waiting, not how many rows the page has. Rendered only\n when there is something to do, so the row is unmarked in the\n ordinary case and a number means something every time it\n appears. */}\n {/* NEW, on a row that has only just appeared. John, 2026-09-03:\n *\"it should show New on the new things added to the sidebar\"*.\n A merchant who switches on bookings gets three rows they have\n never seen among nine they know by shape, and without this the\n thing they just chose is the hardest thing on the rail to find.\n\n Before the count, and never both: a row cannot be simultaneously\n brand new and carrying work, and if it somehow is, the count is\n the one that needs doing. */}\n {isNew?.(item.id) && !badges?.[item.id] ? (\n <span className=\"navnew\">New</span>\n ) : (\n renderBadge(count ?? badges?.[item.id])\n )}\n </Link>\n </RailLabel>\n </li>\n );\n };\n\n // A hub row. With ≤1 child it's a plain link; with real sub-pages it gets a\n // hover-revealed chevron that expands the children inline (label still\n // navigates to the hub landing — only the chevron toggles).\n /**\n * A nav section, headed only when the heading is doing work. One item under a\n * heading is a label restating the row beneath it; two or more is a group.\n * Falsy children are dropped first so the count reflects what actually\n * renders, not how many slots were passed.\n */\n /**\n * Does the current route live inside these items?\n *\n * A section that owns the route is forced open, so the page you are on always\n * has a highlighted row. Hubs are checked through their children too — the\n * route is usually a child, not the hub's own landing page.\n */\n const ownsRoute = (items: (NavItem | NavGroup | undefined)[]) =>\n items.some((item) => {\n if (!item) return false;\n if ('href' in item && item.href && isActive(item.href)) return true;\n const children = 'id' in item ? getGroupItems(item as NavGroup) : [];\n return children.some((c) => c.href && isActive(c.href));\n });\n\n const renderHub = (group: NavGroup) => {\n const children = getGroupItems(group);\n if (children.length === 0 || (children.length === 1 && !group.alwaysHub)) {\n const active = group.href === activeHref;\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed={collapsed}>\n <Link\n href={group.href ?? '/dashboard'}\n prefetch={false}\n onMouseEnter={() => warm(group.href ?? '/dashboard')}\n onFocus={() => warm(group.href ?? '/dashboard')}\n onPointerDown={() => warm(group.href ?? '/dashboard')}\n className={'navlink' + (active ? ' active' : '')}\n aria-label={collapsed ? group.label : undefined}\n >\n {renderHighlight(active)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n /**\n * The hub's own landing page, sitting among its children instead of on the\n * hub's row.\n *\n * Every hub here had a real screen behind it, and putting that screen on\n * the container's row is what made the nav ambiguous: one click both\n * navigated and disclosed, and the highlight settled on the parent while\n * the list underneath it — which had appeared on that same click — read as\n * a menu still awaiting a choice. As a child it is an ordinary\n * destination, reachable by name, and the highlight lands where you\n * actually are.\n */\n const landing: NavItem[] = group.href\n ? [\n {\n id: `${group.id}-landing`,\n // `group.label` has already been through renameGroup, so a hub the\n // merchant renamed carries that name into its own landing row.\n //\n // NOTHING RELIES ON THE FALLBACK, and a new hub should not start.\n // Every group in the spine names its own landing child, because\n // \"Overview\" distinguishes nothing — every hub could carry one, so\n // the word tells the merchant nothing about which hub they are in.\n // A hub with an href owes its landing page a name that says what is\n // on it. The fallback stays only so a group under construction\n // renders rather than throwing.\n name: (group.landingLabel ?? 'Overview').replace(\n '{label}',\n group.label.toLowerCase(),\n ),\n href: group.href,\n icon: group.icon,\n },\n ]\n : [];\n const at = Math.min(group.landingAt ?? 0, children.length);\n const rows = [...children.slice(0, at), ...landing, ...children.slice(at)];\n // Which row the group's highlight sits on. -1 means none of them, so the\n // group renders no pill at all rather than one parked on the first row.\n const activeIndex = rows.findIndex((c) => c.href === activeHref);\n const childActive = activeIndex >= 0;\n // Auto-open when this hub owns the current route, unless the merchant has\n // toggled one open themselves (their choice wins, and it is exclusive —\n // opening this hub is what closed the last one).\n const open = hubChoice === undefined ? childActive : hubChoice === group.id;\n /**\n * Collapsed, the row falls back to being a link.\n *\n * Not a fudge: the rail hides `.navsub` entirely, so there is no list for\n * the row to open and therefore no ambiguity for it to create. The\n * ambiguity this whole change removes only exists when the children are on\n * screen. With them hidden, a container with nowhere to go is just a dead\n * target, so the row goes to its landing page as it always did.\n */\n if (collapsed) {\n const href = group.href ?? '/dashboard';\n return (\n <li className=\"navitem\" key={group.id}>\n <RailLabel label={group.label} collapsed>\n <Link\n href={href}\n prefetch={false}\n className={'navlink' + (href === activeHref ? ' active' : '')}\n aria-label={group.label}\n onMouseEnter={() => warm(href)}\n onPointerDown={() => warm(href)}\n >\n {renderHighlight(href === activeHref)}\n <NavPending />\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {renderBadge(hubTotal(group))}\n </Link>\n </RailLabel>\n </li>\n );\n }\n return (\n <li className=\"navitem\" key={group.id}>\n {/* A button, not a link. The whole row toggles — reaching for a 20px\n chevron to see what is inside a nav item is a target you have to aim\n at, when the row already reads as \"there is more here\". It carries no\n highlight and no pending state, because it goes nowhere. */}\n {/* Warms the whole group, not the row.\n The hub itself goes nowhere, so there is nothing to prefetch FOR it —\n but a hub is only ever pressed on the way to a child, and the pointer\n reaching this row is the earliest honest signal of that. It buys the\n children the length of one pointer travel plus one click, which is the\n difference between a child that renders and a child that waits: every\n route here is `prefetch={false}`, and a cold one was measured at 1.7s.\n Both handlers, because touch has no hover. */}\n <button\n type=\"button\"\n className={'navlink navhub' + (open ? ' open' : '')}\n onClick={() => {\n rows.forEach((c) => warm(c.href));\n setHubOpen(group.id, !open);\n }}\n onMouseEnter={() => rows.forEach((c) => warm(c.href))}\n onFocus={() => rows.forEach((c) => warm(c.href))}\n aria-expanded={open}\n aria-controls={open ? `navsub-${group.id}` : undefined}\n >\n <span className=\"ic\">{renderIcon(group.id, group.icon)}</span>\n <span className=\"lbl\">{group.label}</span>\n {!open && renderBadge(hubTotal(group))}\n <ChevronDown className=\"chev\" width={14} height={14} />\n </button>\n {/* No id-collision guard here any more. It existed because a business\n running several archetypes gets one `catalog` row per archetype, all\n sharing an id, so all three resolved to the same console glyph — and\n the row's own icon had to win. Children have no glyph to collide\n over now; their labels already differ. */}\n {/* THE CHILDREN GROW AND COLLAPSE, they do not blink.\n This was `{open && <ul>}` — a bare mount and unmount, so a hub's\n children appeared and vanished between one frame and the next while\n the chevron above them rotated over 200ms. One control, two\n different speeds, and the faster one was a cut.\n\n Height, not opacity alone: the rows below have to MOVE, and a fade\n over a jump still jumps. `height: auto` is the only honest target\n here because a hub's child count varies, and Motion measures it\n rather than us guessing a row pitch that breaks the moment a label\n wraps.\n\n Opacity finishes early on the way in and starts late on the way out,\n so the children are solid for most of the travel — content that\n fades over the full distance reads as sluggish even when the\n distance is right.\n\n The bottom margin travels WITH the height rather than sitting in\n the stylesheet. As CSS it would hold its full value all the way\n through the close and then vanish on the frame the list unmounts —\n the one snap in an otherwise continuous move, at the bottom edge\n where the rows below are already travelling. */}\n <AnimatePresence initial={false}>\n {open && (\n <motion.ul\n id={`navsub-${group.id}`}\n className=\"navsub\"\n initial={{ height: 0, opacity: 0, marginBottom: 0 }}\n animate={{ height: 'auto', opacity: 1, marginBottom: SUB_GAP }}\n exit={{ height: 0, opacity: 0, marginBottom: 0 }}\n transition={\n prefersReducedMotion()\n ? { duration: 0 }\n : {\n height: { duration: 0.24, ease: eases.settle },\n marginBottom: { duration: 0.24, ease: eases.settle },\n opacity: { duration: 0.16, ease: eases.out },\n }\n }\n // Clipped, or the rows spill past the collapsing box on the way\n // out and the whole thing reads as a glitch rather than a close.\n style={{ overflow: 'hidden' }}\n >\n {activeIndex >= 0 && (\n <SubHighlight key={group.id} index={activeIndex} />\n )}\n {/* THE HUB'S OWN COUNT FOLLOWS IT DOWN.\n John, 2026-09-03: *\"when I click on the parent nav, where\n does the notification thingy go, what sub nav is the\n notification from, why don't I see it\"*.\n\n Nowhere, was the answer. A count keyed on the HUB's id showed\n on the closed row and then vanished on open, because the\n landing child carries `${group.id}-landing` and nothing was\n looking there. So a merchant pressed the row to find out what\n the number meant and the number disappeared — which is worse\n than never showing it.\n\n Most counts belong to a real child and land on it by id;\n this is for the ones that belong to the hub itself, which is\n to say to its landing page. Low stock is the live example:\n it is about the catalogue, and the catalogue IS the hub's\n landing row. */}\n {rows.map((c, i) =>\n renderRow(\n c,\n true,\n landing.length && i === at ? badges?.[group.id] : undefined,\n ),\n )}\n </motion.ul>\n )}\n </AnimatePresence>\n </li>\n );\n };\n\n /**\n * A work-surface row, which is a hub when it carries children.\n *\n * The fixed spine already had this shape (Customers opens onto Inquiries and\n * Reviews); Operations rows could not, so anything attached to one had to\n * become its own top-level row or be reached from inside the page. Neither is\n * right for a queue like Waitlist: as a peer row it claims to be a second\n * equal destination, and buried in the page header it competes with the\n * page's own actions and pushed the Appointments title down to \"App…\".\n */\n const renderWorkRow = (item: NavItem) =>\n item.children?.length\n ? renderHub({\n id: item.id,\n label: item.name,\n icon: item.icon,\n href: item.href,\n items: item.children,\n alwaysHub: true,\n landingLabel: item.landingLabel,\n })\n : renderRow(item);\n\n // Settings sub-nav that drills into the sidebar (Back returns you out).\n return {\n renderIcon,\n renderHighlight,\n renderRow,\n renderHub,\n renderWorkRow,\n ownsRoute,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
3
|
+
import { useCallback, useId, useSyncExternalStore } from "react";
|
|
4
4
|
import { Check, ChevronDown, Settings2 } from "lucide-react";
|
|
5
5
|
import { EditableName } from "./rail-editable";
|
|
6
6
|
const STORAGE_KEY = "pb_nav_sections_closed";
|
|
@@ -47,6 +47,7 @@ function NavSection({
|
|
|
47
47
|
onDone
|
|
48
48
|
}) {
|
|
49
49
|
const key = foldKey ?? label;
|
|
50
|
+
const listId = useId();
|
|
50
51
|
const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);
|
|
51
52
|
const items = children.filter(Boolean);
|
|
52
53
|
const isClosed = !containsActive && closedMap[key] === true;
|
|
@@ -91,7 +92,7 @@ function NavSection({
|
|
|
91
92
|
type: "button",
|
|
92
93
|
onClick: onConfigure,
|
|
93
94
|
className: "navheading-gear",
|
|
94
|
-
"aria-label":
|
|
95
|
+
"aria-label": `Customize sidebar, ${label} section`,
|
|
95
96
|
title: "Customize sidebar",
|
|
96
97
|
children: /* @__PURE__ */ jsx(Settings2, { "aria-hidden": "true" })
|
|
97
98
|
}
|
|
@@ -103,6 +104,7 @@ function NavSection({
|
|
|
103
104
|
onClick: toggle,
|
|
104
105
|
className: "navheading navheading-toggle",
|
|
105
106
|
"aria-expanded": !isClosed,
|
|
107
|
+
"aria-controls": isClosed ? void 0 : listId,
|
|
106
108
|
children: [
|
|
107
109
|
/* @__PURE__ */ jsx("span", { children: label }),
|
|
108
110
|
/* @__PURE__ */ jsx(
|
|
@@ -115,7 +117,7 @@ function NavSection({
|
|
|
115
117
|
]
|
|
116
118
|
}
|
|
117
119
|
),
|
|
118
|
-
!isClosed && /* @__PURE__ */ jsx("ul", { className: "navgroup", children: items })
|
|
120
|
+
!isClosed && /* @__PURE__ */ jsx("ul", { id: listId, className: "navgroup", children: items })
|
|
119
121
|
] });
|
|
120
122
|
}
|
|
121
123
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label
|
|
1
|
+
{"version":3,"sources":["../../src/sidebar/nav-section.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useId, useSyncExternalStore } from 'react';\nimport { Check, ChevronDown, Settings2 } from 'lucide-react';\nimport { EditableName } from './rail-editable';\n\nconst STORAGE_KEY = 'pb_nav_sections_closed';\n\n/**\n * Which sections the merchant has folded away, as an external store.\n *\n * Not `useState` + `useEffect`: localStorage does not exist while the server\n * renders, so reading it into initial state mismatches the HTML, and reading it\n * in an effect is a synchronous setState in an effect — a cascading render the\n * lint rule rejects. `useSyncExternalStore` is built for exactly this, and it\n * is the pattern the home money snapshot already uses here.\n */\nlet closedCache: Record<string, boolean> = {};\nlet closedRaw: string | null = null;\nconst listeners = new Set<() => void>();\n\nfunction readClosed(): Record<string, boolean> {\n if (typeof window === 'undefined') return closedCache;\n const raw = localStorage.getItem(STORAGE_KEY);\n // Cached by raw string so the snapshot is referentially stable between\n // renders — a fresh object every call makes useSyncExternalStore loop.\n if (raw !== closedRaw) {\n closedRaw = raw;\n try {\n closedCache = raw ? JSON.parse(raw) : {};\n } catch {\n closedCache = {};\n }\n }\n return closedCache;\n}\n\n/** The server has no localStorage, so nothing is folded on first paint. */\nconst serverSnapshot = () => closedCache;\n\nfunction subscribe(fn: () => void) {\n listeners.add(fn);\n return () => listeners.delete(fn);\n}\n\nfunction setSectionClosed(label: string, closed: boolean) {\n const next = { ...readClosed(), [label]: closed };\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n } catch {\n /* private mode — the choice just won't survive the visit */\n }\n closedRaw = JSON.stringify(next);\n closedCache = next;\n listeners.forEach((fn) => fn());\n}\n\n/**\n * A nav section — \"Operations\", \"Grow\", \"Build\" — that can be folded away.\n *\n * A heading earns its row by grouping. Over one item it costs a line of height\n * and a label restating what the item already says, so below two children the\n * section renders as a plain run of rows with no heading and nothing to toggle:\n * there is no point offering to hide a single link behind a chevron.\n *\n * Folding is the merchant's choice and it sticks, per section, across visits.\n * But a section is FORCED OPEN while it owns the current route — otherwise the\n * page you are on has no highlighted row anywhere in the nav, which reads as\n * the sidebar having lost your place rather than as a section being shut.\n *\n * On the collapsed icon-only rail there are no headings, so there is nothing to\n * fold and the rows render bare.\n */\nexport function NavSection({\n label,\n children,\n collapsed,\n containsActive,\n keepLabel = false,\n onConfigure,\n foldKey,\n onRenameLabel,\n onDone,\n}: {\n label: string;\n children: React.ReactNode[];\n /** The icon-only rail: no headings, so no toggle. */\n collapsed: boolean;\n /** The current route lives in this section — it cannot be folded shut. */\n containsActive: boolean;\n /**\n * Keep the heading over a single row.\n *\n * The default below drops it, and that rule is right almost everywhere: a\n * heading over one item usually restates the item (\"Build · Website\"), which\n * is the case this file was written to stop.\n *\n * The exception is a heading that answers a DIFFERENT question from its row.\n * \"Your business · Add more\" is the one in the app: the row names the act\n * and the heading names the scope, and the scope is the part a merchant was\n * missing — the label read \"Add to Box\" until somebody pointed out that a\n * shop is not adding to Box. Dropping the heading there would take the\n * answer away again.\n *\n * Opt-in rather than a judgement the component makes, because it cannot tell\n * the two apart and the caller can.\n */\n keepLabel?: boolean;\n /**\n * Opens the sidebar editor (reorder, hide, rename). Shown as a gear beside\n * the chevron when the pointer or focus is on the heading, so a resting\n * sidebar stays quiet. A sibling of the toggle, not inside it: a button\n * cannot hold a button.\n */\n onConfigure?: () => void;\n /**\n * What the fold state is remembered under. Defaults to `label`, which is\n * right until the merchant renames the section: then the label changes and\n * the remembered fold would be lost, so the caller pins it to the default.\n */\n foldKey?: string;\n /**\n * Present only while the rail is being edited: the heading becomes a field\n * and the section stays open (there is nothing to fold while you edit).\n */\n onRenameLabel?: (next: string) => void;\n /** While editing: leaves edit mode. Drawn on the heading being edited. */\n onDone?: () => void;\n}) {\n const key = foldKey ?? label;\n const listId = useId();\n const closedMap = useSyncExternalStore(subscribe, readClosed, serverSnapshot);\n const items = children.filter(Boolean);\n const isClosed = !containsActive && closedMap[key] === true;\n const toggle = useCallback(\n () => setSectionClosed(key, !isClosed),\n [key, isClosed],\n );\n\n if (!items.length) return null;\n\n // One item: a heading would only restate the row beneath it — unless the\n // caller says the heading carries something the row does not. See `keepLabel`.\n if ((items.length === 1 && !keepLabel) || collapsed) {\n return (\n <div className=\"navsection-plain\">\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n if (onRenameLabel) {\n return (\n <div className=\"navsection\">\n <div className=\"navheading nav-edit-heading\">\n <EditableName\n value={label}\n label={`Rename section ${label}`}\n onCommit={onRenameLabel}\n />\n {onDone && (\n <button\n type=\"button\"\n onClick={onDone}\n className=\"nav-edit-done\"\n aria-label=\"Done customizing\"\n title=\"Done\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <ul className=\"navgroup\">{items}</ul>\n </div>\n );\n }\n\n return (\n <div className=\"navsection\">\n {onConfigure && (\n <button\n type=\"button\"\n onClick={onConfigure}\n className=\"navheading-gear\"\n aria-label={`Customize sidebar, ${label} section`}\n title=\"Customize sidebar\"\n >\n <Settings2 aria-hidden=\"true\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={toggle}\n className=\"navheading navheading-toggle\"\n aria-expanded={!isClosed}\n aria-controls={isClosed ? undefined : listId}\n >\n <span>{label}</span>\n <ChevronDown\n className={'navheading-chevron' + (isClosed ? ' closed' : '')}\n aria-hidden=\"true\"\n />\n </button>\n {!isClosed && (\n <ul id={listId} className=\"navgroup\">\n {items}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/components",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.86",
|
|
4
4
|
"description": "Box dashboard feature components: editors, mobile screens, pickers, and composite UI built on @withpotter/ui-kit.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"sideEffects": [
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"@tiptap/react": "^3.22.5",
|
|
51
51
|
"@tiptap/starter-kit": "^3.22.5",
|
|
52
52
|
"@tiptap/suggestion": "^3.22.5",
|
|
53
|
-
"@withpotter/hooks": "^0.13.0-next.
|
|
54
|
-
"@withpotter/lib": "^0.13.0-next.
|
|
55
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
53
|
+
"@withpotter/hooks": "^0.13.0-next.86",
|
|
54
|
+
"@withpotter/lib": "^0.13.0-next.86",
|
|
55
|
+
"@withpotter/ui-kit": "^0.13.0-next.86",
|
|
56
56
|
"@xyflow/react": "^12.11.2",
|
|
57
57
|
"dompurify": "^3.3.1",
|
|
58
58
|
"jsqr": "1.4.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"typescript": "^5"
|
|
78
78
|
},
|
|
79
79
|
"publishConfig": {
|
|
80
|
-
"registry": "https://registry.npmjs.org
|
|
80
|
+
"registry": "https://registry.npmjs.org",
|
|
81
81
|
"access": "restricted"
|
|
82
82
|
}
|
|
83
83
|
}
|