@withpotter/components 0.13.0-next.97 → 0.13.0-next.99

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.
@@ -7,6 +7,7 @@ import { durations, eases, springs } from "@withpotter/lib/motion";
7
7
  import { useRouter } from "next/navigation";
8
8
  import { usePermissions } from "@withpotter/lib/use-permissions";
9
9
  import { Check, ChevronDown, Settings, UserPlus } from "lucide-react";
10
+ import { useSidebarCards } from "./sidebar-cards";
10
11
  import { useAuth } from "@withpotter/lib/auth-context";
11
12
  import { WorkspaceMark } from "./workspace-mark";
12
13
  import { businessCategoryLabel } from "@withpotter/lib/business-categories";
@@ -28,6 +29,7 @@ function ProductSwitcher({
28
29
  const foot = variant === "foot";
29
30
  const head = variant === "head";
30
31
  const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();
32
+ const planLabel = useSidebarCards()?.planLabel;
31
33
  const router = useRouter();
32
34
  const { can, loading: permissionsLoading } = usePermissions();
33
35
  const canManageTeam = can("canManageTeam") && !permissionsLoading;
@@ -204,6 +206,7 @@ function ProductSwitcher({
204
206
  /* @__PURE__ */ jsx("b", { children: name }),
205
207
  activeDescription && /* @__PURE__ */ jsx("span", { className: "cap", children: activeDescription })
206
208
  ] }),
209
+ planLabel && /* @__PURE__ */ jsx("span", { className: "pv-psw-plan", children: planLabel }),
207
210
  /* @__PURE__ */ jsx(Check, { className: "pv-psw-check", width: 15, height: 15 })
208
211
  ]
209
212
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/product-switcher.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { useRouter } from 'next/navigation';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { Check, ChevronDown, Settings, UserPlus } from 'lucide-react';\nimport { useAuth, type Workspace } from '@withpotter/lib/auth-context';\nimport { WorkspaceMark } from './workspace-mark';\nimport { businessCategoryLabel } from '@withpotter/lib/business-categories';\nimport { byRecentlyUsed, noteWorkspaceUsed } from '@withpotter/lib/workspace-recents';\nimport { SwitcherScrim } from './switcher-scrim';\nimport { SwitcherWorkspaceList } from './switcher-workspace-list';\nimport { CreateBusinessModal } from '@withpotter/components/create-business-modal';\nimport { useSettingsModal } from './settings-modal-context';\nimport { ShellIcon } from './icons';\nimport { TeamModal } from '@withpotter/pages-core/dashboard/settings/team/_components/team-modal';\nimport {\n LocationScopePin,\n SwitcherLocationGroup,\n} from './switcher-location-group';\n\n/**\n * How many other workspaces the menu lists before deferring to the page.\n *\n * Two, and the two most recently opened — John, 2026-08-23. Three rows of\n * somewhere-else was the largest block in a menu whose subject is the\n * workspace you are in, and picking which two by recency is the only ordering\n * that gets better the more the menu is used. See `workspace-recents`.\n */\nconst INLINE_WORKSPACES = 2;\n\n/**\n * The business switcher: one trigger, one scope.\n *\n * Ordered by hierarchy, not by scope: the business you are in (a header block,\n * not a row), then *Switch to* the ones you could be in, then the person they\n * belong to (`switcher-account-group`). Out of this menu on purpose: the theme\n * flip (a preference, it rides the footer), Plan & billing, the team row and\n * business settings (footer rows now), and the plan label under the name. A\n * merchant presses this to change business, so every row between them and that\n * is in the way.\n */\nexport function ProductSwitcher({\n variant = 'top',\n}: {\n /**\n * `head` is the flat row at the top of the rail: mark and name only, with\n * search beside it (see `side-head` in shell.css).\n * `foot` is the same switcher drawn as a flat row at the bottom of the rail\n * (mark, name, chevron) whose menu opens UPWARD, because there is no room\n * under it. Same menu, same state, same keyboard walk.\n */\n variant?: 'top' | 'foot' | 'head';\n} = {}) {\n const foot = variant === 'foot';\n const head = variant === 'head';\n const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();\n const router = useRouter();\n const { can, loading: permissionsLoading } = usePermissions();\n // False while the membership request is in flight, so the row appears when\n // the answer arrives rather than flashing at somebody who turns out to be\n // staff.\n const canManageTeam = can('canManageTeam') && !permissionsLoading;\n const [open, setOpen] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n // The settings window is one shared modal (`SettingsModalProvider`), so closing\n // this menu cannot unmount it.\n const { openSettings } = useSettingsModal();\n // Mounted only while open: the modal reads the whole team on mount.\n const [teamOpen, setTeamOpen] = useState(false);\n const [pos, setPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n }>({ top: 0, left: 0, width: 280 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n /**\n * Stamp the workspace you are in as the most recently used one.\n *\n * Here rather than in `persistActiveWorkspace`, which is the tidier choke\n * point and the wrong place to spend lines: auth-context is on the file-size\n * ledger and must shrink, not grow. This is equivalent — the shell renders\n * for every workspace that becomes active, whatever route made it active —\n * and it keeps the whole feature in the one component that reads it.\n */\n useEffect(() => {\n if (tenant?.slug) noteWorkspaceUsed(tenant.slug);\n }, [tenant?.slug]);\n\n // Measure at click time (not in an effect) so the portaled menu is\n // positioned before it paints.\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b)\n setPos({\n ...(foot\n ? { bottom: window.innerHeight - b.top + 6 }\n : { top: b.bottom + 6 }),\n left: b.left,\n width: Math.max(b.width, 280),\n });\n }\n setOpen((v) => !v);\n };\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n (e.target as Element | null)?.closest?.('[data-pv-psw-flyout]') ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n // This is the app's primary navigation surface, so it has to be usable\n // from the keyboard alone: opening lands on the first control and the\n // arrows walk the list.\n //\n // THE MENU, not its first row. There is nothing to type into any more, so\n // focusing the first control puts a ring on the workspace you are already\n // in the instant the menu opens, which reads as a selected row rather than\n // a landing place. The container takes focus instead and the arrow handler\n // below walks into the list from there — `indexOf` returns -1 for the\n // container, so ArrowDown lands on the first item.\n popRef.current?.focus();\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const onMenuKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Tab') {\n setOpen(false);\n return;\n }\n const step =\n e.key === 'ArrowDown'\n ? 1\n : e.key === 'ArrowUp'\n ? -1\n : e.key === 'Home' || e.key === 'End'\n ? 0\n : null;\n if (step === null) return;\n e.preventDefault();\n // `button, a[href]` — not `button`. Help & support is a real Link so it can\n // be cmd-clicked like any destination, and a selector of `button` alone\n // walked straight past it: the one row in this menu that leaves the app for\n // a page was the one row the arrows could not reach.\n const items = Array.from(\n popRef.current?.querySelectorAll<HTMLElement>('button, a[href]') ?? [],\n );\n if (!items.length) return;\n if (step === 0) {\n (e.key === 'Home' ? items[0] : items[items.length - 1]).focus();\n return;\n }\n const at = items.indexOf(document.activeElement as HTMLElement);\n items[(at + step + items.length) % items.length].focus();\n };\n\n const name = tenant?.businessName ?? 'Your business';\n\n /** Sub-line for a row: what kind of business it is, prefixed by a role worth naming. */\n const describe = (w: Workspace) =>\n [w.role !== 'owner' ? w.role : '', businessCategoryLabel(w.tenant)]\n .filter(Boolean)\n .join(' · ');\n\n /**\n * What the workspace you are in is, in the sub-line the list rows use.\n *\n * Off the workspace ENTRY where there is one, because that is what carries\n * your role here; `tenant` alone can only answer the category half.\n */\n const activeDescription = useMemo(() => {\n const mine = workspaces.find((w) => w.tenant.slug === tenant?.slug);\n if (mine) return describe(mine);\n return tenant ? businessCategoryLabel(tenant) : '';\n }, [workspaces, tenant]);\n\n const go = (href: string) => {\n setOpen(false);\n router.push(href);\n };\n\n const onSwitch = async (slug: string) => {\n setOpen(false);\n if (slug === tenant?.slug) return;\n // Soft swap: switchWorkspace swaps the tenant in context and every\n // useAuth() consumer re-renders. Refresh server segments only — no reload.\n await switchWorkspace(slug);\n router.replace('/dashboard');\n };\n\n /**\n * Everywhere but here. The workspace you are in leads the menu as the first\n * row of its own group, ticked and carrying its picture and its category —\n * so listing it again down here would be the same row twice, which is what\n * the tick had to distinguish it from.\n */\n const listed = useMemo(\n () =>\n byRecentlyUsed(\n workspaces.filter((w) => w.tenant.slug !== tenant?.slug),\n (w) => w.tenant.slug,\n ),\n // Re-read on every switch: the slug changing IS the event that reorders\n // this, and it is the only one that can happen while the menu is mounted.\n [workspaces, tenant?.slug],\n );\n\n // The first few, and a way to the rest. There is no filter here any more:\n // John, 2026-08-23, \"take out the search bar\". It only ever appeared above\n // three workspaces, it was the first thing focus landed on, and the page it\n // defers to can search properly.\n const shown = listed.slice(0, INLINE_WORKSPACES);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n className={\n 'pswitch' +\n (foot ? ' pswitch-foot' : '') +\n (head ? ' pswitch-head' : '') +\n (activeDescription && variant === 'top' ? ' has-sub' : '') +\n (open ? ' open' : '')\n }\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={toggle}\n title={name}\n >\n <WorkspaceMark\n className=\"wmark\"\n logo={tenant?.businessLogo}\n label={name}\n />\n <span className=\"pl\">\n <b>{name}</b>\n {/* What the business IS, where the plan label used to be (John,\n 2026-08-23) — a fact about the workspace rather than a billing\n status, which is what the most permanent line in the app should\n hold. Absent when nothing is stored, and the card drops a step\n rather than holding the space open. Set in Settings → General. */}\n {activeDescription && variant === 'top' && (\n <span>{activeDescription}</span>\n )}\n </span>\n <span className=\"sw\">\n <LocationScopePin />\n {foot || head ? (\n <ChevronDown width={15} height={15} />\n ) : (\n <ShellIcon.updown width={15} height={15} />\n )}\n </span>\n </button>\n\n {/* AnimatePresence INSIDE the portal, and the portal itself is only\n rendered once there is somewhere to put it. `{open && createPortal(…)}`\n ripped the menu out of the tree the moment it closed, so it arrived on\n a spring and vanished on a frame — the exact defect `FormDialog`\n documents for `{editing && <FormDialog/>}`. */}\n {/* `document` rather than a mounted flag: the portal's children are\n `open && …`, which is false on the first client render, so it puts\n nothing in the DOM and there is no markup for the server to disagree\n with. A state-in-effect flag would have been one render pass to learn\n what `typeof` already knows. */}\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n key=\"menu\"\n ref={popRef}\n className=\"pv-psw-menu\"\n tabIndex={-1}\n role=\"menu\"\n aria-label=\"Businesses and account\"\n onKeyDown={onMenuKey}\n /* THE MOTION SYSTEM, not a 140ms CSS keyframe (John, 2026-08-23).\n `springs.gentle` is the surface preset every dialog and sheet\n arrives on; this menu was one of the hand-rolled surfaces that\n never got it. It grows from its top-left, the corner nearest the\n trigger, so it reads as coming OUT of the control. No exit, for\n the reason `PageTransition` gives: a dismissed menu lingering\n delays whatever you pressed it to reach. */\n initial={{ opacity: 0, scale: 0.96, y: foot ? 4 : -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n /* IT LEAVES FASTER THAN IT ARRIVES. The note above is still true —\n a dismissed menu lingering delays whatever you pressed it to\n reach — but \"no delay\" and \"no motion\" are different things, and\n what shipped was the second. So the exit is a tween at\n `--motion-fast`, half the arrival and on the way OUT of the same\n corner: gone before you can want it gone, and still a movement\n rather than a cut. */\n exit={{\n opacity: 0,\n scale: 0.98,\n y: foot ? 2 : -2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n minWidth: pos.width,\n transformOrigin: foot ? 'bottom left' : 'top left',\n }}\n >\n {/* NO ROWS ABOVE THE LIST AT ALL NOW, and that is the end of\n a sequence rather than a fourth opinion. Workspace settings and\n the team went to the rail (John, 2026-09-03); the heading and\n the rule under this group went with them; and Plan & billing\n went last, once we had actually checked what it describes.\n\n WHY PLAN & BILLING WENT. John, 2026-09-03: *\"exactly no plans\n just credits.\"* There is one plan and nothing renews it; what a\n merchant can buy is credits. It is a settings page\n (/dashboard/settings/billing), reached through Workspace\n settings in the rail.\n\n SO THE MENU IS WHAT IT SAYS IT IS — the workspace you are in,\n then the ones you are not, then you.\n\n THE WORKSPACE ITSELF LEADS IT. John, 2026-08-23: *\"the active\n workspace should be on top the workspace settings.\"* It had\n been a row down in the list wearing a tick, which is where you\n look to CHANGE workspace, not to read which one you are in —\n and it left the rows above it acting on a business nothing on\n screen had named. As the group's first row it is both: the\n name, the picture and the kind of business, ticked, with what\n you can do to it directly beneath.\n\n It stays pressable rather than becoming a static block, because\n a row that looks like the rows under it should behave like\n them. `onSwitch` returns early on the slug you are already on,\n so the press is a no-op rather than a reload. */}\n <div role=\"group\" aria-label={`This business: ${name}`}>\n {tenant && (\n <button\n type=\"button\"\n className=\"pv-psw-item current\"\n role=\"menuitem\"\n aria-current=\"true\"\n onClick={() => onSwitch(tenant.slug)}\n >\n <WorkspaceMark\n className=\"pv-psw-ic\"\n logo={tenant.businessLogo}\n label={name}\n />\n <span className=\"pv-psw-txt\">\n <b>{name}</b>\n {/* The SAME sub-line the rows below use, and guarded the\n same way. It was `businessCategoryLabel` written out\n again, rendered unconditionally — so a workspace with\n no category stored (which is most of them until\n somebody picks one) got an empty span holding open a\n line, and the row you are in was the one row in the\n menu wearing a blank where its neighbours wore\n nothing. `describe` also carries the role, which the\n rows below show and this one silently dropped. */}\n {activeDescription && (\n <span className=\"cap\">{activeDescription}</span>\n )}\n </span>\n <Check className=\"pv-psw-check\" width={15} height={15} />\n </button>\n )}\n {/* What you can do to the business you are in, directly beneath\n it. Back in the menu on John's call, 2026-10-04 (it had\n moved to a rail row on 2026-09-03). */}\n {canManageTeam && (\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n setTeamOpen(true);\n }}\n >\n <UserPlus width={15} height={15} />\n Invite your team\n </button>\n )}\n <SwitcherLocationGroup\n onPicked={() => setOpen(false)}\n onManage={() => go('/dashboard/operations/locations')}\n />\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n openSettings();\n }}\n >\n <Settings width={15} height={15} />\n Settings\n </button>\n </div>\n\n {/* Closes off the active business and its actions — only when\n there is something under it. */}\n <div className=\"pv-psw-sep\" />\n\n <SwitcherWorkspaceList\n shown={shown}\n total={workspaces.length}\n describe={describe}\n onSwitch={onSwitch}\n onAdd={() => {\n setOpen(false);\n setShowCreate(true);\n }}\n onManage={() => go('/dashboard/workspaces')}\n user={user ?? undefined}\n onAccount={() => {\n setOpen(false);\n openSettings('/dashboard/settings/profile');\n }}\n onLogout={() => {\n setOpen(false);\n logout();\n }}\n onClose={() => setOpen(false)}\n />\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n\n <CreateBusinessModal\n open={showCreate}\n onClose={() => setShowCreate(false)}\n />\n\n {teamOpen && (\n <TeamModal open initialInvite onClose={() => setTeamOpen(false)} />\n )}\n </>\n );\n}\n"],"mappings":";AA+OI,mBAgBI,KAKA,YArBJ;AA7OJ,SAAS,WAAW,SAAS,QAAQ,gBAAgB;AACrD,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB,cAAc;AACxC,SAAS,WAAW,OAAO,eAAe;AAC1C,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAC/B,SAAS,OAAO,aAAa,UAAU,gBAAgB;AACvD,SAAS,eAA+B;AACxC,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,gBAAgB,yBAAyB;AAClD,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB;AACjC,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAUP,MAAM,oBAAoB;AAanB,SAAS,gBAAgB;AAAA,EAC9B,UAAU;AACZ,IASI,CAAC,GAAG;AACN,QAAM,OAAO,YAAY;AACzB,QAAM,OAAO,YAAY;AACzB,QAAM,EAAE,QAAQ,MAAM,YAAY,iBAAiB,OAAO,IAAI,QAAQ;AACtE,QAAM,SAAS,UAAU;AACzB,QAAM,EAAE,KAAK,SAAS,mBAAmB,IAAI,eAAe;AAI5D,QAAM,gBAAgB,IAAI,eAAe,KAAK,CAAC;AAC/C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAGlD,QAAM,EAAE,aAAa,IAAI,iBAAiB;AAE1C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,KAAK,MAAM,IAAI,SAKnB,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,IAAI,CAAC;AAClC,QAAM,SAAS,OAA0B,IAAI;AAC7C,QAAM,SAAS,OAAuB,IAAI;AAW1C,YAAU,MAAM;AACd,QAAI,QAAQ,KAAM,mBAAkB,OAAO,IAAI;AAAA,EACjD,GAAG,CAAC,QAAQ,IAAI,CAAC;AAIjB,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,OAAO,SAAS,sBAAsB;AAChD,UAAI;AACF,eAAO;AAAA,UACL,GAAI,OACA,EAAE,QAAQ,OAAO,cAAc,EAAE,MAAM,EAAE,IACzC,EAAE,KAAK,EAAE,SAAS,EAAE;AAAA,UACxB,MAAM,EAAE;AAAA,UACR,OAAO,KAAK,IAAI,EAAE,OAAO,GAAG;AAAA,QAC9B,CAAC;AAAA,IACL;AACA,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAEA,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,MAAkB;AAC/B,UACE,OAAO,SAAS,SAAS,EAAE,MAAc,KACxC,EAAE,QAA2B,UAAU,sBAAsB,KAC9D,OAAO,SAAS,SAAS,EAAE,MAAc;AAEzC;AACF,cAAQ,KAAK;AAAA,IACf;AACA,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,cAAQ,KAAK;AACb,aAAO,SAAS,MAAM;AAAA,IACxB;AACA,UAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,aAAS,iBAAiB,aAAa,KAAK;AAC5C,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,iBAAiB,UAAU,OAAO;AAWzC,WAAO,SAAS,MAAM;AACtB,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,KAAK;AAC/C,aAAO,oBAAoB,WAAW,KAAK;AAC3C,aAAO,oBAAoB,UAAU,OAAO;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,YAAY,CAAC,MAA2C;AAC5D,QAAI,EAAE,QAAQ,OAAO;AACnB,cAAQ,KAAK;AACb;AAAA,IACF;AACA,UAAM,OACJ,EAAE,QAAQ,cACN,IACA,EAAE,QAAQ,YACR,KACA,EAAE,QAAQ,UAAU,EAAE,QAAQ,QAC5B,IACA;AACV,QAAI,SAAS,KAAM;AACnB,MAAE,eAAe;AAKjB,UAAM,QAAQ,MAAM;AAAA,MAClB,OAAO,SAAS,iBAA8B,iBAAiB,KAAK,CAAC;AAAA,IACvE;AACA,QAAI,CAAC,MAAM,OAAQ;AACnB,QAAI,SAAS,GAAG;AACd,OAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,IAAI,MAAM,MAAM,SAAS,CAAC,GAAG,MAAM;AAC9D;AAAA,IACF;AACA,UAAM,KAAK,MAAM,QAAQ,SAAS,aAA4B;AAC9D,WAAO,KAAK,OAAO,MAAM,UAAU,MAAM,MAAM,EAAE,MAAM;AAAA,EACzD;AAEA,QAAM,OAAO,QAAQ,gBAAgB;AAGrC,QAAM,WAAW,CAAC,MAChB,CAAC,EAAE,SAAS,UAAU,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,CAAC,EAC/D,OAAO,OAAO,EACd,KAAK,QAAK;AAQf,QAAM,oBAAoB,QAAQ,MAAM;AACtC,UAAM,OAAO,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS,QAAQ,IAAI;AAClE,QAAI,KAAM,QAAO,SAAS,IAAI;AAC9B,WAAO,SAAS,sBAAsB,MAAM,IAAI;AAAA,EAClD,GAAG,CAAC,YAAY,MAAM,CAAC;AAEvB,QAAM,KAAK,CAAC,SAAiB;AAC3B,YAAQ,KAAK;AACb,WAAO,KAAK,IAAI;AAAA,EAClB;AAEA,QAAM,WAAW,OAAO,SAAiB;AACvC,YAAQ,KAAK;AACb,QAAI,SAAS,QAAQ,KAAM;AAG3B,UAAM,gBAAgB,IAAI;AAC1B,WAAO,QAAQ,YAAY;AAAA,EAC7B;AAQA,QAAM,SAAS;AAAA,IACb,MACE;AAAA,MACE,WAAW,OAAO,CAAC,MAAM,EAAE,OAAO,SAAS,QAAQ,IAAI;AAAA,MACvD,CAAC,MAAM,EAAE,OAAO;AAAA,IAClB;AAAA;AAAA;AAAA,IAGF,CAAC,YAAY,QAAQ,IAAI;AAAA,EAC3B;AAMA,QAAM,QAAQ,OAAO,MAAM,GAAG,iBAAiB;AAE/C,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,KAAK;AAAA,QACL,WACE,aACC,OAAO,kBAAkB,OACzB,OAAO,kBAAkB,OACzB,qBAAqB,YAAY,QAAQ,aAAa,OACtD,OAAO,UAAU;AAAA,QAEpB,iBAAc;AAAA,QACd,iBAAe;AAAA,QACf,SAAS;AAAA,QACT,OAAO;AAAA,QAEP;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,MAAM,QAAQ;AAAA,cACd,OAAO;AAAA;AAAA,UACT;AAAA,UACA,qBAAC,UAAK,WAAU,MACd;AAAA,gCAAC,OAAG,gBAAK;AAAA,YAMR,qBAAqB,YAAY,SAChC,oBAAC,UAAM,6BAAkB;AAAA,aAE7B;AAAA,UACA,qBAAC,UAAK,WAAU,MACd;AAAA,gCAAC,oBAAiB;AAAA,YACjB,QAAQ,OACP,oBAAC,eAAY,OAAO,IAAI,QAAQ,IAAI,IAEpC,oBAAC,UAAU,QAAV,EAAiB,OAAO,IAAI,QAAQ,IAAI;AAAA,aAE7C;AAAA;AAAA;AAAA,IACF;AAAA,IAYC,OAAO,aAAa,eACnB;AAAA,MACE,qBAAC,mBACE;AAAA,gBAAQ,oBAAC,mBAAkB,OAAQ;AAAA,QACnC,QACC;AAAA,UAAC,OAAO;AAAA,UAAP;AAAA,YAEC,KAAK;AAAA,YACL,WAAU;AAAA,YACV,UAAU;AAAA,YACV,MAAK;AAAA,YACL,cAAW;AAAA,YACX,WAAW;AAAA,YAQX,SAAS,EAAE,SAAS,GAAG,OAAO,MAAM,GAAG,OAAO,IAAI,GAAG;AAAA,YACrD,SAAS,EAAE,SAAS,GAAG,OAAO,GAAG,GAAG,EAAE;AAAA,YACtC,YAAY,QAAQ;AAAA,YAQpB,MAAM;AAAA,cACJ,SAAS;AAAA,cACT,OAAO;AAAA,cACP,GAAG,OAAO,IAAI;AAAA,cACd,YAAY,EAAE,UAAU,UAAU,MAAM,MAAM,MAAM,IAAI;AAAA,YAC1D;AAAA,YACA,OAAO;AAAA,cACL,KAAK,IAAI;AAAA,cACT,QAAQ,IAAI;AAAA,cACZ,MAAM,IAAI;AAAA,cACV,UAAU,IAAI;AAAA,cACd,iBAAiB,OAAO,gBAAgB;AAAA,YAC1C;AAAA,YA8BA;AAAA,mCAAC,SAAI,MAAK,SAAQ,cAAY,kBAAkB,IAAI,IACjD;AAAA,0BACC;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,MAAK;AAAA,oBACL,gBAAa;AAAA,oBACb,SAAS,MAAM,SAAS,OAAO,IAAI;AAAA,oBAEnC;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,WAAU;AAAA,0BACV,MAAM,OAAO;AAAA,0BACb,OAAO;AAAA;AAAA,sBACT;AAAA,sBACA,qBAAC,UAAK,WAAU,cACd;AAAA,4CAAC,OAAG,gBAAK;AAAA,wBAUR,qBACC,oBAAC,UAAK,WAAU,OAAO,6BAAkB;AAAA,yBAE7C;AAAA,sBACA,oBAAC,SAAM,WAAU,gBAAe,OAAO,IAAI,QAAQ,IAAI;AAAA;AAAA;AAAA,gBACzD;AAAA,gBAKD,iBACC;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,MAAK;AAAA,oBACL,SAAS,MAAM;AACb,8BAAQ,KAAK;AACb,kCAAY,IAAI;AAAA,oBAClB;AAAA,oBAEA;AAAA,0CAAC,YAAS,OAAO,IAAI,QAAQ,IAAI;AAAA,sBAAE;AAAA;AAAA;AAAA,gBAErC;AAAA,gBAEF;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,MAAM,QAAQ,KAAK;AAAA,oBAC7B,UAAU,MAAM,GAAG,iCAAiC;AAAA;AAAA,gBACtD;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,MAAK;AAAA,oBACL,SAAS,MAAM;AACb,8BAAQ,KAAK;AACb,mCAAa;AAAA,oBACf;AAAA,oBAEA;AAAA,0CAAC,YAAS,OAAO,IAAI,QAAQ,IAAI;AAAA,sBAAE;AAAA;AAAA;AAAA,gBAErC;AAAA,iBACF;AAAA,cAIA,oBAAC,SAAI,WAAU,cAAa;AAAA,cAE5B;AAAA,gBAAC;AAAA;AAAA,kBACC;AAAA,kBACA,OAAO,WAAW;AAAA,kBAClB;AAAA,kBACA;AAAA,kBACA,OAAO,MAAM;AACX,4BAAQ,KAAK;AACb,kCAAc,IAAI;AAAA,kBACpB;AAAA,kBACA,UAAU,MAAM,GAAG,uBAAuB;AAAA,kBAC1C,MAAM,QAAQ;AAAA,kBACd,WAAW,MAAM;AACf,4BAAQ,KAAK;AACb,iCAAa,6BAA6B;AAAA,kBAC5C;AAAA,kBACA,UAAU,MAAM;AACd,4BAAQ,KAAK;AACb,2BAAO;AAAA,kBACT;AAAA,kBACA,SAAS,MAAM,QAAQ,KAAK;AAAA;AAAA,cAC9B;AAAA;AAAA;AAAA,UA7JI;AAAA,QA8JN;AAAA,SAEJ;AAAA,MACA,SAAS;AAAA,IACX;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,SAAS,MAAM,cAAc,KAAK;AAAA;AAAA,IACpC;AAAA,IAEC,YACC,oBAAC,aAAU,MAAI,MAAC,eAAa,MAAC,SAAS,MAAM,YAAY,KAAK,GAAG;AAAA,KAErE;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/product-switcher.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { useRouter } from 'next/navigation';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { Check, ChevronDown, Settings, UserPlus } from 'lucide-react';\nimport { useSidebarCards } from './sidebar-cards';\nimport { useAuth, type Workspace } from '@withpotter/lib/auth-context';\nimport { WorkspaceMark } from './workspace-mark';\nimport { businessCategoryLabel } from '@withpotter/lib/business-categories';\nimport { byRecentlyUsed, noteWorkspaceUsed } from '@withpotter/lib/workspace-recents';\nimport { SwitcherScrim } from './switcher-scrim';\nimport { SwitcherWorkspaceList } from './switcher-workspace-list';\nimport { CreateBusinessModal } from '@withpotter/components/create-business-modal';\nimport { useSettingsModal } from './settings-modal-context';\nimport { ShellIcon } from './icons';\nimport { TeamModal } from '@withpotter/pages-core/dashboard/settings/team/_components/team-modal';\nimport {\n LocationScopePin,\n SwitcherLocationGroup,\n} from './switcher-location-group';\n\n/**\n * How many other workspaces the menu lists before deferring to the page.\n *\n * Two, and the two most recently opened — John, 2026-08-23. Three rows of\n * somewhere-else was the largest block in a menu whose subject is the\n * workspace you are in, and picking which two by recency is the only ordering\n * that gets better the more the menu is used. See `workspace-recents`.\n */\nconst INLINE_WORKSPACES = 2;\n\n/**\n * The business switcher: one trigger, one scope.\n *\n * Ordered by hierarchy, not by scope: the business you are in (a header block,\n * not a row), then *Switch to* the ones you could be in, then the person they\n * belong to (`switcher-account-group`). Out of this menu on purpose: the theme\n * flip (a preference, it rides the footer), Plan & billing, the team row and\n * business settings (footer rows now), and the plan label under the name. A\n * merchant presses this to change business, so every row between them and that\n * is in the way.\n */\nexport function ProductSwitcher({\n variant = 'top',\n}: {\n /**\n * `head` is the flat row at the top of the rail: mark and name only, with\n * search beside it (see `side-head` in shell.css).\n * `foot` is the same switcher drawn as a flat row at the bottom of the rail\n * (mark, name, chevron) whose menu opens UPWARD, because there is no room\n * under it. Same menu, same state, same keyboard walk.\n */\n variant?: 'top' | 'foot' | 'head';\n} = {}) {\n const foot = variant === 'foot';\n const head = variant === 'head';\n const { tenant, user, workspaces, switchWorkspace, logout } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const router = useRouter();\n const { can, loading: permissionsLoading } = usePermissions();\n // False while the membership request is in flight, so the row appears when\n // the answer arrives rather than flashing at somebody who turns out to be\n // staff.\n const canManageTeam = can('canManageTeam') && !permissionsLoading;\n const [open, setOpen] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n // The settings window is one shared modal (`SettingsModalProvider`), so closing\n // this menu cannot unmount it.\n const { openSettings } = useSettingsModal();\n // Mounted only while open: the modal reads the whole team on mount.\n const [teamOpen, setTeamOpen] = useState(false);\n const [pos, setPos] = useState<{\n top?: number;\n bottom?: number;\n left: number;\n width: number;\n }>({ top: 0, left: 0, width: 280 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n /**\n * Stamp the workspace you are in as the most recently used one.\n *\n * Here rather than in `persistActiveWorkspace`, which is the tidier choke\n * point and the wrong place to spend lines: auth-context is on the file-size\n * ledger and must shrink, not grow. This is equivalent — the shell renders\n * for every workspace that becomes active, whatever route made it active —\n * and it keeps the whole feature in the one component that reads it.\n */\n useEffect(() => {\n if (tenant?.slug) noteWorkspaceUsed(tenant.slug);\n }, [tenant?.slug]);\n\n // Measure at click time (not in an effect) so the portaled menu is\n // positioned before it paints.\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b)\n setPos({\n ...(foot\n ? { bottom: window.innerHeight - b.top + 6 }\n : { top: b.bottom + 6 }),\n left: b.left,\n width: Math.max(b.width, 280),\n });\n }\n setOpen((v) => !v);\n };\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\n (e.target as Element | null)?.closest?.('[data-pv-psw-flyout]') ||\n btnRef.current?.contains(e.target as Node)\n )\n return;\n setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n setOpen(false);\n btnRef.current?.focus();\n };\n const dismiss = () => setOpen(false);\n document.addEventListener('mousedown', onDoc);\n window.addEventListener('keydown', onKey);\n window.addEventListener('resize', dismiss);\n // This is the app's primary navigation surface, so it has to be usable\n // from the keyboard alone: opening lands on the first control and the\n // arrows walk the list.\n //\n // THE MENU, not its first row. There is nothing to type into any more, so\n // focusing the first control puts a ring on the workspace you are already\n // in the instant the menu opens, which reads as a selected row rather than\n // a landing place. The container takes focus instead and the arrow handler\n // below walks into the list from there — `indexOf` returns -1 for the\n // container, so ArrowDown lands on the first item.\n popRef.current?.focus();\n return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const onMenuKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Tab') {\n setOpen(false);\n return;\n }\n const step =\n e.key === 'ArrowDown'\n ? 1\n : e.key === 'ArrowUp'\n ? -1\n : e.key === 'Home' || e.key === 'End'\n ? 0\n : null;\n if (step === null) return;\n e.preventDefault();\n // `button, a[href]` — not `button`. Help & support is a real Link so it can\n // be cmd-clicked like any destination, and a selector of `button` alone\n // walked straight past it: the one row in this menu that leaves the app for\n // a page was the one row the arrows could not reach.\n const items = Array.from(\n popRef.current?.querySelectorAll<HTMLElement>('button, a[href]') ?? [],\n );\n if (!items.length) return;\n if (step === 0) {\n (e.key === 'Home' ? items[0] : items[items.length - 1]).focus();\n return;\n }\n const at = items.indexOf(document.activeElement as HTMLElement);\n items[(at + step + items.length) % items.length].focus();\n };\n\n const name = tenant?.businessName ?? 'Your business';\n\n /** Sub-line for a row: what kind of business it is, prefixed by a role worth naming. */\n const describe = (w: Workspace) =>\n [w.role !== 'owner' ? w.role : '', businessCategoryLabel(w.tenant)]\n .filter(Boolean)\n .join(' · ');\n\n /**\n * What the workspace you are in is, in the sub-line the list rows use.\n *\n * Off the workspace ENTRY where there is one, because that is what carries\n * your role here; `tenant` alone can only answer the category half.\n */\n const activeDescription = useMemo(() => {\n const mine = workspaces.find((w) => w.tenant.slug === tenant?.slug);\n if (mine) return describe(mine);\n return tenant ? businessCategoryLabel(tenant) : '';\n }, [workspaces, tenant]);\n\n const go = (href: string) => {\n setOpen(false);\n router.push(href);\n };\n\n const onSwitch = async (slug: string) => {\n setOpen(false);\n if (slug === tenant?.slug) return;\n // Soft swap: switchWorkspace swaps the tenant in context and every\n // useAuth() consumer re-renders. Refresh server segments only — no reload.\n await switchWorkspace(slug);\n router.replace('/dashboard');\n };\n\n /**\n * Everywhere but here. The workspace you are in leads the menu as the first\n * row of its own group, ticked and carrying its picture and its category —\n * so listing it again down here would be the same row twice, which is what\n * the tick had to distinguish it from.\n */\n const listed = useMemo(\n () =>\n byRecentlyUsed(\n workspaces.filter((w) => w.tenant.slug !== tenant?.slug),\n (w) => w.tenant.slug,\n ),\n // Re-read on every switch: the slug changing IS the event that reorders\n // this, and it is the only one that can happen while the menu is mounted.\n [workspaces, tenant?.slug],\n );\n\n // The first few, and a way to the rest. There is no filter here any more:\n // John, 2026-08-23, \"take out the search bar\". It only ever appeared above\n // three workspaces, it was the first thing focus landed on, and the page it\n // defers to can search properly.\n const shown = listed.slice(0, INLINE_WORKSPACES);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n className={\n 'pswitch' +\n (foot ? ' pswitch-foot' : '') +\n (head ? ' pswitch-head' : '') +\n (activeDescription && variant === 'top' ? ' has-sub' : '') +\n (open ? ' open' : '')\n }\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={toggle}\n title={name}\n >\n <WorkspaceMark\n className=\"wmark\"\n logo={tenant?.businessLogo}\n label={name}\n />\n <span className=\"pl\">\n <b>{name}</b>\n {/* What the business IS, where the plan label used to be (John,\n 2026-08-23) — a fact about the workspace rather than a billing\n status, which is what the most permanent line in the app should\n hold. Absent when nothing is stored, and the card drops a step\n rather than holding the space open. Set in Settings → General. */}\n {activeDescription && variant === 'top' && (\n <span>{activeDescription}</span>\n )}\n </span>\n <span className=\"sw\">\n <LocationScopePin />\n {foot || head ? (\n <ChevronDown width={15} height={15} />\n ) : (\n <ShellIcon.updown width={15} height={15} />\n )}\n </span>\n </button>\n\n {/* AnimatePresence INSIDE the portal, and the portal itself is only\n rendered once there is somewhere to put it. `{open && createPortal(…)}`\n ripped the menu out of the tree the moment it closed, so it arrived on\n a spring and vanished on a frame — the exact defect `FormDialog`\n documents for `{editing && <FormDialog/>}`. */}\n {/* `document` rather than a mounted flag: the portal's children are\n `open && …`, which is false on the first client render, so it puts\n nothing in the DOM and there is no markup for the server to disagree\n with. A state-in-effect flag would have been one render pass to learn\n what `typeof` already knows. */}\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n key=\"menu\"\n ref={popRef}\n className=\"pv-psw-menu\"\n tabIndex={-1}\n role=\"menu\"\n aria-label=\"Businesses and account\"\n onKeyDown={onMenuKey}\n /* THE MOTION SYSTEM, not a 140ms CSS keyframe (John, 2026-08-23).\n `springs.gentle` is the surface preset every dialog and sheet\n arrives on; this menu was one of the hand-rolled surfaces that\n never got it. It grows from its top-left, the corner nearest the\n trigger, so it reads as coming OUT of the control. No exit, for\n the reason `PageTransition` gives: a dismissed menu lingering\n delays whatever you pressed it to reach. */\n initial={{ opacity: 0, scale: 0.96, y: foot ? 4 : -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n /* IT LEAVES FASTER THAN IT ARRIVES. The note above is still true —\n a dismissed menu lingering delays whatever you pressed it to\n reach — but \"no delay\" and \"no motion\" are different things, and\n what shipped was the second. So the exit is a tween at\n `--motion-fast`, half the arrival and on the way OUT of the same\n corner: gone before you can want it gone, and still a movement\n rather than a cut. */\n exit={{\n opacity: 0,\n scale: 0.98,\n y: foot ? 2 : -2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n minWidth: pos.width,\n transformOrigin: foot ? 'bottom left' : 'top left',\n }}\n >\n {/* NO ROWS ABOVE THE LIST AT ALL NOW, and that is the end of\n a sequence rather than a fourth opinion. Workspace settings and\n the team went to the rail (John, 2026-09-03); the heading and\n the rule under this group went with them; and Plan & billing\n went last, once we had actually checked what it describes.\n\n WHY PLAN & BILLING WENT. John, 2026-09-03: *\"exactly no plans\n just credits.\"* There is one plan and nothing renews it; what a\n merchant can buy is credits. It is a settings page\n (/dashboard/settings/billing), reached through Workspace\n settings in the rail.\n\n SO THE MENU IS WHAT IT SAYS IT IS — the workspace you are in,\n then the ones you are not, then you.\n\n THE WORKSPACE ITSELF LEADS IT. John, 2026-08-23: *\"the active\n workspace should be on top the workspace settings.\"* It had\n been a row down in the list wearing a tick, which is where you\n look to CHANGE workspace, not to read which one you are in —\n and it left the rows above it acting on a business nothing on\n screen had named. As the group's first row it is both: the\n name, the picture and the kind of business, ticked, with what\n you can do to it directly beneath.\n\n It stays pressable rather than becoming a static block, because\n a row that looks like the rows under it should behave like\n them. `onSwitch` returns early on the slug you are already on,\n so the press is a no-op rather than a reload. */}\n <div role=\"group\" aria-label={`This business: ${name}`}>\n {tenant && (\n <button\n type=\"button\"\n className=\"pv-psw-item current\"\n role=\"menuitem\"\n aria-current=\"true\"\n onClick={() => onSwitch(tenant.slug)}\n >\n <WorkspaceMark\n className=\"pv-psw-ic\"\n logo={tenant.businessLogo}\n label={name}\n />\n <span className=\"pv-psw-txt\">\n <b>{name}</b>\n {/* The SAME sub-line the rows below use, and guarded the\n same way. It was `businessCategoryLabel` written out\n again, rendered unconditionally — so a workspace with\n no category stored (which is most of them until\n somebody picks one) got an empty span holding open a\n line, and the row you are in was the one row in the\n menu wearing a blank where its neighbours wore\n nothing. `describe` also carries the role, which the\n rows below show and this one silently dropped. */}\n {activeDescription && (\n <span className=\"cap\">{activeDescription}</span>\n )}\n </span>\n {planLabel && <span className=\"pv-psw-plan\">{planLabel}</span>}\n <Check className=\"pv-psw-check\" width={15} height={15} />\n </button>\n )}\n {/* What you can do to the business you are in, directly beneath\n it. Back in the menu on John's call, 2026-10-04 (it had\n moved to a rail row on 2026-09-03). */}\n {canManageTeam && (\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n setTeamOpen(true);\n }}\n >\n <UserPlus width={15} height={15} />\n Invite your team\n </button>\n )}\n <SwitcherLocationGroup\n onPicked={() => setOpen(false)}\n onManage={() => go('/dashboard/operations/locations')}\n />\n <button\n type=\"button\"\n className=\"pv-psw-row\"\n role=\"menuitem\"\n onClick={() => {\n setOpen(false);\n openSettings();\n }}\n >\n <Settings width={15} height={15} />\n Settings\n </button>\n </div>\n\n {/* Closes off the active business and its actions — only when\n there is something under it. */}\n <div className=\"pv-psw-sep\" />\n\n <SwitcherWorkspaceList\n shown={shown}\n total={workspaces.length}\n describe={describe}\n onSwitch={onSwitch}\n onAdd={() => {\n setOpen(false);\n setShowCreate(true);\n }}\n onManage={() => go('/dashboard/workspaces')}\n user={user ?? undefined}\n onAccount={() => {\n setOpen(false);\n openSettings('/dashboard/settings/profile');\n }}\n onLogout={() => {\n setOpen(false);\n logout();\n }}\n onClose={() => setOpen(false)}\n />\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n\n <CreateBusinessModal\n open={showCreate}\n onClose={() => setShowCreate(false)}\n />\n\n {teamOpen && (\n <TeamModal open initialInvite onClose={() => setTeamOpen(false)} />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2797,3 +2797,111 @@
2797
2797
  background-image: none;
2798
2798
  }
2799
2799
  }
2800
+
2801
+ /* The plan on the business you are in, only where the host names one. */
2802
+ .pv-psw-plan {
2803
+ flex: none;
2804
+ margin-left: auto;
2805
+ padding: 1px 8px;
2806
+ border-radius: 999px;
2807
+ border: 1px solid color-mix(in srgb, var(--accent-primary) 45%, transparent);
2808
+ background: color-mix(in srgb, var(--accent-primary) 22%, transparent);
2809
+ color: var(--text-primary);
2810
+ font-size: 11px;
2811
+ font-weight: 500;
2812
+ line-height: 16px;
2813
+ white-space: nowrap;
2814
+ }
2815
+
2816
+ /* Two cards stacked at the foot, opening on hover or focus. Numbers are
2817
+ Lovable's: 56px cards, 8px apart, a 0.2s ease-out. */
2818
+ .pv-shell .side-stack {
2819
+ position: relative;
2820
+ margin: 0 -8px;
2821
+ padding: 8px;
2822
+ }
2823
+ .pv-shell .side-stack-in {
2824
+ position: relative;
2825
+ height: 56px;
2826
+ transition: height 0.2s cubic-bezier(0, 0, 0.2, 1);
2827
+ }
2828
+ .pv-shell .side-stack-in:has(.side-stack-back) {
2829
+ height: 64px;
2830
+ }
2831
+ .pv-shell .side-stack:hover .side-stack-in:has(.side-stack-back),
2832
+ .pv-shell .side-stack:focus-within .side-stack-in:has(.side-stack-back) {
2833
+ height: 120px;
2834
+ }
2835
+ .pv-shell .side-stack .side-stack-card {
2836
+ position: absolute;
2837
+ inset-inline: 0;
2838
+ bottom: 0;
2839
+ height: 56px;
2840
+ margin: 0;
2841
+ display: flex;
2842
+ flex-direction: row;
2843
+ align-items: center;
2844
+ justify-content: space-between;
2845
+ gap: 10px;
2846
+ padding: 0 12px 0 14px;
2847
+ border-radius: 12px;
2848
+ border: 1px solid var(--border-default);
2849
+ background: var(--bg-elevated, var(--bg-muted));
2850
+ transform-origin: bottom;
2851
+ transition: transform 0.2s cubic-bezier(0, 0, 0.2, 1), background 0.12s;
2852
+ }
2853
+ .pv-shell .side-stack .side-stack-front {
2854
+ z-index: 2;
2855
+ text-align: left;
2856
+ }
2857
+ .pv-shell .side-stack-in:has(.side-stack-back) .side-stack-front {
2858
+ transform: translateY(-8px);
2859
+ }
2860
+ .pv-shell .side-stack .side-stack-back {
2861
+ z-index: 1;
2862
+ flex-direction: column;
2863
+ align-items: stretch;
2864
+ justify-content: center;
2865
+ gap: 7px;
2866
+ transform: translateY(-1px) scale(0.95);
2867
+ }
2868
+ .pv-shell .side-stack:hover .side-stack-front,
2869
+ .pv-shell .side-stack:focus-within .side-stack-front {
2870
+ transform: translateY(-64px);
2871
+ }
2872
+ .pv-shell .side-stack:hover .side-stack-back,
2873
+ .pv-shell .side-stack:focus-within .side-stack-back {
2874
+ transform: none;
2875
+ }
2876
+ .pv-shell .side-stack-text {
2877
+ display: flex;
2878
+ flex-direction: column;
2879
+ min-width: 0;
2880
+ }
2881
+ .pv-shell .side-stack-text b {
2882
+ font-size: 13px;
2883
+ font-weight: 500;
2884
+ line-height: 18px;
2885
+ color: var(--text-primary);
2886
+ }
2887
+ .pv-shell .side-stack-text span {
2888
+ font-size: 12px;
2889
+ line-height: 16px;
2890
+ color: var(--text-tertiary);
2891
+ }
2892
+ .pv-shell .side-stack-glyph {
2893
+ display: grid;
2894
+ place-items: center;
2895
+ flex: none;
2896
+ width: 32px;
2897
+ height: 32px;
2898
+ border-radius: 50%;
2899
+ background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
2900
+ color: var(--accent-primary);
2901
+ }
2902
+ @media (prefers-reduced-motion: reduce) {
2903
+ .pv-shell .side-stack-in,
2904
+ .pv-shell .side-stack .side-stack-card {
2905
+ transition: none;
2906
+ }
2907
+ }
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * What a host adds to the rail's foot and the business menu for its plan.
4
+ *
5
+ * Nothing here is on by default: a host that never wraps the dashboard in
6
+ * `SidebarCardsProvider` gets the rail it had. Olive does, because its plans
7
+ * come from its own engine and the pricing switches this package reads
8
+ * (`menuUpgrade`) are not what turns them on there.
9
+ */
10
+ export interface SidebarCardsConfig {
11
+ /** The front card: where to go to change plan. Leave out on the top plan. */
12
+ upgrade?: {
13
+ title: string;
14
+ hint: string;
15
+ href: string;
16
+ };
17
+ /** The plan, as a tag on the current business in the business menu. */
18
+ planLabel?: string;
19
+ }
20
+ export declare function SidebarCardsProvider({ value, children, }: {
21
+ value: SidebarCardsConfig | null;
22
+ children: ReactNode;
23
+ }): import("react").JSX.Element;
24
+ export declare function useSidebarCards(): SidebarCardsConfig | null;
25
+ /**
26
+ * Two standing cards stacked at the foot of the rail, the way Lovable does it.
27
+ * At rest the Upgrade card sits in front with the next card's edge showing
28
+ * beneath it; on hover or focus the stack opens and both are readable. The back
29
+ * card is the setup progress, and it is simply absent once setup is done, which
30
+ * leaves the Upgrade card alone.
31
+ */
32
+ export declare function SidebarCardStack(): import("react").JSX.Element | null;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import { createContext, useContext } from "react";
5
+ import { Zap } from "lucide-react";
6
+ import { SetupProgress } from "../sidebar/setup-progress";
7
+ const Context = createContext(null);
8
+ function SidebarCardsProvider({
9
+ value,
10
+ children
11
+ }) {
12
+ return /* @__PURE__ */ jsx(Context.Provider, { value, children });
13
+ }
14
+ function useSidebarCards() {
15
+ return useContext(Context);
16
+ }
17
+ function SidebarCardStack() {
18
+ const config = useSidebarCards();
19
+ if (!config?.upgrade) return null;
20
+ const { title, hint, href } = config.upgrade;
21
+ return /* @__PURE__ */ jsx("div", { className: "side-stack", children: /* @__PURE__ */ jsxs("div", { className: "side-stack-in", children: [
22
+ /* @__PURE__ */ jsxs(Link, { href, className: "side-stack-card side-stack-front", children: [
23
+ /* @__PURE__ */ jsxs("span", { className: "side-stack-text", children: [
24
+ /* @__PURE__ */ jsx("b", { children: title }),
25
+ /* @__PURE__ */ jsx("span", { children: hint })
26
+ ] }),
27
+ /* @__PURE__ */ jsx("span", { className: "side-stack-glyph", "aria-hidden": true, children: /* @__PURE__ */ jsx(Zap, { width: 15, height: 15 }) })
28
+ ] }),
29
+ /* @__PURE__ */ jsx(SetupProgress, { stacked: true })
30
+ ] }) });
31
+ }
32
+ export {
33
+ SidebarCardStack,
34
+ SidebarCardsProvider,
35
+ useSidebarCards
36
+ };
37
+ //# sourceMappingURL=sidebar-cards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/sidebar-cards.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { createContext, useContext, type ReactNode } from 'react';\nimport { Zap } from 'lucide-react';\nimport { SetupProgress } from '../sidebar/setup-progress';\n\n/**\n * What a host adds to the rail's foot and the business menu for its plan.\n *\n * Nothing here is on by default: a host that never wraps the dashboard in\n * `SidebarCardsProvider` gets the rail it had. Olive does, because its plans\n * come from its own engine and the pricing switches this package reads\n * (`menuUpgrade`) are not what turns them on there.\n */\nexport interface SidebarCardsConfig {\n /** The front card: where to go to change plan. Leave out on the top plan. */\n upgrade?: { title: string; hint: string; href: string };\n /** The plan, as a tag on the current business in the business menu. */\n planLabel?: string;\n}\n\nconst Context = createContext<SidebarCardsConfig | null>(null);\n\nexport function SidebarCardsProvider({\n value,\n children,\n}: {\n value: SidebarCardsConfig | null;\n children: ReactNode;\n}) {\n return <Context.Provider value={value}>{children}</Context.Provider>;\n}\n\nexport function useSidebarCards(): SidebarCardsConfig | null {\n return useContext(Context);\n}\n\n/**\n * Two standing cards stacked at the foot of the rail, the way Lovable does it.\n * At rest the Upgrade card sits in front with the next card's edge showing\n * beneath it; on hover or focus the stack opens and both are readable. The back\n * card is the setup progress, and it is simply absent once setup is done, which\n * leaves the Upgrade card alone.\n */\nexport function SidebarCardStack() {\n const config = useSidebarCards();\n\n if (!config?.upgrade) return null;\n\n const { title, hint, href } = config.upgrade;\n\n return (\n <div className=\"side-stack\">\n <div className=\"side-stack-in\">\n <Link href={href} className=\"side-stack-card side-stack-front\">\n <span className=\"side-stack-text\">\n <b>{title}</b>\n <span>{hint}</span>\n </span>\n <span className=\"side-stack-glyph\" aria-hidden>\n <Zap width={15} height={15} />\n </span>\n </Link>\n <SetupProgress stacked />\n </div>\n </div>\n );\n}\n"],"mappings":";AA+BS,cAyBC,YAzBD;AA7BT,OAAO,UAAU;AACjB,SAAS,eAAe,kBAAkC;AAC1D,SAAS,WAAW;AACpB,SAAS,qBAAqB;AAiB9B,MAAM,UAAU,cAAyC,IAAI;AAEtD,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,QAAQ,UAAR,EAAiB,OAAe,UAAS;AACnD;AAEO,SAAS,kBAA6C;AAC3D,SAAO,WAAW,OAAO;AAC3B;AASO,SAAS,mBAAmB;AACjC,QAAM,SAAS,gBAAgB;AAE/B,MAAI,CAAC,QAAQ,QAAS,QAAO;AAE7B,QAAM,EAAE,OAAO,MAAM,KAAK,IAAI,OAAO;AAErC,SACE,oBAAC,SAAI,WAAU,cACb,+BAAC,SAAI,WAAU,iBACb;AAAA,yBAAC,QAAK,MAAY,WAAU,oCAC1B;AAAA,2BAAC,UAAK,WAAU,mBACd;AAAA,4BAAC,OAAG,iBAAM;AAAA,QACV,oBAAC,UAAM,gBAAK;AAAA,SACd;AAAA,MACA,oBAAC,UAAK,WAAU,oBAAmB,eAAW,MAC5C,8BAAC,OAAI,OAAO,IAAI,QAAQ,IAAI,GAC9B;AAAA,OACF;AAAA,IACA,oBAAC,iBAAc,SAAO,MAAC;AAAA,KACzB,GACF;AAEJ;","names":[]}
@@ -2,11 +2,13 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { usePathname } from "@withpotter/lib/use-pathname";
4
4
  import { useAuth } from "@withpotter/lib/auth-context";
5
+ import { usePermissions } from "@withpotter/lib/use-permissions";
5
6
  import { NotificationsDropdown } from "../notifications-dropdown";
6
7
  import { SetupProgress } from "../sidebar/setup-progress";
7
8
  import { FootResourcesMenu } from "./foot-resources-menu";
8
9
  import { FootThemeMenu } from "./foot-theme-menu";
9
10
  import { PlanUpgradeLink } from "./plan-upgrade-link";
11
+ import { SidebarCardStack, useSidebarCards } from "./sidebar-cards";
10
12
  function SidebarFooter({
11
13
  collapsed = false
12
14
  }) {
@@ -14,10 +16,12 @@ function SidebarFooter({
14
16
  const { tenant } = useAuth();
15
17
  const onSettings = pathname?.startsWith("/dashboard/settings") ?? false;
16
18
  const iconBtn = "side-foot-link !w-7 !justify-center !gap-0 !px-0";
19
+ const { isOwner } = usePermissions();
20
+ const stacked = Boolean(useSidebarCards()?.upgrade) && isOwner && !collapsed && !onSettings;
17
21
  return /* @__PURE__ */ jsxs("div", { className: "side-foot-wrap", children: [
18
- !collapsed && !onSettings && /* @__PURE__ */ jsx(SetupProgress, {}),
22
+ stacked ? /* @__PURE__ */ jsx(SidebarCardStack, {}) : !collapsed && !onSettings && /* @__PURE__ */ jsx(SetupProgress, {}),
19
23
  /* @__PURE__ */ jsxs("div", { className: "side-foot", children: [
20
- /* @__PURE__ */ jsx(PlanUpgradeLink, { collapsed }),
24
+ !stacked && /* @__PURE__ */ jsx(PlanUpgradeLink, { collapsed }),
21
25
  /* @__PURE__ */ jsx("div", { className: "side-foot-switch", children: /* @__PURE__ */ jsxs("div", { className: "side-foot-icons", children: [
22
26
  /* @__PURE__ */ jsx(FootResourcesMenu, { className: "side-foot-link !w-auto" }),
23
27
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/sidebar-footer.tsx"],"sourcesContent":["'use client';\n\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { NotificationsDropdown } from '../notifications-dropdown';\nimport { SetupProgress } from '../sidebar/setup-progress';\nimport { FootResourcesMenu } from './foot-resources-menu';\nimport { FootThemeMenu } from './foot-theme-menu';\nimport { PlanUpgradeLink } from './plan-upgrade-link';\n\n/**\n * The foot of the sidebar: who's in this business, and the things that are\n * about YOU rather than about the page.\n *\n * One exception, and it leads: Take payment. It belongs to no page, which is\n * precisely why it cannot live on one — see `take-payment-button.tsx`.\n *\n * THREE THINGS LEFT OR MOVED, against Lovable, the reference for this rail:\n * workspace at the top, nav, then the foot. Lovable's bottom row is an avatar\n * on the left and an inbox on the right, and ours is not any more — the\n * account went into the switcher on 2026-09-03, so the last line is two\n * switches rather than an identity.\n *\n * Invite/Team → BACK, and it is one row now rather than a face stack you\n * could not act on. It went to the workspace switcher on the\n * argument that adding someone to a business is a workspace\n * act; scope turned\n * out to be the wrong axis to sort that menu on.\n * Help → BACK, on John's call 2026-08-23. It was moved to the home\n * on the argument that a rail row for a twice-a-year\n * destination is rail spent on the rare case. What that\n * missed is WHEN it is wanted: a merchant looking for help is\n * stuck on some screen deep in the app, and \"go back to the\n * home first\" is the one instruction a stuck person will not\n * follow. A rail row costs 30px on every screen and is on the\n * screen where the trouble actually is. The two escapes that\n * used to sit inside the ask intro — \"Report a bug\" and\n * \"Message the team\" — are gone with it; this is where a\n * person goes when something is wrong, and three doors to the\n * same room is worse than one that is always visible.\n * Notifications→ BACK. It was built against a live API and mounted nowhere,\n * so the whole read model had no face: notifications were\n * being written and nobody could see one. It now shares the\n * bottom line with the theme flip, the account having gone to\n * the switcher.\n *\n * TWO WORKSPACE-SCOPE ROWS SIT HERE — Workspace settings and Team, moved back\n * out of the switcher on John's call, 2026-09-03, and they are the LAST block\n * before the account line rather than the first; the rows argue their own\n * order where they are written. That is a deliberate hole in\n * the \"footer is person scope\" line above, and the line is the thing that was\n * wrong: a rail sorted by scope makes a merchant work out which scope a\n * destination belongs to before they can find it, which is the same complaint\n * the nav has. They are rows because they are destinations, and a destination\n * you visit weekly should not be behind a control whose job is switching\n * business. Both are gated — `canManageSettings` and, inside the row itself,\n * `canManageTeam` — so someone working a shift sees neither.\n *\n * Pinned with margin-top:auto and nothing stretched to meet it: the gap above\n * is the nav's breathing room, not a hole that wants filling.\n */\nexport function SidebarFooter({\n collapsed = false,\n}: {\n collapsed?: boolean;\n /** Still passed by the sidebar until the navigation PR drops the footer's\n * old edit toggle; the footer no longer draws one. */\n editing?: boolean;\n onToggleEdit?: () => void;\n}) {\n const pathname = usePathname();\n const { tenant } = useAuth();\n const onSettings = pathname?.startsWith('/dashboard/settings') ?? false;\n /* The same shape as Support (`side-foot-link`: 28px, 10px corners, the same\n hover and open fill), square so it holds only the icon. */\n const iconBtn = 'side-foot-link !w-7 !justify-center !gap-0 !px-0';\n\n return (\n <div className=\"side-foot-wrap\">\n {/* How far through setting up, ABOVE the foot's divider. Not on the\n collapsed rail or inside settings, where there is no room or no need. */}\n {!collapsed && !onSettings && <SetupProgress />}\n <div className=\"side-foot\">\n {/* NO CARDS IN THE RAIL. John, 2026-09-03: *\"this credits card and refer\n a business thingy, let's remove it from the sidebar footer... the\n refer a business can be on the homepage and the credits maybe.\"*\n\n Right, and the deck's own note had the reason without following it:\n the argument for Lovable's stacked pair is that a sidebar can carry\n two standing offers without spending two rows on them ALL DAY. It\n still spends them on every screen — a merchant reading an order has a\n credit meter and a referral pitch in the corner of their eye. The\n home is where a business looks up rather than down, and it already\n has a board built for exactly this: widgets a merchant can size,\n move, or throw away, which is the one thing a rail card can never\n offer.\n\n The deck's mechanic is not lost. `AnimatedHeight` came out of the\n create-business modal for it and stayed in `ui/`. */}\n\n {/* Search is at the head of the rail now (see `sidebar.tsx`). */}\n\n {/* NO HELP ROW. John, 2026-09-03: *\"let's remove the help and support\n from the sidebar footer and let's see how the footer looks like\"*.\n\n WHAT LEFT WITH IT, because this row was carrying more than its\n label by the end of that day: Give feedback had been merged INTO the\n help page an hour before (two rows for one errand), and\n `/dashboard/help` is the only door in the app to support cases — it\n is where \"Start a case\" and \"Your cases\" live. So removing one row\n took out the app's only desktop route to help, to feedback, and to a\n case a merchant had already opened.\n\n The consolation written here was wrong and worth recording: it said\n search covered part of it by answering from `HELP_TOPICS` inline. It\n does not. `sidebar-ask` and `use-command-search` between them search\n orders, customers, invoices, products and support cases, and no help\n topic — so the removal left zero doors, not a partial one. That is\n the shape of this whole class of bug: a row is deleted and the thing\n that was supposed to absorb it never did.\n\n IT NOW LIVES IN THE SWITCHER, which is where the note that used to\n be here said it should go — `switcher-account-group`, two rows under\n the person: Help & support, and Send feedback. The foot stays lean\n and the doors exist. */}\n\n {/* NO ASK FIELD IN THIS RAIL, at either end of it. John, 2026-08-25,\n twice in ten minutes: first *\"remove the ask anything on the side\n bar side put it on the footer sidebar\"*, then — with it sitting in\n the footer — *\"the ask anything search bar remove it form the\n sidebar\"*. Read together, the objection is not the slot, it is a\n text field in the rail at all: the sidebar is where you go\n somewhere, and a box you type sentences into is a surface you work\n in.\n\n So the app's doors to Box are the home's own bar, `/dashboard/\n ask`, and `PotterLauncher` on mobile. Desktop has none on the\n screens in between; that is the trade this makes, and the mount is\n one revert away in `components/shell/sidebar-ask.tsx` if it should\n come back somewhere that is not a rail. */}\n {/* THE LAST LINE: the help menu, the notifications bell and the theme\n flip, at the left. The account is in the workspace menu at the top\n now (John, 2026-10-11). The bell is the one the note above says came\n BACK: it had been left out of this line, so notifications were\n written and still nobody could see one. */}\n {/* Upgrade, only when the admin switches it on (Pricing › Display). */}\n <PlanUpgradeLink collapsed={collapsed} />\n <div className=\"side-foot-switch\">\n <div className=\"side-foot-icons\">\n <FootResourcesMenu className=\"side-foot-link !w-auto\" />\n <span\n aria-hidden=\"true\"\n className=\"mx-1 h-4 w-px shrink-0 bg-[var(--border-subtle)]\"\n />\n <NotificationsDropdown\n tenantSlug={tenant?.slug}\n className={iconBtn}\n />\n <FootThemeMenu className={iconBtn} />\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";AAiFoC,cAmE1B,YAnE0B;AA/EpC,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AACxB,SAAS,6BAA6B;AACtC,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAqDzB,SAAS,cAAc;AAAA,EAC5B,YAAY;AACd,GAMG;AACD,QAAM,WAAW,YAAY;AAC7B,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,aAAa,UAAU,WAAW,qBAAqB,KAAK;AAGlE,QAAM,UAAU;AAEhB,SACE,qBAAC,SAAI,WAAU,kBAGZ;AAAA,KAAC,aAAa,CAAC,cAAc,oBAAC,iBAAc;AAAA,IAC7C,qBAAC,SAAI,WAAU,aAgEb;AAAA,0BAAC,mBAAgB,WAAsB;AAAA,MACvC,oBAAC,SAAI,WAAU,oBACb,+BAAC,SAAI,WAAU,mBACb;AAAA,4BAAC,qBAAkB,WAAU,0BAAyB;AAAA,QACtD;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA;AAAA,QACZ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,YAAY,QAAQ;AAAA,YACpB,WAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,iBAAc,WAAW,SAAS;AAAA,SACrC,GACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/sidebar-footer.tsx"],"sourcesContent":["'use client';\n\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { NotificationsDropdown } from '../notifications-dropdown';\nimport { SetupProgress } from '../sidebar/setup-progress';\nimport { FootResourcesMenu } from './foot-resources-menu';\nimport { FootThemeMenu } from './foot-theme-menu';\nimport { PlanUpgradeLink } from './plan-upgrade-link';\nimport { SidebarCardStack, useSidebarCards } from './sidebar-cards';\n\n/**\n * The foot of the sidebar: who's in this business, and the things that are\n * about YOU rather than about the page.\n *\n * One exception, and it leads: Take payment. It belongs to no page, which is\n * precisely why it cannot live on one — see `take-payment-button.tsx`.\n *\n * THREE THINGS LEFT OR MOVED, against Lovable, the reference for this rail:\n * workspace at the top, nav, then the foot. Lovable's bottom row is an avatar\n * on the left and an inbox on the right, and ours is not any more — the\n * account went into the switcher on 2026-09-03, so the last line is two\n * switches rather than an identity.\n *\n * Invite/Team → BACK, and it is one row now rather than a face stack you\n * could not act on. It went to the workspace switcher on the\n * argument that adding someone to a business is a workspace\n * act; scope turned\n * out to be the wrong axis to sort that menu on.\n * Help → BACK, on John's call 2026-08-23. It was moved to the home\n * on the argument that a rail row for a twice-a-year\n * destination is rail spent on the rare case. What that\n * missed is WHEN it is wanted: a merchant looking for help is\n * stuck on some screen deep in the app, and \"go back to the\n * home first\" is the one instruction a stuck person will not\n * follow. A rail row costs 30px on every screen and is on the\n * screen where the trouble actually is. The two escapes that\n * used to sit inside the ask intro — \"Report a bug\" and\n * \"Message the team\" — are gone with it; this is where a\n * person goes when something is wrong, and three doors to the\n * same room is worse than one that is always visible.\n * Notifications→ BACK. It was built against a live API and mounted nowhere,\n * so the whole read model had no face: notifications were\n * being written and nobody could see one. It now shares the\n * bottom line with the theme flip, the account having gone to\n * the switcher.\n *\n * TWO WORKSPACE-SCOPE ROWS SIT HERE — Workspace settings and Team, moved back\n * out of the switcher on John's call, 2026-09-03, and they are the LAST block\n * before the account line rather than the first; the rows argue their own\n * order where they are written. That is a deliberate hole in\n * the \"footer is person scope\" line above, and the line is the thing that was\n * wrong: a rail sorted by scope makes a merchant work out which scope a\n * destination belongs to before they can find it, which is the same complaint\n * the nav has. They are rows because they are destinations, and a destination\n * you visit weekly should not be behind a control whose job is switching\n * business. Both are gated — `canManageSettings` and, inside the row itself,\n * `canManageTeam` — so someone working a shift sees neither.\n *\n * Pinned with margin-top:auto and nothing stretched to meet it: the gap above\n * is the nav's breathing room, not a hole that wants filling.\n */\nexport function SidebarFooter({\n collapsed = false,\n}: {\n collapsed?: boolean;\n /** Still passed by the sidebar until the navigation PR drops the footer's\n * old edit toggle; the footer no longer draws one. */\n editing?: boolean;\n onToggleEdit?: () => void;\n}) {\n const pathname = usePathname();\n const { tenant } = useAuth();\n const onSettings = pathname?.startsWith('/dashboard/settings') ?? false;\n /* The same shape as Support (`side-foot-link`: 28px, 10px corners, the same\n hover and open fill), square so it holds only the icon. */\n const iconBtn = 'side-foot-link !w-7 !justify-center !gap-0 !px-0';\n /* A host that supplies plan cards gets the stack in place of the setup bar and\n the Upgrade row; every other host keeps both exactly as they were. */\n const { isOwner } = usePermissions();\n const stacked = Boolean(useSidebarCards()?.upgrade) && isOwner && !collapsed && !onSettings;\n\n return (\n <div className=\"side-foot-wrap\">\n {/* How far through setting up, ABOVE the foot's divider. Not on the\n collapsed rail or inside settings, where there is no room or no need. */}\n {stacked ? <SidebarCardStack /> : !collapsed && !onSettings && <SetupProgress />}\n <div className=\"side-foot\">\n {/* NO CARDS IN THE RAIL. John, 2026-09-03: *\"this credits card and refer\n a business thingy, let's remove it from the sidebar footer... the\n refer a business can be on the homepage and the credits maybe.\"*\n\n Right, and the deck's own note had the reason without following it:\n the argument for Lovable's stacked pair is that a sidebar can carry\n two standing offers without spending two rows on them ALL DAY. It\n still spends them on every screen — a merchant reading an order has a\n credit meter and a referral pitch in the corner of their eye. The\n home is where a business looks up rather than down, and it already\n has a board built for exactly this: widgets a merchant can size,\n move, or throw away, which is the one thing a rail card can never\n offer.\n\n The deck's mechanic is not lost. `AnimatedHeight` came out of the\n create-business modal for it and stayed in `ui/`. */}\n\n {/* Search is at the head of the rail now (see `sidebar.tsx`). */}\n\n {/* NO HELP ROW. John, 2026-09-03: *\"let's remove the help and support\n from the sidebar footer and let's see how the footer looks like\"*.\n\n WHAT LEFT WITH IT, because this row was carrying more than its\n label by the end of that day: Give feedback had been merged INTO the\n help page an hour before (two rows for one errand), and\n `/dashboard/help` is the only door in the app to support cases — it\n is where \"Start a case\" and \"Your cases\" live. So removing one row\n took out the app's only desktop route to help, to feedback, and to a\n case a merchant had already opened.\n\n The consolation written here was wrong and worth recording: it said\n search covered part of it by answering from `HELP_TOPICS` inline. It\n does not. `sidebar-ask` and `use-command-search` between them search\n orders, customers, invoices, products and support cases, and no help\n topic — so the removal left zero doors, not a partial one. That is\n the shape of this whole class of bug: a row is deleted and the thing\n that was supposed to absorb it never did.\n\n IT NOW LIVES IN THE SWITCHER, which is where the note that used to\n be here said it should go — `switcher-account-group`, two rows under\n the person: Help & support, and Send feedback. The foot stays lean\n and the doors exist. */}\n\n {/* NO ASK FIELD IN THIS RAIL, at either end of it. John, 2026-08-25,\n twice in ten minutes: first *\"remove the ask anything on the side\n bar side put it on the footer sidebar\"*, then — with it sitting in\n the footer — *\"the ask anything search bar remove it form the\n sidebar\"*. Read together, the objection is not the slot, it is a\n text field in the rail at all: the sidebar is where you go\n somewhere, and a box you type sentences into is a surface you work\n in.\n\n So the app's doors to Box are the home's own bar, `/dashboard/\n ask`, and `PotterLauncher` on mobile. Desktop has none on the\n screens in between; that is the trade this makes, and the mount is\n one revert away in `components/shell/sidebar-ask.tsx` if it should\n come back somewhere that is not a rail. */}\n {/* THE LAST LINE: the help menu, the notifications bell and the theme\n flip, at the left. The account is in the workspace menu at the top\n now (John, 2026-10-11). The bell is the one the note above says came\n BACK: it had been left out of this line, so notifications were\n written and still nobody could see one. */}\n {/* Upgrade, only when the admin switches it on (Pricing › Display). */}\n {!stacked && <PlanUpgradeLink collapsed={collapsed} />}\n <div className=\"side-foot-switch\">\n <div className=\"side-foot-icons\">\n <FootResourcesMenu className=\"side-foot-link !w-auto\" />\n <span\n aria-hidden=\"true\"\n className=\"mx-1 h-4 w-px shrink-0 bg-[var(--border-subtle)]\"\n />\n <NotificationsDropdown\n tenantSlug={tenant?.slug}\n className={iconBtn}\n />\n <FootThemeMenu className={iconBtn} />\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";AAuFiB,cAmEP,YAnEO;AArFjB,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB,uBAAuB;AAqD3C,SAAS,cAAc;AAAA,EAC5B,YAAY;AACd,GAMG;AACD,QAAM,WAAW,YAAY;AAC7B,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,aAAa,UAAU,WAAW,qBAAqB,KAAK;AAGlE,QAAM,UAAU;AAGhB,QAAM,EAAE,QAAQ,IAAI,eAAe;AACnC,QAAM,UAAU,QAAQ,gBAAgB,GAAG,OAAO,KAAK,WAAW,CAAC,aAAa,CAAC;AAEjF,SACE,qBAAC,SAAI,WAAU,kBAGZ;AAAA,cAAU,oBAAC,oBAAiB,IAAK,CAAC,aAAa,CAAC,cAAc,oBAAC,iBAAc;AAAA,IAC9E,qBAAC,SAAI,WAAU,aAgEZ;AAAA,OAAC,WAAW,oBAAC,mBAAgB,WAAsB;AAAA,MACpD,oBAAC,SAAI,WAAU,oBACb,+BAAC,SAAI,WAAU,mBACb;AAAA,4BAAC,qBAAkB,WAAU,0BAAyB;AAAA,QACtD;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA;AAAA,QACZ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,YAAY,QAAQ;AAAA,YACpB,WAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,iBAAc,WAAW,SAAS;AAAA,SACrC,GACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -23,4 +23,6 @@ export declare function useSetupProgress(): {
23
23
  done: number;
24
24
  total: number;
25
25
  } | null;
26
- export declare function SetupProgress(): import("react").JSX.Element | null;
26
+ export declare function SetupProgress({ stacked }?: {
27
+ stacked?: boolean;
28
+ }): import("react").JSX.Element | null;
@@ -301,7 +301,7 @@ function useSetupProgress() {
301
301
  const setup = useSetupData();
302
302
  return setup ? { done: setup.done, total: setup.total } : null;
303
303
  }
304
- function SetupProgress() {
304
+ function SetupProgress({ stacked = false } = {}) {
305
305
  const [open, setOpen] = useState(false);
306
306
  const setup = useSetupData();
307
307
  if (!setup) return null;
@@ -314,7 +314,7 @@ function SetupProgress() {
314
314
  onClick: () => setOpen(true),
315
315
  "aria-haspopup": "dialog",
316
316
  "aria-expanded": open,
317
- className: "side-setup",
317
+ className: stacked ? "side-setup side-stack-card side-stack-back" : "side-setup",
318
318
  "aria-label": `Setup ${pct}% complete. See what is left`,
319
319
  children: [
320
320
  /* @__PURE__ */ jsxs("span", { className: "side-setup-row", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sidebar/setup-progress.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useRef, useState } from 'react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { DeliveryStep, PayoutStep } from '@withpotter/components/dashboard/go-live-steps';\nimport { SetupContactDialog } from './setup-contact-dialog';\nimport { taxAndPolicySteps } from './setup-extra-steps';\nimport { SetupStepSections } from './setup-step-sections';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { onboardingApi } from '@withpotter/lib/api/onboarding';\nimport { tenantApi } from '@withpotter/lib/api/tenant';\nimport { WALLET_SETTLEMENT } from '@withpotter/lib/wallet-settlement';\nimport { isHostCheck } from '@withpotter/lib/go-live-gate';\nimport type {\n CoreReadinessKey,\n OnboardingProgress,\n OnboardingStep,\n ReadinessBlocker,\n StorefrontReadiness,\n} from '@withpotter/lib/api/types';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n} from '@withpotter/ui-kit/dialog';\nimport {\n SetupRing,\n isSettled,\n orderSteps,\n relabelStep,\n} from '@withpotter/components/dashboard/home/widgets/setup-steps';\n\n/** A row the checklist API does not send, in the shape its rows are drawn in. */\nfunction row(\n stepKey: string,\n label: string,\n done: boolean,\n link: string,\n): OnboardingStep {\n return {\n id: stepKey,\n stepKey,\n label,\n status: done ? 'completed' : 'pending',\n link,\n };\n}\n\n/** Checklist steps that already ask for what each go-live check asks for. */\nconst CHECKLIST_COVERS: Record<CoreReadinessKey, string[]> = {\n payable: ['payments'],\n sellable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n bookable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n schedulable: ['availability', 'schedule'],\n reachable: ['shipping'],\n // The two profile rows below ask for exactly this.\n contactable: ['profile_email', 'profile_phone'],\n};\n\n/** The checklist steps a check covers. A host check is covered by the step with its own key. */\nfunction coversOf(key: ReadinessBlocker['key']): string[] {\n return isHostCheck(key) ? [key] : CHECKLIST_COVERS[key];\n}\n\n/**\n * The address of a row that is finished in a dialog and never navigates.\n *\n * NOT a settings path. The shell opens the settings window on any click on a\n * link into `/dashboard/settings`, in the capture phase, before the row's own\n * handler can cancel it — so these rows opened their dialog AND the settings\n * window behind it. The press is always cancelled, so the address is only what\n * the row is drawn as.\n */\nconst INLINE_HREF = '/dashboard';\n\n/** Rows finished in a dialog over the checklist instead of on a page. */\nconst INLINE_STEPS: Record<string, 'email' | 'phone' | 'delivery' | 'payout'> =\n {\n profile_email: 'email',\n profile_phone: 'phone',\n live_reachable: 'delivery',\n shipping: 'delivery',\n // A bank account is finished here too, as it was in the old go-live gate.\n live_payable: 'payout',\n payments: 'payout',\n };\n\n/** Steps that put a logo and colours on the business, so a logo row is not\n * asked for twice. */\nconst BRANDING_KEYS = ['branding', 'customize_theme', 'add_logo'];\n\n/**\n * How far through setting up this business is, as a bar and a percentage in the\n * foot of the rail, on every screen. Pressing it opens the whole checklist.\n *\n * It reads the same checklist as the home's \"Finish setting up\" card (the same\n * query key and the same rows from `setup-steps`), and that card draws its ring\n * from the count made here, through `useSetupProgress`, so the two can never\n * show different numbers. In the window each step is a row that goes to where\n * it is done, and the window closes as you leave for it.\n *\n * It leaves when the checklist is done, as that card does, and draws nothing\n * while the answer is loading or when there is no checklist. The caller hides it\n * on the collapsed rail, which has no room for a sentence and a bar.\n */\nfunction useSetupData() {\n const { tenant, refreshTenant } = useAuth();\n const item = useEntityLabel('item');\n /** The step being finished in a dialog over this one, rather than on a page. */\n const progressQuery = useApiQuery<OnboardingProgress>(\n ['onboarding-progress'],\n () => onboardingApi.getProgress(tenant!.id),\n // A refusal is not asked again each time another reader of this mounts.\n { enabled: !!tenant?.id, retryOnMount: false },\n );\n const { data, refetch: refetchProgress } = progressQuery;\n\n const readinessQuery = useApiQuery<StorefrontReadiness>(\n ['dashboard-storefront-readiness'],\n (slug) => tenantApi.readiness(slug),\n { retryOnMount: false },\n );\n const { data: readinessRaw, refetch: refetchReadiness } = readinessQuery;\n\n // A wallet box has no payout account, so \"payable\" is neither a thing to\n // finish nor a thing that was finished. Dropped from both lists here, which\n // is what every row below is built from.\n // Memoised: effects further down watch `readiness`, and a fresh object every\n // render made them refetch on every render, which never ends.\n const readiness: StorefrontReadiness | undefined = useMemo(\n () =>\n readinessRaw && WALLET_SETTLEMENT\n ? {\n ...readinessRaw,\n blockers: (readinessRaw.blockers ?? []).filter(\n (b) => b.key !== 'payable',\n ),\n satisfied: (readinessRaw.satisfied ?? []).filter(\n (key) => key !== 'payable',\n ),\n }\n : readinessRaw,\n [readinessRaw],\n );\n\n // NO NUMBER UNTIL BOTH HAVE ANSWERED. The percentage is counted over three\n // lists: the profile rows, which are known at once, and the checklist and the\n // go-live checks, which each arrive on a request of their own. Counted before\n // those landed it was the profile rows alone, so the rail read 33%, then 43%\n // a moment later, for a business that had changed nothing.\n //\n // \"Answered\" includes a first refusal. A backend that does not serve one of\n // these routes will not serve it on the retry either, and holding the bar\n // back for the whole retry window would hide it for no better number.\n //\n // ONCE ANSWERED, ALWAYS ANSWERED. A refused query asks again when something\n // new starts reading it, and for that moment it is pending with no failures\n // counted, exactly as it was the first time. Deciding from that alone hid the\n // card, which unmounted its readers, which showed it again when the request\n // was refused, which mounted them, which asked again: thousands of requests\n // a minute against a backend that had already said no. `isFetched` does not\n // go back, and the ref keeps the card up through any later refetch.\n const firstAnswerPending =\n (!progressQuery.isFetched && progressQuery.failureCount === 0) ||\n (!readinessQuery.isFetched && readinessQuery.failureCount === 0);\n const answered = useRef(false);\n if (!firstAnswerPending) answered.current = true;\n const asking = !answered.current;\n\n // Two sources say what is finished: the onboarding checklist and the\n // readiness check that gates going live. They are read at different times and\n // can disagree, and the list should never show something as missing that the\n // check has already passed. A step the readiness check covers is done when\n // that check is. `contactable` is left out: it passes on an email OR a phone,\n // so it cannot say both are done.\n const satisfiedCovers = new Set(\n (readiness?.satisfied ?? [])\n .filter((key) => key !== 'contactable')\n .flatMap((key) => coversOf(key)),\n );\n const checklist = orderSteps(data?.checklist?.steps ?? []).map((s) => {\n const step = relabelStep(s, item.singular);\n return satisfiedCovers.has(step.stepKey) && !isSettled(step)\n ? { ...step, status: 'completed' as const }\n : step;\n });\n const checklistKeys = new Set(checklist.map((s) => s.stepKey));\n\n // The business's own details: how a customer reaches it, and how it looks.\n const profile = [\n row(\n 'profile_email',\n 'Add a contact email',\n !!tenant?.contactEmail,\n INLINE_HREF,\n ),\n row(\n 'profile_phone',\n 'Add a phone number',\n !!tenant?.contactPhone,\n INLINE_HREF,\n ),\n ...(BRANDING_KEYS.some((k) => checklistKeys.has(k))\n ? []\n : [\n row(\n 'profile_logo',\n 'Add your logo',\n !!tenant?.businessLogo,\n '/dashboard/settings#branding',\n ),\n ]),\n ...taxAndPolicySteps(tenant?.settings),\n ];\n\n // What still stops it opening, that the checklist above does not already say.\n const goLiveLabel: Record<CoreReadinessKey, string> = {\n sellable: `Add your first ${item.singular.toLowerCase()}`,\n bookable: `Add a ${item.singular.toLowerCase()} people can book`,\n schedulable: 'Set the hours you take bookings',\n payable: 'Add a bank account',\n reachable: 'Add a delivery area',\n contactable: 'Add an email or phone number',\n };\n const goLiveLink: Record<CoreReadinessKey, string> = {\n sellable: '/dashboard/catalog/new',\n bookable: '/dashboard/catalog/new',\n schedulable: '/dashboard/bookings?view=availability',\n payable: '/dashboard/payments/payouts',\n reachable: '/dashboard/business/shipping',\n contactable: '/dashboard/settings#workspace-email',\n };\n const live = (readiness?.blockers ?? [])\n .filter(\n (b) =>\n b.key !== 'contactable' &&\n !coversOf(b.key).some((k) => checklistKeys.has(k)),\n )\n .map((b) =>\n isHostCheck(b.key)\n ? row(`live_${b.key}`, b.title, false, b.dashboardPath)\n : row(`live_${b.key}`, goLiveLabel[b.key], false, goLiveLink[b.key]),\n );\n\n /**\n * THE ONES THAT STAND BETWEEN THE BUSINESS AND GOING LIVE, marked with a red\n * asterisk and listed first. A row is required when the server still reports\n * the go-live check it answers: a delivery area, a payout account, something\n * to sell, a way to be reached. Everything else is worth doing and does not\n * stop the site opening.\n */\n // What is already satisfied is a done row, not a missing one, so the list\n // shows what the business has finished as well as what is left.\n // A host check that passed has no label of its own here; its checklist step\n // (same key) already shows it done, through `satisfiedCovers` above.\n const liveDone = (readiness?.satisfied ?? [])\n .filter(\n (key): key is CoreReadinessKey =>\n key !== 'contactable' &&\n !isHostCheck(key) &&\n !CHECKLIST_COVERS[key].some((k) => checklistKeys.has(k)),\n )\n .map((key) => row(`live_${key}`, goLiveLabel[key], true, goLiveLink[key]));\n\n const outstanding = readiness?.blockers ?? [];\n const requiredKeys = new Set<string>([\n ...live.map((st) => st.stepKey),\n ...liveDone.map((st) => st.stepKey),\n ...outstanding.flatMap((b) => coversOf(b.key)),\n ...(outstanding.some((b) => b.key === 'contactable')\n ? ['profile_email', 'profile_phone']\n : []),\n ]);\n const isRequired = (st: OnboardingStep) => requiredKeys.has(st.stepKey);\n const rest = [...profile, ...checklist, ...live, ...liveDone];\n\n const sections = [\n { title: 'Needed to go live', steps: rest.filter(isRequired) },\n {\n title: 'Your business',\n steps: profile.filter((st) => !isRequired(st)),\n },\n {\n title: 'Setting up',\n steps: checklist.filter((st) => !isRequired(st)),\n },\n ].filter((sec) => sec.steps.length > 0);\n const all = sections.flatMap((sec) => sec.steps);\n if (asking || all.length === 0) return null;\n\n const done = all.filter(isSettled).length;\n const total = all.length;\n const next = all.find((st) => !isSettled(st));\n if (done >= total) return null;\n const pct = Math.round((done / total) * 100);\n\n return {\n tenant,\n refreshTenant,\n sections,\n all,\n done,\n total,\n next,\n pct,\n isRequired,\n refetchProgress,\n refetchReadiness,\n };\n}\n\n/**\n * The Complete your setup list, as a dialog anything can open: the sidebar's\n * progress pill, and Publish when something still stands in the way.\n *\n * Publish used to open its own, older dialog with a list of its own. Two lists\n * for one question drift, and the second never showed what had already been\n * done. There is one now. `mode=\"go-live\"` only changes the line under the\n * title, to how many of the needed items are left.\n */\nexport function SetupDialog({\n open,\n onOpenChange,\n mode = 'setup',\n onFixed,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n mode?: 'setup' | 'go-live';\n /** Re-run whatever opened this once something was set up from in here. */\n onFixed?: () => void | Promise<void>;\n}) {\n const [inline, setInline] = useState<\n 'email' | 'phone' | 'delivery' | 'payout' | null\n >(null);\n const setup = useSetupData();\n if (!setup) return null;\n const {\n tenant,\n refreshTenant,\n sections,\n done,\n total,\n next,\n pct,\n isRequired,\n all,\n refetchProgress,\n refetchReadiness,\n } = setup;\n const needed = all.filter((st) => isRequired(st) && !isSettled(st)).length;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-label=\"Complete your setup\"\n className=\"max-h-[85vh] max-w-[440px] overflow-y-auto p-6\"\n >\n <div className=\"flex items-center gap-4\">\n {/* The ring draws the count; this says it. */}\n <span role=\"img\" aria-label={`${done} of ${total} steps done`}>\n <SetupRing done={done} total={total} showCount={false} />\n </span>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-semibold text-[var(--text-primary)]\">\n Complete your setup\n </DialogTitle>\n <DialogDescription className=\"mt-0.5 text-md text-[var(--text-tertiary)]\">\n {mode === 'go-live' && needed > 0\n ? `${needed} left to go live`\n : `${done} of ${total} done · ${pct}%`}\n {next ? ` · Next: ${next.label}` : ''}\n </DialogDescription>\n </div>\n </div>\n\n <SetupStepSections\n sections={sections}\n isRequired={isRequired}\n onActivate={(step) => {\n // These are finished here, in a dialog over the list, so the\n // list is still there when they are done.\n const inlineKey = INLINE_STEPS[step.stepKey];\n if (inlineKey) {\n setInline(inlineKey);\n return true;\n }\n onOpenChange(false);\n }}\n />\n </DialogContent>\n </Dialog>\n\n <SetupContactDialog\n field={inline === 'email' || inline === 'phone' ? inline : null}\n tenantSlug={tenant?.slug ?? ''}\n onClose={() => setInline(null)}\n onSaved={async () => {\n await refreshTenant();\n await refetchReadiness();\n await onFixed?.();\n }}\n />\n <DeliveryStep\n open={inline === 'delivery'}\n onOpenChange={(v) => setInline(v ? 'delivery' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n <PayoutStep\n open={inline === 'payout'}\n onOpenChange={(v) => setInline(v ? 'payout' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n </>\n );\n}\n\n/**\n * The one count of how far through setup this business is, for a surface that\n * draws it without the list. Null while it is being worked out and once\n * everything is done.\n */\nexport function useSetupProgress(): { done: number; total: number } | null {\n const setup = useSetupData();\n return setup ? { done: setup.done, total: setup.total } : null;\n}\n\nexport function SetupProgress() {\n const [open, setOpen] = useState(false);\n const setup = useSetupData();\n if (!setup) return null;\n const { pct } = setup;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className=\"side-setup\"\n aria-label={`Setup ${pct}% complete. See what is left`}\n >\n <span className=\"side-setup-row\">\n <span>Complete your setup</span>\n <b>{pct}%</b>\n </span>\n <span\n className=\"side-setup-track\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n >\n <span className=\"side-setup-fill\" style={{ width: `${pct}%` }} />\n </span>\n </button>\n\n <SetupDialog open={open} onOpenChange={setOpen} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sidebar/setup-progress.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useRef, useState } from 'react';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { DeliveryStep, PayoutStep } from '@withpotter/components/dashboard/go-live-steps';\nimport { SetupContactDialog } from './setup-contact-dialog';\nimport { taxAndPolicySteps } from './setup-extra-steps';\nimport { SetupStepSections } from './setup-step-sections';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useEntityLabel } from '@withpotter/lib/intents/use-entity-label';\nimport { onboardingApi } from '@withpotter/lib/api/onboarding';\nimport { tenantApi } from '@withpotter/lib/api/tenant';\nimport { WALLET_SETTLEMENT } from '@withpotter/lib/wallet-settlement';\nimport { isHostCheck } from '@withpotter/lib/go-live-gate';\nimport type {\n CoreReadinessKey,\n OnboardingProgress,\n OnboardingStep,\n ReadinessBlocker,\n StorefrontReadiness,\n} from '@withpotter/lib/api/types';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n} from '@withpotter/ui-kit/dialog';\nimport {\n SetupRing,\n isSettled,\n orderSteps,\n relabelStep,\n} from '@withpotter/components/dashboard/home/widgets/setup-steps';\n\n/** A row the checklist API does not send, in the shape its rows are drawn in. */\nfunction row(\n stepKey: string,\n label: string,\n done: boolean,\n link: string,\n): OnboardingStep {\n return {\n id: stepKey,\n stepKey,\n label,\n status: done ? 'completed' : 'pending',\n link,\n };\n}\n\n/** Checklist steps that already ask for what each go-live check asks for. */\nconst CHECKLIST_COVERS: Record<CoreReadinessKey, string[]> = {\n payable: ['payments'],\n sellable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n bookable: [\n 'add_product',\n 'content_main',\n 'content_products',\n 'content_services',\n 'content_menu',\n 'content_packages',\n ],\n schedulable: ['availability', 'schedule'],\n reachable: ['shipping'],\n // The two profile rows below ask for exactly this.\n contactable: ['profile_email', 'profile_phone'],\n};\n\n/** The checklist steps a check covers. A host check is covered by the step with its own key. */\nfunction coversOf(key: ReadinessBlocker['key']): string[] {\n return isHostCheck(key) ? [key] : CHECKLIST_COVERS[key];\n}\n\n/**\n * The address of a row that is finished in a dialog and never navigates.\n *\n * NOT a settings path. The shell opens the settings window on any click on a\n * link into `/dashboard/settings`, in the capture phase, before the row's own\n * handler can cancel it — so these rows opened their dialog AND the settings\n * window behind it. The press is always cancelled, so the address is only what\n * the row is drawn as.\n */\nconst INLINE_HREF = '/dashboard';\n\n/** Rows finished in a dialog over the checklist instead of on a page. */\nconst INLINE_STEPS: Record<string, 'email' | 'phone' | 'delivery' | 'payout'> =\n {\n profile_email: 'email',\n profile_phone: 'phone',\n live_reachable: 'delivery',\n shipping: 'delivery',\n // A bank account is finished here too, as it was in the old go-live gate.\n live_payable: 'payout',\n payments: 'payout',\n };\n\n/** Steps that put a logo and colours on the business, so a logo row is not\n * asked for twice. */\nconst BRANDING_KEYS = ['branding', 'customize_theme', 'add_logo'];\n\n/**\n * How far through setting up this business is, as a bar and a percentage in the\n * foot of the rail, on every screen. Pressing it opens the whole checklist.\n *\n * It reads the same checklist as the home's \"Finish setting up\" card (the same\n * query key and the same rows from `setup-steps`), and that card draws its ring\n * from the count made here, through `useSetupProgress`, so the two can never\n * show different numbers. In the window each step is a row that goes to where\n * it is done, and the window closes as you leave for it.\n *\n * It leaves when the checklist is done, as that card does, and draws nothing\n * while the answer is loading or when there is no checklist. The caller hides it\n * on the collapsed rail, which has no room for a sentence and a bar.\n */\nfunction useSetupData() {\n const { tenant, refreshTenant } = useAuth();\n const item = useEntityLabel('item');\n /** The step being finished in a dialog over this one, rather than on a page. */\n const progressQuery = useApiQuery<OnboardingProgress>(\n ['onboarding-progress'],\n () => onboardingApi.getProgress(tenant!.id),\n // A refusal is not asked again each time another reader of this mounts.\n { enabled: !!tenant?.id, retryOnMount: false },\n );\n const { data, refetch: refetchProgress } = progressQuery;\n\n const readinessQuery = useApiQuery<StorefrontReadiness>(\n ['dashboard-storefront-readiness'],\n (slug) => tenantApi.readiness(slug),\n { retryOnMount: false },\n );\n const { data: readinessRaw, refetch: refetchReadiness } = readinessQuery;\n\n // A wallet box has no payout account, so \"payable\" is neither a thing to\n // finish nor a thing that was finished. Dropped from both lists here, which\n // is what every row below is built from.\n // Memoised: effects further down watch `readiness`, and a fresh object every\n // render made them refetch on every render, which never ends.\n const readiness: StorefrontReadiness | undefined = useMemo(\n () =>\n readinessRaw && WALLET_SETTLEMENT\n ? {\n ...readinessRaw,\n blockers: (readinessRaw.blockers ?? []).filter(\n (b) => b.key !== 'payable',\n ),\n satisfied: (readinessRaw.satisfied ?? []).filter(\n (key) => key !== 'payable',\n ),\n }\n : readinessRaw,\n [readinessRaw],\n );\n\n // NO NUMBER UNTIL BOTH HAVE ANSWERED. The percentage is counted over three\n // lists: the profile rows, which are known at once, and the checklist and the\n // go-live checks, which each arrive on a request of their own. Counted before\n // those landed it was the profile rows alone, so the rail read 33%, then 43%\n // a moment later, for a business that had changed nothing.\n //\n // \"Answered\" includes a first refusal. A backend that does not serve one of\n // these routes will not serve it on the retry either, and holding the bar\n // back for the whole retry window would hide it for no better number.\n //\n // ONCE ANSWERED, ALWAYS ANSWERED. A refused query asks again when something\n // new starts reading it, and for that moment it is pending with no failures\n // counted, exactly as it was the first time. Deciding from that alone hid the\n // card, which unmounted its readers, which showed it again when the request\n // was refused, which mounted them, which asked again: thousands of requests\n // a minute against a backend that had already said no. `isFetched` does not\n // go back, and the ref keeps the card up through any later refetch.\n const firstAnswerPending =\n (!progressQuery.isFetched && progressQuery.failureCount === 0) ||\n (!readinessQuery.isFetched && readinessQuery.failureCount === 0);\n const answered = useRef(false);\n if (!firstAnswerPending) answered.current = true;\n const asking = !answered.current;\n\n // Two sources say what is finished: the onboarding checklist and the\n // readiness check that gates going live. They are read at different times and\n // can disagree, and the list should never show something as missing that the\n // check has already passed. A step the readiness check covers is done when\n // that check is. `contactable` is left out: it passes on an email OR a phone,\n // so it cannot say both are done.\n const satisfiedCovers = new Set(\n (readiness?.satisfied ?? [])\n .filter((key) => key !== 'contactable')\n .flatMap((key) => coversOf(key)),\n );\n const checklist = orderSteps(data?.checklist?.steps ?? []).map((s) => {\n const step = relabelStep(s, item.singular);\n return satisfiedCovers.has(step.stepKey) && !isSettled(step)\n ? { ...step, status: 'completed' as const }\n : step;\n });\n const checklistKeys = new Set(checklist.map((s) => s.stepKey));\n\n // The business's own details: how a customer reaches it, and how it looks.\n const profile = [\n row(\n 'profile_email',\n 'Add a contact email',\n !!tenant?.contactEmail,\n INLINE_HREF,\n ),\n row(\n 'profile_phone',\n 'Add a phone number',\n !!tenant?.contactPhone,\n INLINE_HREF,\n ),\n ...(BRANDING_KEYS.some((k) => checklistKeys.has(k))\n ? []\n : [\n row(\n 'profile_logo',\n 'Add your logo',\n !!tenant?.businessLogo,\n '/dashboard/settings#branding',\n ),\n ]),\n ...taxAndPolicySteps(tenant?.settings),\n ];\n\n // What still stops it opening, that the checklist above does not already say.\n const goLiveLabel: Record<CoreReadinessKey, string> = {\n sellable: `Add your first ${item.singular.toLowerCase()}`,\n bookable: `Add a ${item.singular.toLowerCase()} people can book`,\n schedulable: 'Set the hours you take bookings',\n payable: 'Add a bank account',\n reachable: 'Add a delivery area',\n contactable: 'Add an email or phone number',\n };\n const goLiveLink: Record<CoreReadinessKey, string> = {\n sellable: '/dashboard/catalog/new',\n bookable: '/dashboard/catalog/new',\n schedulable: '/dashboard/bookings?view=availability',\n payable: '/dashboard/payments/payouts',\n reachable: '/dashboard/business/shipping',\n contactable: '/dashboard/settings#workspace-email',\n };\n const live = (readiness?.blockers ?? [])\n .filter(\n (b) =>\n b.key !== 'contactable' &&\n !coversOf(b.key).some((k) => checklistKeys.has(k)),\n )\n .map((b) =>\n isHostCheck(b.key)\n ? row(`live_${b.key}`, b.title, false, b.dashboardPath)\n : row(`live_${b.key}`, goLiveLabel[b.key], false, goLiveLink[b.key]),\n );\n\n /**\n * THE ONES THAT STAND BETWEEN THE BUSINESS AND GOING LIVE, marked with a red\n * asterisk and listed first. A row is required when the server still reports\n * the go-live check it answers: a delivery area, a payout account, something\n * to sell, a way to be reached. Everything else is worth doing and does not\n * stop the site opening.\n */\n // What is already satisfied is a done row, not a missing one, so the list\n // shows what the business has finished as well as what is left.\n // A host check that passed has no label of its own here; its checklist step\n // (same key) already shows it done, through `satisfiedCovers` above.\n const liveDone = (readiness?.satisfied ?? [])\n .filter(\n (key): key is CoreReadinessKey =>\n key !== 'contactable' &&\n !isHostCheck(key) &&\n !CHECKLIST_COVERS[key].some((k) => checklistKeys.has(k)),\n )\n .map((key) => row(`live_${key}`, goLiveLabel[key], true, goLiveLink[key]));\n\n const outstanding = readiness?.blockers ?? [];\n const requiredKeys = new Set<string>([\n ...live.map((st) => st.stepKey),\n ...liveDone.map((st) => st.stepKey),\n ...outstanding.flatMap((b) => coversOf(b.key)),\n ...(outstanding.some((b) => b.key === 'contactable')\n ? ['profile_email', 'profile_phone']\n : []),\n ]);\n const isRequired = (st: OnboardingStep) => requiredKeys.has(st.stepKey);\n const rest = [...profile, ...checklist, ...live, ...liveDone];\n\n const sections = [\n { title: 'Needed to go live', steps: rest.filter(isRequired) },\n {\n title: 'Your business',\n steps: profile.filter((st) => !isRequired(st)),\n },\n {\n title: 'Setting up',\n steps: checklist.filter((st) => !isRequired(st)),\n },\n ].filter((sec) => sec.steps.length > 0);\n const all = sections.flatMap((sec) => sec.steps);\n if (asking || all.length === 0) return null;\n\n const done = all.filter(isSettled).length;\n const total = all.length;\n const next = all.find((st) => !isSettled(st));\n if (done >= total) return null;\n const pct = Math.round((done / total) * 100);\n\n return {\n tenant,\n refreshTenant,\n sections,\n all,\n done,\n total,\n next,\n pct,\n isRequired,\n refetchProgress,\n refetchReadiness,\n };\n}\n\n/**\n * The Complete your setup list, as a dialog anything can open: the sidebar's\n * progress pill, and Publish when something still stands in the way.\n *\n * Publish used to open its own, older dialog with a list of its own. Two lists\n * for one question drift, and the second never showed what had already been\n * done. There is one now. `mode=\"go-live\"` only changes the line under the\n * title, to how many of the needed items are left.\n */\nexport function SetupDialog({\n open,\n onOpenChange,\n mode = 'setup',\n onFixed,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n mode?: 'setup' | 'go-live';\n /** Re-run whatever opened this once something was set up from in here. */\n onFixed?: () => void | Promise<void>;\n}) {\n const [inline, setInline] = useState<\n 'email' | 'phone' | 'delivery' | 'payout' | null\n >(null);\n const setup = useSetupData();\n if (!setup) return null;\n const {\n tenant,\n refreshTenant,\n sections,\n done,\n total,\n next,\n pct,\n isRequired,\n all,\n refetchProgress,\n refetchReadiness,\n } = setup;\n const needed = all.filter((st) => isRequired(st) && !isSettled(st)).length;\n\n return (\n <>\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n aria-label=\"Complete your setup\"\n className=\"max-h-[85vh] max-w-[440px] overflow-y-auto p-6\"\n >\n <div className=\"flex items-center gap-4\">\n {/* The ring draws the count; this says it. */}\n <span role=\"img\" aria-label={`${done} of ${total} steps done`}>\n <SetupRing done={done} total={total} showCount={false} />\n </span>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-semibold text-[var(--text-primary)]\">\n Complete your setup\n </DialogTitle>\n <DialogDescription className=\"mt-0.5 text-md text-[var(--text-tertiary)]\">\n {mode === 'go-live' && needed > 0\n ? `${needed} left to go live`\n : `${done} of ${total} done · ${pct}%`}\n {next ? ` · Next: ${next.label}` : ''}\n </DialogDescription>\n </div>\n </div>\n\n <SetupStepSections\n sections={sections}\n isRequired={isRequired}\n onActivate={(step) => {\n // These are finished here, in a dialog over the list, so the\n // list is still there when they are done.\n const inlineKey = INLINE_STEPS[step.stepKey];\n if (inlineKey) {\n setInline(inlineKey);\n return true;\n }\n onOpenChange(false);\n }}\n />\n </DialogContent>\n </Dialog>\n\n <SetupContactDialog\n field={inline === 'email' || inline === 'phone' ? inline : null}\n tenantSlug={tenant?.slug ?? ''}\n onClose={() => setInline(null)}\n onSaved={async () => {\n await refreshTenant();\n await refetchReadiness();\n await onFixed?.();\n }}\n />\n <DeliveryStep\n open={inline === 'delivery'}\n onOpenChange={(v) => setInline(v ? 'delivery' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n <PayoutStep\n open={inline === 'payout'}\n onOpenChange={(v) => setInline(v ? 'payout' : null)}\n tenantSlug={tenant?.slug ?? ''}\n onFixed={async () => {\n setInline(null);\n await Promise.all([refetchReadiness(), refetchProgress()]);\n await onFixed?.();\n }}\n />\n </>\n );\n}\n\n/**\n * The one count of how far through setup this business is, for a surface that\n * draws it without the list. Null while it is being worked out and once\n * everything is done.\n */\nexport function useSetupProgress(): { done: number; total: number } | null {\n const setup = useSetupData();\n return setup ? { done: setup.done, total: setup.total } : null;\n}\n\nexport function SetupProgress({ stacked = false }: { stacked?: boolean } = {}) {\n const [open, setOpen] = useState(false);\n const setup = useSetupData();\n if (!setup) return null;\n const { pct } = setup;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={stacked ? 'side-setup side-stack-card side-stack-back' : 'side-setup'}\n aria-label={`Setup ${pct}% complete. See what is left`}\n >\n <span className=\"side-setup-row\">\n <span>Complete your setup</span>\n <b>{pct}%</b>\n </span>\n <span\n className=\"side-setup-track\"\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n >\n <span className=\"side-setup-fill\" style={{ width: `${pct}%` }} />\n </span>\n </button>\n\n <SetupDialog open={open} onOpenChange={setOpen} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.97",
3
+ "version": "0.13.0-next.99",
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.97",
54
- "@withpotter/lib": "^0.13.0-next.97",
55
- "@withpotter/ui-kit": "^0.13.0-next.97",
53
+ "@withpotter/hooks": "^0.13.0-next.99",
54
+ "@withpotter/lib": "^0.13.0-next.99",
55
+ "@withpotter/ui-kit": "^0.13.0-next.99",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",