@eifi1/ui-kit 0.50.0 → 0.51.0
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/README.md +14 -12
- package/dist/admin/user-roster.js +31 -12
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/verify-email.d.ts +4 -0
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +9 -0
- package/dist/components/alert-banner.js +9 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/autocomplete.js +13 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -19
- package/dist/components/button-look.js +3 -30
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox.js +3 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +1 -0
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/field-base.d.ts +31 -1
- package/dist/components/field-base.js +28 -0
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/floating-panel.d.ts +6 -13
- package/dist/components/floating-panel.js +2 -9
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/icon-button.js +3 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +12 -0
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/select.js +3 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/stat-tile.d.ts +8 -1
- package/dist/components/stat-tile.js +7 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/german.js +2 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +2 -0
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +2 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +2 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +2 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +2 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +2 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/page-seo.d.ts +19 -11
- package/dist/landing/page-seo.js +11 -7
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/landing/public-page.d.ts +57 -0
- package/dist/landing/public-page.js +45 -0
- package/dist/landing/public-page.js.map +1 -0
- package/dist/lib/quiet-focus.d.ts +4 -0
- package/dist/lib/quiet-focus.js +19 -0
- package/dist/lib/quiet-focus.js.map +1 -0
- package/dist/lib/server-wake.d.ts +3 -2
- package/dist/lib/server-wake.js +19 -13
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -15
- package/dist/shell/app-shell.js +7 -26
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +98 -0
- package/dist/shell/dev-menu.js +133 -0
- package/dist/shell/dev-menu.js.map +1 -0
- package/dist/shell/dev-switchers.d.ts +6 -53
- package/dist/shell/dev-switchers.js +5 -100
- package/dist/shell/dev-switchers.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +4 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +18 -9
- package/dist/shell/top-bar-parts-labels.js +2 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +17 -2
- package/dist/shell/topbar-action-menu.js +3 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +3 -1
- package/dist/shell.js +3 -1
- package/dist/shell.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/tour.d.ts +31 -0
- package/dist/testing/tour.js +12 -0
- package/dist/testing/tour.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +0 -3
- package/dist/theme/palette-presets.js +0 -2
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour-engine.d.ts +12 -0
- package/dist/tour/tour-engine.js +31 -0
- package/dist/tour/tour-engine.js.map +1 -0
- package/dist/tour/tour.d.ts +14 -3
- package/dist/tour/tour.js +60 -45
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -3
- package/dist/tour/use-tour-url-sync.js +21 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/user-roster.tsx +30 -10
- package/src/auth/verify-email.tsx +4 -0
- package/src/components/alert-banner.tsx +37 -10
- package/src/components/autocomplete.tsx +18 -0
- package/src/components/button-look.ts +7 -57
- package/src/components/combobox.tsx +5 -1
- package/src/components/data-table-fit.ts +4 -1
- package/src/components/data-table-virtual.ts +14 -1
- package/src/components/field-base.tsx +53 -1
- package/src/components/floating-panel.tsx +6 -28
- package/src/components/icon-button.tsx +6 -8
- package/src/components/inline-entity-combobox.tsx +17 -0
- package/src/components/select.tsx +3 -6
- package/src/components/stat-tile.tsx +27 -9
- package/src/i18n/german.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -2
- package/src/i18n/locales/es.ts +2 -0
- package/src/i18n/locales/fr.ts +2 -0
- package/src/i18n/locales/hu.ts +2 -0
- package/src/i18n/locales/it.ts +2 -0
- package/src/i18n/locales/zh.ts +2 -0
- package/src/index.ts +4 -1
- package/src/landing/page-seo.ts +28 -16
- package/src/landing/public-page.tsx +106 -0
- package/src/lib/quiet-focus.ts +32 -0
- package/src/lib/server-wake.ts +41 -17
- package/src/shell/app-shell.tsx +11 -48
- package/src/shell/dev-menu.tsx +259 -0
- package/src/shell/dev-switchers.tsx +13 -174
- package/src/shell/top-bar-actions.tsx +4 -1
- package/src/shell/top-bar-parts-labels.ts +20 -9
- package/src/shell/topbar-action-menu.tsx +28 -2
- package/src/shell.ts +5 -1
- package/src/testing/index.ts +8 -0
- package/src/testing/tour.ts +41 -0
- package/src/theme/palette-presets.ts +0 -4
- package/src/tour/tour-engine.ts +41 -0
- package/src/tour/tour.tsx +64 -24
- package/src/tour/use-tour-url-sync.ts +33 -4
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { FlaskConical, Undo2 } from "lucide-react";
|
|
5
|
+
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
6
|
+
import { TopBarActionMenu } from "./topbar-action-menu.js";
|
|
7
|
+
import { useTopBarPartsLabels } from "./top-bar-parts-labels.js";
|
|
8
|
+
const DEV_ORIGINAL_ROLE_KEY = "ui-kit:dev-original-role";
|
|
9
|
+
function readKey(key) {
|
|
10
|
+
try {
|
|
11
|
+
return sessionStorage.getItem(key);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeKey(key, value) {
|
|
17
|
+
try {
|
|
18
|
+
if (value === null) sessionStorage.removeItem(key);
|
|
19
|
+
else sessionStorage.setItem(key, value);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const flask = /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-warning" });
|
|
24
|
+
const textOf = (label, value) => typeof label === "string" || typeof label === "number" ? String(label) : value;
|
|
25
|
+
const isRow = (entry) => entry.kind !== "heading" && entry.kind !== "divider";
|
|
26
|
+
function DevMenu({
|
|
27
|
+
enabled,
|
|
28
|
+
roles,
|
|
29
|
+
currentRole: role,
|
|
30
|
+
onRole,
|
|
31
|
+
plans,
|
|
32
|
+
currentPlan: plan,
|
|
33
|
+
onPlan,
|
|
34
|
+
originalRole,
|
|
35
|
+
storageKey = DEV_ORIGINAL_ROLE_KEY,
|
|
36
|
+
billingOff = false,
|
|
37
|
+
roleLabel = (r) => r,
|
|
38
|
+
planLabel = (p) => p,
|
|
39
|
+
labels: labelsProp,
|
|
40
|
+
sections = [],
|
|
41
|
+
active = false
|
|
42
|
+
}) {
|
|
43
|
+
const bar = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS);
|
|
44
|
+
const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : void 0).dev;
|
|
45
|
+
const [kept, setKept] = useState(() => enabled ? readKey(storageKey) : null);
|
|
46
|
+
const [wasEnabled, setWasEnabled] = useState(enabled);
|
|
47
|
+
if (wasEnabled !== enabled) {
|
|
48
|
+
setWasEnabled(enabled);
|
|
49
|
+
if (enabled) setKept(readKey(storageKey));
|
|
50
|
+
}
|
|
51
|
+
if (!enabled) return null;
|
|
52
|
+
const original = originalRole ?? kept ?? void 0;
|
|
53
|
+
const pickRole = (next) => {
|
|
54
|
+
if (originalRole === void 0) {
|
|
55
|
+
if (next === original) {
|
|
56
|
+
writeKey(storageKey, null);
|
|
57
|
+
setKept(null);
|
|
58
|
+
} else if (kept === null && role !== void 0 && next !== role) {
|
|
59
|
+
writeKey(storageKey, role);
|
|
60
|
+
setKept(role);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
onRole?.(next);
|
|
64
|
+
};
|
|
65
|
+
const roleGroup = roles && roles.length > 0 && role !== void 0 && onRole ? [
|
|
66
|
+
{ kind: "heading", key: "head-role", label: words.roleHeading },
|
|
67
|
+
...roles.map(
|
|
68
|
+
(r) => ({ key: `role-${r}`, label: roleLabel(r), checked: r === role, onSelect: () => pickRole(r) })
|
|
69
|
+
),
|
|
70
|
+
// Behind a rule, which ends the role group: "Back to" is a way out, not a role.
|
|
71
|
+
...original !== void 0 && original !== role ? [
|
|
72
|
+
{ kind: "divider", key: "sep-back" },
|
|
73
|
+
{
|
|
74
|
+
key: "back",
|
|
75
|
+
icon: /* @__PURE__ */ jsx(Undo2, { className: "size-4" }),
|
|
76
|
+
label: words.backTo(textOf(roleLabel(original), original)),
|
|
77
|
+
onSelect: () => pickRole(original)
|
|
78
|
+
}
|
|
79
|
+
] : []
|
|
80
|
+
] : [];
|
|
81
|
+
const planGroup = plans && plans.length > 0 && onPlan ? [
|
|
82
|
+
{
|
|
83
|
+
kind: "heading",
|
|
84
|
+
key: "head-plan",
|
|
85
|
+
// The billing-off note is part of the group's NAME, under the heading. In 0.42's
|
|
86
|
+
// own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed
|
|
87
|
+
// over it and a screen reader moving through the menu never reached it. Named,
|
|
88
|
+
// it is read on the way into the plans. And in one menu a trailing note would
|
|
89
|
+
// sit under the app's sections, not under the plans.
|
|
90
|
+
label: billingOff ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
+
words.planHeading,
|
|
92
|
+
" ",
|
|
93
|
+
/* @__PURE__ */ jsx(
|
|
94
|
+
"span",
|
|
95
|
+
{
|
|
96
|
+
"data-dev-billing-off": "",
|
|
97
|
+
className: "mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted",
|
|
98
|
+
children: words.billingOff
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] }) : words.planHeading
|
|
102
|
+
},
|
|
103
|
+
...plans.map(
|
|
104
|
+
(p) => ({ key: `plan-${p}`, label: planLabel(p), checked: p === plan, onSelect: () => onPlan(p) })
|
|
105
|
+
)
|
|
106
|
+
] : [];
|
|
107
|
+
const appGroups = sections.flatMap((section) => [
|
|
108
|
+
{ kind: "heading", key: `section:${section.key}`, label: section.title },
|
|
109
|
+
...section.entries.map((entry) => ({ ...entry, key: `${section.key}:${entry.key}` }))
|
|
110
|
+
]);
|
|
111
|
+
const entries = [...roleGroup, ...planGroup, ...appGroups];
|
|
112
|
+
if (!entries.some(isRow)) return null;
|
|
113
|
+
const name = [
|
|
114
|
+
words.menu,
|
|
115
|
+
roleGroup.length > 0 && role !== void 0 ? bar.role(role) : null,
|
|
116
|
+
planGroup.length > 0 && plan !== void 0 ? words.plan(plan) : null
|
|
117
|
+
].filter((part) => part !== null).join(" \xB7 ");
|
|
118
|
+
return /* @__PURE__ */ jsx(
|
|
119
|
+
TopBarActionMenu,
|
|
120
|
+
{
|
|
121
|
+
icon: flask,
|
|
122
|
+
iconBadge: active ? { label: words.overridesOn } : null,
|
|
123
|
+
"aria-label": name,
|
|
124
|
+
entries,
|
|
125
|
+
scrollable: true
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
DEV_ORIGINAL_ROLE_KEY,
|
|
131
|
+
DevMenu
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=dev-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/dev-menu.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { FlaskConical, Undo2 } from \"lucide-react\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type DevSwitchersLabels } from \"./top-bar-parts-labels\";\n\n/** Where the role the session started with is kept, unless the app names its own key. */\nexport const DEV_ORIGINAL_ROLE_KEY = \"ui-kit:dev-original-role\";\n\nfunction readKey(key: string): string | null {\n try {\n return sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeKey(key: string, value: string | null): void {\n try {\n if (value === null) sessionStorage.removeItem(key);\n else sessionStorage.setItem(key, value);\n } catch {\n /* storage blocked: \"Back to\" lasts as long as this page */\n }\n}\n\n/**\n * One of the app's own groups in the {@link DevMenu} (docs/top-bar-harmonization.md\n * §2.7.1): keksdose's budget role, its \"today\" override, its resets.\n *\n * The rows are plain {@link TopBarMenuEntry}s, and there is no dev item type beside them\n * (the contract with keksdose, 0.51): a choice of one is radio rows (`checked`), a toggle\n * is a `checkable: \"checkbox\"` row, an action is a plain row. No form fields — a menu is\n * a `role=\"menu\"`, where a text or date input has no place in the arrow keys; a free\n * value is a \"Custom…\" row that opens the app's own dialog.\n */\nexport interface DevMenuSection {\n /** Unique among the sections. The rows' keys need be unique only within theirs: the\n * menu prefixes them with this. */\n key: string;\n /** The group's heading, drawn above its rows and announced as the group's name. */\n title: ReactNode;\n /** The rows, as `TopBarActionMenu` takes them. A `divider` ends the group, as there. */\n entries: readonly TopBarMenuEntry[];\n}\n\nexport interface DevMenuProps<R extends string, P extends string> {\n /**\n * Draw anything at all. The app passes `import.meta.env.DEV` AND its probe of the dev\n * route (§2.7.1): the constant keeps the menu off a production build's screen, the\n * probe off a dev build whose server has the route shut (§6.1) — flasks with nothing\n * behind them. Neither takes code out of the bundle; see `sections`.\n */\n enabled: boolean;\n roles?: readonly R[];\n /** The role the server enforces now. Named `currentRole`, not `role`, because `role` on\n * a JSX element is the ARIA role (as `RoleChip`'s `value`). */\n currentRole?: R;\n /** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads\n * `/auth/me` (§2.7, §6.2). */\n onRole?: (role: R) => void;\n plans?: readonly P[];\n /** The plan the server enforces now. Left out (not known yet — kastlan's overview is a\n * 404 while billing is off), the plan group still renders, with no row checked; a\n * `GET /dev/switch` answers it. */\n currentPlan?: P;\n /** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */\n onPlan?: (plan: P) => void;\n /**\n * The role to offer \"Back to {role}\" for. Left out, the kit keeps it itself: the role\n * before the first switch of this browser session, in `sessionStorage` under\n * `storageKey` — the server answers with the NEW role after a switch, so only the\n * browser still knows the old one.\n */\n originalRole?: R;\n /** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};\n * an app with several accounts on one browser may scope it per user. */\n storageKey?: string;\n /** Billing is off locally: the plan group says plans change nothing, and stays usable\n * (§6.3). */\n billingOff?: boolean;\n /** A role's or a plan's words in the menu. Default: the value itself. */\n roleLabel?: (role: R) => ReactNode;\n planLabel?: (plan: P) => ReactNode;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */\n labels?: LabelOverride<DevSwitchersLabels>;\n /** 0.51: the app's own dev groups, after the kit's role and plan, in this order. Their\n * DEFINITIONS belong in a module only a dev build imports (§2.7.1): `enabled={false}`\n * hides the menu, but the code of a section built inline stays in the bundle. */\n sections?: readonly DevMenuSection[];\n /**\n * 0.51: an override is on that the developer could forget — a \"today\" that is not\n * today, a simulated slow server. The flask wears a dot, and its name says so (\"Dev\n * tools · Role: ADMIN · Plan: free Overrides on\"), since a dot alone tells a screen\n * reader nothing. The app decides what counts; the kit cannot see into its sections.\n */\n active?: boolean;\n}\n\nconst flask = <FlaskConical className=\"size-5 text-warning\" />;\n\n/** A label's text for a sentence, or the raw value when the label is not text. */\nconst textOf = (label: ReactNode, value: string) =>\n typeof label === \"string\" || typeof label === \"number\" ? String(label) : value;\n\n/** A row someone can pick — what makes a menu worth drawing. */\nconst isRow = (entry: TopBarMenuEntry) => entry.kind !== \"heading\" && entry.kind !== \"divider\";\n\n/**\n * The top bar's local-dev menu (docs/top-bar-harmonization.md §2.7, §2.7.1, §6.2, §6.3):\n * ONE amber flask, `RoleSwitcher`'s, over the kit's Account section and the app's own.\n *\n * 0.42's `DevSwitchers` drew a flask per switcher and had no place for anything else,\n * and an app has more dev controls than a role and a plan — keksdose's budget role, a\n * \"today\" override, resets (agreed with keksdose for 0.51). So the kit's part is now the\n * menu, and the app's rows go in it.\n *\n * - **The Account section** comes first: the role group, ending in \"Back to {role}\" once\n * the role differs from the one the session started with (the dev route needs only a\n * signed-in user, so a switch to a lesser role can always switch back); then the plan\n * group, which with `billingOff` notes that plans change nothing. Each renders only\n * with its list and handler.\n * - **The app's `sections`** follow, each a group under its own heading.\n * - **`active`** puts a dot on the flask for an override that is easy to forget.\n *\n * Nothing renders unless `enabled`, and nothing with no row to show. The panel scrolls\n * (`TopBarActionMenu`'s `scrollable`): some 25 rows ran off a 390×844 phone.\n */\nexport function DevMenu<R extends string, P extends string>({\n enabled,\n roles,\n currentRole: role,\n onRole,\n plans,\n currentPlan: plan,\n onPlan,\n originalRole,\n storageKey = DEV_ORIGINAL_ROLE_KEY,\n billingOff = false,\n roleLabel = (r) => r,\n planLabel = (p) => p,\n labels: labelsProp,\n sections = [],\n active = false,\n}: DevMenuProps<R, P>) {\n const bar = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;\n const [kept, setKept] = useState<string | null>(() => (enabled ? readKey(storageKey) : null));\n // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the\n // dev route — so the kept role is read again then, or \"Back to {role}\" is lost after a\n // reload. Adjusted during render, React's pattern for state that follows a prop.\n const [wasEnabled, setWasEnabled] = useState(enabled);\n if (wasEnabled !== enabled) {\n setWasEnabled(enabled);\n if (enabled) setKept(readKey(storageKey));\n }\n if (!enabled) return null;\n\n const original = (originalRole ?? kept ?? undefined) as R | undefined;\n\n const pickRole = (next: R) => {\n if (originalRole === undefined) {\n if (next === original) {\n // Back where the session began: nothing left to go back to.\n writeKey(storageKey, null);\n setKept(null);\n } else if (kept === null && role !== undefined && next !== role) {\n writeKey(storageKey, role);\n setKept(role);\n }\n }\n onRole?.(next);\n };\n\n const roleGroup: TopBarMenuEntry[] =\n roles && roles.length > 0 && role !== undefined && onRole\n ? [\n { kind: \"heading\", key: \"head-role\", label: words.roleHeading },\n ...roles.map(\n (r): TopBarMenuEntry => ({ key: `role-${r}`, label: roleLabel(r), checked: r === role, onSelect: () => pickRole(r) }),\n ),\n // Behind a rule, which ends the role group: \"Back to\" is a way out, not a role.\n ...(original !== undefined && original !== role\n ? ([\n { kind: \"divider\", key: \"sep-back\" },\n {\n key: \"back\",\n icon: <Undo2 className=\"size-4\" />,\n label: words.backTo(textOf(roleLabel(original), original)),\n onSelect: () => pickRole(original),\n },\n ] satisfies TopBarMenuEntry[])\n : []),\n ]\n : [];\n\n const planGroup: TopBarMenuEntry[] =\n plans && plans.length > 0 && onPlan\n ? [\n {\n kind: \"heading\",\n key: \"head-plan\",\n // The billing-off note is part of the group's NAME, under the heading. In 0.42's\n // own plan menu it was a trailing `<li>`: not a row, so the arrow keys passed\n // over it and a screen reader moving through the menu never reached it. Named,\n // it is read on the way into the plans. And in one menu a trailing note would\n // sit under the app's sections, not under the plans.\n label: billingOff ? (\n <>\n {words.planHeading}{\" \"}\n <span\n data-dev-billing-off=\"\"\n className=\"mt-0.5 block font-normal normal-case tracking-normal text-xs text-muted\"\n >\n {words.billingOff}\n </span>\n </>\n ) : (\n words.planHeading\n ),\n },\n ...plans.map(\n (p): TopBarMenuEntry => ({ key: `plan-${p}`, label: planLabel(p), checked: p === plan, onSelect: () => onPlan(p) }),\n ),\n ]\n : [];\n\n // The app's rows keyed under their section: two sections may well both have a \"reset\".\n const appGroups = sections.flatMap((section): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `section:${section.key}`, label: section.title },\n ...section.entries.map((entry): TopBarMenuEntry => ({ ...entry, key: `${section.key}:${entry.key}` })),\n ]);\n\n const entries = [...roleGroup, ...planGroup, ...appGroups];\n if (!entries.some(isRow)) return null;\n\n // The menu, then the role and the plan, as 0.42's two triggers named them: the bar\n // shows the flask only, so its name (and tooltip) is where they are read.\n const name = [\n words.menu,\n roleGroup.length > 0 && role !== undefined ? bar.role(role) : null,\n planGroup.length > 0 && plan !== undefined ? words.plan(plan) : null,\n ]\n .filter((part): part is string => part !== null)\n .join(\" · \");\n\n return (\n <TopBarActionMenu\n icon={flask}\n // The badge's words join the trigger's name, as the tours compass's NEW dot does.\n // `danger`, the badge's default: an amber dot would vanish into the amber flask.\n iconBadge={active ? { label: words.overridesOn } : null}\n aria-label={name}\n entries={entries}\n scrollable\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,53 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
/** Draw anything at all: the app passes `import.meta.env.DEV`, so a production build
|
|
8
|
-
* tree-shakes the switchers with the constant. */
|
|
9
|
-
enabled: boolean;
|
|
10
|
-
roles?: readonly R[];
|
|
11
|
-
/** The role the server enforces now. Named `currentRole`, not `role`, because `role` on
|
|
12
|
-
* a JSX element is the ARIA role (as `RoleChip`'s `value`). */
|
|
13
|
-
currentRole?: R;
|
|
14
|
-
/** Apply a role — the app calls server-kit's `POST /dev/switch {role}` and re-reads
|
|
15
|
-
* `/auth/me` (§2.7, §6.2). */
|
|
16
|
-
onRole?: (role: R) => void;
|
|
17
|
-
plans?: readonly P[];
|
|
18
|
-
/** The plan the server enforces now. Left out (not known yet — kastlan's overview is a
|
|
19
|
-
* 404 while billing is off), the plan menu still renders, with no row checked; a
|
|
20
|
-
* `GET /dev/switch` answers it. */
|
|
21
|
-
currentPlan?: P;
|
|
22
|
-
/** Apply a plan — `POST /dev/switch {plan}`, then the overview (§6.3). */
|
|
23
|
-
onPlan?: (plan: P) => void;
|
|
24
|
-
/**
|
|
25
|
-
* The role to offer "Back to {role}" for. Left out, the kit keeps it itself: the role
|
|
26
|
-
* before the first switch of this browser session, in `sessionStorage` under
|
|
27
|
-
* `storageKey` — the server answers with the NEW role after a switch, so only the
|
|
28
|
-
* browser still knows the old one.
|
|
29
|
-
*/
|
|
30
|
-
originalRole?: R;
|
|
31
|
-
/** The `sessionStorage` key for the kept role. Default {@link DEV_ORIGINAL_ROLE_KEY};
|
|
32
|
-
* an app with several accounts on one browser may scope it per user. */
|
|
33
|
-
storageKey?: string;
|
|
34
|
-
/** Billing is off locally: the plan menu says plans change nothing, and stays usable
|
|
35
|
-
* (§6.3). */
|
|
36
|
-
billingOff?: boolean;
|
|
37
|
-
/** A role's or a plan's words in the menu. Default: the value itself. */
|
|
38
|
-
roleLabel?: (role: R) => ReactNode;
|
|
39
|
-
planLabel?: (plan: P) => ReactNode;
|
|
40
|
-
/** Prop > `<UiKitProvider labels={{ topBarParts: { dev } }}>` > English. */
|
|
41
|
-
labels?: LabelOverride<DevSwitchersLabels>;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The top bar's local-dev switchers (docs/top-bar-harmonization.md §2.7, §6.2, §6.3): the
|
|
45
|
-
* role, then the plan, each a menu behind `RoleSwitcher`'s amber flask. Nothing renders
|
|
46
|
-
* unless `enabled`; either menu renders only with its list and handler.
|
|
47
|
-
*
|
|
48
|
-
* The role menu ends in "Back to {role}" once the role differs from the one the session
|
|
49
|
-
* started with: the dev route needs only a signed-in user, so a switch to a lesser role
|
|
50
|
-
* can always switch back. With `billingOff` the plan menu notes that plans change nothing
|
|
51
|
-
* while billing is off.
|
|
52
|
-
*/
|
|
53
|
-
export declare function DevSwitchers<R extends string, P extends string>({ enabled, roles, currentRole: role, onRole, plans, currentPlan: plan, onPlan, originalRole, storageKey, billingOff, roleLabel, planLabel, labels: labelsProp, }: DevSwitchersProps<R, P>): import("react").JSX.Element | null;
|
|
1
|
+
import { type DevMenuProps } from "./dev-menu.js";
|
|
2
|
+
export { DEV_ORIGINAL_ROLE_KEY } from "./dev-menu.js";
|
|
3
|
+
/** @deprecated 0.51 — use {@link DevMenuProps} (the same props); removed in 0.52. */
|
|
4
|
+
export type DevSwitchersProps<R extends string, P extends string> = Omit<DevMenuProps<R, P>, "sections" | "active">;
|
|
5
|
+
/** @deprecated 0.51 — use {@link DevMenu} (one flask; same props); removed in 0.52. */
|
|
6
|
+
export declare function DevSwitchers<R extends string, P extends string>(props: DevSwitchersProps<R, P>): import("react").JSX.Element;
|
|
@@ -1,104 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { useTopBarPartsLabels } from "./top-bar-parts-labels.js";
|
|
8
|
-
const DEV_ORIGINAL_ROLE_KEY = "ui-kit:dev-original-role";
|
|
9
|
-
function readKey(key) {
|
|
10
|
-
try {
|
|
11
|
-
return sessionStorage.getItem(key);
|
|
12
|
-
} catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function writeKey(key, value) {
|
|
17
|
-
try {
|
|
18
|
-
if (value === null) sessionStorage.removeItem(key);
|
|
19
|
-
else sessionStorage.setItem(key, value);
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const flask = /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-warning" });
|
|
24
|
-
const textOf = (label, value) => typeof label === "string" || typeof label === "number" ? String(label) : value;
|
|
25
|
-
function DevSwitchers({
|
|
26
|
-
enabled,
|
|
27
|
-
roles,
|
|
28
|
-
currentRole: role,
|
|
29
|
-
onRole,
|
|
30
|
-
plans,
|
|
31
|
-
currentPlan: plan,
|
|
32
|
-
onPlan,
|
|
33
|
-
originalRole,
|
|
34
|
-
storageKey = DEV_ORIGINAL_ROLE_KEY,
|
|
35
|
-
billingOff = false,
|
|
36
|
-
roleLabel = (r) => r,
|
|
37
|
-
planLabel = (p) => p,
|
|
38
|
-
labels: labelsProp
|
|
39
|
-
}) {
|
|
40
|
-
const bar = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS);
|
|
41
|
-
const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : void 0).dev;
|
|
42
|
-
const [kept, setKept] = useState(() => enabled ? readKey(storageKey) : null);
|
|
43
|
-
const [wasEnabled, setWasEnabled] = useState(enabled);
|
|
44
|
-
if (wasEnabled !== enabled) {
|
|
45
|
-
setWasEnabled(enabled);
|
|
46
|
-
if (enabled) setKept(readKey(storageKey));
|
|
47
|
-
}
|
|
48
|
-
if (!enabled) return null;
|
|
49
|
-
const original = originalRole ?? kept ?? void 0;
|
|
50
|
-
const pickRole = (next) => {
|
|
51
|
-
if (originalRole === void 0) {
|
|
52
|
-
if (next === original) {
|
|
53
|
-
writeKey(storageKey, null);
|
|
54
|
-
setKept(null);
|
|
55
|
-
} else if (kept === null && role !== void 0 && next !== role) {
|
|
56
|
-
writeKey(storageKey, role);
|
|
57
|
-
setKept(role);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
onRole?.(next);
|
|
61
|
-
};
|
|
62
|
-
const roleMenu = roles && roles.length > 0 && role !== void 0 && onRole ? /* @__PURE__ */ jsx(
|
|
63
|
-
TopBarActionMenu,
|
|
64
|
-
{
|
|
65
|
-
icon: flask,
|
|
66
|
-
"aria-label": `${bar.switchRole} \xB7 ${bar.role(role)}`,
|
|
67
|
-
entries: [
|
|
68
|
-
{ kind: "heading", key: "head-role", label: words.roleHeading },
|
|
69
|
-
...roles.map(
|
|
70
|
-
(r) => ({ key: `role-${r}`, label: roleLabel(r), checked: r === role, onSelect: () => pickRole(r) })
|
|
71
|
-
),
|
|
72
|
-
...original !== void 0 && original !== role ? [
|
|
73
|
-
{ kind: "divider", key: "sep-back" },
|
|
74
|
-
{
|
|
75
|
-
key: "back",
|
|
76
|
-
icon: /* @__PURE__ */ jsx(Undo2, { className: "size-4" }),
|
|
77
|
-
label: words.backTo(textOf(roleLabel(original), original)),
|
|
78
|
-
onSelect: () => pickRole(original)
|
|
79
|
-
}
|
|
80
|
-
] : []
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
) : null;
|
|
84
|
-
const planMenu = plans && plans.length > 0 && onPlan ? /* @__PURE__ */ jsx(
|
|
85
|
-
TopBarActionMenu,
|
|
86
|
-
{
|
|
87
|
-
icon: flask,
|
|
88
|
-
"aria-label": plan !== void 0 ? `${words.switchPlan} \xB7 ${words.plan(plan)}` : words.switchPlan,
|
|
89
|
-
entries: [
|
|
90
|
-
{ kind: "heading", key: "head-plan", label: words.planHeading },
|
|
91
|
-
...plans.map(
|
|
92
|
-
(p) => ({ key: `plan-${p}`, label: planLabel(p), checked: p === plan, onSelect: () => onPlan(p) })
|
|
93
|
-
)
|
|
94
|
-
],
|
|
95
|
-
children: billingOff ? () => /* @__PURE__ */ jsx("li", { "data-dev-billing-off": "", className: "mt-1 max-w-64 border-t border-subtle px-3 py-2 text-xs text-muted", children: words.billingOff }) : void 0
|
|
96
|
-
}
|
|
97
|
-
) : null;
|
|
98
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
99
|
-
roleMenu,
|
|
100
|
-
planMenu
|
|
101
|
-
] });
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DevMenu } from "./dev-menu.js";
|
|
4
|
+
import { DEV_ORIGINAL_ROLE_KEY } from "./dev-menu.js";
|
|
5
|
+
function DevSwitchers(props) {
|
|
6
|
+
return /* @__PURE__ */ jsx(DevMenu, { ...props });
|
|
102
7
|
}
|
|
103
8
|
export {
|
|
104
9
|
DEV_ORIGINAL_ROLE_KEY,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/dev-switchers.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../src/shell/dev-switchers.tsx"],"sourcesContent":["import { DevMenu, type DevMenuProps } from \"./dev-menu\";\n\n// 0.42's local-dev switchers (docs/top-bar-harmonization.md §2.7, §6.2, §6.3), since 0.51\n// a `DevMenu` with no app sections (§2.7.1). The props are the same and so is every row,\n// \"Back to {role}\" and its `sessionStorage` included — but it draws ONE flask where it\n// drew two: the role and the plan are groups of one menu, as in DevMenu.\n\n// Where it has always been imported from, until the wrapper goes in 0.52; the key and the\n// \"Back to\" logic it serves are DevMenu's now.\nexport { DEV_ORIGINAL_ROLE_KEY } from \"./dev-menu\";\n\n/** @deprecated 0.51 — use {@link DevMenuProps} (the same props); removed in 0.52. */\nexport type DevSwitchersProps<R extends string, P extends string> = Omit<DevMenuProps<R, P>, \"sections\" | \"active\">;\n\n/** @deprecated 0.51 — use {@link DevMenu} (one flask; same props); removed in 0.52. */\nexport function DevSwitchers<R extends string, P extends string>(props: DevSwitchersProps<R, P>) {\n return <DevMenu {...props} />;\n}\n"],"mappings":";AAgBS;AAhBT,SAAS,eAAkC;AAS3C,SAAS,6BAA6B;AAM/B,SAAS,aAAiD,OAAgC;AAC/F,SAAO,oBAAC,WAAS,GAAG,OAAO;AAC7B;","names":[]}
|
|
@@ -18,7 +18,10 @@ export interface TopBarActionsProps {
|
|
|
18
18
|
assistant?: ReactNode;
|
|
19
19
|
/** `SyncStatusIndicator`. */
|
|
20
20
|
sync?: ReactNode;
|
|
21
|
-
/** `
|
|
21
|
+
/** `DevMenu` (0.51, docs/top-bar-harmonization.md §2.7.1): the one flask — the role,
|
|
22
|
+
* the plan, then the app's own dev sections. Fill it only behind
|
|
23
|
+
* `import.meta.env.DEV`, so a production build drops the branch. (0.42's
|
|
24
|
+
* `DevSwitchers`, deprecated, is a DevMenu with no sections.) */
|
|
22
25
|
dev?: ReactNode;
|
|
23
26
|
/** kastlan's platform button. */
|
|
24
27
|
platform?: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/top-bar-actions.tsx"],"sourcesContent":["import { Fragment } from \"react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * The top bar's items by name (docs/top-bar-harmonization.md §2.1, §6.6). Each is the\n * app's own node — a kit part or its own — and any may be left out.\n */\nexport interface TopBarActionsProps {\n /** kastlan's `CompanySwitcher`: the context, first. */\n companySwitcher?: ReactNode;\n /** `GlobalSearch` (⌘K). */\n search?: ReactNode;\n /** `ThemeToggle`. */\n theme?: ReactNode;\n /** `FeedbackMenu`. */\n feedback?: ReactNode;\n /** `TourMenu`. */\n tours?: ReactNode;\n /** kastlan's assistant, beside the tours while it is on. */\n assistant?: ReactNode;\n /** `SyncStatusIndicator`. */\n sync?: ReactNode;\n /** `
|
|
1
|
+
{"version":3,"sources":["../../src/shell/top-bar-actions.tsx"],"sourcesContent":["import { Fragment } from \"react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * The top bar's items by name (docs/top-bar-harmonization.md §2.1, §6.6). Each is the\n * app's own node — a kit part or its own — and any may be left out.\n */\nexport interface TopBarActionsProps {\n /** kastlan's `CompanySwitcher`: the context, first. */\n companySwitcher?: ReactNode;\n /** `GlobalSearch` (⌘K). */\n search?: ReactNode;\n /** `ThemeToggle`. */\n theme?: ReactNode;\n /** `FeedbackMenu`. */\n feedback?: ReactNode;\n /** `TourMenu`. */\n tours?: ReactNode;\n /** kastlan's assistant, beside the tours while it is on. */\n assistant?: ReactNode;\n /** `SyncStatusIndicator`. */\n sync?: ReactNode;\n /** `DevMenu` (0.51, docs/top-bar-harmonization.md §2.7.1): the one flask — the role,\n * the plan, then the app's own dev sections. Fill it only behind\n * `import.meta.env.DEV`, so a production build drops the branch. (0.42's\n * `DevSwitchers`, deprecated, is a DevMenu with no sections.) */\n dev?: ReactNode;\n /** kastlan's platform button. */\n platform?: ReactNode;\n /** 0.46 (docs/notifications-harmonization.md §10.1): `NotificationBell`, right before the\n * account menu. Optional per app: an app shows the bell or an unread dot on the avatar,\n * never both (keksdose keeps its dot and leaves this empty). */\n notifications?: ReactNode;\n /** The account menu. When it is set the signed-out slots are not drawn. */\n account?: ReactNode;\n /** Signed out (§6.6): the bar's `LanguageMenu`, since there is no account menu to hold\n * the language. Drawn only without `account`. */\n language?: ReactNode;\n /** Signed out (§6.6): the \"Sign in\" button, where the account menu would be. Drawn only\n * without `account`. */\n signIn?: ReactNode;\n}\n\n/** §2.1's order, then the signed-out pair in the account menu's place. */\nexport const TOP_BAR_ACTION_ORDER = [\n \"companySwitcher\",\n \"search\",\n \"theme\",\n \"feedback\",\n \"tours\",\n \"assistant\",\n \"sync\",\n \"dev\",\n \"platform\",\n \"notifications\",\n \"account\",\n \"language\",\n \"signIn\",\n] as const satisfies readonly (keyof TopBarActionsProps)[];\n\nconst present = (node: ReactNode) => node !== undefined && node !== null && node !== false && node !== \"\";\n\n/**\n * The top bar's actions in the one order every app shares (§2.1): company switcher ·\n * Search · Theme · Feedback · Tours (· Assistant) · Sync · dev role · dev plan · platform ·\n * notifications (the bell, 0.46) · account menu — and, signed out, Language · Sign in where the account menu goes (§6.6).\n * The app fills the slots it has; an empty slot is skipped, so it leaves no gap.\n *\n * It draws a fragment, no box of its own: hand it to `TopBar`'s `actions`, whose row\n * spaces the items.\n *\n * ```tsx\n * <TopBar brand={…} actions={<TopBarActions search={…} theme={…} tours={…} account={…} />} />\n * ```\n */\nexport function TopBarActions(props: TopBarActionsProps) {\n const signedOut = !present(props.account);\n return (\n <>\n {TOP_BAR_ACTION_ORDER.map((slot) => {\n if ((slot === \"language\" || slot === \"signIn\") && !signedOut) return null;\n const node = props[slot];\n return present(node) ? <Fragment key={slot}>{node}</Fragment> : null;\n })}\n </>\n );\n}\n"],"mappings":";AA8EI,mBAI2B,WAJ3B;AA9EJ,SAAS,YAAAA,iBAAgB;AA4ClB,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,UAAU,CAAC,SAAoB,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAehG,SAAS,cAAc,OAA2B;AACvD,QAAM,YAAY,CAAC,QAAQ,MAAM,OAAO;AACxC,SACE,gCACG,+BAAqB,IAAI,CAAC,SAAS;AAClC,SAAK,SAAS,cAAc,SAAS,aAAa,CAAC,UAAW,QAAO;AACrE,UAAM,OAAO,MAAM,IAAI;AACvB,WAAO,QAAQ,IAAI,IAAI,oBAACA,WAAA,EAAqB,kBAAP,IAAY,IAAc;AAAA,EAClE,CAAC,GACH;AAEJ;","names":["Fragment"]}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
2
2
|
/**
|
|
3
3
|
* The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the
|
|
4
|
-
* top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `
|
|
4
|
+
* top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's
|
|
5
|
+
* `DevSwitchers`, whose words these still are, §2.7.1). A module
|
|
5
6
|
* of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language
|
|
6
|
-
* sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the
|
|
7
|
-
*
|
|
7
|
+
* sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the
|
|
8
|
+
* dev flask's name is `topBar.role`, as `RoleSwitcher`'s tooltip.
|
|
8
9
|
*/
|
|
9
10
|
/** `TourMenu`'s words (§2.4). */
|
|
10
11
|
export interface TourMenuLabels {
|
|
@@ -65,19 +66,27 @@ export interface SyncStatusLabels {
|
|
|
65
66
|
* words beside the cloud. */
|
|
66
67
|
saving: string;
|
|
67
68
|
}
|
|
68
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the
|
|
71
|
+
* namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.
|
|
72
|
+
*/
|
|
69
73
|
export interface DevSwitchersLabels {
|
|
70
|
-
/**
|
|
74
|
+
/** 0.51: the flask's name, and the menu's — the role and the plan follow it. */
|
|
75
|
+
menu: string;
|
|
76
|
+
/** 0.51: what the dot on the flask means while `active`, joined to its name — an
|
|
77
|
+
* override the developer could forget is on (a "today", a slow server). */
|
|
78
|
+
overridesOn: string;
|
|
79
|
+
/** The heading of the role group. */
|
|
71
80
|
roleHeading: string;
|
|
72
|
-
/**
|
|
81
|
+
/** @deprecated 0.51 — DevMenu's one flask is named by `menu`; removed in 0.52. */
|
|
73
82
|
switchPlan: string;
|
|
74
|
-
/** The plan
|
|
83
|
+
/** The plan in the flask's name, given the active plan. */
|
|
75
84
|
plan: (value: string) => string;
|
|
76
|
-
/** The heading of the plan
|
|
85
|
+
/** The heading of the plan group. */
|
|
77
86
|
planHeading: string;
|
|
78
87
|
/** The row back to the role the session started with. */
|
|
79
88
|
backTo: (role: string) => string;
|
|
80
|
-
/** The plan
|
|
89
|
+
/** The plan group's note while billing is off locally. */
|
|
81
90
|
billingOff: string;
|
|
82
91
|
}
|
|
83
92
|
export interface TopBarPartsLabels {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevSwitchers
|
|
1
|
+
{"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's\n * `DevSwitchers`, whose words these still are, §2.7.1). A module\n * of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language\n * sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the\n * dev flask's name is `topBar.role`, as `RoleSwitcher`'s tooltip.\n */\n\n/** `TourMenu`'s words (§2.4). */\nexport interface TourMenuLabels {\n /** The compass's name, and the menu's. */\n menu: string;\n /** The NEW dot on the compass, for a reader: it joins the trigger's name. */\n newDot: string;\n /** The heading over the tours that start on the open page. */\n thisPage: string;\n /** The heading over the untaken new tours. */\n newTours: string;\n /** The last row, the way to the app's tours page (only with `href`). */\n allTours: string;\n /** The chip on a new tour's row. */\n badgeNew: string;\n /** What the ✓ on a taken tour's row says to a reader. */\n completed: string;\n /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */\n badgeUpdated: string;\n /** The dot on the compass while only updated tours light it, for a reader. */\n updatedDot: string;\n /** The heading over the untaken new tours when updated ones are among them. */\n newAndUpdated: string;\n}\n\n/** `SyncStatusIndicator`'s words (§2.6). The \"waiting for a plan\" state is\n * `billing.waitingChanges`, the kit's words for it since billing §12.5. */\nexport interface SyncStatusLabels {\n /** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */\n blocked: (count: number) => string;\n offline: string;\n /** The server refused the last round. */\n refused: string;\n syncing: string;\n pending: (count: number) => string;\n /** Settled, with the time of the last good round (already formatted). */\n syncedAt: (time: string) => string;\n /** Settled, before the first round. */\n idle: string;\n /** Joined to the state when a tap syncs: what the tap does. */\n syncHint: string;\n /** Joined to the state while offline, when a tap can do nothing. */\n offlineHint: string;\n /** The state and what a tap does, as one tooltip: \"Offline — nothing to sync…\". */\n withHint: (state: string, hint: string) => string;\n /** The state with the app's `scope` words before it: \"Handovers on this device: …\". */\n scoped: (scope: string, state: string) => string;\n /** The short words beside the cloud (`showLabel`): offline. */\n offlineShort: string;\n /** The short words while syncing. */\n syncingShort: string;\n /** The short words while changes wait (pending, held for a plan, or blocked). */\n waitingShort: (count: number) => string;\n /** The short words after a refused round. */\n refusedShort: string;\n /** Offline in an online-only app (`storage=\"none\"`): nothing is kept on the device. */\n offlineUnsaved: string;\n /** A round under way in an online-only app — it saves, it doesn't sync. Also its short\n * words beside the cloud. */\n saving: string;\n}\n\n/**\n * `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the\n * namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.\n */\nexport interface DevSwitchersLabels {\n /** 0.51: the flask's name, and the menu's — the role and the plan follow it. */\n menu: string;\n /** 0.51: what the dot on the flask means while `active`, joined to its name — an\n * override the developer could forget is on (a \"today\", a slow server). */\n overridesOn: string;\n /** The heading of the role group. */\n roleHeading: string;\n /** @deprecated 0.51 — DevMenu's one flask is named by `menu`; removed in 0.52. */\n switchPlan: string;\n /** The plan in the flask's name, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan group. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan group's note while billing is off locally. */\n billingOff: string;\n}\n\nexport interface TopBarPartsLabels {\n tours: TourMenuLabels;\n sync: SyncStatusLabels;\n dev: DevSwitchersLabels;\n}\n\nexport const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {\n tours: {\n menu: \"Guided tours\",\n newDot: \"New tours\",\n thisPage: \"On this page\",\n newTours: \"New tours\",\n allTours: \"All tours…\",\n badgeNew: \"New\",\n completed: \"Completed\",\n badgeUpdated: \"Updated\",\n updatedDot: \"Updated tours\",\n newAndUpdated: \"New and updated\",\n },\n sync: {\n blocked: (count) =>\n count === 1\n ? \"1 change cannot be sent from this device\"\n : `${count} changes cannot be sent from this device`,\n offline: \"Offline — changes are saved on this device and will sync when you reconnect\",\n refused: \"The server refused the last sync\",\n syncing: \"Syncing…\",\n pending: (count) => (count === 1 ? \"1 change waiting to sync\" : `${count} changes waiting to sync`),\n syncedAt: (time) => `Synced at ${time}`,\n idle: \"Up to date\",\n syncHint: \"Tap to sync now\",\n offlineHint: \"nothing to sync while offline\",\n withHint: (state, hint) => `${state} — ${hint}`,\n scoped: (scope, state) => `${scope}: ${state}`,\n offlineShort: \"Offline\",\n syncingShort: \"Syncing…\",\n waitingShort: (count) => `${count} waiting`,\n refusedShort: \"Not synced\",\n offlineUnsaved: \"Offline — changes can't be saved until you reconnect\",\n saving: \"Saving…\",\n },\n dev: {\n menu: \"Dev tools\",\n overridesOn: \"Overrides on\",\n roleHeading: \"Dev · role\",\n switchPlan: \"Switch plan\",\n plan: (value) => `Plan: ${value}`,\n planHeading: \"Dev · plan\",\n backTo: (role) => `Back to ${role}`,\n billingOff: \"Billing is off locally: plans change nothing\",\n },\n};\n\n/** The `topBarParts` namespace, resolved — English, then the provider, then `labels`. */\nexport function useTopBarPartsLabels(labels?: LabelOverride<TopBarPartsLabels>): TopBarPartsLabels {\n return useKitLabels(\"topBarParts\", DEFAULT_TOP_BAR_PARTS_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAsG1C,MAAM,+BAAkD;AAAA,EAC7D,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AAAA,EACA,MAAM;AAAA,IACJ,SAAS,CAAC,UACR,UAAU,IACN,6CACA,GAAG,KAAK;AAAA,IACd,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,CAAC,UAAW,UAAU,IAAI,6BAA6B,GAAG,KAAK;AAAA,IACxE,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,UAAU,CAAC,OAAO,SAAS,GAAG,KAAK,WAAM,IAAI;AAAA,IAC7C,QAAQ,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,IAC5C,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc,CAAC,UAAU,GAAG,KAAK;AAAA,IACjC,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,MAAM,CAAC,UAAU,SAAS,KAAK;AAAA,IAC/B,aAAa;AAAA,IACb,QAAQ,CAAC,SAAS,WAAW,IAAI;AAAA,IACjC,YAAY;AAAA,EACd;AACF;AAGO,SAAS,qBAAqB,QAA8D;AACjG,SAAO,aAAa,eAAe,8BAA8B,MAAM;AACzE;","names":[]}
|
|
@@ -103,9 +103,10 @@ export interface TopBarMenuHeader {
|
|
|
103
103
|
* A long menu is grouped by topic with `{ kind: "heading" }` entries (0.23) — see
|
|
104
104
|
* {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same
|
|
105
105
|
* hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps
|
|
106
|
-
* rather than widening it.
|
|
106
|
+
* rather than widening it. A menu that can grow taller than a phone's glass passes
|
|
107
|
+
* `scrollable` (0.51), since that clamp is sideways only.
|
|
107
108
|
*/
|
|
108
|
-
export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabel, heading, header, entries, leading, children, footer, footerLabel, align, panelClassName, triggerAttributes, }: {
|
|
109
|
+
export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabel, heading, header, entries, leading, children, footer, footerLabel, align, panelClassName, scrollable, triggerAttributes, }: {
|
|
109
110
|
/** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
|
|
110
111
|
icon?: ReactNode;
|
|
111
112
|
/**
|
|
@@ -175,6 +176,20 @@ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClas
|
|
|
175
176
|
* direction (see `HoverMenu`, whose type this is). Default `end`. */
|
|
176
177
|
align?: HoverMenuProps["align"];
|
|
177
178
|
panelClassName?: string;
|
|
179
|
+
/**
|
|
180
|
+
* Cap the panel at the glass under the top bar and scroll the rows inside it (0.51,
|
|
181
|
+
* `DevMenu`). `HoverMenu` keeps a panel inside the viewport sideways only, so a tall
|
|
182
|
+
* one ran off the bottom of the screen: DevMenu's ~25 rows on a 390×844 phone, with
|
|
183
|
+
* the last ones out of reach — the page cannot scroll them up, the panel hangs from
|
|
184
|
+
* the sticky bar.
|
|
185
|
+
*
|
|
186
|
+
* Off by default: a menu that fits keeps its natural height and has no scroll
|
|
187
|
+
* container (`TourMenu` keeps its own `max-h-[70vh]`). The keyboard needs nothing
|
|
188
|
+
* extra: `HoverMenu`'s roving focus is a plain `focus()`, which the browser follows by
|
|
189
|
+
* scrolling the panel to the row, so ↓, ↑, Home and End reach a row below the cap. The
|
|
190
|
+
* header and the footer are inside the list and scroll with the rows.
|
|
191
|
+
*/
|
|
192
|
+
scrollable?: boolean;
|
|
178
193
|
/** `data-*` attributes for the trigger itself — a tour's anchor (`data-tour`), a test
|
|
179
194
|
* hook. The parts built on this menu (`TourMenu`) pass theirs through here. */
|
|
180
195
|
triggerAttributes?: {
|
|
@@ -28,6 +28,7 @@ function toBlocks(entries) {
|
|
|
28
28
|
}
|
|
29
29
|
const SECTION_CONTROLS = "a[href]:not([role]),button:not([role])";
|
|
30
30
|
const TABBABLE = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]";
|
|
31
|
+
const SCROLLABLE_PANEL = "max-h-[calc(100dvh-4rem)] overflow-y-auto overscroll-contain";
|
|
31
32
|
function MenuSection({ className, children }) {
|
|
32
33
|
const ref = useRef(null);
|
|
33
34
|
useLayoutEffect(() => {
|
|
@@ -66,6 +67,7 @@ function TopBarActionMenu({
|
|
|
66
67
|
footerLabel,
|
|
67
68
|
align,
|
|
68
69
|
panelClassName,
|
|
70
|
+
scrollable = false,
|
|
69
71
|
triggerAttributes
|
|
70
72
|
}) {
|
|
71
73
|
const uid = useId();
|
|
@@ -138,7 +140,7 @@ function TopBarActionMenu({
|
|
|
138
140
|
) }, entry.key);
|
|
139
141
|
return (
|
|
140
142
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
|
|
141
|
-
/* @__PURE__ */ jsxs("ul", { className: cn("py-1", panelClassName), onKeyDown: keepTabInsidePanel, children: [
|
|
143
|
+
/* @__PURE__ */ jsxs("ul", { className: cn("py-1", scrollable && SCROLLABLE_PANEL, panelClassName), onKeyDown: keepTabInsidePanel, children: [
|
|
142
144
|
header && /* @__PURE__ */ jsxs(MenuSection, { className: "mb-1 border-b border-subtle px-3 py-2", children: [
|
|
143
145
|
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-primary", children: header.title }),
|
|
144
146
|
header.subtitle != null && /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted", children: header.subtitle }),
|