@withpotter/components 0.13.0-next.62 → 0.13.0-next.63

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,7 +9,8 @@ import {
9
9
  Database,
10
10
  ShieldCheck,
11
11
  Accessibility,
12
- ChartColumn
12
+ ChartColumn,
13
+ Sparkles
13
14
  } from "lucide-react";
14
15
  import { INTENT_ICON_MAP } from "@withpotter/lib/intents/icon-map";
15
16
  const ICON_MAP = INTENT_ICON_MAP;
@@ -117,6 +118,13 @@ const BUSINESS_GROUPS = [
117
118
  name: "Notifications",
118
119
  href: "/dashboard/settings/notifications",
119
120
  icon: Bell
121
+ },
122
+ {
123
+ // Sample products, pages and pictures a business can start with, clear
124
+ // in one go, or add again. Owner and admins act; others see the state.
125
+ name: "Demo content",
126
+ href: "/dashboard/settings/demo-content",
127
+ icon: Sparkles
120
128
  }
121
129
  // Connectors moved to Customize (2026-10-03), beside Add more: both are
122
130
  // "what is in this workspace". `/dashboard/settings/integrations` now
@@ -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} 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 // 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 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.62",
3
+ "version": "0.13.0-next.63",
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.62",
54
- "@withpotter/lib": "^0.13.0-next.62",
55
- "@withpotter/ui-kit": "^0.13.0-next.62",
53
+ "@withpotter/hooks": "^0.13.0-next.63",
54
+ "@withpotter/lib": "^0.13.0-next.63",
55
+ "@withpotter/ui-kit": "^0.13.0-next.63",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",
@@ -77,7 +77,7 @@
77
77
  "typescript": "^5"
78
78
  },
79
79
  "publishConfig": {
80
- "registry": "https://registry.npmjs.org",
80
+ "registry": "https://registry.npmjs.org/",
81
81
  "access": "restricted"
82
82
  }
83
83
  }