@withpotter/components 0.13.0-next.65 → 0.13.0-next.67

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.
@@ -19,6 +19,7 @@ import { BillingContent } from "@withpotter/pages-core/dashboard/settings/billin
19
19
  import { LegalContent } from "@withpotter/pages-core/dashboard/settings/legal/_components/legal-content";
20
20
  import { PaymentApprovalsContent } from "@withpotter/pages-core/dashboard/settings/payment-approvals/_components/payment-approvals-content";
21
21
  import { DemoContentCard } from "@withpotter/pages-core/dashboard/settings/demo-content/_components/demo-content-card";
22
+ import { FeaturesContent } from "@withpotter/pages-core/dashboard/settings/features/_components/features-content";
22
23
  const loading = () => /* @__PURE__ */ jsx(StateView, { kind: "loading", title: "Loading" });
23
24
  function Section({
24
25
  title,
@@ -64,6 +65,7 @@ function PreferencesPanel() {
64
65
  }
65
66
  const PANELS = {
66
67
  general: () => /* @__PURE__ */ jsx(GeneralContent, {}),
68
+ features: () => /* @__PURE__ */ jsx(FeaturesContent, {}),
67
69
  account: () => /* @__PURE__ */ jsx(AccountPanel, {}),
68
70
  team: () => /* @__PURE__ */ jsx(TeamContent, {}),
69
71
  notifications: () => /* @__PURE__ */ jsx(NotificationsContent, {}),
@@ -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 { 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 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 * Everything about the person: profile, how they sign in, their data, and the\n * way out. Closing the account is last, as it is the one thing here that\n * cannot be undone.\n */\nfunction AccountPanel() {\n return (\n <div className=\"space-y-8\">\n <Section title=\"Profile\">\n <ProfileContent />\n </Section>\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 <SecurityContent />\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 <div className=\"space-y-8\">\n <Section title=\"Appearance, language and region\">\n <PreferencesContent />\n </Section>\n <Section title=\"Accessibility\" action={<ResetAccessibility />}>\n <div className=\"px-4\">\n <AccessibilityCard />\n </div>\n </Section>\n </div>\n );\n}\n\nconst PANELS: Record<SettingsRowId, () => ReactNode> = {\n general: () => <GeneralContent />,\n account: () => <AccountPanel />,\n team: () => <TeamContent />,\n notifications: () => <NotificationsContent />,\n 'demo-content': () => <DemoContentCard />,\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":";AAgCsB,cAchB,YAdgB;AA9BtB,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,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;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;AAOA,SAAS,eAAe;AACtB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,WACb,8BAAC,kBAAe,GAClB;AAAA,IACA,oBAAC,WAAQ,OAAM,wBACb,8BAAC,SAAI,WAAU,QACb,8BAAC,iBAAc,GACjB,GACF;AAAA,IACA,oBAAC,WAAQ,OAAM,kBACb,8BAAC,sBAAmB,GACtB;AAAA,IACA,oBAAC,mBAAgB;AAAA,KACnB;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,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,mCACb,8BAAC,sBAAmB,GACtB;AAAA,IACA,oBAAC,WAAQ,OAAM,iBAAgB,QAAQ,oBAAC,sBAAmB,GACzD,8BAAC,SAAI,WAAU,QACb,8BAAC,qBAAkB,GACrB,GACF;AAAA,KACF;AAEJ;AAEA,MAAM,SAAiD;AAAA,EACrD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,SAAS,MAAM,oBAAC,gBAAa;AAAA,EAC7B,MAAM,MAAM,oBAAC,eAAY;AAAA,EACzB,eAAe,MAAM,oBAAC,wBAAqB;AAAA,EAC3C,gBAAgB,MAAM,oBAAC,mBAAgB;AAAA,EACvC,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 { 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 * Everything about the person: profile, how they sign in, their data, and the\n * way out. Closing the account is last, as it is the one thing here that\n * cannot be undone.\n */\nfunction AccountPanel() {\n return (\n <div className=\"space-y-8\">\n <Section title=\"Profile\">\n <ProfileContent />\n </Section>\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 <SecurityContent />\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 <div className=\"space-y-8\">\n <Section title=\"Appearance, language and region\">\n <PreferencesContent />\n </Section>\n <Section title=\"Accessibility\" action={<ResetAccessibility />}>\n <div className=\"px-4\">\n <AccessibilityCard />\n </div>\n </Section>\n </div>\n );\n}\n\nconst PANELS: Record<SettingsRowId, () => ReactNode> = {\n general: () => <GeneralContent />,\n features: () => <FeaturesContent />,\n account: () => <AccountPanel />,\n team: () => <TeamContent />,\n notifications: () => <NotificationsContent />,\n 'demo-content': () => <DemoContentCard />,\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":";AAiCsB,cAchB,YAdgB;AA/BtB,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,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;AAOA,SAAS,eAAe;AACtB,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,WACb,8BAAC,kBAAe,GAClB;AAAA,IACA,oBAAC,WAAQ,OAAM,wBACb,8BAAC,SAAI,WAAU,QACb,8BAAC,iBAAc,GACjB,GACF;AAAA,IACA,oBAAC,WAAQ,OAAM,kBACb,8BAAC,sBAAmB,GACtB;AAAA,IACA,oBAAC,mBAAgB;AAAA,KACnB;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,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,WAAQ,OAAM,mCACb,8BAAC,sBAAmB,GACtB;AAAA,IACA,oBAAC,WAAQ,OAAM,iBAAgB,QAAQ,oBAAC,sBAAmB,GACzD,8BAAC,SAAI,WAAU,QACb,8BAAC,qBAAkB,GACrB,GACF;AAAA,KACF;AAEJ;AAEA,MAAM,SAAiD;AAAA,EACrD,SAAS,MAAM,oBAAC,kBAAe;AAAA,EAC/B,UAAU,MAAM,oBAAC,mBAAgB;AAAA,EACjC,SAAS,MAAM,oBAAC,gBAAa;AAAA,EAC7B,MAAM,MAAM,oBAAC,eAAY;AAAA,EACzB,eAAe,MAAM,oBAAC,wBAAqB;AAAA,EAC3C,gBAAgB,MAAM,oBAAC,mBAAgB;AAAA,EACvC,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":[]}
@@ -12,7 +12,7 @@ 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' | 'account' | 'team' | 'notifications' | 'demo-content' | 'preferences' | 'payment-approvals' | 'billing' | 'usage' | 'policies' | 'legal';
15
+ export type SettingsRowId = 'general' | 'features' | 'account' | 'team' | 'notifications' | 'demo-content' | 'preferences' | 'payment-approvals' | 'billing' | 'usage' | 'policies' | 'legal';
16
16
  export interface SettingsRow {
17
17
  id: SettingsRowId;
18
18
  name: string;
@@ -9,6 +9,7 @@ import {
9
9
  Scale,
10
10
  Sparkles,
11
11
  SlidersHorizontal,
12
+ ToggleLeft,
12
13
  UserRound,
13
14
  Users
14
15
  } from "lucide-react";
@@ -40,6 +41,17 @@ const SETTINGS_ROWS = [
40
41
  keywords: ["business name", "branding", "logo", "colours", "numbering"],
41
42
  permission: ["canManageBusinessProfile", "canManageSettings"]
42
43
  },
44
+ {
45
+ // The switches no business type implies: Blog today. Next to General
46
+ // because it is the business's own setting, before the rows that are
47
+ // about the person (Account, Preferences).
48
+ id: "features",
49
+ name: "Features",
50
+ icon: ToggleLeft,
51
+ paths: ["/dashboard/settings/features"],
52
+ keywords: ["blog", "posts", "write", "publish", "switch", "website", "features"],
53
+ permission: ["canManageSettings"]
54
+ },
43
55
  {
44
56
  id: "account",
45
57
  name: "Account",
@@ -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 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 | 'account'\n | 'team'\n | 'notifications'\n | 'demo-content'\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 {\n id: 'account',\n name: 'Account',\n icon: UserRound,\n paths: [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'profile',\n 'name',\n 'email',\n 'photo',\n 'security',\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'privacy',\n 'export',\n 'close account',\n ],\n },\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 {\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 // Sample products, pages and pictures to start with, clear, or add again.\n // No permission gate, like the sidebar entry: everyone sees the state and\n // the card itself shows the actions only to the owner and admins.\n id: 'demo-content',\n name: 'Demo content',\n icon: Sparkles,\n paths: ['/dashboard/settings/demo-content'],\n keywords: ['sample', 'products', 'starter', 'clear', 'example data'],\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 ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\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 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: 'policies',\n name: 'Policies',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\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(rows: SettingsRow[], query: string): SettingsRow[] {\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 Scale,\n Sparkles,\n SlidersHorizontal,\n ToggleLeft,\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 | 'features'\n | 'account'\n | 'team'\n | 'notifications'\n | 'demo-content'\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 {\n // The switches no business type implies: Blog today. Next to General\n // because it is the business's own setting, before the rows that are\n // about the person (Account, Preferences).\n id: 'features',\n name: 'Features',\n icon: ToggleLeft,\n paths: ['/dashboard/settings/features'],\n keywords: ['blog', 'posts', 'write', 'publish', 'switch', 'website', 'features'],\n permission: ['canManageSettings'],\n },\n {\n id: 'account',\n name: 'Account',\n icon: UserRound,\n paths: [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'profile',\n 'name',\n 'email',\n 'photo',\n 'security',\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'privacy',\n 'export',\n 'close account',\n ],\n },\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 {\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 // Sample products, pages and pictures to start with, clear, or add again.\n // No permission gate, like the sidebar entry: everyone sees the state and\n // the card itself shows the actions only to the owner and admins.\n id: 'demo-content',\n name: 'Demo content',\n icon: Sparkles,\n paths: ['/dashboard/settings/demo-content'],\n keywords: ['sample', 'products', 'starter', 'clear', 'example data'],\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 ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\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 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: 'policies',\n name: 'Policies',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\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(rows: SettingsRow[], query: string): SettingsRow[] {\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":[]}
@@ -6,6 +6,7 @@ import {
6
6
  UserRound,
7
7
  LayoutGrid,
8
8
  SlidersHorizontal,
9
+ ToggleLeft,
9
10
  Database,
10
11
  ShieldCheck,
11
12
  Accessibility,
@@ -72,6 +73,13 @@ const BUSINESS_GROUPS = [
72
73
  match: ["/dashboard/settings/branding", "/dashboard/settings/regional"],
73
74
  icon: SlidersHorizontal
74
75
  },
76
+ // FEATURES — the switches no business type implies (Blog today).
77
+ // Business types themselves are Explore's, see below.
78
+ {
79
+ name: "Features",
80
+ href: "/dashboard/settings/features",
81
+ icon: ToggleLeft
82
+ },
75
83
  // NO BUSINESS TYPE ROW. It moved out of Settings entirely on
76
84
  // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.
77
85
  //
@@ -83,8 +91,8 @@ const BUSINESS_GROUPS = [
83
91
  // something you already have, and this is where you go to get something
84
92
  // you do not.
85
93
  //
86
- // `/dashboard/settings/features` redirects, so nothing that linked to it
87
- // breaks.
94
+ // `/dashboard/settings/features` is Features again (above): it no longer
95
+ // holds the business model, only the switches no model implies.
88
96
  // MONEY, then the PEOPLE who can spend it.
89
97
  // A licensed box that bills outside the console hides the row.
90
98
  ...process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === "true" ? [] : [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/config.ts"],"sourcesContent":["/**\n * The sidebar's static shape: icon and permission maps, the nav types, the\n * route sets that decide what counts as active, and the settings drill-in\n * groups.\n *\n * Split out of sidebar.tsx, which had grown to 1,510 lines — three times the\n * house limit. None of this depends on component state, so it does not belong\n * inside the component that renders it.\n */\nimport {\n CreditCard,\n Users,\n Bell,\n Scale,\n UserRound,\n LayoutGrid,\n SlidersHorizontal,\n Database,\n ShieldCheck,\n Accessibility,\n Globe2,\n Palette,\n ChartColumn,\n Sparkles,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { INTENT_ICON_MAP } from '@withpotter/lib/intents/icon-map';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/** Map string icon names from IntentConfig to Lucide components */\nexport const ICON_MAP = INTENT_ICON_MAP;\n\n// The nav permission maps are data and live in ./nav-permissions, so the\n// coverage test can read them without pulling this file's icon imports in.\n// Re-exported here because every existing importer reaches for them via config.\nexport {\n INTENT_ITEM_PERMISSIONS,\n CORE_TOOL_PERMISSIONS,\n} from './nav-permissions';\n\nexport interface NavItem {\n id: string;\n name: string;\n href: string;\n icon: LucideIcon;\n requiredBehavior?: string[];\n /**\n * The row is only drawn on a box whose capability list carries this key. A\n * box that published no list is unchanged, so the main app keeps every row.\n * For a screen whose backend only some engines have: without it the row led\n * to \"Cannot GET /referrals/stats\" on a box that never had that module.\n */\n requiredCapability?: string;\n /**\n * The item is only rendered when the current team member has AT LEAST ONE of\n * these permissions (OWNER always passes). Undefined or empty means the item\n * is hidden from every non-owner — see INTENT_ITEM_PERMISSIONS for why the\n * default is closed rather than open.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Rows revealed under this one. An item with children renders with the same\n * disclosure the fixed spine hubs use, so an Operations surface can carry a\n * smaller surface (Appointments → Waitlist) without claiming a second\n * top-level row for it.\n */\n children?: NavItem[];\n /**\n * Names this row's own landing page once it carries children.\n *\n * A work row with children renders through `renderHub`, which moves the\n * row's own screen into the group as its first child — a row is a place or a\n * container, never both. Without this the child falls back to \"Overview\",\n * which the hub renderer says outright no new hub should rely on: every hub\n * could carry one, so the word tells the merchant nothing about which hub\n * they are in. `{label}` interpolates the row's post-rename label, so a\n * tenant calling them Bookings gets \"All bookings\" rather than a literal.\n */\n landingLabel?: string;\n}\n\nexport interface NavGroupSection {\n id: string;\n label: string;\n items: NavItem[];\n}\n\nexport interface NavGroup {\n id: string;\n label: string;\n icon: LucideIcon;\n items?: NavItem[];\n sections?: NavGroupSection[];\n /**\n * With no `items`, the group is a flat top-level destination (rendered like\n * the Home link — no chevron, no children).\n *\n * With `items`, this is the group's landing page, and it is rendered as the\n * FIRST CHILD rather than on the group's own row. A row is either a place or\n * a container, never both: a row that navigated *and* disclosed gave two\n * answers to one click, and the highlight landed on the parent while the\n * list that had just opened underneath it looked like a menu you had yet to\n * choose from. See `landingLabel` for what the child is called.\n */\n href?: string;\n /**\n * What this group's landing page is called once it sits among its own\n * children. Defaults to \"Overview\". Ignored unless the group has both an\n * `href` and `items`.\n *\n * `{label}` interpolates the group's OWN label, after renaming. A hub named\n * for a merchant-facing noun is renamed per tenant — Customers becomes\n * Guests, Clients, Members — and a literal \"All customers\" would have sat\n * under a hub reading \"Guests\", which is the exact mismatch `renameGroup`\n * exists to prevent. Write `All {label}`, not `All customers`.\n */\n landingLabel?: string;\n /**\n * Where the landing page sits among the children, counting from 0. Absent, it\n * leads, which every hub but Money wants. Money puts the wallet above its\n * Transactions page (John, 2026-10-11): the balance a business holds is the\n * first question, the log of what moved it the second.\n */\n landingAt?: number;\n /**\n * Only meaningful for flat (href) groups: gate visibility on AT LEAST ONE\n * of these permissions (OWNER always passes). Collapsible groups gate at\n * the item level instead.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Render as a disclosure hub even with a single child (normally a 1-child\n * group collapses to a flat link). Used by Inventory so Collections shows\n * inside it.\n */\n alwaysHub?: boolean;\n}\n\nexport const EXACT_MATCH_ROUTES = new Set([\n '/dashboard',\n '/dashboard/settings',\n]);\nexport const PLATFORM_CONTENT_TYPES = new Set(['product', 'products']);\nexport const PLATFORM_CONTENT_ROUTES = new Set([\n '/dashboard/content/product',\n '/dashboard/content/products',\n]);\n\n/** Settings sub-nav — shown in the sidebar (drill-in) when on a settings route. */\nexport type SettingsItem = {\n name: string;\n href: string;\n requires?: string;\n requiresAny?: string[];\n /** Extra routes that also mark this (collapsed) entry active, e.g. tabs. */\n match?: string[];\n /** Rail glyph. The settings rail had labels only, so it was the one nav in\n * the app you had to read rather than recognise — and it's the nav people\n * visit least often, which is exactly where recognition helps most. */\n icon: LucideIcon;\n};\n// The settings drill-in is context-aware. At the ACCOUNT level (account,\n// security, the Workspaces hub) it shows the account's own settings. When you\n// drill into a specific workspace's setting, it becomes THAT workspace's rail.\n// BUSINESS_GROUPS is now the only place these sections are listed — the\n// per-workspace hub page that used to mirror them was a screen whose only job\n// was linking to what this rail already shows, so it was deleted.\n// Login & security is its OWN rail item, not folded into Account.\n//\n// It was meant to be folded — the note here said so — but the fold was never\n// finished: the standalone page kept passkeys, connected sign-in and the device\n// list, gained no row, and was linked from nowhere, so the richest security\n// surface in the app could only be reached by typing the URL and nothing lit up\n// when you got there. Worse, the half that DID move was duplicated rather than\n// moved: two-factor and sessions rendered on both pages, so switching 2FA on in\n// one left the other showing it off until a reload.\n//\n// One page owns security now. Account keeps who you are and the door out.\nexport const ACCOUNT_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n label: 'Account',\n items: [\n { name: 'Account', href: '/dashboard/settings/profile', icon: UserRound },\n {\n name: 'Login & security',\n href: '/dashboard/settings/devices',\n icon: ShieldCheck,\n },\n { name: 'Businesses', href: '/dashboard/workspaces', icon: LayoutGrid },\n {\n name: 'Preferences',\n href: '/dashboard/settings/language',\n icon: SlidersHorizontal,\n },\n // Reachable from the mobile Me screen and from nowhere on desktop, so\n // the setting a merchant needs in order to USE the product was the one\n // they could not navigate to.\n {\n name: 'Accessibility',\n href: '/dashboard/settings/accessibility',\n icon: Accessibility,\n },\n {\n name: 'Data & privacy',\n href: '/dashboard/settings/data-privacy',\n icon: Database,\n },\n { name: 'Legal', href: '/dashboard/settings/legal', icon: Scale },\n ],\n },\n];\n\n/**\n * ONE ORDERED LIST, NO GROUP HEADINGS.\n *\n * These rows sat under `Setup` / `Selling` / `Records` and the headings were\n * doing no work. `Setup` held Billing and Notifications, which you visit for\n * years after setting up. `Records` held exactly one row. And a merchant\n * looking for Taxes does not first decide which of the three thirds it is in —\n * they scan for the word \"Taxes\".\n *\n * Shopify carries nineteen settings rows with no headings at all and it reads\n * fine, because every label is a noun the merchant already owns. What does the\n * work there is the ORDER, and it is not alphabetical: identity, then money,\n * then people, then selling, then places, then the storefront, then the log.\n * That is the order below.\n *\n * ON THE LABELS: an ampersand is fine when the second half says what else is\n * inside — `Team & permissions`, `Billing & plan`, `Login & security`,\n * `Data & privacy` each tell you something the first word alone does not. What\n * is not fine is a word from our side of the fence:\n *\n * `Operations` → **Locations**. An umbrella nobody says out loud; the row lands\n * on the locations list, hours and booking policy are tabs of a location, and\n * Locations is what both Shopify and Square call it.\n *\n * `Integrations` → **Connections**. Integration is a developer's word for what\n * a merchant experiences as connecting their WhatsApp or their Google Calendar.\n * Not `Apps` — that is Shopify's word for a marketplace we do not have, and the\n * label would promise a store that isn't there.\n *\n * `Tracking`, not Shopify's `Customer events`: a merchant running Meta ads\n * already calls it tracking, and \"customer events\" is a platform's internal\n * noun leaking into the nav.\n */\nexport const BUSINESS_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n // No heading. See the note above — the order is the structure.\n label: '',\n items: [\n // IDENTITY — who this workspace is.\n {\n name: 'General',\n href: '/dashboard/settings',\n match: ['/dashboard/settings/branding', '/dashboard/settings/regional'],\n icon: SlidersHorizontal,\n },\n // NO BUSINESS TYPE ROW. It moved out of Settings entirely on\n // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.\n //\n // The page always DID the right thing — picking a model derives the\n // behaviour set that builds the sidebar — and nobody could find it. It\n // was three names deep in the wrong place: behind Workspace settings,\n // under a heading a merchant does not type when the thought is \"can\n // Box do events?\". A settings list is where you go to change\n // something you already have, and this is where you go to get something\n // you do not.\n //\n // `/dashboard/settings/features` redirects, so nothing that linked to it\n // breaks.\n\n // MONEY, then the PEOPLE who can spend it.\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true'\n ? []\n : [\n {\n // Per business: each tenant has its own subscription.\n name: 'Billing & plan',\n href: '/dashboard/settings/billing',\n icon: CreditCard,\n },\n ]),\n {\n // What the plan is being spent on: fees, credits, delivery. Billing is\n // the bill; this is the meter behind it. Credits are bought from here.\n name: 'Usage',\n href: '/dashboard/settings/usage',\n match: ['/dashboard/settings/billing/credits'],\n icon: ChartColumn,\n },\n {\n name: 'Team & permissions',\n href: '/dashboard/settings/team',\n match: ['/dashboard/settings/roles', '/dashboard/settings/activity'],\n icon: Users,\n },\n\n // THE TERMS, and who gets told.\n { name: 'Policies', href: '/dashboard/settings/policies', icon: Scale },\n {\n // Configuration, not history. This sat beside Activity, which is the\n // log — so the one screen that decides whether a customer is told\n // anything was filed with the screen reporting what already happened.\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n {\n // Sample products, pages and pictures a business can start with, clear\n // in one go, or add again. Owner and admins act; others see the state.\n name: 'Demo content',\n href: '/dashboard/settings/demo-content',\n icon: Sparkles,\n },\n\n // Connectors moved to Customize (2026-10-03), beside Add more: both are\n // \"what is in this workspace\". `/dashboard/settings/integrations` now\n // redirects there; its sub-routes (calendar-sync, sales-channels,\n // tracking, a single connection) still live under Settings.\n ],\n },\n];\n\n// Settings routes that belong to the PERSON, not a business. Everything else\n// under /dashboard/settings is a business setting and gets the business rail.\nexport const ACCOUNT_ROUTE_PREFIXES = [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/language',\n '/dashboard/settings/data-privacy',\n '/dashboard/settings/legal',\n '/dashboard/settings/accessibility',\n];\n\nexport const parseSidebarHref = (href: string) => {\n const [path, query = ''] = href.split('?');\n const params = new URLSearchParams(query);\n return {\n path,\n intent: params.get('intent'),\n /**\n * Everything else the row narrows by — `group`, `tab`, `view`.\n *\n * This was `group` alone, added because a business with more than one group\n * of bookable things reaches the roster through rows that all lead to the\n * same PATH: without it every row lit and the tie-break picked whichever\n * href was longer. Every OTHER param is the same row on the same path, and\n * was still invisible — see `active-href.ts`.\n */\n params,\n };\n};\n\n/**\n * The archetypes an `?intent=` narrows to.\n *\n * A comma list, because a collapsed row stands for every archetype behind it —\n * a restaurant's Menu is `dine-in,food-orders`, which is neither one archetype\n * nor all of them. One key is still the common case; empty means not narrowed.\n */\nexport const intentKeySet = (intent: string | null | undefined): Set<string> =>\n new Set(\n (intent ?? '')\n .split(',')\n .map((key) => key.trim())\n .filter(Boolean),\n );\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/config.ts"],"sourcesContent":["/**\n * The sidebar's static shape: icon and permission maps, the nav types, the\n * route sets that decide what counts as active, and the settings drill-in\n * groups.\n *\n * Split out of sidebar.tsx, which had grown to 1,510 lines — three times the\n * house limit. None of this depends on component state, so it does not belong\n * inside the component that renders it.\n */\nimport {\n CreditCard,\n Users,\n Bell,\n Scale,\n UserRound,\n LayoutGrid,\n SlidersHorizontal,\n ToggleLeft,\n Database,\n ShieldCheck,\n Accessibility,\n Globe2,\n Palette,\n ChartColumn,\n Sparkles,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { INTENT_ICON_MAP } from '@withpotter/lib/intents/icon-map';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/** Map string icon names from IntentConfig to Lucide components */\nexport const ICON_MAP = INTENT_ICON_MAP;\n\n// The nav permission maps are data and live in ./nav-permissions, so the\n// coverage test can read them without pulling this file's icon imports in.\n// Re-exported here because every existing importer reaches for them via config.\nexport {\n INTENT_ITEM_PERMISSIONS,\n CORE_TOOL_PERMISSIONS,\n} from './nav-permissions';\n\nexport interface NavItem {\n id: string;\n name: string;\n href: string;\n icon: LucideIcon;\n requiredBehavior?: string[];\n /**\n * The row is only drawn on a box whose capability list carries this key. A\n * box that published no list is unchanged, so the main app keeps every row.\n * For a screen whose backend only some engines have: without it the row led\n * to \"Cannot GET /referrals/stats\" on a box that never had that module.\n */\n requiredCapability?: string;\n /**\n * The item is only rendered when the current team member has AT LEAST ONE of\n * these permissions (OWNER always passes). Undefined or empty means the item\n * is hidden from every non-owner — see INTENT_ITEM_PERMISSIONS for why the\n * default is closed rather than open.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Rows revealed under this one. An item with children renders with the same\n * disclosure the fixed spine hubs use, so an Operations surface can carry a\n * smaller surface (Appointments → Waitlist) without claiming a second\n * top-level row for it.\n */\n children?: NavItem[];\n /**\n * Names this row's own landing page once it carries children.\n *\n * A work row with children renders through `renderHub`, which moves the\n * row's own screen into the group as its first child — a row is a place or a\n * container, never both. Without this the child falls back to \"Overview\",\n * which the hub renderer says outright no new hub should rely on: every hub\n * could carry one, so the word tells the merchant nothing about which hub\n * they are in. `{label}` interpolates the row's post-rename label, so a\n * tenant calling them Bookings gets \"All bookings\" rather than a literal.\n */\n landingLabel?: string;\n}\n\nexport interface NavGroupSection {\n id: string;\n label: string;\n items: NavItem[];\n}\n\nexport interface NavGroup {\n id: string;\n label: string;\n icon: LucideIcon;\n items?: NavItem[];\n sections?: NavGroupSection[];\n /**\n * With no `items`, the group is a flat top-level destination (rendered like\n * the Home link — no chevron, no children).\n *\n * With `items`, this is the group's landing page, and it is rendered as the\n * FIRST CHILD rather than on the group's own row. A row is either a place or\n * a container, never both: a row that navigated *and* disclosed gave two\n * answers to one click, and the highlight landed on the parent while the\n * list that had just opened underneath it looked like a menu you had yet to\n * choose from. See `landingLabel` for what the child is called.\n */\n href?: string;\n /**\n * What this group's landing page is called once it sits among its own\n * children. Defaults to \"Overview\". Ignored unless the group has both an\n * `href` and `items`.\n *\n * `{label}` interpolates the group's OWN label, after renaming. A hub named\n * for a merchant-facing noun is renamed per tenant — Customers becomes\n * Guests, Clients, Members — and a literal \"All customers\" would have sat\n * under a hub reading \"Guests\", which is the exact mismatch `renameGroup`\n * exists to prevent. Write `All {label}`, not `All customers`.\n */\n landingLabel?: string;\n /**\n * Where the landing page sits among the children, counting from 0. Absent, it\n * leads, which every hub but Money wants. Money puts the wallet above its\n * Transactions page (John, 2026-10-11): the balance a business holds is the\n * first question, the log of what moved it the second.\n */\n landingAt?: number;\n /**\n * Only meaningful for flat (href) groups: gate visibility on AT LEAST ONE\n * of these permissions (OWNER always passes). Collapsible groups gate at\n * the item level instead.\n */\n requiredPermission?: PermissionKey[];\n /**\n * Render as a disclosure hub even with a single child (normally a 1-child\n * group collapses to a flat link). Used by Inventory so Collections shows\n * inside it.\n */\n alwaysHub?: boolean;\n}\n\nexport const EXACT_MATCH_ROUTES = new Set([\n '/dashboard',\n '/dashboard/settings',\n]);\nexport const PLATFORM_CONTENT_TYPES = new Set(['product', 'products']);\nexport const PLATFORM_CONTENT_ROUTES = new Set([\n '/dashboard/content/product',\n '/dashboard/content/products',\n]);\n\n/** Settings sub-nav — shown in the sidebar (drill-in) when on a settings route. */\nexport type SettingsItem = {\n name: string;\n href: string;\n requires?: string;\n requiresAny?: string[];\n /** Extra routes that also mark this (collapsed) entry active, e.g. tabs. */\n match?: string[];\n /** Rail glyph. The settings rail had labels only, so it was the one nav in\n * the app you had to read rather than recognise — and it's the nav people\n * visit least often, which is exactly where recognition helps most. */\n icon: LucideIcon;\n};\n// The settings drill-in is context-aware. At the ACCOUNT level (account,\n// security, the Workspaces hub) it shows the account's own settings. When you\n// drill into a specific workspace's setting, it becomes THAT workspace's rail.\n// BUSINESS_GROUPS is now the only place these sections are listed — the\n// per-workspace hub page that used to mirror them was a screen whose only job\n// was linking to what this rail already shows, so it was deleted.\n// Login & security is its OWN rail item, not folded into Account.\n//\n// It was meant to be folded — the note here said so — but the fold was never\n// finished: the standalone page kept passkeys, connected sign-in and the device\n// list, gained no row, and was linked from nowhere, so the richest security\n// surface in the app could only be reached by typing the URL and nothing lit up\n// when you got there. Worse, the half that DID move was duplicated rather than\n// moved: two-factor and sessions rendered on both pages, so switching 2FA on in\n// one left the other showing it off until a reload.\n//\n// One page owns security now. Account keeps who you are and the door out.\nexport const ACCOUNT_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n label: 'Account',\n items: [\n { name: 'Account', href: '/dashboard/settings/profile', icon: UserRound },\n {\n name: 'Login & security',\n href: '/dashboard/settings/devices',\n icon: ShieldCheck,\n },\n { name: 'Businesses', href: '/dashboard/workspaces', icon: LayoutGrid },\n {\n name: 'Preferences',\n href: '/dashboard/settings/language',\n icon: SlidersHorizontal,\n },\n // Reachable from the mobile Me screen and from nowhere on desktop, so\n // the setting a merchant needs in order to USE the product was the one\n // they could not navigate to.\n {\n name: 'Accessibility',\n href: '/dashboard/settings/accessibility',\n icon: Accessibility,\n },\n {\n name: 'Data & privacy',\n href: '/dashboard/settings/data-privacy',\n icon: Database,\n },\n { name: 'Legal', href: '/dashboard/settings/legal', icon: Scale },\n ],\n },\n];\n\n/**\n * ONE ORDERED LIST, NO GROUP HEADINGS.\n *\n * These rows sat under `Setup` / `Selling` / `Records` and the headings were\n * doing no work. `Setup` held Billing and Notifications, which you visit for\n * years after setting up. `Records` held exactly one row. And a merchant\n * looking for Taxes does not first decide which of the three thirds it is in —\n * they scan for the word \"Taxes\".\n *\n * Shopify carries nineteen settings rows with no headings at all and it reads\n * fine, because every label is a noun the merchant already owns. What does the\n * work there is the ORDER, and it is not alphabetical: identity, then money,\n * then people, then selling, then places, then the storefront, then the log.\n * That is the order below.\n *\n * ON THE LABELS: an ampersand is fine when the second half says what else is\n * inside — `Team & permissions`, `Billing & plan`, `Login & security`,\n * `Data & privacy` each tell you something the first word alone does not. What\n * is not fine is a word from our side of the fence:\n *\n * `Operations` → **Locations**. An umbrella nobody says out loud; the row lands\n * on the locations list, hours and booking policy are tabs of a location, and\n * Locations is what both Shopify and Square call it.\n *\n * `Integrations` → **Connections**. Integration is a developer's word for what\n * a merchant experiences as connecting their WhatsApp or their Google Calendar.\n * Not `Apps` — that is Shopify's word for a marketplace we do not have, and the\n * label would promise a store that isn't there.\n *\n * `Tracking`, not Shopify's `Customer events`: a merchant running Meta ads\n * already calls it tracking, and \"customer events\" is a platform's internal\n * noun leaking into the nav.\n */\nexport const BUSINESS_GROUPS: { label: string; items: SettingsItem[] }[] = [\n {\n // No heading. See the note above — the order is the structure.\n label: '',\n items: [\n // IDENTITY — who this workspace is.\n {\n name: 'General',\n href: '/dashboard/settings',\n match: ['/dashboard/settings/branding', '/dashboard/settings/regional'],\n icon: SlidersHorizontal,\n },\n // FEATURES — the switches no business type implies (Blog today).\n // Business types themselves are Explore's, see below.\n {\n name: 'Features',\n href: '/dashboard/settings/features',\n icon: ToggleLeft,\n },\n // NO BUSINESS TYPE ROW. It moved out of Settings entirely on\n // 2026-09-03 and became `/dashboard/explore` with a rail row of its own.\n //\n // The page always DID the right thing — picking a model derives the\n // behaviour set that builds the sidebar — and nobody could find it. It\n // was three names deep in the wrong place: behind Workspace settings,\n // under a heading a merchant does not type when the thought is \"can\n // Box do events?\". A settings list is where you go to change\n // something you already have, and this is where you go to get something\n // you do not.\n //\n // `/dashboard/settings/features` is Features again (above): it no longer\n // holds the business model, only the switches no model implies.\n\n // MONEY, then the PEOPLE who can spend it.\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true'\n ? []\n : [\n {\n // Per business: each tenant has its own subscription.\n name: 'Billing & plan',\n href: '/dashboard/settings/billing',\n icon: CreditCard,\n },\n ]),\n {\n // What the plan is being spent on: fees, credits, delivery. Billing is\n // the bill; this is the meter behind it. Credits are bought from here.\n name: 'Usage',\n href: '/dashboard/settings/usage',\n match: ['/dashboard/settings/billing/credits'],\n icon: ChartColumn,\n },\n {\n name: 'Team & permissions',\n href: '/dashboard/settings/team',\n match: ['/dashboard/settings/roles', '/dashboard/settings/activity'],\n icon: Users,\n },\n\n // THE TERMS, and who gets told.\n { name: 'Policies', href: '/dashboard/settings/policies', icon: Scale },\n {\n // Configuration, not history. This sat beside Activity, which is the\n // log — so the one screen that decides whether a customer is told\n // anything was filed with the screen reporting what already happened.\n name: 'Notifications',\n href: '/dashboard/settings/notifications',\n icon: Bell,\n },\n {\n // Sample products, pages and pictures a business can start with, clear\n // in one go, or add again. Owner and admins act; others see the state.\n name: 'Demo content',\n href: '/dashboard/settings/demo-content',\n icon: Sparkles,\n },\n\n // Connectors moved to Customize (2026-10-03), beside Add more: both are\n // \"what is in this workspace\". `/dashboard/settings/integrations` now\n // redirects there; its sub-routes (calendar-sync, sales-channels,\n // tracking, a single connection) still live under Settings.\n ],\n },\n];\n\n// Settings routes that belong to the PERSON, not a business. Everything else\n// under /dashboard/settings is a business setting and gets the business rail.\nexport const ACCOUNT_ROUTE_PREFIXES = [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/language',\n '/dashboard/settings/data-privacy',\n '/dashboard/settings/legal',\n '/dashboard/settings/accessibility',\n];\n\nexport const parseSidebarHref = (href: string) => {\n const [path, query = ''] = href.split('?');\n const params = new URLSearchParams(query);\n return {\n path,\n intent: params.get('intent'),\n /**\n * Everything else the row narrows by — `group`, `tab`, `view`.\n *\n * This was `group` alone, added because a business with more than one group\n * of bookable things reaches the roster through rows that all lead to the\n * same PATH: without it every row lit and the tie-break picked whichever\n * href was longer. Every OTHER param is the same row on the same path, and\n * was still invisible — see `active-href.ts`.\n */\n params,\n };\n};\n\n/**\n * The archetypes an `?intent=` narrows to.\n *\n * A comma list, because a collapsed row stands for every archetype behind it —\n * a restaurant's Menu is `dine-in,food-orders`, which is neither one archetype\n * nor all of them. One key is still the common case; empty means not narrowed.\n */\nexport const intentKeySet = (intent: string | null | undefined): Set<string> =>\n new Set(\n (intent ?? '')\n .split(',')\n .map((key) => key.trim())\n .filter(Boolean),\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.65",
3
+ "version": "0.13.0-next.67",
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.65",
54
- "@withpotter/lib": "^0.13.0-next.65",
55
- "@withpotter/ui-kit": "^0.13.0-next.65",
53
+ "@withpotter/hooks": "^0.13.0-next.67",
54
+ "@withpotter/lib": "^0.13.0-next.67",
55
+ "@withpotter/ui-kit": "^0.13.0-next.67",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",