@eifi1/ui-kit 0.46.0 → 0.46.1

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 (75) hide show
  1. package/README.md +10 -9
  2. package/dist/admin/user-roster.d.ts +6 -2
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/components/data-table-row-actions.d.ts +22 -5
  6. package/dist/components/data-table-row-actions.js +14 -5
  7. package/dist/components/data-table-row-actions.js.map +1 -1
  8. package/dist/components/data-table.d.ts +16 -1
  9. package/dist/components/data-table.js +2 -0
  10. package/dist/components/data-table.js.map +1 -1
  11. package/dist/components/row-actions.d.ts +12 -5
  12. package/dist/components/row-actions.js +11 -4
  13. package/dist/components/row-actions.js.map +1 -1
  14. package/dist/components/translation-review.js +3 -1
  15. package/dist/components/translation-review.js.map +1 -1
  16. package/dist/data-table.d.ts +1 -0
  17. package/dist/data-table.js +2 -0
  18. package/dist/data-table.js.map +1 -1
  19. package/dist/eslint/index.js +1 -1
  20. package/dist/i18n/locales/en.js.map +2 -2
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +4 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/notifications/device-zone-report.d.ts +7 -0
  25. package/dist/notifications/device-zone-report.js +8 -1
  26. package/dist/notifications/device-zone-report.js.map +1 -1
  27. package/dist/notifications/feedback-notification-types.d.ts +14 -1
  28. package/dist/notifications/feedback-notification-types.js +5 -1
  29. package/dist/notifications/feedback-notification-types.js.map +1 -1
  30. package/dist/notifications/notification-bell.d.ts +1 -1
  31. package/dist/notifications/notification-bell.js.map +1 -1
  32. package/dist/notifications/notification-inbox.d.ts +12 -3
  33. package/dist/notifications/notification-inbox.js +2 -2
  34. package/dist/notifications/notification-inbox.js.map +1 -1
  35. package/dist/notifications/notifications-labels.d.ts +3 -1
  36. package/dist/notifications/notifications-labels.js.map +1 -1
  37. package/dist/notifications/time-zone-select.d.ts +5 -4
  38. package/dist/notifications/time-zone-select.js +2 -7
  39. package/dist/notifications/time-zone-select.js.map +1 -1
  40. package/dist/offline/local-first.d.ts +9 -0
  41. package/dist/offline/local-first.js +14 -0
  42. package/dist/offline/local-first.js.map +1 -1
  43. package/dist/offline.d.ts +1 -1
  44. package/dist/offline.js +2 -1
  45. package/dist/offline.js.map +1 -1
  46. package/dist/sync/db.d.ts +17 -0
  47. package/dist/sync/db.js +10 -1
  48. package/dist/sync/db.js.map +1 -1
  49. package/dist/sync/engine.d.ts +11 -9
  50. package/dist/sync/engine.js.map +1 -1
  51. package/dist/sync/hooks.d.ts +5 -0
  52. package/dist/sync/hooks.js.map +1 -1
  53. package/dist/sync.d.ts +2 -2
  54. package/dist/sync.js +5 -2
  55. package/dist/sync.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/admin/user-roster.tsx +12 -3
  58. package/src/components/data-table-row-actions.tsx +33 -9
  59. package/src/components/data-table.tsx +17 -0
  60. package/src/components/row-actions.tsx +33 -9
  61. package/src/components/translation-review.tsx +3 -1
  62. package/src/data-table.ts +3 -0
  63. package/src/index.ts +10 -3
  64. package/src/notifications/device-zone-report.ts +14 -1
  65. package/src/notifications/feedback-notification-types.ts +19 -1
  66. package/src/notifications/notification-bell.tsx +1 -1
  67. package/src/notifications/notification-inbox.tsx +15 -5
  68. package/src/notifications/notifications-labels.ts +3 -1
  69. package/src/notifications/time-zone-select.tsx +6 -10
  70. package/src/offline/local-first.ts +24 -0
  71. package/src/offline.ts +1 -1
  72. package/src/sync/db.ts +22 -0
  73. package/src/sync/engine.ts +11 -9
  74. package/src/sync/hooks.ts +5 -0
  75. package/src/sync.ts +2 -2
@@ -21,7 +21,13 @@ export interface NotificationInboxItem<Id extends string | number = string | num
21
21
  /** 0.46 (server-kit 0.16 `InboxItem.company`): the company the row is about, named on
22
22
  * the row — kastlan's inbox holds every company's rows. */
23
23
  company?: string | null;
24
+ /** 0.46.1 (server-kit 0.16.1 `InboxItem.company_id`): the same company by its id, so
25
+ * `onOpen` switches to it exactly (two companies may share a name). */
26
+ company_id?: number | string | null;
24
27
  }
28
+ /** The inbox request that failed, as `describeError` gets it (0.46.1): one row marked read,
29
+ * "Mark all as read", or "Clear all". */
30
+ export type NotificationInboxAction = "read" | "readAll" | "clear";
25
31
  export interface NotificationInboxProps<Id extends string | number = string | number> extends Omit<ComponentPropsWithRef<"div">, "children" | "title"> {
26
32
  /** The rows, newest first; `undefined` while the first page loads. */
27
33
  items: readonly NotificationInboxItem<Id>[] | undefined;
@@ -34,7 +40,7 @@ export interface NotificationInboxProps<Id extends string | number = string | nu
34
40
  onClear: () => Promise<unknown> | void;
35
41
  /**
36
42
  * Open a row: the kit marks it read first, then calls this, and the app routes to
37
- * `item.url` (kastlan switches the acting company first). Every row is a button then.
43
+ * `item.url` (kastlan switches the acting company, `item.company_id`, first). Every row is a button then.
38
44
  * Left out, a row with a `url` is a real link — an in-app path through the provider's
39
45
  * router link, an absolute URL in a new tab — that marks itself read on a click or a
40
46
  * middle click (keksdose #451), and a row without one marks itself read.
@@ -53,8 +59,11 @@ export interface NotificationInboxProps<Id extends string | number = string | nu
53
59
  formatDate?: (iso: string) => string;
54
60
  /** The heading. Default `labels.inboxTitle` ("Inbox"). */
55
61
  title?: ReactNode;
56
- /** The app's own words for a failed request, or `undefined` for the kit's `failed`. */
57
- describeError?: (error: unknown) => ReactNode | undefined;
62
+ /** The app's own words for a failed request, or `undefined` for the kit's `failed`.
63
+ * `action` (0.46.1) says which request failed: `"read"` (one row), `"readAll"` ("Mark
64
+ * all as read") or `"clear"` ("Clear all") — so a failed mark-read is not worded as a
65
+ * failed clear. */
66
+ describeError?: (error: unknown, action: NotificationInboxAction) => ReactNode | undefined;
58
67
  labels?: LabelOverride<NotificationsLabels>;
59
68
  }
60
69
  /**
@@ -63,14 +63,14 @@ function NotificationInbox({
63
63
  (error) => {
64
64
  if (!mounted.current) return;
65
65
  setBusy(null);
66
- setFailure(describeFailure(describeError?.(error), labels.failed));
66
+ setFailure(describeFailure(describeError?.(error, which === "read" ? "readAll" : "clear"), labels.failed));
67
67
  }
68
68
  );
69
69
  };
70
70
  const markOne = (item) => {
71
71
  if (item.read_at !== null) return;
72
72
  settle(() => onMarkRead([item.id])).catch((error) => {
73
- if (mounted.current) setFailure(describeFailure(describeError?.(error), labels.failed));
73
+ if (mounted.current) setFailure(describeFailure(describeError?.(error, "read"), labels.failed));
74
74
  });
75
75
  };
76
76
  const leave = () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notifications/notification-inbox.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Bell, CheckCheck, Trash2 } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { IconButton } from \"../components/icon-button\";\nimport { List, ListItem } from \"../components/list\";\nimport { Skeleton } from \"../components/skeleton\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { SettingsCardTitle } from \"../settings/settings-heading\";\nimport { settle, useMounted } from \"../account/account-parts\";\nimport { describeFailure, FailureLine, useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationsLabels } from \"./notifications-labels\";\n\n/**\n * One inbox row as `GET /notifications/inbox` answers it (server-kit `InboxItem`, §3.7):\n * the title and body already in the recipient's language.\n */\nexport interface NotificationInboxItem<Id extends string | number = string | number> {\n id: Id;\n /** The type id, for the row's icon. */\n notification_type: string;\n title: string;\n body: string;\n /** A path in the app, an absolute URL, or `null` (nothing to open). */\n url: string | null;\n /** ISO date-time. */\n created_at: string;\n /** ISO date-time, or `null` while unread. */\n read_at: string | null;\n /** 0.46 (server-kit 0.16 `InboxItem.company`): the company the row is about, named on\n * the row — kastlan's inbox holds every company's rows. */\n company?: string | null;\n}\n\nexport interface NotificationInboxProps<Id extends string | number = string | number>\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The rows, newest first; `undefined` while the first page loads. */\n items: readonly NotificationInboxItem<Id>[] | undefined;\n /** Every unread row of the account (`unread_count`), not only this page's. */\n unreadCount: number;\n loading?: boolean;\n /** `POST /notifications/inbox/read {ids}` — these rows, or `null` for all. */\n onMarkRead: (ids: Id[] | null) => Promise<unknown> | void;\n /** `DELETE /notifications/inbox`. Asked first, in the header. */\n onClear: () => Promise<unknown> | void;\n /**\n * Open a row: the kit marks it read first, then calls this, and the app routes to\n * `item.url` (kastlan switches the acting company first). Every row is a button then.\n * Left out, a row with a `url` is a real link — an in-app path through the provider's\n * router link, an absolute URL in a new tab — that marks itself read on a click or a\n * middle click (keksdose #451), and a row without one marks itself read.\n */\n onOpen?: (item: NotificationInboxItem<Id>) => void;\n /** The row's glyph for a type — the app's map (keksdose's `TYPE_ICONS`). `undefined`,\n * or a type it does not know (a retired one), is the `Bell`. */\n icon?: (type: string) => LucideIcon | undefined;\n /** More rows than shown: a \"Show more\" row calls `onLoadMore` (the API pages by\n * `offset`). */\n hasMore?: boolean;\n onLoadMore?: () => void;\n /** The next page is loading. */\n loadingMore?: boolean;\n /** A row's time. Default: the provider's locale and `timeZone`, date and time. */\n formatDate?: (iso: string) => string;\n /** The heading. Default `labels.inboxTitle` (\"Inbox\"). */\n title?: ReactNode;\n /** The app's own words for a failed request, or `undefined` for the kit's `failed`. */\n describeError?: (error: unknown) => ReactNode | undefined;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/**\n * The in-app inbox (0.46, docs/notifications-harmonization.md §10.1), keksdose's list\n * lifted: newest first, an icon per type (`Bell` for one the app does not map), the\n * title and two lines of body, the time, and — kastlan — the company the row is about.\n * Unread rows are bold with a dot. Props only: the app keeps the data (poll the badge\n * every {@link INBOX_BADGE_INTERVAL}, invalidate on its worker's push message).\n *\n * - **Opening a row marks it read** (see `onOpen`).\n * - **\"Mark all as read\"** while anything is unread; **\"Clear all\"** asks first, in the\n * header — no `ConfirmProvider` needed — with focus on Cancel.\n * - **\"Show more\"** with `hasMore`; the list scrolls inside a box of at most\n * `min(24rem, 70vh)`, so it never pushes the settings below it off the page.\n *\n * Under a write lock the header's actions are `commit` controls exempt from billing.\n */\nexport function NotificationInbox<Id extends string | number = string | number>({\n items,\n unreadCount,\n loading,\n onMarkRead,\n onClear,\n onOpen,\n icon,\n hasMore = false,\n onLoadMore,\n loadingMore = false,\n formatDate: formatDateProp,\n title,\n describeError,\n labels: labelsProp,\n className,\n ...rest\n}: NotificationInboxProps<Id>) {\n const labels = useNotificationsLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { formatDate } = useKitFormat();\n const [asking, setAsking] = useState(false);\n const [busy, setBusy] = useState<\"read\" | \"clear\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n const headingId = useId();\n const cancelRef = useRef<HTMLButtonElement>(null);\n const clearRef = useRef<HTMLButtonElement>(null);\n const [returning, setReturning] = useState(false);\n useEffect(() => {\n if (asking) cancelRef.current?.focus();\n else if (returning) clearRef.current?.focus();\n }, [asking, returning]);\n\n const timeOf = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, \"dateTime\"));\n const iconOf = (type: string): LucideIcon => icon?.(type) ?? Bell;\n\n const run = (which: \"read\" | \"clear\", call: () => unknown, after?: () => void) => {\n setBusy(which);\n setFailure(null);\n settle(call).then(\n () => {\n if (!mounted.current) return;\n setBusy(null);\n after?.();\n },\n (error: unknown) => {\n if (!mounted.current) return;\n setBusy(null);\n setFailure(describeFailure(describeError?.(error), labels.failed));\n },\n );\n };\n\n /** Mark one row read; a failure is said under the list, the row stays as it is. */\n const markOne = (item: NotificationInboxItem<Id>) => {\n if (item.read_at !== null) return;\n settle(() => onMarkRead([item.id])).catch((error: unknown) => {\n if (mounted.current) setFailure(describeFailure(describeError?.(error), labels.failed));\n });\n };\n\n const leave = () => {\n setReturning(true);\n setAsking(false);\n };\n const escapeLeaves = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n leave();\n }\n };\n\n const count = items?.length ?? 0;\n\n const header = asking ? (\n <div className=\"space-y-2 border-b border-subtle px-3 py-2\" data-inbox-confirm=\"\">\n <p role=\"alert\" className=\"text-sm\">\n {labels.clearPrompt}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"clear\"}\n onClick={() => run(\"clear\", onClear, () => setAsking(false))}\n onKeyDown={escapeLeaves}\n >\n {labels.clearAll}\n </Button>\n <Button ref={cancelRef} type=\"button\" size=\"sm\" variant=\"ghost\" onClick={leave} onKeyDown={escapeLeaves}>\n {common.cancel}\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 border-b border-subtle px-3 py-2\">\n <SettingsCardTitle id={headingId}>{title ?? labels.inboxTitle}</SettingsCardTitle>\n <div className=\"flex items-center gap-0.5\">\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit={COMMIT_EXCEPT_BILLING}\n onClick={() => run(\"read\", () => onMarkRead(null))}\n disabled={unreadCount === 0 || busy !== null}\n label={labels.markAllRead}\n >\n <CheckCheck />\n </IconButton>\n <IconButton\n ref={clearRef}\n type=\"button\"\n size=\"xs\"\n tone=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n onClick={() => {\n setReturning(false);\n setAsking(true);\n }}\n disabled={count === 0 || busy !== null}\n label={labels.clearAll}\n >\n <Trash2 />\n </IconButton>\n </div>\n </div>\n );\n\n let body: ReactNode;\n if (loading || items === undefined) {\n body = <Skeleton lines={3} label={labels.inboxLoading} className=\"p-3\" />;\n } else if (items.length === 0) {\n body = <EmptyState variant=\"inline\" title={labels.inboxEmpty} className=\"px-3 py-2\" />;\n } else {\n body = (\n // Named by the heading — except while \"Clear all\" asks, when the heading is away.\n <List className=\"p-1\" aria-labelledby={asking ? undefined : headingId} aria-label={asking ? labels.inboxTitle : undefined}>\n {items.map((item) => {\n const row = {\n title: item.title,\n subtitle: item.body,\n subtitleLines: 2 as const,\n overline: item.company ?? undefined,\n meta: timeOf(item.created_at),\n icon: iconOf(item.notification_type),\n unread: item.read_at === null,\n align: \"start\" as const,\n };\n if (onOpen) {\n return (\n <ListItem\n key={item.id}\n {...row}\n data-inbox-item={String(item.id)}\n onClick={() => {\n markOne(item);\n onOpen(item);\n }}\n />\n );\n }\n if (!item.url) {\n return <ListItem key={item.id} {...row} data-inbox-item={String(item.id)} onClick={() => markOne(item)} />;\n }\n // A real link, so a middle click opens a background tab; a middle click fires no\n // `click`, hence `onAuxClick` (keksdose #451).\n return (\n <ListItem\n key={item.id}\n {...row}\n data-inbox-item={String(item.id)}\n href={item.url}\n external={!item.url.startsWith(\"/\")}\n onClick={() => markOne(item)}\n onAuxClick={(event) => {\n if (event.button === 1) markOne(item);\n }}\n />\n );\n })}\n {hasMore && onLoadMore && (\n <li className=\"px-2 py-1\">\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" pending={loadingMore} onClick={onLoadMore} className=\"w-full\">\n {labels.showMore}\n </Button>\n </li>\n )}\n </List>\n );\n }\n\n return (\n <Card {...rest} className={cn(\"overflow-hidden p-0\", className)}>\n {header}\n <div className=\"max-h-[min(24rem,70vh)] overflow-y-auto overscroll-contain\">{body}</div>\n {failure !== null && (\n <div className=\"px-3 pb-3\">\n <FailureLine>{failure}</FailureLine>\n </div>\n )}\n </Card>\n );\n}\n\n/** How often an app polls the unread count for the badge (keksdose's 60 s, `limit:1`). */\nexport const INBOX_BADGE_INTERVAL = 60_000;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/notifications/notification-inbox.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Bell, CheckCheck, Trash2 } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { IconButton } from \"../components/icon-button\";\nimport { List, ListItem } from \"../components/list\";\nimport { Skeleton } from \"../components/skeleton\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { SettingsCardTitle } from \"../settings/settings-heading\";\nimport { settle, useMounted } from \"../account/account-parts\";\nimport { describeFailure, FailureLine, useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationsLabels } from \"./notifications-labels\";\n\n/**\n * One inbox row as `GET /notifications/inbox` answers it (server-kit `InboxItem`, §3.7):\n * the title and body already in the recipient's language.\n */\nexport interface NotificationInboxItem<Id extends string | number = string | number> {\n id: Id;\n /** The type id, for the row's icon. */\n notification_type: string;\n title: string;\n body: string;\n /** A path in the app, an absolute URL, or `null` (nothing to open). */\n url: string | null;\n /** ISO date-time. */\n created_at: string;\n /** ISO date-time, or `null` while unread. */\n read_at: string | null;\n /** 0.46 (server-kit 0.16 `InboxItem.company`): the company the row is about, named on\n * the row — kastlan's inbox holds every company's rows. */\n company?: string | null;\n /** 0.46.1 (server-kit 0.16.1 `InboxItem.company_id`): the same company by its id, so\n * `onOpen` switches to it exactly (two companies may share a name). */\n company_id?: number | string | null;\n}\n\n/** The inbox request that failed, as `describeError` gets it (0.46.1): one row marked read,\n * \"Mark all as read\", or \"Clear all\". */\nexport type NotificationInboxAction = \"read\" | \"readAll\" | \"clear\";\n\nexport interface NotificationInboxProps<Id extends string | number = string | number>\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The rows, newest first; `undefined` while the first page loads. */\n items: readonly NotificationInboxItem<Id>[] | undefined;\n /** Every unread row of the account (`unread_count`), not only this page's. */\n unreadCount: number;\n loading?: boolean;\n /** `POST /notifications/inbox/read {ids}` — these rows, or `null` for all. */\n onMarkRead: (ids: Id[] | null) => Promise<unknown> | void;\n /** `DELETE /notifications/inbox`. Asked first, in the header. */\n onClear: () => Promise<unknown> | void;\n /**\n * Open a row: the kit marks it read first, then calls this, and the app routes to\n * `item.url` (kastlan switches the acting company, `item.company_id`, first). Every row is a button then.\n * Left out, a row with a `url` is a real link — an in-app path through the provider's\n * router link, an absolute URL in a new tab — that marks itself read on a click or a\n * middle click (keksdose #451), and a row without one marks itself read.\n */\n onOpen?: (item: NotificationInboxItem<Id>) => void;\n /** The row's glyph for a type — the app's map (keksdose's `TYPE_ICONS`). `undefined`,\n * or a type it does not know (a retired one), is the `Bell`. */\n icon?: (type: string) => LucideIcon | undefined;\n /** More rows than shown: a \"Show more\" row calls `onLoadMore` (the API pages by\n * `offset`). */\n hasMore?: boolean;\n onLoadMore?: () => void;\n /** The next page is loading. */\n loadingMore?: boolean;\n /** A row's time. Default: the provider's locale and `timeZone`, date and time. */\n formatDate?: (iso: string) => string;\n /** The heading. Default `labels.inboxTitle` (\"Inbox\"). */\n title?: ReactNode;\n /** The app's own words for a failed request, or `undefined` for the kit's `failed`.\n * `action` (0.46.1) says which request failed: `\"read\"` (one row), `\"readAll\"` (\"Mark\n * all as read\") or `\"clear\"` (\"Clear all\") — so a failed mark-read is not worded as a\n * failed clear. */\n describeError?: (error: unknown, action: NotificationInboxAction) => ReactNode | undefined;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/**\n * The in-app inbox (0.46, docs/notifications-harmonization.md §10.1), keksdose's list\n * lifted: newest first, an icon per type (`Bell` for one the app does not map), the\n * title and two lines of body, the time, and — kastlan — the company the row is about.\n * Unread rows are bold with a dot. Props only: the app keeps the data (poll the badge\n * every {@link INBOX_BADGE_INTERVAL}, invalidate on its worker's push message).\n *\n * - **Opening a row marks it read** (see `onOpen`).\n * - **\"Mark all as read\"** while anything is unread; **\"Clear all\"** asks first, in the\n * header — no `ConfirmProvider` needed — with focus on Cancel.\n * - **\"Show more\"** with `hasMore`; the list scrolls inside a box of at most\n * `min(24rem, 70vh)`, so it never pushes the settings below it off the page.\n *\n * Under a write lock the header's actions are `commit` controls exempt from billing.\n */\nexport function NotificationInbox<Id extends string | number = string | number>({\n items,\n unreadCount,\n loading,\n onMarkRead,\n onClear,\n onOpen,\n icon,\n hasMore = false,\n onLoadMore,\n loadingMore = false,\n formatDate: formatDateProp,\n title,\n describeError,\n labels: labelsProp,\n className,\n ...rest\n}: NotificationInboxProps<Id>) {\n const labels = useNotificationsLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { formatDate } = useKitFormat();\n const [asking, setAsking] = useState(false);\n const [busy, setBusy] = useState<\"read\" | \"clear\" | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n const mounted = useMounted();\n const headingId = useId();\n const cancelRef = useRef<HTMLButtonElement>(null);\n const clearRef = useRef<HTMLButtonElement>(null);\n const [returning, setReturning] = useState(false);\n useEffect(() => {\n if (asking) cancelRef.current?.focus();\n else if (returning) clearRef.current?.focus();\n }, [asking, returning]);\n\n const timeOf = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, \"dateTime\"));\n const iconOf = (type: string): LucideIcon => icon?.(type) ?? Bell;\n\n const run = (which: \"read\" | \"clear\", call: () => unknown, after?: () => void) => {\n setBusy(which);\n setFailure(null);\n settle(call).then(\n () => {\n if (!mounted.current) return;\n setBusy(null);\n after?.();\n },\n (error: unknown) => {\n if (!mounted.current) return;\n setBusy(null);\n setFailure(describeFailure(describeError?.(error, which === \"read\" ? \"readAll\" : \"clear\"), labels.failed));\n },\n );\n };\n\n /** Mark one row read; a failure is said under the list, the row stays as it is. */\n const markOne = (item: NotificationInboxItem<Id>) => {\n if (item.read_at !== null) return;\n settle(() => onMarkRead([item.id])).catch((error: unknown) => {\n if (mounted.current) setFailure(describeFailure(describeError?.(error, \"read\"), labels.failed));\n });\n };\n\n const leave = () => {\n setReturning(true);\n setAsking(false);\n };\n const escapeLeaves = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n event.stopPropagation();\n leave();\n }\n };\n\n const count = items?.length ?? 0;\n\n const header = asking ? (\n <div className=\"space-y-2 border-b border-subtle px-3 py-2\" data-inbox-confirm=\"\">\n <p role=\"alert\" className=\"text-sm\">\n {labels.clearPrompt}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={busy === \"clear\"}\n onClick={() => run(\"clear\", onClear, () => setAsking(false))}\n onKeyDown={escapeLeaves}\n >\n {labels.clearAll}\n </Button>\n <Button ref={cancelRef} type=\"button\" size=\"sm\" variant=\"ghost\" onClick={leave} onKeyDown={escapeLeaves}>\n {common.cancel}\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 border-b border-subtle px-3 py-2\">\n <SettingsCardTitle id={headingId}>{title ?? labels.inboxTitle}</SettingsCardTitle>\n <div className=\"flex items-center gap-0.5\">\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit={COMMIT_EXCEPT_BILLING}\n onClick={() => run(\"read\", () => onMarkRead(null))}\n disabled={unreadCount === 0 || busy !== null}\n label={labels.markAllRead}\n >\n <CheckCheck />\n </IconButton>\n <IconButton\n ref={clearRef}\n type=\"button\"\n size=\"xs\"\n tone=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n onClick={() => {\n setReturning(false);\n setAsking(true);\n }}\n disabled={count === 0 || busy !== null}\n label={labels.clearAll}\n >\n <Trash2 />\n </IconButton>\n </div>\n </div>\n );\n\n let body: ReactNode;\n if (loading || items === undefined) {\n body = <Skeleton lines={3} label={labels.inboxLoading} className=\"p-3\" />;\n } else if (items.length === 0) {\n body = <EmptyState variant=\"inline\" title={labels.inboxEmpty} className=\"px-3 py-2\" />;\n } else {\n body = (\n // Named by the heading — except while \"Clear all\" asks, when the heading is away.\n <List className=\"p-1\" aria-labelledby={asking ? undefined : headingId} aria-label={asking ? labels.inboxTitle : undefined}>\n {items.map((item) => {\n const row = {\n title: item.title,\n subtitle: item.body,\n subtitleLines: 2 as const,\n overline: item.company ?? undefined,\n meta: timeOf(item.created_at),\n icon: iconOf(item.notification_type),\n unread: item.read_at === null,\n align: \"start\" as const,\n };\n if (onOpen) {\n return (\n <ListItem\n key={item.id}\n {...row}\n data-inbox-item={String(item.id)}\n onClick={() => {\n markOne(item);\n onOpen(item);\n }}\n />\n );\n }\n if (!item.url) {\n return <ListItem key={item.id} {...row} data-inbox-item={String(item.id)} onClick={() => markOne(item)} />;\n }\n // A real link, so a middle click opens a background tab; a middle click fires no\n // `click`, hence `onAuxClick` (keksdose #451).\n return (\n <ListItem\n key={item.id}\n {...row}\n data-inbox-item={String(item.id)}\n href={item.url}\n external={!item.url.startsWith(\"/\")}\n onClick={() => markOne(item)}\n onAuxClick={(event) => {\n if (event.button === 1) markOne(item);\n }}\n />\n );\n })}\n {hasMore && onLoadMore && (\n <li className=\"px-2 py-1\">\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" pending={loadingMore} onClick={onLoadMore} className=\"w-full\">\n {labels.showMore}\n </Button>\n </li>\n )}\n </List>\n );\n }\n\n return (\n <Card {...rest} className={cn(\"overflow-hidden p-0\", className)}>\n {header}\n <div className=\"max-h-[min(24rem,70vh)] overflow-y-auto overscroll-contain\">{body}</div>\n {failure !== null && (\n <div className=\"px-3 pb-3\">\n <FailureLine>{failure}</FailureLine>\n </div>\n )}\n </Card>\n );\n}\n\n/** How often an app polls the unread count for the badge (keksdose's 60 s, `limit:1`). */\nexport const INBOX_BADGE_INTERVAL = 60_000;\n"],"mappings":"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","names":[]}
@@ -55,7 +55,9 @@ export interface NotificationsLabels {
55
55
  quietUntil: string;
56
56
  /** One time set, the other not: quiet hours fail open until both are. */
57
57
  quietIncomplete: string;
58
- /** The zone picker's label. */
58
+ /** The zone picker's label.
59
+ * @deprecated 0.46 — the zone is automatic (§10.3): only the deprecated `TimeZoneSelect` reads it, the
60
+ * 0.46 card does not; removed in 0.47. Kept so an app's override still type-checks. */
59
61
  timeZone: string;
60
62
  /** @deprecated 0.46 — the zone is automatic (§10.3), the card offers no device zone; removed in 0.47.
61
63
  * Unused, kept so an app's override still type-checks. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notifications/notifications-labels.ts"],"sourcesContent":["// The words of the notification cards (docs/notifications-harmonization.md §4.2), in a\n// module with no component code: the English catalogue (src/i18n/defaults.ts) imports\n// them without the components, so `en` loads as one chunk like the other languages\n// (0.34, KS U8). The cards read it through `useKitLabels(\"notifications\", …)`.\n\n/** The ten IANA regions `Intl` lists zones under — keksdose's `tz_region_*` — in the\n * kit's words. A region a runtime adds later keeps its IANA name. */\nexport interface NotificationRegionLabels {\n Africa: string;\n America: string;\n Antarctica: string;\n Arctic: string;\n Asia: string;\n Atlantic: string;\n Australia: string;\n Europe: string;\n Indian: string;\n Pacific: string;\n}\n\n/**\n * Every string the notification parts render — the `notifications` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`: the types card,\n * quiet hours and the zone picker, this device's push, the device list, and the\n * unsubscribe page. Cancel is `common.cancel`.\n */\nexport interface NotificationsLabels {\n /* ── The types card ─────────────────────────────────────────────────── */\n /** The types card's title. */\n typesTitle: string;\n /** The line under it. */\n typesDescription: string;\n /** @deprecated 0.46 — the card's channels are columns now (`channelEmail`); removed in 0.47. Unused,\n * kept so an app's override still type-checks. */\n byEmail: string;\n /** 0.46 (§10.2): the channel columns' headings, and each switch's name. */\n channelApp: string;\n channelPush: string;\n channelEmail: string;\n /** Under every e-mail switch while the account's address is unverified (decision 16):\n * no notification mail goes to an unverified address. */\n emailUnverified: string;\n /** The digest's cadence select. */\n digestCadence: string;\n digestWeekly: string;\n digestMonthly: string;\n /** While the settings load. */\n typesLoading: string;\n /** The server answered no types for this account. */\n typesEmpty: string;\n\n /* ── Quiet hours ────────────────────────────────────────────────────── */\n quietTitle: string;\n /** The line under the title; `inbox`: the app keeps an inbox, which a held push still\n * reaches; `held` (0.46, §10.4): the app queues a held push and sends it when the\n * window ends, instead of dropping it. */\n quietDescription: (inbox: boolean, held?: boolean) => string;\n /** 0.46 (§10.3, automatic only): the card names the account's zone, read-only, given\n * the zone as `zoneName` writes it (\"Europe / Zurich\"); it follows the device last used. */\n quietZone: (zone: string) => string;\n /** The start time's label. */\n quietFrom: string;\n /** The end time's label. */\n quietUntil: string;\n /** One time set, the other not: quiet hours fail open until both are. */\n quietIncomplete: string;\n /** The zone picker's label. */\n timeZone: string;\n /** @deprecated 0.46 — the zone is automatic (§10.3), the card offers no device zone; removed in 0.47.\n * Unused, kept so an app's override still type-checks. */\n deviceZone: (zone: string) => string;\n /** @deprecated 0.46 — the zone is automatic (§10.3), nothing to take; removed in 0.47. Unused, kept\n * so an app's override still type-checks. */\n useDeviceZone: string;\n /** The zone's region headings, in the picker and in a zone's name (\"Europe / Zurich\"). */\n regions: NotificationRegionLabels;\n\n /* ── This device ────────────────────────────────────────────────────── */\n pushTitle: string;\n pushDescription: string;\n /** The browser has no push. */\n unsupported: string;\n /** iOS outside the installed app, given the app's name. */\n iosInstall: (app: string) => string;\n /** The server has no VAPID keys. */\n notConfigured: string;\n /** The browser's permission is denied. */\n blocked: string;\n /** This device gets notifications for the signed-in account. */\n on: string;\n off: string;\n /** This browser's subscription belongs to another account. */\n otherAccount: string;\n enable: string;\n disable: string;\n sendTest: string;\n /** Turning on did not finish (the permission prompt dismissed, a refusal). */\n enableFailed: string;\n /** No service worker, or one that is not ready in 10 s. */\n noServiceWorker: string;\n /** The browser's push service did not answer in 15 s. */\n pushTimeout: string;\n /** Given how many devices the push service accepted the test for (≥ 1). */\n testSent: (count: number) => string;\n /** The test reached no device. */\n testNone: string;\n testFailed: string;\n /** The explainer before the browser's one permission prompt. */\n explainerTitle: string;\n explainerBody: (app: string) => string;\n explainerHint: string;\n explainerContinue: string;\n\n /* ── The device list ────────────────────────────────────────────────── */\n devicesTitle: string;\n devicesDescription: string;\n devicesLoading: string;\n devicesEmpty: string;\n /** A device whose user agent says nothing the kit recognises. */\n unknownDevice: string;\n /** The chip on this browser's row. */\n thisDevice: string;\n /** Given the registration date, formatted. */\n added: (date: string) => string;\n /** Given the last accepted delivery, formatted. */\n lastDelivery: (date: string) => string;\n noDelivery: string;\n /** Given the failed deliveries in a row (≥ 1). */\n failures: (count: number) => string;\n /** A row's button, visible text — and the confirm's answer. */\n remove: string;\n /** The same button's accessible name, given the device: it starts with `remove`. */\n removeItem: (device: string) => string;\n /** The question the row asks before it removes. */\n removePrompt: string;\n /** 0.46 (§10.2): the disclosure on a device's row listing the types with push on. */\n deviceTypes: string;\n /** The line inside it. */\n deviceTypesDescription: string;\n /** No type has push on for the account. */\n deviceTypesEmpty: string;\n\n /* ── The inbox and the bell (0.46, §10.1) ───────────────────────────── */\n /** The inbox's heading (keksdose's \"Inbox\"). */\n inboxTitle: string;\n inboxLoading: string;\n /** Nothing in the inbox. */\n inboxEmpty: string;\n markAllRead: string;\n /** The header's clear button, and the confirm's answer. */\n clearAll: string;\n /** The question asked before the inbox is cleared. */\n clearPrompt: string;\n /** The row that loads the next page. */\n showMore: string;\n /** The bell's name. */\n bellLabel: string;\n /** Read after the bell's name, given the unread count (≥ 1). */\n bellUnread: (count: number) => string;\n /** The bell panel's link to the app's inbox. */\n allNotifications: string;\n\n /* ── The unsubscribe page ───────────────────────────────────────────── */\n unsubscribeTitle: string;\n /** Given the type's name in the app's words. */\n unsubscribeAbout: (type: string) => string;\n /** No type name to give. */\n unsubscribeGeneric: string;\n stopThese: string;\n stopAll: (app: string) => string;\n stoppedThese: string;\n stoppedAll: (app: string) => string;\n /** Account mail is not a notification, and keeps coming. */\n accountMail: string;\n turnBackOn: string;\n openSettings: string;\n /** A token the server refused, or one that does not parse. */\n invalidLink: string;\n\n /* ── Every card ─────────────────────────────────────────────────────── */\n /** A request failed, and the app had no words of its own. */\n failed: string;\n}\n\nexport const DEFAULT_NOTIFICATIONS_LABELS: NotificationsLabels = {\n typesTitle: \"Notify me about\",\n typesDescription: \"Choose what you hear about, and how.\",\n byEmail: \"By e-mail\",\n channelApp: \"In the app\",\n channelPush: \"Push\",\n channelEmail: \"E-mail\",\n emailUnverified: \"Confirm your address to receive these.\",\n digestCadence: \"Frequency\",\n digestWeekly: \"Weekly\",\n digestMonthly: \"Monthly\",\n typesLoading: \"Loading notification settings…\",\n typesEmpty: \"There are no notifications for this account.\",\n\n quietTitle: \"Quiet hours\",\n quietDescription: (inbox, held) =>\n held\n ? inbox\n ? \"Push notifications wait during this window and arrive when it ends. They reach your inbox at once.\"\n : \"Push notifications wait during this window and arrive when it ends.\"\n : inbox\n ? \"No push notifications during this window. They still reach your inbox.\"\n : \"No push notifications during this window.\",\n quietZone: (zone) => `In your time zone (${zone}), which follows the device you last used.`,\n quietFrom: \"From\",\n quietUntil: \"Until\",\n quietIncomplete: \"Set both a start and an end time. Quiet hours take effect once both are set.\",\n timeZone: \"Time zone\",\n deviceZone: (zone) => `This device is in ${zone}.`,\n useDeviceZone: \"Use it\",\n regions: {\n Africa: \"Africa\",\n America: \"Americas\",\n Antarctica: \"Antarctica\",\n Arctic: \"Arctic\",\n Asia: \"Asia\",\n Atlantic: \"Atlantic\",\n Australia: \"Australia\",\n Europe: \"Europe\",\n Indian: \"Indian Ocean\",\n Pacific: \"Pacific\",\n },\n\n pushTitle: \"Push notifications\",\n pushDescription: \"Notifications on this device, also while the app is closed.\",\n unsupported: \"This browser doesn’t support push notifications.\",\n iosInstall: (app) =>\n `On iPhone or iPad, add ${app} to your Home Screen first: iOS delivers notifications only to the installed app.`,\n notConfigured: \"Push notifications aren’t set up on this server.\",\n blocked: \"Notifications are blocked in this browser’s settings.\",\n on: \"Notifications are on for this device.\",\n off: \"Notifications are off for this device.\",\n otherAccount:\n \"This browser gets notifications for another account. Turn them on here to get them for the account you’re signed in with.\",\n enable: \"Turn on notifications\",\n disable: \"Turn off\",\n sendTest: \"Send a test\",\n enableFailed: \"Couldn’t turn on notifications.\",\n noServiceWorker: \"Couldn’t reach the app’s service worker. Reload the page and try again.\",\n pushTimeout: \"The browser’s push service didn’t answer. Check your connection and try again.\",\n testSent: (count) => (count === 1 ? \"Test sent to 1 device\" : `Test sent to ${count} devices`),\n testNone: \"No device accepted the test. Turn notifications on for this device first.\",\n testFailed: \"Couldn’t send a test notification.\",\n explainerTitle: \"Turn on notifications?\",\n explainerBody: (app) =>\n `${app} sends a notification only for the kinds you choose, and you can turn them off at any time.`,\n explainerHint: \"Your browser asks for permission next.\",\n explainerContinue: \"Continue\",\n\n devicesTitle: \"Notification devices\",\n devicesDescription: \"Every device that gets push notifications for your account. Remove any you no longer use.\",\n devicesLoading: \"Loading devices…\",\n devicesEmpty: \"No device gets notifications yet.\",\n unknownDevice: \"Unknown device\",\n thisDevice: \"This device\",\n added: (date) => `Added ${date}`,\n lastDelivery: (date) => `Last delivery ${date}`,\n noDelivery: \"No delivery yet\",\n failures: (count) =>\n count === 1 ? \"1 failed delivery in a row\" : `${count} failed deliveries in a row`,\n remove: \"Remove\",\n removeItem: (device) => `Remove ${device}`,\n removePrompt: \"Stop sending notifications to this device?\",\n deviceTypes: \"Types on this device\",\n deviceTypesDescription: \"Turn a type off to mute it on this device only.\",\n deviceTypesEmpty: \"No type has push notifications turned on.\",\n\n inboxTitle: \"Inbox\",\n inboxLoading: \"Loading notifications…\",\n inboxEmpty: \"You’re all caught up.\",\n markAllRead: \"Mark all as read\",\n clearAll: \"Clear all\",\n clearPrompt: \"Delete all notifications from your inbox?\",\n showMore: \"Show more\",\n bellLabel: \"Notifications\",\n bellUnread: (count) => `${count} unread`,\n allNotifications: \"All notifications\",\n\n unsubscribeTitle: \"E-mail notifications\",\n unsubscribeAbout: (type) => `You get e-mails about: ${type}.`,\n unsubscribeGeneric: \"You get notification e-mails.\",\n stopThese: \"Stop these e-mails\",\n stopAll: (app) => `Stop all notification e-mails from ${app}`,\n stoppedThese: \"Done. You won’t get these e-mails any more.\",\n stoppedAll: (app) => `Done. You won’t get notification e-mails from ${app} any more.`,\n accountMail: \"E-mails about your account still come.\",\n turnBackOn: \"You can turn them back on at any time in Settings.\",\n openSettings: \"Open Settings\",\n invalidLink: \"This link doesn’t work any more. You can turn e-mails off in Settings instead.\",\n\n failed: \"That didn’t work. Please try again.\",\n};\n"],"mappings":";AAwLO,MAAM,+BAAoD;AAAA,EAC/D,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAAA,EACd,YAAY;AAAA,EAEZ,YAAY;AAAA,EACZ,kBAAkB,CAAC,OAAO,SACxB,OACI,QACE,uGACA,wEACF,QACE,2EACA;AAAA,EACR,WAAW,CAAC,SAAS,sBAAsB,IAAI;AAAA,EAC/C,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,YAAY,CAAC,SAAS,qBAAqB,IAAI;AAAA,EAC/C,eAAe;AAAA,EACf,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,UAAU;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AAAA,EAEA,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,YAAY,CAAC,QACX,0BAA0B,GAAG;AAAA,EAC/B,eAAe;AAAA,EACf,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,cACE;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,UAAU,CAAC,UAAW,UAAU,IAAI,0BAA0B,gBAAgB,KAAK;AAAA,EACnF,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe,CAAC,QACd,GAAG,GAAG;AAAA,EACR,eAAe;AAAA,EACf,mBAAmB;AAAA,EAEnB,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,OAAO,CAAC,SAAS,SAAS,IAAI;AAAA,EAC9B,cAAc,CAAC,SAAS,iBAAiB,IAAI;AAAA,EAC7C,YAAY;AAAA,EACZ,UAAU,CAAC,UACT,UAAU,IAAI,+BAA+B,GAAG,KAAK;AAAA,EACvD,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW,UAAU,MAAM;AAAA,EACxC,cAAc;AAAA,EACd,aAAa;AAAA,EACb,wBAAwB;AAAA,EACxB,kBAAkB;AAAA,EAElB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,WAAW;AAAA,EACX,YAAY,CAAC,UAAU,GAAG,KAAK;AAAA,EAC/B,kBAAkB;AAAA,EAElB,kBAAkB;AAAA,EAClB,kBAAkB,CAAC,SAAS,0BAA0B,IAAI;AAAA,EAC1D,oBAAoB;AAAA,EACpB,WAAW;AAAA,EACX,SAAS,CAAC,QAAQ,sCAAsC,GAAG;AAAA,EAC3D,cAAc;AAAA,EACd,YAAY,CAAC,QAAQ,sDAAiD,GAAG;AAAA,EACzE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,aAAa;AAAA,EAEb,QAAQ;AACV;","names":[]}
1
+ {"version":3,"sources":["../../src/notifications/notifications-labels.ts"],"sourcesContent":["// The words of the notification cards (docs/notifications-harmonization.md §4.2), in a\n// module with no component code: the English catalogue (src/i18n/defaults.ts) imports\n// them without the components, so `en` loads as one chunk like the other languages\n// (0.34, KS U8). The cards read it through `useKitLabels(\"notifications\", …)`.\n\n/** The ten IANA regions `Intl` lists zones under — keksdose's `tz_region_*` — in the\n * kit's words. A region a runtime adds later keeps its IANA name. */\nexport interface NotificationRegionLabels {\n Africa: string;\n America: string;\n Antarctica: string;\n Arctic: string;\n Asia: string;\n Atlantic: string;\n Australia: string;\n Europe: string;\n Indian: string;\n Pacific: string;\n}\n\n/**\n * Every string the notification parts render — the `notifications` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`: the types card,\n * quiet hours and the zone picker, this device's push, the device list, and the\n * unsubscribe page. Cancel is `common.cancel`.\n */\nexport interface NotificationsLabels {\n /* ── The types card ─────────────────────────────────────────────────── */\n /** The types card's title. */\n typesTitle: string;\n /** The line under it. */\n typesDescription: string;\n /** @deprecated 0.46 — the card's channels are columns now (`channelEmail`); removed in 0.47. Unused,\n * kept so an app's override still type-checks. */\n byEmail: string;\n /** 0.46 (§10.2): the channel columns' headings, and each switch's name. */\n channelApp: string;\n channelPush: string;\n channelEmail: string;\n /** Under every e-mail switch while the account's address is unverified (decision 16):\n * no notification mail goes to an unverified address. */\n emailUnverified: string;\n /** The digest's cadence select. */\n digestCadence: string;\n digestWeekly: string;\n digestMonthly: string;\n /** While the settings load. */\n typesLoading: string;\n /** The server answered no types for this account. */\n typesEmpty: string;\n\n /* ── Quiet hours ────────────────────────────────────────────────────── */\n quietTitle: string;\n /** The line under the title; `inbox`: the app keeps an inbox, which a held push still\n * reaches; `held` (0.46, §10.4): the app queues a held push and sends it when the\n * window ends, instead of dropping it. */\n quietDescription: (inbox: boolean, held?: boolean) => string;\n /** 0.46 (§10.3, automatic only): the card names the account's zone, read-only, given\n * the zone as `zoneName` writes it (\"Europe / Zurich\"); it follows the device last used. */\n quietZone: (zone: string) => string;\n /** The start time's label. */\n quietFrom: string;\n /** The end time's label. */\n quietUntil: string;\n /** One time set, the other not: quiet hours fail open until both are. */\n quietIncomplete: string;\n /** The zone picker's label.\n * @deprecated 0.46 — the zone is automatic (§10.3): only the deprecated `TimeZoneSelect` reads it, the\n * 0.46 card does not; removed in 0.47. Kept so an app's override still type-checks. */\n timeZone: string;\n /** @deprecated 0.46 — the zone is automatic (§10.3), the card offers no device zone; removed in 0.47.\n * Unused, kept so an app's override still type-checks. */\n deviceZone: (zone: string) => string;\n /** @deprecated 0.46 — the zone is automatic (§10.3), nothing to take; removed in 0.47. Unused, kept\n * so an app's override still type-checks. */\n useDeviceZone: string;\n /** The zone's region headings, in the picker and in a zone's name (\"Europe / Zurich\"). */\n regions: NotificationRegionLabels;\n\n /* ── This device ────────────────────────────────────────────────────── */\n pushTitle: string;\n pushDescription: string;\n /** The browser has no push. */\n unsupported: string;\n /** iOS outside the installed app, given the app's name. */\n iosInstall: (app: string) => string;\n /** The server has no VAPID keys. */\n notConfigured: string;\n /** The browser's permission is denied. */\n blocked: string;\n /** This device gets notifications for the signed-in account. */\n on: string;\n off: string;\n /** This browser's subscription belongs to another account. */\n otherAccount: string;\n enable: string;\n disable: string;\n sendTest: string;\n /** Turning on did not finish (the permission prompt dismissed, a refusal). */\n enableFailed: string;\n /** No service worker, or one that is not ready in 10 s. */\n noServiceWorker: string;\n /** The browser's push service did not answer in 15 s. */\n pushTimeout: string;\n /** Given how many devices the push service accepted the test for (≥ 1). */\n testSent: (count: number) => string;\n /** The test reached no device. */\n testNone: string;\n testFailed: string;\n /** The explainer before the browser's one permission prompt. */\n explainerTitle: string;\n explainerBody: (app: string) => string;\n explainerHint: string;\n explainerContinue: string;\n\n /* ── The device list ────────────────────────────────────────────────── */\n devicesTitle: string;\n devicesDescription: string;\n devicesLoading: string;\n devicesEmpty: string;\n /** A device whose user agent says nothing the kit recognises. */\n unknownDevice: string;\n /** The chip on this browser's row. */\n thisDevice: string;\n /** Given the registration date, formatted. */\n added: (date: string) => string;\n /** Given the last accepted delivery, formatted. */\n lastDelivery: (date: string) => string;\n noDelivery: string;\n /** Given the failed deliveries in a row (≥ 1). */\n failures: (count: number) => string;\n /** A row's button, visible text — and the confirm's answer. */\n remove: string;\n /** The same button's accessible name, given the device: it starts with `remove`. */\n removeItem: (device: string) => string;\n /** The question the row asks before it removes. */\n removePrompt: string;\n /** 0.46 (§10.2): the disclosure on a device's row listing the types with push on. */\n deviceTypes: string;\n /** The line inside it. */\n deviceTypesDescription: string;\n /** No type has push on for the account. */\n deviceTypesEmpty: string;\n\n /* ── The inbox and the bell (0.46, §10.1) ───────────────────────────── */\n /** The inbox's heading (keksdose's \"Inbox\"). */\n inboxTitle: string;\n inboxLoading: string;\n /** Nothing in the inbox. */\n inboxEmpty: string;\n markAllRead: string;\n /** The header's clear button, and the confirm's answer. */\n clearAll: string;\n /** The question asked before the inbox is cleared. */\n clearPrompt: string;\n /** The row that loads the next page. */\n showMore: string;\n /** The bell's name. */\n bellLabel: string;\n /** Read after the bell's name, given the unread count (≥ 1). */\n bellUnread: (count: number) => string;\n /** The bell panel's link to the app's inbox. */\n allNotifications: string;\n\n /* ── The unsubscribe page ───────────────────────────────────────────── */\n unsubscribeTitle: string;\n /** Given the type's name in the app's words. */\n unsubscribeAbout: (type: string) => string;\n /** No type name to give. */\n unsubscribeGeneric: string;\n stopThese: string;\n stopAll: (app: string) => string;\n stoppedThese: string;\n stoppedAll: (app: string) => string;\n /** Account mail is not a notification, and keeps coming. */\n accountMail: string;\n turnBackOn: string;\n openSettings: string;\n /** A token the server refused, or one that does not parse. */\n invalidLink: string;\n\n /* ── Every card ─────────────────────────────────────────────────────── */\n /** A request failed, and the app had no words of its own. */\n failed: string;\n}\n\nexport const DEFAULT_NOTIFICATIONS_LABELS: NotificationsLabels = {\n typesTitle: \"Notify me about\",\n typesDescription: \"Choose what you hear about, and how.\",\n byEmail: \"By e-mail\",\n channelApp: \"In the app\",\n channelPush: \"Push\",\n channelEmail: \"E-mail\",\n emailUnverified: \"Confirm your address to receive these.\",\n digestCadence: \"Frequency\",\n digestWeekly: \"Weekly\",\n digestMonthly: \"Monthly\",\n typesLoading: \"Loading notification settings…\",\n typesEmpty: \"There are no notifications for this account.\",\n\n quietTitle: \"Quiet hours\",\n quietDescription: (inbox, held) =>\n held\n ? inbox\n ? \"Push notifications wait during this window and arrive when it ends. They reach your inbox at once.\"\n : \"Push notifications wait during this window and arrive when it ends.\"\n : inbox\n ? \"No push notifications during this window. They still reach your inbox.\"\n : \"No push notifications during this window.\",\n quietZone: (zone) => `In your time zone (${zone}), which follows the device you last used.`,\n quietFrom: \"From\",\n quietUntil: \"Until\",\n quietIncomplete: \"Set both a start and an end time. Quiet hours take effect once both are set.\",\n timeZone: \"Time zone\",\n deviceZone: (zone) => `This device is in ${zone}.`,\n useDeviceZone: \"Use it\",\n regions: {\n Africa: \"Africa\",\n America: \"Americas\",\n Antarctica: \"Antarctica\",\n Arctic: \"Arctic\",\n Asia: \"Asia\",\n Atlantic: \"Atlantic\",\n Australia: \"Australia\",\n Europe: \"Europe\",\n Indian: \"Indian Ocean\",\n Pacific: \"Pacific\",\n },\n\n pushTitle: \"Push notifications\",\n pushDescription: \"Notifications on this device, also while the app is closed.\",\n unsupported: \"This browser doesn’t support push notifications.\",\n iosInstall: (app) =>\n `On iPhone or iPad, add ${app} to your Home Screen first: iOS delivers notifications only to the installed app.`,\n notConfigured: \"Push notifications aren’t set up on this server.\",\n blocked: \"Notifications are blocked in this browser’s settings.\",\n on: \"Notifications are on for this device.\",\n off: \"Notifications are off for this device.\",\n otherAccount:\n \"This browser gets notifications for another account. Turn them on here to get them for the account you’re signed in with.\",\n enable: \"Turn on notifications\",\n disable: \"Turn off\",\n sendTest: \"Send a test\",\n enableFailed: \"Couldn’t turn on notifications.\",\n noServiceWorker: \"Couldn’t reach the app’s service worker. Reload the page and try again.\",\n pushTimeout: \"The browser’s push service didn’t answer. Check your connection and try again.\",\n testSent: (count) => (count === 1 ? \"Test sent to 1 device\" : `Test sent to ${count} devices`),\n testNone: \"No device accepted the test. Turn notifications on for this device first.\",\n testFailed: \"Couldn’t send a test notification.\",\n explainerTitle: \"Turn on notifications?\",\n explainerBody: (app) =>\n `${app} sends a notification only for the kinds you choose, and you can turn them off at any time.`,\n explainerHint: \"Your browser asks for permission next.\",\n explainerContinue: \"Continue\",\n\n devicesTitle: \"Notification devices\",\n devicesDescription: \"Every device that gets push notifications for your account. Remove any you no longer use.\",\n devicesLoading: \"Loading devices…\",\n devicesEmpty: \"No device gets notifications yet.\",\n unknownDevice: \"Unknown device\",\n thisDevice: \"This device\",\n added: (date) => `Added ${date}`,\n lastDelivery: (date) => `Last delivery ${date}`,\n noDelivery: \"No delivery yet\",\n failures: (count) =>\n count === 1 ? \"1 failed delivery in a row\" : `${count} failed deliveries in a row`,\n remove: \"Remove\",\n removeItem: (device) => `Remove ${device}`,\n removePrompt: \"Stop sending notifications to this device?\",\n deviceTypes: \"Types on this device\",\n deviceTypesDescription: \"Turn a type off to mute it on this device only.\",\n deviceTypesEmpty: \"No type has push notifications turned on.\",\n\n inboxTitle: \"Inbox\",\n inboxLoading: \"Loading notifications…\",\n inboxEmpty: \"You’re all caught up.\",\n markAllRead: \"Mark all as read\",\n clearAll: \"Clear all\",\n clearPrompt: \"Delete all notifications from your inbox?\",\n showMore: \"Show more\",\n bellLabel: \"Notifications\",\n bellUnread: (count) => `${count} unread`,\n allNotifications: \"All notifications\",\n\n unsubscribeTitle: \"E-mail notifications\",\n unsubscribeAbout: (type) => `You get e-mails about: ${type}.`,\n unsubscribeGeneric: \"You get notification e-mails.\",\n stopThese: \"Stop these e-mails\",\n stopAll: (app) => `Stop all notification e-mails from ${app}`,\n stoppedThese: \"Done. You won’t get these e-mails any more.\",\n stoppedAll: (app) => `Done. You won’t get notification e-mails from ${app} any more.`,\n accountMail: \"E-mails about your account still come.\",\n turnBackOn: \"You can turn them back on at any time in Settings.\",\n openSettings: \"Open Settings\",\n invalidLink: \"This link doesn’t work any more. You can turn e-mails off in Settings instead.\",\n\n failed: \"That didn’t work. Please try again.\",\n};\n"],"mappings":";AA0LO,MAAM,+BAAoD;AAAA,EAC/D,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAAA,EACd,YAAY;AAAA,EAEZ,YAAY;AAAA,EACZ,kBAAkB,CAAC,OAAO,SACxB,OACI,QACE,uGACA,wEACF,QACE,2EACA;AAAA,EACR,WAAW,CAAC,SAAS,sBAAsB,IAAI;AAAA,EAC/C,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,YAAY,CAAC,SAAS,qBAAqB,IAAI;AAAA,EAC/C,eAAe;AAAA,EACf,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,UAAU;AAAA,IACV,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AAAA,EAEA,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,YAAY,CAAC,QACX,0BAA0B,GAAG;AAAA,EAC/B,eAAe;AAAA,EACf,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,cACE;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,UAAU,CAAC,UAAW,UAAU,IAAI,0BAA0B,gBAAgB,KAAK;AAAA,EACnF,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe,CAAC,QACd,GAAG,GAAG;AAAA,EACR,eAAe;AAAA,EACf,mBAAmB;AAAA,EAEnB,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,OAAO,CAAC,SAAS,SAAS,IAAI;AAAA,EAC9B,cAAc,CAAC,SAAS,iBAAiB,IAAI;AAAA,EAC7C,YAAY;AAAA,EACZ,UAAU,CAAC,UACT,UAAU,IAAI,+BAA+B,GAAG,KAAK;AAAA,EACvD,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW,UAAU,MAAM;AAAA,EACxC,cAAc;AAAA,EACd,aAAa;AAAA,EACb,wBAAwB;AAAA,EACxB,kBAAkB;AAAA,EAElB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,WAAW;AAAA,EACX,YAAY,CAAC,UAAU,GAAG,KAAK;AAAA,EAC/B,kBAAkB;AAAA,EAElB,kBAAkB;AAAA,EAClB,kBAAkB,CAAC,SAAS,0BAA0B,IAAI;AAAA,EAC1D,oBAAoB;AAAA,EACpB,WAAW;AAAA,EACX,SAAS,CAAC,QAAQ,sCAAsC,GAAG;AAAA,EAC3D,cAAc;AAAA,EACd,YAAY,CAAC,QAAQ,sDAAiD,GAAG;AAAA,EACzE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,aAAa;AAAA,EAEb,QAAQ;AACV;","names":[]}
@@ -4,11 +4,12 @@ import type { ComboOption } from "../components/combobox-core.js";
4
4
  import type { CommitScope } from "../components/write-lock.js";
5
5
  import type { NotificationRegionLabels, NotificationsLabels } from "./notifications-labels.js";
6
6
  /**
7
- * This device's IANA zone ("Europe/Zurich"), or `"UTC"` where the runtime names none —
8
- * the zone `useDeviceZoneReport` reports, which becomes the account's (§10.3, automatic
9
- * only).
7
+ * This device's IANA zone — the same function as the package root's, which lives beside
8
+ * `useDeviceZoneReport` now (0.46.1).
9
+ * @deprecated 0.46 — import `deviceTimeZone` from `@eifi1/ui-kit` (notifications/device-zone-report); this
10
+ * module is removed in 0.47.
10
11
  */
11
- export declare function deviceTimeZone(): string;
12
+ export declare const deviceTimeZone: () => string;
12
13
  /**
13
14
  * The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`
14
15
  * lists, as its city ("Argentina / Buenos Aires") under its region, the region named by
@@ -2,14 +2,9 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { InlineEntityCombobox } from "../components/inline-entity-combobox.js";
5
+ import { deviceTimeZone as zoneOfDevice } from "./device-zone-report.js";
5
6
  import { useNotificationsLabels } from "./notification-parts.js";
6
- function deviceTimeZone() {
7
- try {
8
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
9
- } catch {
10
- return "UTC";
11
- }
12
- }
7
+ const deviceTimeZone = zoneOfDevice;
13
8
  function runtimeZones() {
14
9
  const intl = Intl;
15
10
  return typeof intl.supportedValuesOf === "function" ? intl.supportedValuesOf("timeZone") : [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notifications/time-zone-select.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport type { ComboOption } from \"../components/combobox-core\";\nimport { InlineEntityCombobox } from \"../components/inline-entity-combobox\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationRegionLabels, NotificationsLabels } from \"./notifications-labels\";\n\n/**\n * This device's IANA zone (\"Europe/Zurich\"), or `\"UTC\"` where the runtime names none —\n * the zone `useDeviceZoneReport` reports, which becomes the account's (§10.3, automatic\n * only).\n */\nexport function deviceTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Every zone the runtime lists; `[]` where `Intl.supportedValuesOf` is missing. */\nfunction runtimeZones(): string[] {\n const intl = Intl as typeof Intl & { supportedValuesOf?: (key: \"timeZone\") => string[] };\n return typeof intl.supportedValuesOf === \"function\" ? intl.supportedValuesOf(\"timeZone\") : [];\n}\n\n/** \"Argentina/Buenos_Aires\" → \"Argentina / Buenos Aires\": the zone's city, as a row. */\nfunction cityOf(rest: string): string {\n return rest.replace(/_/g, \" \").replace(/\\//g, \" / \");\n}\n\n/**\n * The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`\n * lists, as its city (\"Argentina / Buenos Aires\") under its region, the region named by\n * `regionLabel` — which answers `undefined` for a region it does not know, and the region\n * keeps its IANA name. A zone with no region (\"UTC\") comes first, ahead of every heading,\n * rather than trailing the last group as if it were Pacific. `current` is kept even when\n * the runtime does not list it (an old alias such as \"Europe/Kiev\"): a picker without its\n * own value would show an empty field.\n * @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47.\n */\nexport function timeZoneOptions(\n current: string,\n regionLabel: (region: string) => string | undefined,\n): ComboOption<string>[] {\n const zones = runtimeZones();\n const all = !current || zones.includes(current) ? zones : [...zones, current].sort();\n const rows = all.map((zone): ComboOption<string> => {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return { value: zone, label: zone };\n const region = zone.slice(0, slash);\n return { value: zone, label: cityOf(zone.slice(slash + 1)), group: regionLabel(region) ?? region };\n });\n return [...rows.filter((row) => !row.group), ...rows.filter((row) => row.group)];\n}\n\n/** The region's name in `regions`, or `undefined` for one the labels do not know.\n * @internal Not part of the barrel. */\nexport function regionName(regions: NotificationRegionLabels, region: string): string | undefined {\n return Object.prototype.hasOwnProperty.call(regions, region)\n ? regions[region as keyof NotificationRegionLabels]\n : undefined;\n}\n\n/** A zone as the picker shows it, for a sentence: \"Europe / Zurich\", the region in the\n * kit's words. @internal Not part of the barrel. */\nexport function zoneName(zone: string, regions: NotificationRegionLabels): string {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return zone;\n const region = zone.slice(0, slash);\n return `${regionName(regions, region) ?? region} / ${cityOf(zone.slice(slash + 1))}`;\n}\n\n/** @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47. */\nexport interface TimeZoneSelectProps {\n /** The IANA zone. Not clearable: a zone can be changed, never emptied. */\n value: string;\n /** The zone picked. Never called for the value it already has. */\n onChange: (zone: string) => void;\n /** Default `labels.timeZone`. */\n label?: ReactNode;\n disabled?: boolean;\n /** The input's id. */\n id?: string;\n className?: string;\n /** The picker SAVES on change (quiet hours do): locked under a write lock in scope. */\n commit?: CommitScope;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/**\n * An IANA time zone, searchable (docs/notifications-harmonization.md §4.2, keksdose's\n * quiet-hours picker): type \"Zur\" for Europe/Zurich, and on a phone the kit's sheet with\n * a search box instead of a 400-row wheel. Rows are the city under its region, the region\n * in the kit's words (`labels.regions`); a zone with no region first; a stored zone the\n * runtime does not list is kept. On the kit's {@link InlineEntityCombobox}.\n * @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47.\n */\nexport function TimeZoneSelect({\n value,\n onChange,\n label,\n disabled,\n id,\n className,\n commit,\n labels: labelsProp,\n}: TimeZoneSelectProps) {\n const labels = useNotificationsLabels(labelsProp);\n const { regions } = labels;\n const options = useMemo(() => timeZoneOptions(value, (region) => regionName(regions, region)), [value, regions]);\n return (\n <InlineEntityCombobox<string>\n id={id}\n label={label ?? labels.timeZone}\n value={value}\n options={options}\n disabled={disabled}\n commit={commit}\n onChange={(zone) => {\n if (zone && zone !== value) onChange(zone);\n }}\n className={className}\n />\n );\n}\n"],"mappings":";AAmHI;AAnHJ,SAAS,eAAe;AAKxB,SAAS,4BAA4B;AAErC,SAAS,8BAA8B;AAQhC,SAAS,iBAAyB;AACvC,MAAI;AACF,WAAO,KAAK,eAAe,EAAE,gBAAgB,EAAE,YAAY;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAyB;AAChC,QAAM,OAAO;AACb,SAAO,OAAO,KAAK,sBAAsB,aAAa,KAAK,kBAAkB,UAAU,IAAI,CAAC;AAC9F;AAGA,SAAS,OAAO,MAAsB;AACpC,SAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,OAAO,KAAK;AACrD;AAYO,SAAS,gBACd,SACA,aACuB;AACvB,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAM,CAAC,WAAW,MAAM,SAAS,OAAO,IAAI,QAAQ,CAAC,GAAG,OAAO,OAAO,EAAE,KAAK;AACnF,QAAM,OAAO,IAAI,IAAI,CAAC,SAA8B;AAClD,UAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,QAAI,QAAQ,EAAG,QAAO,EAAE,OAAO,MAAM,OAAO,KAAK;AACjD,UAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,WAAO,EAAE,OAAO,MAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,YAAY,MAAM,KAAK,OAAO;AAAA,EACnG,CAAC;AACD,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,GAAG,GAAG,KAAK,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;AACjF;AAIO,SAAS,WAAW,SAAmC,QAAoC;AAChG,SAAO,OAAO,UAAU,eAAe,KAAK,SAAS,MAAM,IACvD,QAAQ,MAAwC,IAChD;AACN;AAIO,SAAS,SAAS,MAAc,SAA2C;AAChF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,SAAO,GAAG,WAAW,SAAS,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AACpF;AA2BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAwB;AACtB,QAAM,SAAS,uBAAuB,UAAU;AAChD,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,UAAU,QAAQ,MAAM,gBAAgB,OAAO,CAAC,WAAW,WAAW,SAAS,MAAM,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC;AAC/G,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,SAAS,OAAO;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,SAAS;AAClB,YAAI,QAAQ,SAAS,MAAO,UAAS,IAAI;AAAA,MAC3C;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/notifications/time-zone-select.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport type { ComboOption } from \"../components/combobox-core\";\nimport { InlineEntityCombobox } from \"../components/inline-entity-combobox\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { deviceTimeZone as zoneOfDevice } from \"./device-zone-report\";\nimport { useNotificationsLabels } from \"./notification-parts\";\nimport type { NotificationRegionLabels, NotificationsLabels } from \"./notifications-labels\";\n\n/**\n * This device's IANA zone — the same function as the package root's, which lives beside\n * `useDeviceZoneReport` now (0.46.1).\n * @deprecated 0.46 — import `deviceTimeZone` from `@eifi1/ui-kit` (notifications/device-zone-report); this\n * module is removed in 0.47.\n */\nexport const deviceTimeZone: () => string = zoneOfDevice;\n\n/** Every zone the runtime lists; `[]` where `Intl.supportedValuesOf` is missing. */\nfunction runtimeZones(): string[] {\n const intl = Intl as typeof Intl & { supportedValuesOf?: (key: \"timeZone\") => string[] };\n return typeof intl.supportedValuesOf === \"function\" ? intl.supportedValuesOf(\"timeZone\") : [];\n}\n\n/** \"Argentina/Buenos_Aires\" → \"Argentina / Buenos Aires\": the zone's city, as a row. */\nfunction cityOf(rest: string): string {\n return rest.replace(/_/g, \" \").replace(/\\//g, \" / \");\n}\n\n/**\n * The zone picker's rows (keksdose `notifications-card.tsx:131-146`): every zone `Intl`\n * lists, as its city (\"Argentina / Buenos Aires\") under its region, the region named by\n * `regionLabel` — which answers `undefined` for a region it does not know, and the region\n * keeps its IANA name. A zone with no region (\"UTC\") comes first, ahead of every heading,\n * rather than trailing the last group as if it were Pacific. `current` is kept even when\n * the runtime does not list it (an old alias such as \"Europe/Kiev\"): a picker without its\n * own value would show an empty field.\n * @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47.\n */\nexport function timeZoneOptions(\n current: string,\n regionLabel: (region: string) => string | undefined,\n): ComboOption<string>[] {\n const zones = runtimeZones();\n const all = !current || zones.includes(current) ? zones : [...zones, current].sort();\n const rows = all.map((zone): ComboOption<string> => {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return { value: zone, label: zone };\n const region = zone.slice(0, slash);\n return { value: zone, label: cityOf(zone.slice(slash + 1)), group: regionLabel(region) ?? region };\n });\n return [...rows.filter((row) => !row.group), ...rows.filter((row) => row.group)];\n}\n\n/** The region's name in `regions`, or `undefined` for one the labels do not know.\n * @internal Not part of the barrel. */\nexport function regionName(regions: NotificationRegionLabels, region: string): string | undefined {\n return Object.prototype.hasOwnProperty.call(regions, region)\n ? regions[region as keyof NotificationRegionLabels]\n : undefined;\n}\n\n/** A zone as the picker shows it, for a sentence: \"Europe / Zurich\", the region in the\n * kit's words. @internal Not part of the barrel. */\nexport function zoneName(zone: string, regions: NotificationRegionLabels): string {\n const slash = zone.indexOf(\"/\");\n if (slash < 0) return zone;\n const region = zone.slice(0, slash);\n return `${regionName(regions, region) ?? region} / ${cityOf(zone.slice(slash + 1))}`;\n}\n\n/** @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47. */\nexport interface TimeZoneSelectProps {\n /** The IANA zone. Not clearable: a zone can be changed, never emptied. */\n value: string;\n /** The zone picked. Never called for the value it already has. */\n onChange: (zone: string) => void;\n /** Default `labels.timeZone`. */\n label?: ReactNode;\n disabled?: boolean;\n /** The input's id. */\n id?: string;\n className?: string;\n /** The picker SAVES on change (quiet hours do): locked under a write lock in scope. */\n commit?: CommitScope;\n labels?: LabelOverride<NotificationsLabels>;\n}\n\n/**\n * An IANA time zone, searchable (docs/notifications-harmonization.md §4.2, keksdose's\n * quiet-hours picker): type \"Zur\" for Europe/Zurich, and on a phone the kit's sheet with\n * a search box instead of a 400-row wheel. Rows are the city under its region, the region\n * in the kit's words (`labels.regions`); a zone with no region first; a stored zone the\n * runtime does not list is kept. On the kit's {@link InlineEntityCombobox}.\n * @deprecated 0.46 — the zone is automatic (§10.3, amended 2026-10-20), no app picks one; removed in 0.47.\n */\nexport function TimeZoneSelect({\n value,\n onChange,\n label,\n disabled,\n id,\n className,\n commit,\n labels: labelsProp,\n}: TimeZoneSelectProps) {\n const labels = useNotificationsLabels(labelsProp);\n const { regions } = labels;\n const options = useMemo(() => timeZoneOptions(value, (region) => regionName(regions, region)), [value, regions]);\n return (\n <InlineEntityCombobox<string>\n id={id}\n label={label ?? labels.timeZone}\n value={value}\n options={options}\n disabled={disabled}\n commit={commit}\n onChange={(zone) => {\n if (zone && zone !== value) onChange(zone);\n }}\n className={className}\n />\n );\n}\n"],"mappings":";AA+GI;AA/GJ,SAAS,eAAe;AAKxB,SAAS,4BAA4B;AAErC,SAAS,kBAAkB,oBAAoB;AAC/C,SAAS,8BAA8B;AAShC,MAAM,iBAA+B;AAG5C,SAAS,eAAyB;AAChC,QAAM,OAAO;AACb,SAAO,OAAO,KAAK,sBAAsB,aAAa,KAAK,kBAAkB,UAAU,IAAI,CAAC;AAC9F;AAGA,SAAS,OAAO,MAAsB;AACpC,SAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,OAAO,KAAK;AACrD;AAYO,SAAS,gBACd,SACA,aACuB;AACvB,QAAM,QAAQ,aAAa;AAC3B,QAAM,MAAM,CAAC,WAAW,MAAM,SAAS,OAAO,IAAI,QAAQ,CAAC,GAAG,OAAO,OAAO,EAAE,KAAK;AACnF,QAAM,OAAO,IAAI,IAAI,CAAC,SAA8B;AAClD,UAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,QAAI,QAAQ,EAAG,QAAO,EAAE,OAAO,MAAM,OAAO,KAAK;AACjD,UAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,WAAO,EAAE,OAAO,MAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,YAAY,MAAM,KAAK,OAAO;AAAA,EACnG,CAAC;AACD,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,GAAG,GAAG,KAAK,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;AACjF;AAIO,SAAS,WAAW,SAAmC,QAAoC;AAChG,SAAO,OAAO,UAAU,eAAe,KAAK,SAAS,MAAM,IACvD,QAAQ,MAAwC,IAChD;AACN;AAIO,SAAS,SAAS,MAAc,SAA2C;AAChF,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,MAAM,GAAG,KAAK;AAClC,SAAO,GAAG,WAAW,SAAS,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AACpF;AA2BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAwB;AACtB,QAAM,SAAS,uBAAuB,UAAU;AAChD,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,UAAU,QAAQ,MAAM,gBAAgB,OAAO,CAAC,WAAW,WAAW,SAAS,MAAM,CAAC,GAAG,CAAC,OAAO,OAAO,CAAC;AAC/G,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,SAAS,OAAO;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,SAAS;AAClB,YAAI,QAAQ,SAAS,MAAO,UAAS,IAAI;AAAA,MAC3C;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -8,6 +8,15 @@
8
8
  * enough although the server keeps microseconds: `Date.parse` truncates them.
9
9
  */
10
10
  export declare function stampAfter(previous: unknown): string;
11
+ /**
12
+ * A name-based UUID, version 5 (RFC 4122 §4.3: SHA-1 of the namespace's 16 bytes and the
13
+ * name's UTF-8), the same as Python's `uuid.uuid5(uuid.UUID(namespace), name)` — for an id
14
+ * the device and the server derive alike (§2: the per-owner vote id
15
+ * `uuid5(namespace, "<item>:<invitation>")`). Lower case, hyphenated. `namespace` is a UUID,
16
+ * hyphenated or not, in either case; anything else is a `TypeError`. Uses `crypto.subtle`
17
+ * (a secure context: https or localhost).
18
+ */
19
+ export declare function uuid5(namespace: string, name: string): Promise<string>;
11
20
  /**
12
21
  * The connection, not the server, failed: the request had no answer (an axios-like error
13
22
  * without `response`, or `fetch`'s `TypeError`), or the session refresh marked a 401 as
@@ -5,6 +5,19 @@ function stampAfter(previous) {
5
5
  const prior = typeof previous === "string" ? Date.parse(previous) : Number.NaN;
6
6
  return new Date(Number.isFinite(prior) && prior >= now ? prior + 1 : now).toISOString();
7
7
  }
8
+ async function uuid5(namespace, name) {
9
+ const hex = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i.test(namespace) ? namespace.replaceAll("-", "") : null;
10
+ if (!hex) throw new TypeError(`uuid5: the namespace must be a UUID, not ${JSON.stringify(namespace)}`);
11
+ const nameBytes = new TextEncoder().encode(name);
12
+ const input = new Uint8Array(16 + nameBytes.length);
13
+ for (let i = 0; i < 16; i += 1) input[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
14
+ input.set(nameBytes, 16);
15
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-1", input)).slice(0, 16);
16
+ bytes[6] = bytes[6] & 15 | 80;
17
+ bytes[8] = bytes[8] & 63 | 128;
18
+ const out = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
19
+ return `${out.slice(0, 8)}-${out.slice(8, 12)}-${out.slice(12, 16)}-${out.slice(16, 20)}-${out.slice(20)}`;
20
+ }
8
21
  function isOfflineError(err) {
9
22
  if (isTransientRefreshFailure(err)) return true;
10
23
  if (typeof err !== "object" || err === null) return false;
@@ -34,6 +47,7 @@ export {
34
47
  isOfflineError,
35
48
  readLocalFirst,
36
49
  stampAfter,
50
+ uuid5,
37
51
  writeLocalFirst
38
52
  };
39
53
  //# sourceMappingURL=local-first.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/offline/local-first.ts"],"sourcesContent":["import { isTransientRefreshFailure } from \"../lib/session-refresh\";\n\n/*\n * The leaf utilities of local-first (docs/local-first-harmonization.md §2.3, 0.46): what a\n * page and a sync engine both need, with no store behind them. One module that both\n * `@eifi1/ui-kit/offline` and `@eifi1/ui-kit/sync` export, so `/sync` never loads\n * `/offline`'s TanStack and idb-keyval peers, and `/offline` never loads Dexie. It imports\n * no package at all (packaging-contract.test.ts holds that).\n */\n\n/**\n * An `updated_at` that wins Last-Write-Wins over `previous`: now, or one millisecond past\n * `previous` when that is not behind this clock (keksdose's sync-client.ts:639-643).\n *\n * `/sync` keeps the server's row when it is STRICTLY newer, comparing this device's clock\n * with the server's. A row the server just wrote carries the server's time, so an edit\n * stamped by a clock a second slow would lose and come straight back. The millisecond is\n * enough although the server keeps microseconds: `Date.parse` truncates them.\n */\nexport function stampAfter(previous: unknown): string {\n const now = Date.now();\n const prior = typeof previous === \"string\" ? Date.parse(previous) : Number.NaN;\n return new Date(Number.isFinite(prior) && prior >= now ? prior + 1 : now).toISOString();\n}\n\n/**\n * The connection, not the server, failed: the request had no answer (an axios-like error\n * without `response`, or `fetch`'s `TypeError`), or the session refresh marked a 401 as\n * passing because the server was out of reach ({@link isTransientRefreshFailure}). A real\n * HTTP answer — a 4xx or a 5xx — is never offline: the caller must see it.\n */\nexport function isOfflineError(err: unknown): boolean {\n if (isTransientRefreshFailure(err)) return true;\n if (typeof err !== \"object\" || err === null) return false;\n const bag = err as { isAxiosError?: unknown; response?: unknown };\n if (bag.isAxiosError === true || \"response\" in bag) return bag.response == null;\n return err instanceof TypeError;\n}\n\n/**\n * Read from the server, and from the device's copy only when the connection failed\n * ({@link isOfflineError}); a 4xx or 5xx still throws. `onOnline` gets the server's answer\n * before it is returned (to refresh the copy, say); a throw from it counts as a failure of\n * the online path. The plain form of keksdose's net.ts:58, without its sealed-household\n * and cold-start branches (those stay keksdose's).\n */\nexport async function readLocalFirst<T>(\n online: () => Promise<T>,\n offline: () => Promise<T>,\n onOnline?: (result: T) => void | Promise<void>,\n): Promise<T> {\n try {\n const result = await online();\n await onOnline?.(result);\n return result;\n } catch (err) {\n if (!isOfflineError(err)) throw err;\n return offline();\n }\n}\n\n/**\n * Write through the server first (`rest`), and queue the change on the device (`queue`)\n * only when the connection failed; any other failure throws, nothing queued. Online writes\n * stay server-stamped (D2). A create mints its id BEFORE calling this and sends it on both\n * paths, so a REST create that timed out after the server applied it is not created twice\n * (D16).\n */\nexport async function writeLocalFirst<T>(rest: () => Promise<T>, queue: () => Promise<T>): Promise<T> {\n try {\n return await rest();\n } catch (err) {\n if (!isOfflineError(err)) throw err;\n return queue();\n }\n}\n"],"mappings":";AAAA,SAAS,iCAAiC;AAmBnC,SAAS,WAAW,UAA2B;AACpD,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,OAAO,aAAa,WAAW,KAAK,MAAM,QAAQ,IAAI,OAAO;AAC3E,SAAO,IAAI,KAAK,OAAO,SAAS,KAAK,KAAK,SAAS,MAAM,QAAQ,IAAI,GAAG,EAAE,YAAY;AACxF;AAQO,SAAS,eAAe,KAAuB;AACpD,MAAI,0BAA0B,GAAG,EAAG,QAAO;AAC3C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO;AACpD,QAAM,MAAM;AACZ,MAAI,IAAI,iBAAiB,QAAQ,cAAc,IAAK,QAAO,IAAI,YAAY;AAC3E,SAAO,eAAe;AACxB;AASA,eAAsB,eACpB,QACA,SACA,UACY;AACZ,MAAI;AACF,UAAM,SAAS,MAAM,OAAO;AAC5B,UAAM,WAAW,MAAM;AACvB,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,CAAC,eAAe,GAAG,EAAG,OAAM;AAChC,WAAO,QAAQ;AAAA,EACjB;AACF;AASA,eAAsB,gBAAmB,MAAwB,OAAqC;AACpG,MAAI;AACF,WAAO,MAAM,KAAK;AAAA,EACpB,SAAS,KAAK;AACZ,QAAI,CAAC,eAAe,GAAG,EAAG,OAAM;AAChC,WAAO,MAAM;AAAA,EACf;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/offline/local-first.ts"],"sourcesContent":["import { isTransientRefreshFailure } from \"../lib/session-refresh\";\n\n/*\n * The leaf utilities of local-first (docs/local-first-harmonization.md §2.3, 0.46): what a\n * page and a sync engine both need, with no store behind them. One module that both\n * `@eifi1/ui-kit/offline` and `@eifi1/ui-kit/sync` export, so `/sync` never loads\n * `/offline`'s TanStack and idb-keyval peers, and `/offline` never loads Dexie. It imports\n * no package at all (packaging-contract.test.ts holds that).\n */\n\n/**\n * An `updated_at` that wins Last-Write-Wins over `previous`: now, or one millisecond past\n * `previous` when that is not behind this clock (keksdose's sync-client.ts:639-643).\n *\n * `/sync` keeps the server's row when it is STRICTLY newer, comparing this device's clock\n * with the server's. A row the server just wrote carries the server's time, so an edit\n * stamped by a clock a second slow would lose and come straight back. The millisecond is\n * enough although the server keeps microseconds: `Date.parse` truncates them.\n */\nexport function stampAfter(previous: unknown): string {\n const now = Date.now();\n const prior = typeof previous === \"string\" ? Date.parse(previous) : Number.NaN;\n return new Date(Number.isFinite(prior) && prior >= now ? prior + 1 : now).toISOString();\n}\n\n/**\n * A name-based UUID, version 5 (RFC 4122 §4.3: SHA-1 of the namespace's 16 bytes and the\n * name's UTF-8), the same as Python's `uuid.uuid5(uuid.UUID(namespace), name)` — for an id\n * the device and the server derive alike (§2: the per-owner vote id\n * `uuid5(namespace, \"<item>:<invitation>\")`). Lower case, hyphenated. `namespace` is a UUID,\n * hyphenated or not, in either case; anything else is a `TypeError`. Uses `crypto.subtle`\n * (a secure context: https or localhost).\n */\nexport async function uuid5(namespace: string, name: string): Promise<string> {\n const hex = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i.test(namespace)\n ? namespace.replaceAll(\"-\", \"\")\n : null;\n if (!hex) throw new TypeError(`uuid5: the namespace must be a UUID, not ${JSON.stringify(namespace)}`);\n const nameBytes = new TextEncoder().encode(name);\n const input = new Uint8Array(16 + nameBytes.length);\n for (let i = 0; i < 16; i += 1) input[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n input.set(nameBytes, 16);\n const bytes = new Uint8Array(await crypto.subtle.digest(\"SHA-1\", input)).slice(0, 16);\n bytes[6] = (bytes[6] & 0x0f) | 0x50; // version 5\n bytes[8] = (bytes[8] & 0x3f) | 0x80; // the RFC 4122 variant\n const out = Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `${out.slice(0, 8)}-${out.slice(8, 12)}-${out.slice(12, 16)}-${out.slice(16, 20)}-${out.slice(20)}`;\n}\n\n/**\n * The connection, not the server, failed: the request had no answer (an axios-like error\n * without `response`, or `fetch`'s `TypeError`), or the session refresh marked a 401 as\n * passing because the server was out of reach ({@link isTransientRefreshFailure}). A real\n * HTTP answer — a 4xx or a 5xx — is never offline: the caller must see it.\n */\nexport function isOfflineError(err: unknown): boolean {\n if (isTransientRefreshFailure(err)) return true;\n if (typeof err !== \"object\" || err === null) return false;\n const bag = err as { isAxiosError?: unknown; response?: unknown };\n if (bag.isAxiosError === true || \"response\" in bag) return bag.response == null;\n return err instanceof TypeError;\n}\n\n/**\n * Read from the server, and from the device's copy only when the connection failed\n * ({@link isOfflineError}); a 4xx or 5xx still throws. `onOnline` gets the server's answer\n * before it is returned (to refresh the copy, say); a throw from it counts as a failure of\n * the online path. The plain form of keksdose's net.ts:58, without its sealed-household\n * and cold-start branches (those stay keksdose's).\n */\nexport async function readLocalFirst<T>(\n online: () => Promise<T>,\n offline: () => Promise<T>,\n onOnline?: (result: T) => void | Promise<void>,\n): Promise<T> {\n try {\n const result = await online();\n await onOnline?.(result);\n return result;\n } catch (err) {\n if (!isOfflineError(err)) throw err;\n return offline();\n }\n}\n\n/**\n * Write through the server first (`rest`), and queue the change on the device (`queue`)\n * only when the connection failed; any other failure throws, nothing queued. Online writes\n * stay server-stamped (D2). A create mints its id BEFORE calling this and sends it on both\n * paths, so a REST create that timed out after the server applied it is not created twice\n * (D16).\n */\nexport async function writeLocalFirst<T>(rest: () => Promise<T>, queue: () => Promise<T>): Promise<T> {\n try {\n return await rest();\n } catch (err) {\n if (!isOfflineError(err)) throw err;\n return queue();\n }\n}\n"],"mappings":";AAAA,SAAS,iCAAiC;AAmBnC,SAAS,WAAW,UAA2B;AACpD,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,OAAO,aAAa,WAAW,KAAK,MAAM,QAAQ,IAAI,OAAO;AAC3E,SAAO,IAAI,KAAK,OAAO,SAAS,KAAK,KAAK,SAAS,MAAM,QAAQ,IAAI,GAAG,EAAE,YAAY;AACxF;AAUA,eAAsB,MAAM,WAAmB,MAA+B;AAC5E,QAAM,MAAM,sEAAsE,KAAK,SAAS,IAC5F,UAAU,WAAW,KAAK,EAAE,IAC5B;AACJ,MAAI,CAAC,IAAK,OAAM,IAAI,UAAU,4CAA4C,KAAK,UAAU,SAAS,CAAC,EAAE;AACrG,QAAM,YAAY,IAAI,YAAY,EAAE,OAAO,IAAI;AAC/C,QAAM,QAAQ,IAAI,WAAW,KAAK,UAAU,MAAM;AAClD,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK,EAAG,OAAM,CAAC,IAAI,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;AAC1F,QAAM,IAAI,WAAW,EAAE;AACvB,QAAM,QAAQ,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,EAAE;AACpF,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC7E,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;AAC1G;AAQO,SAAS,eAAe,KAAuB;AACpD,MAAI,0BAA0B,GAAG,EAAG,QAAO;AAC3C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO;AACpD,QAAM,MAAM;AACZ,MAAI,IAAI,iBAAiB,QAAQ,cAAc,IAAK,QAAO,IAAI,YAAY;AAC3E,SAAO,eAAe;AACxB;AASA,eAAsB,eACpB,QACA,SACA,UACY;AACZ,MAAI;AACF,UAAM,SAAS,MAAM,OAAO;AAC5B,UAAM,WAAW,MAAM;AACvB,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,CAAC,eAAe,GAAG,EAAG,OAAM;AAChC,WAAO,QAAQ;AAAA,EACjB;AACF;AASA,eAAsB,gBAAmB,MAAwB,OAAqC;AACpG,MAAI;AACF,WAAO,MAAM,KAAK;AAAA,EACpB,SAAS,KAAK;AACZ,QAAI,CAAC,eAAe,GAAG,EAAG,OAAM;AAChC,WAAO,MAAM;AAAA,EACf;AACF;","names":[]}
package/dist/offline.d.ts CHANGED
@@ -8,4 +8,4 @@ export { createServiceWorkerUpdates, useServiceWorkerUpdate } from "./offline/se
8
8
  export type { RegisterServiceWorker, ServiceWorkerUpdates } from "./offline/service-worker-updates.js";
9
9
  export { SharedComputerChoice, DEFAULT_SHARED_COMPUTER_LABELS, isSharedComputer, setSharedComputer, } from "./offline/shared-computer.js";
10
10
  export type { SharedComputerChoiceProps, SharedComputerLabels, SharedComputerStorage } from "./offline/shared-computer.js";
11
- export { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
11
+ export { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from "./offline/local-first.js";
package/dist/offline.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isSharedComputer,
10
10
  setSharedComputer
11
11
  } from "./offline/shared-computer.js";
12
- import { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
12
+ import { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from "./offline/local-first.js";
13
13
  export {
14
14
  DEFAULT_PWA_UPDATE_PROMPT_LABELS,
15
15
  DEFAULT_SCRUB_FIELDS,
@@ -28,6 +28,7 @@ export {
28
28
  setSharedComputer,
29
29
  stampAfter,
30
30
  useServiceWorkerUpdate,
31
+ uuid5,
31
32
  writeLocalFirst
32
33
  };
33
34
  //# sourceMappingURL=offline.js.map
@@ -1 +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":[]}
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, uuid5, 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,OAAO,uBAAuB;","names":[]}
package/dist/sync/db.d.ts CHANGED
@@ -63,6 +63,23 @@ export declare const SYNC_DB_STORES: {
63
63
  readonly outbox: "++seq, [entity+id], status, scope";
64
64
  readonly meta: "key";
65
65
  };
66
+ /**
67
+ * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a
68
+ * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in
69
+ * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a
70
+ * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,
71
+ * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:
72
+ * `extraStores: { ...SYNC_DB_PK_INDEX }`.
73
+ */
74
+ export declare const SYNC_DB_PK_INDEX: {
75
+ readonly rows: "[entity+id], entity, [entity+pk]";
76
+ };
77
+ /**
78
+ * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when
79
+ * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.
80
+ * `undefined` when the copy holds no such row.
81
+ */
82
+ export declare function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined>;
66
83
  /**
67
84
  * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's
68
85
  * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:
package/dist/sync/db.js CHANGED
@@ -5,6 +5,13 @@ const SYNC_DB_STORES = {
5
5
  outbox: "++seq, [entity+id], status, scope",
6
6
  meta: "key"
7
7
  };
8
+ const SYNC_DB_PK_INDEX = {
9
+ rows: "[entity+id], entity, [entity+pk]"
10
+ };
11
+ async function rowByPk(db, entity, pk) {
12
+ if (db.rows.schema.idxByName["[entity+pk]"]) return db.rows.where("[entity+pk]").equals([entity, pk]).first();
13
+ return db.rows.where("entity").equals(entity).filter((row) => row.pk === pk).first();
14
+ }
8
15
  function createSyncDb(name, options = {}) {
9
16
  const db = new Dexie(name);
10
17
  let stores = { ...SYNC_DB_STORES, ...options.extraStores };
@@ -18,7 +25,9 @@ function createSyncDb(name, options = {}) {
18
25
  return db;
19
26
  }
20
27
  export {
28
+ SYNC_DB_PK_INDEX,
21
29
  SYNC_DB_STORES,
22
- createSyncDb
30
+ createSyncDb,
31
+ rowByPk
23
32
  };
24
33
  //# sourceMappingURL=db.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAYO,SAAS,aAAwC,MAAc,UAA+B,CAAC,GAAM;AAC1G,QAAM,KAAK,IAAI,MAAM,IAAI;AACzB,MAAI,SAAwC,EAAE,GAAG,gBAAgB,GAAG,QAAQ,YAAY;AACxF,KAAG,QAAQ,CAAC,EAAE,OAAO,MAAM;AAC3B,QAAM,WAAW,CAAC,GAAI,QAAQ,YAAY,CAAC,CAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AACnF,aAAW,QAAQ,UAAU;AAC3B,aAAS,EAAE,GAAG,QAAQ,GAAG,KAAK,OAAO;AACrC,UAAM,UAAU,GAAG,QAAQ,KAAK,OAAO,EAAE,OAAO,MAAM;AACtD,QAAI,KAAK,QAAS,SAAQ,QAAQ,KAAK,OAAO;AAAA,EAChD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/sync/db.ts"],"sourcesContent":["import Dexie, { type Table, type Transaction } from \"dexie\";\n\n/*\n * The device's copy for the sync engine (docs/local-first-harmonization.md §2.3, D10): ONE\n * Dexie database with the engine's three tables — the rows as `/sync` carries them, the\n * outbox of changes made here, and a key/value `meta` — plus the app's own (kastlan's\n * `photos`). A generic store, not keksdose's per-entity tables.\n */\n\n/** A row as `/sync` carries it: the entity (a table name), its sync id, the server's id\n * once it has one, and its fields. */\nexport interface SyncedRow {\n entity: string;\n id: string;\n pk: number | null;\n data: Record<string, unknown>;\n /** The server's stamp of the row, as the pull carried it BESIDE `data` (kastlan's wire);\n * the next edit here is stamped after it (`stampAfter`, D2). */\n updated_at?: string | null;\n}\n\n/**\n * Where a change made here stands. `pending`: queued. `sending`: in the request now out (a\n * new edit starts a change of its own). `waiting`: refused with a hold code (a lapsed plan,\n * `holdCodes`), sent again every round. `blocked`: refused with any other code, kept, sent\n * again only on `retryBlocked()`. `rejected`: the server's rules refused it (`reason` says\n * why); never sent again.\n */\nexport type OutboxStatus = \"pending\" | \"sending\" | \"waiting\" | \"blocked\" | \"rejected\";\n\n/** A change made on this device, for `/sync`, in the scope its caller named (D11). */\nexport interface OutboxChange {\n seq?: number;\n scope: string;\n entity: string;\n id: string;\n op: \"upsert\" | \"delete\";\n data: Record<string, unknown>;\n /** The edit's time on this device (D2), stamped with `stampAfter`. */\n updated_at: string;\n status: OutboxStatus;\n /** The server's words: a rejection's `detail`, a refusal's `detail`. */\n reason?: string;\n /** The server's code: a rejection's or a refusal's `code`. */\n code?: string;\n}\n\nexport interface SyncMetaEntry {\n key: string;\n value: unknown;\n}\n\n/** The engine's database: its three tables, and the app's own under the names it gave. */\nexport type SyncDb = Dexie & {\n rows: Table<SyncedRow, [string, string]>;\n outbox: Table<OutboxChange, number>;\n meta: Table<SyncMetaEntry, string>;\n};\n\n/** A schema step of an existing database (kastlan's v2: `scope` on the outbox). */\nexport interface SyncDbUpgrade {\n version: number;\n /** Stores this version adds or changes, beside the engine's and `extraStores`. */\n stores?: Record<string, string | null>;\n upgrade?(tx: Transaction): Promise<void> | void;\n}\n\nexport interface CreateSyncDbOptions {\n /** The app's own tables, Dexie schema strings: kastlan `{ photos: \"id, roomId, status\" }`. */\n extraStores?: Record<string, string>;\n /** Later versions of a database that exists on devices already, oldest first. */\n upgrades?: readonly SyncDbUpgrade[];\n}\n\n/** The engine's own tables. `scope` is indexed: a round takes one scope's queue (D11). */\nexport const SYNC_DB_STORES = {\n rows: \"[entity+id], entity\",\n outbox: \"++seq, [entity+id], status, scope\",\n meta: \"key\",\n} as const;\n\n/**\n * The `rows` schema with an `[entity+pk]` index (0.46.1), opt-in: the D16 pages start from a\n * server id (`/meters/7`) and {@link rowByPk} finds its copy row by this index. Not in\n * {@link SYNC_DB_STORES}, so no database on a device changes until its app names it in a\n * version of its own — kastlan, whose devices are on v2: `upgrades: [..., { version: 3,\n * stores: SYNC_DB_PK_INDEX }]`. A database new to the devices may take it at once:\n * `extraStores: { ...SYNC_DB_PK_INDEX }`.\n */\nexport const SYNC_DB_PK_INDEX = {\n rows: \"[entity+id], entity, [entity+pk]\",\n} as const;\n\n/**\n * The copy row of the server's id `pk` in `entity` (0.46.1): by the `[entity+pk]` index when\n * the database has it ({@link SYNC_DB_PK_INDEX}), else by a scan of the entity's rows.\n * `undefined` when the copy holds no such row.\n */\nexport async function rowByPk(db: SyncDb, entity: string, pk: number): Promise<SyncedRow | undefined> {\n if (db.rows.schema.idxByName[\"[entity+pk]\"]) return db.rows.where(\"[entity+pk]\").equals([entity, pk]).first();\n return db.rows.where(\"entity\").equals(entity).filter((row) => row.pk === pk).first();\n}\n\n/**\n * Open (lazily, on first use) the sync database `name`: the engine's tables plus the app's\n * `extraStores`, at version 1 or the last of `upgrades`. One database per account (kastlan:\n * per account and company, `kastlan-handover-<user>-<company>`), and one engine on it (D15).\n *\n * An app whose database predates the engine names its next version in `upgrades` and moves\n * its data there — kastlan: `{ version: 2, upgrade: (tx) => tx.table(\"outbox\").toCollection()\n * .modify({ scope: \"handover\" }) }`. Dexie diffs the schema against the one on the device.\n * Name the app's own type for its tables: `createSyncDb<HandoverDb>(name, { extraStores })`.\n */\nexport function createSyncDb<D extends SyncDb = SyncDb>(name: string, options: CreateSyncDbOptions = {}): D {\n const db = new Dexie(name) as D;\n let stores: Record<string, string | null> = { ...SYNC_DB_STORES, ...options.extraStores };\n db.version(1).stores(stores);\n const upgrades = [...(options.upgrades ?? [])].sort((a, b) => a.version - b.version);\n for (const step of upgrades) {\n stores = { ...stores, ...step.stores };\n const version = db.version(step.version).stores(stores);\n if (step.upgrade) version.upgrade(step.upgrade);\n }\n return db;\n}\n"],"mappings":";AAAA,OAAO,WAA6C;AA2E7C,MAAM,iBAAiB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AACR;AAUO,MAAM,mBAAmB;AAAA,EAC9B,MAAM;AACR;AAOA,eAAsB,QAAQ,IAAY,QAAgB,IAA4C;AACpG,MAAI,GAAG,KAAK,OAAO,UAAU,aAAa,EAAG,QAAO,GAAG,KAAK,MAAM,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;AAC5G,SAAO,GAAG,KAAK,MAAM,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,EAAE,MAAM;AACrF;AAYO,SAAS,aAAwC,MAAc,UAA+B,CAAC,GAAM;AAC1G,QAAM,KAAK,IAAI,MAAM,IAAI;AACzB,MAAI,SAAwC,EAAE,GAAG,gBAAgB,GAAG,QAAQ,YAAY;AACxF,KAAG,QAAQ,CAAC,EAAE,OAAO,MAAM;AAC3B,QAAM,WAAW,CAAC,GAAI,QAAQ,YAAY,CAAC,CAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AACnF,aAAW,QAAQ,UAAU;AAC3B,aAAS,EAAE,GAAG,QAAQ,GAAG,KAAK,OAAO;AACrC,UAAM,UAAU,GAAG,QAAQ,KAAK,OAAO,EAAE,OAAO,MAAM;AACtD,QAAI,KAAK,QAAS,SAAQ,QAAQ,KAAK,OAAO;AAAA,EAChD;AACA,SAAO;AACT;","names":[]}
@@ -19,12 +19,13 @@ export interface SyncStepResult {
19
19
  /** What the step keeps on the device, added to the engine's counts. */
20
20
  counts?: Partial<SyncCounts>;
21
21
  }
22
- /** Work after the push/pull, such as kastlan's photo upload. */
23
- export interface SyncStep {
22
+ /** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type
23
+ * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */
24
+ export interface SyncStep<D extends SyncDb = SyncDb> {
24
25
  name: string;
25
- run(db: SyncDb, ctx: SyncStepContext): Promise<SyncStepResult>;
26
+ run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;
26
27
  /** Its counts read from the device between rounds (a photo taken offline shows at once). */
27
- counts?(db: SyncDb): Promise<Partial<SyncCounts>>;
28
+ counts?(db: D): Promise<Partial<SyncCounts>>;
28
29
  }
29
30
  export interface SyncEngineState {
30
31
  phase: "idle" | "syncing" | "offline" | "error";
@@ -44,8 +45,9 @@ export interface SyncRoundResult {
44
45
  rejected: number;
45
46
  error?: unknown;
46
47
  }
47
- export interface SyncEngineOptions {
48
- db: SyncDb;
48
+ export interface SyncEngineOptions<D extends SyncDb = SyncDb> {
49
+ /** The database; its type `D` is the one the `steps` get. */
50
+ db: D;
49
51
  /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,
50
52
  * maintenance, contacts). Read at the start of every round: roles are per company. */
51
53
  scopes: () => readonly string[];
@@ -61,7 +63,7 @@ export interface SyncEngineOptions {
61
63
  holdCodes?: readonly string[];
62
64
  /** Ask for a paged pull and follow its cursor (D12). */
63
65
  paged?: boolean;
64
- steps?: readonly SyncStep[];
66
+ steps?: readonly SyncStep<D>[];
65
67
  /** No round while this holds (kastlan: a practice tour fakes the server's answers). */
66
68
  paused?: () => boolean;
67
69
  /** The poll (30 s) and the debounce after a write (800 ms). */
@@ -106,7 +108,7 @@ export declare const DEFAULT_HOLD_CODES: readonly string[];
106
108
  * engine on the new database and stops the old one). Nothing runs until `start()`
107
109
  * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.
108
110
  */
109
- export declare function createSyncEngine(options: SyncEngineOptions): SyncEngine;
111
+ export declare function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine;
110
112
  /**
111
113
  * The engine kept for `options.db` (by its name), built on first use: one engine per
112
114
  * database for the session (D15), so a company switch A→B→A finds A's engine again instead
@@ -114,7 +116,7 @@ export declare function createSyncEngine(options: SyncEngineOptions): SyncEngine
114
116
  * changing values (scopes, the client) through functions. A database object opened afresh
115
117
  * under the same name replaces the kept engine (the old one is stopped).
116
118
  */
117
- export declare function syncEngineFor(options: SyncEngineOptions): SyncEngine;
119
+ export declare function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine;
118
120
  /**
119
121
  * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is
120
122
  * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it