@withpotter/components 0.13.0-next.108 → 0.13.0-next.110

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.
@@ -9,6 +9,8 @@ import type { LucideIcon } from 'lucide-react';
9
9
  export interface HostSettingsRow {
10
10
  id: string;
11
11
  name: string;
12
+ /** The line under the title when the row is open. */
13
+ subtitle?: string;
12
14
  icon: LucideIcon;
13
15
  /** Addresses this row owns: a link to any of them opens the window on it. */
14
16
  paths: string[];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-host.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\n\n/**\n * A row a host adds to the settings window, with the screen behind it.\n *\n * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.\n * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.\n */\nexport interface HostSettingsRow {\n id: string;\n name: string;\n icon: LucideIcon;\n /** Addresses this row owns: a link to any of them opens the window on it. */\n paths: string[];\n /** What search also matches. */\n keywords: string[];\n /** The row it is placed after. Default: after General. */\n after?: string;\n /** The screen drawn in the window. */\n render: () => ReactNode;\n}\n\nconst Context = createContext<HostSettingsRow[]>([]);\nconst GeneralSlotContext = createContext<(() => ReactNode) | null>(null);\n\nexport function SettingsHostProvider({\n rows,\n generalRows,\n children,\n}: {\n rows: HostSettingsRow[];\n /**\n * Rows a host adds to the Business card of General, below the business's own details. Olive shows its CAC\n * certificate there. A host that passes nothing gets the card it had.\n */\n generalRows?: () => ReactNode;\n children: ReactNode;\n}) {\n return (\n <Context.Provider value={rows}>\n <GeneralSlotContext.Provider value={generalRows ?? null}>\n {children}\n </GeneralSlotContext.Provider>\n </Context.Provider>\n );\n}\n\n/** What a host drew into a slot, or null when there is no host, or the host has nothing to show right now. */\nexport function slotContent(render: (() => ReactNode) | null | undefined): ReactNode {\n if (!render) return null;\n\n const content = render();\n\n return content === undefined || content === false ? null : content;\n}\n\n/** The host's rows for the Business card of General, drawn by the card itself. */\nexport function useGeneralHostRows(): ReactNode {\n return slotContent(useContext(GeneralSlotContext));\n}\n\nexport function useSettingsHostRows(): HostSettingsRow[] {\n return useContext(Context);\n}\n\n/** The host row that owns an address: the address is one of its paths or sits below one. */\nexport function hostRowForHref(\n rows: HostSettingsRow[],\n href: string | null | undefined,\n): HostSettingsRow | undefined {\n const path = (href ?? '').split(/[?#]/)[0];\n\n return rows.find((row) =>\n row.paths.some((p) => path === p || path.startsWith(`${p}/`)),\n );\n}\n"],"mappings":";AA2CM;AAzCN,SAAS,eAAe,kBAAkC;AAuB1D,MAAM,UAAU,cAAiC,CAAC,CAAC;AACnD,MAAM,qBAAqB,cAAwC,IAAI;AAEhE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE,oBAAC,QAAQ,UAAR,EAAiB,OAAO,MACvB,8BAAC,mBAAmB,UAAnB,EAA4B,OAAO,eAAe,MAChD,UACH,GACF;AAEJ;AAGO,SAAS,YAAY,QAAyD;AACnF,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,UAAU,OAAO;AAEvB,SAAO,YAAY,UAAa,YAAY,QAAQ,OAAO;AAC7D;AAGO,SAAS,qBAAgC;AAC9C,SAAO,YAAY,WAAW,kBAAkB,CAAC;AACnD;AAEO,SAAS,sBAAyC;AACvD,SAAO,WAAW,OAAO;AAC3B;AAGO,SAAS,eACd,MACA,MAC6B;AAC7B,QAAM,QAAQ,QAAQ,IAAI,MAAM,MAAM,EAAE,CAAC;AAEzC,SAAO,KAAK;AAAA,IAAK,CAAC,QAChB,IAAI,MAAM,KAAK,CAAC,MAAM,SAAS,KAAK,KAAK,WAAW,GAAG,CAAC,GAAG,CAAC;AAAA,EAC9D;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-host.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\n\n/**\n * A row a host adds to the settings window, with the screen behind it.\n *\n * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.\n * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.\n */\nexport interface HostSettingsRow {\n id: string;\n name: string;\n /** The line under the title when the row is open. */\n subtitle?: string;\n icon: LucideIcon;\n /** Addresses this row owns: a link to any of them opens the window on it. */\n paths: string[];\n /** What search also matches. */\n keywords: string[];\n /** The row it is placed after. Default: after General. */\n after?: string;\n /** The screen drawn in the window. */\n render: () => ReactNode;\n}\n\nconst Context = createContext<HostSettingsRow[]>([]);\nconst GeneralSlotContext = createContext<(() => ReactNode) | null>(null);\n\nexport function SettingsHostProvider({\n rows,\n generalRows,\n children,\n}: {\n rows: HostSettingsRow[];\n /**\n * Rows a host adds to the Business card of General, below the business's own details. Olive shows its CAC\n * certificate there. A host that passes nothing gets the card it had.\n */\n generalRows?: () => ReactNode;\n children: ReactNode;\n}) {\n return (\n <Context.Provider value={rows}>\n <GeneralSlotContext.Provider value={generalRows ?? null}>\n {children}\n </GeneralSlotContext.Provider>\n </Context.Provider>\n );\n}\n\n/** What a host drew into a slot, or null when there is no host, or the host has nothing to show right now. */\nexport function slotContent(render: (() => ReactNode) | null | undefined): ReactNode {\n if (!render) return null;\n\n const content = render();\n\n return content === undefined || content === false ? null : content;\n}\n\n/** The host's rows for the Business card of General, drawn by the card itself. */\nexport function useGeneralHostRows(): ReactNode {\n return slotContent(useContext(GeneralSlotContext));\n}\n\nexport function useSettingsHostRows(): HostSettingsRow[] {\n return useContext(Context);\n}\n\n/** The host row that owns an address: the address is one of its paths or sits below one. */\nexport function hostRowForHref(\n rows: HostSettingsRow[],\n href: string | null | undefined,\n): HostSettingsRow | undefined {\n const path = (href ?? '').split(/[?#]/)[0];\n\n return rows.find((row) =>\n row.paths.some((p) => path === p || path.startsWith(`${p}/`)),\n );\n}\n"],"mappings":";AA6CM;AA3CN,SAAS,eAAe,kBAAkC;AAyB1D,MAAM,UAAU,cAAiC,CAAC,CAAC;AACnD,MAAM,qBAAqB,cAAwC,IAAI;AAEhE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE,oBAAC,QAAQ,UAAR,EAAiB,OAAO,MACvB,8BAAC,mBAAmB,UAAnB,EAA4B,OAAO,eAAe,MAChD,UACH,GACF;AAEJ;AAGO,SAAS,YAAY,QAAyD;AACnF,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,UAAU,OAAO;AAEvB,SAAO,YAAY,UAAa,YAAY,QAAQ,OAAO;AAC7D;AAGO,SAAS,qBAAgC;AAC9C,SAAO,YAAY,WAAW,kBAAkB,CAAC;AACnD;AAEO,SAAS,sBAAyC;AACvD,SAAO,WAAW,OAAO;AAC3B;AAGO,SAAS,eACd,MACA,MAC6B;AAC7B,QAAM,QAAQ,QAAQ,IAAI,MAAM,MAAM,EAAE,CAAC;AAEzC,SAAO,KAAK;AAAA,IAAK,CAAC,QAChB,IAAI,MAAM,KAAK,CAAC,MAAM,SAAS,KAAK,KAAK,WAAW,GAAG,CAAC,GAAG,CAAC;AAAA,EAC9D;AACF;","names":[]}
@@ -11,6 +11,7 @@ import { useAccessibility } from "@withpotter/lib/accessibility-context";
11
11
  import { usePermissions } from "@withpotter/lib/use-permissions";
12
12
  import { RotateCcw } from "lucide-react";
13
13
  import { AccessibilityCard } from "@withpotter/pages-core/dashboard/settings/accessibility/_components/accessibility-card";
14
+ import { TaxesSettingsContent } from "@withpotter/pages-core/dashboard/settings/taxes/_components/taxes-content";
14
15
  import { PoliciesContent } from "@withpotter/pages-core/dashboard/settings/policies/_components/policies-content";
15
16
  import { UsageContent } from "@withpotter/pages-core/dashboard/settings/usage/_components/usage-content";
16
17
  import { GeneralContent } from "@withpotter/pages-core/dashboard/settings/_components/general-content";
@@ -87,6 +88,7 @@ const PANELS = {
87
88
  preferences: () => /* @__PURE__ */ jsx(PreferencesPanel, {}),
88
89
  "payment-approvals": () => /* @__PURE__ */ jsx(PaymentApprovalsContent, {}),
89
90
  billing: () => /* @__PURE__ */ jsx(BillingContent, {}),
91
+ taxes: () => /* @__PURE__ */ jsx(TaxesSettingsContent, {}),
90
92
  usage: () => /* @__PURE__ */ jsx(UsageContent, {}),
91
93
  policies: () => /* @__PURE__ */ jsx(PoliciesContent, {}),
92
94
  legal: () => /* @__PURE__ */ jsx(LegalContent, {})
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-panels.tsx"],"sourcesContent":["'use client';\n\nimport { StateView } from '@withpotter/ui-kit/state-view';\nimport { Suspense, type ReactNode } from 'react';\nimport { ProfileContent } from '@withpotter/pages-core/dashboard/settings/profile/_components/profile-content';\nimport { SecurityContent } from '@withpotter/pages-core/dashboard/settings/profile/_components/security-content';\nimport { SignInContent } from '@withpotter/pages-core/dashboard/settings/devices/_components/sign-in-content';\nimport { DataPrivacyContent } from '@withpotter/pages-core/dashboard/settings/data-privacy/_components/data-privacy-content';\nimport { PreferencesContent } from '@withpotter/pages-core/dashboard/settings/language/_components/preferences-content';\nimport { useAccessibility } from '@withpotter/lib/accessibility-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { RotateCcw } from 'lucide-react';\nimport { AccessibilityCard } from '@withpotter/pages-core/dashboard/settings/accessibility/_components/accessibility-card';\nimport { PoliciesContent } from '@withpotter/pages-core/dashboard/settings/policies/_components/policies-content';\nimport { UsageContent } from '@withpotter/pages-core/dashboard/settings/usage/_components/usage-content';\nimport { GeneralContent } from '@withpotter/pages-core/dashboard/settings/_components/general-content';\nimport { NotificationsContent } from '@withpotter/pages-core/dashboard/settings/notifications/_components/notifications-content';\nimport { TeamContent } from '@withpotter/pages-core/dashboard/settings/team/_components/team-content';\nimport { BillingContent } from '@withpotter/pages-core/dashboard/settings/billing/_components/billing-content';\nimport { LegalContent } from '@withpotter/pages-core/dashboard/settings/legal/_components/legal-content';\nimport { PaymentApprovalsContent } from '@withpotter/pages-core/dashboard/settings/payment-approvals/_components/payment-approvals-content';\nimport { DemoContentCard } from '@withpotter/pages-core/dashboard/settings/demo-content/_components/demo-content-card';\nimport { FeaturesContent } from '@withpotter/pages-core/dashboard/settings/features/_components/features-content';\nimport type { SettingsRowId } from './settings-rows';\n\n/**\n * The page behind a row, drawn in the window.\n *\n * The business rows render the real settings page, so each keeps its own\n * loading, saving and sections. They are loaded on first open of a row rather\n * than with the window, and each is a client page that reads the same hooks it\n * always did.\n */\n/** What a row shows while its code loads, so a first open is never blank. */\nconst loading = () => <StateView kind=\"loading\" title=\"Loading\" />;\n\n/** A section heading inside a merged page. */\nfunction Section({\n title,\n action,\n children,\n}: {\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <section className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3 px-4\">\n <h3 className=\"text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </h3>\n {action}\n </div>\n {children}\n </section>\n );\n}\n\n/**\n * Who the person is: their profile. Closing the account is last, as it is the\n * one thing here that cannot be undone.\n */\nfunction ProfilePanel() {\n return (\n <div className=\"space-y-8\">\n <ProfileContent />\n <SecurityContent />\n </div>\n );\n}\n\n/** How the person signs in and where, and what is kept about them. */\nfunction SecurityPanel() {\n return (\n <div className=\"space-y-8\">\n <Section title=\"Sign-in and security\">\n <div className=\"px-4\">\n <SignInContent />\n </div>\n </Section>\n <Section title=\"Data & privacy\">\n <DataPrivacyContent />\n </Section>\n </div>\n );\n}\n\n/**\n * The switches (Blog today) and the sample content, in one place. The switches\n * are shown only to someone who may change them; the demo card decides for\n * itself who sees its actions.\n */\nfunction ExtrasPanel() {\n const { can } = usePermissions();\n return (\n <div className=\"space-y-8\">\n {can('canManageSettings') && <FeaturesContent />}\n <Section title=\"Sample content\">\n <DemoContentCard />\n </Section>\n </div>\n );\n}\n\n/** Reset to defaults was a page header action; here it sits on its section. */\nfunction ResetAccessibility() {\n const a11y = useAccessibility();\n return (\n <button\n type=\"button\"\n onClick={a11y.reset}\n className=\"inline-flex items-center gap-2 text-xs font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <RotateCcw className=\"size-4\" />\n Reset to defaults\n </button>\n );\n}\n\n/** How the app looks and behaves for the person using it. */\nfunction PreferencesPanel() {\n return (\n <PreferencesContent\n accessibility={<AccessibilityCard action={<ResetAccessibility />} />}\n />\n );\n}\n\nconst PANELS: Record<SettingsRowId, () => ReactNode> = {\n general: () => <GeneralContent />,\n extras: () => <ExtrasPanel />,\n profile: () => <ProfilePanel />,\n security: () => <SecurityPanel />,\n team: () => <TeamContent />,\n notifications: () => <NotificationsContent />,\n preferences: () => <PreferencesPanel />,\n 'payment-approvals': () => <PaymentApprovalsContent />,\n billing: () => <BillingContent />,\n usage: () => <UsageContent />,\n policies: () => <PoliciesContent />,\n legal: () => <LegalContent />,\n};\n\nexport function SettingsPanel({ id }: { id: SettingsRowId }) {\n // Some pages read the query string, which needs a boundary above it.\n return <Suspense fallback={loading()}>{PANELS[id]()}</Suspense>;\n}\n"],"mappings":";AAkCsB,cAchB,YAdgB;AAhCtB,SAAS,iBAAiB;AAC1B,SAAS,gBAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB;AAC1B,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAC5B,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,+BAA+B;AACxC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAYhC,MAAM,UAAU,MAAM,oBAAC,aAAU,MAAK,WAAU,OAAM,WAAU;AAGhE,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,aAAQ,WAAU,aACjB;AAAA,yBAAC,SAAI,WAAU,gDACb;AAAA,0BAAC,QAAG,WAAU,oDACX,iBACH;AAAA,MACC;AAAA,OACH;AAAA,IACC;AAAA,KACH;AAEJ;AAMA,SAAS,eAAe;AACtB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,kBAAe;AAAA,IAChB,oBAAC,mBAAgB;AAAA,KACnB;AAEJ;AAGA,SAAS,gBAAgB;AACvB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,wBACb,8BAAC,SAAI,WAAU,QACb,8BAAC,iBAAc,GACjB,GACF;AAAA,IACA,oBAAC,WAAQ,OAAM,kBACb,8BAAC,sBAAmB,GACtB;AAAA,KACF;AAEJ;AAOA,SAAS,cAAc;AACrB,QAAM,EAAE,IAAI,IAAI,eAAe;AAC/B,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,QAAI,mBAAmB,KAAK,oBAAC,mBAAgB;AAAA,IAC9C,oBAAC,WAAQ,OAAM,kBACb,8BAAC,mBAAgB,GACnB;AAAA,KACF;AAEJ;AAGA,SAAS,qBAAqB;AAC5B,QAAM,OAAO,iBAAiB;AAC9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,KAAK;AAAA,MACd,WAAU;AAAA,MAEV;AAAA,4BAAC,aAAU,WAAU,UAAS;AAAA,QAAE;AAAA;AAAA;AAAA,EAElC;AAEJ;AAGA,SAAS,mBAAmB;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAe,oBAAC,qBAAkB,QAAQ,oBAAC,sBAAmB,GAAI;AAAA;AAAA,EACpE;AAEJ;AAEA,MAAM,SAAiD;AAAA,EACrD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,QAAQ,MAAM,oBAAC,eAAY;AAAA,EAC3B,SAAS,MAAM,oBAAC,gBAAa;AAAA,EAC7B,UAAU,MAAM,oBAAC,iBAAc;AAAA,EAC/B,MAAM,MAAM,oBAAC,eAAY;AAAA,EACzB,eAAe,MAAM,oBAAC,wBAAqB;AAAA,EAC3C,aAAa,MAAM,oBAAC,oBAAiB;AAAA,EACrC,qBAAqB,MAAM,oBAAC,2BAAwB;AAAA,EACpD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,OAAO,MAAM,oBAAC,gBAAa;AAAA,EAC3B,UAAU,MAAM,oBAAC,mBAAgB;AAAA,EACjC,OAAO,MAAM,oBAAC,gBAAa;AAC7B;AAEO,SAAS,cAAc,EAAE,GAAG,GAA0B;AAE3D,SAAO,oBAAC,YAAS,UAAU,QAAQ,GAAI,iBAAO,EAAE,EAAE,GAAE;AACtD;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-panels.tsx"],"sourcesContent":["'use client';\n\nimport { StateView } from '@withpotter/ui-kit/state-view';\nimport { Suspense, type ReactNode } from 'react';\nimport { ProfileContent } from '@withpotter/pages-core/dashboard/settings/profile/_components/profile-content';\nimport { SecurityContent } from '@withpotter/pages-core/dashboard/settings/profile/_components/security-content';\nimport { SignInContent } from '@withpotter/pages-core/dashboard/settings/devices/_components/sign-in-content';\nimport { DataPrivacyContent } from '@withpotter/pages-core/dashboard/settings/data-privacy/_components/data-privacy-content';\nimport { PreferencesContent } from '@withpotter/pages-core/dashboard/settings/language/_components/preferences-content';\nimport { useAccessibility } from '@withpotter/lib/accessibility-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { RotateCcw } from 'lucide-react';\nimport { AccessibilityCard } from '@withpotter/pages-core/dashboard/settings/accessibility/_components/accessibility-card';\nimport { TaxesSettingsContent } from '@withpotter/pages-core/dashboard/settings/taxes/_components/taxes-content';\nimport { PoliciesContent } from '@withpotter/pages-core/dashboard/settings/policies/_components/policies-content';\nimport { UsageContent } from '@withpotter/pages-core/dashboard/settings/usage/_components/usage-content';\nimport { GeneralContent } from '@withpotter/pages-core/dashboard/settings/_components/general-content';\nimport { NotificationsContent } from '@withpotter/pages-core/dashboard/settings/notifications/_components/notifications-content';\nimport { TeamContent } from '@withpotter/pages-core/dashboard/settings/team/_components/team-content';\nimport { BillingContent } from '@withpotter/pages-core/dashboard/settings/billing/_components/billing-content';\nimport { LegalContent } from '@withpotter/pages-core/dashboard/settings/legal/_components/legal-content';\nimport { PaymentApprovalsContent } from '@withpotter/pages-core/dashboard/settings/payment-approvals/_components/payment-approvals-content';\nimport { DemoContentCard } from '@withpotter/pages-core/dashboard/settings/demo-content/_components/demo-content-card';\nimport { FeaturesContent } from '@withpotter/pages-core/dashboard/settings/features/_components/features-content';\nimport type { SettingsRowId } from './settings-rows';\n\n/**\n * The page behind a row, drawn in the window.\n *\n * The business rows render the real settings page, so each keeps its own\n * loading, saving and sections. They are loaded on first open of a row rather\n * than with the window, and each is a client page that reads the same hooks it\n * always did.\n */\n/** What a row shows while its code loads, so a first open is never blank. */\nconst loading = () => <StateView kind=\"loading\" title=\"Loading\" />;\n\n/** A section heading inside a merged page. */\nfunction Section({\n title,\n action,\n children,\n}: {\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <section className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3 px-4\">\n <h3 className=\"text-sm font-semibold text-[var(--text-primary)]\">\n {title}\n </h3>\n {action}\n </div>\n {children}\n </section>\n );\n}\n\n/**\n * Who the person is: their profile. Closing the account is last, as it is the\n * one thing here that cannot be undone.\n */\nfunction ProfilePanel() {\n return (\n <div className=\"space-y-8\">\n <ProfileContent />\n <SecurityContent />\n </div>\n );\n}\n\n/** How the person signs in and where, and what is kept about them. */\nfunction SecurityPanel() {\n return (\n <div className=\"space-y-8\">\n <Section title=\"Sign-in and security\">\n <div className=\"px-4\">\n <SignInContent />\n </div>\n </Section>\n <Section title=\"Data & privacy\">\n <DataPrivacyContent />\n </Section>\n </div>\n );\n}\n\n/**\n * The switches (Blog today) and the sample content, in one place. The switches\n * are shown only to someone who may change them; the demo card decides for\n * itself who sees its actions.\n */\nfunction ExtrasPanel() {\n const { can } = usePermissions();\n return (\n <div className=\"space-y-8\">\n {can('canManageSettings') && <FeaturesContent />}\n <Section title=\"Sample content\">\n <DemoContentCard />\n </Section>\n </div>\n );\n}\n\n/** Reset to defaults was a page header action; here it sits on its section. */\nfunction ResetAccessibility() {\n const a11y = useAccessibility();\n return (\n <button\n type=\"button\"\n onClick={a11y.reset}\n className=\"inline-flex items-center gap-2 text-xs font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <RotateCcw className=\"size-4\" />\n Reset to defaults\n </button>\n );\n}\n\n/** How the app looks and behaves for the person using it. */\nfunction PreferencesPanel() {\n return (\n <PreferencesContent\n accessibility={<AccessibilityCard action={<ResetAccessibility />} />}\n />\n );\n}\n\nconst PANELS: Record<SettingsRowId, () => ReactNode> = {\n general: () => <GeneralContent />,\n extras: () => <ExtrasPanel />,\n profile: () => <ProfilePanel />,\n security: () => <SecurityPanel />,\n team: () => <TeamContent />,\n notifications: () => <NotificationsContent />,\n preferences: () => <PreferencesPanel />,\n 'payment-approvals': () => <PaymentApprovalsContent />,\n billing: () => <BillingContent />,\n taxes: () => <TaxesSettingsContent />,\n usage: () => <UsageContent />,\n policies: () => <PoliciesContent />,\n legal: () => <LegalContent />,\n};\n\nexport function SettingsPanel({ id }: { id: SettingsRowId }) {\n // Some pages read the query string, which needs a boundary above it.\n return <Suspense fallback={loading()}>{PANELS[id]()}</Suspense>;\n}\n"],"mappings":";AAmCsB,cAchB,YAdgB;AAjCtB,SAAS,iBAAiB;AAC1B,SAAS,gBAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB;AAC1B,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAC5B,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,+BAA+B;AACxC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAYhC,MAAM,UAAU,MAAM,oBAAC,aAAU,MAAK,WAAU,OAAM,WAAU;AAGhE,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,aAAQ,WAAU,aACjB;AAAA,yBAAC,SAAI,WAAU,gDACb;AAAA,0BAAC,QAAG,WAAU,oDACX,iBACH;AAAA,MACC;AAAA,OACH;AAAA,IACC;AAAA,KACH;AAEJ;AAMA,SAAS,eAAe;AACtB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,kBAAe;AAAA,IAChB,oBAAC,mBAAgB;AAAA,KACnB;AAEJ;AAGA,SAAS,gBAAgB;AACvB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,wBACb,8BAAC,SAAI,WAAU,QACb,8BAAC,iBAAc,GACjB,GACF;AAAA,IACA,oBAAC,WAAQ,OAAM,kBACb,8BAAC,sBAAmB,GACtB;AAAA,KACF;AAEJ;AAOA,SAAS,cAAc;AACrB,QAAM,EAAE,IAAI,IAAI,eAAe;AAC/B,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,QAAI,mBAAmB,KAAK,oBAAC,mBAAgB;AAAA,IAC9C,oBAAC,WAAQ,OAAM,kBACb,8BAAC,mBAAgB,GACnB;AAAA,KACF;AAEJ;AAGA,SAAS,qBAAqB;AAC5B,QAAM,OAAO,iBAAiB;AAC9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,KAAK;AAAA,MACd,WAAU;AAAA,MAEV;AAAA,4BAAC,aAAU,WAAU,UAAS;AAAA,QAAE;AAAA;AAAA;AAAA,EAElC;AAEJ;AAGA,SAAS,mBAAmB;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAe,oBAAC,qBAAkB,QAAQ,oBAAC,sBAAmB,GAAI;AAAA;AAAA,EACpE;AAEJ;AAEA,MAAM,SAAiD;AAAA,EACrD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,QAAQ,MAAM,oBAAC,eAAY;AAAA,EAC3B,SAAS,MAAM,oBAAC,gBAAa;AAAA,EAC7B,UAAU,MAAM,oBAAC,iBAAc;AAAA,EAC/B,MAAM,MAAM,oBAAC,eAAY;AAAA,EACzB,eAAe,MAAM,oBAAC,wBAAqB;AAAA,EAC3C,aAAa,MAAM,oBAAC,oBAAiB;AAAA,EACrC,qBAAqB,MAAM,oBAAC,2BAAwB;AAAA,EACpD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,OAAO,MAAM,oBAAC,wBAAqB;AAAA,EACnC,OAAO,MAAM,oBAAC,gBAAa;AAAA,EAC3B,UAAU,MAAM,oBAAC,mBAAgB;AAAA,EACjC,OAAO,MAAM,oBAAC,gBAAa;AAC7B;AAEO,SAAS,cAAc,EAAE,GAAG,GAA0B;AAE3D,SAAO,oBAAC,YAAS,UAAU,QAAQ,GAAI,iBAAO,EAAE,EAAE,GAAE;AACtD;","names":[]}
@@ -12,10 +12,12 @@ import type { PermissionKey } from '@withpotter/lib/use-permissions';
12
12
  * `keywords` are what search also matches: the names of the sections inside a
13
13
  * row, so "passkeys" or "time zone" finds the row that holds them.
14
14
  */
15
- export type SettingsRowId = 'general' | 'extras' | 'profile' | 'security' | 'team' | 'notifications' | 'preferences' | 'payment-approvals' | 'billing' | 'usage' | 'policies' | 'legal';
15
+ export type SettingsRowId = 'general' | 'extras' | 'profile' | 'security' | 'team' | 'notifications' | 'preferences' | 'payment-approvals' | 'billing' | 'taxes' | 'usage' | 'policies' | 'legal';
16
16
  export interface SettingsRow {
17
17
  id: SettingsRowId;
18
18
  name: string;
19
+ /** The line under the title when the row is open. */
20
+ subtitle?: string;
19
21
  icon: LucideIcon;
20
22
  paths: string[];
21
23
  keywords: string[];
@@ -7,6 +7,7 @@ import {
7
7
  CreditCard,
8
8
  FileText,
9
9
  Lock,
10
+ ReceiptText,
10
11
  Scale,
11
12
  Sparkles,
12
13
  SlidersHorizontal,
@@ -16,14 +17,25 @@ import {
16
17
  const BILLING_ROW = {
17
18
  id: "billing",
18
19
  name: "Billing & plan",
20
+ subtitle: "Your plan, how you pay, and your payments.",
19
21
  icon: CreditCard,
20
22
  paths: ["/dashboard/settings/billing"],
21
23
  keywords: ["plan", "invoice", "credits", "top-up", "cancel"],
22
24
  permission: ["canManageSettings"]
23
25
  };
26
+ const TAXES_ROW = {
27
+ id: "taxes",
28
+ name: "Taxes",
29
+ subtitle: "What you charge, and the number shown on receipts.",
30
+ icon: ReceiptText,
31
+ paths: ["/dashboard/settings/taxes"],
32
+ keywords: ["vat", "tax rate", "registration number", "receipts"],
33
+ permission: ["canManageSettings"]
34
+ };
24
35
  const PAYMENT_APPROVALS_ROW = {
25
36
  id: "payment-approvals",
26
37
  name: "Payment approvals",
38
+ subtitle: "Who approves money going out.",
27
39
  icon: CircleCheck,
28
40
  paths: ["/dashboard/settings/payment-approvals"],
29
41
  keywords: ["approval", "accepted", "rejected", "decisions", "history"]
@@ -32,6 +44,7 @@ const SETTINGS_ROWS = [
32
44
  {
33
45
  id: "general",
34
46
  name: "General",
47
+ subtitle: "Your business details and store address.",
35
48
  icon: Building2,
36
49
  paths: [
37
50
  "/dashboard/settings",
@@ -43,9 +56,12 @@ const SETTINGS_ROWS = [
43
56
  },
44
57
  // A licensed box that bills outside the console hides the row.
45
58
  ...process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === "true" ? [] : [BILLING_ROW],
59
+ // A box that keeps a tax (VAT) setting has it as its own row, after the plan.
60
+ ...process.env.NEXT_PUBLIC_BOX_TAXES_IN_POLICIES === "true" ? [TAXES_ROW] : [],
46
61
  {
47
62
  id: "team",
48
63
  name: "Team & permissions",
64
+ subtitle: "Who can do what in this business.",
49
65
  icon: Users,
50
66
  paths: [
51
67
  "/dashboard/settings/team",
@@ -62,6 +78,7 @@ const SETTINGS_ROWS = [
62
78
  {
63
79
  id: "policies",
64
80
  name: "Policies",
81
+ subtitle: "What your customers agree to.",
65
82
  icon: Scale,
66
83
  paths: ["/dashboard/settings/policies"],
67
84
  keywords: ["returns", "shipping", "privacy", "terms"],
@@ -76,6 +93,7 @@ const SETTINGS_ROWS = [
76
93
  // and admins.
77
94
  id: "extras",
78
95
  name: "Extras",
96
+ subtitle: "Switches and sample content for your store.",
79
97
  icon: Sparkles,
80
98
  paths: [
81
99
  "/dashboard/settings/features",
@@ -100,6 +118,7 @@ const SETTINGS_ROWS = [
100
118
  {
101
119
  id: "profile",
102
120
  name: "Profile",
121
+ subtitle: "Your name and how we reach you.",
103
122
  icon: UserRound,
104
123
  paths: ["/dashboard/settings/profile"],
105
124
  keywords: ["name", "email", "photo", "phone", "close account"]
@@ -107,6 +126,7 @@ const SETTINGS_ROWS = [
107
126
  {
108
127
  id: "security",
109
128
  name: "Security",
129
+ subtitle: "How you sign in, and where you are signed in.",
110
130
  icon: Lock,
111
131
  paths: [
112
132
  "/dashboard/settings/devices",
@@ -126,6 +146,7 @@ const SETTINGS_ROWS = [
126
146
  {
127
147
  id: "notifications",
128
148
  name: "Notifications",
149
+ subtitle: "What we tell you, and what your customers hear from you.",
129
150
  icon: Bell,
130
151
  paths: ["/dashboard/settings/notifications"],
131
152
  keywords: ["alerts", "low stock", "email sender", "digest", "reminders"]
@@ -133,6 +154,7 @@ const SETTINGS_ROWS = [
133
154
  {
134
155
  id: "preferences",
135
156
  name: "Preferences",
157
+ subtitle: "How the dashboard looks and behaves for you.",
136
158
  icon: SlidersHorizontal,
137
159
  paths: [
138
160
  "/dashboard/settings/language",
@@ -157,6 +179,7 @@ const SETTINGS_ROWS = [
157
179
  {
158
180
  id: "usage",
159
181
  name: "Usage",
182
+ subtitle: "What you have used this month.",
160
183
  icon: ChartColumn,
161
184
  paths: ["/dashboard/settings/usage"],
162
185
  keywords: ["credits", "delivery", "spend"],
@@ -166,6 +189,7 @@ const SETTINGS_ROWS = [
166
189
  {
167
190
  id: "legal",
168
191
  name: "Legal",
192
+ subtitle: "Your terms and privacy pages.",
169
193
  icon: FileText,
170
194
  paths: ["/dashboard/settings/legal"],
171
195
  keywords: ["terms", "privacy policy", "licences"]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-rows.ts"],"sourcesContent":["import {\n Bell,\n Building2,\n ChartColumn,\n CircleCheck,\n CircleHelp,\n CreditCard,\n FileText,\n Lock,\n Scale,\n Sparkles,\n SlidersHorizontal,\n UserRound,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/**\n * The rows of the settings window, in the order they are drawn.\n *\n * One list for the whole window: yours and the business's, mixed, because the\n * person looking for \"Notifications\" or \"Language\" should not have to know\n * whose setting it is. Rows that leave the window (`external`) carry an arrow.\n *\n * `paths` are the addresses a row owns. A link to any of them, or to anything\n * below them (a team member, a credits page), opens the window on that row.\n * `keywords` are what search also matches: the names of the sections inside a\n * row, so \"passkeys\" or \"time zone\" finds the row that holds them.\n */\nexport type SettingsRowId =\n | 'general'\n | 'extras'\n | 'profile'\n | 'security'\n | 'team'\n | 'notifications'\n | 'preferences'\n | 'payment-approvals'\n | 'billing'\n | 'usage'\n | 'policies'\n | 'legal';\n\nexport interface SettingsRow {\n id: SettingsRowId;\n name: string;\n icon: LucideIcon;\n paths: string[];\n keywords: string[];\n /** Hidden from a member who lacks every one of these. Empty: everyone. */\n permission?: PermissionKey[];\n /**\n * A page this row leaves the window for, where a host has its own screen for\n * the subject. The row then is a link, and the panel is never drawn.\n */\n href?: string;\n}\n\nconst BILLING_ROW: SettingsRow = {\n id: 'billing',\n name: 'Billing & plan',\n icon: CreditCard,\n paths: ['/dashboard/settings/billing'],\n keywords: ['plan', 'invoice', 'credits', 'top-up', 'cancel'],\n permission: ['canManageSettings'],\n};\n\n// A box whose card payments wait for a second person lists the decisions.\nconst PAYMENT_APPROVALS_ROW: SettingsRow = {\n id: 'payment-approvals',\n name: 'Payment approvals',\n icon: CircleCheck,\n paths: ['/dashboard/settings/payment-approvals'],\n keywords: ['approval', 'accepted', 'rejected', 'decisions', 'history'],\n};\n\nexport const SETTINGS_ROWS: SettingsRow[] = [\n {\n id: 'general',\n name: 'General',\n icon: Building2,\n paths: [\n '/dashboard/settings',\n '/dashboard/settings/branding',\n '/dashboard/settings/regional',\n ],\n keywords: ['business name', 'branding', 'logo', 'colours', 'numbering'],\n permission: ['canManageBusinessProfile', 'canManageSettings'],\n },\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true' ? [] : [BILLING_ROW]),\n {\n id: 'team',\n name: 'Team & permissions',\n icon: Users,\n paths: [\n '/dashboard/settings/team',\n '/dashboard/settings/roles',\n '/dashboard/settings/activity',\n ],\n keywords: ['people', 'invite', 'roles', 'activity', 'seats'],\n permission: ['canManageTeam'],\n // A box with its own team screen (its roles and invitations come from\n // its engine, which does not serve this panel's routes) names it here.\n href: process.env.NEXT_PUBLIC_BOX_TEAM_PAGE || undefined,\n },\n ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\n : []),\n {\n id: 'policies',\n name: 'Policies',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\n },\n {\n // The switches no business type implies (Blog today) and the sample\n // content to start with, clear, or add again. One row for both. No\n // permission gate on the row, like the Demo content entry in the sidebar:\n // everyone sees the state, the panel shows the switches only to those who\n // may change them, and the demo card shows its actions only to the owner\n // and admins.\n id: 'extras',\n name: 'Extras',\n icon: Sparkles,\n paths: [\n '/dashboard/settings/features',\n '/dashboard/settings/demo-content',\n ],\n keywords: [\n 'blog',\n 'posts',\n 'write',\n 'publish',\n 'switch',\n 'website',\n 'features',\n 'sample',\n 'products',\n 'starter',\n 'clear',\n 'example data',\n 'demo content',\n ],\n },\n {\n id: 'profile',\n name: 'Profile',\n icon: UserRound,\n paths: ['/dashboard/settings/profile'],\n keywords: ['name', 'email', 'photo', 'phone', 'close account'],\n },\n {\n id: 'security',\n name: 'Security',\n icon: Lock,\n paths: [\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'sign in',\n 'privacy',\n 'export',\n ],\n },\n {\n id: 'notifications',\n name: 'Notifications',\n icon: Bell,\n paths: ['/dashboard/settings/notifications'],\n keywords: ['alerts', 'low stock', 'email sender', 'digest', 'reminders'],\n },\n {\n id: 'preferences',\n name: 'Preferences',\n icon: SlidersHorizontal,\n paths: [\n '/dashboard/settings/language',\n '/dashboard/settings/accessibility',\n ],\n keywords: [\n 'appearance',\n 'theme',\n 'dark',\n 'light',\n 'language',\n 'time zone',\n 'date format',\n 'accessibility',\n 'high contrast',\n 'reduce motion',\n 'text size',\n ],\n },\n // A box that meters nothing in the console leaves Usage out.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_USAGE === 'true'\n ? []\n : [\n {\n id: 'usage' as const,\n name: 'Usage',\n icon: ChartColumn,\n paths: ['/dashboard/settings/usage'],\n keywords: ['credits', 'delivery', 'spend'],\n permission: ['canManageSettings' as const],\n },\n ]),\n {\n id: 'legal',\n name: 'Legal',\n icon: FileText,\n paths: ['/dashboard/settings/legal'],\n keywords: ['terms', 'privacy policy', 'licences'],\n },\n];\n\n/** Where \"Get help\" goes. It leaves the window, so it is not a row above. */\nexport const HELP_HREF = '/dashboard/help';\nexport const HELP_ICON = CircleHelp;\n\nconst pathOf = (full: string) => full.split(/[?#]/)[0];\n\n/**\n * The row that owns an address: the longest of its paths that equals the\n * address or is a parent of it. General's own address is a prefix of every\n * other settings address, so it only owns itself.\n */\nexport function rowForHref(href: string | null | undefined): SettingsRowId {\n const path = pathOf(href ?? '/dashboard/settings');\n let best: { id: SettingsRowId; len: number } | null = null;\n for (const row of SETTINGS_ROWS) {\n for (const p of row.paths) {\n const owns =\n path === p || (p !== '/dashboard/settings' && path.startsWith(`${p}/`));\n if (owns && (!best || p.length > best.len))\n best = { id: row.id, len: p.length };\n }\n }\n return best?.id ?? 'general';\n}\n\n/** Rows whose name or inner sections match what was typed. */\nexport function matchRows<T extends { name: string; keywords: string[] }>(\n rows: T[],\n query: string,\n): T[] {\n const q = query.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter(\n (r) =>\n r.name.toLowerCase().includes(q) || r.keywords.some((k) => k.includes(q)),\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-rows.ts"],"sourcesContent":["import {\n Bell,\n Building2,\n ChartColumn,\n CircleCheck,\n CircleHelp,\n CreditCard,\n FileText,\n Lock,\n ReceiptText,\n Scale,\n Sparkles,\n SlidersHorizontal,\n UserRound,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/**\n * The rows of the settings window, in the order they are drawn.\n *\n * One list for the whole window: yours and the business's, mixed, because the\n * person looking for \"Notifications\" or \"Language\" should not have to know\n * whose setting it is. Rows that leave the window (`external`) carry an arrow.\n *\n * `paths` are the addresses a row owns. A link to any of them, or to anything\n * below them (a team member, a credits page), opens the window on that row.\n * `keywords` are what search also matches: the names of the sections inside a\n * row, so \"passkeys\" or \"time zone\" finds the row that holds them.\n */\nexport type SettingsRowId =\n | 'general'\n | 'extras'\n | 'profile'\n | 'security'\n | 'team'\n | 'notifications'\n | 'preferences'\n | 'payment-approvals'\n | 'billing'\n | 'taxes'\n | 'usage'\n | 'policies'\n | 'legal';\n\nexport interface SettingsRow {\n id: SettingsRowId;\n name: string;\n /** The line under the title when the row is open. */\n subtitle?: string;\n icon: LucideIcon;\n paths: string[];\n keywords: string[];\n /** Hidden from a member who lacks every one of these. Empty: everyone. */\n permission?: PermissionKey[];\n /**\n * A page this row leaves the window for, where a host has its own screen for\n * the subject. The row then is a link, and the panel is never drawn.\n */\n href?: string;\n}\n\nconst BILLING_ROW: SettingsRow = {\n id: 'billing',\n name: 'Billing & plan',\n subtitle: 'Your plan, how you pay, and your payments.',\n icon: CreditCard,\n paths: ['/dashboard/settings/billing'],\n keywords: ['plan', 'invoice', 'credits', 'top-up', 'cancel'],\n permission: ['canManageSettings'],\n};\n\nconst TAXES_ROW: SettingsRow = {\n id: 'taxes',\n name: 'Taxes',\n subtitle: 'What you charge, and the number shown on receipts.',\n icon: ReceiptText,\n paths: ['/dashboard/settings/taxes'],\n keywords: ['vat', 'tax rate', 'registration number', 'receipts'],\n permission: ['canManageSettings'],\n};\n\n// A box whose card payments wait for a second person lists the decisions.\nconst PAYMENT_APPROVALS_ROW: SettingsRow = {\n id: 'payment-approvals',\n name: 'Payment approvals',\n subtitle: 'Who approves money going out.',\n icon: CircleCheck,\n paths: ['/dashboard/settings/payment-approvals'],\n keywords: ['approval', 'accepted', 'rejected', 'decisions', 'history'],\n};\n\nexport const SETTINGS_ROWS: SettingsRow[] = [\n {\n id: 'general',\n name: 'General',\n subtitle: 'Your business details and store address.',\n icon: Building2,\n paths: [\n '/dashboard/settings',\n '/dashboard/settings/branding',\n '/dashboard/settings/regional',\n ],\n keywords: ['business name', 'branding', 'logo', 'colours', 'numbering'],\n permission: ['canManageBusinessProfile', 'canManageSettings'],\n },\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true' ? [] : [BILLING_ROW]),\n // A box that keeps a tax (VAT) setting has it as its own row, after the plan.\n ...(process.env.NEXT_PUBLIC_BOX_TAXES_IN_POLICIES === 'true' ? [TAXES_ROW] : []),\n {\n id: 'team',\n name: 'Team & permissions',\n subtitle: 'Who can do what in this business.',\n icon: Users,\n paths: [\n '/dashboard/settings/team',\n '/dashboard/settings/roles',\n '/dashboard/settings/activity',\n ],\n keywords: ['people', 'invite', 'roles', 'activity', 'seats'],\n permission: ['canManageTeam'],\n // A box with its own team screen (its roles and invitations come from\n // its engine, which does not serve this panel's routes) names it here.\n href: process.env.NEXT_PUBLIC_BOX_TEAM_PAGE || undefined,\n },\n ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\n : []),\n {\n id: 'policies',\n name: 'Policies',\n subtitle: 'What your customers agree to.',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\n },\n {\n // The switches no business type implies (Blog today) and the sample\n // content to start with, clear, or add again. One row for both. No\n // permission gate on the row, like the Demo content entry in the sidebar:\n // everyone sees the state, the panel shows the switches only to those who\n // may change them, and the demo card shows its actions only to the owner\n // and admins.\n id: 'extras',\n name: 'Extras',\n subtitle: 'Switches and sample content for your store.',\n icon: Sparkles,\n paths: [\n '/dashboard/settings/features',\n '/dashboard/settings/demo-content',\n ],\n keywords: [\n 'blog',\n 'posts',\n 'write',\n 'publish',\n 'switch',\n 'website',\n 'features',\n 'sample',\n 'products',\n 'starter',\n 'clear',\n 'example data',\n 'demo content',\n ],\n },\n {\n id: 'profile',\n name: 'Profile',\n subtitle: 'Your name and how we reach you.',\n icon: UserRound,\n paths: ['/dashboard/settings/profile'],\n keywords: ['name', 'email', 'photo', 'phone', 'close account'],\n },\n {\n id: 'security',\n name: 'Security',\n subtitle: 'How you sign in, and where you are signed in.',\n icon: Lock,\n paths: [\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'sign in',\n 'privacy',\n 'export',\n ],\n },\n {\n id: 'notifications',\n name: 'Notifications',\n subtitle: 'What we tell you, and what your customers hear from you.',\n icon: Bell,\n paths: ['/dashboard/settings/notifications'],\n keywords: ['alerts', 'low stock', 'email sender', 'digest', 'reminders'],\n },\n {\n id: 'preferences',\n name: 'Preferences',\n subtitle: 'How the dashboard looks and behaves for you.',\n icon: SlidersHorizontal,\n paths: [\n '/dashboard/settings/language',\n '/dashboard/settings/accessibility',\n ],\n keywords: [\n 'appearance',\n 'theme',\n 'dark',\n 'light',\n 'language',\n 'time zone',\n 'date format',\n 'accessibility',\n 'high contrast',\n 'reduce motion',\n 'text size',\n ],\n },\n // A box that meters nothing in the console leaves Usage out.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_USAGE === 'true'\n ? []\n : [\n {\n id: 'usage' as const,\n name: 'Usage',\n subtitle: 'What you have used this month.',\n icon: ChartColumn,\n paths: ['/dashboard/settings/usage'],\n keywords: ['credits', 'delivery', 'spend'],\n permission: ['canManageSettings' as const],\n },\n ]),\n {\n id: 'legal',\n name: 'Legal',\n subtitle: 'Your terms and privacy pages.',\n icon: FileText,\n paths: ['/dashboard/settings/legal'],\n keywords: ['terms', 'privacy policy', 'licences'],\n },\n];\n\n/** Where \"Get help\" goes. It leaves the window, so it is not a row above. */\nexport const HELP_HREF = '/dashboard/help';\nexport const HELP_ICON = CircleHelp;\n\nconst pathOf = (full: string) => full.split(/[?#]/)[0];\n\n/**\n * The row that owns an address: the longest of its paths that equals the\n * address or is a parent of it. General's own address is a prefix of every\n * other settings address, so it only owns itself.\n */\nexport function rowForHref(href: string | null | undefined): SettingsRowId {\n const path = pathOf(href ?? '/dashboard/settings');\n let best: { id: SettingsRowId; len: number } | null = null;\n for (const row of SETTINGS_ROWS) {\n for (const p of row.paths) {\n const owns =\n path === p || (p !== '/dashboard/settings' && path.startsWith(`${p}/`));\n if (owns && (!best || p.length > best.len))\n best = { id: row.id, len: p.length };\n }\n }\n return best?.id ?? 'general';\n}\n\n/** Rows whose name or inner sections match what was typed. */\nexport function matchRows<T extends { name: string; keywords: string[] }>(\n rows: T[],\n query: string,\n): T[] {\n const q = query.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter(\n (r) =>\n r.name.toLowerCase().includes(q) || r.keywords.some((k) => k.includes(q)),\n );\n}\n"],"mappings":"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","names":[]}
@@ -2905,3 +2905,44 @@
2905
2905
  transition: none;
2906
2906
  }
2907
2907
  }
2908
+
2909
+ /* ===================== Settings window (the modal) =====================
2910
+ * One named variant: the window's own palette, so its rail, rows and panels draw the same values everywhere
2911
+ * (the prototype's warm frame). Light takes the prototype's values; dark falls back to the theme tokens so the window
2912
+ * still follows the theme. Panels inside the window read the same variables, with their own fallbacks outside it. */
2913
+ .st-modal {
2914
+ --st-rail: #f6f6f3;
2915
+ --st-rail-line: #ecece8;
2916
+ --st-rail-rule: #e6e6e2;
2917
+ --st-ink-2: #4a4a52;
2918
+ --st-ink-3: #6b6b72;
2919
+ --st-ink-4: #8b8b93;
2920
+ --st-row-hover: rgba(0, 0, 0, 0.045);
2921
+ --st-row-on-ring: 0 0 0 1px rgba(0, 0, 29, 0.06), 0 1px 3px rgba(0, 0, 0, 0.06);
2922
+ --st-search-ring: 0 0 0 1px rgba(0, 0, 29, 0.07);
2923
+ --st-card: #f6f6f3;
2924
+ --st-card-ring: 0 0 0 1px rgba(0, 0, 29, 0.05);
2925
+ --st-pick: #f3faf6;
2926
+ --st-pick-ring: 0 0 0 1px rgba(63, 156, 122, 0.45);
2927
+ --st-pick-dot: #3f9c7a;
2928
+ --st-chip: #f1f1ee;
2929
+ --st-rule: #eeeeea;
2930
+ }
2931
+ .dark .st-modal {
2932
+ --st-rail: var(--bg-muted);
2933
+ --st-rail-line: var(--border-subtle);
2934
+ --st-rail-rule: var(--border-subtle);
2935
+ --st-ink-2: var(--text-secondary);
2936
+ --st-ink-3: var(--text-tertiary);
2937
+ --st-ink-4: var(--text-tertiary);
2938
+ --st-row-hover: var(--interactive-hover);
2939
+ --st-row-on-ring: 0 0 0 1px var(--border-subtle);
2940
+ --st-search-ring: 0 0 0 1px var(--border-subtle);
2941
+ --st-card: var(--bg-muted);
2942
+ --st-card-ring: 0 0 0 1px var(--border-subtle);
2943
+ --st-pick: color-mix(in srgb, var(--pv-accent, #3f9c7a) 10%, var(--bg-base));
2944
+ --st-pick-ring: 0 0 0 1px color-mix(in srgb, #3f9c7a 55%, transparent);
2945
+ --st-pick-dot: #3f9c7a;
2946
+ --st-chip: var(--bg-muted);
2947
+ --st-rule: var(--border-subtle);
2948
+ }
@@ -2,10 +2,10 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Fragment, useEffect, useMemo, useRef, useState } from "react";
4
4
  import Link from "next/link";
5
- import { ArrowLeft, ArrowUpRight, Search } from "lucide-react";
5
+ import { ArrowUpRight, ChevronLeft, Search, X } from "lucide-react";
6
6
  import { useAnimate, useReducedMotion } from "motion/react";
7
7
  import { BusinessMark } from "@withpotter/ui-kit/business-mark";
8
- import { Dialog, DialogContent, DialogTitle } from "@withpotter/ui-kit/dialog";
8
+ import { Dialog, DialogClose, DialogContent, DialogTitle } from "@withpotter/ui-kit/dialog";
9
9
  import { Input } from "@withpotter/ui-kit/input";
10
10
  import { TextMorph } from "@withpotter/ui-kit/text-morph";
11
11
  import {
@@ -28,7 +28,9 @@ import {
28
28
  matchRows,
29
29
  rowForHref
30
30
  } from "./settings-rows";
31
- const ROW_CLASS = "flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm font-medium transition-colors";
31
+ const ROW_CLASS = "mb-px flex h-[34px] w-full items-center gap-[11px] rounded-[9px] px-2 text-left text-[13.5px] font-medium leading-5 transition-colors";
32
+ const ROW_IDLE = "text-[var(--st-ink-2)] hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)]";
33
+ const ROW_ON = "bg-[var(--bg-base)] text-[var(--text-primary)] shadow-[var(--st-row-on-ring)]";
32
34
  function UnifiedSettingsModal({
33
35
  open,
34
36
  href,
@@ -57,6 +59,8 @@ function UnifiedSettingsModal({
57
59
  }, own);
58
60
  }, [can, loading, host]);
59
61
  const shown = matchRows(rows, query);
62
+ const railRows = shown.filter((r) => r.id !== "legal");
63
+ const legalRow = shown.find((r) => r.id === "legal");
60
64
  const drill = useMemo(
61
65
  () => ({
62
66
  view,
@@ -90,7 +94,8 @@ function UnifiedSettingsModal({
90
94
  DialogContent,
91
95
  {
92
96
  "aria-label": "Settings",
93
- className: "flex h-[min(78vh,640px)] max-w-[860px] gap-0 overflow-hidden p-0",
97
+ showClose: false,
98
+ className: "st-modal flex h-[min(640px,calc(100dvh-2.5rem))] w-[calc(100%-2.5rem)] max-w-[920px] gap-0 overflow-hidden rounded-2xl border-0 p-0 text-[13.5px] shadow-[0_24px_60px_-20px_rgba(18,18,34,0.45),0_0_0_1px_rgba(0,0,0,0.04)]",
94
99
  children: [
95
100
  /* @__PURE__ */ jsxs(
96
101
  "nav",
@@ -107,116 +112,152 @@ function UnifiedSettingsModal({
107
112
  const next = e.key === "ArrowDown" ? (at + 1) % rowsEls.length : (at - 1 + rowsEls.length) % rowsEls.length;
108
113
  rowsEls[next].focus();
109
114
  },
110
- className: "flex w-[224px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--border-subtle)] bg-[var(--bg-muted)] p-3",
115
+ className: "flex w-[248px] shrink-0 flex-col overflow-hidden border-r border-[var(--st-rail-line)] bg-[var(--st-rail)] px-3 pb-2.5 pt-4",
111
116
  children: [
112
- name && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2.5 px-1 pt-0.5", children: [
117
+ name && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-1.5 pb-3.5 pt-0.5", children: [
113
118
  /* @__PURE__ */ jsx(BusinessMark, { name, src: tenant?.businessLogo, size: 34 }),
114
119
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
115
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-[var(--text-primary)]", children: name }),
116
- planLabel && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-[var(--text-tertiary)]", children: planLabel })
120
+ /* @__PURE__ */ jsx("p", { className: "max-w-[150px] truncate text-[14px] font-semibold leading-[18px] text-[var(--text-primary)]", children: name }),
121
+ planLabel && /* @__PURE__ */ jsx("p", { className: "truncate text-[12px] leading-4 text-[var(--st-ink-3)]", children: planLabel })
117
122
  ] })
118
123
  ] }),
119
- /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 text-[var(--text-tertiary)] focus-within:text-[var(--text-primary)]", children: [
120
- /* @__PURE__ */ jsx(Search, { className: "size-4 shrink-0" }),
124
+ /* @__PURE__ */ jsxs("div", { className: "mb-1.5 flex h-8 shrink-0 items-center gap-2 rounded-[10px] bg-[var(--bg-base)] px-2.5 text-[var(--st-ink-3)] shadow-[var(--st-search-ring)] focus-within:text-[var(--text-primary)]", children: [
125
+ /* @__PURE__ */ jsx(Search, { className: "size-[15px] shrink-0", strokeWidth: 1.8 }),
121
126
  /* @__PURE__ */ jsx(
122
127
  Input,
123
128
  {
124
129
  variant: "bare",
125
130
  value: query,
126
131
  onChange: (e) => setQuery(e.target.value),
127
- placeholder: "Search",
132
+ placeholder: "Search settings",
128
133
  "aria-label": "Search settings",
129
134
  className: "min-w-0 flex-1"
130
135
  }
131
136
  )
132
137
  ] }),
133
- shown.length === 0 && /* @__PURE__ */ jsxs("p", { className: "px-3 py-2 text-sm text-[var(--text-tertiary)]", children: [
134
- "Nothing matches \u201C",
135
- query,
136
- "\u201D."
137
- ] }),
138
- groupRows(shown).map((group) => /* @__PURE__ */ jsxs(Fragment, { children: [
139
- /* @__PURE__ */ jsx("p", { className: "px-3 pb-1 pt-3 text-xs font-medium text-[var(--text-tertiary)]", children: group.label }),
140
- group.rows.map((r) => {
141
- const selected = r.id === current?.id;
142
- const Icon = r.icon;
143
- if (r.href) {
138
+ /* @__PURE__ */ jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1", children: [
139
+ shown.length === 0 && /* @__PURE__ */ jsxs("p", { className: "px-2 py-2 text-[13px] text-[var(--st-ink-3)]", children: [
140
+ "Nothing matches \u201C",
141
+ query,
142
+ "\u201D."
143
+ ] }),
144
+ groupRows(railRows).map((group) => /* @__PURE__ */ jsxs(Fragment, { children: [
145
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1.5 pt-3.5 text-[12px] font-medium leading-5 text-[var(--st-ink-4)]", children: group.label }),
146
+ group.rows.map((r) => {
147
+ const selected = r.id === current?.id;
148
+ const Icon = r.icon;
149
+ if (r.href) {
150
+ return /* @__PURE__ */ jsxs(
151
+ Link,
152
+ {
153
+ "data-row": true,
154
+ href: r.href,
155
+ onClick: onClose,
156
+ className: `${ROW_CLASS} ${ROW_IDLE}`,
157
+ children: [
158
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
159
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: r.name }),
160
+ /* @__PURE__ */ jsx(ArrowUpRight, { className: "ml-auto size-[13px] shrink-0 opacity-[.55]" })
161
+ ]
162
+ },
163
+ r.id
164
+ );
165
+ }
144
166
  return /* @__PURE__ */ jsxs(
145
- Link,
167
+ "button",
146
168
  {
147
169
  "data-row": true,
148
- href: r.href,
149
- onClick: onClose,
150
- className: `${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`,
170
+ type: "button",
171
+ "aria-current": selected ? "page" : void 0,
172
+ onClick: () => {
173
+ setPicked(r.id);
174
+ setView(null);
175
+ },
176
+ className: `${ROW_CLASS} ${selected ? ROW_ON : ROW_IDLE}`,
151
177
  children: [
152
- /* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
178
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
153
179
  /* @__PURE__ */ jsx("span", { className: "truncate", children: r.name })
154
180
  ]
155
181
  },
156
182
  r.id
157
183
  );
158
- }
159
- return /* @__PURE__ */ jsxs(
160
- "button",
161
- {
162
- "data-row": true,
163
- type: "button",
164
- "aria-current": selected ? "page" : void 0,
165
- onClick: () => {
166
- setPicked(r.id);
167
- setView(null);
168
- },
169
- className: `${ROW_CLASS} ${selected ? "bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm" : "text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]"}`,
170
- children: [
171
- /* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
172
- /* @__PURE__ */ jsx("span", { className: "truncate", children: r.name })
173
- ]
184
+ })
185
+ ] }, group.key))
186
+ ] }),
187
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 shrink-0 border-t border-[var(--st-rail-rule)] pt-2", children: [
188
+ legalRow && /* @__PURE__ */ jsxs(
189
+ "button",
190
+ {
191
+ type: "button",
192
+ "data-row": true,
193
+ "aria-current": legalRow.id === current?.id ? "page" : void 0,
194
+ onClick: () => {
195
+ setPicked(legalRow.id);
196
+ setView(null);
174
197
  },
175
- r.id
176
- );
177
- })
178
- ] }, group.key)),
179
- /* @__PURE__ */ jsx("div", { className: "mt-auto border-t border-[var(--border-subtle)] pt-2", children: /* @__PURE__ */ jsxs(
180
- Link,
181
- {
182
- href: HELP_HREF,
183
- onClick: onClose,
184
- className: `${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`,
185
- children: [
186
- /* @__PURE__ */ jsx(HelpIcon, { className: "size-[18px] shrink-0" }),
187
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: "Get help" }),
188
- /* @__PURE__ */ jsx(ArrowUpRight, { className: "size-4 shrink-0 text-[var(--text-tertiary)]" })
189
- ]
190
- }
191
- ) })
198
+ className: `${ROW_CLASS} ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`,
199
+ children: [
200
+ /* @__PURE__ */ jsx(legalRow.icon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
201
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: legalRow.name })
202
+ ]
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsxs(
206
+ Link,
207
+ {
208
+ href: HELP_HREF,
209
+ onClick: onClose,
210
+ className: `${ROW_CLASS} ${ROW_IDLE}`,
211
+ children: [
212
+ /* @__PURE__ */ jsx(HelpIcon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
213
+ /* @__PURE__ */ jsx("span", { children: "Get help" }),
214
+ /* @__PURE__ */ jsx(ArrowUpRight, { className: "ml-auto size-[13px] shrink-0" })
215
+ ]
216
+ }
217
+ )
218
+ ] })
192
219
  ]
193
220
  }
194
221
  ),
195
222
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
196
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-4 py-3", children: [
197
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
198
- view && /* @__PURE__ */ jsx(
223
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-none items-start justify-between pb-4 pl-10 pr-6 pt-5", children: [
224
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
225
+ view && /* @__PURE__ */ jsxs(
199
226
  "button",
200
227
  {
201
228
  type: "button",
202
229
  onClick: drill.back,
203
230
  "aria-label": `Back to ${current?.name ?? "Settings"}`,
204
- className: "grid size-7 shrink-0 place-items-center rounded-lg text-[var(--text-secondary)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]",
205
- children: /* @__PURE__ */ jsx(ArrowLeft, { className: "size-4" })
231
+ className: "-ml-0.5 mb-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-[var(--st-ink-2)] transition-colors hover:text-[var(--text-primary)]",
232
+ children: [
233
+ /* @__PURE__ */ jsx(ChevronLeft, { className: "size-[15px]", strokeWidth: 2 }),
234
+ current?.name ?? "Settings"
235
+ ]
206
236
  }
207
237
  ),
208
238
  /* @__PURE__ */ jsx(
209
239
  DialogTitle,
210
240
  {
211
241
  "aria-label": view?.title ?? current?.name ?? "Settings",
212
- className: "truncate text-md font-semibold text-[var(--text-primary)]",
242
+ className: "truncate text-[18px] font-semibold leading-6 tracking-[-0.01em] text-[var(--text-primary)]",
213
243
  children: /* @__PURE__ */ jsx(TextMorph, { as: "span", children: view?.title ?? current?.name ?? "Settings" })
214
244
  }
215
- )
245
+ ),
246
+ !view && current?.subtitle && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[13px] leading-[18px] text-[var(--st-ink-3)]", children: current.subtitle })
216
247
  ] }),
217
- /* @__PURE__ */ jsx("div", { className: "mr-7 flex items-center gap-2", children: /* @__PURE__ */ jsx("div", { ref: setStatusSlot, className: "flex items-center" }) })
248
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
249
+ /* @__PURE__ */ jsx("div", { ref: setStatusSlot, className: "flex items-center" }),
250
+ /* @__PURE__ */ jsx(
251
+ DialogClose,
252
+ {
253
+ "aria-label": "Close",
254
+ className: "grid size-8 place-items-center rounded-[9px] text-[var(--st-ink-2)] transition-colors hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]",
255
+ children: /* @__PURE__ */ jsx(X, { className: "size-[18px]", strokeWidth: 2 })
256
+ }
257
+ )
258
+ ] })
218
259
  ] }),
219
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none", children: /* @__PURE__ */ jsx("div", { ref: scope, children: current && /* @__PURE__ */ jsx(SettingsDrillProvider, { value: drill, children: /* @__PURE__ */ jsx(SettingsSurface, { variant: "plain", children: /* @__PURE__ */ jsx(SettingsStatusSlot, { element: statusSlot, children: current.render ? current.render() : /* @__PURE__ */ jsx(
260
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-6 pb-7 outline-none", children: /* @__PURE__ */ jsx("div", { ref: scope, children: current && /* @__PURE__ */ jsx(SettingsDrillProvider, { value: drill, children: /* @__PURE__ */ jsx(SettingsSurface, { variant: "plain", children: /* @__PURE__ */ jsx(SettingsStatusSlot, { element: statusSlot, children: current.render ? current.render() : /* @__PURE__ */ jsx(
220
261
  SettingsPanel,
221
262
  {
222
263
  id: current.id
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowLeft, ArrowUpRight, Search } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\nimport { Dialog, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\nconst ROW_CLASS =\n 'flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm font-medium transition-colors';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right):\n * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n className=\"flex h-[min(78vh,640px)] max-w-[860px] gap-0 overflow-hidden p-0\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[224px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--border-subtle)] bg-[var(--bg-muted)] p-3\"\n >\n {name && (\n <div className=\"mb-3 flex items-center gap-2.5 px-1 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-xs text-[var(--text-tertiary)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\n )}\n <div className=\"mb-2 flex items-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 text-[var(--text-tertiary)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-4 shrink-0\" />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n {shown.length === 0 && (\n <p className=\"px-3 py-2 text-sm text-[var(--text-tertiary)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(shown).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-3 pb-1 pt-3 text-xs font-medium text-[var(--text-tertiary)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${\n selected\n ? 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm'\n : 'text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n </Fragment>\n ))}\n <div className=\"mt-auto border-t border-[var(--border-subtle)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <HelpIcon className=\"size-[18px] shrink-0\" />\n <span className=\"flex-1\">Get help</span>\n <ArrowUpRight className=\"size-4 shrink-0 text-[var(--text-tertiary)]\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"grid size-7 shrink-0 place-items-center rounded-lg text-[var(--text-secondary)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]\"\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-md font-semibold text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n </div>\n <div className=\"mr-7 flex items-center gap-2\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none\">\n <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":";AAuKc,cACA,YADA;AArKd,SAAS,UAAU,WAAW,SAAS,QAAQ,gBAAgC;AAC/E,OAAO,UAAU;AACjB,SAAS,WAAW,cAAc,cAAc;AAChD,SAAS,YAAY,wBAAwB;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,eAAe,mBAAmB;AACnD,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB,2BAA2B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAQP,MAAM,YACJ;AAeK,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,KAAK,QAAQ,IAAI,eAAe;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AAGrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAgC,IAAI;AAG5D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AACxD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAoC,MAAS;AACrE,MAAI,QAAQ,SAAS,MAAM;AACzB,YAAQ,IAAI;AACZ,cAAU,IAAI;AACd,aAAS,EAAE;AACX,YAAQ,IAAI;AAAA,EACd;AAEA,QAAM,OAAO,oBAAoB;AACjC,QAAM,OAAO,QAAQ,MAAM;AACzB,UAAM,MAAkB,cAAc;AAAA,MACpC,CAAC,MAAM,CAAC,EAAE,cAAc,WAAW,EAAE,WAAW,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;AAAA,IACpE;AAGA,WAAO,KAAK,OAAmB,CAAC,MAAM,UAAU;AAC9C,YAAM,KAAK,KAAK,UAAU,CAAC,MAAM,EAAE,QAAQ,MAAM,SAAS,UAAU;AAEpE,YAAM,OAAO,OAAO,KAAK,KAAK,SAAS,KAAK;AAE5C,aAAO,CAAC,GAAG,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK,MAAM,IAAI,CAAC;AAAA,IAC5D,GAAG,GAAG;AAAA,EACR,GAAG,CAAC,KAAK,SAAS,IAAI,CAAC;AACvB,QAAM,QAAQ,UAAU,MAAM,KAAK;AAEnC,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA,MAAM,CAAC,KAAK,UAAU,QAAQ,EAAE,KAAK,MAAM,CAAC;AAAA,MAC5C,SAAS,CAAC,KAAK,KAAK,UAAU;AAC5B,kBAAU,GAAoB;AAC9B,gBAAQ,EAAE,KAAK,MAAM,CAAC;AAAA,MACxB;AAAA,MACA,MAAM,MAAM,QAAQ,IAAI;AAAA,IAC1B;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AAEA,QAAM,SAAS,UAAU,eAAe,MAAM,IAAI,GAAG,MAAM,WAAW,IAAI;AAE1E,QAAM,UAAU,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,KAAK,KAAK,CAAC;AAC3D,QAAM,WAAW;AAEjB,QAAM,CAAC,YAAY,aAAa,IAAI,SAA6B,IAAI;AAGrE,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,YAAY,gBAAgB,GAAG;AACrC,QAAM,OAAO,QAAQ,cAAc,KAAK,KAAK;AAI7C,QAAM,CAAC,OAAO,OAAO,IAAI,WAA2B;AACpD,QAAM,UAAU,iBAAiB;AACjC,QAAM,WAAW,GAAG,SAAS,MAAM,EAAE,IAAI,MAAM,OAAO,EAAE;AACxD,QAAM,UAAU,OAAO,QAAQ;AAE/B,YAAU,MAAM;AACd,QAAI,QAAQ,YAAY,SAAU;AAElC,YAAQ,UAAU;AAElB,QAAI,WAAW,CAAC,MAAM,QAAS;AAE/B,SAAK,QAAQ,MAAM,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,UAAU,MAAM,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;AAAA,EAC1G,GAAG,CAAC,UAAU,SAAS,SAAS,KAAK,CAAC;AAEtC,SACE,oBAAC,UAAO,MAAY,cAAc,CAAC,SAAS,CAAC,QAAQ,QAAQ,GAC3D;AAAA,IAAC;AAAA;AAAA,MACC,cAAW;AAAA,MACX,WAAU;AAAA,MAEV;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,cAAW;AAAA,YACX,WAAW,CAAC,MAAM;AAEhB,kBAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,UAAW;AAClD,oBAAM,UAAU,MAAM;AAAA,gBACpB,EAAE,cAAc,iBAA8B,kBAAkB;AAAA,cAClE;AACA,oBAAM,KAAK,QAAQ,QAAQ,SAAS,aAA4B;AAChE,kBAAI,OAAO,GAAI;AACf,gBAAE,eAAe;AACjB,oBAAM,OACJ,EAAE,QAAQ,eACL,KAAK,KAAK,QAAQ,UAClB,KAAK,IAAI,QAAQ,UAAU,QAAQ;AAC1C,sBAAQ,IAAI,EAAE,MAAM;AAAA,YACtB;AAAA,YACA,WAAU;AAAA,YAET;AAAA,sBACC,qBAAC,SAAI,WAAU,8CACb;AAAA,oCAAC,gBAAa,MAAY,KAAK,QAAQ,cAAc,MAAM,IAAI;AAAA,gBAC/D,qBAAC,SAAI,WAAU,WACb;AAAA,sCAAC,OAAE,WAAU,6DACV,gBACH;AAAA,kBACC,aACC,oBAAC,OAAE,WAAU,gDACV,qBACH;AAAA,mBAEJ;AAAA,iBACF;AAAA,cAEF,qBAAC,SAAI,WAAU,6KACb;AAAA,oCAAC,UAAO,WAAU,mBAAkB;AAAA,gBACpC;AAAA,kBAAC;AAAA;AAAA,oBACC,SAAQ;AAAA,oBACR,OAAO;AAAA,oBACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,oBACxC,aAAY;AAAA,oBACZ,cAAW;AAAA,oBACX,WAAU;AAAA;AAAA,gBACZ;AAAA,iBACF;AAAA,cACC,MAAM,WAAW,KAChB,qBAAC,OAAE,WAAU,iDAAgD;AAAA;AAAA,gBACzC;AAAA,gBAAM;AAAA,iBAC1B;AAAA,cAED,UAAU,KAAK,EAAE,IAAI,CAAC,UACrB,qBAAC,YACC;AAAA,oCAAC,OAAE,WAAU,kEACV,gBAAM,OACT;AAAA,gBACC,MAAM,KAAK,IAAI,CAAC,MAAM;AACrB,wBAAM,WAAW,EAAE,OAAO,SAAS;AACnC,wBAAM,OAAO,EAAE;AACf,sBAAI,EAAE,MAAM;AACV,2BACE;AAAA,sBAAC;AAAA;AAAA,wBAEC,YAAQ;AAAA,wBACR,MAAM,EAAE;AAAA,wBACR,SAAS;AAAA,wBACT,WAAW,GAAG,SAAS;AAAA,wBAEvB;AAAA,8CAAC,QAAK,WAAU,wBAAuB;AAAA,0BACvC,oBAAC,UAAK,WAAU,YAAY,YAAE,MAAK;AAAA;AAAA;AAAA,sBAP9B,EAAE;AAAA,oBAQT;AAAA,kBAEJ;AACA,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,YAAQ;AAAA,sBACR,MAAK;AAAA,sBACL,gBAAc,WAAW,SAAS;AAAA,sBAClC,SAAS,MAAM;AACb,kCAAU,EAAE,EAAE;AACd,gCAAQ,IAAI;AAAA,sBACd;AAAA,sBACA,WAAW,GAAG,SAAS,IACrB,WACI,6DACA,mGACN;AAAA,sBAEA;AAAA,4CAAC,QAAK,WAAU,wBAAuB;AAAA,wBACvC,oBAAC,UAAK,WAAU,YAAY,YAAE,MAAK;AAAA;AAAA;AAAA,oBAf9B,EAAE;AAAA,kBAgBT;AAAA,gBAEJ,CAAC;AAAA,mBAzCY,MAAM,GA0CrB,CACD;AAAA,cACD,oBAAC,SAAI,WAAU,uDACb;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM;AAAA,kBACN,SAAS;AAAA,kBACT,WAAW,GAAG,SAAS;AAAA,kBAEvB;AAAA,wCAAC,YAAS,WAAU,wBAAuB;AAAA,oBAC3C,oBAAC,UAAK,WAAU,UAAS,sBAAQ;AAAA,oBACjC,oBAAC,gBAAa,WAAU,+CAA8C;AAAA;AAAA;AAAA,cACxE,GACF;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,gCACb;AAAA,+BAAC,SAAI,WAAU,4FACb;AAAA,iCAAC,SAAI,WAAU,mCACZ;AAAA,sBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM;AAAA,kBACf,cAAY,WAAW,SAAS,QAAQ,UAAU;AAAA,kBAClD,WAAU;AAAA,kBAEV,8BAAC,aAAU,WAAU,UAAS;AAAA;AAAA,cAChC;AAAA,cAEF;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY,MAAM,SAAS,SAAS,QAAQ;AAAA,kBAC5C,WAAU;AAAA,kBAEV,8BAAC,aAAU,IAAG,QACX,gBAAM,SAAS,SAAS,QAAQ,YACnC;AAAA;AAAA,cACF;AAAA,eACF;AAAA,YACA,oBAAC,SAAI,WAAU,gCACb,8BAAC,SAAI,KAAK,eAAe,WAAU,qBAAoB,GACzD;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,yDACb,8BAAC,SAAI,KAAK,OACT,qBACC,oBAAC,yBAAsB,OAAO,OAC5B,8BAAC,mBAAgB,SAAQ,SACvB,8BAAC,sBAAmB,SAAS,YAC1B,kBAAQ,SACP,QAAQ,OAAO,IAEf;AAAA,YAAC;AAAA;AAAA,cAEC,IAAI,QAAQ;AAAA;AAAA,YADP,QAAQ;AAAA,UAEf,GAEJ,GACF,GACF,GAEF,GACF;AAAA,WACF;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowUpRight, ChevronLeft, Search, X } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\nimport { Dialog, DialogClose, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\n/**\n * A row of the rail: 34px tall, 13.5px, an icon 16px wide at 1.75 stroke. The values are the settings prototype's; the\n * colours come from the window's own palette (`.st-modal` in shell.css), which falls back to the theme in dark.\n */\nconst ROW_CLASS =\n 'mb-px flex h-[34px] w-full items-center gap-[11px] rounded-[9px] px-2 text-left text-[13.5px] font-medium leading-5 transition-colors';\nconst ROW_IDLE =\n 'text-[var(--st-ink-2)] hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)]';\nconst ROW_ON =\n 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-[var(--st-row-on-ring)]';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right):\n * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n // Legal is not on the rail: it sits at the foot, beside Get help.\n const railRows = shown.filter((r) => r.id !== 'legal');\n const legalRow = shown.find((r) => r.id === 'legal');\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n showClose={false}\n className=\"st-modal flex h-[min(640px,calc(100dvh-2.5rem))] w-[calc(100%-2.5rem)] max-w-[920px] gap-0 overflow-hidden rounded-2xl border-0 p-0 text-[13.5px] shadow-[0_24px_60px_-20px_rgba(18,18,34,0.45),0_0_0_1px_rgba(0,0,0,0.04)]\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[248px] shrink-0 flex-col overflow-hidden border-r border-[var(--st-rail-line)] bg-[var(--st-rail)] px-3 pb-2.5 pt-4\"\n >\n {name && (\n <div className=\"flex items-center gap-2.5 px-1.5 pb-3.5 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"max-w-[150px] truncate text-[14px] font-semibold leading-[18px] text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-[12px] leading-4 text-[var(--st-ink-3)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\n )}\n <div className=\"mb-1.5 flex h-8 shrink-0 items-center gap-2 rounded-[10px] bg-[var(--bg-base)] px-2.5 text-[var(--st-ink-3)] shadow-[var(--st-search-ring)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-[15px] shrink-0\" strokeWidth={1.8} />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <div className=\"-mx-1 min-h-0 flex-1 overflow-y-auto px-1\">\n {shown.length === 0 && (\n <p className=\"px-2 py-2 text-[13px] text-[var(--st-ink-3)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(railRows).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-2 pb-1.5 pt-3.5 text-[12px] font-medium leading-5 text-[var(--st-ink-4)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0 opacity-[.55]\" />\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${selected ? ROW_ON : ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n </Fragment>\n ))}\n </div>\n <div className=\"mt-1.5 shrink-0 border-t border-[var(--st-rail-rule)] pt-2\">\n {legalRow && (\n <button\n type=\"button\"\n data-row\n aria-current={legalRow.id === current?.id ? 'page' : undefined}\n onClick={() => {\n setPicked(legalRow.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`}\n >\n <legalRow.icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{legalRow.name}</span>\n </button>\n )}\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <HelpIcon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span>Get help</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex flex-none items-start justify-between pb-4 pl-10 pr-6 pt-5\">\n <div className=\"min-w-0\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"-ml-0.5 mb-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-[var(--st-ink-2)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <ChevronLeft className=\"size-[15px]\" strokeWidth={2} />\n {current?.name ?? 'Settings'}\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-[18px] font-semibold leading-6 tracking-[-0.01em] text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n {!view && current?.subtitle && (\n <p className=\"mt-0.5 text-[13px] leading-[18px] text-[var(--st-ink-3)]\">\n {current.subtitle}\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-2.5\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n <DialogClose\n aria-label=\"Close\"\n className=\"grid size-8 place-items-center rounded-[9px] text-[var(--st-ink-2)] transition-colors hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]\"\n >\n <X className=\"size-[18px]\" strokeWidth={2} />\n </DialogClose>\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-6 pb-7 outline-none\">\n <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.108",
3
+ "version": "0.13.0-next.110",
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.108",
54
- "@withpotter/lib": "^0.13.0-next.108",
55
- "@withpotter/ui-kit": "^0.13.0-next.108",
53
+ "@withpotter/hooks": "^0.13.0-next.110",
54
+ "@withpotter/lib": "^0.13.0-next.110",
55
+ "@withpotter/ui-kit": "^0.13.0-next.110",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",