@withpotter/components 0.13.0-next.106 → 0.13.0-next.107
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.
- package/dist/shell/settings-groups.d.ts +23 -0
- package/dist/shell/settings-groups.js +22 -0
- package/dist/shell/settings-groups.js.map +1 -0
- package/dist/shell/settings-rows.js +11 -10
- package/dist/shell/settings-rows.js.map +1 -1
- package/dist/shell/unified-settings-modal.d.ts +3 -2
- package/dist/shell/unified-settings-modal.js +59 -32
- package/dist/shell/unified-settings-modal.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rail of the settings window is two groups: the business's settings, and the person's own. A row belongs to
|
|
3
|
+
* Business unless it is one of the few that are about the signed-in person. A host's own rows are the business's.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SettingsGroupKey: {
|
|
6
|
+
readonly Business: "business";
|
|
7
|
+
readonly You: "you";
|
|
8
|
+
};
|
|
9
|
+
export type SettingsGroupKey = (typeof SettingsGroupKey)[keyof typeof SettingsGroupKey];
|
|
10
|
+
export declare const SETTINGS_GROUP_LABEL: Record<SettingsGroupKey, string>;
|
|
11
|
+
export declare function groupOf(id: string): SettingsGroupKey;
|
|
12
|
+
export interface SettingsGroup<T> {
|
|
13
|
+
key: SettingsGroupKey;
|
|
14
|
+
label: string;
|
|
15
|
+
rows: T[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The rows split under their group labels, each group in the order the rows came in, Business first. A group with no
|
|
19
|
+
* rows (a search that matches none of them) is left out, so no label is ever drawn over nothing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function groupRows<T extends {
|
|
22
|
+
id: string;
|
|
23
|
+
}>(rows: T[]): SettingsGroup<T>[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const SettingsGroupKey = {
|
|
2
|
+
Business: "business",
|
|
3
|
+
You: "you"
|
|
4
|
+
};
|
|
5
|
+
const SETTINGS_GROUP_LABEL = {
|
|
6
|
+
[SettingsGroupKey.Business]: "Business",
|
|
7
|
+
[SettingsGroupKey.You]: "You"
|
|
8
|
+
};
|
|
9
|
+
const PERSONAL = /* @__PURE__ */ new Set(["account", "notifications", "preferences"]);
|
|
10
|
+
function groupOf(id) {
|
|
11
|
+
return PERSONAL.has(id) ? SettingsGroupKey.You : SettingsGroupKey.Business;
|
|
12
|
+
}
|
|
13
|
+
function groupRows(rows) {
|
|
14
|
+
return [SettingsGroupKey.Business, SettingsGroupKey.You].map((key) => ({ key, label: SETTINGS_GROUP_LABEL[key], rows: rows.filter((row) => groupOf(row.id) === key) })).filter((group) => group.rows.length > 0);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
SETTINGS_GROUP_LABEL,
|
|
18
|
+
SettingsGroupKey,
|
|
19
|
+
groupOf,
|
|
20
|
+
groupRows
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=settings-groups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/settings-groups.ts"],"sourcesContent":["/**\n * The rail of the settings window is two groups: the business's settings, and the person's own. A row belongs to\n * Business unless it is one of the few that are about the signed-in person. A host's own rows are the business's.\n */\nexport const SettingsGroupKey = {\n Business: 'business',\n You: 'you',\n} as const;\nexport type SettingsGroupKey = (typeof SettingsGroupKey)[keyof typeof SettingsGroupKey];\n\nexport const SETTINGS_GROUP_LABEL: Record<SettingsGroupKey, string> = {\n [SettingsGroupKey.Business]: 'Business',\n [SettingsGroupKey.You]: 'You',\n};\n\n/** The rows that are about the person, not the business. */\nconst PERSONAL = new Set(['account', 'notifications', 'preferences']);\n\nexport function groupOf(id: string): SettingsGroupKey {\n return PERSONAL.has(id) ? SettingsGroupKey.You : SettingsGroupKey.Business;\n}\n\nexport interface SettingsGroup<T> {\n key: SettingsGroupKey;\n label: string;\n rows: T[];\n}\n\n/**\n * The rows split under their group labels, each group in the order the rows came in, Business first. A group with no\n * rows (a search that matches none of them) is left out, so no label is ever drawn over nothing.\n */\nexport function groupRows<T extends { id: string }>(rows: T[]): SettingsGroup<T>[] {\n return [SettingsGroupKey.Business, SettingsGroupKey.You]\n .map((key) => ({ key, label: SETTINGS_GROUP_LABEL[key], rows: rows.filter((row) => groupOf(row.id) === key) }))\n .filter((group) => group.rows.length > 0);\n}\n"],"mappings":"AAIO,MAAM,mBAAmB;AAAA,EAC9B,UAAU;AAAA,EACV,KAAK;AACP;AAGO,MAAM,uBAAyD;AAAA,EACpE,CAAC,iBAAiB,QAAQ,GAAG;AAAA,EAC7B,CAAC,iBAAiB,GAAG,GAAG;AAC1B;AAGA,MAAM,WAAW,oBAAI,IAAI,CAAC,WAAW,iBAAiB,aAAa,CAAC;AAE7D,SAAS,QAAQ,IAA8B;AACpD,SAAO,SAAS,IAAI,EAAE,IAAI,iBAAiB,MAAM,iBAAiB;AACpE;AAYO,SAAS,UAAoC,MAA+B;AACjF,SAAO,CAAC,iBAAiB,UAAU,iBAAiB,GAAG,EACpD,IAAI,CAAC,SAAS,EAAE,KAAK,OAAO,qBAAqB,GAAG,GAAG,MAAM,KAAK,OAAO,CAAC,QAAQ,QAAQ,IAAI,EAAE,MAAM,GAAG,EAAE,EAAE,EAC7G,OAAO,CAAC,UAAU,MAAM,KAAK,SAAS,CAAC;AAC5C;","names":[]}
|
|
@@ -28,6 +28,16 @@ const PAYMENT_APPROVALS_ROW = {
|
|
|
28
28
|
paths: ["/dashboard/settings/payment-approvals"],
|
|
29
29
|
keywords: ["approval", "accepted", "rejected", "decisions", "history"]
|
|
30
30
|
};
|
|
31
|
+
const DEMO_CONTENT_ROW = {
|
|
32
|
+
// Sample products, pages and pictures to start with, clear, or add again.
|
|
33
|
+
// No permission gate, like the sidebar entry: everyone sees the state and
|
|
34
|
+
// the card itself shows the actions only to the owner and admins.
|
|
35
|
+
id: "demo-content",
|
|
36
|
+
name: "Demo content",
|
|
37
|
+
icon: Sparkles,
|
|
38
|
+
paths: ["/dashboard/settings/demo-content"],
|
|
39
|
+
keywords: ["sample", "products", "starter", "clear", "example data"]
|
|
40
|
+
};
|
|
31
41
|
const SETTINGS_ROWS = [
|
|
32
42
|
{
|
|
33
43
|
id: "general",
|
|
@@ -99,16 +109,7 @@ const SETTINGS_ROWS = [
|
|
|
99
109
|
paths: ["/dashboard/settings/notifications"],
|
|
100
110
|
keywords: ["alerts", "low stock", "email sender", "digest", "reminders"]
|
|
101
111
|
},
|
|
102
|
-
|
|
103
|
-
// Sample products, pages and pictures to start with, clear, or add again.
|
|
104
|
-
// No permission gate, like the sidebar entry: everyone sees the state and
|
|
105
|
-
// the card itself shows the actions only to the owner and admins.
|
|
106
|
-
id: "demo-content",
|
|
107
|
-
name: "Demo content",
|
|
108
|
-
icon: Sparkles,
|
|
109
|
-
paths: ["/dashboard/settings/demo-content"],
|
|
110
|
-
keywords: ["sample", "products", "starter", "clear", "example data"]
|
|
111
|
-
},
|
|
112
|
+
DEMO_CONTENT_ROW,
|
|
112
113
|
{
|
|
113
114
|
id: "preferences",
|
|
114
115
|
name: "Preferences",
|
|
@@ -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<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":[]}
|
|
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\nconst DEMO_CONTENT_ROW: SettingsRow = {\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\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 DEMO_CONTENT_ROW,\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":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings, yours and the business's, in one window.
|
|
3
3
|
*
|
|
4
|
-
* It is the account modal's shell (a rail on the left, the page on the right)
|
|
5
|
-
*
|
|
4
|
+
* It is the account modal's shell (a rail on the left, the page on the right):
|
|
5
|
+
* the business at the top of the rail with the plan it is on when the host says,
|
|
6
|
+
* then the rows in two groups, Business and You. A search matches row names and the
|
|
6
7
|
* sections inside them, so "passkeys" or "time zone" finds its row. Rows the
|
|
7
8
|
* member may not open are not drawn. Get help leaves the window, so it carries
|
|
8
9
|
* an arrow.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { ArrowLeft, ArrowUpRight, Search } from "lucide-react";
|
|
6
|
+
import { useAnimate, useReducedMotion } from "motion/react";
|
|
7
|
+
import { BusinessMark } from "@withpotter/ui-kit/business-mark";
|
|
6
8
|
import { Dialog, DialogContent, DialogTitle } from "@withpotter/ui-kit/dialog";
|
|
7
9
|
import { Input } from "@withpotter/ui-kit/input";
|
|
8
10
|
import { TextMorph } from "@withpotter/ui-kit/text-morph";
|
|
@@ -13,8 +15,11 @@ import {
|
|
|
13
15
|
import {
|
|
14
16
|
SettingsDrillProvider
|
|
15
17
|
} from "@withpotter/pages-core/dashboard/settings/_components/settings-drill";
|
|
18
|
+
import { useAuth } from "@withpotter/lib/auth-context";
|
|
16
19
|
import { usePermissions } from "@withpotter/lib/use-permissions";
|
|
17
20
|
import { SettingsPanel } from "./settings-panels";
|
|
21
|
+
import { groupRows } from "./settings-groups";
|
|
22
|
+
import { useSidebarCards } from "./sidebar-cards";
|
|
18
23
|
import { hostRowForHref, useSettingsHostRows } from "./settings-host";
|
|
19
24
|
import {
|
|
20
25
|
HELP_HREF,
|
|
@@ -68,6 +73,19 @@ function UnifiedSettingsModal({
|
|
|
68
73
|
const current = rows.find((r) => r.id === wanted) ?? rows[0];
|
|
69
74
|
const HelpIcon = HELP_ICON;
|
|
70
75
|
const [statusSlot, setStatusSlot] = useState(null);
|
|
76
|
+
const { tenant } = useAuth();
|
|
77
|
+
const planLabel = useSidebarCards()?.planLabel;
|
|
78
|
+
const name = tenant?.businessName?.trim() ?? "";
|
|
79
|
+
const [scope, animate] = useAnimate();
|
|
80
|
+
const reduced = useReducedMotion();
|
|
81
|
+
const shownKey = `${current?.id ?? ""}:${view?.key ?? ""}`;
|
|
82
|
+
const lastKey = useRef(shownKey);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (lastKey.current === shownKey) return;
|
|
85
|
+
lastKey.current = shownKey;
|
|
86
|
+
if (reduced || !scope.current) return;
|
|
87
|
+
void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });
|
|
88
|
+
}, [shownKey, reduced, animate, scope]);
|
|
71
89
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ jsxs(
|
|
72
90
|
DialogContent,
|
|
73
91
|
{
|
|
@@ -91,6 +109,13 @@ function UnifiedSettingsModal({
|
|
|
91
109
|
},
|
|
92
110
|
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",
|
|
93
111
|
children: [
|
|
112
|
+
name && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2.5 px-1 pt-0.5", children: [
|
|
113
|
+
/* @__PURE__ */ jsx(BusinessMark, { name, src: tenant?.businessLogo, size: 34 }),
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
115
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold text-[var(--text-primary)]", children: name }),
|
|
116
|
+
planLabel && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-[var(--text-tertiary)]", children: planLabel })
|
|
117
|
+
] })
|
|
118
|
+
] }),
|
|
94
119
|
/* @__PURE__ */ jsxs("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)]", children: [
|
|
95
120
|
/* @__PURE__ */ jsx(Search, { className: "size-4 shrink-0" }),
|
|
96
121
|
/* @__PURE__ */ jsx(
|
|
@@ -105,23 +130,43 @@ function UnifiedSettingsModal({
|
|
|
105
130
|
}
|
|
106
131
|
)
|
|
107
132
|
] }),
|
|
108
|
-
/* @__PURE__ */ jsx("p", { className: "px-3 pb-1 pt-1 text-xs font-medium text-[var(--text-tertiary)]", children: "Settings" }),
|
|
109
133
|
shown.length === 0 && /* @__PURE__ */ jsxs("p", { className: "px-3 py-2 text-sm text-[var(--text-tertiary)]", children: [
|
|
110
134
|
"Nothing matches \u201C",
|
|
111
135
|
query,
|
|
112
136
|
"\u201D."
|
|
113
137
|
] }),
|
|
114
|
-
shown.map((
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
groupRows(shown).map((group) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
139
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 pb-1 pt-3 text-xs font-medium text-[var(--text-tertiary)]", children: group.label }),
|
|
140
|
+
group.rows.map((r) => {
|
|
141
|
+
const selected = r.id === current?.id;
|
|
142
|
+
const Icon = r.icon;
|
|
143
|
+
if (r.href) {
|
|
144
|
+
return /* @__PURE__ */ jsxs(
|
|
145
|
+
Link,
|
|
146
|
+
{
|
|
147
|
+
"data-row": true,
|
|
148
|
+
href: r.href,
|
|
149
|
+
onClick: onClose,
|
|
150
|
+
className: `${ROW_CLASS} text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]`,
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
|
|
153
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: r.name })
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
r.id
|
|
157
|
+
);
|
|
158
|
+
}
|
|
118
159
|
return /* @__PURE__ */ jsxs(
|
|
119
|
-
|
|
160
|
+
"button",
|
|
120
161
|
{
|
|
121
162
|
"data-row": true,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
163
|
+
type: "button",
|
|
164
|
+
"aria-current": selected ? "page" : void 0,
|
|
165
|
+
onClick: () => {
|
|
166
|
+
setPicked(r.id);
|
|
167
|
+
setView(null);
|
|
168
|
+
},
|
|
169
|
+
className: `${ROW_CLASS} ${selected ? "bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm" : "text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]"}`,
|
|
125
170
|
children: [
|
|
126
171
|
/* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
|
|
127
172
|
/* @__PURE__ */ jsx("span", { className: "truncate", children: r.name })
|
|
@@ -129,26 +174,8 @@ function UnifiedSettingsModal({
|
|
|
129
174
|
},
|
|
130
175
|
r.id
|
|
131
176
|
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
"button",
|
|
135
|
-
{
|
|
136
|
-
"data-row": true,
|
|
137
|
-
type: "button",
|
|
138
|
-
"aria-current": selected ? "page" : void 0,
|
|
139
|
-
onClick: () => {
|
|
140
|
-
setPicked(r.id);
|
|
141
|
-
setView(null);
|
|
142
|
-
},
|
|
143
|
-
className: `${ROW_CLASS} ${selected ? "bg-[var(--bg-base)] text-[var(--text-primary)] shadow-sm" : "text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)] hover:text-[var(--text-primary)]"}`,
|
|
144
|
-
children: [
|
|
145
|
-
/* @__PURE__ */ jsx(Icon, { className: "size-[18px] shrink-0" }),
|
|
146
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: r.name })
|
|
147
|
-
]
|
|
148
|
-
},
|
|
149
|
-
r.id
|
|
150
|
-
);
|
|
151
|
-
}),
|
|
177
|
+
})
|
|
178
|
+
] }, group.key)),
|
|
152
179
|
/* @__PURE__ */ jsx("div", { className: "mt-auto border-t border-[var(--border-subtle)] pt-2", children: /* @__PURE__ */ jsxs(
|
|
153
180
|
Link,
|
|
154
181
|
{
|
|
@@ -189,13 +216,13 @@ function UnifiedSettingsModal({
|
|
|
189
216
|
] }),
|
|
190
217
|
/* @__PURE__ */ jsx("div", { className: "mr-7 flex items-center gap-2", children: /* @__PURE__ */ jsx("div", { ref: setStatusSlot, className: "flex items-center" }) })
|
|
191
218
|
] }),
|
|
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(
|
|
219
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-1 py-4 outline-none", children: /* @__PURE__ */ jsx("div", { ref: scope, 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
220
|
SettingsPanel,
|
|
194
221
|
{
|
|
195
222
|
id: current.id
|
|
196
223
|
},
|
|
197
224
|
current.id
|
|
198
|
-
) }) }) }) })
|
|
225
|
+
) }) }) }) }) })
|
|
199
226
|
] })
|
|
200
227
|
]
|
|
201
228
|
}
|
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/unified-settings-modal.tsx"],"sourcesContent":["'use client';\n\nimport { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport Link from 'next/link';\nimport { ArrowLeft, ArrowUpRight, Search } from 'lucide-react';\nimport { useAnimate, useReducedMotion } from 'motion/react';\nimport { BusinessMark } from '@withpotter/ui-kit/business-mark';\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 { useAuth } from '@withpotter/lib/auth-context';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { SettingsPanel } from './settings-panels';\nimport { groupRows } from './settings-groups';\nimport { useSidebarCards } from './sidebar-cards';\nimport { hostRowForHref, useSettingsHostRows } from './settings-host';\nimport {\n HELP_HREF,\n HELP_ICON,\n SETTINGS_ROWS,\n matchRows,\n rowForHref,\n type SettingsRowId,\n} from './settings-rows';\n\n/** A row of the window: the shared ones, or one a host added with the screen to draw. */\ntype ShownRow = Omit<(typeof SETTINGS_ROWS)[number], 'id'> & {\n id: string;\n render?: () => ReactNode;\n};\n\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 * the business at the top of the rail with the plan it is on when the host says,\n * then the rows in two groups, Business and You. A search matches row names and the\n * sections inside them, so \"passkeys\" or \"time zone\" finds its row. Rows the\n * member may not open are not drawn. Get help leaves the window, so it carries\n * an arrow.\n *\n * It replaces the window that showed each page in a frame: the pages are drawn\n * here directly, so there is no second copy of the app running behind it.\n */\nexport function UnifiedSettingsModal({\n open,\n href,\n onClose,\n}: {\n open: boolean;\n /** A settings address to open on. A row's own page when it owns it. */\n href?: string | null;\n onClose: () => void;\n}) {\n const { can, loading } = usePermissions();\n const [query, setQuery] = useState('');\n // A level inside the open row (a policy's editor). Cleared when the row, or\n // the address the window was opened on, changes.\n const [view, setView] = useState<SettingsDrill['view']>(null);\n // The row chosen inside the window, and the address it was opened on, so\n // opening on a different address moves the selection (adjusted in render).\n const [picked, setPicked] = useState<string | null>(null);\n const [seen, setSeen] = useState<string | null | undefined>(undefined);\n if (open && seen !== href) {\n setSeen(href);\n setPicked(null);\n setQuery('');\n setView(null);\n }\n\n const host = useSettingsHostRows();\n const rows = useMemo(() => {\n const own: ShownRow[] = SETTINGS_ROWS.filter(\n (r) => !r.permission || loading || r.permission.some((p) => can(p)),\n );\n\n // A host's rows go where they say, or after General.\n return host.reduce<ShownRow[]>((list, extra) => {\n const at = list.findIndex((r) => r.id === (extra.after ?? 'general'));\n\n const into = at === -1 ? list.length : at + 1;\n\n return [...list.slice(0, into), extra, ...list.slice(into)];\n }, own);\n }, [can, loading, host]);\n const shown = matchRows(rows, query);\n\n const drill = useMemo<SettingsDrill>(\n () => ({\n view,\n open: (key, title) => setView({ key, title }),\n openRow: (row, key, title) => {\n setPicked(row as SettingsRowId);\n setView({ key, title });\n },\n back: () => setView(null),\n }),\n [view],\n );\n\n const wanted = picked ?? hostRowForHref(host, href)?.id ?? rowForHref(href);\n // An address that names a row the member cannot see falls to their first.\n const current = rows.find((r) => r.id === wanted) ?? rows[0];\n const HelpIcon = HELP_ICON;\n\n const [statusSlot, setStatusSlot] = useState<HTMLElement | null>(null);\n\n // The business the window is about, and the plan it is on when the host says (Olive does; other products say nothing).\n const { tenant } = useAuth();\n const planLabel = useSidebarCards()?.planLabel;\n const name = tenant?.businessName?.trim() ?? '';\n\n // Moving to another row, or into and out of a level inside one, brings the content in: it fades up from 8px below. The\n // content itself is not remounted, so a screen keeps its own state and scroll; only its wrapper moves.\n const [scope, animate] = useAnimate<HTMLDivElement>();\n const reduced = useReducedMotion();\n const shownKey = `${current?.id ?? ''}:${view?.key ?? ''}`;\n const lastKey = useRef(shownKey);\n\n useEffect(() => {\n if (lastKey.current === shownKey) return;\n\n lastKey.current = shownKey;\n\n if (reduced || !scope.current) return;\n\n void animate(scope.current, { opacity: [0, 1], y: [8, 0] }, { duration: 0.22, ease: [0.22, 1, 0.36, 1] });\n }, [shownKey, reduced, animate, scope]);\n\n return (\n <Dialog open={open} onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n aria-label=\"Settings\"\n 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 {name && (\n <div className=\"mb-3 flex items-center gap-2.5 px-1 pt-0.5\">\n <BusinessMark name={name} src={tenant?.businessLogo} size={34} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-[var(--text-primary)]\">\n {name}\n </p>\n {planLabel && (\n <p className=\"truncate text-xs text-[var(--text-tertiary)]\">\n {planLabel}\n </p>\n )}\n </div>\n </div>\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 {shown.length === 0 && (\n <p className=\"px-3 py-2 text-sm text-[var(--text-tertiary)]\">\n Nothing matches “{query}”.\n </p>\n )}\n {groupRows(shown).map((group) => (\n <Fragment key={group.key}>\n <p className=\"px-3 pb-1 pt-3 text-xs font-medium text-[var(--text-tertiary)]\">\n {group.label}\n </p>\n {group.rows.map((r) => {\n const selected = r.id === current?.id;\n const Icon = r.icon;\n if (r.href) {\n return (\n <Link\n key={r.id}\n data-row\n href={r.href}\n onClick={onClose}\n className={`${ROW_CLASS} 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 </Fragment>\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 <div ref={scope}>\n {current && (\n <SettingsDrillProvider value={drill}>\n <SettingsSurface variant=\"plain\">\n <SettingsStatusSlot element={statusSlot}>\n {current.render ? (\n current.render()\n ) : (\n <SettingsPanel\n key={current.id}\n id={current.id as SettingsRowId}\n />\n )}\n </SettingsStatusSlot>\n </SettingsSurface>\n </SettingsDrillProvider>\n )}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/components",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.107",
|
|
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.
|
|
54
|
-
"@withpotter/lib": "^0.13.0-next.
|
|
55
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
53
|
+
"@withpotter/hooks": "^0.13.0-next.107",
|
|
54
|
+
"@withpotter/lib": "^0.13.0-next.107",
|
|
55
|
+
"@withpotter/ui-kit": "^0.13.0-next.107",
|
|
56
56
|
"@xyflow/react": "^12.11.2",
|
|
57
57
|
"dompurify": "^3.3.1",
|
|
58
58
|
"jsqr": "1.4.0",
|