@eifi1/ui-kit 0.41.0 → 0.43.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 +26 -7
- package/dist/admin/admin-area-labels.d.ts +59 -0
- package/dist/admin/admin-area-labels.js +35 -0
- package/dist/admin/admin-area-labels.js.map +1 -0
- package/dist/admin/admin-forbidden.d.ts +35 -0
- package/dist/admin/admin-forbidden.js +36 -0
- package/dist/admin/admin-forbidden.js.map +1 -0
- package/dist/admin/admin-overview.d.ts +110 -0
- package/dist/admin/admin-overview.js +92 -0
- package/dist/admin/admin-overview.js.map +1 -0
- package/dist/admin/admin-sections.d.ts +94 -0
- package/dist/admin/admin-sections.js +81 -0
- package/dist/admin/admin-sections.js.map +1 -0
- package/dist/admin/admin-tool-links.d.ts +48 -0
- package/dist/admin/admin-tool-links.js +31 -0
- package/dist/admin/admin-tool-links.js.map +1 -0
- package/dist/billing/billing-banner.d.ts +33 -2
- package/dist/billing/billing-banner.js +29 -3
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +36 -0
- package/dist/billing/billing-labels.js +10 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +33 -0
- package/dist/billing/billing-overview.js +6 -0
- package/dist/billing/billing-overview.js.map +1 -1
- package/dist/billing/billing-standing.d.ts +8 -0
- package/dist/billing/billing-standing.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +53 -0
- package/dist/billing/cancel-request.js +21 -0
- package/dist/billing/cancel-request.js.map +1 -0
- package/dist/billing/cancel-subscription.d.ts +46 -0
- package/dist/billing/cancel-subscription.js +57 -0
- package/dist/billing/cancel-subscription.js.map +1 -0
- package/dist/billing/plan-card.d.ts +45 -1
- package/dist/billing/plan-card.js +53 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-switch.d.ts +10 -1
- package/dist/billing/plan-switch.js +22 -13
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/provider-page.d.ts +40 -0
- package/dist/billing/provider-page.js +33 -0
- package/dist/billing/provider-page.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +68 -4
- package/dist/billing/subscription-actions.js +65 -5
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/components/account-chips.d.ts +50 -2
- package/dist/components/account-chips.js +33 -0
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/empty-state.d.ts +2 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-menu.d.ts +7 -4
- package/dist/feedback/feedback-menu.js +4 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/hooks/use-search-param-state.js +4 -1
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +113 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +12 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +142 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +110 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +109 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +110 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +110 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +109 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +34 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/search/global-search.d.ts +3 -0
- package/dist/search/global-search.js +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/settings/settings-catalogue.d.ts +9 -1
- package/dist/settings/settings-catalogue.js +1 -1
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.d.ts +7 -1
- package/dist/settings/settings-layout.js +35 -27
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +46 -0
- package/dist/shell/account-menu-language.js +64 -0
- package/dist/shell/account-menu-language.js.map +1 -0
- package/dist/shell/admin-menu.d.ts +69 -0
- package/dist/shell/admin-menu.js +64 -0
- package/dist/shell/admin-menu.js.map +1 -0
- package/dist/shell/app-shell.d.ts +10 -0
- package/dist/shell/app-shell.js +22 -4
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-switchers.d.ts +53 -0
- package/dist/shell/dev-switchers.js +107 -0
- package/dist/shell/dev-switchers.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +86 -0
- package/dist/shell/sync-status-indicator.js +170 -0
- package/dist/shell/sync-status-indicator.js.map +1 -0
- package/dist/shell/top-bar-actions.d.ts +49 -0
- package/dist/shell/top-bar-actions.js +31 -0
- package/dist/shell/top-bar-actions.js.map +1 -0
- package/dist/shell/top-bar-parts-labels.d.ts +90 -0
- package/dist/shell/top-bar-parts-labels.js +51 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +5 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +16 -3
- package/dist/shell/topbar-controls.js +5 -1
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.d.ts +67 -0
- package/dist/shell/tour-menu.js +87 -0
- package/dist/shell/tour-menu.js.map +1 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +21 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour-catalogue-labels.d.ts +44 -0
- package/dist/tour/tour-catalogue-labels.js +32 -0
- package/dist/tour/tour-catalogue-labels.js.map +1 -0
- package/dist/tour/tour-catalogue.d.ts +68 -0
- package/dist/tour/tour-catalogue.js +189 -0
- package/dist/tour/tour-catalogue.js.map +1 -0
- package/dist/tour/tour-info.d.ts +112 -0
- package/dist/tour/tour-info.js +36 -0
- package/dist/tour/tour-info.js.map +1 -0
- package/dist/tour/use-tour-progress.d.ts +61 -0
- package/dist/tour/use-tour-progress.js +116 -0
- package/dist/tour/use-tour-progress.js.map +1 -0
- package/dist/tour/use-tour-url-sync.d.ts +58 -0
- package/dist/tour/use-tour-url-sync.js +110 -0
- package/dist/tour/use-tour-url-sync.js.map +1 -0
- package/dist/tour.d.ts +8 -0
- package/dist/tour.js +18 -0
- package/dist/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-area-labels.ts +91 -0
- package/src/admin/admin-forbidden.tsx +75 -0
- package/src/admin/admin-overview.tsx +217 -0
- package/src/admin/admin-sections.tsx +198 -0
- package/src/admin/admin-tool-links.tsx +84 -0
- package/src/billing/billing-banner.tsx +57 -6
- package/src/billing/billing-labels.ts +59 -0
- package/src/billing/billing-overview.ts +37 -0
- package/src/billing/billing-standing.ts +8 -0
- package/src/billing/cancel-request.ts +75 -0
- package/src/billing/cancel-subscription.tsx +102 -0
- package/src/billing/plan-card.tsx +97 -1
- package/src/billing/plan-switch.tsx +16 -0
- package/src/billing/provider-page.ts +75 -0
- package/src/billing/subscription-actions.tsx +155 -20
- package/src/components/account-chips.tsx +89 -2
- package/src/components/empty-state.tsx +2 -1
- package/src/feedback/feedback-menu.tsx +8 -3
- package/src/hooks/use-search-param-state.ts +6 -1
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/german.ts +127 -0
- package/src/i18n/kit-labels.tsx +12 -0
- package/src/i18n/locales/en.ts +32 -0
- package/src/i18n/locales/es.ts +121 -0
- package/src/i18n/locales/fr.ts +121 -0
- package/src/i18n/locales/hu.ts +119 -0
- package/src/i18n/locales/it.ts +122 -0
- package/src/i18n/locales/zh.ts +114 -0
- package/src/i18n/review.ts +33 -0
- package/src/index.ts +38 -2
- package/src/search/global-search.tsx +5 -0
- package/src/settings/settings-catalogue.ts +10 -2
- package/src/settings/settings-layout.tsx +58 -34
- package/src/shell/account-menu-language.tsx +116 -0
- package/src/shell/admin-menu.tsx +146 -0
- package/src/shell/app-shell.tsx +44 -3
- package/src/shell/dev-switchers.tsx +179 -0
- package/src/shell/sync-status-indicator.tsx +301 -0
- package/src/shell/top-bar-actions.tsx +79 -0
- package/src/shell/top-bar-parts-labels.ts +142 -0
- package/src/shell/topbar-action-menu.tsx +15 -2
- package/src/shell/topbar-controls.tsx +20 -3
- package/src/shell/tour-menu.tsx +179 -0
- package/src/shell.ts +27 -0
- package/src/tour/tour-catalogue-labels.ts +75 -0
- package/src/tour/tour-catalogue.tsx +316 -0
- package/src/tour/tour-info.ts +144 -0
- package/src/tour/use-tour-progress.ts +198 -0
- package/src/tour/use-tour-url-sync.ts +221 -0
- package/src/tour.ts +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/dev-switchers.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\nexport interface DevSwitchersProps<R extends string, P extends string> {\n /** Draw anything at all: the app passes `import.meta.env.DEV`, so a production build\n * tree-shakes the switchers with the constant. */\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 menu 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 menu 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}\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/**\n * The top bar's local-dev switchers (docs/top-bar-harmonization.md §2.7, §6.2, §6.3): the\n * role, then the plan, each a menu behind `RoleSwitcher`'s amber flask. Nothing renders\n * unless `enabled`; either menu renders only with its list and handler.\n *\n * The role menu ends in \"Back to {role}\" once the role differs from the one the session\n * started with: the dev route needs only a signed-in user, so a switch to a lesser role\n * can always switch back. With `billingOff` the plan menu notes that plans change nothing\n * while billing is off.\n */\nexport function DevSwitchers<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}: DevSwitchersProps<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 roleMenu =\n roles && roles.length > 0 && role !== undefined && onRole ? (\n <TopBarActionMenu\n icon={flask}\n aria-label={`${bar.switchRole} · ${bar.role(role)}`}\n entries={[\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 ...(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 ) : null;\n\n const planMenu =\n plans && plans.length > 0 && onPlan ? (\n <TopBarActionMenu\n icon={flask}\n aria-label={plan !== undefined ? `${words.switchPlan} · ${words.plan(plan)}` : words.switchPlan}\n entries={[\n { kind: \"heading\", key: \"head-plan\", label: words.planHeading },\n ...plans.map(\n (p): TopBarMenuEntry => ({ key: `plan-${p}`, label: planLabel(p), checked: p === plan, onSelect: () => onPlan(p) }),\n ),\n ]}\n >\n {billingOff\n ? () => (\n <li data-dev-billing-off=\"\" className=\"mt-1 max-w-64 border-t border-subtle px-3 py-2 text-xs text-muted\">\n {words.billingOff}\n </li>\n )\n : undefined}\n </TopBarActionMenu>\n ) : null;\n\n return (\n <>\n {roleMenu}\n {planMenu}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
2
|
+
import { type SyncStatusLabels } from "./top-bar-parts-labels.js";
|
|
3
|
+
/**
|
|
4
|
+
* What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md
|
|
5
|
+
* §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and
|
|
6
|
+
* Kurvenschmiede's online state + `useIsMutating` all fill it.
|
|
7
|
+
*/
|
|
8
|
+
export interface SyncStatus {
|
|
9
|
+
/** What the engine is doing. `offline` is the engine's own verdict; `online: false` is
|
|
10
|
+
* the browser's — either shows the offline state. */
|
|
11
|
+
phase: "idle" | "syncing" | "offline" | "error";
|
|
12
|
+
/** The browser's `navigator.onLine` (or the app's better guess). */
|
|
13
|
+
online: boolean;
|
|
14
|
+
/** Writes kept on this device and not yet sent. */
|
|
15
|
+
pending: number;
|
|
16
|
+
/** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */
|
|
17
|
+
blocked?: number;
|
|
18
|
+
/** Writes the server refused as read-only and keeps until a plan (billing §12.5). */
|
|
19
|
+
waitingForPlan?: number;
|
|
20
|
+
/** When the last good round ended — epoch ms, an ISO string or a `Date`. */
|
|
21
|
+
lastSyncedAt?: number | string | Date | null;
|
|
22
|
+
}
|
|
23
|
+
/** The seven states, in their ranking: the first that holds wins. */
|
|
24
|
+
export type SyncState = "blocked" | "waitingForPlan" | "offline" | "error" | "syncing" | "pending" | "synced";
|
|
25
|
+
/**
|
|
26
|
+
* Which of the seven states a {@link SyncStatus} is in — keksdose's ranking
|
|
27
|
+
* (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,
|
|
28
|
+
* because waiting cannot help it; then one waiting for a plan; then offline; then a refused
|
|
29
|
+
* round; then a round under way; then writes waiting; else in sync. `syncing` also holds
|
|
30
|
+
* while a tap's own round runs (`refreshing`), before the engine publishes its phase.
|
|
31
|
+
*/
|
|
32
|
+
export declare function syncState(status: SyncStatus, refreshing?: boolean): SyncState;
|
|
33
|
+
/** The scope words of an indicator that covers only part of the app (§6.10). */
|
|
34
|
+
export interface SyncStatusScope {
|
|
35
|
+
/** Put before every state in the tooltip: "Handovers on this device" →
|
|
36
|
+
* "Handovers on this device: 3 changes waiting to sync". */
|
|
37
|
+
name: string;
|
|
38
|
+
/** The whole offline sentence instead of the kit's, when offline means something
|
|
39
|
+
* narrower: "Offline: handovers keep working on this device; everything else needs a
|
|
40
|
+
* connection". */
|
|
41
|
+
offline?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SyncStatusIndicatorProps {
|
|
44
|
+
status: SyncStatus;
|
|
45
|
+
/**
|
|
46
|
+
* Run a round now (push, then pull, then re-read what is on screen). With it the
|
|
47
|
+
* indicator is a "sync now" button, disabled offline and mid-round but keeping its
|
|
48
|
+
* colour; a returned promise keeps the spin until it settles. Without it, a status only
|
|
49
|
+
* (§6.9).
|
|
50
|
+
*/
|
|
51
|
+
onSync?: () => void | Promise<unknown>;
|
|
52
|
+
/** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,
|
|
53
|
+
* syncing or with changes waiting, held or refused (§2.6). */
|
|
54
|
+
hideWhenIdle?: boolean;
|
|
55
|
+
/** The app's words for its part of the app (§6.10). */
|
|
56
|
+
scope?: SyncStatusScope;
|
|
57
|
+
/**
|
|
58
|
+
* Where the app keeps a change before the server has it. `"device"` (the default): an
|
|
59
|
+
* offline-first app (keksdose, kastlan's handovers) — offline means "kept here, sent
|
|
60
|
+
* later". `"none"`: an online-only app that keeps nothing on the device (Kurvenschmiede,
|
|
61
|
+
* build review B1) — offline reads "changes can't be saved until you reconnect", a
|
|
62
|
+
* round reads "Saving…", and `pending`, `blocked` and `waitingForPlan` are ignored,
|
|
63
|
+
* since there is nowhere for a change to wait. "Up to date" when settled.
|
|
64
|
+
*/
|
|
65
|
+
storage?: "device" | "none";
|
|
66
|
+
/** Short words beside the cloud ("Offline", "Syncing…", "3 waiting") while not in sync. */
|
|
67
|
+
showLabel?: boolean;
|
|
68
|
+
/** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */
|
|
69
|
+
labels?: LabelOverride<SyncStatusLabels>;
|
|
70
|
+
/** The clock's language for "Synced at 14:05". Default: the provider's locale. */
|
|
71
|
+
locale?: string;
|
|
72
|
+
className?: string;
|
|
73
|
+
/** `data-*` attributes go on the trigger — a tour's anchor (`data-tour="sync-status"`). */
|
|
74
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's
|
|
78
|
+
* `SyncStatusIndicator`: the seven states and their ranking ({@link syncState}), one cloud
|
|
79
|
+
* per state, a ✓ badge when in sync and a spin while syncing. With `onSync` it is also the
|
|
80
|
+
* "sync now" button, and its name says both what it is and what a tap does.
|
|
81
|
+
*
|
|
82
|
+
* Colours are the kit's roles — danger for a write that cannot be sent, warning for
|
|
83
|
+
* anything waiting, info while syncing, success when in sync — where keksdose had raw
|
|
84
|
+
* rose/amber/sky/emerald pairs.
|
|
85
|
+
*/
|
|
86
|
+
export declare function SyncStatusIndicator({ status, onSync, hideWhenIdle, scope, showLabel, labels: labelsProp, locale: localeProp, className, storage, ...rest }: SyncStatusIndicatorProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { IconButton } from "../components/icon-button.js";
|
|
7
|
+
import { Tooltip } from "../components/tooltip.js";
|
|
8
|
+
import { useBillingLabels } from "../billing/billing-labels.js";
|
|
9
|
+
import { useKitLocale } from "../i18n/kit-labels.js";
|
|
10
|
+
import { useTopBarPartsLabels } from "./top-bar-parts-labels.js";
|
|
11
|
+
function syncState(status, refreshing = false) {
|
|
12
|
+
if ((status.blocked ?? 0) > 0) return "blocked";
|
|
13
|
+
if ((status.waitingForPlan ?? 0) > 0) return "waitingForPlan";
|
|
14
|
+
if (!status.online || status.phase === "offline") return "offline";
|
|
15
|
+
if (status.phase === "error") return "error";
|
|
16
|
+
if (status.phase === "syncing" || refreshing) return "syncing";
|
|
17
|
+
if (status.pending > 0) return "pending";
|
|
18
|
+
return "synced";
|
|
19
|
+
}
|
|
20
|
+
const LOOK = {
|
|
21
|
+
// The one the user must act on: the kit's danger, where keksdose had rose.
|
|
22
|
+
blocked: { Icon: CloudAlert, tone: "danger" },
|
|
23
|
+
// Safe, and a payment sends them: amber, the kit's warning.
|
|
24
|
+
waitingForPlan: { Icon: CloudAlert, tone: "warning" },
|
|
25
|
+
offline: { Icon: CloudOff, tone: "warning" },
|
|
26
|
+
// Usually resolves by itself (a deploy, a read-only view): amber, not danger.
|
|
27
|
+
error: { Icon: CloudAlert, tone: "warning" },
|
|
28
|
+
syncing: { Icon: RefreshCw, tone: "info" },
|
|
29
|
+
pending: { Icon: UploadCloud, tone: "warning" },
|
|
30
|
+
synced: { Icon: Cloud, tone: "success" }
|
|
31
|
+
};
|
|
32
|
+
const TEXT_TONE = {
|
|
33
|
+
danger: "text-danger",
|
|
34
|
+
warning: "text-warning",
|
|
35
|
+
info: "text-info",
|
|
36
|
+
success: "text-success"
|
|
37
|
+
};
|
|
38
|
+
function dataAttributes(rest) {
|
|
39
|
+
return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
|
|
40
|
+
}
|
|
41
|
+
function SyncStatusIndicator({
|
|
42
|
+
status,
|
|
43
|
+
onSync,
|
|
44
|
+
hideWhenIdle = false,
|
|
45
|
+
scope,
|
|
46
|
+
showLabel = false,
|
|
47
|
+
labels: labelsProp,
|
|
48
|
+
locale: localeProp,
|
|
49
|
+
className,
|
|
50
|
+
storage = "device",
|
|
51
|
+
...rest
|
|
52
|
+
}) {
|
|
53
|
+
const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : void 0).sync;
|
|
54
|
+
const billing = useBillingLabels();
|
|
55
|
+
const locale = useKitLocale(localeProp);
|
|
56
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
57
|
+
const mounted = useRef(true);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
mounted.current = true;
|
|
60
|
+
return () => {
|
|
61
|
+
mounted.current = false;
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
const onlineOnly = storage === "none";
|
|
65
|
+
const state = syncState(
|
|
66
|
+
onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status,
|
|
67
|
+
refreshing
|
|
68
|
+
);
|
|
69
|
+
const data = dataAttributes(rest);
|
|
70
|
+
if (hideWhenIdle && state === "synced") return null;
|
|
71
|
+
const { Icon, tone } = LOOK[state];
|
|
72
|
+
const spin = state === "syncing";
|
|
73
|
+
let text;
|
|
74
|
+
switch (state) {
|
|
75
|
+
case "blocked":
|
|
76
|
+
text = words.blocked(status.blocked ?? 0);
|
|
77
|
+
break;
|
|
78
|
+
case "waitingForPlan":
|
|
79
|
+
text = billing.waitingChanges(status.waitingForPlan ?? 0);
|
|
80
|
+
break;
|
|
81
|
+
case "offline":
|
|
82
|
+
text = onlineOnly ? words.offlineUnsaved : words.offline;
|
|
83
|
+
break;
|
|
84
|
+
case "error":
|
|
85
|
+
text = words.refused;
|
|
86
|
+
break;
|
|
87
|
+
case "syncing":
|
|
88
|
+
text = onlineOnly ? words.saving : words.syncing;
|
|
89
|
+
break;
|
|
90
|
+
case "pending":
|
|
91
|
+
text = words.pending(status.pending);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
text = !onlineOnly && status.lastSyncedAt ? words.syncedAt(
|
|
95
|
+
// A clock time in the UI's language, never the browser's (keksdose live #246).
|
|
96
|
+
new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" })
|
|
97
|
+
) : words.idle;
|
|
98
|
+
}
|
|
99
|
+
if (scope) {
|
|
100
|
+
text = state === "offline" && scope.offline ? scope.offline : words.scoped(scope.name, text);
|
|
101
|
+
}
|
|
102
|
+
const short = state === "offline" ? words.offlineShort : state === "syncing" ? onlineOnly ? words.saving : words.syncingShort : state === "error" ? words.refusedShort : state === "pending" || state === "blocked" || state === "waitingForPlan" ? words.waitingShort(
|
|
103
|
+
state === "pending" ? status.pending : state === "blocked" ? status.blocked ?? 0 : status.waitingForPlan ?? 0
|
|
104
|
+
) : null;
|
|
105
|
+
const offline = state === "offline";
|
|
106
|
+
const canSync = !!onSync && status.online && status.phase !== "offline" && !spin;
|
|
107
|
+
const name = onSync ? canSync ? words.withHint(text, words.syncHint) : offline ? words.withHint(text, words.offlineHint) : text : text;
|
|
108
|
+
const badge = state === "synced" ? /* @__PURE__ */ jsx(Check, { className: "stroke-[3]" }) : void 0;
|
|
109
|
+
const glyph = /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: spin ? "animate-spin" : void 0 });
|
|
110
|
+
const sync = async () => {
|
|
111
|
+
if (!canSync || !onSync) return;
|
|
112
|
+
setRefreshing(true);
|
|
113
|
+
try {
|
|
114
|
+
await onSync();
|
|
115
|
+
} finally {
|
|
116
|
+
if (mounted.current) setRefreshing(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const control = onSync ? /* @__PURE__ */ jsx(
|
|
120
|
+
IconButton,
|
|
121
|
+
{
|
|
122
|
+
...data,
|
|
123
|
+
"data-sync-state": state,
|
|
124
|
+
size: "2xs",
|
|
125
|
+
glyphSize: 20,
|
|
126
|
+
shape: "round",
|
|
127
|
+
disabledStyle: "keep",
|
|
128
|
+
tone,
|
|
129
|
+
quiet: false,
|
|
130
|
+
label: name,
|
|
131
|
+
labelVisible: false,
|
|
132
|
+
tooltipSide: "bottom",
|
|
133
|
+
onClick: () => void sync(),
|
|
134
|
+
disabled: !canSync,
|
|
135
|
+
"aria-live": "polite",
|
|
136
|
+
badge,
|
|
137
|
+
children: glyph
|
|
138
|
+
}
|
|
139
|
+
) : /* @__PURE__ */ jsx(Tooltip, { label: name, side: "bottom", children: /* @__PURE__ */ jsxs(
|
|
140
|
+
"span",
|
|
141
|
+
{
|
|
142
|
+
...data,
|
|
143
|
+
role: "status",
|
|
144
|
+
"data-sync-state": state,
|
|
145
|
+
className: cn("relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5", TEXT_TONE[tone]),
|
|
146
|
+
children: [
|
|
147
|
+
glyph,
|
|
148
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: name }),
|
|
149
|
+
badge && /* @__PURE__ */ jsx(
|
|
150
|
+
"span",
|
|
151
|
+
{
|
|
152
|
+
"aria-hidden": true,
|
|
153
|
+
className: "pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3",
|
|
154
|
+
children: badge
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
) });
|
|
160
|
+
if (!showLabel || !short) return className ? /* @__PURE__ */ jsx("span", { className: cn("inline-flex", className), children: control }) : control;
|
|
161
|
+
return /* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-1", className), children: [
|
|
162
|
+
control,
|
|
163
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("whitespace-nowrap text-xs font-medium", TEXT_TONE[tone]), children: short })
|
|
164
|
+
] });
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
SyncStatusIndicator,
|
|
168
|
+
syncState
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=sync-status-indicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sync-status-indicator.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ComponentType } from \"react\";\nimport { Check, Cloud, CloudAlert, CloudOff, RefreshCw, UploadCloud } from \"lucide-react\";\nimport type { LucideProps } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useBillingLabels } from \"../billing/billing-labels\";\nimport { useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTopBarPartsLabels, type SyncStatusLabels } from \"./top-bar-parts-labels\";\n\n/**\n * What the indicator shows, fed by the app's sync engine (docs/top-bar-harmonization.md\n * §2.6). Small on purpose: keksdose's sync-status store, kastlan's handover controller and\n * Kurvenschmiede's online state + `useIsMutating` all fill it.\n */\nexport interface SyncStatus {\n /** What the engine is doing. `offline` is the engine's own verdict; `online: false` is\n * the browser's — either shows the offline state. */\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n /** The browser's `navigator.onLine` (or the app's better guess). */\n online: boolean;\n /** Writes kept on this device and not yet sent. */\n pending: number;\n /** Writes this device cannot send at all (keksdose: no key for the budget). Ranked first. */\n blocked?: number;\n /** Writes the server refused as read-only and keeps until a plan (billing §12.5). */\n waitingForPlan?: number;\n /** When the last good round ended — epoch ms, an ISO string or a `Date`. */\n lastSyncedAt?: number | string | Date | null;\n}\n\n/** The seven states, in their ranking: the first that holds wins. */\nexport type SyncState = \"blocked\" | \"waitingForPlan\" | \"offline\" | \"error\" | \"syncing\" | \"pending\" | \"synced\";\n\n/**\n * Which of the seven states a {@link SyncStatus} is in — keksdose's ranking\n * (sync-status-indicator.tsx): a write that cannot be sent at all ranks above everything,\n * because waiting cannot help it; then one waiting for a plan; then offline; then a refused\n * round; then a round under way; then writes waiting; else in sync. `syncing` also holds\n * while a tap's own round runs (`refreshing`), before the engine publishes its phase.\n */\nexport function syncState(status: SyncStatus, refreshing = false): SyncState {\n if ((status.blocked ?? 0) > 0) return \"blocked\";\n if ((status.waitingForPlan ?? 0) > 0) return \"waitingForPlan\";\n if (!status.online || status.phase === \"offline\") return \"offline\";\n if (status.phase === \"error\") return \"error\";\n if (status.phase === \"syncing\" || refreshing) return \"syncing\";\n if (status.pending > 0) return \"pending\";\n return \"synced\";\n}\n\ntype Tone = \"danger\" | \"warning\" | \"info\" | \"success\";\n\nconst LOOK: Record<SyncState, { Icon: ComponentType<LucideProps>; tone: Tone }> = {\n // The one the user must act on: the kit's danger, where keksdose had rose.\n blocked: { Icon: CloudAlert, tone: \"danger\" },\n // Safe, and a payment sends them: amber, the kit's warning.\n waitingForPlan: { Icon: CloudAlert, tone: \"warning\" },\n offline: { Icon: CloudOff, tone: \"warning\" },\n // Usually resolves by itself (a deploy, a read-only view): amber, not danger.\n error: { Icon: CloudAlert, tone: \"warning\" },\n syncing: { Icon: RefreshCw, tone: \"info\" },\n pending: { Icon: UploadCloud, tone: \"warning\" },\n synced: { Icon: Cloud, tone: \"success\" },\n};\n\nconst TEXT_TONE: Record<Tone, string> = {\n danger: \"text-danger\",\n warning: \"text-warning\",\n info: \"text-info\",\n success: \"text-success\",\n};\n\n/** The scope words of an indicator that covers only part of the app (§6.10). */\nexport interface SyncStatusScope {\n /** Put before every state in the tooltip: \"Handovers on this device\" →\n * \"Handovers on this device: 3 changes waiting to sync\". */\n name: string;\n /** The whole offline sentence instead of the kit's, when offline means something\n * narrower: \"Offline: handovers keep working on this device; everything else needs a\n * connection\". */\n offline?: string;\n}\n\nexport interface SyncStatusIndicatorProps {\n status: SyncStatus;\n /**\n * Run a round now (push, then pull, then re-read what is on screen). With it the\n * indicator is a \"sync now\" button, disabled offline and mid-round but keeping its\n * colour; a returned promise keeps the spin until it settles. Without it, a status only\n * (§6.9).\n */\n onSync?: () => void | Promise<unknown>;\n /** Render nothing while in sync — kastlan and Kurvenschmiede show it only while offline,\n * syncing or with changes waiting, held or refused (§2.6). */\n hideWhenIdle?: boolean;\n /** The app's words for its part of the app (§6.10). */\n scope?: SyncStatusScope;\n /**\n * Where the app keeps a change before the server has it. `\"device\"` (the default): an\n * offline-first app (keksdose, kastlan's handovers) — offline means \"kept here, sent\n * later\". `\"none\"`: an online-only app that keeps nothing on the device (Kurvenschmiede,\n * build review B1) — offline reads \"changes can't be saved until you reconnect\", a\n * round reads \"Saving…\", and `pending`, `blocked` and `waitingForPlan` are ignored,\n * since there is nowhere for a change to wait. \"Up to date\" when settled.\n */\n storage?: \"device\" | \"none\";\n /** Short words beside the cloud (\"Offline\", \"Syncing…\", \"3 waiting\") while not in sync. */\n showLabel?: boolean;\n /** Prop > `<UiKitProvider labels={{ topBarParts: { sync } }}>` > English. */\n labels?: LabelOverride<SyncStatusLabels>;\n /** The clock's language for \"Synced at 14:05\". Default: the provider's locale. */\n locale?: string;\n className?: string;\n /** `data-*` attributes go on the trigger — a tour's anchor (`data-tour=\"sync-status\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The `data-*` attributes of a rest object, for a trigger. */\nfunction dataAttributes(rest: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith(\"data-\")));\n}\n\n/**\n * The top bar's sync status (docs/top-bar-harmonization.md §2.6), lifted from keksdose's\n * `SyncStatusIndicator`: the seven states and their ranking ({@link syncState}), one cloud\n * per state, a ✓ badge when in sync and a spin while syncing. With `onSync` it is also the\n * \"sync now\" button, and its name says both what it is and what a tap does.\n *\n * Colours are the kit's roles — danger for a write that cannot be sent, warning for\n * anything waiting, info while syncing, success when in sync — where keksdose had raw\n * rose/amber/sky/emerald pairs.\n */\nexport function SyncStatusIndicator({\n status,\n onSync,\n hideWhenIdle = false,\n scope,\n showLabel = false,\n labels: labelsProp,\n locale: localeProp,\n className,\n storage = \"device\",\n ...rest\n}: SyncStatusIndicatorProps) {\n const words = useTopBarPartsLabels(labelsProp ? { sync: labelsProp } : undefined).sync;\n const billing = useBillingLabels();\n const locale = useKitLocale(localeProp);\n // The tap's own round, local rather than in the app's store: the store's phase belongs\n // to the sync engine, and a second writer would race the round itself.\n const [refreshing, setRefreshing] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const onlineOnly = storage === \"none\";\n // Online-only: nothing waits on the device, so only offline, error, saving and settled.\n const state = syncState(\n onlineOnly ? { ...status, pending: 0, blocked: 0, waitingForPlan: 0 } : status,\n refreshing,\n );\n const data = dataAttributes(rest);\n if (hideWhenIdle && state === \"synced\") return null;\n\n const { Icon, tone } = LOOK[state];\n const spin = state === \"syncing\";\n let text: string;\n switch (state) {\n case \"blocked\":\n text = words.blocked(status.blocked ?? 0);\n break;\n case \"waitingForPlan\":\n text = billing.waitingChanges(status.waitingForPlan ?? 0);\n break;\n case \"offline\":\n text = onlineOnly ? words.offlineUnsaved : words.offline;\n break;\n case \"error\":\n text = words.refused;\n break;\n case \"syncing\":\n text = onlineOnly ? words.saving : words.syncing;\n break;\n case \"pending\":\n text = words.pending(status.pending);\n break;\n default:\n text = !onlineOnly && status.lastSyncedAt\n ? words.syncedAt(\n // A clock time in the UI's language, never the browser's (keksdose live #246).\n new Date(status.lastSyncedAt).toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\" }),\n )\n : words.idle;\n }\n if (scope) {\n text = state === \"offline\" && scope.offline ? scope.offline : words.scoped(scope.name, text);\n }\n\n const short =\n state === \"offline\"\n ? words.offlineShort\n : state === \"syncing\"\n ? onlineOnly\n ? words.saving\n : words.syncingShort\n : state === \"error\"\n ? words.refusedShort\n : state === \"pending\" || state === \"blocked\" || state === \"waitingForPlan\"\n ? words.waitingShort(\n state === \"pending\"\n ? status.pending\n : state === \"blocked\"\n ? (status.blocked ?? 0)\n : (status.waitingForPlan ?? 0),\n )\n : null;\n\n const offline = state === \"offline\";\n const canSync = !!onSync && status.online && status.phase !== \"offline\" && !spin;\n const name = onSync\n ? canSync\n ? words.withHint(text, words.syncHint)\n : offline\n ? words.withHint(text, words.offlineHint)\n : text\n : text;\n const badge = state === \"synced\" ? <Check className=\"stroke-[3]\" /> : undefined;\n const glyph = <Icon aria-hidden className={spin ? \"animate-spin\" : undefined} />;\n\n const sync = async () => {\n if (!canSync || !onSync) return;\n setRefreshing(true);\n try {\n await onSync();\n } finally {\n if (mounted.current) setRefreshing(false);\n }\n };\n\n const control = onSync ? (\n <IconButton\n {...data}\n data-sync-state={state}\n size=\"2xs\"\n glyphSize={20}\n shape=\"round\"\n // A status that is also an action: disabled (offline, mid-round) is exactly when\n // the state matters most, so it keeps its colour.\n disabledStyle=\"keep\"\n tone={tone}\n quiet={false}\n label={name}\n labelVisible={false}\n // Below: the bar is at the top edge, where a bubble above has no room.\n tooltipSide=\"bottom\"\n onClick={() => void sync()}\n disabled={!canSync}\n // It announces itself as a status: a reader hears the state change.\n aria-live=\"polite\"\n badge={badge}\n >\n {glyph}\n </IconButton>\n ) : (\n <Tooltip label={name} side=\"bottom\">\n <span\n {...data}\n role=\"status\"\n data-sync-state={state}\n className={cn(\"relative inline-flex size-9 shrink-0 items-center justify-center [&>svg]:size-5\", TEXT_TONE[tone])}\n >\n {glyph}\n <span className=\"sr-only\">{name}</span>\n {badge && (\n <span\n aria-hidden\n className=\"pointer-events-none absolute bottom-1.5 end-1.5 inline-flex rounded-full bg-surface [&_svg]:size-3\"\n >\n {badge}\n </span>\n )}\n </span>\n </Tooltip>\n );\n\n if (!showLabel || !short) return className ? <span className={cn(\"inline-flex\", className)}>{control}</span> : control;\n return (\n <span className={cn(\"inline-flex items-center gap-1\", className)}>\n {control}\n {/* The eye's short words; the name above already says it in full. */}\n <span aria-hidden className={cn(\"whitespace-nowrap text-xs font-medium\", TEXT_TONE[tone])}>\n {short}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The top bar's items by name (docs/top-bar-harmonization.md §2.1, §6.6). Each is the
|
|
4
|
+
* app's own node — a kit part or its own — and any may be left out.
|
|
5
|
+
*/
|
|
6
|
+
export interface TopBarActionsProps {
|
|
7
|
+
/** kastlan's `CompanySwitcher`: the context, first. */
|
|
8
|
+
companySwitcher?: ReactNode;
|
|
9
|
+
/** `GlobalSearch` (⌘K). */
|
|
10
|
+
search?: ReactNode;
|
|
11
|
+
/** `ThemeToggle`. */
|
|
12
|
+
theme?: ReactNode;
|
|
13
|
+
/** `FeedbackMenu`. */
|
|
14
|
+
feedback?: ReactNode;
|
|
15
|
+
/** `TourMenu`. */
|
|
16
|
+
tours?: ReactNode;
|
|
17
|
+
/** kastlan's assistant, beside the tours while it is on. */
|
|
18
|
+
assistant?: ReactNode;
|
|
19
|
+
/** `SyncStatusIndicator`. */
|
|
20
|
+
sync?: ReactNode;
|
|
21
|
+
/** `DevSwitchers` (role, then plan). */
|
|
22
|
+
dev?: ReactNode;
|
|
23
|
+
/** kastlan's platform button. */
|
|
24
|
+
platform?: ReactNode;
|
|
25
|
+
/** The account menu. When it is set the signed-out slots are not drawn. */
|
|
26
|
+
account?: ReactNode;
|
|
27
|
+
/** Signed out (§6.6): the bar's `LanguageMenu`, since there is no account menu to hold
|
|
28
|
+
* the language. Drawn only without `account`. */
|
|
29
|
+
language?: ReactNode;
|
|
30
|
+
/** Signed out (§6.6): the "Sign in" button, where the account menu would be. Drawn only
|
|
31
|
+
* without `account`. */
|
|
32
|
+
signIn?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
/** §2.1's order, then the signed-out pair in the account menu's place. */
|
|
35
|
+
export declare const TOP_BAR_ACTION_ORDER: readonly ["companySwitcher", "search", "theme", "feedback", "tours", "assistant", "sync", "dev", "platform", "account", "language", "signIn"];
|
|
36
|
+
/**
|
|
37
|
+
* The top bar's actions in the one order every app shares (§2.1): company switcher ·
|
|
38
|
+
* Search · Theme · Feedback · Tours (· Assistant) · Sync · dev role · dev plan · platform ·
|
|
39
|
+
* account menu — and, signed out, Language · Sign in where the account menu goes (§6.6).
|
|
40
|
+
* The app fills the slots it has; an empty slot is skipped, so it leaves no gap.
|
|
41
|
+
*
|
|
42
|
+
* It draws a fragment, no box of its own: hand it to `TopBar`'s `actions`, whose row
|
|
43
|
+
* spaces the items.
|
|
44
|
+
*
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <TopBar brand={…} actions={<TopBarActions search={…} theme={…} tours={…} account={…} />} />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function TopBarActions(props: TopBarActionsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment as Fragment2 } from "react";
|
|
4
|
+
const TOP_BAR_ACTION_ORDER = [
|
|
5
|
+
"companySwitcher",
|
|
6
|
+
"search",
|
|
7
|
+
"theme",
|
|
8
|
+
"feedback",
|
|
9
|
+
"tours",
|
|
10
|
+
"assistant",
|
|
11
|
+
"sync",
|
|
12
|
+
"dev",
|
|
13
|
+
"platform",
|
|
14
|
+
"account",
|
|
15
|
+
"language",
|
|
16
|
+
"signIn"
|
|
17
|
+
];
|
|
18
|
+
const present = (node) => node !== void 0 && node !== null && node !== false && node !== "";
|
|
19
|
+
function TopBarActions(props) {
|
|
20
|
+
const signedOut = !present(props.account);
|
|
21
|
+
return /* @__PURE__ */ jsx(Fragment, { children: TOP_BAR_ACTION_ORDER.map((slot) => {
|
|
22
|
+
if ((slot === "language" || slot === "signIn") && !signedOut) return null;
|
|
23
|
+
const node = props[slot];
|
|
24
|
+
return present(node) ? /* @__PURE__ */ jsx(Fragment2, { children: node }, slot) : null;
|
|
25
|
+
}) });
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
TOP_BAR_ACTION_ORDER,
|
|
29
|
+
TopBarActions
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=top-bar-actions.js.map
|
|
@@ -0,0 +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 /** `DevSwitchers` (role, then plan). */\n dev?: ReactNode;\n /** kastlan's platform button. */\n platform?: 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 \"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 * 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":";AAsEI,mBAI2B,WAJ3B;AAtEJ,SAAS,YAAAA,iBAAgB;AAqClB,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;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"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
2
|
+
/**
|
|
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 `DevSwitchers`. A module
|
|
5
|
+
* 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 dev role
|
|
7
|
+
* switcher's name and tooltip are `topBar.switchRole` / `topBar.role`, as `RoleSwitcher`'s.
|
|
8
|
+
*/
|
|
9
|
+
/** `TourMenu`'s words (§2.4). */
|
|
10
|
+
export interface TourMenuLabels {
|
|
11
|
+
/** The compass's name, and the menu's. */
|
|
12
|
+
menu: string;
|
|
13
|
+
/** The NEW dot on the compass, for a reader: it joins the trigger's name. */
|
|
14
|
+
newDot: string;
|
|
15
|
+
/** The heading over the tours that start on the open page. */
|
|
16
|
+
thisPage: string;
|
|
17
|
+
/** The heading over the untaken new tours. */
|
|
18
|
+
newTours: string;
|
|
19
|
+
/** The last row, the way to the app's tours page (only with `href`). */
|
|
20
|
+
allTours: string;
|
|
21
|
+
/** The chip on a new tour's row. */
|
|
22
|
+
badgeNew: string;
|
|
23
|
+
/** What the ✓ on a taken tour's row says to a reader. */
|
|
24
|
+
completed: string;
|
|
25
|
+
/** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */
|
|
26
|
+
badgeUpdated: string;
|
|
27
|
+
/** The dot on the compass while only updated tours light it, for a reader. */
|
|
28
|
+
updatedDot: string;
|
|
29
|
+
/** The heading over the untaken new tours when updated ones are among them. */
|
|
30
|
+
newAndUpdated: string;
|
|
31
|
+
}
|
|
32
|
+
/** `SyncStatusIndicator`'s words (§2.6). The "waiting for a plan" state is
|
|
33
|
+
* `billing.waitingChanges`, the kit's words for it since billing §12.5. */
|
|
34
|
+
export interface SyncStatusLabels {
|
|
35
|
+
/** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */
|
|
36
|
+
blocked: (count: number) => string;
|
|
37
|
+
offline: string;
|
|
38
|
+
/** The server refused the last round. */
|
|
39
|
+
refused: string;
|
|
40
|
+
syncing: string;
|
|
41
|
+
pending: (count: number) => string;
|
|
42
|
+
/** Settled, with the time of the last good round (already formatted). */
|
|
43
|
+
syncedAt: (time: string) => string;
|
|
44
|
+
/** Settled, before the first round. */
|
|
45
|
+
idle: string;
|
|
46
|
+
/** Joined to the state when a tap syncs: what the tap does. */
|
|
47
|
+
syncHint: string;
|
|
48
|
+
/** Joined to the state while offline, when a tap can do nothing. */
|
|
49
|
+
offlineHint: string;
|
|
50
|
+
/** The state and what a tap does, as one tooltip: "Offline — nothing to sync…". */
|
|
51
|
+
withHint: (state: string, hint: string) => string;
|
|
52
|
+
/** The state with the app's `scope` words before it: "Handovers on this device: …". */
|
|
53
|
+
scoped: (scope: string, state: string) => string;
|
|
54
|
+
/** The short words beside the cloud (`showLabel`): offline. */
|
|
55
|
+
offlineShort: string;
|
|
56
|
+
/** The short words while syncing. */
|
|
57
|
+
syncingShort: string;
|
|
58
|
+
/** The short words while changes wait (pending, held for a plan, or blocked). */
|
|
59
|
+
waitingShort: (count: number) => string;
|
|
60
|
+
/** The short words after a refused round. */
|
|
61
|
+
refusedShort: string;
|
|
62
|
+
/** Offline in an online-only app (`storage="none"`): nothing is kept on the device. */
|
|
63
|
+
offlineUnsaved: string;
|
|
64
|
+
/** A round under way in an online-only app — it saves, it doesn't sync. Also its short
|
|
65
|
+
* words beside the cloud. */
|
|
66
|
+
saving: string;
|
|
67
|
+
}
|
|
68
|
+
/** `DevSwitchers`' words (§2.7, §6.2, §6.3). */
|
|
69
|
+
export interface DevSwitchersLabels {
|
|
70
|
+
/** The heading of the role menu. */
|
|
71
|
+
roleHeading: string;
|
|
72
|
+
/** The plan switcher's name. */
|
|
73
|
+
switchPlan: string;
|
|
74
|
+
/** The plan switcher's tooltip, given the active plan. */
|
|
75
|
+
plan: (value: string) => string;
|
|
76
|
+
/** The heading of the plan menu. */
|
|
77
|
+
planHeading: string;
|
|
78
|
+
/** The row back to the role the session started with. */
|
|
79
|
+
backTo: (role: string) => string;
|
|
80
|
+
/** The plan menu's note while billing is off locally. */
|
|
81
|
+
billingOff: string;
|
|
82
|
+
}
|
|
83
|
+
export interface TopBarPartsLabels {
|
|
84
|
+
tours: TourMenuLabels;
|
|
85
|
+
sync: SyncStatusLabels;
|
|
86
|
+
dev: DevSwitchersLabels;
|
|
87
|
+
}
|
|
88
|
+
export declare const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels;
|
|
89
|
+
/** The `topBarParts` namespace, resolved — English, then the provider, then `labels`. */
|
|
90
|
+
export declare function useTopBarPartsLabels(labels?: LabelOverride<TopBarPartsLabels>): TopBarPartsLabels;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
3
|
+
const DEFAULT_TOP_BAR_PARTS_LABELS = {
|
|
4
|
+
tours: {
|
|
5
|
+
menu: "Guided tours",
|
|
6
|
+
newDot: "New tours",
|
|
7
|
+
thisPage: "On this page",
|
|
8
|
+
newTours: "New tours",
|
|
9
|
+
allTours: "All tours\u2026",
|
|
10
|
+
badgeNew: "New",
|
|
11
|
+
completed: "Completed",
|
|
12
|
+
badgeUpdated: "Updated",
|
|
13
|
+
updatedDot: "Updated tours",
|
|
14
|
+
newAndUpdated: "New and updated"
|
|
15
|
+
},
|
|
16
|
+
sync: {
|
|
17
|
+
blocked: (count) => count === 1 ? "1 change cannot be sent from this device" : `${count} changes cannot be sent from this device`,
|
|
18
|
+
offline: "Offline \u2014 changes are saved on this device and will sync when you reconnect",
|
|
19
|
+
refused: "The server refused the last sync",
|
|
20
|
+
syncing: "Syncing\u2026",
|
|
21
|
+
pending: (count) => count === 1 ? "1 change waiting to sync" : `${count} changes waiting to sync`,
|
|
22
|
+
syncedAt: (time) => `Synced at ${time}`,
|
|
23
|
+
idle: "Up to date",
|
|
24
|
+
syncHint: "Tap to sync now",
|
|
25
|
+
offlineHint: "nothing to sync while offline",
|
|
26
|
+
withHint: (state, hint) => `${state} \u2014 ${hint}`,
|
|
27
|
+
scoped: (scope, state) => `${scope}: ${state}`,
|
|
28
|
+
offlineShort: "Offline",
|
|
29
|
+
syncingShort: "Syncing\u2026",
|
|
30
|
+
waitingShort: (count) => `${count} waiting`,
|
|
31
|
+
refusedShort: "Not synced",
|
|
32
|
+
offlineUnsaved: "Offline \u2014 changes can't be saved until you reconnect",
|
|
33
|
+
saving: "Saving\u2026"
|
|
34
|
+
},
|
|
35
|
+
dev: {
|
|
36
|
+
roleHeading: "Dev \xB7 role",
|
|
37
|
+
switchPlan: "Switch plan",
|
|
38
|
+
plan: (value) => `Plan: ${value}`,
|
|
39
|
+
planHeading: "Dev \xB7 plan",
|
|
40
|
+
backTo: (role) => `Back to ${role}`,
|
|
41
|
+
billingOff: "Billing is off locally: plans change nothing"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function useTopBarPartsLabels(labels) {
|
|
45
|
+
return useKitLabels("topBarParts", DEFAULT_TOP_BAR_PARTS_LABELS, labels);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
DEFAULT_TOP_BAR_PARTS_LABELS,
|
|
49
|
+
useTopBarPartsLabels
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=top-bar-parts-labels.js.map
|
|
@@ -0,0 +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`. 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 dev role\n * switcher's name and tooltip are `topBar.switchRole` / `topBar.role`, as `RoleSwitcher`'s.\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/** `DevSwitchers`' words (§2.7, §6.2, §6.3). */\nexport interface DevSwitchersLabels {\n /** The heading of the role menu. */\n roleHeading: string;\n /** The plan switcher's name. */\n switchPlan: string;\n /** The plan switcher's tooltip, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan menu. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan menu'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 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;AA6F1C,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,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":[]}
|
|
@@ -105,7 +105,7 @@ export interface TopBarMenuHeader {
|
|
|
105
105
|
* hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps
|
|
106
106
|
* rather than widening it.
|
|
107
107
|
*/
|
|
108
|
-
export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabel, heading, header, entries, children, footer, footerLabel, align, panelClassName, }: {
|
|
108
|
+
export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabel, heading, header, entries, leading, children, footer, footerLabel, align, panelClassName, triggerAttributes, }: {
|
|
109
109
|
/** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
|
|
110
110
|
icon?: ReactNode;
|
|
111
111
|
/**
|
|
@@ -156,6 +156,13 @@ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClas
|
|
|
156
156
|
* `close` shuts the menu.
|
|
157
157
|
*/
|
|
158
158
|
children?: (close: () => void) => ReactNode;
|
|
159
|
+
/**
|
|
160
|
+
* Rows like {@link children}, but BEFORE `entries`, under the header — the account
|
|
161
|
+
* menu's `AccountMenuLanguage`, which goes first after the app's context rows (§6.5)
|
|
162
|
+
* in a menu whose other rows are all entries (kastlan, build review B1). Render `<li>`s;
|
|
163
|
+
* `close` shuts the menu.
|
|
164
|
+
*/
|
|
165
|
+
leading?: (close: () => void) => ReactNode;
|
|
159
166
|
/**
|
|
160
167
|
* Free content below the rows, behind a rule — the legal links. Its links and
|
|
161
168
|
* buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a
|
|
@@ -168,4 +175,9 @@ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClas
|
|
|
168
175
|
* direction (see `HoverMenu`, whose type this is). Default `end`. */
|
|
169
176
|
align?: HoverMenuProps["align"];
|
|
170
177
|
panelClassName?: string;
|
|
178
|
+
/** `data-*` attributes for the trigger itself — a tour's anchor (`data-tour`), a test
|
|
179
|
+
* hook. The parts built on this menu (`TourMenu`) pass theirs through here. */
|
|
180
|
+
triggerAttributes?: {
|
|
181
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
182
|
+
};
|
|
171
183
|
}): import("react").JSX.Element;
|
|
@@ -59,11 +59,13 @@ function TopBarActionMenu({
|
|
|
59
59
|
heading,
|
|
60
60
|
header,
|
|
61
61
|
entries,
|
|
62
|
+
leading,
|
|
62
63
|
children,
|
|
63
64
|
footer,
|
|
64
65
|
footerLabel,
|
|
65
66
|
align,
|
|
66
|
-
panelClassName
|
|
67
|
+
panelClassName,
|
|
68
|
+
triggerAttributes
|
|
67
69
|
}) {
|
|
68
70
|
const uid = useId();
|
|
69
71
|
return /* @__PURE__ */ jsx(
|
|
@@ -72,7 +74,7 @@ function TopBarActionMenu({
|
|
|
72
74
|
"aria-label": ariaLabel,
|
|
73
75
|
align,
|
|
74
76
|
trigger: ({ open, toggle, close }) => {
|
|
75
|
-
const control = (className, children2, label) => href ? /* @__PURE__ */ jsx(Link, { to: href, onClick: close, "aria-label": label, className, children: children2 }) : /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, "aria-label": label, className, children: children2 });
|
|
77
|
+
const control = (className, children2, label) => href ? /* @__PURE__ */ jsx(Link, { ...triggerAttributes, to: href, onClick: close, "aria-label": label, className, children: children2 }) : /* @__PURE__ */ jsx("button", { ...triggerAttributes, type: "button", onClick: toggle, "aria-label": label, className, children: children2 });
|
|
76
78
|
if (trigger) {
|
|
77
79
|
return control(
|
|
78
80
|
cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName),
|
|
@@ -142,6 +144,7 @@ function TopBarActionMenu({
|
|
|
142
144
|
headerExtra != null && headerExtra !== false && /* @__PURE__ */ jsx("div", { className: "mt-1", children: headerExtra })
|
|
143
145
|
] }),
|
|
144
146
|
heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder", children: heading }),
|
|
147
|
+
leading?.(close),
|
|
145
148
|
toBlocks(entries).map((block, i, blocks) => {
|
|
146
149
|
if (block.kind === "loose") {
|
|
147
150
|
const entry = block.entry;
|