@eifi1/ui-kit 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +11 -7
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +6 -0
  8. package/dist/admin/admin-area-labels.js +1 -0
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  11. package/dist/admin/mail-test-card-labels.js +17 -0
  12. package/dist/admin/mail-test-card-labels.js.map +1 -0
  13. package/dist/admin/mail-test-card.d.ts +74 -0
  14. package/dist/admin/mail-test-card.js +80 -0
  15. package/dist/admin/mail-test-card.js.map +1 -0
  16. package/dist/billing/provider-page.d.ts +9 -0
  17. package/dist/billing/provider-page.js.map +1 -1
  18. package/dist/components/data-table-mobile.js +17 -2
  19. package/dist/components/data-table-mobile.js.map +1 -1
  20. package/dist/components/data-table-shared.d.ts +2 -0
  21. package/dist/components/data-table-shared.js +8 -0
  22. package/dist/components/data-table-shared.js.map +1 -1
  23. package/dist/components/data-table.js +1 -6
  24. package/dist/components/data-table.js.map +1 -1
  25. package/dist/eslint/index.js +1 -1
  26. package/dist/i18n/defaults.js +9 -1
  27. package/dist/i18n/defaults.js.map +1 -1
  28. package/dist/i18n/german.js +42 -2
  29. package/dist/i18n/german.js.map +1 -1
  30. package/dist/i18n/kit-labels.d.ts +13 -0
  31. package/dist/i18n/kit-labels.js.map +1 -1
  32. package/dist/i18n/locales/en.js +60 -4
  33. package/dist/i18n/locales/en.js.map +3 -3
  34. package/dist/i18n/locales/es.js +40 -0
  35. package/dist/i18n/locales/es.js.map +1 -1
  36. package/dist/i18n/locales/fr.js +42 -2
  37. package/dist/i18n/locales/fr.js.map +1 -1
  38. package/dist/i18n/locales/hu.js +40 -0
  39. package/dist/i18n/locales/hu.js.map +1 -1
  40. package/dist/i18n/locales/it.js +41 -1
  41. package/dist/i18n/locales/it.js.map +1 -1
  42. package/dist/i18n/locales/zh.js +40 -0
  43. package/dist/i18n/locales/zh.js.map +1 -1
  44. package/dist/i18n/review.js +13 -1
  45. package/dist/i18n/review.js.map +1 -1
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +5 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/lib/format.d.ts +2 -1
  50. package/dist/lib/format.js +6 -1
  51. package/dist/lib/format.js.map +1 -1
  52. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  53. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  54. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  55. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  56. package/dist/offline/pwa-update-prompt.js +36 -0
  57. package/dist/offline/pwa-update-prompt.js.map +1 -0
  58. package/dist/offline/query-persistence.d.ts +100 -0
  59. package/dist/offline/query-persistence.js +179 -0
  60. package/dist/offline/query-persistence.js.map +1 -0
  61. package/dist/offline/shared-computer-labels.d.ts +2 -0
  62. package/dist/offline/shared-computer-labels.js +9 -0
  63. package/dist/offline/shared-computer-labels.js.map +1 -0
  64. package/dist/offline/shared-computer.d.ts +44 -0
  65. package/dist/offline/shared-computer.js +51 -0
  66. package/dist/offline/shared-computer.js.map +1 -0
  67. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  68. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  69. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  70. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  71. package/dist/offline/sign-out-pending-dialog.js +66 -0
  72. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  73. package/dist/offline.d.ts +8 -0
  74. package/dist/offline.js +24 -0
  75. package/dist/offline.js.map +1 -0
  76. package/dist/shell/admin-menu.d.ts +24 -0
  77. package/dist/shell/admin-menu.js +12 -3
  78. package/dist/shell/admin-menu.js.map +1 -1
  79. package/dist/shell.d.ts +2 -0
  80. package/dist/shell.js +2 -0
  81. package/dist/shell.js.map +1 -1
  82. package/dist/tour/tour-catalogue-labels.js +2 -2
  83. package/dist/tour/tour-catalogue-labels.js.map +1 -1
  84. package/dist/tour/tour-catalogue.d.ts +7 -1
  85. package/dist/tour/tour-catalogue.js.map +1 -1
  86. package/dist/tour/tour-info.d.ts +2 -2
  87. package/dist/tour/tour-info.js.map +1 -1
  88. package/dist/tour/tour.d.ts +38 -2
  89. package/dist/tour/tour.js +42 -3
  90. package/dist/tour/tour.js.map +1 -1
  91. package/dist/tour/use-tour-progress.d.ts +165 -12
  92. package/dist/tour/use-tour-progress.js +271 -25
  93. package/dist/tour/use-tour-progress.js.map +1 -1
  94. package/dist/tour.d.ts +3 -1
  95. package/dist/tour.js +4 -1
  96. package/dist/tour.js.map +1 -1
  97. package/package.json +25 -1
  98. package/src/admin/admin-action-log-labels.ts +9 -0
  99. package/src/admin/admin-action-log.tsx +41 -5
  100. package/src/admin/admin-area-labels.ts +7 -0
  101. package/src/admin/mail-test-card-labels.ts +19 -0
  102. package/src/admin/mail-test-card.tsx +186 -0
  103. package/src/billing/provider-page.ts +9 -0
  104. package/src/components/data-table-mobile.tsx +26 -3
  105. package/src/components/data-table-shared.tsx +18 -1
  106. package/src/components/data-table.tsx +1 -15
  107. package/src/i18n/defaults.ts +8 -0
  108. package/src/i18n/german.ts +48 -2
  109. package/src/i18n/kit-labels.tsx +13 -0
  110. package/src/i18n/locales/en.ts +13 -1
  111. package/src/i18n/locales/es.ts +46 -0
  112. package/src/i18n/locales/fr.ts +48 -2
  113. package/src/i18n/locales/hu.ts +42 -0
  114. package/src/i18n/locales/it.ts +47 -1
  115. package/src/i18n/locales/zh.ts +40 -0
  116. package/src/i18n/review.ts +12 -0
  117. package/src/index.ts +3 -0
  118. package/src/lib/format.ts +13 -2
  119. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  120. package/src/offline/pwa-update-prompt.tsx +72 -0
  121. package/src/offline/query-persistence.ts +323 -0
  122. package/src/offline/shared-computer-labels.ts +8 -0
  123. package/src/offline/shared-computer.tsx +87 -0
  124. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  125. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  126. package/src/offline.ts +26 -0
  127. package/src/shell/admin-menu.tsx +40 -3
  128. package/src/shell.ts +4 -0
  129. package/src/tour/tour-catalogue-labels.ts +2 -2
  130. package/src/tour/tour-catalogue.tsx +7 -1
  131. package/src/tour/tour-info.ts +2 -2
  132. package/src/tour/tour.tsx +87 -3
  133. package/src/tour/use-tour-progress.ts +501 -34
  134. package/src/tour.ts +5 -1
@@ -626,7 +626,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
626
626
  stateDone: (count) => `Completati (${n(count)})`,
627
627
  showing: (shown, total) => `${n(shown)} di ${n(total)} tour`,
628
628
  noMatches: "Nessun tour corrisponde",
629
- noMatchesHint: "Provi un’altra parola o cancelli i filtri.",
629
+ noMatchesHint: "Provi un’altra parola o cancelli il filtro.",
630
630
  clearFilters: "Cancella filtri",
631
631
  badgeNew: "Nuovo",
632
632
  badgeUpdated: "Aggiornato",
@@ -1385,11 +1385,14 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1385
1385
  // ("Azienda creata"; Kurvenschmiede's "il team" is masculine, never "squadra"), a
1386
1386
  // member's with "account", as `actions` do. `budget` is keksdose's; `company` keeps
1387
1387
  // `actions.membership_remove`'s string.
1388
+ renamedFromTo: (from, to) => `da ${from} a ${to}`,
1388
1389
  groupActions: {
1389
1390
  group: {
1390
1391
  group_create: "Gruppo creato",
1391
1392
  group_rename: "Gruppo rinominato",
1392
1393
  group_delete: "Gruppo eliminato",
1394
+ group_deactivate: "Gruppo disattivato",
1395
+ group_reactivate: "Gruppo riattivato",
1393
1396
  membership_add: "Aggiunto al gruppo",
1394
1397
  membership_role: "Ruolo nel gruppo cambiato",
1395
1398
  membership_remove: "Rimosso dal gruppo",
@@ -1399,6 +1402,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1399
1402
  group_create: "Team creato",
1400
1403
  group_rename: "Team rinominato",
1401
1404
  group_delete: "Team eliminato",
1405
+ group_deactivate: "Team disattivato",
1406
+ group_reactivate: "Team riattivato",
1402
1407
  membership_add: "Aggiunto al team",
1403
1408
  membership_role: "Ruolo nel team cambiato",
1404
1409
  membership_remove: "Rimosso dal team",
@@ -1408,6 +1413,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1408
1413
  group_create: "Azienda creata",
1409
1414
  group_rename: "Azienda rinominata",
1410
1415
  group_delete: "Azienda eliminata",
1416
+ group_deactivate: "Azienda disattivata",
1417
+ group_reactivate: "Azienda riattivata",
1411
1418
  membership_add: "Aggiunto all’azienda",
1412
1419
  membership_role: "Ruolo nell’azienda cambiato",
1413
1420
  membership_remove: "Rimosso dall’azienda",
@@ -1417,6 +1424,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1417
1424
  group_create: "Budget creato",
1418
1425
  group_rename: "Budget rinominato",
1419
1426
  group_delete: "Budget eliminato",
1427
+ group_deactivate: "Budget disattivato",
1428
+ group_reactivate: "Budget riattivato",
1420
1429
  membership_add: "Aggiunto al budget",
1421
1430
  membership_role: "Ruolo nel budget cambiato",
1422
1431
  membership_remove: "Rimosso dal budget",
@@ -1850,9 +1859,21 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1850
1859
  },
1851
1860
  // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words. "Lei", as
1852
1861
  // the rest of the catalogue.
1862
+ mailTestCard: {
1863
+ title: "Email di prova",
1864
+ description: "Invia un’email al Suo indirizzo, tramite il trasporto con cui questa installazione invia tutte le sue email.",
1865
+ transport: (backend) => `Trasporto: ${backend}`,
1866
+ action: "Inviami un’email di prova",
1867
+ sent: (address) => `Inviata a ${address}. Controlli la posta in arrivo.`,
1868
+ console: "Il trasporto console registra solo le email; non è stato inviato nulla.",
1869
+ failed: "Invio non riuscito. Il log del server contiene la risposta del provider.",
1870
+ rateLimited,
1871
+ keywords: "email prova test posta trasporto smtp resend console inviare consegna",
1872
+ },
1853
1873
  adminArea: {
1854
1874
  title: "Amministrazione",
1855
1875
  waiting: (count) => `${n(count)} in attesa`,
1876
+ waitingLabel: (label, count) => `${label}, ${n(count)} in attesa`,
1856
1877
  forbiddenTitle: "Solo per gli amministratori",
1857
1878
  forbiddenBody: (appName) =>
1858
1879
  `Qui gli amministratori gestiscono ${appName}. Il Suo account non può aprire questa pagina.`,
@@ -1921,6 +1942,31 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1921
1942
  billingOff: "La fatturazione è disattivata in locale: i piani non cambiano nulla",
1922
1943
  },
1923
1944
  },
1945
+ // 0.45.0 — docs/local-first-harmonization.md §1.6 (`@eifi1/ui-kit/offline`): keksdose's
1946
+ // reviewed words (nav.logout_queued_*, pwa.*) where it has the language.
1947
+ signOutPending: {
1948
+ title: "Alcune modifiche non sono ancora state salvate sul server",
1949
+ lost: (count) =>
1950
+ count === 1
1951
+ ? `${n(count)} modifica è ancora in attesa di sincronizzazione. Uscendo, la copia su questo dispositivo viene cancellata, ed è l’unica.`
1952
+ : `${n(count)} modifiche sono ancora in attesa di sincronizzazione. Uscendo, la copia su questo dispositivo viene cancellata, ed è l’unica.`,
1953
+ kept: (count) =>
1954
+ count === 1
1955
+ ? `${n(count)} modifica è ancora in attesa di sincronizzazione. Resta per Lei su questo dispositivo e verrà sincronizzata al Suo prossimo accesso qui.`
1956
+ : `${n(count)} modifiche sono ancora in attesa di sincronizzazione. Restano per Lei su questo dispositivo e verranno sincronizzate al Suo prossimo accesso qui.`,
1957
+ syncNow: "Sincronizza ora",
1958
+ signOutAnyway: "Esci comunque",
1959
+ discardWaiting: "Scarta le modifiche in attesa",
1960
+ cancel: "Annulla",
1961
+ },
1962
+ pwaUpdate: {
1963
+ updateAvailable: "È disponibile una nuova versione.",
1964
+ reload: "Ricarica",
1965
+ },
1966
+ sharedComputer: {
1967
+ label: "Questo è un computer condiviso",
1968
+ hint: "Su questo dispositivo non resta alcuna copia dei Suoi dati. Le pagine si aprono quindi solo con una connessione.",
1969
+ },
1924
1970
  };
1925
1971
  }
1926
1972
 
@@ -1321,11 +1321,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1321
1321
  },
1322
1322
  // 0.34 (§11.5): whole phrases per noun. `team` is Kurvenschmiede's "团队"; `company`
1323
1323
  // keeps `actions.membership_remove`'s string.
1324
+ renamedFromTo: (from, to) => `从 ${from} 改为 ${to}`,
1324
1325
  groupActions: {
1325
1326
  group: {
1326
1327
  group_create: "群组已创建",
1327
1328
  group_rename: "群组已重命名",
1328
1329
  group_delete: "群组已删除",
1330
+ group_deactivate: "群组已停用",
1331
+ group_reactivate: "群组已重新激活",
1329
1332
  membership_add: "已添加到群组",
1330
1333
  membership_role: "群组中的角色已更改",
1331
1334
  membership_remove: "已从群组移除",
@@ -1335,6 +1338,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1335
1338
  group_create: "团队已创建",
1336
1339
  group_rename: "团队已重命名",
1337
1340
  group_delete: "团队已删除",
1341
+ group_deactivate: "团队已停用",
1342
+ group_reactivate: "团队已重新激活",
1338
1343
  membership_add: "已添加到团队",
1339
1344
  membership_role: "团队中的角色已更改",
1340
1345
  membership_remove: "已从团队移除",
@@ -1344,6 +1349,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1344
1349
  group_create: "公司已创建",
1345
1350
  group_rename: "公司已重命名",
1346
1351
  group_delete: "公司已删除",
1352
+ group_deactivate: "公司已停用",
1353
+ group_reactivate: "公司已重新激活",
1347
1354
  membership_add: "已添加到公司",
1348
1355
  membership_role: "公司中的角色已更改",
1349
1356
  membership_remove: "已从公司移除",
@@ -1353,6 +1360,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1353
1360
  group_create: "预算已创建",
1354
1361
  group_rename: "预算已重命名",
1355
1362
  group_delete: "预算已删除",
1363
+ group_deactivate: "预算已停用",
1364
+ group_reactivate: "预算已重新激活",
1356
1365
  membership_add: "已添加到预算",
1357
1366
  membership_role: "预算中的角色已更改",
1358
1367
  membership_remove: "已从预算移除",
@@ -1754,9 +1763,21 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1754
1763
  moreTitle: "更多页面",
1755
1764
  },
1756
1765
  // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words.
1766
+ mailTestCard: {
1767
+ title: "测试邮件",
1768
+ description: "通过本安装发送所有邮件所用的传输方式,向您自己的地址发送一封邮件。",
1769
+ transport: (backend) => `传输方式:${backend}`,
1770
+ action: "给我发送测试邮件",
1771
+ sent: (address) => `已发送至 ${address}。请查看收件箱。`,
1772
+ console: "控制台传输只记录邮件,未发送任何邮件。",
1773
+ failed: "无法发送。服务器日志中有服务商的回复。",
1774
+ rateLimited,
1775
+ keywords: "测试邮件 邮件 电子邮件 传输 smtp resend 控制台 发送 投递",
1776
+ },
1757
1777
  adminArea: {
1758
1778
  title: "管理",
1759
1779
  waiting: (count) => `${n(count)} 项待处理`,
1780
+ waitingLabel: (label, count) => `${label},${n(count)} 项待处理`,
1760
1781
  forbiddenTitle: "仅限管理员",
1761
1782
  forbiddenBody: (appName) => `管理员在这里管理 ${appName}。您的账户无法打开此页面。`,
1762
1783
  forbiddenHome: "前往首页",
@@ -1822,6 +1843,25 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1822
1843
  billingOff: "本地已关闭计费:切换套餐不会改变任何内容",
1823
1844
  },
1824
1845
  },
1846
+ // 0.45.0 — docs/local-first-harmonization.md §1.6 (`@eifi1/ui-kit/offline`): keksdose's
1847
+ // reviewed words (nav.logout_queued_*, pwa.*) where it has the language.
1848
+ signOutPending: {
1849
+ title: "部分修改尚未保存到服务器",
1850
+ lost: (count) => `${n(count)} 项修改仍在等待同步。退出登录会清除此设备上的副本,而这是唯一的副本。`,
1851
+ kept: (count) => `${n(count)} 项修改仍在等待同步。它们会为您保留在此设备上,并在您下次在此登录时同步。`,
1852
+ syncNow: "立即同步",
1853
+ signOutAnyway: "仍然退出登录",
1854
+ discardWaiting: "放弃等待中的修改",
1855
+ cancel: "取消",
1856
+ },
1857
+ pwaUpdate: {
1858
+ updateAvailable: "有新版本可用。",
1859
+ reload: "重新加载",
1860
+ },
1861
+ sharedComputer: {
1862
+ label: "这是一台共用电脑",
1863
+ hint: "此设备上不会保留您的数据副本。页面将只能在联网时打开。",
1864
+ },
1825
1865
  };
1826
1866
  }
1827
1867
 
@@ -353,6 +353,8 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
353
353
  "adminActionLog.by": [["{{actor}}"]],
354
354
  "adminActionLog.filteredTo": [["{{target}}"]],
355
355
  "adminActionLog.filterBy": [["{{target}}"]],
356
+ // 0.45.0 (admin §9.4): a company rename that logged both names.
357
+ "adminActionLog.renamedFromTo": [["{{from}}", "{{to}}"]],
356
358
  "transferOwnership.title": [["{{name}}"]],
357
359
  "transferOwnership.unavailable": [["{{account}}", "{{reason}}"]],
358
360
  "invitations.sent": [["{{date}}"]],
@@ -406,6 +408,12 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
406
408
  // 0.42.0 (docs/admin-harmonization.md §3.1): the menu row's count, the 403 page's app,
407
409
  // the users tile's new accounts.
408
410
  "adminArea.waiting": one,
411
+ // 0.45.0 (admin §9.3.4): an IconButton's name with its waiting count.
412
+ "adminArea.waitingLabel": [["{{label}}", 1], ["{{label}}", 3]],
413
+ // 0.45.0 (admin §9.2.4): the test mail's transport, masked address and limiter.
414
+ "mailTestCard.transport": [["{{backend}}"]],
415
+ "mailTestCard.sent": [["{{address}}"]],
416
+ "mailTestCard.rateLimited": [[], [30], [300]],
409
417
  "adminArea.forbiddenBody": [["{{app}}"]],
410
418
  "adminArea.tiles.usersNew": one,
411
419
  // 0.42.0 (docs/top-bar-harmonization.md §3.1): the sync indicator's counts, its time, the
@@ -431,4 +439,8 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
431
439
  "tourCatalogue.steps": one,
432
440
  "tourCatalogue.duration": one,
433
441
  "tourCatalogue.completedOn": [["{{date}}"]],
442
+ // 0.45.0 (docs/local-first-harmonization.md §1.6): the sign-out question's count, in
443
+ // both word sets.
444
+ "signOutPending.lost": one,
445
+ "signOutPending.kept": one,
434
446
  };
package/src/index.ts CHANGED
@@ -811,6 +811,9 @@ export type {
811
811
  } from "./admin/admin-overview";
812
812
  export { AdminToolLinks } from "./admin/admin-tool-links";
813
813
  export type { AdminToolLink, AdminToolLinksProps } from "./admin/admin-tool-links";
814
+ // 0.45.0 (docs/admin-harmonization.md §9.2.4): "Send a test mail to me".
815
+ export { MailTestCard, DEFAULT_MAIL_TEST_CARD_LABELS, MAIL_TEST_ANCHOR, maskMailAddress } from "./admin/mail-test-card";
816
+ export type { MailTestCardProps, MailTestCardLabels, MailTestAnswer } from "./admin/mail-test-card";
814
817
  // 0.33.0: the operator's plan parts (docs/billing-harmonization.md §14.12).
815
818
  export {
816
819
  planColumn,
package/src/lib/format.ts CHANGED
@@ -182,10 +182,14 @@ const DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {
182
182
  };
183
183
 
184
184
  export interface FormatDateOptions extends BaseOptions {
185
- /** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. */
185
+ /** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. A bare
186
+ * "YYYY-MM-DD" ignores it: a calendar day is the day it names in every zone. */
186
187
  timeZone?: string;
187
188
  }
188
189
 
190
+ /** A bare ISO calendar day, `"YYYY-MM-DD"`. */
191
+ const BARE_ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
192
+
189
193
  /** What {@link formatDate} and {@link formatRelativeTime} accept. */
190
194
  export type DateInput = string | number | Date | null | undefined;
191
195
 
@@ -197,7 +201,7 @@ export type DateInput = string | number | Date | null | undefined;
197
201
  */
198
202
  export function toDate(value: DateInput): Date | null {
199
203
  if (value === null || value === undefined || value === "") return null;
200
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return parseIsoDate(value);
204
+ if (typeof value === "string" && BARE_ISO_DAY.test(value)) return parseIsoDate(value);
201
205
  const d = value instanceof Date ? value : new Date(value);
202
206
  return Number.isNaN(d.getTime()) ? null : d;
203
207
  }
@@ -229,6 +233,13 @@ export function formatDate(
229
233
  const d = toDate(value);
230
234
  if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
231
235
  const base = typeof style === "string" ? DATE_STYLES[style] : style;
236
+ // A bare "YYYY-MM-DD" is a calendar day, which has no zone: it is the day it names
237
+ // wherever it is read, so a `timeZone` (in `options` or the style) never moves it — the
238
+ // device's local midnight shown in a zone west of it was the day before (round 0.45, N9).
239
+ if (typeof value === "string" && BARE_ISO_DAY.test(value)) {
240
+ const day = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
241
+ return dateFormatter(options.locale, { ...base, timeZone: "UTC" }).format(day);
242
+ }
232
243
  return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);
233
244
  }
234
245
 
@@ -0,0 +1,9 @@
1
+ // The English words of ./pwa-update-prompt, in a module with no component code (0.34, KS U8).
2
+ // keksdose's reviewed words (pwa.updateAvailable, pwa.reload) are the canon.
3
+
4
+ import type { PwaUpdatePromptLabels } from "./pwa-update-prompt";
5
+
6
+ export const DEFAULT_PWA_UPDATE_PROMPT_LABELS: PwaUpdatePromptLabels = {
7
+ updateAvailable: "A new version is available.",
8
+ reload: "Reload",
9
+ };
@@ -0,0 +1,72 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
3
+ import { toast } from "../components/toast";
4
+ import { DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "./pwa-update-prompt-labels";
5
+
6
+ /**
7
+ * "A new version is available. — Reload": keksdose's service-worker toast
8
+ * (app/pwa-update-prompt.tsx) as a kit component (docs/local-first-harmonization.md §1.5
9
+ * D8). The app keeps `useRegisterSW` (vite-plugin-pwa, `registerType: "prompt"`) and hands
10
+ * its state in: `needRefresh` from it, `onReload` = `updateServiceWorker(true)`,
11
+ * `onDismiss` = `setNeedRefresh(false)`.
12
+ *
13
+ * `hold` defers the prompt while a reload would hurt — a handover being signed, a tour
14
+ * running. Nothing shows while it holds; the toast appears once it drops. A hold that
15
+ * starts while the toast is up takes it down again until it drops.
16
+ *
17
+ * Renders nothing itself: the toast needs a mounted `<Toaster>`.
18
+ */
19
+ export interface PwaUpdatePromptLabels {
20
+ updateAvailable: string;
21
+ reload: string;
22
+ }
23
+
24
+ // Defined in ./pwa-update-prompt-labels, which holds no component code (0.34, KS U8).
25
+ export { DEFAULT_PWA_UPDATE_PROMPT_LABELS };
26
+
27
+ export interface PwaUpdatePromptProps {
28
+ /** A new service worker is waiting. */
29
+ needRefresh: boolean;
30
+ /** Activates it and reloads — `updateServiceWorker(true)`. */
31
+ onReload: () => void;
32
+ /** The toast was closed without reloading. */
33
+ onDismiss?: () => void;
34
+ /** Wait: a handover is open, a tour runs. */
35
+ hold?: boolean;
36
+ labels?: LabelOverride<PwaUpdatePromptLabels>;
37
+ }
38
+
39
+ /** The toast's id: one prompt at a time, replaced in place. */
40
+ const PWA_UPDATE_TOAST_ID = "pwa-update";
41
+
42
+ export function PwaUpdatePrompt({ needRefresh, onReload, onDismiss, hold = false, labels: labelsProp }: PwaUpdatePromptProps) {
43
+ const labels = useKitLabels("pwaUpdate", DEFAULT_PWA_UPDATE_PROMPT_LABELS, labelsProp);
44
+ // The callbacks are read at click time, so a new function each render does not
45
+ // re-raise the toast.
46
+ const handlers = useRef({ onReload, onDismiss });
47
+ useEffect(() => {
48
+ handlers.current = { onReload, onDismiss };
49
+ });
50
+
51
+ const show = needRefresh && !hold;
52
+ useEffect(() => {
53
+ if (!show) return;
54
+ // sonner calls `onDismiss` for a programmatic dismiss too: the one this effect makes
55
+ // when a hold starts must not tell the app the person waved the update away.
56
+ let ours = false;
57
+ toast(labels.updateAvailable, {
58
+ id: PWA_UPDATE_TOAST_ID,
59
+ duration: Infinity,
60
+ action: { label: labels.reload, onClick: () => handlers.current.onReload() },
61
+ onDismiss: () => {
62
+ if (!ours) handlers.current.onDismiss?.();
63
+ },
64
+ });
65
+ return () => {
66
+ ours = true;
67
+ toast.dismiss(PWA_UPDATE_TOAST_ID);
68
+ };
69
+ }, [show, labels.updateAvailable, labels.reload]);
70
+
71
+ return null;
72
+ }