@withpotter/components 0.13.0-next.104 → 0.13.0-next.106

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.
@@ -11,6 +11,7 @@ import { useAuth } from "@withpotter/lib/auth-context";
11
11
  import { merchantNotificationsApi } from "@withpotter/lib/api/merchant-notifications";
12
12
  import { durations, eases, springs } from "@withpotter/lib/motion";
13
13
  import { SwitcherScrim } from "./shell/switcher-scrim";
14
+ import { useNotificationActions } from "./shell/notification-actions";
14
15
  import { NotificationRow } from "./shell/notification-row";
15
16
  import { BellMark, bellLabel } from "./shell/bell-mark";
16
17
  function timeAgo(iso) {
@@ -78,6 +79,14 @@ function NotificationsDropdown({
78
79
  }
79
80
  setOpen((v) => !v);
80
81
  };
82
+ const act = useNotificationActions();
83
+ const doAction = (intent, n) => {
84
+ setOpen(false);
85
+ if (!n.readAt && slug) {
86
+ void merchantNotificationsApi.markRead(slug, n.id).catch(() => null).then(refresh);
87
+ }
88
+ act?.(intent, n);
89
+ };
81
90
  const openItem = (n) => {
82
91
  setOpen(false);
83
92
  if (!n.readAt && slug) {
@@ -151,7 +160,8 @@ function NotificationsDropdown({
151
160
  {
152
161
  notification: n,
153
162
  when: timeAgo(n.createdAt),
154
- onOpen: () => openItem(n)
163
+ onOpen: () => openItem(n),
164
+ onAction: act ? doAction : void 0
155
165
  },
156
166
  n.id
157
167
  )) })
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notifications-dropdown.tsx"],"sourcesContent":["'use client';\n\n/**\n * The bell at the foot of the rail: everything that has happened, in one list.\n *\n * Drawn the way the Boxspace dashboard draws it (`shell/notifications-menu`\n * there). The bell always stays in view; when something is unread, a small dot\n * in the brand colour sits on its top-right corner, with a ring of the ground\n * colour so it reads as part of the icon. No count on the bell: it used to\n * carry a 16px pill in a 28px button, which covered the icon and shouted. The\n * number is in the button's label for screen readers, and the list says the\n * rest.\n *\n * The list opens upward from the bell in the same menu skin as Support\n * (`.pv-psw-menu`): a header with Mark all read, then one row per notification\n * (`shell/notification-row`). It reads the same query as the counts on the nav\n * rows (`use-nav-notifications`), so it is one request and the same data.\n * Opening an item marks it read and follows its link.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useRouter } from 'next/navigation';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useDemoMode } from '@withpotter/hooks/use-demo-mode';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { merchantNotificationsApi } from '@withpotter/lib/api/merchant-notifications';\nimport type {\n MerchantNotification,\n MerchantNotificationList,\n} from '@withpotter/lib/api/types';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { SwitcherScrim } from './shell/switcher-scrim';\nimport { NotificationRow } from './shell/notification-row';\nimport { BellMark, bellLabel } from './shell/bell-mark';\n\nfunction timeAgo(iso: string): string {\n const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n return new Date(iso).toLocaleDateString();\n}\n\nconst TRIGGER =\n 'p-2 rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--interactive-active)] transition-colors cursor-pointer';\n\nexport function NotificationsDropdown({\n tenantSlug,\n className = TRIGGER,\n}: {\n /** Defaults to the signed-in business. */\n tenantSlug?: string;\n /** The bell button's classes, so it can match the row it sits in. */\n className?: string;\n}) {\n const router = useRouter();\n const demo = useDemoMode();\n const { tenant } = useAuth();\n const queryClient = useQueryClient();\n const slug = tenantSlug ?? tenant?.slug;\n\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n const { data } = useApiQuery<MerchantNotificationList>(\n ['merchant-notifications'],\n (s) => merchantNotificationsApi.list(s, { limit: 50 }),\n { enabled: !demo, live: true },\n );\n const notifications = data?.notifications ?? [];\n const unread = notifications.filter((n) => !n.readAt).length;\n const unreadCount = Math.max(unread, data?.unreadCount ?? 0);\n\n const refresh = () =>\n queryClient.invalidateQueries({\n queryKey: ['tenant', tenant?.slug, 'merchant-notifications'],\n });\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\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 return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b) setPos({ bottom: window.innerHeight - b.top + 6, left: b.left });\n }\n setOpen((v) => !v);\n };\n\n const openItem = (n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n if (n.link) router.push(n.link);\n };\n\n const markAll = () => {\n if (!slug) return;\n void merchantNotificationsApi\n .markAllRead(slug)\n .catch(() => null)\n .then(refresh);\n };\n\n const label = bellLabel(unreadCount);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n data-tour=\"notifications\"\n className={`relative ${className}`}\n title={label}\n aria-label={label}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n onClick={toggle}\n >\n <BellMark unreadCount={unreadCount} />\n </button>\n\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n ref={popRef}\n className=\"pv-psw-menu\"\n role=\"dialog\"\n aria-label=\"Notifications\"\n initial={{ opacity: 0, scale: 0.96, y: 4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n exit={{\n opacity: 0,\n scale: 0.98,\n y: 2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n bottom: pos.bottom,\n left: pos.left,\n width: 280,\n maxWidth: 'calc(100vw - 24px)',\n transformOrigin: 'bottom left',\n }}\n >\n <div className=\"flex items-center justify-between px-3 py-2\">\n <span className=\"text-md font-semibold text-[var(--text-primary)]\">\n Notifications\n </span>\n {unread > 0 && (\n <button\n type=\"button\"\n onClick={markAll}\n className=\"text-xs font-medium text-[var(--accent-primary)] transition-opacity hover:opacity-80\"\n >\n Mark all read\n </button>\n )}\n </div>\n <div className=\"pv-psw-sep\" />\n <div className=\"max-h-96 overflow-y-auto\">\n {notifications.length === 0 ? (\n <p className=\"px-3 py-8 text-center text-sm text-[var(--text-muted)]\">\n Nothing yet. Replies from support, new orders and stock\n alerts show up here.\n </p>\n ) : (\n notifications.map((n) => (\n <NotificationRow\n key={n.id}\n notification={n}\n when={timeAgo(n.createdAt)}\n onOpen={() => openItem(n)}\n />\n ))\n )}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notifications-dropdown.tsx"],"sourcesContent":["'use client';\n\n/**\n * The bell at the foot of the rail: everything that has happened, in one list.\n *\n * Drawn the way the Boxspace dashboard draws it (`shell/notifications-menu`\n * there). The bell always stays in view; when something is unread, a small dot\n * in the brand colour sits on its top-right corner, with a ring of the ground\n * colour so it reads as part of the icon. No count on the bell: it used to\n * carry a 16px pill in a 28px button, which covered the icon and shouted. The\n * number is in the button's label for screen readers, and the list says the\n * rest.\n *\n * The list opens upward from the bell in the same menu skin as Support\n * (`.pv-psw-menu`): a header with Mark all read, then one row per notification\n * (`shell/notification-row`). It reads the same query as the counts on the nav\n * rows (`use-nav-notifications`), so it is one request and the same data.\n * Opening an item marks it read and follows its link.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useRouter } from 'next/navigation';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useApiQuery } from '@withpotter/hooks/use-api-query';\nimport { useDemoMode } from '@withpotter/hooks/use-demo-mode';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { merchantNotificationsApi } from '@withpotter/lib/api/merchant-notifications';\nimport type {\n MerchantNotification,\n MerchantNotificationList,\n} from '@withpotter/lib/api/types';\nimport { durations, eases, springs } from '@withpotter/lib/motion';\nimport { SwitcherScrim } from './shell/switcher-scrim';\nimport { useNotificationActions } from './shell/notification-actions';\nimport { NotificationRow } from './shell/notification-row';\nimport { BellMark, bellLabel } from './shell/bell-mark';\n\nfunction timeAgo(iso: string): string {\n const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n return new Date(iso).toLocaleDateString();\n}\n\nconst TRIGGER =\n 'p-2 rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--interactive-active)] transition-colors cursor-pointer';\n\nexport function NotificationsDropdown({\n tenantSlug,\n className = TRIGGER,\n}: {\n /** Defaults to the signed-in business. */\n tenantSlug?: string;\n /** The bell button's classes, so it can match the row it sits in. */\n className?: string;\n}) {\n const router = useRouter();\n const demo = useDemoMode();\n const { tenant } = useAuth();\n const queryClient = useQueryClient();\n const slug = tenantSlug ?? tenant?.slug;\n\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n const btnRef = useRef<HTMLButtonElement>(null);\n const popRef = useRef<HTMLDivElement>(null);\n\n const { data } = useApiQuery<MerchantNotificationList>(\n ['merchant-notifications'],\n (s) => merchantNotificationsApi.list(s, { limit: 50 }),\n { enabled: !demo, live: true },\n );\n const notifications = data?.notifications ?? [];\n const unread = notifications.filter((n) => !n.readAt).length;\n const unreadCount = Math.max(unread, data?.unreadCount ?? 0);\n\n const refresh = () =>\n queryClient.invalidateQueries({\n queryKey: ['tenant', tenant?.slug, 'merchant-notifications'],\n });\n\n useEffect(() => {\n if (!open) return;\n const onDoc = (e: MouseEvent) => {\n if (\n popRef.current?.contains(e.target as Node) ||\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 return () => {\n document.removeEventListener('mousedown', onDoc);\n window.removeEventListener('keydown', onKey);\n window.removeEventListener('resize', dismiss);\n };\n }, [open]);\n\n const toggle = () => {\n if (!open) {\n const b = btnRef.current?.getBoundingClientRect();\n if (b) setPos({ bottom: window.innerHeight - b.top + 6, left: b.left });\n }\n setOpen((v) => !v);\n };\n\n const act = useNotificationActions();\n\n // Pressing a pill does the step it names, and the news is read: closing the bell and marking it first, as opening it does.\n const doAction = (intent: string, n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n act?.(intent, n);\n };\n\n const openItem = (n: MerchantNotification) => {\n setOpen(false);\n if (!n.readAt && slug) {\n void merchantNotificationsApi\n .markRead(slug, n.id)\n .catch(() => null)\n .then(refresh);\n }\n if (n.link) router.push(n.link);\n };\n\n const markAll = () => {\n if (!slug) return;\n void merchantNotificationsApi\n .markAllRead(slug)\n .catch(() => null)\n .then(refresh);\n };\n\n const label = bellLabel(unreadCount);\n\n return (\n <>\n <button\n type=\"button\"\n ref={btnRef}\n data-tour=\"notifications\"\n className={`relative ${className}`}\n title={label}\n aria-label={label}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n onClick={toggle}\n >\n <BellMark unreadCount={unreadCount} />\n </button>\n\n {typeof document !== 'undefined' &&\n createPortal(\n <AnimatePresence>\n {open && <SwitcherScrim key=\"scrim\" />}\n {open && (\n <motion.div\n ref={popRef}\n className=\"pv-psw-menu\"\n role=\"dialog\"\n aria-label=\"Notifications\"\n initial={{ opacity: 0, scale: 0.96, y: 4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={springs.gentle}\n exit={{\n opacity: 0,\n scale: 0.98,\n y: 2,\n transition: { duration: durations.fast, ease: eases.out },\n }}\n style={{\n bottom: pos.bottom,\n left: pos.left,\n width: 280,\n maxWidth: 'calc(100vw - 24px)',\n transformOrigin: 'bottom left',\n }}\n >\n <div className=\"flex items-center justify-between px-3 py-2\">\n <span className=\"text-md font-semibold text-[var(--text-primary)]\">\n Notifications\n </span>\n {unread > 0 && (\n <button\n type=\"button\"\n onClick={markAll}\n className=\"text-xs font-medium text-[var(--accent-primary)] transition-opacity hover:opacity-80\"\n >\n Mark all read\n </button>\n )}\n </div>\n <div className=\"pv-psw-sep\" />\n <div className=\"max-h-96 overflow-y-auto\">\n {notifications.length === 0 ? (\n <p className=\"px-3 py-8 text-center text-sm text-[var(--text-muted)]\">\n Nothing yet. Replies from support, new orders and stock\n alerts show up here.\n </p>\n ) : (\n notifications.map((n) => (\n <NotificationRow\n key={n.id}\n notification={n}\n when={timeAgo(n.createdAt)}\n onOpen={() => openItem(n)}\n onAction={act ? doAction : undefined}\n />\n ))\n )}\n </div>\n </motion.div>\n )}\n </AnimatePresence>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { MerchantNotification } from '@withpotter/lib/api/types';
3
+ /**
4
+ * What a press on a bell row's pill does. The row only knows the intent ("pay", "keep", "resume"); the host knows what
5
+ * that means in its product, so it answers here. Without a provider the pill is not drawn at all.
6
+ */
7
+ export type NotificationActionHandler = (intent: string, notification: MerchantNotification) => void;
8
+ export declare function NotificationActionsProvider({ onAction, children, }: {
9
+ onAction: NotificationActionHandler | null;
10
+ children: ReactNode;
11
+ }): import("react").JSX.Element;
12
+ export declare function useNotificationActions(): NotificationActionHandler | null;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const Context = createContext(null);
5
+ function NotificationActionsProvider({
6
+ onAction,
7
+ children
8
+ }) {
9
+ return /* @__PURE__ */ jsx(Context.Provider, { value: onAction, children });
10
+ }
11
+ function useNotificationActions() {
12
+ return useContext(Context);
13
+ }
14
+ export {
15
+ NotificationActionsProvider,
16
+ useNotificationActions
17
+ };
18
+ //# sourceMappingURL=notification-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/notification-actions.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { MerchantNotification } from '@withpotter/lib/api/types';\n\n/**\n * What a press on a bell row's pill does. The row only knows the intent (\"pay\", \"keep\", \"resume\"); the host knows what\n * that means in its product, so it answers here. Without a provider the pill is not drawn at all.\n */\nexport type NotificationActionHandler = (\n intent: string,\n notification: MerchantNotification,\n) => void;\n\nconst Context = createContext<NotificationActionHandler | null>(null);\n\nexport function NotificationActionsProvider({\n onAction,\n children,\n}: {\n onAction: NotificationActionHandler | null;\n children: ReactNode;\n}) {\n return <Context.Provider value={onAction}>{children}</Context.Provider>;\n}\n\nexport function useNotificationActions(): NotificationActionHandler | null {\n return useContext(Context);\n}\n"],"mappings":";AAuBS;AArBT,SAAS,eAAe,kBAAkC;AAY1D,MAAM,UAAU,cAAgD,IAAI;AAE7D,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,QAAQ,UAAR,EAAiB,OAAO,UAAW,UAAS;AACtD;AAEO,SAAS,yBAA2D;AACzE,SAAO,WAAW,OAAO;AAC3B;","names":[]}
@@ -7,6 +7,15 @@ export interface NotificationKind {
7
7
  label: string;
8
8
  tone: NotificationTone;
9
9
  }
10
- /** How a notification of this type is drawn. A type nobody has styled is still a bell. */
11
- export declare function notificationKind(type: string): NotificationKind;
10
+ /**
11
+ * How a notification of this type is drawn. A type nobody has styled is still a bell. A plan notice
12
+ * (`subscription.*`) carries its own tone, since the same kind can be good news or a warning.
13
+ */
14
+ export declare function notificationKind(type: string, metadata?: Record<string, unknown> | null): NotificationKind;
15
+ /** The next step a notification offers on its row: a label for the pill and the intent it stands for. */
16
+ export interface NotificationAction {
17
+ label: string;
18
+ intent: string;
19
+ }
20
+ export declare function notificationAction(metadata?: Record<string, unknown> | null): NotificationAction | null;
12
21
  export declare const TONE_DOT: Record<NotificationTone, string>;
@@ -2,6 +2,7 @@ import {
2
2
  Bell,
3
3
  CalendarCheck,
4
4
  Clock,
5
+ CreditCard,
5
6
  Inbox,
6
7
  LifeBuoy,
7
8
  Package,
@@ -32,9 +33,22 @@ const KINDS = {
32
33
  "delivery.declined": { Icon: UserX, label: "Delivery", tone: "warning" },
33
34
  "delivery.not_accepted": { Icon: Clock, label: "Delivery", tone: "warning" }
34
35
  };
35
- function notificationKind(type) {
36
+ const TONES = /* @__PURE__ */ new Set(["good", "warning", "info", "neutral"]);
37
+ function notificationKind(type, metadata) {
38
+ if (type.startsWith("subscription.")) {
39
+ const tone = metadata?.tone;
40
+ return {
41
+ Icon: CreditCard,
42
+ label: "Plan",
43
+ tone: typeof tone === "string" && TONES.has(tone) ? tone : "neutral"
44
+ };
45
+ }
36
46
  return KINDS[type] ?? { Icon: Bell, label: "Update", tone: "neutral" };
37
47
  }
48
+ function notificationAction(metadata) {
49
+ const action = metadata?.action;
50
+ return action && typeof action.label === "string" && typeof action.intent === "string" ? { label: action.label, intent: action.intent } : null;
51
+ }
38
52
  const TONE_DOT = {
39
53
  good: "var(--success)",
40
54
  warning: "var(--warning)",
@@ -43,6 +57,7 @@ const TONE_DOT = {
43
57
  };
44
58
  export {
45
59
  TONE_DOT,
60
+ notificationAction,
46
61
  notificationKind
47
62
  };
48
63
  //# sourceMappingURL=notification-kinds.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notification-kinds.ts"],"sourcesContent":["import {\n Bell,\n CalendarCheck,\n Clock,\n Inbox,\n LifeBuoy,\n Package,\n PackageCheck,\n PackageX,\n ShoppingBag,\n UserPlus,\n UserX,\n type LucideIcon,\n} from 'lucide-react';\n\n/** The colour of the dot on a notification's tile: what kind of news it is. */\nexport type NotificationTone = 'good' | 'warning' | 'info' | 'neutral';\n\nexport interface NotificationKind {\n Icon: LucideIcon;\n /** The context line under the title, before the time. */\n label: string;\n tone: NotificationTone;\n}\n\nconst KINDS: Record<string, NotificationKind> = {\n 'merchant.new_order': { Icon: ShoppingBag, label: 'Order', tone: 'good' },\n 'booking.new_alert': { Icon: CalendarCheck, label: 'Booking', tone: 'good' },\n 'merchant.low_stock': { Icon: Package, label: 'Stock', tone: 'warning' },\n 'merchant.support_reply': {\n Icon: LifeBuoy,\n label: 'Support',\n tone: 'info',\n },\n 'merchant.new_inquiry': { Icon: Inbox, label: 'Enquiry', tone: 'info' },\n 'merchant.team_member_joined': {\n Icon: UserPlus,\n label: 'Team',\n tone: 'neutral',\n },\n // A shop's own riders. Each links to the order (`/dashboard/orders/:id`).\n 'delivery.delivered': { Icon: PackageCheck, label: 'Delivery', tone: 'good' },\n 'delivery.failed': { Icon: PackageX, label: 'Delivery', tone: 'warning' },\n 'delivery.declined': { Icon: UserX, label: 'Delivery', tone: 'warning' },\n 'delivery.not_accepted': { Icon: Clock, label: 'Delivery', tone: 'warning' },\n};\n\n/** How a notification of this type is drawn. A type nobody has styled is still a bell. */\nexport function notificationKind(type: string): NotificationKind {\n return KINDS[type] ?? { Icon: Bell, label: 'Update', tone: 'neutral' };\n}\n\nexport const TONE_DOT: Record<NotificationTone, string> = {\n good: 'var(--success)',\n warning: 'var(--warning)',\n info: 'var(--brand-500)',\n neutral: 'var(--text-muted)',\n};\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAYP,MAAM,QAA0C;AAAA,EAC9C,sBAAsB,EAAE,MAAM,aAAa,OAAO,SAAS,MAAM,OAAO;AAAA,EACxE,qBAAqB,EAAE,MAAM,eAAe,OAAO,WAAW,MAAM,OAAO;AAAA,EAC3E,sBAAsB,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,UAAU;AAAA,EACvE,0BAA0B;AAAA,IACxB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,wBAAwB,EAAE,MAAM,OAAO,OAAO,WAAW,MAAM,OAAO;AAAA,EACtE,+BAA+B;AAAA,IAC7B,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA;AAAA,EAEA,sBAAsB,EAAE,MAAM,cAAc,OAAO,YAAY,MAAM,OAAO;AAAA,EAC5E,mBAAmB,EAAE,MAAM,UAAU,OAAO,YAAY,MAAM,UAAU;AAAA,EACxE,qBAAqB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAAA,EACvE,yBAAyB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAC7E;AAGO,SAAS,iBAAiB,MAAgC;AAC/D,SAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,OAAO,UAAU,MAAM,UAAU;AACvE;AAEO,MAAM,WAA6C;AAAA,EACxD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AACX;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notification-kinds.ts"],"sourcesContent":["import {\n Bell,\n CalendarCheck,\n Clock,\n CreditCard,\n Inbox,\n LifeBuoy,\n Package,\n PackageCheck,\n PackageX,\n ShoppingBag,\n UserPlus,\n UserX,\n type LucideIcon,\n} from 'lucide-react';\n\n/** The colour of the dot on a notification's tile: what kind of news it is. */\nexport type NotificationTone = 'good' | 'warning' | 'info' | 'neutral';\n\nexport interface NotificationKind {\n Icon: LucideIcon;\n /** The context line under the title, before the time. */\n label: string;\n tone: NotificationTone;\n}\n\nconst KINDS: Record<string, NotificationKind> = {\n 'merchant.new_order': { Icon: ShoppingBag, label: 'Order', tone: 'good' },\n 'booking.new_alert': { Icon: CalendarCheck, label: 'Booking', tone: 'good' },\n 'merchant.low_stock': { Icon: Package, label: 'Stock', tone: 'warning' },\n 'merchant.support_reply': {\n Icon: LifeBuoy,\n label: 'Support',\n tone: 'info',\n },\n 'merchant.new_inquiry': { Icon: Inbox, label: 'Enquiry', tone: 'info' },\n 'merchant.team_member_joined': {\n Icon: UserPlus,\n label: 'Team',\n tone: 'neutral',\n },\n // A shop's own riders. Each links to the order (`/dashboard/orders/:id`).\n 'delivery.delivered': { Icon: PackageCheck, label: 'Delivery', tone: 'good' },\n 'delivery.failed': { Icon: PackageX, label: 'Delivery', tone: 'warning' },\n 'delivery.declined': { Icon: UserX, label: 'Delivery', tone: 'warning' },\n 'delivery.not_accepted': { Icon: Clock, label: 'Delivery', tone: 'warning' },\n};\n\nconst TONES = new Set<string>(['good', 'warning', 'info', 'neutral']);\n\n/**\n * How a notification of this type is drawn. A type nobody has styled is still a bell. A plan notice\n * (`subscription.*`) carries its own tone, since the same kind can be good news or a warning.\n */\nexport function notificationKind(\n type: string,\n metadata?: Record<string, unknown> | null,\n): NotificationKind {\n if (type.startsWith('subscription.')) {\n const tone = metadata?.tone;\n\n return {\n Icon: CreditCard,\n label: 'Plan',\n tone: typeof tone === 'string' && TONES.has(tone) ? (tone as NotificationTone) : 'neutral',\n };\n }\n\n return KINDS[type] ?? { Icon: Bell, label: 'Update', tone: 'neutral' };\n}\n\n/** The next step a notification offers on its row: a label for the pill and the intent it stands for. */\nexport interface NotificationAction {\n label: string;\n intent: string;\n}\n\nexport function notificationAction(\n metadata?: Record<string, unknown> | null,\n): NotificationAction | null {\n const action = metadata?.action as { label?: unknown; intent?: unknown } | null | undefined;\n\n return action && typeof action.label === 'string' && typeof action.intent === 'string'\n ? { label: action.label, intent: action.intent }\n : null;\n}\n\nexport const TONE_DOT: Record<NotificationTone, string> = {\n good: 'var(--success)',\n warning: 'var(--warning)',\n info: 'var(--brand-500)',\n neutral: 'var(--text-muted)',\n};\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAYP,MAAM,QAA0C;AAAA,EAC9C,sBAAsB,EAAE,MAAM,aAAa,OAAO,SAAS,MAAM,OAAO;AAAA,EACxE,qBAAqB,EAAE,MAAM,eAAe,OAAO,WAAW,MAAM,OAAO;AAAA,EAC3E,sBAAsB,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,UAAU;AAAA,EACvE,0BAA0B;AAAA,IACxB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA,EACA,wBAAwB,EAAE,MAAM,OAAO,OAAO,WAAW,MAAM,OAAO;AAAA,EACtE,+BAA+B;AAAA,IAC7B,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AAAA;AAAA,EAEA,sBAAsB,EAAE,MAAM,cAAc,OAAO,YAAY,MAAM,OAAO;AAAA,EAC5E,mBAAmB,EAAE,MAAM,UAAU,OAAO,YAAY,MAAM,UAAU;AAAA,EACxE,qBAAqB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAAA,EACvE,yBAAyB,EAAE,MAAM,OAAO,OAAO,YAAY,MAAM,UAAU;AAC7E;AAEA,MAAM,QAAQ,oBAAI,IAAY,CAAC,QAAQ,WAAW,QAAQ,SAAS,CAAC;AAM7D,SAAS,iBACd,MACA,UACkB;AAClB,MAAI,KAAK,WAAW,eAAe,GAAG;AACpC,UAAM,OAAO,UAAU;AAEvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,MAAM,OAAO,SAAS,YAAY,MAAM,IAAI,IAAI,IAAK,OAA4B;AAAA,IACnF;AAAA,EACF;AAEA,SAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,OAAO,UAAU,MAAM,UAAU;AACvE;AAQO,SAAS,mBACd,UAC2B;AAC3B,QAAM,SAAS,UAAU;AAEzB,SAAO,UAAU,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,WAC1E,EAAE,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,IAC7C;AACN;AAEO,MAAM,WAA6C;AAAA,EACxD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AACX;","names":[]}
@@ -4,11 +4,15 @@ import type { MerchantNotification } from '@withpotter/lib/api/types';
4
4
  * went, the headline, and where and when underneath.
5
5
  *
6
6
  * Unread reads at full strength on a tinted row and read recedes, so a long
7
- * list shows what is waiting without a second marker.
7
+ * list shows what is waiting without a second marker. When the news has a next
8
+ * step (pay, keep, resume) it is a pill under the text, its own button, so
9
+ * pressing it does that step and pressing the row opens the news.
8
10
  */
9
- export declare function NotificationRow({ notification, when, onOpen, }: {
11
+ export declare function NotificationRow({ notification, when, onOpen, onAction, }: {
10
12
  notification: MerchantNotification;
11
13
  /** The age, already worded ("just now", "5m ago"). */
12
14
  when: string;
13
15
  onOpen: () => void;
16
+ /** The pill was pressed. Rows without a next step never call it. */
17
+ onAction?: (intent: string, notification: MerchantNotification) => void;
14
18
  }): import("react").JSX.Element;
@@ -1,45 +1,71 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { notificationKind, TONE_DOT } from "./notification-kinds";
2
+ import {
3
+ notificationAction,
4
+ notificationKind,
5
+ TONE_DOT
6
+ } from "./notification-kinds";
3
7
  function NotificationRow({
4
8
  notification,
5
9
  when,
6
- onOpen
10
+ onOpen,
11
+ onAction
7
12
  }) {
8
- const { Icon, label, tone } = notificationKind(notification.type);
13
+ const { Icon, label, tone } = notificationKind(
14
+ notification.type,
15
+ notification.metadata
16
+ );
17
+ const action = onAction ? notificationAction(notification.metadata) : null;
9
18
  const unread = !notification.readAt;
10
19
  return /* @__PURE__ */ jsxs(
11
- "button",
20
+ "div",
12
21
  {
13
- type: "button",
14
- onClick: onOpen,
15
- title: notification.body || void 0,
16
- className: `flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)] ${unread ? "bg-[var(--bg-subtle)]" : ""}`,
22
+ className: `transition-colors hover:bg-[var(--interactive-hover)] ${unread ? "bg-[var(--bg-subtle)]" : ""}`,
17
23
  children: [
18
- /* @__PURE__ */ jsxs("span", { className: "relative shrink-0", children: [
19
- /* @__PURE__ */ jsx("span", { className: "grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]", children: /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": true }) }),
20
- /* @__PURE__ */ jsx(
21
- "span",
22
- {
23
- "aria-hidden": true,
24
- className: "absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]",
25
- style: { background: TONE_DOT[tone] }
26
- }
27
- )
28
- ] }),
29
- /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
30
- /* @__PURE__ */ jsx(
31
- "span",
32
- {
33
- className: `block truncate text-sm leading-snug ${unread ? "font-medium text-[var(--text-primary)]" : "text-[var(--text-tertiary)]"}`,
34
- children: notification.title
35
- }
36
- ),
37
- /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block truncate text-xs text-[var(--text-tertiary)]", children: [
38
- label,
39
- " \xB7 ",
40
- when
41
- ] })
42
- ] })
24
+ /* @__PURE__ */ jsxs(
25
+ "button",
26
+ {
27
+ type: "button",
28
+ onClick: onOpen,
29
+ title: notification.body || void 0,
30
+ className: "flex w-full items-start gap-2 px-3 py-2 text-left",
31
+ children: [
32
+ /* @__PURE__ */ jsxs("span", { className: "relative shrink-0", children: [
33
+ /* @__PURE__ */ jsx("span", { className: "grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]", children: /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": true }) }),
34
+ /* @__PURE__ */ jsx(
35
+ "span",
36
+ {
37
+ "aria-hidden": true,
38
+ className: "absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]",
39
+ style: { background: TONE_DOT[tone] }
40
+ }
41
+ )
42
+ ] }),
43
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
44
+ /* @__PURE__ */ jsx(
45
+ "span",
46
+ {
47
+ className: `block truncate text-sm leading-snug ${unread ? "font-medium text-[var(--text-primary)]" : "text-[var(--text-tertiary)]"}`,
48
+ children: notification.title
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block truncate text-xs text-[var(--text-tertiary)]", children: [
52
+ label,
53
+ " \xB7 ",
54
+ when
55
+ ] })
56
+ ] })
57
+ ]
58
+ }
59
+ ),
60
+ action && /* @__PURE__ */ jsx("div", { className: "pb-2 pl-14 pr-3", children: /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ type: "button",
64
+ onClick: () => onAction?.(action.intent, notification),
65
+ className: "h-7 rounded-full border border-[var(--border-default)] bg-[var(--bg-base)] px-3 text-xs font-medium text-[var(--text-primary)] transition-colors hover:bg-[var(--interactive-hover)]",
66
+ children: action.label
67
+ }
68
+ ) })
43
69
  ]
44
70
  }
45
71
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/notification-row.tsx"],"sourcesContent":["import type { MerchantNotification } from '@withpotter/lib/api/types';\nimport { notificationKind, TONE_DOT } from './notification-kinds';\n\n/**\n * One notification: a tile for what kind of news it is, with a dot for how it\n * went, the headline, and where and when underneath.\n *\n * Unread reads at full strength on a tinted row and read recedes, so a long\n * list shows what is waiting without a second marker.\n */\nexport function NotificationRow({\n notification,\n when,\n onOpen,\n}: {\n notification: MerchantNotification;\n /** The age, already worded (\"just now\", \"5m ago\"). */\n when: string;\n onOpen: () => void;\n}) {\n const { Icon, label, tone } = notificationKind(notification.type);\n const unread = !notification.readAt;\n\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n title={notification.body || undefined}\n className={`flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--interactive-hover)] ${\n unread ? 'bg-[var(--bg-subtle)]' : ''\n }`}\n >\n <span className=\"relative shrink-0\">\n <span className=\"grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]\">\n <Icon className=\"size-4\" aria-hidden />\n </span>\n <span\n aria-hidden\n className=\"absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]\"\n style={{ background: TONE_DOT[tone] }}\n />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n className={`block truncate text-sm leading-snug ${\n unread\n ? 'font-medium text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)]'\n }`}\n >\n {notification.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-[var(--text-tertiary)]\">\n {label} · {when}\n </span>\n </span>\n </button>\n );\n}\n"],"mappings":"AAgCM,SAEI,KAFJ;AA/BN,SAAS,kBAAkB,gBAAgB;AASpC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,MAAM,OAAO,KAAK,IAAI,iBAAiB,aAAa,IAAI;AAChE,QAAM,SAAS,CAAC,aAAa;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO,aAAa,QAAQ;AAAA,MAC5B,WAAW,2GACT,SAAS,0BAA0B,EACrC;AAAA,MAEA;AAAA,6BAAC,UAAK,WAAU,qBACd;AAAA,8BAAC,UAAK,WAAU,mIACd,8BAAC,QAAK,WAAU,UAAS,eAAW,MAAC,GACvC;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA,cACV,OAAO,EAAE,YAAY,SAAS,IAAI,EAAE;AAAA;AAAA,UACtC;AAAA,WACF;AAAA,QACA,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,uCACT,SACI,2CACA,6BACN;AAAA,cAEC,uBAAa;AAAA;AAAA,UAChB;AAAA,UACA,qBAAC,UAAK,WAAU,6DACb;AAAA;AAAA,YAAM;AAAA,YAAI;AAAA,aACb;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/notification-row.tsx"],"sourcesContent":["import type { MerchantNotification } from '@withpotter/lib/api/types';\nimport {\n notificationAction,\n notificationKind,\n TONE_DOT,\n} from './notification-kinds';\n\n/**\n * One notification: a tile for what kind of news it is, with a dot for how it\n * went, the headline, and where and when underneath.\n *\n * Unread reads at full strength on a tinted row and read recedes, so a long\n * list shows what is waiting without a second marker. When the news has a next\n * step (pay, keep, resume) it is a pill under the text, its own button, so\n * pressing it does that step and pressing the row opens the news.\n */\nexport function NotificationRow({\n notification,\n when,\n onOpen,\n onAction,\n}: {\n notification: MerchantNotification;\n /** The age, already worded (\"just now\", \"5m ago\"). */\n when: string;\n onOpen: () => void;\n /** The pill was pressed. Rows without a next step never call it. */\n onAction?: (intent: string, notification: MerchantNotification) => void;\n}) {\n const { Icon, label, tone } = notificationKind(\n notification.type,\n notification.metadata,\n );\n const action = onAction ? notificationAction(notification.metadata) : null;\n const unread = !notification.readAt;\n\n return (\n <div\n className={`transition-colors hover:bg-[var(--interactive-hover)] ${\n unread ? 'bg-[var(--bg-subtle)]' : ''\n }`}\n >\n <button\n type=\"button\"\n onClick={onOpen}\n title={notification.body || undefined}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <span className=\"relative shrink-0\">\n <span className=\"grid size-9 place-items-center rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] text-[var(--text-secondary)]\">\n <Icon className=\"size-4\" aria-hidden />\n </span>\n <span\n aria-hidden\n className=\"absolute -right-0.5 -top-0.5 size-3 rounded-full ring-2 ring-[var(--bg-base)]\"\n style={{ background: TONE_DOT[tone] }}\n />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n className={`block truncate text-sm leading-snug ${\n unread\n ? 'font-medium text-[var(--text-primary)]'\n : 'text-[var(--text-tertiary)]'\n }`}\n >\n {notification.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-[var(--text-tertiary)]\">\n {label} · {when}\n </span>\n </span>\n </button>\n {action && (\n <div className=\"pb-2 pl-14 pr-3\">\n <button\n type=\"button\"\n onClick={() => onAction?.(action.intent, notification)}\n className=\"h-7 rounded-full border border-[var(--border-default)] bg-[var(--bg-base)] px-3 text-xs font-medium text-[var(--text-primary)] transition-colors hover:bg-[var(--interactive-hover)]\"\n >\n {action.label}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"AAgDQ,SAEI,KAFJ;AA/CR;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWA,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,EAAE,MAAM,OAAO,KAAK,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,aAAa;AAAA,EACf;AACA,QAAM,SAAS,WAAW,mBAAmB,aAAa,QAAQ,IAAI;AACtE,QAAM,SAAS,CAAC,aAAa;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,yDACT,SAAS,0BAA0B,EACrC;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,OAAO,aAAa,QAAQ;AAAA,YAC5B,WAAU;AAAA,YAEV;AAAA,mCAAC,UAAK,WAAU,qBACd;AAAA,oCAAC,UAAK,WAAU,mIACd,8BAAC,QAAK,WAAU,UAAS,eAAW,MAAC,GACvC;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,eAAW;AAAA,oBACX,WAAU;AAAA,oBACV,OAAO,EAAE,YAAY,SAAS,IAAI,EAAE;AAAA;AAAA,gBACtC;AAAA,iBACF;AAAA,cACA,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,uCACT,SACI,2CACA,6BACN;AAAA,oBAEC,uBAAa;AAAA;AAAA,gBAChB;AAAA,gBACA,qBAAC,UAAK,WAAU,6DACb;AAAA;AAAA,kBAAM;AAAA,kBAAI;AAAA,mBACb;AAAA,iBACF;AAAA;AAAA;AAAA,QACF;AAAA,QACC,UACC,oBAAC,SAAI,WAAU,mBACb;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,WAAW,OAAO,QAAQ,YAAY;AAAA,YACrD,WAAU;AAAA,YAET,iBAAO;AAAA;AAAA,QACV,GACF;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ /**
4
+ * A row a host adds to the settings window, with the screen behind it.
5
+ *
6
+ * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.
7
+ * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.
8
+ */
9
+ export interface HostSettingsRow {
10
+ id: string;
11
+ name: string;
12
+ icon: LucideIcon;
13
+ /** Addresses this row owns: a link to any of them opens the window on it. */
14
+ paths: string[];
15
+ /** What search also matches. */
16
+ keywords: string[];
17
+ /** The row it is placed after. Default: after General. */
18
+ after?: string;
19
+ /** The screen drawn in the window. */
20
+ render: () => ReactNode;
21
+ }
22
+ export declare function SettingsHostProvider({ rows, children, }: {
23
+ rows: HostSettingsRow[];
24
+ children: ReactNode;
25
+ }): import("react").JSX.Element;
26
+ export declare function useSettingsHostRows(): HostSettingsRow[];
27
+ /** The host row that owns an address: the address is one of its paths or sits below one. */
28
+ export declare function hostRowForHref(rows: HostSettingsRow[], href: string | null | undefined): HostSettingsRow | undefined;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const Context = createContext([]);
5
+ function SettingsHostProvider({
6
+ rows,
7
+ children
8
+ }) {
9
+ return /* @__PURE__ */ jsx(Context.Provider, { value: rows, children });
10
+ }
11
+ function useSettingsHostRows() {
12
+ return useContext(Context);
13
+ }
14
+ function hostRowForHref(rows, href) {
15
+ const path = (href ?? "").split(/[?#]/)[0];
16
+ return rows.find(
17
+ (row) => row.paths.some((p) => path === p || path.startsWith(`${p}/`))
18
+ );
19
+ }
20
+ export {
21
+ SettingsHostProvider,
22
+ hostRowForHref,
23
+ useSettingsHostRows
24
+ };
25
+ //# sourceMappingURL=settings-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/settings-host.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { LucideIcon } from 'lucide-react';\n\n/**\n * A row a host adds to the settings window, with the screen behind it.\n *\n * Olive's plans come from its own engine, so its Plan & billing is its own screen rather than the shared Billing page.\n * A host that never wraps the dashboard in `SettingsHostProvider` gets the window it had.\n */\nexport interface HostSettingsRow {\n id: string;\n name: string;\n icon: LucideIcon;\n /** Addresses this row owns: a link to any of them opens the window on it. */\n paths: string[];\n /** What search also matches. */\n keywords: string[];\n /** The row it is placed after. Default: after General. */\n after?: string;\n /** The screen drawn in the window. */\n render: () => ReactNode;\n}\n\nconst Context = createContext<HostSettingsRow[]>([]);\n\nexport function SettingsHostProvider({\n rows,\n children,\n}: {\n rows: HostSettingsRow[];\n children: ReactNode;\n}) {\n return <Context.Provider value={rows}>{children}</Context.Provider>;\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":";AAkCS;AAhCT,SAAS,eAAe,kBAAkC;AAuB1D,MAAM,UAAU,cAAiC,CAAC,CAAC;AAE5C,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,QAAQ,UAAR,EAAiB,OAAO,MAAO,UAAS;AAClD;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":[]}
@@ -2,6 +2,8 @@ import { type ReactNode } from 'react';
2
2
  interface SettingsModalApi {
3
3
  /** Open the settings window, on a section when given a settings address. */
4
4
  openSettings: (href?: string) => void;
5
+ /** Close it, for a screen inside it that leaves for another page. */
6
+ closeSettings: () => void;
5
7
  /**
6
8
  * Warm the window before it is opened: a hidden copy of the settings page
7
9
  * loads once so the real one boots with its code fetched and its route
@@ -20,6 +20,8 @@ const isSettingsPath = (path) => !!path && (path === SETTINGS_PREFIX || path.sta
20
20
  const SettingsModalContext = createContext({
21
21
  openSettings: () => {
22
22
  },
23
+ closeSettings: () => {
24
+ },
23
25
  prewarmSettings: () => {
24
26
  }
25
27
  });
@@ -115,8 +117,8 @@ function SettingsModalProvider({ children }) {
115
117
  router.replace(lastOutside.current);
116
118
  }, [pathname, openSettings, router]);
117
119
  const api = useMemo(
118
- () => ({ openSettings, prewarmSettings }),
119
- [openSettings, prewarmSettings]
120
+ () => ({ openSettings, closeSettings: close, prewarmSettings }),
121
+ [openSettings, close, prewarmSettings]
120
122
  );
121
123
  return /* @__PURE__ */ jsxs(SettingsModalContext.Provider, { value: api, children: [
122
124
  children,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-modal-context.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { usePathname, useRouter } from 'next/navigation';\nimport { isEmbeddedFrame } from '@withpotter/lib/embedded-frame';\nimport { isMobileViewport } from '@withpotter/hooks/use-mobile';\nimport { UnifiedSettingsModal } from './unified-settings-modal';\nimport { movedSettingsTarget } from './moved-settings';\n\nconst SETTINGS_PREFIX = '/dashboard/settings';\n\n/** `/dashboard/settings` and anything under it, but not `/dashboard/settingsX`. */\nconst isSettingsPath = (path: string | null | undefined) =>\n !!path &&\n (path === SETTINGS_PREFIX || path.startsWith(`${SETTINGS_PREFIX}/`));\n\ninterface SettingsModalApi {\n /** Open the settings window, on a section when given a settings address. */\n openSettings: (href?: string) => void;\n /**\n * Warm the window before it is opened: a hidden copy of the settings page\n * loads once so the real one boots with its code fetched and its route\n * compiled. Cheap to call repeatedly; only the first call in a page load does\n * anything. Call it on the signals that precede opening (a menu opening, a\n * pointer arriving on a settings link).\n */\n prewarmSettings: () => void;\n}\n\nconst SettingsModalContext = createContext<SettingsModalApi>({\n openSettings: () => {},\n prewarmSettings: () => {},\n});\n\nexport const useSettingsModal = () => useContext(SettingsModalContext);\n\n/**\n * Settings is a window, never a page.\n *\n * The workspace menu always opened it as one; everything else that pointed at a\n * settings address (a setup step, the Customize sidebar row, a link in an email,\n * a typed URL) still navigated to the page. This owns the one modal and makes\n * every road into settings arrive in it:\n *\n * - a CLICK on a link to a settings address is caught before it navigates and\n * opens the window on that section;\n * - anything else that lands on a settings route (`router.push`, a pasted\n * address, back/forward) is sent back to where you were and the window opens\n * on it, with the page itself never drawn (see the layout).\n *\n * The framed copy of a page is the one place a settings route is allowed to\n * render, so nothing here runs inside it.\n */\nexport function SettingsModalProvider({ children }: { children: ReactNode }) {\n const router = useRouter();\n const pathname = usePathname();\n const [state, setState] = useState<{ open: boolean; href: string | null }>({\n open: false,\n href: null,\n });\n\n // The window is for a desktop screen. On a phone settings stays what it was:\n // pages you go to, with the mobile shell around them.\n const openSettings = useCallback(\n (href?: string) => {\n // An address that moved out of settings goes to its page, not the window.\n const moved = href\n ? movedSettingsTarget(\n href.split(/[?#]/)[0],\n href.includes('?') ? `?${href.split('?')[1].split('#')[0]}` : '',\n )\n : null;\n if (moved) {\n router.push(moved);\n return;\n }\n if (isMobileViewport()) {\n router.push(href ?? '/dashboard/settings');\n return;\n }\n setState({ open: true, href: href ?? null });\n },\n [router],\n );\n // Kept so callers that warm the window on hover still compile: the window no\n // longer boots a frame, so there is nothing to warm.\n const prewarmSettings = useCallback(() => {}, []);\n\n const close = useCallback(() => setState((s) => ({ ...s, open: false })), []);\n\n // Where you were before settings, so closing leaves you there.\n const lastOutside = useRef('/dashboard');\n useEffect(() => {\n if (!isSettingsPath(pathname) && pathname) lastOutside.current = pathname;\n }, [pathname]);\n\n // A click on a link to settings: open the window instead of going there.\n useEffect(() => {\n if (isEmbeddedFrame()) return;\n const onClick = (e: MouseEvent) => {\n if (isMobileViewport()) return;\n if (e.defaultPrevented || e.button !== 0) return;\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const a = (e.target as Element | null)?.closest?.('a[href]');\n if (!a || a.getAttribute('target') === '_blank') return;\n let url: URL;\n try {\n url = new URL(a.getAttribute('href') ?? '', window.location.href);\n } catch {\n return;\n }\n if (url.origin !== window.location.origin) return;\n if (!isSettingsPath(url.pathname)) return;\n const moved = movedSettingsTarget(url.pathname, url.search);\n if (moved) {\n e.preventDefault();\n router.push(moved);\n return;\n }\n // Not stopped: the link's own handlers still run (a dialog closing as you\n // leave it, say). Next skips its navigation for a prevented click.\n e.preventDefault();\n openSettings(url.pathname + url.search + url.hash);\n };\n // Capture, so this runs before the framework's own link handling.\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n }, [openSettings, router]);\n\n // A pointer or keyboard focus arriving on a link to settings is the last\n // moment before the click; start warming then.\n useEffect(() => {\n if (isEmbeddedFrame()) return;\n const onNear = (e: Event) => {\n const a = (e.target as Element | null)?.closest?.('a[href]');\n const href = a?.getAttribute('href');\n const path = href?.split(/[?#]/)[0];\n if (path && isSettingsPath(path) && !movedSettingsTarget(path)) {\n prewarmSettings();\n }\n };\n document.addEventListener('pointerover', onNear, true);\n document.addEventListener('focusin', onNear, true);\n return () => {\n document.removeEventListener('pointerover', onNear, true);\n document.removeEventListener('focusin', onNear, true);\n };\n }, [prewarmSettings]);\n\n // Anything else that lands on a settings route: back to where you were, with\n // the window open on it. Before paint, so the page is never seen.\n useLayoutEffect(() => {\n if (isEmbeddedFrame() || isMobileViewport()) return;\n if (!isSettingsPath(pathname)) return;\n const moved = movedSettingsTarget(pathname, window.location.search);\n if (moved) {\n router.replace(moved);\n return;\n }\n // eslint-disable-next-line react-hooks/set-state-in-effect -- before paint, so the page is never drawn\n openSettings(\n pathname +\n (typeof window !== 'undefined'\n ? window.location.search + window.location.hash\n : ''),\n );\n router.replace(lastOutside.current);\n }, [pathname, openSettings, router]);\n\n const api = useMemo(\n () => ({ openSettings, prewarmSettings }),\n [openSettings, prewarmSettings],\n );\n\n return (\n <SettingsModalContext.Provider value={api}>\n {children}\n <UnifiedSettingsModal\n open={state.open}\n href={state.href}\n onClose={close}\n />\n </SettingsModalContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-modal-context.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { usePathname, useRouter } from 'next/navigation';\nimport { isEmbeddedFrame } from '@withpotter/lib/embedded-frame';\nimport { isMobileViewport } from '@withpotter/hooks/use-mobile';\nimport { UnifiedSettingsModal } from './unified-settings-modal';\nimport { movedSettingsTarget } from './moved-settings';\n\nconst SETTINGS_PREFIX = '/dashboard/settings';\n\n/** `/dashboard/settings` and anything under it, but not `/dashboard/settingsX`. */\nconst isSettingsPath = (path: string | null | undefined) =>\n !!path &&\n (path === SETTINGS_PREFIX || path.startsWith(`${SETTINGS_PREFIX}/`));\n\ninterface SettingsModalApi {\n /** Open the settings window, on a section when given a settings address. */\n openSettings: (href?: string) => void;\n /** Close it, for a screen inside it that leaves for another page. */\n closeSettings: () => void;\n /**\n * Warm the window before it is opened: a hidden copy of the settings page\n * loads once so the real one boots with its code fetched and its route\n * compiled. Cheap to call repeatedly; only the first call in a page load does\n * anything. Call it on the signals that precede opening (a menu opening, a\n * pointer arriving on a settings link).\n */\n prewarmSettings: () => void;\n}\n\nconst SettingsModalContext = createContext<SettingsModalApi>({\n openSettings: () => {},\n closeSettings: () => {},\n prewarmSettings: () => {},\n});\n\nexport const useSettingsModal = () => useContext(SettingsModalContext);\n\n/**\n * Settings is a window, never a page.\n *\n * The workspace menu always opened it as one; everything else that pointed at a\n * settings address (a setup step, the Customize sidebar row, a link in an email,\n * a typed URL) still navigated to the page. This owns the one modal and makes\n * every road into settings arrive in it:\n *\n * - a CLICK on a link to a settings address is caught before it navigates and\n * opens the window on that section;\n * - anything else that lands on a settings route (`router.push`, a pasted\n * address, back/forward) is sent back to where you were and the window opens\n * on it, with the page itself never drawn (see the layout).\n *\n * The framed copy of a page is the one place a settings route is allowed to\n * render, so nothing here runs inside it.\n */\nexport function SettingsModalProvider({ children }: { children: ReactNode }) {\n const router = useRouter();\n const pathname = usePathname();\n const [state, setState] = useState<{ open: boolean; href: string | null }>({\n open: false,\n href: null,\n });\n\n // The window is for a desktop screen. On a phone settings stays what it was:\n // pages you go to, with the mobile shell around them.\n const openSettings = useCallback(\n (href?: string) => {\n // An address that moved out of settings goes to its page, not the window.\n const moved = href\n ? movedSettingsTarget(\n href.split(/[?#]/)[0],\n href.includes('?') ? `?${href.split('?')[1].split('#')[0]}` : '',\n )\n : null;\n if (moved) {\n router.push(moved);\n return;\n }\n if (isMobileViewport()) {\n router.push(href ?? '/dashboard/settings');\n return;\n }\n setState({ open: true, href: href ?? null });\n },\n [router],\n );\n // Kept so callers that warm the window on hover still compile: the window no\n // longer boots a frame, so there is nothing to warm.\n const prewarmSettings = useCallback(() => {}, []);\n\n const close = useCallback(() => setState((s) => ({ ...s, open: false })), []);\n\n // Where you were before settings, so closing leaves you there.\n const lastOutside = useRef('/dashboard');\n useEffect(() => {\n if (!isSettingsPath(pathname) && pathname) lastOutside.current = pathname;\n }, [pathname]);\n\n // A click on a link to settings: open the window instead of going there.\n useEffect(() => {\n if (isEmbeddedFrame()) return;\n const onClick = (e: MouseEvent) => {\n if (isMobileViewport()) return;\n if (e.defaultPrevented || e.button !== 0) return;\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const a = (e.target as Element | null)?.closest?.('a[href]');\n if (!a || a.getAttribute('target') === '_blank') return;\n let url: URL;\n try {\n url = new URL(a.getAttribute('href') ?? '', window.location.href);\n } catch {\n return;\n }\n if (url.origin !== window.location.origin) return;\n if (!isSettingsPath(url.pathname)) return;\n const moved = movedSettingsTarget(url.pathname, url.search);\n if (moved) {\n e.preventDefault();\n router.push(moved);\n return;\n }\n // Not stopped: the link's own handlers still run (a dialog closing as you\n // leave it, say). Next skips its navigation for a prevented click.\n e.preventDefault();\n openSettings(url.pathname + url.search + url.hash);\n };\n // Capture, so this runs before the framework's own link handling.\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n }, [openSettings, router]);\n\n // A pointer or keyboard focus arriving on a link to settings is the last\n // moment before the click; start warming then.\n useEffect(() => {\n if (isEmbeddedFrame()) return;\n const onNear = (e: Event) => {\n const a = (e.target as Element | null)?.closest?.('a[href]');\n const href = a?.getAttribute('href');\n const path = href?.split(/[?#]/)[0];\n if (path && isSettingsPath(path) && !movedSettingsTarget(path)) {\n prewarmSettings();\n }\n };\n document.addEventListener('pointerover', onNear, true);\n document.addEventListener('focusin', onNear, true);\n return () => {\n document.removeEventListener('pointerover', onNear, true);\n document.removeEventListener('focusin', onNear, true);\n };\n }, [prewarmSettings]);\n\n // Anything else that lands on a settings route: back to where you were, with\n // the window open on it. Before paint, so the page is never seen.\n useLayoutEffect(() => {\n if (isEmbeddedFrame() || isMobileViewport()) return;\n if (!isSettingsPath(pathname)) return;\n const moved = movedSettingsTarget(pathname, window.location.search);\n if (moved) {\n router.replace(moved);\n return;\n }\n // eslint-disable-next-line react-hooks/set-state-in-effect -- before paint, so the page is never drawn\n openSettings(\n pathname +\n (typeof window !== 'undefined'\n ? window.location.search + window.location.hash\n : ''),\n );\n router.replace(lastOutside.current);\n }, [pathname, openSettings, router]);\n\n const api = useMemo(\n () => ({ openSettings, closeSettings: close, prewarmSettings }),\n [openSettings, close, prewarmSettings],\n );\n\n return (\n <SettingsModalContext.Provider value={api}>\n {children}\n <UnifiedSettingsModal\n open={state.open}\n href={state.href}\n onClose={close}\n />\n </SettingsModalContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
@@ -38,4 +38,7 @@ export declare const HELP_ICON: import("react").ForwardRefExoticComponent<Omit<i
38
38
  */
39
39
  export declare function rowForHref(href: string | null | undefined): SettingsRowId;
40
40
  /** Rows whose name or inner sections match what was typed. */
41
- export declare function matchRows(rows: SettingsRow[], query: string): SettingsRow[];
41
+ export declare function matchRows<T extends {
42
+ name: string;
43
+ keywords: string[];
44
+ }>(rows: T[], query: string): T[];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/settings-rows.ts"],"sourcesContent":["import {\n Bell,\n Building2,\n ChartColumn,\n CircleCheck,\n CircleHelp,\n CreditCard,\n FileText,\n Scale,\n Sparkles,\n SlidersHorizontal,\n ToggleLeft,\n UserRound,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/**\n * The rows of the settings window, in the order they are drawn.\n *\n * One list for the whole window: yours and the business's, mixed, because the\n * person looking for \"Notifications\" or \"Language\" should not have to know\n * whose setting it is. Rows that leave the window (`external`) carry an arrow.\n *\n * `paths` are the addresses a row owns. A link to any of them, or to anything\n * below them (a team member, a credits page), opens the window on that row.\n * `keywords` are what search also matches: the names of the sections inside a\n * row, so \"passkeys\" or \"time zone\" finds the row that holds them.\n */\nexport type SettingsRowId =\n | 'general'\n | 'features'\n | 'account'\n | 'team'\n | 'notifications'\n | 'demo-content'\n | 'preferences'\n | 'payment-approvals'\n | 'billing'\n | 'usage'\n | 'policies'\n | 'legal';\n\nexport interface SettingsRow {\n id: SettingsRowId;\n name: string;\n icon: LucideIcon;\n paths: string[];\n keywords: string[];\n /** Hidden from a member who lacks every one of these. Empty: everyone. */\n permission?: PermissionKey[];\n /**\n * A page this row leaves the window for, where a host has its own screen for\n * the subject. The row then is a link, and the panel is never drawn.\n */\n href?: string;\n}\n\nconst BILLING_ROW: SettingsRow = {\n id: 'billing',\n name: 'Billing & plan',\n icon: CreditCard,\n paths: ['/dashboard/settings/billing'],\n keywords: ['plan', 'invoice', 'credits', 'top-up', 'cancel'],\n permission: ['canManageSettings'],\n};\n\n// A box whose card payments wait for a second person lists the decisions.\nconst PAYMENT_APPROVALS_ROW: SettingsRow = {\n id: 'payment-approvals',\n name: 'Payment approvals',\n icon: CircleCheck,\n paths: ['/dashboard/settings/payment-approvals'],\n keywords: ['approval', 'accepted', 'rejected', 'decisions', 'history'],\n};\n\nexport const SETTINGS_ROWS: SettingsRow[] = [\n {\n id: 'general',\n name: 'General',\n icon: Building2,\n paths: [\n '/dashboard/settings',\n '/dashboard/settings/branding',\n '/dashboard/settings/regional',\n ],\n keywords: ['business name', 'branding', 'logo', 'colours', 'numbering'],\n permission: ['canManageBusinessProfile', 'canManageSettings'],\n },\n {\n // The switches no business type implies: Blog today. Next to General\n // because it is the business's own setting, before the rows that are\n // about the person (Account, Preferences).\n id: 'features',\n name: 'Features',\n icon: ToggleLeft,\n paths: ['/dashboard/settings/features'],\n keywords: ['blog', 'posts', 'write', 'publish', 'switch', 'website', 'features'],\n permission: ['canManageSettings'],\n },\n {\n id: 'account',\n name: 'Account',\n icon: UserRound,\n paths: [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'profile',\n 'name',\n 'email',\n 'photo',\n 'security',\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'privacy',\n 'export',\n 'close account',\n ],\n },\n {\n id: 'team',\n name: 'Team & permissions',\n icon: Users,\n paths: [\n '/dashboard/settings/team',\n '/dashboard/settings/roles',\n '/dashboard/settings/activity',\n ],\n keywords: ['people', 'invite', 'roles', 'activity', 'seats'],\n permission: ['canManageTeam'],\n // A box with its own team screen (its roles and invitations come from\n // its engine, which does not serve this panel's routes) names it here.\n href: process.env.NEXT_PUBLIC_BOX_TEAM_PAGE || undefined,\n },\n {\n id: 'notifications',\n name: 'Notifications',\n icon: Bell,\n paths: ['/dashboard/settings/notifications'],\n keywords: ['alerts', 'low stock', 'email sender', 'digest', 'reminders'],\n },\n {\n // Sample products, pages and pictures to start with, clear, or add again.\n // No permission gate, like the sidebar entry: everyone sees the state and\n // the card itself shows the actions only to the owner and admins.\n id: 'demo-content',\n name: 'Demo content',\n icon: Sparkles,\n paths: ['/dashboard/settings/demo-content'],\n keywords: ['sample', 'products', 'starter', 'clear', 'example data'],\n },\n {\n id: 'preferences',\n name: 'Preferences',\n icon: SlidersHorizontal,\n paths: [\n '/dashboard/settings/language',\n '/dashboard/settings/accessibility',\n ],\n keywords: [\n 'appearance',\n 'theme',\n 'dark',\n 'light',\n 'language',\n 'time zone',\n 'date format',\n 'accessibility',\n 'high contrast',\n 'reduce motion',\n 'text size',\n ],\n },\n ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\n : []),\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true' ? [] : [BILLING_ROW]),\n // A box that meters nothing in the console leaves Usage out.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_USAGE === 'true'\n ? []\n : [\n {\n id: 'usage' as const,\n name: 'Usage',\n icon: ChartColumn,\n paths: ['/dashboard/settings/usage'],\n keywords: ['credits', 'delivery', 'spend'],\n permission: ['canManageSettings' as const],\n },\n ]),\n {\n id: 'policies',\n name: 'Policies',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\n },\n {\n id: 'legal',\n name: 'Legal',\n icon: FileText,\n paths: ['/dashboard/settings/legal'],\n keywords: ['terms', 'privacy policy', 'licences'],\n },\n];\n\n/** Where \"Get help\" goes. It leaves the window, so it is not a row above. */\nexport const HELP_HREF = '/dashboard/help';\nexport const HELP_ICON = CircleHelp;\n\nconst pathOf = (full: string) => full.split(/[?#]/)[0];\n\n/**\n * The row that owns an address: the longest of its paths that equals the\n * address or is a parent of it. General's own address is a prefix of every\n * other settings address, so it only owns itself.\n */\nexport function rowForHref(href: string | null | undefined): SettingsRowId {\n const path = pathOf(href ?? '/dashboard/settings');\n let best: { id: SettingsRowId; len: number } | null = null;\n for (const row of SETTINGS_ROWS) {\n for (const p of row.paths) {\n const owns =\n path === p || (p !== '/dashboard/settings' && path.startsWith(`${p}/`));\n if (owns && (!best || p.length > best.len))\n best = { id: row.id, len: p.length };\n }\n }\n return best?.id ?? 'general';\n}\n\n/** Rows whose name or inner sections match what was typed. */\nexport function matchRows(rows: SettingsRow[], query: string): SettingsRow[] {\n const q = query.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter(\n (r) =>\n r.name.toLowerCase().includes(q) || r.keywords.some((k) => k.includes(q)),\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/settings-rows.ts"],"sourcesContent":["import {\n Bell,\n Building2,\n ChartColumn,\n CircleCheck,\n CircleHelp,\n CreditCard,\n FileText,\n Scale,\n Sparkles,\n SlidersHorizontal,\n ToggleLeft,\n UserRound,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport type { PermissionKey } from '@withpotter/lib/use-permissions';\n\n/**\n * The rows of the settings window, in the order they are drawn.\n *\n * One list for the whole window: yours and the business's, mixed, because the\n * person looking for \"Notifications\" or \"Language\" should not have to know\n * whose setting it is. Rows that leave the window (`external`) carry an arrow.\n *\n * `paths` are the addresses a row owns. A link to any of them, or to anything\n * below them (a team member, a credits page), opens the window on that row.\n * `keywords` are what search also matches: the names of the sections inside a\n * row, so \"passkeys\" or \"time zone\" finds the row that holds them.\n */\nexport type SettingsRowId =\n | 'general'\n | 'features'\n | 'account'\n | 'team'\n | 'notifications'\n | 'demo-content'\n | 'preferences'\n | 'payment-approvals'\n | 'billing'\n | 'usage'\n | 'policies'\n | 'legal';\n\nexport interface SettingsRow {\n id: SettingsRowId;\n name: string;\n icon: LucideIcon;\n paths: string[];\n keywords: string[];\n /** Hidden from a member who lacks every one of these. Empty: everyone. */\n permission?: PermissionKey[];\n /**\n * A page this row leaves the window for, where a host has its own screen for\n * the subject. The row then is a link, and the panel is never drawn.\n */\n href?: string;\n}\n\nconst BILLING_ROW: SettingsRow = {\n id: 'billing',\n name: 'Billing & plan',\n icon: CreditCard,\n paths: ['/dashboard/settings/billing'],\n keywords: ['plan', 'invoice', 'credits', 'top-up', 'cancel'],\n permission: ['canManageSettings'],\n};\n\n// A box whose card payments wait for a second person lists the decisions.\nconst PAYMENT_APPROVALS_ROW: SettingsRow = {\n id: 'payment-approvals',\n name: 'Payment approvals',\n icon: CircleCheck,\n paths: ['/dashboard/settings/payment-approvals'],\n keywords: ['approval', 'accepted', 'rejected', 'decisions', 'history'],\n};\n\nexport const SETTINGS_ROWS: SettingsRow[] = [\n {\n id: 'general',\n name: 'General',\n icon: Building2,\n paths: [\n '/dashboard/settings',\n '/dashboard/settings/branding',\n '/dashboard/settings/regional',\n ],\n keywords: ['business name', 'branding', 'logo', 'colours', 'numbering'],\n permission: ['canManageBusinessProfile', 'canManageSettings'],\n },\n {\n // The switches no business type implies: Blog today. Next to General\n // because it is the business's own setting, before the rows that are\n // about the person (Account, Preferences).\n id: 'features',\n name: 'Features',\n icon: ToggleLeft,\n paths: ['/dashboard/settings/features'],\n keywords: ['blog', 'posts', 'write', 'publish', 'switch', 'website', 'features'],\n permission: ['canManageSettings'],\n },\n {\n id: 'account',\n name: 'Account',\n icon: UserRound,\n paths: [\n '/dashboard/settings/profile',\n '/dashboard/settings/devices',\n '/dashboard/settings/data-privacy',\n ],\n keywords: [\n 'profile',\n 'name',\n 'email',\n 'photo',\n 'security',\n 'password',\n 'passkeys',\n 'two-factor',\n 'sessions',\n 'devices',\n 'privacy',\n 'export',\n 'close account',\n ],\n },\n {\n id: 'team',\n name: 'Team & permissions',\n icon: Users,\n paths: [\n '/dashboard/settings/team',\n '/dashboard/settings/roles',\n '/dashboard/settings/activity',\n ],\n keywords: ['people', 'invite', 'roles', 'activity', 'seats'],\n permission: ['canManageTeam'],\n // A box with its own team screen (its roles and invitations come from\n // its engine, which does not serve this panel's routes) names it here.\n href: process.env.NEXT_PUBLIC_BOX_TEAM_PAGE || undefined,\n },\n {\n id: 'notifications',\n name: 'Notifications',\n icon: Bell,\n paths: ['/dashboard/settings/notifications'],\n keywords: ['alerts', 'low stock', 'email sender', 'digest', 'reminders'],\n },\n {\n // Sample products, pages and pictures to start with, clear, or add again.\n // No permission gate, like the sidebar entry: everyone sees the state and\n // the card itself shows the actions only to the owner and admins.\n id: 'demo-content',\n name: 'Demo content',\n icon: Sparkles,\n paths: ['/dashboard/settings/demo-content'],\n keywords: ['sample', 'products', 'starter', 'clear', 'example data'],\n },\n {\n id: 'preferences',\n name: 'Preferences',\n icon: SlidersHorizontal,\n paths: [\n '/dashboard/settings/language',\n '/dashboard/settings/accessibility',\n ],\n keywords: [\n 'appearance',\n 'theme',\n 'dark',\n 'light',\n 'language',\n 'time zone',\n 'date format',\n 'accessibility',\n 'high contrast',\n 'reduce motion',\n 'text size',\n ],\n },\n ...(process.env.NEXT_PUBLIC_BOX_PAYMENT_APPROVALS === 'true'\n ? [PAYMENT_APPROVALS_ROW]\n : []),\n // A licensed box that bills outside the console hides the row.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_BILLING === 'true' ? [] : [BILLING_ROW]),\n // A box that meters nothing in the console leaves Usage out.\n ...(process.env.NEXT_PUBLIC_BOX_HIDE_USAGE === 'true'\n ? []\n : [\n {\n id: 'usage' as const,\n name: 'Usage',\n icon: ChartColumn,\n paths: ['/dashboard/settings/usage'],\n keywords: ['credits', 'delivery', 'spend'],\n permission: ['canManageSettings' as const],\n },\n ]),\n {\n id: 'policies',\n name: 'Policies',\n icon: Scale,\n paths: ['/dashboard/settings/policies'],\n keywords: ['returns', 'shipping', 'privacy', 'terms'],\n permission: ['canManageSettings'],\n },\n {\n id: 'legal',\n name: 'Legal',\n icon: FileText,\n paths: ['/dashboard/settings/legal'],\n keywords: ['terms', 'privacy policy', 'licences'],\n },\n];\n\n/** Where \"Get help\" goes. It leaves the window, so it is not a row above. */\nexport const HELP_HREF = '/dashboard/help';\nexport const HELP_ICON = CircleHelp;\n\nconst pathOf = (full: string) => full.split(/[?#]/)[0];\n\n/**\n * The row that owns an address: the longest of its paths that equals the\n * address or is a parent of it. General's own address is a prefix of every\n * other settings address, so it only owns itself.\n */\nexport function rowForHref(href: string | null | undefined): SettingsRowId {\n const path = pathOf(href ?? '/dashboard/settings');\n let best: { id: SettingsRowId; len: number } | null = null;\n for (const row of SETTINGS_ROWS) {\n for (const p of row.paths) {\n const owns =\n path === p || (p !== '/dashboard/settings' && path.startsWith(`${p}/`));\n if (owns && (!best || p.length > best.len))\n best = { id: row.id, len: p.length };\n }\n }\n return best?.id ?? 'general';\n}\n\n/** Rows whose name or inner sections match what was typed. */\nexport function matchRows<T extends { name: string; keywords: string[] }>(\n rows: T[],\n query: string,\n): T[] {\n const q = query.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter(\n (r) =>\n r.name.toLowerCase().includes(q) || r.keywords.some((k) => k.includes(q)),\n );\n}\n"],"mappings":"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","names":[]}
@@ -15,6 +15,7 @@ import {
15
15
  } from "@withpotter/pages-core/dashboard/settings/_components/settings-drill";
16
16
  import { usePermissions } from "@withpotter/lib/use-permissions";
17
17
  import { SettingsPanel } from "./settings-panels";
18
+ import { hostRowForHref, useSettingsHostRows } from "./settings-host";
18
19
  import {
19
20
  HELP_HREF,
20
21
  HELP_ICON,
@@ -39,12 +40,17 @@ function UnifiedSettingsModal({
39
40
  setQuery("");
40
41
  setView(null);
41
42
  }
42
- const rows = useMemo(
43
- () => SETTINGS_ROWS.filter(
43
+ const host = useSettingsHostRows();
44
+ const rows = useMemo(() => {
45
+ const own = SETTINGS_ROWS.filter(
44
46
  (r) => !r.permission || loading || r.permission.some((p) => can(p))
45
- ),
46
- [can, loading]
47
- );
47
+ );
48
+ return host.reduce((list, extra) => {
49
+ const at = list.findIndex((r) => r.id === (extra.after ?? "general"));
50
+ const into = at === -1 ? list.length : at + 1;
51
+ return [...list.slice(0, into), extra, ...list.slice(into)];
52
+ }, own);
53
+ }, [can, loading, host]);
48
54
  const shown = matchRows(rows, query);
49
55
  const drill = useMemo(
50
56
  () => ({
@@ -58,7 +64,7 @@ function UnifiedSettingsModal({
58
64
  }),
59
65
  [view]
60
66
  );
61
- const wanted = picked ?? rowForHref(href);
67
+ const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);
62
68
  const current = rows.find((r) => r.id === wanted) ?? rows[0];
63
69
  const HelpIcon = HELP_ICON;
64
70
  const [statusSlot, setStatusSlot] = useState(null);
@@ -183,7 +189,13 @@ function UnifiedSettingsModal({
183
189
  ] }),
184
190
  /* @__PURE__ */ jsx("div", { className: "mr-7 flex items-center gap-2", children: /* @__PURE__ */ jsx("div", { ref: setStatusSlot, className: "flex items-center" }) })
185
191
  ] }),
186
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none", children: current && /* @__PURE__ */ jsx(SettingsDrillProvider, { value: drill, children: /* @__PURE__ */ jsx(SettingsSurface, { variant: "plain", children: /* @__PURE__ */ jsx(SettingsStatusSlot, { element: statusSlot, children: /* @__PURE__ */ jsx(SettingsPanel, { id: current.id }, current.id) }) }) }) })
192
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none", children: current && /* @__PURE__ */ jsx(SettingsDrillProvider, { value: drill, children: /* @__PURE__ */ jsx(SettingsSurface, { variant: "plain", children: /* @__PURE__ */ jsx(SettingsStatusSlot, { element: statusSlot, children: current.render ? current.render() : /* @__PURE__ */ jsx(
193
+ SettingsPanel,
194
+ {
195
+ id: current.id
196
+ },
197
+ current.id
198
+ ) }) }) }) })
187
199
  ] })
188
200
  ]
189
201
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo, useState } from 'react';\nimport Link from 'next/link';\nimport { ArrowLeft, ArrowUpRight, Search } from 'lucide-react';\nimport { Dialog, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\nconst ROW_CLASS =\n 'flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm font-medium transition-colors';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right)\n * with every row in one list. A search at the top 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<SettingsRowId | 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 rows = useMemo(\n () =>\n SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n ),\n [can, loading],\n );\n const shown = matchRows(rows, query);\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? 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 return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n className=\"flex h-[min(78vh,640px)] max-w-[860px] gap-0 overflow-hidden p-0\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[224px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--border-subtle)] bg-[var(--bg-muted)] p-3\"\n >\n <div className=\"mb-2 flex items-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 text-[var(--text-tertiary)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-4 shrink-0\" />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <p className=\"px-3 pb-1 pt-1 text-xs font-medium text-[var(--text-tertiary)]\">\n Settings\n </p>\n {shown.length === 0 && (\n <p className=\"px-3 py-2 text-sm text-[var(--text-tertiary)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {shown.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${\n selected\n ? 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm'\n : 'text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n <div className=\"mt-auto border-t border-[var(--border-subtle)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <HelpIcon className=\"size-[18px] shrink-0\" />\n <span className=\"flex-1\">Get help</span>\n <ArrowUpRight className=\"size-4 shrink-0 text-[var(--text-tertiary)]\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"grid size-7 shrink-0 place-items-center rounded-lg text-[var(--text-secondary)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]\"\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-md font-semibold text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n </div>\n <div className=\"mr-7 flex items-center gap-2\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none\">\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n <SettingsPanel key={current.id} id={current.id} />\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\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 { useMemo, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowLeft, ArrowUpRight, Search } from 'lucide-react';\nimport { Dialog, DialogContent, DialogTitle } from '@withpotter/ui-kit/dialog';\nimport { Input } from '@withpotter/ui-kit/input';\nimport { TextMorph } from '@withpotter/ui-kit/text-morph';\nimport {\n SettingsStatusSlot,\n SettingsSurface,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-ui';\nimport {\n SettingsDrillProvider,\n type SettingsDrill,\n} from '@withpotter/pages-core/dashboard/settings/_components/settings-drill';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\nconst ROW_CLASS =\n 'flex w-full items-center gap-3 rounded-lg px-3 py-3 text-left text-sm font-medium transition-colors';\n\n/**\n * Settings, yours and the business's, in one window.\n *\n * It is the account modal's shell (a rail on the left, the page on the right)\n * with every row in one list. A search at the top matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n className=\"flex h-[min(78vh,640px)] max-w-[860px] gap-0 overflow-hidden p-0\"\n >\n <nav\n aria-label=\"Settings sections\"\n onKeyDown={(e) => {\n // Up and Down move between the rows, like the account modal's rail.\n if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;\n const rowsEls = Array.from(\n e.currentTarget.querySelectorAll<HTMLElement>('button[data-row]'),\n );\n const at = rowsEls.indexOf(document.activeElement as HTMLElement);\n if (at === -1) return;\n e.preventDefault();\n const next =\n e.key === 'ArrowDown'\n ? (at + 1) % rowsEls.length\n : (at - 1 + rowsEls.length) % rowsEls.length;\n rowsEls[next].focus();\n }}\n className=\"flex w-[224px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--border-subtle)] bg-[var(--bg-muted)] p-3\"\n >\n <div className=\"mb-2 flex items-center gap-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 text-[var(--text-tertiary)] focus-within:text-[var(--text-primary)]\">\n <Search className=\"size-4 shrink-0\" />\n <Input\n variant=\"bare\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search\"\n aria-label=\"Search settings\"\n className=\"min-w-0 flex-1\"\n />\n </div>\n <p className=\"px-3 pb-1 pt-1 text-xs font-medium text-[var(--text-tertiary)]\">\n Settings\n </p>\n {shown.length === 0 && (\n <p className=\"px-3 py-2 text-sm text-[var(--text-tertiary)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {shown.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </Link>\n );\n }\n return (\n <button\n key={r.id}\n data-row\n type=\"button\"\n aria-current={selected ? 'page' : undefined}\n onClick={() => {\n setPicked(r.id);\n setView(null);\n }}\n className={`${ROW_CLASS} ${\n selected\n ? 'bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm'\n : 'text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span className=\"truncate\">{r.name}</span>\n </button>\n );\n })}\n <div className=\"mt-auto border-t border-[var(--border-subtle)] pt-2\">\n <Link\n href={HELP_HREF}\n onClick={onClose}\n className={`${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`}\n >\n <HelpIcon className=\"size-[18px] shrink-0\" />\n <span className=\"flex-1\">Get help</span>\n <ArrowUpRight className=\"size-4 shrink-0 text-[var(--text-tertiary)]\" />\n </Link>\n </div>\n </nav>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"flex items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-4 py-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n {view && (\n <button\n type=\"button\"\n onClick={drill.back}\n aria-label={`Back to ${current?.name ?? 'Settings'}`}\n className=\"grid size-7 shrink-0 place-items-center rounded-lg text-[var(--text-secondary)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]\"\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n )}\n <DialogTitle\n aria-label={view?.title ?? current?.name ?? 'Settings'}\n className=\"truncate text-md font-semibold text-[var(--text-primary)]\"\n >\n <TextMorph as=\"span\">\n {view?.title ?? current?.name ?? 'Settings'}\n </TextMorph>\n </DialogTitle>\n </div>\n <div className=\"mr-7 flex items-center gap-2\">\n <div ref={setStatusSlot} className=\"flex items-center\" />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none\">\n {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 </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.104",
3
+ "version": "0.13.0-next.106",
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.104",
54
- "@withpotter/lib": "^0.13.0-next.104",
55
- "@withpotter/ui-kit": "^0.13.0-next.104",
53
+ "@withpotter/hooks": "^0.13.0-next.106",
54
+ "@withpotter/lib": "^0.13.0-next.106",
55
+ "@withpotter/ui-kit": "^0.13.0-next.106",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",