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

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.
@@ -24,15 +24,15 @@ export interface HostSettingsRow {
24
24
  export declare function SettingsHostProvider({ rows, generalRows, children, }: {
25
25
  rows: HostSettingsRow[];
26
26
  /**
27
- * Rows a host adds to the Business card of General, below the business's own details. Olive shows its CAC
28
- * certificate there. A host that passes nothing gets the card it had.
27
+ * Rows a host adds to General as their own Verification section, between the business's own details and the contact. Olive
28
+ * shows its CAC certificate there. A host that passes nothing gets General as it was: no Verification heading is drawn.
29
29
  */
30
30
  generalRows?: () => ReactNode;
31
31
  children: ReactNode;
32
32
  }): import("react").JSX.Element;
33
33
  /** What a host drew into a slot, or null when there is no host, or the host has nothing to show right now. */
34
34
  export declare function slotContent(render: (() => ReactNode) | null | undefined): ReactNode;
35
- /** The host's rows for the Business card of General, drawn by the card itself. */
35
+ /** The host's rows for the Verification section of General, drawn by General itself. */
36
36
  export declare function useGeneralHostRows(): ReactNode;
37
37
  export declare function useSettingsHostRows(): HostSettingsRow[];
38
38
  /** The host row that owns an address: the address is one of its paths or sits below one. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-host.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\n\n/**\n * A row a host adds to the settings window, with the screen behind it.\n *\n * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.\n * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.\n */\nexport interface HostSettingsRow {\n id: string;\n name: string;\n /** The line under the title when the row is open. */\n subtitle?: string;\n icon: LucideIcon;\n /** Addresses this row owns: a link to any of them opens the window on it. */\n paths: string[];\n /** What search also matches. */\n keywords: string[];\n /** The row it is placed after. Default: after General. */\n after?: string;\n /** The screen drawn in the window. */\n render: () => ReactNode;\n}\n\nconst Context = createContext<HostSettingsRow[]>([]);\nconst GeneralSlotContext = createContext<(() => ReactNode) | null>(null);\n\nexport function SettingsHostProvider({\n rows,\n generalRows,\n children,\n}: {\n rows: HostSettingsRow[];\n /**\n * Rows a host adds to the Business card of General, below the business's own details. Olive shows its CAC\n * certificate there. A host that passes nothing gets the card it had.\n */\n generalRows?: () => ReactNode;\n children: ReactNode;\n}) {\n return (\n <Context.Provider value={rows}>\n <GeneralSlotContext.Provider value={generalRows ?? null}>\n {children}\n </GeneralSlotContext.Provider>\n </Context.Provider>\n );\n}\n\n/** What a host drew into a slot, or null when there is no host, or the host has nothing to show right now. */\nexport function slotContent(render: (() => ReactNode) | null | undefined): ReactNode {\n if (!render) return null;\n\n const content = render();\n\n return content === undefined || content === false ? null : content;\n}\n\n/** The host's rows for the Business card of General, drawn by the card itself. */\nexport function useGeneralHostRows(): ReactNode {\n return slotContent(useContext(GeneralSlotContext));\n}\n\nexport function useSettingsHostRows(): HostSettingsRow[] {\n return useContext(Context);\n}\n\n/** The host row that owns an address: the address is one of its paths or sits below one. */\nexport function hostRowForHref(\n rows: HostSettingsRow[],\n href: string | null | undefined,\n): HostSettingsRow | undefined {\n const path = (href ?? '').split(/[?#]/)[0];\n\n return rows.find((row) =>\n row.paths.some((p) => path === p || path.startsWith(`${p}/`)),\n );\n}\n"],"mappings":";AA6CM;AA3CN,SAAS,eAAe,kBAAkC;AAyB1D,MAAM,UAAU,cAAiC,CAAC,CAAC;AACnD,MAAM,qBAAqB,cAAwC,IAAI;AAEhE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE,oBAAC,QAAQ,UAAR,EAAiB,OAAO,MACvB,8BAAC,mBAAmB,UAAnB,EAA4B,OAAO,eAAe,MAChD,UACH,GACF;AAEJ;AAGO,SAAS,YAAY,QAAyD;AACnF,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,UAAU,OAAO;AAEvB,SAAO,YAAY,UAAa,YAAY,QAAQ,OAAO;AAC7D;AAGO,SAAS,qBAAgC;AAC9C,SAAO,YAAY,WAAW,kBAAkB,CAAC;AACnD;AAEO,SAAS,sBAAyC;AACvD,SAAO,WAAW,OAAO;AAC3B;AAGO,SAAS,eACd,MACA,MAC6B;AAC7B,QAAM,QAAQ,QAAQ,IAAI,MAAM,MAAM,EAAE,CAAC;AAEzC,SAAO,KAAK;AAAA,IAAK,CAAC,QAChB,IAAI,MAAM,KAAK,CAAC,MAAM,SAAS,KAAK,KAAK,WAAW,GAAG,CAAC,GAAG,CAAC;AAAA,EAC9D;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-host.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\n\n/**\n * A row a host adds to the settings window, with the screen behind it.\n *\n * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.\n * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.\n */\nexport interface HostSettingsRow {\n id: string;\n name: string;\n /** The line under the title when the row is open. */\n subtitle?: string;\n icon: LucideIcon;\n /** Addresses this row owns: a link to any of them opens the window on it. */\n paths: string[];\n /** What search also matches. */\n keywords: string[];\n /** The row it is placed after. Default: after General. */\n after?: string;\n /** The screen drawn in the window. */\n render: () => ReactNode;\n}\n\nconst Context = createContext<HostSettingsRow[]>([]);\nconst GeneralSlotContext = createContext<(() => ReactNode) | null>(null);\n\nexport function SettingsHostProvider({\n rows,\n generalRows,\n children,\n}: {\n rows: HostSettingsRow[];\n /**\n * Rows a host adds to General as their own Verification section, between the business's own details and the contact. Olive\n * shows its CAC certificate there. A host that passes nothing gets General as it was: no Verification heading is drawn.\n */\n generalRows?: () => ReactNode;\n children: ReactNode;\n}) {\n return (\n <Context.Provider value={rows}>\n <GeneralSlotContext.Provider value={generalRows ?? null}>\n {children}\n </GeneralSlotContext.Provider>\n </Context.Provider>\n );\n}\n\n/** What a host drew into a slot, or null when there is no host, or the host has nothing to show right now. */\nexport function slotContent(render: (() => ReactNode) | null | undefined): ReactNode {\n if (!render) return null;\n\n const content = render();\n\n return content === undefined || content === false ? null : content;\n}\n\n/** The host's rows for the Verification section of General, drawn by General itself. */\nexport function useGeneralHostRows(): ReactNode {\n return slotContent(useContext(GeneralSlotContext));\n}\n\nexport function useSettingsHostRows(): HostSettingsRow[] {\n return useContext(Context);\n}\n\n/** The host row that owns an address: the address is one of its paths or sits below one. */\nexport function hostRowForHref(\n rows: HostSettingsRow[],\n href: string | null | undefined,\n): HostSettingsRow | undefined {\n const path = (href ?? '').split(/[?#]/)[0];\n\n return rows.find((row) =>\n row.paths.some((p) => path === p || path.startsWith(`${p}/`)),\n );\n}\n"],"mappings":";AA6CM;AA3CN,SAAS,eAAe,kBAAkC;AAyB1D,MAAM,UAAU,cAAiC,CAAC,CAAC;AACnD,MAAM,qBAAqB,cAAwC,IAAI;AAEhE,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE,oBAAC,QAAQ,UAAR,EAAiB,OAAO,MACvB,8BAAC,mBAAmB,UAAnB,EAA4B,OAAO,eAAe,MAChD,UACH,GACF;AAEJ;AAGO,SAAS,YAAY,QAAyD;AACnF,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,UAAU,OAAO;AAEvB,SAAO,YAAY,UAAa,YAAY,QAAQ,OAAO;AAC7D;AAGO,SAAS,qBAAgC;AAC9C,SAAO,YAAY,WAAW,kBAAkB,CAAC;AACnD;AAEO,SAAS,sBAAyC;AACvD,SAAO,WAAW,OAAO;AAC3B;AAGO,SAAS,eACd,MACA,MAC6B;AAC7B,QAAM,QAAQ,QAAQ,IAAI,MAAM,MAAM,EAAE,CAAC;AAEzC,SAAO,KAAK;AAAA,IAAK,CAAC,QAChB,IAAI,MAAM,KAAK,CAAC,MAAM,SAAS,KAAK,KAAK,WAAW,GAAG,CAAC,GAAG,CAAC;AAAA,EAC9D;AACF;","names":[]}
@@ -2909,7 +2909,12 @@
2909
2909
  /* ===================== Settings window (the modal) =====================
2910
2910
  * One named variant: the window's own palette, so its rail, rows and panels draw the same values everywhere
2911
2911
  * (the prototype's warm frame). Light takes the prototype's values; dark falls back to the theme tokens so the window
2912
- * still follows the theme. Panels inside the window read the same variables, with their own fallbacks outside it. */
2912
+ * still follows the theme. Panels inside the window read the same variables, with their own fallbacks outside it.
2913
+ *
2914
+ * "Dark" means the window itself is dark. On a desktop dashboard the root carries `.dark` for the sidebar while the
2915
+ * content card, and every dialog that opens over it, is light: the root then also says `data-shell-light` (see
2916
+ * lib/theme-context.tsx). So the dark fallback is for `.dark` WITHOUT that flag, or the light window would draw in
2917
+ * the theme's tokens (grey card, teal headings) instead of its own. */
2913
2918
  .st-modal {
2914
2919
  --st-rail: #f6f6f3;
2915
2920
  --st-rail-line: #ecece8;
@@ -2927,8 +2932,21 @@
2927
2932
  --st-pick-dot: #3f9c7a;
2928
2933
  --st-chip: #f1f1ee;
2929
2934
  --st-rule: #eeeeea;
2930
- }
2931
- .dark .st-modal {
2935
+ --st-ink-1: #141416;
2936
+ --st-btn: #ffffff;
2937
+ --st-btn-hover: #f6f6f4;
2938
+ --st-btn-ring: 0 0 0 1px rgba(0, 0, 29, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
2939
+ --st-lime: #bbda4c;
2940
+ --st-lime-hover: #c7e25f;
2941
+ --st-lime-ink: #2b3806;
2942
+ --st-frame: #ffffff;
2943
+ --st-frame-ring: 0 0 0 1px rgba(0, 0, 29, 0.1);
2944
+ --st-th: #fafaf8;
2945
+ --st-seg: #ececea;
2946
+ --st-seg-on-ring: 0 1px 2px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 29, 0.05);
2947
+ --st-icon: #3c6462;
2948
+ }
2949
+ .dark:not([data-shell-light]) .st-modal {
2932
2950
  --st-rail: var(--bg-muted);
2933
2951
  --st-rail-line: var(--border-subtle);
2934
2952
  --st-rail-rule: var(--border-subtle);
@@ -2945,4 +2963,25 @@
2945
2963
  --st-pick-dot: #3f9c7a;
2946
2964
  --st-chip: var(--bg-muted);
2947
2965
  --st-rule: var(--border-subtle);
2966
+ --st-ink-1: var(--text-primary);
2967
+ --st-btn: var(--bg-muted);
2968
+ --st-btn-hover: var(--interactive-hover);
2969
+ --st-btn-ring: var(--shadow-button);
2970
+ --st-lime: var(--brand-500, #bbda4c);
2971
+ --st-lime-hover: var(--brand-600, #c7e25f);
2972
+ --st-lime-ink: #ffffff;
2973
+ --st-frame: var(--bg-base);
2974
+ --st-frame-ring: 0 0 0 1px var(--border-subtle);
2975
+ --st-th: var(--bg-muted);
2976
+ --st-seg: var(--bg-muted);
2977
+ --st-seg-on-ring: var(--shadow-sm);
2978
+ --st-icon: var(--text-secondary);
2979
+ }
2980
+
2981
+ /* The muted text every panel draws its hints and captions in (`--text-tertiary`) is the theme's own value, and on the desktop
2982
+ * dashboard the theme's is a teal. The window's muted ink is the prototype's grey. Only when the window is light: in the dark
2983
+ * fallback `--st-ink-3` is itself `--text-tertiary`, and pointing one at the other would loop. */
2984
+ html:not(.dark) .st-modal,
2985
+ html[data-shell-light] .st-modal {
2986
+ --text-tertiary: var(--st-ink-3);
2948
2987
  }
@@ -184,36 +184,33 @@ function UnifiedSettingsModal({
184
184
  })
185
185
  ] }, group.key))
186
186
  ] }),
187
- /* @__PURE__ */ jsxs("div", { className: "mt-1.5 shrink-0 border-t border-[var(--st-rail-rule)] pt-2", children: [
188
- legalRow && /* @__PURE__ */ jsxs(
189
- "button",
190
- {
191
- type: "button",
192
- "data-row": true,
193
- "aria-current": legalRow.id === current?.id ? "page" : void 0,
194
- onClick: () => {
195
- setPicked(legalRow.id);
196
- setView(null);
197
- },
198
- className: `${ROW_CLASS} ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`,
199
- children: [
200
- /* @__PURE__ */ jsx(legalRow.icon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
201
- /* @__PURE__ */ jsx("span", { className: "truncate", children: legalRow.name })
202
- ]
203
- }
204
- ),
187
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex shrink-0 items-center gap-1 border-t border-[var(--st-rail-rule)] pt-2", children: [
205
188
  /* @__PURE__ */ jsxs(
206
189
  Link,
207
190
  {
208
191
  href: HELP_HREF,
209
192
  onClick: onClose,
210
- className: `${ROW_CLASS} ${ROW_IDLE}`,
193
+ className: `${ROW_CLASS} ${ROW_IDLE} min-w-0 flex-1`,
211
194
  children: [
212
195
  /* @__PURE__ */ jsx(HelpIcon, { className: "size-4 shrink-0", strokeWidth: 1.75 }),
213
196
  /* @__PURE__ */ jsx("span", { children: "Get help" }),
214
197
  /* @__PURE__ */ jsx(ArrowUpRight, { className: "ml-auto size-[13px] shrink-0" })
215
198
  ]
216
199
  }
200
+ ),
201
+ legalRow && /* @__PURE__ */ jsx(
202
+ "button",
203
+ {
204
+ type: "button",
205
+ "data-row": true,
206
+ "aria-current": legalRow.id === current?.id ? "page" : void 0,
207
+ onClick: () => {
208
+ setPicked(legalRow.id);
209
+ setView(null);
210
+ },
211
+ className: `flex h-[34px] shrink-0 items-center rounded-[9px] px-2.5 text-[13.5px] font-medium transition-colors ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`,
212
+ children: legalRow.name
213
+ }
217
214
  )
218
215
  ] })
219
216
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowUpRight, ChevronLeft, Search, X } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\nimport { Dialog, DialogClose, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\n/**\n * A row of the rail: 34px tall, 13.5px, an icon 16px wide at 1.75 stroke. The values are the settings prototype's; the\n * colours come from the window's own palette (`.st-modal` in shell.css), which falls back to the theme in dark.\n */\nconst ROW_CLASS =\n 'mb-px flex h-[34px] w-full items-center gap-[11px] rounded-[9px] px-2 text-left text-[13.5px] font-medium leading-5 transition-colors';\nconst ROW_IDLE =\n 'text-[var(--st-ink-2)] hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)]';\nconst ROW_ON =\n 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-[var(--st-row-on-ring)]';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right):\n * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n // Legal is not on the rail: it sits at the foot, beside Get help.\n const railRows = shown.filter((r) => r.id !== 'legal');\n const legalRow = shown.find((r) => r.id === 'legal');\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n showClose={false}\n className=\"st-modal flex h-[min(640px,calc(100dvh-2.5rem))] w-[calc(100%-2.5rem)] max-w-[920px] gap-0 overflow-hidden rounded-2xl border-0 p-0 text-[13.5px] shadow-[0_24px_60px_-20px_rgba(18,18,34,0.45),0_0_0_1px_rgba(0,0,0,0.04)]\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[248px] shrink-0 flex-col overflow-hidden border-r border-[var(--st-rail-line)] bg-[var(--st-rail)] px-3 pb-2.5 pt-4\"\n >\n {name && (\n <div className=\"flex items-center gap-2.5 px-1.5 pb-3.5 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"max-w-[150px] truncate text-[14px] font-semibold leading-[18px] text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-[12px] leading-4 text-[var(--st-ink-3)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\n )}\n <div className=\"mb-1.5 flex h-8 shrink-0 items-center gap-2 rounded-[10px] bg-[var(--bg-base)] px-2.5 text-[var(--st-ink-3)] shadow-[var(--st-search-ring)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-[15px] shrink-0\" strokeWidth={1.8} />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <div className=\"-mx-1 min-h-0 flex-1 overflow-y-auto px-1\">\n {shown.length === 0 && (\n <p className=\"px-2 py-2 text-[13px] text-[var(--st-ink-3)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(railRows).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-2 pb-1.5 pt-3.5 text-[12px] font-medium leading-5 text-[var(--st-ink-4)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0 opacity-[.55]\" />\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${selected ? ROW_ON : ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n </Fragment>\n ))}\n </div>\n <div className=\"mt-1.5 shrink-0 border-t border-[var(--st-rail-rule)] pt-2\">\n {legalRow && (\n <button\n type=\"button\"\n data-row\n aria-current={legalRow.id === current?.id ? 'page' : undefined}\n onClick={() => {\n setPicked(legalRow.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`}\n >\n <legalRow.icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{legalRow.name}</span>\n </button>\n )}\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <HelpIcon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span>Get help</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex flex-none items-start justify-between pb-4 pl-10 pr-6 pt-5\">\n <div className=\"min-w-0\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"-ml-0.5 mb-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-[var(--st-ink-2)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <ChevronLeft className=\"size-[15px]\" strokeWidth={2} />\n {current?.name ?? 'Settings'}\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-[18px] font-semibold leading-6 tracking-[-0.01em] text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n {!view && current?.subtitle && (\n <p className=\"mt-0.5 text-[13px] leading-[18px] text-[var(--st-ink-3)]\">\n {current.subtitle}\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-2.5\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n <DialogClose\n aria-label=\"Close\"\n className=\"grid size-8 place-items-center rounded-[9px] text-[var(--st-ink-2)] transition-colors hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]\"\n >\n <X className=\"size-[18px]\" strokeWidth={2} />\n </DialogClose>\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-6 pb-7 outline-none\">\n <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowUpRight, ChevronLeft, Search, X } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\nimport { Dialog, DialogClose, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\n/**\n * A row of the rail: 34px tall, 13.5px, an icon 16px wide at 1.75 stroke. The values are the settings prototype's; the\n * colours come from the window's own palette (`.st-modal` in shell.css), which falls back to the theme in dark.\n */\nconst ROW_CLASS =\n 'mb-px flex h-[34px] w-full items-center gap-[11px] rounded-[9px] px-2 text-left text-[13.5px] font-medium leading-5 transition-colors';\nconst ROW_IDLE =\n 'text-[var(--st-ink-2)] hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)]';\nconst ROW_ON =\n 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-[var(--st-row-on-ring)]';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right):\n * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n // Legal is not on the rail: it sits at the foot, beside Get help.\n const railRows = shown.filter((r) => r.id !== 'legal');\n const legalRow = shown.find((r) => r.id === 'legal');\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n showClose={false}\n className=\"st-modal flex h-[min(640px,calc(100dvh-2.5rem))] w-[calc(100%-2.5rem)] max-w-[920px] gap-0 overflow-hidden rounded-2xl border-0 p-0 text-[13.5px] shadow-[0_24px_60px_-20px_rgba(18,18,34,0.45),0_0_0_1px_rgba(0,0,0,0.04)]\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[248px] shrink-0 flex-col overflow-hidden border-r border-[var(--st-rail-line)] bg-[var(--st-rail)] px-3 pb-2.5 pt-4\"\n >\n {name && (\n <div className=\"flex items-center gap-2.5 px-1.5 pb-3.5 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"max-w-[150px] truncate text-[14px] font-semibold leading-[18px] text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-[12px] leading-4 text-[var(--st-ink-3)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\n )}\n <div className=\"mb-1.5 flex h-8 shrink-0 items-center gap-2 rounded-[10px] bg-[var(--bg-base)] px-2.5 text-[var(--st-ink-3)] shadow-[var(--st-search-ring)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-[15px] shrink-0\" strokeWidth={1.8} />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <div className=\"-mx-1 min-h-0 flex-1 overflow-y-auto px-1\">\n {shown.length === 0 && (\n <p className=\"px-2 py-2 text-[13px] text-[var(--st-ink-3)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(railRows).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-2 pb-1.5 pt-3.5 text-[12px] font-medium leading-5 text-[var(--st-ink-4)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0 opacity-[.55]\" />\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${selected ? ROW_ON : ROW_IDLE}`}\n >\n <Icon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n </Fragment>\n ))}\n </div>\n {/* The foot is one row, as in the prototype: Get help, with Legal beside it so it stays one press away without\n taking a row of its own (a second row pushed the last rail rows out of view). */}\n <div className=\"mt-1.5 flex shrink-0 items-center gap-1 border-t border-[var(--st-rail-rule)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} ${ROW_IDLE} min-w-0 flex-1`}\n >\n <HelpIcon className=\"size-4 shrink-0\" strokeWidth={1.75} />\n <span>Get help</span>\n <ArrowUpRight className=\"ml-auto size-[13px] shrink-0\" />\n </Link>\n {legalRow && (\n <button\n type=\"button\"\n data-row\n aria-current={legalRow.id === current?.id ? 'page' : undefined}\n onClick={() => {\n setPicked(legalRow.id);\n setView(null);\n }}\n className={`flex h-[34px] shrink-0 items-center rounded-[9px] px-2.5 text-[13.5px] font-medium transition-colors ${legalRow.id === current?.id ? ROW_ON : ROW_IDLE}`}\n >\n {legalRow.name}\n </button>\n )}\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex flex-none items-start justify-between pb-4 pl-10 pr-6 pt-5\">\n <div className=\"min-w-0\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"-ml-0.5 mb-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-[var(--st-ink-2)] transition-colors hover:text-[var(--text-primary)]\"\n >\n <ChevronLeft className=\"size-[15px]\" strokeWidth={2} />\n {current?.name ?? 'Settings'}\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-[18px] font-semibold leading-6 tracking-[-0.01em] text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n {!view && current?.subtitle && (\n <p className=\"mt-0.5 text-[13px] leading-[18px] text-[var(--st-ink-3)]\">\n {current.subtitle}\n </p>\n )}\n </div>\n <div className=\"flex items-center gap-2.5\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n <DialogClose\n aria-label=\"Close\"\n className=\"grid size-8 place-items-center rounded-[9px] text-[var(--st-ink-2)] transition-colors hover:bg-[var(--st-row-hover)] hover:text-[var(--text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]\"\n >\n <X className=\"size-[18px]\" strokeWidth={2} />\n </DialogClose>\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-6 pb-7 outline-none\">\n <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.110",
3
+ "version": "0.13.0-next.113",
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.110",
54
- "@withpotter/lib": "^0.13.0-next.110",
55
- "@withpotter/ui-kit": "^0.13.0-next.110",
53
+ "@withpotter/hooks": "^0.13.0-next.113",
54
+ "@withpotter/lib": "^0.13.0-next.113",
55
+ "@withpotter/ui-kit": "^0.13.0-next.113",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",