@eifi1/ui-kit 0.44.0 → 0.46.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 (285) hide show
  1. package/README.md +29 -14
  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 +15 -0
  8. package/dist/admin/admin-area-labels.js +6 -1
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/admin-overview.d.ts +16 -1
  11. package/dist/admin/admin-overview.js +12 -1
  12. package/dist/admin/admin-overview.js.map +1 -1
  13. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  14. package/dist/admin/mail-test-card-labels.js +17 -0
  15. package/dist/admin/mail-test-card-labels.js.map +1 -0
  16. package/dist/admin/mail-test-card.d.ts +75 -0
  17. package/dist/admin/mail-test-card.js +82 -0
  18. package/dist/admin/mail-test-card.js.map +1 -0
  19. package/dist/components/appearance-labels.d.ts +10 -0
  20. package/dist/components/appearance-labels.js +6 -1
  21. package/dist/components/appearance-labels.js.map +1 -1
  22. package/dist/components/data-table-column-settings.js +2 -2
  23. package/dist/components/data-table-column-settings.js.map +1 -1
  24. package/dist/components/data-table-head.js +2 -2
  25. package/dist/components/data-table-head.js.map +1 -1
  26. package/dist/components/data-table-mobile.js +17 -2
  27. package/dist/components/data-table-mobile.js.map +1 -1
  28. package/dist/components/data-table-row-actions.d.ts +18 -2
  29. package/dist/components/data-table-row-actions.js +86 -2
  30. package/dist/components/data-table-row-actions.js.map +1 -1
  31. package/dist/components/data-table-shared.d.ts +2 -0
  32. package/dist/components/data-table-shared.js +17 -2
  33. package/dist/components/data-table-shared.js.map +1 -1
  34. package/dist/components/data-table.d.ts +43 -2
  35. package/dist/components/data-table.js +10 -10
  36. package/dist/components/data-table.js.map +1 -1
  37. package/dist/components/date-format-setting.d.ts +29 -0
  38. package/dist/components/date-format-setting.js +36 -0
  39. package/dist/components/date-format-setting.js.map +1 -0
  40. package/dist/components/swipeable-row.d.ts +8 -0
  41. package/dist/components/swipeable-row.js.map +1 -1
  42. package/dist/components/tabs.d.ts +3 -2
  43. package/dist/components/tabs.js +1 -1
  44. package/dist/components/tabs.js.map +1 -1
  45. package/dist/components/translation-review-editor.js +1 -0
  46. package/dist/components/translation-review-editor.js.map +1 -1
  47. package/dist/components/translation-review-labels.d.ts +5 -0
  48. package/dist/components/translation-review-labels.js +2 -0
  49. package/dist/components/translation-review-labels.js.map +1 -1
  50. package/dist/components/translation-review.d.ts +18 -8
  51. package/dist/components/translation-review.js +64 -7
  52. package/dist/components/translation-review.js.map +1 -1
  53. package/dist/components/write-lock-labels.js +2 -1
  54. package/dist/components/write-lock-labels.js.map +1 -1
  55. package/dist/components/write-lock.d.ts +33 -3
  56. package/dist/components/write-lock.js +9 -0
  57. package/dist/components/write-lock.js.map +1 -1
  58. package/dist/data-table.d.ts +1 -1
  59. package/dist/data-table.js.map +1 -1
  60. package/dist/eslint/index.js +1 -1
  61. package/dist/feedback/feedback-row-detail.d.ts +4 -2
  62. package/dist/feedback/feedback-row-detail.js +17 -26
  63. package/dist/feedback/feedback-row-detail.js.map +1 -1
  64. package/dist/feedback/feedback-swipe.d.ts +18 -0
  65. package/dist/feedback/feedback-swipe.js +10 -1
  66. package/dist/feedback/feedback-swipe.js.map +1 -1
  67. package/dist/i18n/defaults.js +9 -1
  68. package/dist/i18n/defaults.js.map +1 -1
  69. package/dist/i18n/german.js +81 -5
  70. package/dist/i18n/german.js.map +1 -1
  71. package/dist/i18n/kit-labels.d.ts +25 -1
  72. package/dist/i18n/kit-labels.js +8 -2
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +97 -7
  75. package/dist/i18n/locales/en.js.map +3 -3
  76. package/dist/i18n/locales/es.js +81 -5
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +81 -5
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +81 -5
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +81 -5
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +81 -5
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/review.js +16 -2
  87. package/dist/i18n/review.js.map +1 -1
  88. package/dist/index.d.ts +17 -2
  89. package/dist/index.js +40 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/date-format.d.ts +81 -0
  92. package/dist/lib/date-format.js +56 -0
  93. package/dist/lib/date-format.js.map +1 -0
  94. package/dist/lib/format.d.ts +5 -1
  95. package/dist/lib/format.js +11 -5
  96. package/dist/lib/format.js.map +1 -1
  97. package/dist/notifications/device-zone-report.d.ts +27 -0
  98. package/dist/notifications/device-zone-report.js +24 -0
  99. package/dist/notifications/device-zone-report.js.map +1 -0
  100. package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
  101. package/dist/notifications/feedback-notification-types-labels.js +4 -1
  102. package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
  103. package/dist/notifications/feedback-notification-types.d.ts +11 -5
  104. package/dist/notifications/feedback-notification-types.js +2 -1
  105. package/dist/notifications/feedback-notification-types.js.map +1 -1
  106. package/dist/notifications/notification-bell.d.ts +33 -0
  107. package/dist/notifications/notification-bell.js +74 -0
  108. package/dist/notifications/notification-bell.js.map +1 -0
  109. package/dist/notifications/notification-inbox.d.ts +77 -0
  110. package/dist/notifications/notification-inbox.js +206 -0
  111. package/dist/notifications/notification-inbox.js.map +1 -0
  112. package/dist/notifications/notification-types-setting.d.ts +6 -4
  113. package/dist/notifications/notification-types-setting.js +103 -45
  114. package/dist/notifications/notification-types-setting.js.map +1 -1
  115. package/dist/notifications/notification-types.d.ts +19 -7
  116. package/dist/notifications/notifications-labels.d.ts +41 -7
  117. package/dist/notifications/notifications-labels.js +18 -1
  118. package/dist/notifications/notifications-labels.js.map +1 -1
  119. package/dist/notifications/push-devices-setting.d.ts +20 -3
  120. package/dist/notifications/push-devices-setting.js +93 -33
  121. package/dist/notifications/push-devices-setting.js.map +1 -1
  122. package/dist/notifications/push-worker.d.ts +108 -0
  123. package/dist/notifications/push-worker.js +62 -0
  124. package/dist/notifications/push-worker.js.map +1 -0
  125. package/dist/notifications/quiet-hours-setting.d.ts +29 -17
  126. package/dist/notifications/quiet-hours-setting.js +9 -41
  127. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  128. package/dist/notifications/time-zone-select.d.ts +5 -2
  129. package/dist/notifications/time-zone-select.js.map +1 -1
  130. package/dist/notifications/use-push-device.d.ts +4 -17
  131. package/dist/notifications/use-push-device.js +7 -15
  132. package/dist/notifications/use-push-device.js.map +1 -1
  133. package/dist/offline/local-first.d.ts +33 -0
  134. package/dist/offline/local-first.js +39 -0
  135. package/dist/offline/local-first.js.map +1 -0
  136. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  137. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  138. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  139. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  140. package/dist/offline/pwa-update-prompt.js +36 -0
  141. package/dist/offline/pwa-update-prompt.js.map +1 -0
  142. package/dist/offline/query-persistence.d.ts +125 -0
  143. package/dist/offline/query-persistence.js +183 -0
  144. package/dist/offline/query-persistence.js.map +1 -0
  145. package/dist/offline/service-worker-updates.d.ts +46 -0
  146. package/dist/offline/service-worker-updates.js +39 -0
  147. package/dist/offline/service-worker-updates.js.map +1 -0
  148. package/dist/offline/shared-computer-labels.d.ts +2 -0
  149. package/dist/offline/shared-computer-labels.js +9 -0
  150. package/dist/offline/shared-computer-labels.js.map +1 -0
  151. package/dist/offline/shared-computer.d.ts +44 -0
  152. package/dist/offline/shared-computer.js +51 -0
  153. package/dist/offline/shared-computer.js.map +1 -0
  154. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  155. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  156. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  157. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  158. package/dist/offline/sign-out-pending-dialog.js +66 -0
  159. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  160. package/dist/offline.d.ts +11 -0
  161. package/dist/offline.js +33 -0
  162. package/dist/offline.js.map +1 -0
  163. package/dist/push-worker.d.ts +2 -0
  164. package/dist/push-worker.js +14 -0
  165. package/dist/push-worker.js.map +1 -0
  166. package/dist/shell/account-menu-text-size.d.ts +33 -0
  167. package/dist/shell/account-menu-text-size.js +51 -0
  168. package/dist/shell/account-menu-text-size.js.map +1 -0
  169. package/dist/shell/admin-menu.d.ts +24 -0
  170. package/dist/shell/admin-menu.js +12 -3
  171. package/dist/shell/admin-menu.js.map +1 -1
  172. package/dist/shell/top-bar-actions.d.ts +6 -2
  173. package/dist/shell/top-bar-actions.js +1 -0
  174. package/dist/shell/top-bar-actions.js.map +1 -1
  175. package/dist/shell.d.ts +4 -0
  176. package/dist/shell.js +4 -0
  177. package/dist/shell.js.map +1 -1
  178. package/dist/sync/db.d.ts +76 -0
  179. package/dist/sync/db.js +24 -0
  180. package/dist/sync/db.js.map +1 -0
  181. package/dist/sync/engine.d.ts +124 -0
  182. package/dist/sync/engine.js +474 -0
  183. package/dist/sync/engine.js.map +1 -0
  184. package/dist/sync/hooks.d.ts +30 -0
  185. package/dist/sync/hooks.js +67 -0
  186. package/dist/sync/hooks.js.map +1 -0
  187. package/dist/sync/testing.d.ts +28 -0
  188. package/dist/sync/testing.js +41 -0
  189. package/dist/sync/testing.js.map +1 -0
  190. package/dist/sync/wire.d.ts +50 -0
  191. package/dist/sync/wire.js +2 -0
  192. package/dist/sync/wire.js.map +1 -0
  193. package/dist/sync.d.ts +7 -0
  194. package/dist/sync.js +22 -0
  195. package/dist/sync.js.map +1 -0
  196. package/dist/tour/tour-catalogue.d.ts +7 -1
  197. package/dist/tour/tour-catalogue.js.map +1 -1
  198. package/dist/tour/tour-labels.js +1 -0
  199. package/dist/tour/tour-labels.js.map +1 -1
  200. package/dist/tour/tour.d.ts +30 -0
  201. package/dist/tour/tour.js +54 -8
  202. package/dist/tour/tour.js.map +1 -1
  203. package/dist/tour/use-tour-progress.d.ts +99 -1
  204. package/dist/tour/use-tour-progress.js +228 -12
  205. package/dist/tour/use-tour-progress.js.map +1 -1
  206. package/dist/tour.d.ts +2 -0
  207. package/dist/tour.js +2 -0
  208. package/dist/tour.js.map +1 -1
  209. package/package.json +43 -1
  210. package/src/admin/admin-action-log-labels.ts +9 -0
  211. package/src/admin/admin-action-log.tsx +41 -5
  212. package/src/admin/admin-area-labels.ts +20 -0
  213. package/src/admin/admin-overview.tsx +37 -2
  214. package/src/admin/mail-test-card-labels.ts +19 -0
  215. package/src/admin/mail-test-card.tsx +190 -0
  216. package/src/components/appearance-labels.ts +16 -0
  217. package/src/components/data-table-column-settings.tsx +2 -2
  218. package/src/components/data-table-head.tsx +2 -2
  219. package/src/components/data-table-mobile.tsx +26 -3
  220. package/src/components/data-table-row-actions.tsx +118 -2
  221. package/src/components/data-table-shared.tsx +39 -3
  222. package/src/components/data-table.tsx +52 -21
  223. package/src/components/date-format-setting.tsx +64 -0
  224. package/src/components/swipeable-row.tsx +8 -0
  225. package/src/components/tabs.tsx +4 -3
  226. package/src/components/translation-review-editor.tsx +2 -0
  227. package/src/components/translation-review-labels.ts +7 -0
  228. package/src/components/translation-review.tsx +102 -14
  229. package/src/components/write-lock-labels.ts +1 -0
  230. package/src/components/write-lock.tsx +36 -3
  231. package/src/data-table.ts +1 -0
  232. package/src/feedback/feedback-row-detail.tsx +18 -23
  233. package/src/feedback/feedback-swipe.tsx +28 -1
  234. package/src/i18n/defaults.ts +8 -0
  235. package/src/i18n/german.ts +90 -4
  236. package/src/i18n/kit-labels.tsx +31 -1
  237. package/src/i18n/locales/en.ts +13 -0
  238. package/src/i18n/locales/es.ts +90 -4
  239. package/src/i18n/locales/fr.ts +90 -4
  240. package/src/i18n/locales/hu.ts +86 -4
  241. package/src/i18n/locales/it.ts +90 -4
  242. package/src/i18n/locales/zh.ts +84 -2
  243. package/src/i18n/review.ts +15 -1
  244. package/src/index.ts +45 -0
  245. package/src/lib/date-format.ts +151 -0
  246. package/src/lib/format.ts +23 -6
  247. package/src/notifications/device-zone-report.ts +54 -0
  248. package/src/notifications/feedback-notification-types-labels.ts +10 -1
  249. package/src/notifications/feedback-notification-types.ts +12 -5
  250. package/src/notifications/notification-bell.tsx +116 -0
  251. package/src/notifications/notification-inbox.tsx +304 -0
  252. package/src/notifications/notification-types-setting.tsx +151 -55
  253. package/src/notifications/notification-types.ts +20 -8
  254. package/src/notifications/notifications-labels.ts +69 -11
  255. package/src/notifications/push-devices-setting.tsx +123 -34
  256. package/src/notifications/push-worker.ts +160 -0
  257. package/src/notifications/quiet-hours-setting.tsx +41 -58
  258. package/src/notifications/time-zone-select.tsx +5 -2
  259. package/src/notifications/use-push-device.ts +20 -33
  260. package/src/offline/local-first.ts +76 -0
  261. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  262. package/src/offline/pwa-update-prompt.tsx +72 -0
  263. package/src/offline/query-persistence.ts +356 -0
  264. package/src/offline/service-worker-updates.ts +81 -0
  265. package/src/offline/shared-computer-labels.ts +8 -0
  266. package/src/offline/shared-computer.tsx +87 -0
  267. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  268. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  269. package/src/offline.ts +32 -0
  270. package/src/push-worker.ts +25 -0
  271. package/src/shell/account-menu-text-size.tsx +77 -0
  272. package/src/shell/admin-menu.tsx +40 -3
  273. package/src/shell/top-bar-actions.tsx +6 -1
  274. package/src/shell.ts +7 -0
  275. package/src/sync/db.ts +103 -0
  276. package/src/sync/engine.ts +699 -0
  277. package/src/sync/hooks.ts +93 -0
  278. package/src/sync/testing.ts +73 -0
  279. package/src/sync/wire.ts +61 -0
  280. package/src/sync.ts +29 -0
  281. package/src/tour/tour-catalogue.tsx +7 -1
  282. package/src/tour/tour-labels.ts +1 -0
  283. package/src/tour/tour.tsx +105 -9
  284. package/src/tour/use-tour-progress.ts +378 -12
  285. package/src/tour.ts +3 -0
@@ -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,11 @@
1
+ export { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe, removeStaleQueryCopies } from "./offline/query-persistence.js";
2
+ export type { QueryPersistence, QueryPersistenceOptions, QueryPersistenceStorage, RemoveStaleQueryCopiesOptions, 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 { createServiceWorkerUpdates, useServiceWorkerUpdate } from "./offline/service-worker-updates.js";
8
+ export type { RegisterServiceWorker, ServiceWorkerUpdates } from "./offline/service-worker-updates.js";
9
+ export { SharedComputerChoice, DEFAULT_SHARED_COMPUTER_LABELS, isSharedComputer, setSharedComputer, } from "./offline/shared-computer.js";
10
+ export type { SharedComputerChoiceProps, SharedComputerLabels, SharedComputerStorage } from "./offline/shared-computer.js";
11
+ export { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe, removeStaleQueryCopies } 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 { createServiceWorkerUpdates, useServiceWorkerUpdate } from "./offline/service-worker-updates.js";
6
+ import {
7
+ SharedComputerChoice,
8
+ DEFAULT_SHARED_COMPUTER_LABELS,
9
+ isSharedComputer,
10
+ setSharedComputer
11
+ } from "./offline/shared-computer.js";
12
+ import { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
13
+ export {
14
+ DEFAULT_PWA_UPDATE_PROMPT_LABELS,
15
+ DEFAULT_SCRUB_FIELDS,
16
+ DEFAULT_SHARED_COMPUTER_LABELS,
17
+ DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS,
18
+ PwaUpdatePrompt,
19
+ SharedComputerChoice,
20
+ SignOutPendingDialog,
21
+ createQueryPersistence,
22
+ createServiceWorkerUpdates,
23
+ isCacheSafe,
24
+ isOfflineError,
25
+ isSharedComputer,
26
+ readLocalFirst,
27
+ removeStaleQueryCopies,
28
+ setSharedComputer,
29
+ stampAfter,
30
+ useServiceWorkerUpdate,
31
+ writeLocalFirst
32
+ };
33
+ //# 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, removeStaleQueryCopies } from \"./offline/query-persistence\";\nexport type {\n QueryPersistence,\n QueryPersistenceOptions,\n QueryPersistenceStorage,\n RemoveStaleQueryCopiesOptions,\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\";\n// 0.46 (kastlan K3): the service worker registered once at start, its update state as a store.\nexport { createServiceWorkerUpdates, useServiceWorkerUpdate } from \"./offline/service-worker-updates\";\nexport type { RegisterServiceWorker, ServiceWorkerUpdates } from \"./offline/service-worker-updates\";\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// 0.46 (§2.3): the leaf utilities, shared with `@eifi1/ui-kit/sync` from one module that imports no peer.\nexport { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from \"./offline/local-first\";\n"],"mappings":";AAQA,SAAS,wBAAwB,sBAAsB,aAAa,8BAA8B;AAQlG,SAAS,sBAAsB,8CAA8C;AAE7E,SAAS,iBAAiB,wCAAwC;AAGlE,SAAS,4BAA4B,8BAA8B;AAEnE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gBAAgB,gBAAgB,YAAY,uBAAuB;","names":[]}
@@ -0,0 +1,2 @@
1
+ export { PUSH_SUBSCRIPTION_CHANGED, handlePushSubscriptionChange, pushSubscriptionInput, resubscribeAfterChange, } from "./notifications/push-worker.js";
2
+ export type { PushReplaceInput, PushResubscribeOutcome, PushSubscriptionChangeEventLike, PushSubscriptionChangeOptions, PushSubscriptionInput, PushWorkerScope, } from "./notifications/push-worker.js";
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import {
3
+ PUSH_SUBSCRIPTION_CHANGED,
4
+ handlePushSubscriptionChange,
5
+ pushSubscriptionInput,
6
+ resubscribeAfterChange
7
+ } from "./notifications/push-worker.js";
8
+ export {
9
+ PUSH_SUBSCRIPTION_CHANGED,
10
+ handlePushSubscriptionChange,
11
+ pushSubscriptionInput,
12
+ resubscribeAfterChange
13
+ };
14
+ //# sourceMappingURL=push-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/push-worker.ts"],"sourcesContent":["// `@eifi1/ui-kit/push-worker` — the service worker's half of Web Push (0.46,\n// docs/notifications-harmonization.md §10.6 rule 5): re-registering a subscription the browser\n// rotated. A worker bundle imports from here, never from the main barrel, which reaches React\n// and the DOM components:\n//\n// // sw.ts\n// import { handlePushSubscriptionChange } from \"@eifi1/ui-kit/push-worker\";\n//\n// Worker-safe: this entry reaches no package and no module but src/notifications/push-worker.ts\n// (packaging-contract.test.ts, scripts/check-module-graph.mjs, scripts/verify-package.mjs).\n// The main barrel keeps re-exporting the same names, so an existing import keeps working.\nexport {\n PUSH_SUBSCRIPTION_CHANGED,\n handlePushSubscriptionChange,\n pushSubscriptionInput,\n resubscribeAfterChange,\n} from \"./notifications/push-worker\";\nexport type {\n PushReplaceInput,\n PushResubscribeOutcome,\n PushSubscriptionChangeEventLike,\n PushSubscriptionChangeOptions,\n PushSubscriptionInput,\n PushWorkerScope,\n} from \"./notifications/push-worker\";\n"],"mappings":";AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
@@ -0,0 +1,33 @@
1
+ import { type AppearanceLabels } from "../components/appearance-labels.js";
2
+ import type { LabelOverride } from "../i18n/kit-labels.js";
3
+ import { type TextSize } from "../theme/text-size.js";
4
+ export interface AccountMenuTextSizeProps {
5
+ /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the
6
+ * size whoever applied it put there. */
7
+ value?: TextSize;
8
+ /**
9
+ * A step: the next size down or up. The app applies it and keeps it as its Settings ▸
10
+ * Appearance does — `useAccountAppearance`'s `pickTextSize` (this device, and the
11
+ * account when signed in), with `appearance.saveFailed` as the toast when the account
12
+ * refused. The kit does not apply it itself, so the two places can never disagree.
13
+ */
14
+ onPick: (size: TextSize) => void;
15
+ /** The row's name. Default: `appearance.textSize` ("Text size"). */
16
+ label?: string;
17
+ /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */
18
+ labels?: LabelOverride<AppearanceLabels>;
19
+ }
20
+ /**
21
+ * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its
22
+ * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step
23
+ * through {@link TEXT_SIZES} (Normal → Large → Extra large). A− is disabled at the
24
+ * smallest, A+ at the largest. The browser's pinch zoom stays as it is — this is the
25
+ * quick path to the setting, not a replacement for zoom.
26
+ *
27
+ * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` beside
28
+ * `AccountMenuLanguage`. The menu promotes the two buttons to `menuitem`s, so ↑/↓ reach
29
+ * them like every other row, and it stays open on a step: the person sees the page grow
30
+ * behind it and can step again. The size's name is a polite live region, so a step is
31
+ * announced.
32
+ */
33
+ export declare function AccountMenuTextSize({ value, onPick, label, labels: labelsProp }: AccountMenuTextSizeProps): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { AArrowDown, AArrowUp, ALargeSmall } from "lucide-react";
4
+ import { IconButton } from "../components/icon-button.js";
5
+ import { useAppearanceLabels } from "../components/appearance-labels.js";
6
+ import { TEXT_SIZES, useTextSize } from "../theme/text-size.js";
7
+ function AccountMenuTextSize({ value, onPick, label, labels: labelsProp }) {
8
+ const labels = useAppearanceLabels(labelsProp);
9
+ const onHtml = useTextSize().size;
10
+ const size = value ?? onHtml;
11
+ const index = TEXT_SIZES.indexOf(size);
12
+ const step = (by) => {
13
+ const next = TEXT_SIZES[index + by];
14
+ if (next) onPick(next);
15
+ };
16
+ return /* @__PURE__ */ jsxs("li", { "data-account-menu-text-size": "", className: "flex items-center gap-2 px-3 py-1.5 text-sm", children: [
17
+ /* @__PURE__ */ jsx(ALargeSmall, { "aria-hidden": true, className: "size-4 shrink-0 text-muted" }),
18
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 truncate", children: [
19
+ label ?? labels.textSize,
20
+ /* @__PURE__ */ jsx("span", { className: "ms-2 text-xs text-muted", "aria-live": "polite", children: labels.textSizes[size] })
21
+ ] }),
22
+ /* @__PURE__ */ jsx(
23
+ IconButton,
24
+ {
25
+ size: "sm",
26
+ label: labels.textSmaller,
27
+ labelVisible: false,
28
+ tooltip: false,
29
+ disabled: index <= 0,
30
+ onClick: () => step(-1),
31
+ children: /* @__PURE__ */ jsx(AArrowDown, {})
32
+ }
33
+ ),
34
+ /* @__PURE__ */ jsx(
35
+ IconButton,
36
+ {
37
+ size: "sm",
38
+ label: labels.textLarger,
39
+ labelVisible: false,
40
+ tooltip: false,
41
+ disabled: index >= TEXT_SIZES.length - 1,
42
+ onClick: () => step(1),
43
+ children: /* @__PURE__ */ jsx(AArrowUp, {})
44
+ }
45
+ )
46
+ ] });
47
+ }
48
+ export {
49
+ AccountMenuTextSize
50
+ };
51
+ //# sourceMappingURL=account-menu-text-size.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/account-menu-text-size.tsx"],"sourcesContent":["import { AArrowDown, AArrowUp, ALargeSmall } from \"lucide-react\";\nimport { IconButton } from \"../components/icon-button\";\nimport { useAppearanceLabels, type AppearanceLabels } from \"../components/appearance-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TEXT_SIZES, useTextSize, type TextSize } from \"../theme/text-size\";\n\nexport interface AccountMenuTextSizeProps {\n /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the\n * size whoever applied it put there. */\n value?: TextSize;\n /**\n * A step: the next size down or up. The app applies it and keeps it as its Settings ▸\n * Appearance does — `useAccountAppearance`'s `pickTextSize` (this device, and the\n * account when signed in), with `appearance.saveFailed` as the toast when the account\n * refused. The kit does not apply it itself, so the two places can never disagree.\n */\n onPick: (size: TextSize) => void;\n /** The row's name. Default: `appearance.textSize` (\"Text size\"). */\n label?: string;\n /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */\n labels?: LabelOverride<AppearanceLabels>;\n}\n\n/**\n * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its\n * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step\n * through {@link TEXT_SIZES} (Normal → Large → Extra large). A− is disabled at the\n * smallest, A+ at the largest. The browser's pinch zoom stays as it is — this is the\n * quick path to the setting, not a replacement for zoom.\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` beside\n * `AccountMenuLanguage`. The menu promotes the two buttons to `menuitem`s, so ↑/↓ reach\n * them like every other row, and it stays open on a step: the person sees the page grow\n * behind it and can step again. The size's name is a polite live region, so a step is\n * announced.\n */\nexport function AccountMenuTextSize({ value, onPick, label, labels: labelsProp }: AccountMenuTextSizeProps) {\n const labels = useAppearanceLabels(labelsProp);\n const onHtml = useTextSize().size;\n const size = value ?? onHtml;\n const index = TEXT_SIZES.indexOf(size);\n const step = (by: -1 | 1) => {\n const next = TEXT_SIZES[index + by];\n if (next) onPick(next);\n };\n return (\n <li data-account-menu-text-size=\"\" className=\"flex items-center gap-2 px-3 py-1.5 text-sm\">\n <ALargeSmall aria-hidden className=\"size-4 shrink-0 text-muted\" />\n <span className=\"min-w-0 flex-1 truncate\">\n {label ?? labels.textSize}\n <span className=\"ms-2 text-xs text-muted\" aria-live=\"polite\">\n {labels.textSizes[size]}\n </span>\n </span>\n <IconButton\n size=\"sm\"\n label={labels.textSmaller}\n labelVisible={false}\n tooltip={false}\n disabled={index <= 0}\n onClick={() => step(-1)}\n >\n <AArrowDown />\n </IconButton>\n <IconButton\n size=\"sm\"\n label={labels.textLarger}\n labelVisible={false}\n tooltip={false}\n disabled={index >= TEXT_SIZES.length - 1}\n onClick={() => step(1)}\n >\n <AArrowUp />\n </IconButton>\n </li>\n );\n}\n"],"mappings":";AA+CM,cACA,YADA;AA/CN,SAAS,YAAY,UAAU,mBAAmB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,2BAAkD;AAE3D,SAAS,YAAY,mBAAkC;AAgChD,SAAS,oBAAoB,EAAE,OAAO,QAAQ,OAAO,QAAQ,WAAW,GAA6B;AAC1G,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,SAAS,YAAY,EAAE;AAC7B,QAAM,OAAO,SAAS;AACtB,QAAM,QAAQ,WAAW,QAAQ,IAAI;AACrC,QAAM,OAAO,CAAC,OAAe;AAC3B,UAAM,OAAO,WAAW,QAAQ,EAAE;AAClC,QAAI,KAAM,QAAO,IAAI;AAAA,EACvB;AACA,SACE,qBAAC,QAAG,+BAA4B,IAAG,WAAU,+CAC3C;AAAA,wBAAC,eAAY,eAAW,MAAC,WAAU,8BAA6B;AAAA,IAChE,qBAAC,UAAK,WAAU,2BACb;AAAA,eAAS,OAAO;AAAA,MACjB,oBAAC,UAAK,WAAU,2BAA0B,aAAU,UACjD,iBAAO,UAAU,IAAI,GACxB;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS;AAAA,QACnB,SAAS,MAAM,KAAK,EAAE;AAAA,QAEtB,8BAAC,cAAW;AAAA;AAAA,IACd;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS,WAAW,SAAS;AAAA,QACvC,SAAS,MAAM,KAAK,CAAC;AAAA,QAErB,8BAAC,YAAS;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;","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":[]}
@@ -22,6 +22,10 @@ export interface TopBarActionsProps {
22
22
  dev?: ReactNode;
23
23
  /** kastlan's platform button. */
24
24
  platform?: ReactNode;
25
+ /** 0.46 (docs/notifications-harmonization.md §10.1): `NotificationBell`, right before the
26
+ * account menu. Optional per app: an app shows the bell or an unread dot on the avatar,
27
+ * never both (keksdose keeps its dot and leaves this empty). */
28
+ notifications?: ReactNode;
25
29
  /** The account menu. When it is set the signed-out slots are not drawn. */
26
30
  account?: ReactNode;
27
31
  /** Signed out (§6.6): the bar's `LanguageMenu`, since there is no account menu to hold
@@ -32,11 +36,11 @@ export interface TopBarActionsProps {
32
36
  signIn?: ReactNode;
33
37
  }
34
38
  /** §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"];
39
+ export declare const TOP_BAR_ACTION_ORDER: readonly ["companySwitcher", "search", "theme", "feedback", "tours", "assistant", "sync", "dev", "platform", "notifications", "account", "language", "signIn"];
36
40
  /**
37
41
  * The top bar's actions in the one order every app shares (§2.1): company switcher ·
38
42
  * 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).
43
+ * notifications (the bell, 0.46) · account menu — and, signed out, Language · Sign in where the account menu goes (§6.6).
40
44
  * The app fills the slots it has; an empty slot is skipped, so it leaves no gap.
41
45
  *
42
46
  * It draws a fragment, no box of its own: hand it to `TopBar`'s `actions`, whose row
@@ -11,6 +11,7 @@ const TOP_BAR_ACTION_ORDER = [
11
11
  "sync",
12
12
  "dev",
13
13
  "platform",
14
+ "notifications",
14
15
  "account",
15
16
  "language",
16
17
  "signIn"