@eifi1/ui-kit 0.43.0 → 0.45.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.
Files changed (134) hide show
  1. package/README.md +11 -7
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +6 -0
  8. package/dist/admin/admin-area-labels.js +1 -0
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  11. package/dist/admin/mail-test-card-labels.js +17 -0
  12. package/dist/admin/mail-test-card-labels.js.map +1 -0
  13. package/dist/admin/mail-test-card.d.ts +74 -0
  14. package/dist/admin/mail-test-card.js +80 -0
  15. package/dist/admin/mail-test-card.js.map +1 -0
  16. package/dist/billing/provider-page.d.ts +9 -0
  17. package/dist/billing/provider-page.js.map +1 -1
  18. package/dist/components/data-table-mobile.js +17 -2
  19. package/dist/components/data-table-mobile.js.map +1 -1
  20. package/dist/components/data-table-shared.d.ts +2 -0
  21. package/dist/components/data-table-shared.js +8 -0
  22. package/dist/components/data-table-shared.js.map +1 -1
  23. package/dist/components/data-table.js +1 -6
  24. package/dist/components/data-table.js.map +1 -1
  25. package/dist/eslint/index.js +1 -1
  26. package/dist/i18n/defaults.js +9 -1
  27. package/dist/i18n/defaults.js.map +1 -1
  28. package/dist/i18n/german.js +42 -2
  29. package/dist/i18n/german.js.map +1 -1
  30. package/dist/i18n/kit-labels.d.ts +13 -0
  31. package/dist/i18n/kit-labels.js.map +1 -1
  32. package/dist/i18n/locales/en.js +60 -4
  33. package/dist/i18n/locales/en.js.map +3 -3
  34. package/dist/i18n/locales/es.js +40 -0
  35. package/dist/i18n/locales/es.js.map +1 -1
  36. package/dist/i18n/locales/fr.js +42 -2
  37. package/dist/i18n/locales/fr.js.map +1 -1
  38. package/dist/i18n/locales/hu.js +40 -0
  39. package/dist/i18n/locales/hu.js.map +1 -1
  40. package/dist/i18n/locales/it.js +41 -1
  41. package/dist/i18n/locales/it.js.map +1 -1
  42. package/dist/i18n/locales/zh.js +40 -0
  43. package/dist/i18n/locales/zh.js.map +1 -1
  44. package/dist/i18n/review.js +13 -1
  45. package/dist/i18n/review.js.map +1 -1
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +5 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/lib/format.d.ts +2 -1
  50. package/dist/lib/format.js +6 -1
  51. package/dist/lib/format.js.map +1 -1
  52. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  53. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  54. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  55. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  56. package/dist/offline/pwa-update-prompt.js +36 -0
  57. package/dist/offline/pwa-update-prompt.js.map +1 -0
  58. package/dist/offline/query-persistence.d.ts +100 -0
  59. package/dist/offline/query-persistence.js +179 -0
  60. package/dist/offline/query-persistence.js.map +1 -0
  61. package/dist/offline/shared-computer-labels.d.ts +2 -0
  62. package/dist/offline/shared-computer-labels.js +9 -0
  63. package/dist/offline/shared-computer-labels.js.map +1 -0
  64. package/dist/offline/shared-computer.d.ts +44 -0
  65. package/dist/offline/shared-computer.js +51 -0
  66. package/dist/offline/shared-computer.js.map +1 -0
  67. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  68. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  69. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  70. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  71. package/dist/offline/sign-out-pending-dialog.js +66 -0
  72. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  73. package/dist/offline.d.ts +8 -0
  74. package/dist/offline.js +24 -0
  75. package/dist/offline.js.map +1 -0
  76. package/dist/shell/admin-menu.d.ts +24 -0
  77. package/dist/shell/admin-menu.js +12 -3
  78. package/dist/shell/admin-menu.js.map +1 -1
  79. package/dist/shell.d.ts +2 -0
  80. package/dist/shell.js +2 -0
  81. package/dist/shell.js.map +1 -1
  82. package/dist/tour/tour-catalogue-labels.js +2 -2
  83. package/dist/tour/tour-catalogue-labels.js.map +1 -1
  84. package/dist/tour/tour-catalogue.d.ts +7 -1
  85. package/dist/tour/tour-catalogue.js.map +1 -1
  86. package/dist/tour/tour-info.d.ts +2 -2
  87. package/dist/tour/tour-info.js.map +1 -1
  88. package/dist/tour/tour.d.ts +38 -2
  89. package/dist/tour/tour.js +42 -3
  90. package/dist/tour/tour.js.map +1 -1
  91. package/dist/tour/use-tour-progress.d.ts +165 -12
  92. package/dist/tour/use-tour-progress.js +271 -25
  93. package/dist/tour/use-tour-progress.js.map +1 -1
  94. package/dist/tour.d.ts +3 -1
  95. package/dist/tour.js +4 -1
  96. package/dist/tour.js.map +1 -1
  97. package/package.json +25 -1
  98. package/src/admin/admin-action-log-labels.ts +9 -0
  99. package/src/admin/admin-action-log.tsx +41 -5
  100. package/src/admin/admin-area-labels.ts +7 -0
  101. package/src/admin/mail-test-card-labels.ts +19 -0
  102. package/src/admin/mail-test-card.tsx +186 -0
  103. package/src/billing/provider-page.ts +9 -0
  104. package/src/components/data-table-mobile.tsx +26 -3
  105. package/src/components/data-table-shared.tsx +18 -1
  106. package/src/components/data-table.tsx +1 -15
  107. package/src/i18n/defaults.ts +8 -0
  108. package/src/i18n/german.ts +48 -2
  109. package/src/i18n/kit-labels.tsx +13 -0
  110. package/src/i18n/locales/en.ts +13 -1
  111. package/src/i18n/locales/es.ts +46 -0
  112. package/src/i18n/locales/fr.ts +48 -2
  113. package/src/i18n/locales/hu.ts +42 -0
  114. package/src/i18n/locales/it.ts +47 -1
  115. package/src/i18n/locales/zh.ts +40 -0
  116. package/src/i18n/review.ts +12 -0
  117. package/src/index.ts +3 -0
  118. package/src/lib/format.ts +13 -2
  119. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  120. package/src/offline/pwa-update-prompt.tsx +72 -0
  121. package/src/offline/query-persistence.ts +323 -0
  122. package/src/offline/shared-computer-labels.ts +8 -0
  123. package/src/offline/shared-computer.tsx +87 -0
  124. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  125. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  126. package/src/offline.ts +26 -0
  127. package/src/shell/admin-menu.tsx +40 -3
  128. package/src/shell.ts +4 -0
  129. package/src/tour/tour-catalogue-labels.ts +2 -2
  130. package/src/tour/tour-catalogue.tsx +7 -1
  131. package/src/tour/tour-info.ts +2 -2
  132. package/src/tour/tour.tsx +87 -3
  133. package/src/tour/use-tour-progress.ts +501 -34
  134. package/src/tour.ts +5 -1
@@ -0,0 +1,44 @@
1
+ import { type LabelOverride } from "../i18n/kit-labels.js";
2
+ import { DEFAULT_SHARED_COMPUTER_LABELS } from "./shared-computer-labels.js";
3
+ /**
4
+ * "This is a shared computer" — a choice on the sign-in form that persists nothing
5
+ * (docs/local-first-harmonization.md §1.5 D6, D7). When it is on, the app does not mount
6
+ * the query persistence (`createQueryPersistence`): the cache lives in memory only.
7
+ *
8
+ * The choice must live exactly as long as the session it governs. By default it is held in
9
+ * `sessionStorage`, which fits a session that ends with the tab. An app whose session
10
+ * outlives the tab (a refresh token in `localStorage`: kastlan) passes `storage: "local"`
11
+ * and calls `setSharedComputer(false)` at every end of that session (logout, a failed
12
+ * refresh), beside removing the token. `isSharedComputer()` reads both places, so a choice
13
+ * made in either keeps the cache in memory. Where storage is blocked, `isSharedComputer()`
14
+ * is `false` and `setSharedComputer` does nothing.
15
+ */
16
+ export interface SharedComputerLabels {
17
+ label: string;
18
+ /** The one line under it: what the choice does. */
19
+ hint: string;
20
+ }
21
+ export { DEFAULT_SHARED_COMPUTER_LABELS };
22
+ /** Where the choice is held: `"session"` (this tab, the default) or `"local"` (as long as
23
+ * a session kept in `localStorage`, until `setSharedComputer(false)`). */
24
+ export type SharedComputerStorage = "session" | "local";
25
+ /** Whether this session was signed in as on a shared computer, the choice held in either
26
+ * place. `false` when storage is unavailable. */
27
+ export declare function isSharedComputer(): boolean;
28
+ /** Records the choice in `options.storage` (default `"session"`), and only there; `false`
29
+ * forgets it in both places. */
30
+ export declare function setSharedComputer(on: boolean, options?: {
31
+ storage?: SharedComputerStorage;
32
+ }): void;
33
+ export interface SharedComputerChoiceProps {
34
+ /** Controlled. Left out, the box starts from {@link isSharedComputer}. */
35
+ checked?: boolean;
36
+ /** Told after the choice is stored. */
37
+ onCheckedChange?: (on: boolean) => void;
38
+ /** Where the choice is stored: see {@link setSharedComputer}. Default `"session"`. */
39
+ storage?: SharedComputerStorage;
40
+ disabled?: boolean;
41
+ className?: string;
42
+ labels?: LabelOverride<SharedComputerLabels>;
43
+ }
44
+ export declare function SharedComputerChoice({ checked, onCheckedChange, storage, disabled, className, labels: labelsProp, }: SharedComputerChoiceProps): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { useKitLabels } from "../i18n/kit-labels.js";
5
+ import { Checkbox } from "../components/checkbox.js";
6
+ import { readSessionStored, readStored, removeStored, writeSessionStored, writeStored } from "../lib/safe-storage.js";
7
+ import { DEFAULT_SHARED_COMPUTER_LABELS } from "./shared-computer-labels.js";
8
+ const KEY = "eifi1-shared-computer";
9
+ function isSharedComputer() {
10
+ return readSessionStored(KEY) === "1" || readStored(KEY) === "1";
11
+ }
12
+ function setSharedComputer(on, options = {}) {
13
+ const local = options.storage === "local";
14
+ writeSessionStored(KEY, on && !local ? "1" : null);
15
+ if (on && local) writeStored(KEY, "1");
16
+ else removeStored(KEY);
17
+ }
18
+ function SharedComputerChoice({
19
+ checked,
20
+ onCheckedChange,
21
+ storage,
22
+ disabled,
23
+ className,
24
+ labels: labelsProp
25
+ }) {
26
+ const labels = useKitLabels("sharedComputer", DEFAULT_SHARED_COMPUTER_LABELS, labelsProp);
27
+ const [own, setOwn] = useState(isSharedComputer);
28
+ const on = checked ?? own;
29
+ return /* @__PURE__ */ jsx(
30
+ Checkbox,
31
+ {
32
+ className,
33
+ label: labels.label,
34
+ description: labels.hint,
35
+ checked: on,
36
+ disabled,
37
+ onCheckedChange: (next) => {
38
+ setSharedComputer(next, { storage });
39
+ setOwn(next);
40
+ onCheckedChange?.(next);
41
+ }
42
+ }
43
+ );
44
+ }
45
+ export {
46
+ DEFAULT_SHARED_COMPUTER_LABELS,
47
+ SharedComputerChoice,
48
+ isSharedComputer,
49
+ setSharedComputer
50
+ };
51
+ //# sourceMappingURL=shared-computer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/offline/shared-computer.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Checkbox } from \"../components/checkbox\";\nimport { readSessionStored, readStored, removeStored, writeSessionStored, writeStored } from \"../lib/safe-storage\";\nimport { DEFAULT_SHARED_COMPUTER_LABELS } from \"./shared-computer-labels\";\n\n/**\n * \"This is a shared computer\" — a choice on the sign-in form that persists nothing\n * (docs/local-first-harmonization.md §1.5 D6, D7). When it is on, the app does not mount\n * the query persistence (`createQueryPersistence`): the cache lives in memory only.\n *\n * The choice must live exactly as long as the session it governs. By default it is held in\n * `sessionStorage`, which fits a session that ends with the tab. An app whose session\n * outlives the tab (a refresh token in `localStorage`: kastlan) passes `storage: \"local\"`\n * and calls `setSharedComputer(false)` at every end of that session (logout, a failed\n * refresh), beside removing the token. `isSharedComputer()` reads both places, so a choice\n * made in either keeps the cache in memory. Where storage is blocked, `isSharedComputer()`\n * is `false` and `setSharedComputer` does nothing.\n */\nexport interface SharedComputerLabels {\n label: string;\n /** The one line under it: what the choice does. */\n hint: string;\n}\n\n// Defined in ./shared-computer-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SHARED_COMPUTER_LABELS };\n\nconst KEY = \"eifi1-shared-computer\";\n\n/** Where the choice is held: `\"session\"` (this tab, the default) or `\"local\"` (as long as\n * a session kept in `localStorage`, until `setSharedComputer(false)`). */\nexport type SharedComputerStorage = \"session\" | \"local\";\n\n/** Whether this session was signed in as on a shared computer, the choice held in either\n * place. `false` when storage is unavailable. */\nexport function isSharedComputer(): boolean {\n return readSessionStored(KEY) === \"1\" || readStored(KEY) === \"1\";\n}\n\n/** Records the choice in `options.storage` (default `\"session\"`), and only there; `false`\n * forgets it in both places. */\nexport function setSharedComputer(on: boolean, options: { storage?: SharedComputerStorage } = {}): void {\n const local = options.storage === \"local\";\n writeSessionStored(KEY, on && !local ? \"1\" : null);\n if (on && local) writeStored(KEY, \"1\");\n else removeStored(KEY);\n}\n\nexport interface SharedComputerChoiceProps {\n /** Controlled. Left out, the box starts from {@link isSharedComputer}. */\n checked?: boolean;\n /** Told after the choice is stored. */\n onCheckedChange?: (on: boolean) => void;\n /** Where the choice is stored: see {@link setSharedComputer}. Default `\"session\"`. */\n storage?: SharedComputerStorage;\n disabled?: boolean;\n className?: string;\n labels?: LabelOverride<SharedComputerLabels>;\n}\n\nexport function SharedComputerChoice({\n checked,\n onCheckedChange,\n storage,\n disabled,\n className,\n labels: labelsProp,\n}: SharedComputerChoiceProps) {\n const labels = useKitLabels(\"sharedComputer\", DEFAULT_SHARED_COMPUTER_LABELS, labelsProp);\n const [own, setOwn] = useState(isSharedComputer);\n const on = checked ?? own;\n return (\n <Checkbox\n className={className}\n label={labels.label}\n description={labels.hint}\n checked={on}\n disabled={disabled}\n onCheckedChange={(next) => {\n setSharedComputer(next, { storage });\n setOwn(next);\n onCheckedChange?.(next);\n }}\n />\n );\n}\n"],"mappings":";AAyEI;AAzEJ,SAAS,gBAAgB;AACzB,SAAS,oBAAwC;AACjD,SAAS,gBAAgB;AACzB,SAAS,mBAAmB,YAAY,cAAc,oBAAoB,mBAAmB;AAC7F,SAAS,sCAAsC;AAwB/C,MAAM,MAAM;AAQL,SAAS,mBAA4B;AAC1C,SAAO,kBAAkB,GAAG,MAAM,OAAO,WAAW,GAAG,MAAM;AAC/D;AAIO,SAAS,kBAAkB,IAAa,UAA+C,CAAC,GAAS;AACtG,QAAM,QAAQ,QAAQ,YAAY;AAClC,qBAAmB,KAAK,MAAM,CAAC,QAAQ,MAAM,IAAI;AACjD,MAAI,MAAM,MAAO,aAAY,KAAK,GAAG;AAAA,MAChC,cAAa,GAAG;AACvB;AAcO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA8B;AAC5B,QAAM,SAAS,aAAa,kBAAkB,gCAAgC,UAAU;AACxF,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,gBAAgB;AAC/C,QAAM,KAAK,WAAW;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS;AAAA,MACT;AAAA,MACA,iBAAiB,CAAC,SAAS;AACzB,0BAAkB,MAAM,EAAE,QAAQ,CAAC;AACnC,eAAO,IAAI;AACX,0BAAkB,IAAI;AAAA,MACxB;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,2 @@
1
+ import type { SignOutPendingDialogLabels } from "./sign-out-pending-dialog.js";
2
+ export declare const DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS: SignOutPendingDialogLabels;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ const DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS = {
3
+ title: "Some changes have not been saved to the server yet",
4
+ lost: (count) => count === 1 ? "1 change is still waiting to sync. Logging out clears this device\u2019s copy, and it is the only copy." : `${count} changes are still waiting to sync. Logging out clears this device\u2019s copy, and it is the only copy.`,
5
+ kept: (count) => count === 1 ? "1 change is still waiting to sync. It stays on this device for you and syncs the next time you log in here." : `${count} changes are still waiting to sync. They stay on this device for you and sync the next time you log in here.`,
6
+ syncNow: "Sync now",
7
+ signOutAnyway: "Log out anyway",
8
+ discardWaiting: "Discard waiting changes",
9
+ cancel: "Cancel"
10
+ };
11
+ export {
12
+ DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS
13
+ };
14
+ //# sourceMappingURL=sign-out-pending-dialog-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/offline/sign-out-pending-dialog-labels.ts"],"sourcesContent":["// The English words of ./sign-out-pending-dialog, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component (0.34, KS U8).\n// keksdose's reviewed words (nav.logout_queued_*, account-menu.tsx:247-317) are the canon.\n\nimport type { SignOutPendingDialogLabels } from \"./sign-out-pending-dialog\";\n\nexport const DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS: SignOutPendingDialogLabels = {\n title: \"Some changes have not been saved to the server yet\",\n lost: (count) =>\n count === 1\n ? \"1 change is still waiting to sync. Logging out clears this device’s copy, and it is the only copy.\"\n : `${count} changes are still waiting to sync. Logging out clears this device’s copy, and it is the only copy.`,\n kept: (count) =>\n count === 1\n ? \"1 change is still waiting to sync. It stays on this device for you and syncs the next time you log in here.\"\n : `${count} changes are still waiting to sync. They stay on this device for you and sync the next time you log in here.`,\n syncNow: \"Sync now\",\n signOutAnyway: \"Log out anyway\",\n discardWaiting: \"Discard waiting changes\",\n cancel: \"Cancel\",\n};\n"],"mappings":";AAMO,MAAM,yCAAqE;AAAA,EAChF,OAAO;AAAA,EACP,MAAM,CAAC,UACL,UAAU,IACN,4GACA,GAAG,KAAK;AAAA,EACd,MAAM,CAAC,UACL,UAAU,IACN,gHACA,GAAG,KAAK;AAAA,EACd,SAAS;AAAA,EACT,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,QAAQ;AACV;","names":[]}
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from "react";
2
+ import { type LabelOverride } from "../i18n/kit-labels.js";
3
+ import { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./sign-out-pending-dialog-labels.js";
4
+ /**
5
+ * The question before an explicit sign-out while changes still wait to sync — keksdose's
6
+ * confirm (account-menu.tsx:247-317) as a kit dialog (docs/local-first-harmonization.md
7
+ * §1.5 D6, §1.6). Show it only when `queued > 0` and only on a sign-out someone asked for:
8
+ * never from a session-ended hook, which cannot ask anyone.
9
+ *
10
+ * "Sync now" is the primary action: the queued writes are the person's own data. It runs
11
+ * `onSyncNow`, is busy while that runs, then calls `onClose` without signing out — the
12
+ * count has moved and the caller re-evaluates (signs out at once when nothing is left, or
13
+ * opens this again with the new number). "Log out anyway" stays as the escape hatch, usable
14
+ * while a sync runs (a hanging one would trap the person otherwise): a device that cannot
15
+ * reach the server must still be able to sign out. Only a running discard holds it.
16
+ *
17
+ * The words depend on what the app does with the queue at sign-out (`keepsChanges`):
18
+ * kastlan keeps the outbox for its owner, keksdose wipes it — and keksdose's copy must not
19
+ * say the changes stay.
20
+ */
21
+ export interface SignOutPendingDialogLabels {
22
+ title: string;
23
+ /** `keepsChanges={false}`: signing out destroys the device's copy. */
24
+ lost: (count: number) => string;
25
+ /** `keepsChanges`: the queue stays on the device for its owner. */
26
+ kept: (count: number) => string;
27
+ syncNow: string;
28
+ signOutAnyway: string;
29
+ /** Only with `onDiscardWaiting` and `waitingForPlan > 0` (the billing kit's words). */
30
+ discardWaiting: string;
31
+ cancel: string;
32
+ }
33
+ export { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS };
34
+ export interface SignOutPendingDialogProps {
35
+ open: boolean;
36
+ /** Changes not yet on the server: pending + blocked. The tour `:pending` record is not
37
+ * one of them (D6). */
38
+ queued: number;
39
+ /** Of those, the ones waiting for a plan (billing's read-only refusal). */
40
+ waitingForPlan: number;
41
+ /** Runs one sync round. The dialog closes when it settles, whatever it managed. */
42
+ onSyncNow: () => Promise<unknown> | void;
43
+ /** Signs out now, queue or not. */
44
+ onSignOutAnyway: () => void;
45
+ /** Lets the plan-waiting changes go; the dialog then signs out as asked. Without it
46
+ * (or with `waitingForPlan` 0) the button is not shown. */
47
+ onDiscardWaiting?: () => Promise<unknown> | void;
48
+ /** `true` (kastlan): the changes stay on this device for their owner. `false`
49
+ * (keksdose): they are lost. Picks the words. */
50
+ keepsChanges: boolean;
51
+ /** Room under the sentence — the billing kit's `waitingChanges` line. Shown only while
52
+ * `waitingForPlan > 0`, so a 0-count line never shows. */
53
+ extra?: ReactNode;
54
+ /** Cancel, Escape, the X — and after "Sync now". */
55
+ onClose: () => void;
56
+ labels?: LabelOverride<SignOutPendingDialogLabels>;
57
+ }
58
+ export declare function SignOutPendingDialog({ open, queued, waitingForPlan, onSyncNow, onSignOutAnyway, onDiscardWaiting, keepsChanges, extra, onClose, labels: labelsProp, }: SignOutPendingDialogProps): import("react").JSX.Element;
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { useKitLabels } from "../i18n/kit-labels.js";
5
+ import { Button } from "../components/button.js";
6
+ import { DialogFrame } from "../components/dialog-frame.js";
7
+ import { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./sign-out-pending-dialog-labels.js";
8
+ function SignOutPendingDialog({
9
+ open,
10
+ queued,
11
+ waitingForPlan,
12
+ onSyncNow,
13
+ onSignOutAnyway,
14
+ onDiscardWaiting,
15
+ keepsChanges,
16
+ extra,
17
+ onClose,
18
+ labels: labelsProp
19
+ }) {
20
+ const labels = useKitLabels("signOutPending", DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS, labelsProp);
21
+ const [syncing, setSyncing] = useState(false);
22
+ const [discarding, setDiscarding] = useState(false);
23
+ const syncNow = async () => {
24
+ setSyncing(true);
25
+ try {
26
+ await onSyncNow();
27
+ } catch {
28
+ } finally {
29
+ setSyncing(false);
30
+ }
31
+ onClose();
32
+ };
33
+ const discard = async () => {
34
+ if (!onDiscardWaiting) return;
35
+ setDiscarding(true);
36
+ try {
37
+ await onDiscardWaiting();
38
+ } finally {
39
+ setDiscarding(false);
40
+ }
41
+ onSignOutAnyway();
42
+ };
43
+ return /* @__PURE__ */ jsx(
44
+ DialogFrame,
45
+ {
46
+ open,
47
+ role: "alertdialog",
48
+ onClose,
49
+ title: labels.title,
50
+ description: keepsChanges ? labels.kept(queued) : labels.lost(queued),
51
+ closeButton: true,
52
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
53
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onClose, children: labels.cancel }),
54
+ onDiscardWaiting && waitingForPlan > 0 && /* @__PURE__ */ jsx(Button, { variant: "secondary", pending: discarding, disabled: syncing, onClick: () => void discard(), children: labels.discardWaiting }),
55
+ /* @__PURE__ */ jsx(Button, { pending: syncing, disabled: discarding, onClick: () => void syncNow(), children: labels.syncNow }),
56
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: discarding, onClick: onSignOutAnyway, children: labels.signOutAnyway })
57
+ ] }),
58
+ children: waitingForPlan > 0 && (typeof extra === "string" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted", children: extra }) : extra)
59
+ }
60
+ );
61
+ }
62
+ export {
63
+ DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS,
64
+ SignOutPendingDialog
65
+ };
66
+ //# sourceMappingURL=sign-out-pending-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/offline/sign-out-pending-dialog.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from \"./sign-out-pending-dialog-labels\";\n\n/**\n * The question before an explicit sign-out while changes still wait to sync — keksdose's\n * confirm (account-menu.tsx:247-317) as a kit dialog (docs/local-first-harmonization.md\n * §1.5 D6, §1.6). Show it only when `queued > 0` and only on a sign-out someone asked for:\n * never from a session-ended hook, which cannot ask anyone.\n *\n * \"Sync now\" is the primary action: the queued writes are the person's own data. It runs\n * `onSyncNow`, is busy while that runs, then calls `onClose` without signing out — the\n * count has moved and the caller re-evaluates (signs out at once when nothing is left, or\n * opens this again with the new number). \"Log out anyway\" stays as the escape hatch, usable\n * while a sync runs (a hanging one would trap the person otherwise): a device that cannot\n * reach the server must still be able to sign out. Only a running discard holds it.\n *\n * The words depend on what the app does with the queue at sign-out (`keepsChanges`):\n * kastlan keeps the outbox for its owner, keksdose wipes it — and keksdose's copy must not\n * say the changes stay.\n */\nexport interface SignOutPendingDialogLabels {\n title: string;\n /** `keepsChanges={false}`: signing out destroys the device's copy. */\n lost: (count: number) => string;\n /** `keepsChanges`: the queue stays on the device for its owner. */\n kept: (count: number) => string;\n syncNow: string;\n signOutAnyway: string;\n /** Only with `onDiscardWaiting` and `waitingForPlan > 0` (the billing kit's words). */\n discardWaiting: string;\n cancel: string;\n}\n\n// Defined in ./sign-out-pending-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS };\n\nexport interface SignOutPendingDialogProps {\n open: boolean;\n /** Changes not yet on the server: pending + blocked. The tour `:pending` record is not\n * one of them (D6). */\n queued: number;\n /** Of those, the ones waiting for a plan (billing's read-only refusal). */\n waitingForPlan: number;\n /** Runs one sync round. The dialog closes when it settles, whatever it managed. */\n onSyncNow: () => Promise<unknown> | void;\n /** Signs out now, queue or not. */\n onSignOutAnyway: () => void;\n /** Lets the plan-waiting changes go; the dialog then signs out as asked. Without it\n * (or with `waitingForPlan` 0) the button is not shown. */\n onDiscardWaiting?: () => Promise<unknown> | void;\n /** `true` (kastlan): the changes stay on this device for their owner. `false`\n * (keksdose): they are lost. Picks the words. */\n keepsChanges: boolean;\n /** Room under the sentence — the billing kit's `waitingChanges` line. Shown only while\n * `waitingForPlan > 0`, so a 0-count line never shows. */\n extra?: ReactNode;\n /** Cancel, Escape, the X — and after \"Sync now\". */\n onClose: () => void;\n labels?: LabelOverride<SignOutPendingDialogLabels>;\n}\n\nexport function SignOutPendingDialog({\n open,\n queued,\n waitingForPlan,\n onSyncNow,\n onSignOutAnyway,\n onDiscardWaiting,\n keepsChanges,\n extra,\n onClose,\n labels: labelsProp,\n}: SignOutPendingDialogProps) {\n const labels = useKitLabels(\"signOutPending\", DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS, labelsProp);\n const [syncing, setSyncing] = useState(false);\n const [discarding, setDiscarding] = useState(false);\n\n const syncNow = async () => {\n setSyncing(true);\n try {\n await onSyncNow();\n } catch {\n /* whatever the sync managed, the decision is the person's again */\n } finally {\n setSyncing(false);\n }\n onClose();\n };\n\n const discard = async () => {\n if (!onDiscardWaiting) return;\n setDiscarding(true);\n try {\n await onDiscardWaiting();\n } finally {\n setDiscarding(false);\n }\n onSignOutAnyway();\n };\n\n return (\n <DialogFrame\n open={open}\n role=\"alertdialog\"\n onClose={onClose}\n title={labels.title}\n description={keepsChanges ? labels.kept(queued) : labels.lost(queued)}\n closeButton\n actions={\n <>\n <Button variant=\"secondary\" onClick={onClose}>\n {labels.cancel}\n </Button>\n {onDiscardWaiting && waitingForPlan > 0 && (\n <Button variant=\"secondary\" pending={discarding} disabled={syncing} onClick={() => void discard()}>\n {labels.discardWaiting}\n </Button>\n )}\n <Button pending={syncing} disabled={discarding} onClick={() => void syncNow()}>\n {labels.syncNow}\n </Button>\n <Button variant=\"secondary\" disabled={discarding} onClick={onSignOutAnyway}>\n {labels.signOutAnyway}\n </Button>\n </>\n }\n >\n {waitingForPlan > 0 && (typeof extra === \"string\" ? <p className=\"text-sm text-muted\">{extra}</p> : extra)}\n </DialogFrame>\n );\n}\n"],"mappings":";AAiHQ,mBACE,KADF;AAjHR,SAAS,gBAAgB;AAEzB,SAAS,oBAAwC;AACjD,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,8CAA8C;AA4DhD,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA8B;AAC5B,QAAM,SAAS,aAAa,kBAAkB,wCAAwC,UAAU;AAChG,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAElD,QAAM,UAAU,YAAY;AAC1B,eAAW,IAAI;AACf,QAAI;AACF,YAAM,UAAU;AAAA,IAClB,QAAQ;AAAA,IAER,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AACA,YAAQ;AAAA,EACV;AAEA,QAAM,UAAU,YAAY;AAC1B,QAAI,CAAC,iBAAkB;AACvB,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,iBAAiB;AAAA,IACzB,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AACA,oBAAgB;AAAA,EAClB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,eAAe,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,MAAM;AAAA,MACpE,aAAW;AAAA,MACX,SACE,iCACE;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,SAClC,iBAAO,QACV;AAAA,QACC,oBAAoB,iBAAiB,KACpC,oBAAC,UAAO,SAAQ,aAAY,SAAS,YAAY,UAAU,SAAS,SAAS,MAAM,KAAK,QAAQ,GAC7F,iBAAO,gBACV;AAAA,QAEF,oBAAC,UAAO,SAAS,SAAS,UAAU,YAAY,SAAS,MAAM,KAAK,QAAQ,GACzE,iBAAO,SACV;AAAA,QACA,oBAAC,UAAO,SAAQ,aAAY,UAAU,YAAY,SAAS,iBACxD,iBAAO,eACV;AAAA,SACF;AAAA,MAGD,2BAAiB,MAAM,OAAO,UAAU,WAAW,oBAAC,OAAE,WAAU,sBAAsB,iBAAM,IAAO;AAAA;AAAA,EACtG;AAEJ;","names":[]}
@@ -0,0 +1,8 @@
1
+ export { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe } from "./offline/query-persistence.js";
2
+ export type { QueryPersistence, QueryPersistenceOptions, QueryPersistenceStorage, ScrubField, } from "./offline/query-persistence.js";
3
+ export { SignOutPendingDialog, DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./offline/sign-out-pending-dialog.js";
4
+ export type { SignOutPendingDialogProps, SignOutPendingDialogLabels } from "./offline/sign-out-pending-dialog.js";
5
+ export { PwaUpdatePrompt, DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "./offline/pwa-update-prompt.js";
6
+ export type { PwaUpdatePromptProps, PwaUpdatePromptLabels } from "./offline/pwa-update-prompt.js";
7
+ export { SharedComputerChoice, DEFAULT_SHARED_COMPUTER_LABELS, isSharedComputer, setSharedComputer, } from "./offline/shared-computer.js";
8
+ export type { SharedComputerChoiceProps, SharedComputerLabels, SharedComputerStorage } from "./offline/shared-computer.js";
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe } from "./offline/query-persistence.js";
3
+ import { SignOutPendingDialog, DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./offline/sign-out-pending-dialog.js";
4
+ import { PwaUpdatePrompt, DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "./offline/pwa-update-prompt.js";
5
+ import {
6
+ SharedComputerChoice,
7
+ DEFAULT_SHARED_COMPUTER_LABELS,
8
+ isSharedComputer,
9
+ setSharedComputer
10
+ } from "./offline/shared-computer.js";
11
+ export {
12
+ DEFAULT_PWA_UPDATE_PROMPT_LABELS,
13
+ DEFAULT_SCRUB_FIELDS,
14
+ DEFAULT_SHARED_COMPUTER_LABELS,
15
+ DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS,
16
+ PwaUpdatePrompt,
17
+ SharedComputerChoice,
18
+ SignOutPendingDialog,
19
+ createQueryPersistence,
20
+ isCacheSafe,
21
+ isSharedComputer,
22
+ setSharedComputer
23
+ };
24
+ //# sourceMappingURL=offline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/offline.ts"],"sourcesContent":["// `@eifi1/ui-kit/offline` — local-first phase 1 (docs/local-first-harmonization.md §1.6,\n// 0.45): the persisted query cache with its scrub list, the sign-out question while changes\n// wait, the service-worker update prompt, and the shared-computer choice.\n//\n// The only entry point that requires `@tanstack/react-query`,\n// `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister` and\n// `idb-keyval` — optional peers for that reason. NOT re-exported from the main barrel, which\n// would make them everyone's install; the packaging contract test holds that line.\nexport { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe } from \"./offline/query-persistence\";\nexport type {\n QueryPersistence,\n QueryPersistenceOptions,\n QueryPersistenceStorage,\n ScrubField,\n} from \"./offline/query-persistence\";\nexport { SignOutPendingDialog, DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from \"./offline/sign-out-pending-dialog\";\nexport type { SignOutPendingDialogProps, SignOutPendingDialogLabels } from \"./offline/sign-out-pending-dialog\";\nexport { PwaUpdatePrompt, DEFAULT_PWA_UPDATE_PROMPT_LABELS } from \"./offline/pwa-update-prompt\";\nexport type { PwaUpdatePromptProps, PwaUpdatePromptLabels } from \"./offline/pwa-update-prompt\";\nexport {\n SharedComputerChoice,\n DEFAULT_SHARED_COMPUTER_LABELS,\n isSharedComputer,\n setSharedComputer,\n} from \"./offline/shared-computer\";\nexport type { SharedComputerChoiceProps, SharedComputerLabels, SharedComputerStorage } from \"./offline/shared-computer\";\n"],"mappings":";AAQA,SAAS,wBAAwB,sBAAsB,mBAAmB;AAO1E,SAAS,sBAAsB,8CAA8C;AAE7E,SAAS,iBAAiB,wCAAwC;AAElE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
@@ -1,6 +1,30 @@
1
1
  import type { LabelOverride } from "../i18n/kit-labels.js";
2
2
  import type { AdminAreaLabels } from "../admin/admin-area-labels.js";
3
3
  import type { TopBarMenuEntry } from "./topbar-action-menu.js";
4
+ export interface AdminWaitingBadgeProps {
5
+ /** The waiting count: only work that waits on this admin. `0`, `null` or none draws
6
+ * nothing; past 99 it reads "99+". */
7
+ count: number | null | undefined;
8
+ /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */
9
+ labels?: LabelOverride<AdminAreaLabels>;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * The waiting pill of {@link AdminMenuRow} on its own (0.45, docs/admin-harmonization.md
14
+ * §9.3.4): the `solid` warning count, "99+" past 99, nothing for 0. Fit for `IconButton`'s
15
+ * `badge` — kastlan's top-bar platform button. There the badge is DECORATIVE (`aria-hidden`
16
+ * by IconButton), so the button's name carries the count: `adminArea.waitingLabel`.
17
+ *
18
+ * const area = useAdminAreaLabels();
19
+ * <IconButton
20
+ * label={waiting > 0 ? area.waitingLabel("Platform", waiting) : "Platform"}
21
+ * badge={<AdminWaitingBadge count={waiting} />}
22
+ * …
23
+ * />
24
+ *
25
+ * Read on its own (outside an IconButton), the pill still says ", 3 waiting" to a reader.
26
+ */
27
+ export declare function AdminWaitingBadge({ count, labels: labelsProp, className }: AdminWaitingBadgeProps): import("react").JSX.Element;
4
28
  export interface AdminMenuEntryOptions {
5
29
  /** The admin area. Default `/admin`. */
6
30
  href?: string;
@@ -9,10 +9,14 @@ function badgeText(badge) {
9
9
  if (typeof badge !== "number" || !(badge > 0)) return null;
10
10
  return badge > 99 ? "99+" : String(Math.floor(badge));
11
11
  }
12
- function WaitingBadge({ badge, labels }) {
12
+ function WaitingBadge({
13
+ badge,
14
+ labels,
15
+ className
16
+ }) {
13
17
  const text = badgeText(badge);
14
18
  if (text === null) return null;
15
- return /* @__PURE__ */ jsxs(Chip, { size: "sm", variant: "solid", tone: "warning", "data-slot": "admin-badge", children: [
19
+ return /* @__PURE__ */ jsxs(Chip, { size: "sm", variant: "solid", tone: "warning", "data-slot": "admin-badge", className, children: [
16
20
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: text }),
17
21
  /* @__PURE__ */ jsxs("span", { className: "sr-only-fixed", children: [
18
22
  ", ",
@@ -20,6 +24,10 @@ function WaitingBadge({ badge, labels }) {
20
24
  ] })
21
25
  ] });
22
26
  }
27
+ function AdminWaitingBadge({ count, labels: labelsProp, className }) {
28
+ const labels = useAdminAreaLabels(labelsProp);
29
+ return /* @__PURE__ */ jsx(WaitingBadge, { badge: count, labels, className });
30
+ }
23
31
  function adminMenuEntry({
24
32
  href = "/admin",
25
33
  badge,
@@ -50,7 +58,7 @@ function AdminMenuRow({ href = "/admin", badge, onNavigate, current, labels: lab
50
58
  renderLink: ({ href: to, ...p }) => /* @__PURE__ */ jsx(Link, { to, ...p }),
51
59
  onClick: onNavigate ? () => onNavigate() : void 0,
52
60
  leading: /* @__PURE__ */ jsx(ShieldCheck, { className: "size-4", "aria-hidden": true }),
53
- trailing: badgeText(badge) === null ? void 0 : /* @__PURE__ */ jsx(WaitingBadge, { badge, labels }),
61
+ trailing: badgeText(badge) === null ? void 0 : /* @__PURE__ */ jsx(AdminWaitingBadge, { count: badge, labels }),
54
62
  current,
55
63
  children: labels.title
56
64
  }
@@ -58,6 +66,7 @@ function AdminMenuRow({ href = "/admin", badge, onNavigate, current, labels: lab
58
66
  }
59
67
  export {
60
68
  AdminMenuRow,
69
+ AdminWaitingBadge,
61
70
  adminMenuEntry,
62
71
  useAdminMenuEntry
63
72
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/admin-menu.tsx"],"sourcesContent":["import { Link } from \"react-router\";\nimport { ShieldCheck } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_ADMIN_AREA_LABELS, useAdminAreaLabels } from \"../admin/admin-area-labels\";\nimport type { AdminAreaLabels } from \"../admin/admin-area-labels\";\nimport type { TopBarMenuEntry } from \"./topbar-action-menu\";\n\n/**\n * The account menu's \"Administration\" row (0.42.0, docs/admin-harmonization.md §2.1, §6.6,\n * §7): the way into the admin area everyone finds, in every app, for an admin only. Two\n * forms of one row — {@link adminMenuEntry} for `TopBarActionMenu`'s `entries`, and\n * {@link AdminMenuRow} for a menu built from `HoverMenu` children (Kurvenschmiede's).\n */\n\n/** The badge's text: nothing for none or 0, \"99+\" past 99. */\nfunction badgeText(badge: number | null | undefined): string | null {\n if (typeof badge !== \"number\" || !(badge > 0)) return null;\n return badge > 99 ? \"99+\" : String(Math.floor(badge));\n}\n\n/**\n * The waiting count at the row's end: the `solid` count pill of {@link Chip}, in the\n * pending count's tone, with the number for the eye and `adminArea.waiting` for a reader\n * (\"Administration, 3 waiting\"). `null` for no count.\n */\nfunction WaitingBadge({ badge, labels }: { badge: number | null | undefined; labels: AdminAreaLabels }) {\n const text = badgeText(badge);\n if (text === null) return null;\n return (\n <Chip size=\"sm\" variant=\"solid\" tone=\"warning\" data-slot=\"admin-badge\">\n <span aria-hidden>{text}</span>\n {/* The comma parts it from the row's label in the name: \"Administration, 3 waiting\". */}\n <span className=\"sr-only-fixed\">, {labels.waiting(badge as number)}</span>\n </Chip>\n );\n}\n\nexport interface AdminMenuEntryOptions {\n /** The admin area. Default `/admin`. */\n href?: string;\n /**\n * The waiting count (§2.4 decision 15, §6.3): only work that waits on this admin —\n * `adminWaitingCount(attention, […])`. `0`, `null` or none shows no badge.\n */\n badge?: number | null;\n /**\n * The resolved `adminArea` words — pass `useAdminAreaLabels()`, or the row is ENGLISH\n * in every language: a plain function reads no provider (kastlan, 0.42 adoption). Builds\n * no hook, so the entry can be made in the list of entries. {@link useAdminMenuEntry}\n * (0.43) is the same entry with the provider's words read for you.\n */\n labels?: AdminAreaLabels;\n /** The entry's `key`. Default `\"administration\"`. */\n key?: string;\n /** The page already open: `aria-current` and a check mark. */\n current?: boolean;\n}\n\n/**\n * The \"Administration\" {@link TopBarMenuEntry}: a link row with the `ShieldCheck` icon,\n * the kit's word and the waiting count as its badge. Put it in the entries for an admin\n * only — the menu closes on the way, as any link row:\n *\n * const labels = useAdminAreaLabels();\n * entries={[…, ...(isAdmin ? [adminMenuEntry({ badge: waiting, labels })] : []), …]}\n *\n * A function, not a hook: it reads NO provider, so pass `labels` (`useAdminAreaLabels()`)\n * or the row says \"Administration\" in every language — or call {@link useAdminMenuEntry}\n * at the component's top.\n */\nexport function adminMenuEntry({\n href = \"/admin\",\n badge,\n labels = DEFAULT_ADMIN_AREA_LABELS,\n key = \"administration\",\n current,\n}: AdminMenuEntryOptions = {}): TopBarMenuEntry {\n return {\n kind: \"link\",\n key,\n to: href,\n icon: <ShieldCheck className=\"size-4\" aria-hidden />,\n label: labels.title,\n trailing: badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />,\n current,\n };\n}\n\nexport interface UseAdminMenuEntryOptions extends Omit<AdminMenuEntryOptions, \"labels\"> {\n /** Over the provider's `adminArea` words: prop > `<UiKitProvider labels={{ adminArea }}>`\n * > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n}\n\n/**\n * {@link adminMenuEntry} with the provider's words (0.43, kastlan): the hook reads\n * `useAdminAreaLabels()` itself, so the row is in the app's language without a `labels`\n * to remember. Call it at the component's top, then put it in the entries:\n *\n * const admin = useAdminMenuEntry({ badge: waiting });\n * entries={[…, ...(isAdmin ? [admin] : []), …]}\n */\nexport function useAdminMenuEntry({ labels: labelsProp, ...options }: UseAdminMenuEntryOptions = {}): TopBarMenuEntry {\n const labels = useAdminAreaLabels(labelsProp);\n return adminMenuEntry({ ...options, labels });\n}\n\nexport interface AdminMenuRowProps {\n /** The admin area. Default `/admin`. */\n href?: string;\n /** As {@link AdminMenuEntryOptions.badge}. */\n badge?: number | null;\n /** Runs on the way — `HoverMenu`'s `close`. */\n onNavigate?: () => void;\n current?: boolean;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n}\n\n/**\n * The same row as {@link adminMenuEntry} for a menu drawn from `HoverMenu` children: the\n * `<li>` holding the router-link {@link MenuItem} `TopBarActionMenu` draws for a link\n * entry, so the two menus look alike.\n *\n * {(close) => <ul>… {isAdmin && <AdminMenuRow badge={waiting} onNavigate={close} />} …</ul>}\n */\nexport function AdminMenuRow({ href = \"/admin\", badge, onNavigate, current, labels: labelsProp }: AdminMenuRowProps) {\n // The provider's words, as every kit part (0.43: through the namespace's own hook).\n const labels = useAdminAreaLabels(labelsProp);\n return (\n <li>\n <MenuItem\n href={href}\n renderLink={({ href: to, ...p }) => <Link to={to} {...p} />}\n onClick={onNavigate ? () => onNavigate() : undefined}\n leading={<ShieldCheck className=\"size-4\" aria-hidden />}\n trailing={badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />}\n current={current}\n >\n {labels.title}\n </MenuItem>\n </li>\n );\n}\n"],"mappings":";AAgCM,cAEA,YAFA;AAhCN,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AACrB,SAAS,gBAAgB;AAEzB,SAAS,2BAA2B,0BAA0B;AAY9D,SAAS,UAAU,OAAiD;AAClE,MAAI,OAAO,UAAU,YAAY,EAAE,QAAQ,GAAI,QAAO;AACtD,SAAO,QAAQ,KAAK,QAAQ,OAAO,KAAK,MAAM,KAAK,CAAC;AACtD;AAOA,SAAS,aAAa,EAAE,OAAO,OAAO,GAAkE;AACtG,QAAM,OAAO,UAAU,KAAK;AAC5B,MAAI,SAAS,KAAM,QAAO;AAC1B,SACE,qBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,MAAK,WAAU,aAAU,eACvD;AAAA,wBAAC,UAAK,eAAW,MAAE,gBAAK;AAAA,IAExB,qBAAC,UAAK,WAAU,iBAAgB;AAAA;AAAA,MAAG,OAAO,QAAQ,KAAe;AAAA,OAAE;AAAA,KACrE;AAEJ;AAmCO,SAAS,eAAe;AAAA,EAC7B,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,MAAM;AAAA,EACN;AACF,IAA2B,CAAC,GAAoB;AAC9C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,IAAI;AAAA,IACJ,MAAM,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,IAClD,OAAO,OAAO;AAAA,IACd,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,gBAAa,OAAc,QAAgB;AAAA,IAC9F;AAAA,EACF;AACF;AAgBO,SAAS,kBAAkB,EAAE,QAAQ,YAAY,GAAG,QAAQ,IAA8B,CAAC,GAAoB;AACpH,QAAM,SAAS,mBAAmB,UAAU;AAC5C,SAAO,eAAe,EAAE,GAAG,SAAS,OAAO,CAAC;AAC9C;AAqBO,SAAS,aAAa,EAAE,OAAO,UAAU,OAAO,YAAY,SAAS,QAAQ,WAAW,GAAsB;AAEnH,QAAM,SAAS,mBAAmB,UAAU;AAC5C,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM,oBAAC,QAAK,IAAS,GAAG,GAAG;AAAA,MACzD,SAAS,aAAa,MAAM,WAAW,IAAI;AAAA,MAC3C,SAAS,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,MACrD,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,gBAAa,OAAc,QAAgB;AAAA,MAC9F;AAAA,MAEC,iBAAO;AAAA;AAAA,EACV,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/admin-menu.tsx"],"sourcesContent":["import { Link } from \"react-router\";\nimport { ShieldCheck } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_ADMIN_AREA_LABELS, useAdminAreaLabels } from \"../admin/admin-area-labels\";\nimport type { AdminAreaLabels } from \"../admin/admin-area-labels\";\nimport type { TopBarMenuEntry } from \"./topbar-action-menu\";\n\n/**\n * The account menu's \"Administration\" row (0.42.0, docs/admin-harmonization.md §2.1, §6.6,\n * §7): the way into the admin area everyone finds, in every app, for an admin only. Two\n * forms of one row — {@link adminMenuEntry} for `TopBarActionMenu`'s `entries`, and\n * {@link AdminMenuRow} for a menu built from `HoverMenu` children (Kurvenschmiede's).\n */\n\n/** The badge's text: nothing for none or 0, \"99+\" past 99. */\nfunction badgeText(badge: number | null | undefined): string | null {\n if (typeof badge !== \"number\" || !(badge > 0)) return null;\n return badge > 99 ? \"99+\" : String(Math.floor(badge));\n}\n\n/**\n * The waiting count at the row's end: the `solid` count pill of {@link Chip}, in the\n * pending count's tone, with the number for the eye and `adminArea.waiting` for a reader\n * (\"Administration, 3 waiting\"). `null` for no count.\n */\nfunction WaitingBadge({\n badge,\n labels,\n className,\n}: {\n badge: number | null | undefined;\n labels: AdminAreaLabels;\n className?: string;\n}) {\n const text = badgeText(badge);\n if (text === null) return null;\n return (\n <Chip size=\"sm\" variant=\"solid\" tone=\"warning\" data-slot=\"admin-badge\" className={className}>\n <span aria-hidden>{text}</span>\n {/* The comma parts it from the row's label in the name: \"Administration, 3 waiting\". */}\n <span className=\"sr-only-fixed\">, {labels.waiting(badge as number)}</span>\n </Chip>\n );\n}\n\nexport interface AdminWaitingBadgeProps {\n /** The waiting count: only work that waits on this admin. `0`, `null` or none draws\n * nothing; past 99 it reads \"99+\". */\n count: number | null | undefined;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n className?: string;\n}\n\n/**\n * The waiting pill of {@link AdminMenuRow} on its own (0.45, docs/admin-harmonization.md\n * §9.3.4): the `solid` warning count, \"99+\" past 99, nothing for 0. Fit for `IconButton`'s\n * `badge` — kastlan's top-bar platform button. There the badge is DECORATIVE (`aria-hidden`\n * by IconButton), so the button's name carries the count: `adminArea.waitingLabel`.\n *\n * const area = useAdminAreaLabels();\n * <IconButton\n * label={waiting > 0 ? area.waitingLabel(\"Platform\", waiting) : \"Platform\"}\n * badge={<AdminWaitingBadge count={waiting} />}\n * …\n * />\n *\n * Read on its own (outside an IconButton), the pill still says \", 3 waiting\" to a reader.\n */\nexport function AdminWaitingBadge({ count, labels: labelsProp, className }: AdminWaitingBadgeProps) {\n const labels = useAdminAreaLabels(labelsProp);\n return <WaitingBadge badge={count} labels={labels} className={className} />;\n}\n\nexport interface AdminMenuEntryOptions {\n /** The admin area. Default `/admin`. */\n href?: string;\n /**\n * The waiting count (§2.4 decision 15, §6.3): only work that waits on this admin —\n * `adminWaitingCount(attention, […])`. `0`, `null` or none shows no badge.\n */\n badge?: number | null;\n /**\n * The resolved `adminArea` words — pass `useAdminAreaLabels()`, or the row is ENGLISH\n * in every language: a plain function reads no provider (kastlan, 0.42 adoption). Builds\n * no hook, so the entry can be made in the list of entries. {@link useAdminMenuEntry}\n * (0.43) is the same entry with the provider's words read for you.\n */\n labels?: AdminAreaLabels;\n /** The entry's `key`. Default `\"administration\"`. */\n key?: string;\n /** The page already open: `aria-current` and a check mark. */\n current?: boolean;\n}\n\n/**\n * The \"Administration\" {@link TopBarMenuEntry}: a link row with the `ShieldCheck` icon,\n * the kit's word and the waiting count as its badge. Put it in the entries for an admin\n * only — the menu closes on the way, as any link row:\n *\n * const labels = useAdminAreaLabels();\n * entries={[…, ...(isAdmin ? [adminMenuEntry({ badge: waiting, labels })] : []), …]}\n *\n * A function, not a hook: it reads NO provider, so pass `labels` (`useAdminAreaLabels()`)\n * or the row says \"Administration\" in every language — or call {@link useAdminMenuEntry}\n * at the component's top.\n */\nexport function adminMenuEntry({\n href = \"/admin\",\n badge,\n labels = DEFAULT_ADMIN_AREA_LABELS,\n key = \"administration\",\n current,\n}: AdminMenuEntryOptions = {}): TopBarMenuEntry {\n return {\n kind: \"link\",\n key,\n to: href,\n icon: <ShieldCheck className=\"size-4\" aria-hidden />,\n label: labels.title,\n trailing: badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />,\n current,\n };\n}\n\nexport interface UseAdminMenuEntryOptions extends Omit<AdminMenuEntryOptions, \"labels\"> {\n /** Over the provider's `adminArea` words: prop > `<UiKitProvider labels={{ adminArea }}>`\n * > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n}\n\n/**\n * {@link adminMenuEntry} with the provider's words (0.43, kastlan): the hook reads\n * `useAdminAreaLabels()` itself, so the row is in the app's language without a `labels`\n * to remember. Call it at the component's top, then put it in the entries:\n *\n * const admin = useAdminMenuEntry({ badge: waiting });\n * entries={[…, ...(isAdmin ? [admin] : []), …]}\n */\nexport function useAdminMenuEntry({ labels: labelsProp, ...options }: UseAdminMenuEntryOptions = {}): TopBarMenuEntry {\n const labels = useAdminAreaLabels(labelsProp);\n return adminMenuEntry({ ...options, labels });\n}\n\nexport interface AdminMenuRowProps {\n /** The admin area. Default `/admin`. */\n href?: string;\n /** As {@link AdminMenuEntryOptions.badge}. */\n badge?: number | null;\n /** Runs on the way — `HoverMenu`'s `close`. */\n onNavigate?: () => void;\n current?: boolean;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n}\n\n/**\n * The same row as {@link adminMenuEntry} for a menu drawn from `HoverMenu` children: the\n * `<li>` holding the router-link {@link MenuItem} `TopBarActionMenu` draws for a link\n * entry, so the two menus look alike.\n *\n * {(close) => <ul>… {isAdmin && <AdminMenuRow badge={waiting} onNavigate={close} />} …</ul>}\n */\nexport function AdminMenuRow({ href = \"/admin\", badge, onNavigate, current, labels: labelsProp }: AdminMenuRowProps) {\n // The provider's words, as every kit part (0.43: through the namespace's own hook).\n const labels = useAdminAreaLabels(labelsProp);\n return (\n <li>\n <MenuItem\n href={href}\n renderLink={({ href: to, ...p }) => <Link to={to} {...p} />}\n onClick={onNavigate ? () => onNavigate() : undefined}\n leading={<ShieldCheck className=\"size-4\" aria-hidden />}\n trailing={badgeText(badge) === null ? undefined : <AdminWaitingBadge count={badge} labels={labels} />}\n current={current}\n >\n {labels.title}\n </MenuItem>\n </li>\n );\n}\n"],"mappings":";AAwCM,cAEA,YAFA;AAxCN,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AACrB,SAAS,gBAAgB;AAEzB,SAAS,2BAA2B,0BAA0B;AAY9D,SAAS,UAAU,OAAiD;AAClE,MAAI,OAAO,UAAU,YAAY,EAAE,QAAQ,GAAI,QAAO;AACtD,SAAO,QAAQ,KAAK,QAAQ,OAAO,KAAK,MAAM,KAAK,CAAC;AACtD;AAOA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,UAAU,KAAK;AAC5B,MAAI,SAAS,KAAM,QAAO;AAC1B,SACE,qBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,MAAK,WAAU,aAAU,eAAc,WACrE;AAAA,wBAAC,UAAK,eAAW,MAAE,gBAAK;AAAA,IAExB,qBAAC,UAAK,WAAU,iBAAgB;AAAA;AAAA,MAAG,OAAO,QAAQ,KAAe;AAAA,OAAE;AAAA,KACrE;AAEJ;AA0BO,SAAS,kBAAkB,EAAE,OAAO,QAAQ,YAAY,UAAU,GAA2B;AAClG,QAAM,SAAS,mBAAmB,UAAU;AAC5C,SAAO,oBAAC,gBAAa,OAAO,OAAO,QAAgB,WAAsB;AAC3E;AAmCO,SAAS,eAAe;AAAA,EAC7B,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,MAAM;AAAA,EACN;AACF,IAA2B,CAAC,GAAoB;AAC9C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,IAAI;AAAA,IACJ,MAAM,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,IAClD,OAAO,OAAO;AAAA,IACd,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,gBAAa,OAAc,QAAgB;AAAA,IAC9F;AAAA,EACF;AACF;AAgBO,SAAS,kBAAkB,EAAE,QAAQ,YAAY,GAAG,QAAQ,IAA8B,CAAC,GAAoB;AACpH,QAAM,SAAS,mBAAmB,UAAU;AAC5C,SAAO,eAAe,EAAE,GAAG,SAAS,OAAO,CAAC;AAC9C;AAqBO,SAAS,aAAa,EAAE,OAAO,UAAU,OAAO,YAAY,SAAS,QAAQ,WAAW,GAAsB;AAEnH,QAAM,SAAS,mBAAmB,UAAU;AAC5C,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM,oBAAC,QAAK,IAAS,GAAG,GAAG;AAAA,MACzD,SAAS,aAAa,MAAM,WAAW,IAAI;AAAA,MAC3C,SAAS,oBAAC,eAAY,WAAU,UAAS,eAAW,MAAC;AAAA,MACrD,UAAU,UAAU,KAAK,MAAM,OAAO,SAAY,oBAAC,qBAAkB,OAAO,OAAO,QAAgB;AAAA,MACnG;AAAA,MAEC,iBAAO;AAAA;AAAA,EACV,GACF;AAEJ;","names":[]}
package/dist/shell.d.ts CHANGED
@@ -12,6 +12,8 @@ export { adminMenuEntry, AdminMenuRow } from "./shell/admin-menu.js";
12
12
  export type { AdminMenuEntryOptions, AdminMenuRowProps } from "./shell/admin-menu.js";
13
13
  export { useAdminMenuEntry } from "./shell/admin-menu.js";
14
14
  export type { UseAdminMenuEntryOptions } from "./shell/admin-menu.js";
15
+ export { AdminWaitingBadge } from "./shell/admin-menu.js";
16
+ export type { AdminWaitingBadgeProps } from "./shell/admin-menu.js";
15
17
  export { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout.js";
16
18
  export type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from "./settings/settings-layout.js";
17
19
  export { useSettingsRoute, resolveSettingsLocation, settingsFromListState, SETTINGS_FROM_LIST_STATE, } from "./settings/use-settings-route.js";
package/dist/shell.js CHANGED
@@ -10,6 +10,7 @@ export * from "./shell/auth-layout.js";
10
10
  import { FillHeight, FILL_HEIGHT_CLASS } from "./shell/fill-height.js";
11
11
  import { adminMenuEntry, AdminMenuRow } from "./shell/admin-menu.js";
12
12
  import { useAdminMenuEntry } from "./shell/admin-menu.js";
13
+ import { AdminWaitingBadge } from "./shell/admin-menu.js";
13
14
  import { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout.js";
14
15
  import {
15
16
  useSettingsRoute,
@@ -47,6 +48,7 @@ import { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from "./shell/top-
47
48
  export {
48
49
  AccountMenuLanguage,
49
50
  AdminMenuRow,
51
+ AdminWaitingBadge,
50
52
  DEFAULT_LAST_VISITED_EXCLUDES,
51
53
  DEFAULT_TOP_BAR_PARTS_LABELS,
52
54
  DEV_ORIGINAL_ROLE_KEY,
package/dist/shell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\nexport { DevSwitchers, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;AACzE,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAE/C,SAAS,cAAc,6BAA6B;AAEpD,SAAS,8BAA8B,4BAA4B;","names":[]}
1
+ {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\nexport { DevSwitchers, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;AACzE,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAE/C,SAAS,cAAc,6BAA6B;AAEpD,SAAS,8BAA8B,4BAA4B;","names":[]}
@@ -7,11 +7,11 @@ const DEFAULT_TOUR_CATALOGUE_LABELS = {
7
7
  groupOption: (group, count) => `${group} (${count})`,
8
8
  stateLabel: "Filter tours",
9
9
  stateAll: (count) => `All (${count})`,
10
- stateTodo: (count) => `To do (${count})`,
10
+ stateTodo: (count) => `Not taken (${count})`,
11
11
  stateDone: (count) => `Completed (${count})`,
12
12
  showing: (shown, total) => `Showing ${shown} of ${total} tours`,
13
13
  noMatches: "No tour matches",
14
- noMatchesHint: "Try another word, or clear the filters.",
14
+ noMatchesHint: "Try another word, or clear the filter.",
15
15
  clearFilters: "Clear filters",
16
16
  badgeNew: "New",
17
17
  badgeUpdated: "Updated",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tour/tour-catalogue-labels.ts"],"sourcesContent":["// The English words of `TourCatalogue` and `TourCatalogueCard` (docs/tours-harmonization.md\n// §3), in a module with no component code, like every `*-labels.ts` (0.34, KS U8).\n// keksdose's reviewed words (tour.* in its en.json) where it had them.\n\n/** The `tourCatalogue` namespace. Counts arrive as numbers, dates already formatted. */\nexport interface TourCatalogueLabels {\n /** The search box's name. */\n searchLabel: string;\n searchPlaceholder: string;\n /** The group select's name (keksdose: \"Topic\"). */\n groupLabel: string;\n /** The select's first option, with the count the other filters leave. */\n groupAll: (count: number) => string;\n /** A group's option: its title and its count. */\n groupOption: (group: string, count: number) => string;\n /** The all / to do / done toggle's name. */\n stateLabel: string;\n stateAll: (count: number) => string;\n stateTodo: (count: number) => string;\n stateDone: (count: number) => string;\n /** Under the filter bar while a filter is on. */\n showing: (shown: number, total: number) => string;\n noMatches: string;\n noMatchesHint: string;\n clearFilters: string;\n /** The NEW pill. */\n badgeNew: string;\n /** The pill on a tour reset by an update (§8). */\n badgeUpdated: string;\n /** A reset tour's line: the update's day (formatted) and its reason (§8); `reason` is\n * left out when the tour's update carries none. */\n resetOn: (date: string, reason?: string) => string;\n /** The points chip. */\n points: (count: number) => string;\n steps: (count: number) => string;\n /** The duration, in minutes. */\n duration: (minutes: number) => string;\n /** Completed, with the day (formatted). */\n completedOn: (date: string) => string;\n /** Completed without a date (kastlan's old id list, §6.2). */\n completed: string;\n /** Not taken yet — the state icon, for a reader. */\n notCompleted: string;\n markUndone: string;\n start: string;\n replay: string;\n}\n\nexport const DEFAULT_TOUR_CATALOGUE_LABELS: TourCatalogueLabels = {\n searchLabel: \"Search tours\",\n searchPlaceholder: \"Search tours…\",\n groupLabel: \"Topic\",\n groupAll: (count) => `All topics (${count})`,\n groupOption: (group, count) => `${group} (${count})`,\n stateLabel: \"Filter tours\",\n stateAll: (count) => `All (${count})`,\n stateTodo: (count) => `To do (${count})`,\n stateDone: (count) => `Completed (${count})`,\n showing: (shown, total) => `Showing ${shown} of ${total} tours`,\n noMatches: \"No tour matches\",\n noMatchesHint: \"Try another word, or clear the filters.\",\n clearFilters: \"Clear filters\",\n badgeNew: \"New\",\n badgeUpdated: \"Updated\",\n resetOn: (date, reason) => (reason ? `Reset on ${date}: ${reason}` : `Reset on ${date}`),\n points: (count) => `+${count} pts`,\n steps: (count) => (count === 1 ? \"1 step\" : `${count} steps`),\n duration: (minutes) => `About ${minutes} min`,\n completedOn: (date) => `Completed on ${date}`,\n completed: \"Completed\",\n notCompleted: \"Not completed\",\n markUndone: \"Mark as not done\",\n start: \"Start\",\n replay: \"Replay\",\n};\n"],"mappings":";AAgDO,MAAM,gCAAqD;AAAA,EAChE,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,eAAe,KAAK;AAAA,EACzC,aAAa,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,EACjD,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,QAAQ,KAAK;AAAA,EAClC,WAAW,CAAC,UAAU,UAAU,KAAK;AAAA,EACrC,WAAW,CAAC,UAAU,cAAc,KAAK;AAAA,EACzC,SAAS,CAAC,OAAO,UAAU,WAAW,KAAK,OAAO,KAAK;AAAA,EACvD,WAAW;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU;AAAA,EACV,cAAc;AAAA,EACd,SAAS,CAAC,MAAM,WAAY,SAAS,YAAY,IAAI,KAAK,MAAM,KAAK,YAAY,IAAI;AAAA,EACrF,QAAQ,CAAC,UAAU,IAAI,KAAK;AAAA,EAC5B,OAAO,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAAA,EACpD,UAAU,CAAC,YAAY,SAAS,OAAO;AAAA,EACvC,aAAa,CAAC,SAAS,gBAAgB,IAAI;AAAA,EAC3C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
1
+ {"version":3,"sources":["../../src/tour/tour-catalogue-labels.ts"],"sourcesContent":["// The English words of `TourCatalogue` and `TourCatalogueCard` (docs/tours-harmonization.md\n// §3), in a module with no component code, like every `*-labels.ts` (0.34, KS U8).\n// keksdose's reviewed words (tour.* in its en.json) where it had them.\n\n/** The `tourCatalogue` namespace. Counts arrive as numbers, dates already formatted. */\nexport interface TourCatalogueLabels {\n /** The search box's name. */\n searchLabel: string;\n searchPlaceholder: string;\n /** The group select's name (keksdose: \"Topic\"). */\n groupLabel: string;\n /** The select's first option, with the count the other filters leave. */\n groupAll: (count: number) => string;\n /** A group's option: its title and its count. */\n groupOption: (group: string, count: number) => string;\n /** The all / to do / done toggle's name. */\n stateLabel: string;\n stateAll: (count: number) => string;\n stateTodo: (count: number) => string;\n stateDone: (count: number) => string;\n /** Under the filter bar while a filter is on. */\n showing: (shown: number, total: number) => string;\n noMatches: string;\n noMatchesHint: string;\n clearFilters: string;\n /** The NEW pill. */\n badgeNew: string;\n /** The pill on a tour reset by an update (§8). */\n badgeUpdated: string;\n /** A reset tour's line: the update's day (formatted) and its reason (§8); `reason` is\n * left out when the tour's update carries none. */\n resetOn: (date: string, reason?: string) => string;\n /** The points chip. */\n points: (count: number) => string;\n steps: (count: number) => string;\n /** The duration, in minutes. */\n duration: (minutes: number) => string;\n /** Completed, with the day (formatted). */\n completedOn: (date: string) => string;\n /** Completed without a date (kastlan's old id list, §6.2). */\n completed: string;\n /** Not taken yet — the state icon, for a reader. */\n notCompleted: string;\n markUndone: string;\n start: string;\n replay: string;\n}\n\nexport const DEFAULT_TOUR_CATALOGUE_LABELS: TourCatalogueLabels = {\n searchLabel: \"Search tours\",\n searchPlaceholder: \"Search tours…\",\n groupLabel: \"Topic\",\n groupAll: (count) => `All topics (${count})`,\n groupOption: (group, count) => `${group} (${count})`,\n stateLabel: \"Filter tours\",\n stateAll: (count) => `All (${count})`,\n stateTodo: (count) => `Not taken (${count})`,\n stateDone: (count) => `Completed (${count})`,\n showing: (shown, total) => `Showing ${shown} of ${total} tours`,\n noMatches: \"No tour matches\",\n noMatchesHint: \"Try another word, or clear the filter.\",\n clearFilters: \"Clear filters\",\n badgeNew: \"New\",\n badgeUpdated: \"Updated\",\n resetOn: (date, reason) => (reason ? `Reset on ${date}: ${reason}` : `Reset on ${date}`),\n points: (count) => `+${count} pts`,\n steps: (count) => (count === 1 ? \"1 step\" : `${count} steps`),\n duration: (minutes) => `About ${minutes} min`,\n completedOn: (date) => `Completed on ${date}`,\n completed: \"Completed\",\n notCompleted: \"Not completed\",\n markUndone: \"Mark as not done\",\n start: \"Start\",\n replay: \"Replay\",\n};\n"],"mappings":";AAgDO,MAAM,gCAAqD;AAAA,EAChE,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,eAAe,KAAK;AAAA,EACzC,aAAa,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,EACjD,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,QAAQ,KAAK;AAAA,EAClC,WAAW,CAAC,UAAU,cAAc,KAAK;AAAA,EACzC,WAAW,CAAC,UAAU,cAAc,KAAK;AAAA,EACzC,SAAS,CAAC,OAAO,UAAU,WAAW,KAAK,OAAO,KAAK;AAAA,EACvD,WAAW;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU;AAAA,EACV,cAAc;AAAA,EACd,SAAS,CAAC,MAAM,WAAY,SAAS,YAAY,IAAI,KAAK,MAAM,KAAK,YAAY,IAAI;AAAA,EACrF,QAAQ,CAAC,UAAU,IAAI,KAAK;AAAA,EAC5B,OAAO,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAAA,EACpD,UAAU,CAAC,YAAY,SAAS,OAAO;AAAA,EACvC,aAAa,CAAC,SAAS,gBAAgB,IAAI;AAAA,EAC3C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
@@ -14,7 +14,12 @@ export interface TourCatalogueCardProps {
14
14
  onLaunch: (id: string) => void;
15
15
  /** "Mark as not done". Without it, the link is not drawn. */
16
16
  onReset?: (id: string) => void;
17
- /** Formats the completion and update days. Default: the kit's medium date in the provider's locale. */
17
+ /**
18
+ * Formats the completion and update days. Default: the kit's medium date in the provider's locale.
19
+ * It gets the completion's ISO instant and the update's bare `"YYYY-MM-DD"` (`updated.date`): read
20
+ * the bare day on its calendar (the kit's `formatDate`, `parseIsoDate`), never `new Date(day)`,
21
+ * which is UTC midnight and the day before west of UTC (round 0.45, N9).
22
+ */
18
23
  formatDate?: (iso: string) => string;
19
24
  labels?: LabelOverride<TourCatalogueLabels>;
20
25
  className?: string;
@@ -44,6 +49,7 @@ export interface TourCatalogueProps extends TourCatalogueFilter {
44
49
  onReset?: (id: string) => void;
45
50
  /** Above the filter bar: the app's page header with its points tile (§6.1). */
46
51
  header?: ReactNode;
52
+ /** Each card's `formatDate`: see {@link TourCatalogueCardProps.formatDate} (a bare day for the update). */
47
53
  formatDate?: (iso: string) => string;
48
54
  /** The search's locale (case folding). Default: the provider's. */
49
55
  locale?: string;