@eifi1/ui-kit 0.48.1 → 0.49.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 (318) hide show
  1. package/README.md +28 -13
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +11 -5
  7. package/dist/admin/user-roster.js +15 -4
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.js +2 -2
  16. package/dist/auth/verify-email.js.map +1 -1
  17. package/dist/billing/plan-card.js +2 -2
  18. package/dist/billing/plan-card.js.map +1 -1
  19. package/dist/components/breadcrumbs.d.ts +19 -1
  20. package/dist/components/breadcrumbs.js +11 -4
  21. package/dist/components/breadcrumbs.js.map +1 -1
  22. package/dist/components/button-look.d.ts +37 -7
  23. package/dist/components/button-look.js +33 -6
  24. package/dist/components/button-look.js.map +1 -1
  25. package/dist/components/button.d.ts +4 -1
  26. package/dist/components/button.js +2 -2
  27. package/dist/components/button.js.map +1 -1
  28. package/dist/components/calendar-heatmap.js +2 -1
  29. package/dist/components/calendar-heatmap.js.map +1 -1
  30. package/dist/components/chart-edge.d.ts +10 -0
  31. package/dist/components/chart-edge.js +57 -0
  32. package/dist/components/chart-edge.js.map +1 -0
  33. package/dist/components/chart.js +2 -1
  34. package/dist/components/chart.js.map +1 -1
  35. package/dist/components/chip.d.ts +8 -1
  36. package/dist/components/chip.js +4 -1
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/confirm-context.d.ts +5 -0
  39. package/dist/components/confirm-context.js +7 -0
  40. package/dist/components/confirm-context.js.map +1 -0
  41. package/dist/components/confirm-dialog.js +3 -3
  42. package/dist/components/confirm-dialog.js.map +1 -1
  43. package/dist/components/danger-confirm.d.ts +2 -2
  44. package/dist/components/danger-confirm.js +1 -1
  45. package/dist/components/danger-confirm.js.map +1 -1
  46. package/dist/components/data-table-fit.d.ts +8 -1
  47. package/dist/components/data-table-fit.js +28 -4
  48. package/dist/components/data-table-fit.js.map +1 -1
  49. package/dist/components/data-table-groups.d.ts +3 -1
  50. package/dist/components/data-table-groups.js +11 -5
  51. package/dist/components/data-table-groups.js.map +1 -1
  52. package/dist/components/data-table-head.d.ts +8 -1
  53. package/dist/components/data-table-head.js +11 -3
  54. package/dist/components/data-table-head.js.map +1 -1
  55. package/dist/components/data-table-leading-row.d.ts +3 -1
  56. package/dist/components/data-table-leading-row.js +2 -1
  57. package/dist/components/data-table-leading-row.js.map +1 -1
  58. package/dist/components/data-table-mobile.d.ts +48 -2
  59. package/dist/components/data-table-mobile.js +87 -10
  60. package/dist/components/data-table-mobile.js.map +1 -1
  61. package/dist/components/data-table-row-actions.d.ts +2 -1
  62. package/dist/components/data-table-row-actions.js +9 -2
  63. package/dist/components/data-table-row-actions.js.map +1 -1
  64. package/dist/components/data-table-row.d.ts +84 -0
  65. package/dist/components/data-table-row.js +261 -0
  66. package/dist/components/data-table-row.js.map +1 -0
  67. package/dist/components/data-table-selection.d.ts +3 -0
  68. package/dist/components/data-table-selection.js +12 -1
  69. package/dist/components/data-table-selection.js.map +1 -1
  70. package/dist/components/data-table-virtual.d.ts +98 -0
  71. package/dist/components/data-table-virtual.js +369 -0
  72. package/dist/components/data-table-virtual.js.map +1 -0
  73. package/dist/components/data-table.d.ts +74 -2
  74. package/dist/components/data-table.js +241 -221
  75. package/dist/components/data-table.js.map +1 -1
  76. package/dist/components/error-boundary.js +2 -2
  77. package/dist/components/error-boundary.js.map +1 -1
  78. package/dist/components/floating-panel.d.ts +36 -13
  79. package/dist/components/floating-panel.js +27 -7
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +1 -1
  82. package/dist/components/form-actions.js +1 -1
  83. package/dist/components/form-actions.js.map +1 -1
  84. package/dist/components/icon-button.d.ts +11 -0
  85. package/dist/components/icon-button.js +7 -5
  86. package/dist/components/icon-button.js.map +1 -1
  87. package/dist/components/nav-pills.d.ts +8 -1
  88. package/dist/components/nav-pills.js +61 -38
  89. package/dist/components/nav-pills.js.map +1 -1
  90. package/dist/components/page-header-context.d.ts +8 -0
  91. package/dist/components/page-header-context.js +7 -0
  92. package/dist/components/page-header-context.js.map +1 -0
  93. package/dist/components/page-header.d.ts +14 -2
  94. package/dist/components/page-header.js +10 -1
  95. package/dist/components/page-header.js.map +1 -1
  96. package/dist/components/pie-chart.js +2 -1
  97. package/dist/components/pie-chart.js.map +1 -1
  98. package/dist/components/progress-bar.js +5 -4
  99. package/dist/components/progress-bar.js.map +1 -1
  100. package/dist/components/reauth-dialog.js +1 -1
  101. package/dist/components/reauth-dialog.js.map +1 -1
  102. package/dist/components/row-actions-labels.js +3 -1
  103. package/dist/components/row-actions-labels.js.map +1 -1
  104. package/dist/components/row-actions.d.ts +43 -9
  105. package/dist/components/row-actions.js +78 -15
  106. package/dist/components/row-actions.js.map +1 -1
  107. package/dist/components/section-nav.d.ts +37 -0
  108. package/dist/components/section-nav.js +33 -0
  109. package/dist/components/section-nav.js.map +1 -0
  110. package/dist/components/series-chart.js +8 -3
  111. package/dist/components/series-chart.js.map +1 -1
  112. package/dist/components/share-card.js +1 -1
  113. package/dist/components/share-card.js.map +1 -1
  114. package/dist/components/signature-pad.js +1 -1
  115. package/dist/components/signature-pad.js.map +1 -1
  116. package/dist/components/sparkline.js +2 -1
  117. package/dist/components/sparkline.js.map +1 -1
  118. package/dist/components/stat-tile.js +14 -7
  119. package/dist/components/stat-tile.js.map +1 -1
  120. package/dist/components/status-dot.d.ts +5 -1
  121. package/dist/components/status-dot.js +4 -1
  122. package/dist/components/status-dot.js.map +1 -1
  123. package/dist/components/tooltip.js +1 -1
  124. package/dist/components/tooltip.js.map +1 -1
  125. package/dist/components/translation-review-editor.js +2 -2
  126. package/dist/components/translation-review-editor.js.map +1 -1
  127. package/dist/components/translation-review.js +2 -2
  128. package/dist/components/translation-review.js.map +1 -1
  129. package/dist/components/treemap.d.ts +5 -2
  130. package/dist/components/treemap.js +26 -6
  131. package/dist/components/treemap.js.map +1 -1
  132. package/dist/components/use-table-state.js +37 -6
  133. package/dist/components/use-table-state.js.map +1 -1
  134. package/dist/data-table.d.ts +1 -0
  135. package/dist/data-table.js.map +1 -1
  136. package/dist/demo/demo-banner.js +1 -1
  137. package/dist/demo/demo-banner.js.map +1 -1
  138. package/dist/demo/demo-ended.js +1 -1
  139. package/dist/demo/demo-ended.js.map +1 -1
  140. package/dist/demo/demo-start.js +1 -1
  141. package/dist/demo/demo-start.js.map +1 -1
  142. package/dist/eslint/index.js +1 -1
  143. package/dist/feedback/feedback-inbox.js +1 -1
  144. package/dist/feedback/feedback-inbox.js.map +1 -1
  145. package/dist/hooks/use-anchored-panel.js +2 -1
  146. package/dist/hooks/use-anchored-panel.js.map +1 -1
  147. package/dist/hooks/use-dismiss.d.ts +9 -1
  148. package/dist/hooks/use-dismiss.js +25 -5
  149. package/dist/hooks/use-dismiss.js.map +1 -1
  150. package/dist/hooks/use-document-title.d.ts +14 -0
  151. package/dist/hooks/use-document-title.js +22 -0
  152. package/dist/hooks/use-document-title.js.map +1 -0
  153. package/dist/i18n/german.js +6 -2
  154. package/dist/i18n/german.js.map +1 -1
  155. package/dist/i18n/kit-labels.d.ts +31 -1
  156. package/dist/i18n/kit-labels.js +43 -2
  157. package/dist/i18n/kit-labels.js.map +1 -1
  158. package/dist/i18n/locales/en.js +6 -2
  159. package/dist/i18n/locales/en.js.map +2 -2
  160. package/dist/i18n/locales/es.js +6 -2
  161. package/dist/i18n/locales/es.js.map +1 -1
  162. package/dist/i18n/locales/fr.js +6 -2
  163. package/dist/i18n/locales/fr.js.map +1 -1
  164. package/dist/i18n/locales/hu.js +6 -2
  165. package/dist/i18n/locales/hu.js.map +1 -1
  166. package/dist/i18n/locales/it.js +6 -2
  167. package/dist/i18n/locales/it.js.map +1 -1
  168. package/dist/i18n/locales/zh.js +6 -2
  169. package/dist/i18n/locales/zh.js.map +1 -1
  170. package/dist/i18n/review.js +2 -0
  171. package/dist/i18n/review.js.map +1 -1
  172. package/dist/i18n/shared-labels.js +1 -0
  173. package/dist/i18n/shared-labels.js.map +1 -1
  174. package/dist/index.d.ts +8 -1
  175. package/dist/index.js +11 -0
  176. package/dist/index.js.map +1 -1
  177. package/dist/landing/landing-actions.js +2 -2
  178. package/dist/landing/landing-actions.js.map +1 -1
  179. package/dist/landing/public-header.js +2 -2
  180. package/dist/landing/public-header.js.map +1 -1
  181. package/dist/lib/clipboard-minus.d.ts +27 -0
  182. package/dist/lib/clipboard-minus.js +63 -0
  183. package/dist/lib/clipboard-minus.js.map +1 -0
  184. package/dist/lib/document-title.d.ts +17 -0
  185. package/dist/lib/document-title.js +29 -0
  186. package/dist/lib/document-title.js.map +1 -0
  187. package/dist/lib/float-clearance.js +8 -2
  188. package/dist/lib/float-clearance.js.map +1 -1
  189. package/dist/lib/format.d.ts +35 -10
  190. package/dist/lib/format.js +55 -11
  191. package/dist/lib/format.js.map +1 -1
  192. package/dist/lib/minus.d.ts +22 -0
  193. package/dist/lib/minus.js +14 -0
  194. package/dist/lib/minus.js.map +1 -0
  195. package/dist/lib/status-tone.d.ts +30 -0
  196. package/dist/lib/status-tone.js +17 -0
  197. package/dist/lib/status-tone.js.map +1 -0
  198. package/dist/settings/settings-layout.js +3 -2
  199. package/dist/settings/settings-layout.js.map +1 -1
  200. package/dist/shell/account-menu-language.d.ts +1 -1
  201. package/dist/shell/account-menu-language.js +9 -1
  202. package/dist/shell/account-menu-language.js.map +1 -1
  203. package/dist/shell/app-shell-labels.js +2 -1
  204. package/dist/shell/app-shell-labels.js.map +1 -1
  205. package/dist/shell/app-shell.d.ts +58 -16
  206. package/dist/shell/app-shell.js +158 -44
  207. package/dist/shell/app-shell.js.map +1 -1
  208. package/dist/shell/language-code.d.ts +3 -0
  209. package/dist/shell/language-code.js +8 -0
  210. package/dist/shell/language-code.js.map +1 -0
  211. package/dist/shell/topbar-controls.d.ts +14 -6
  212. package/dist/shell/topbar-controls.js +14 -17
  213. package/dist/shell/topbar-controls.js.map +1 -1
  214. package/dist/testing/css-variables.generated.d.ts +1 -1
  215. package/dist/testing/css-variables.generated.js +1 -1
  216. package/dist/testing/css-variables.generated.js.map +1 -1
  217. package/dist/theme/chart-palette.d.ts +82 -15
  218. package/dist/theme/chart-palette.js +51 -24
  219. package/dist/theme/chart-palette.js.map +1 -1
  220. package/dist/theme/palette-presets.d.ts +9 -0
  221. package/dist/theme/palette-presets.js +77 -62
  222. package/dist/theme/palette-presets.js.map +1 -1
  223. package/dist/tour/tour.js +1 -1
  224. package/dist/tour/tour.js.map +1 -1
  225. package/dist/wizard/stepper-nav.d.ts +1 -1
  226. package/dist/wizard/stepper-nav.js +3 -3
  227. package/dist/wizard/stepper-nav.js.map +1 -1
  228. package/package.json +1 -1
  229. package/src/admin/admin-action-confirm.tsx +1 -1
  230. package/src/admin/invitations-panel.tsx +7 -3
  231. package/src/admin/user-roster.tsx +23 -6
  232. package/src/auth/accept-invitation.tsx +3 -3
  233. package/src/auth/not-found-page.tsx +1 -1
  234. package/src/auth/reset-password-form.tsx +3 -3
  235. package/src/auth/verify-email.tsx +3 -3
  236. package/src/billing/plan-card.tsx +2 -2
  237. package/src/components/breadcrumbs.tsx +32 -4
  238. package/src/components/button-look.ts +86 -15
  239. package/src/components/button.tsx +6 -3
  240. package/src/components/calendar-heatmap.tsx +2 -1
  241. package/src/components/chart-edge.ts +73 -0
  242. package/src/components/chart.tsx +3 -1
  243. package/src/components/chip.tsx +10 -1
  244. package/src/components/confirm-context.ts +7 -0
  245. package/src/components/confirm-dialog.tsx +3 -3
  246. package/src/components/danger-confirm.tsx +3 -3
  247. package/src/components/data-table-fit.ts +58 -8
  248. package/src/components/data-table-groups.tsx +15 -4
  249. package/src/components/data-table-head.tsx +25 -2
  250. package/src/components/data-table-leading-row.tsx +4 -1
  251. package/src/components/data-table-mobile.tsx +162 -11
  252. package/src/components/data-table-row-actions.tsx +12 -3
  253. package/src/components/data-table-row.tsx +485 -0
  254. package/src/components/data-table-selection.ts +17 -1
  255. package/src/components/data-table-virtual.ts +547 -0
  256. package/src/components/data-table.tsx +367 -326
  257. package/src/components/error-boundary.tsx +2 -2
  258. package/src/components/floating-panel.tsx +67 -16
  259. package/src/components/form-actions.tsx +2 -2
  260. package/src/components/icon-button.tsx +21 -8
  261. package/src/components/nav-pills.tsx +29 -2
  262. package/src/components/page-header-context.ts +8 -0
  263. package/src/components/page-header.tsx +34 -2
  264. package/src/components/pie-chart.tsx +2 -1
  265. package/src/components/progress-bar.tsx +6 -3
  266. package/src/components/reauth-dialog.tsx +1 -1
  267. package/src/components/row-actions-labels.ts +2 -0
  268. package/src/components/row-actions.tsx +154 -33
  269. package/src/components/section-nav.tsx +65 -0
  270. package/src/components/series-chart.tsx +12 -3
  271. package/src/components/share-card.tsx +1 -1
  272. package/src/components/signature-pad.tsx +1 -1
  273. package/src/components/sparkline.tsx +2 -1
  274. package/src/components/stat-tile.tsx +19 -7
  275. package/src/components/status-dot.tsx +7 -1
  276. package/src/components/tooltip.tsx +3 -1
  277. package/src/components/translation-review-editor.tsx +2 -2
  278. package/src/components/translation-review.tsx +2 -2
  279. package/src/components/treemap.tsx +34 -8
  280. package/src/components/use-table-state.ts +63 -8
  281. package/src/data-table.ts +5 -3
  282. package/src/demo/demo-banner.tsx +1 -1
  283. package/src/demo/demo-ended.tsx +1 -1
  284. package/src/demo/demo-start.tsx +1 -1
  285. package/src/feedback/feedback-inbox.tsx +1 -1
  286. package/src/hooks/use-anchored-panel.ts +9 -1
  287. package/src/hooks/use-dismiss.ts +43 -4
  288. package/src/hooks/use-document-title.ts +31 -0
  289. package/src/i18n/german.ts +4 -0
  290. package/src/i18n/kit-labels.tsx +76 -1
  291. package/src/i18n/locales/es.ts +4 -0
  292. package/src/i18n/locales/fr.ts +4 -0
  293. package/src/i18n/locales/hu.ts +4 -0
  294. package/src/i18n/locales/it.ts +4 -0
  295. package/src/i18n/locales/zh.ts +4 -0
  296. package/src/i18n/review.ts +2 -0
  297. package/src/i18n/shared-labels.ts +1 -0
  298. package/src/index.ts +13 -0
  299. package/src/landing/landing-actions.tsx +2 -2
  300. package/src/landing/public-header.tsx +2 -2
  301. package/src/lib/clipboard-minus.ts +94 -0
  302. package/src/lib/document-title.ts +41 -0
  303. package/src/lib/float-clearance.ts +19 -3
  304. package/src/lib/format.ts +115 -30
  305. package/src/lib/minus.ts +40 -0
  306. package/src/lib/status-tone.ts +43 -0
  307. package/src/settings/settings-layout.tsx +3 -2
  308. package/src/shell/account-menu-language.tsx +14 -2
  309. package/src/shell/app-shell-labels.ts +1 -0
  310. package/src/shell/app-shell.tsx +298 -69
  311. package/src/shell/language-code.ts +5 -0
  312. package/src/shell/topbar-controls.tsx +35 -15
  313. package/src/testing/css-variables.generated.ts +6 -6
  314. package/src/theme/chart-palette.ts +102 -20
  315. package/src/theme/palette-presets.ts +121 -84
  316. package/src/tour/tour.tsx +1 -1
  317. package/src/wizard/stepper-nav.tsx +4 -4
  318. package/tokens.css +28 -18
@@ -546,6 +546,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
546
546
  theme: "Cambia tema",
547
547
  palette: "Preset di aspetto",
548
548
  language: "Lingua",
549
+ languageCurrent: (name) => `Scegli la lingua, attuale: ${name}`,
549
550
  switchRole: "Cambia ruolo",
550
551
  role: (value) => `Ruolo: ${value}`,
551
552
  },
@@ -1888,10 +1889,13 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1888
1889
  rowActions: {
1889
1890
  actions: "Azioni",
1890
1891
  actionsFor: (name) => `Azioni per ${name}`,
1892
+ confirmTitle: (action, name) => (name ? `${action} «${name}»?` : `${action}?`),
1893
+ confirmBody: "Questa azione non può essere annullata.",
1891
1894
  },
1892
1895
  appShellMore: {
1893
1896
  more: "Altro",
1894
1897
  moreTitle: "Altre pagine",
1898
+ overview: "Panoramica",
1895
1899
  },
1896
1900
  // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words. "Lei", as
1897
1901
  // the rest of the catalogue.
@@ -529,6 +529,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
529
529
  theme: "切换主题",
530
530
  palette: "外观预设",
531
531
  language: "语言",
532
+ languageCurrent: (name) => `选择语言,当前:${name}`,
532
533
  switchRole: "切换角色",
533
534
  role: (value) => `角色:${value}`,
534
535
  },
@@ -1795,10 +1796,13 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1795
1796
  rowActions: {
1796
1797
  actions: "操作",
1797
1798
  actionsFor: (name) => `${name} 的操作`,
1799
+ confirmTitle: (action, name) => (name ? `${action}“${name}”?` : `${action}?`),
1800
+ confirmBody: "此操作无法撤销。",
1798
1801
  },
1799
1802
  appShellMore: {
1800
1803
  more: "更多",
1801
1804
  moreTitle: "更多页面",
1805
+ overview: "概览",
1802
1806
  },
1803
1807
  // 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words.
1804
1808
  mailTestCard: {
@@ -153,6 +153,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
153
153
  "tabs.remove": [["{{tab}}"]],
154
154
  "appShell.toggleGroup": [["{{group}}"]],
155
155
  "topBar.role": [["{{role}}"]],
156
+ "topBar.languageCurrent": [["{{name}}"]],
156
157
  "file.size": [[2_500_000]],
157
158
  "wizard.step": [[2, 4]],
158
159
  "tour.step": [[2, 4]],
@@ -279,6 +280,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
279
280
  "signChip.direction": [["{{current}}", "{{next}}"]],
280
281
  "signChip.switchTo": [["{{next}}"]],
281
282
  "rowActions.actionsFor": [["{{name}}"]],
283
+ "rowActions.confirmTitle": [["{{action}}", "{{name}}"], ["{{action}}"]],
282
284
  "columnMapper.readError": [["{{name}}"]],
283
285
  "columnMapper.summary": [
284
286
  [1, 1],
@@ -106,6 +106,7 @@ export const DEFAULT_TOP_BAR_LABELS: TopBarLabels = {
106
106
  theme: "Toggle theme",
107
107
  palette: "Appearance preset",
108
108
  language: "Language",
109
+ languageCurrent: (name) => `Choose language, current: ${name}`,
109
110
  switchRole: "Switch role",
110
111
  role: (value) => `Role: ${value}`,
111
112
  };
package/src/index.ts CHANGED
@@ -154,6 +154,12 @@ export { useCloseTransition, OVERLAY_EXIT_MS } from "./hooks/use-close-transitio
154
154
  export { useRowSwipe } from "./hooks/use-row-swipe";
155
155
  // 0.28: keksdose's noindex marker, for the legal and auth pages of every app.
156
156
  export { useNoIndex } from "./hooks/use-noindex";
157
+ // 0.49 KIT-9: the window's title from page, section and app, in the provider's format.
158
+ export { useDocumentTitle } from "./hooks/use-document-title";
159
+ // 0.49 KIT-1b: U+2212 before a number as "-", the copy handler's rule, for an app's text.
160
+ export { asciiMinusInNumbers } from "./lib/clipboard-minus";
161
+ export { DEFAULT_DOCUMENT_TITLE_FORMAT, formatDocumentTitle } from "./lib/document-title";
162
+ export type { DocumentTitleParts } from "./lib/document-title";
157
163
  export type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from "./hooks/use-row-swipe";
158
164
  // Any element as a file drop target, screened like FileButton (kastlan).
159
165
  export { useFileDrop, dragHasFiles } from "./hooks/use-file-drop";
@@ -381,10 +387,15 @@ export * from "./components/alert-banner";
381
387
  export * from "./components/toggle-group";
382
388
  // A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.
383
389
  export * from "./components/nav-pills";
390
+ // 0.49.0 (KIT-6): a section's pages on the phone, one scrolling row of pills.
391
+ export * from "./components/section-nav";
384
392
  // A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is
385
393
  // given — and the list field built from it. Distinct from Button on purpose: a row of
386
394
  // buttons reads as "choose an action", a row of chips as "here are the things".
387
395
  export * from "./components/chip";
396
+ // 0.49.0 (KIT-8): five domain-free status kinds and their tones — Chip/StatusDot `status`.
397
+ export { statusTone, STATUS_KINDS } from "./lib/status-tone";
398
+ export type { StatusKind, StatusTone } from "./lib/status-tone";
388
399
  // ChipInput's own module since MOD-9: it renders Chip, so chip.tsx re-exports only its types.
389
400
  export * from "./components/chip-input";
390
401
  export * from "./components/wizard-stepper";
@@ -589,6 +600,8 @@ export type {
589
600
  RowActionsColumnOptions,
590
601
  RowActionsHeadProps,
591
602
  RowActionTone,
603
+ // 0.49 KIT-12: a danger action's confirmation words.
604
+ RowActionConfirm,
592
605
  } from "./components/row-actions";
593
606
  // 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits).
594
607
  export * from "./components/checkbox-group";
@@ -77,12 +77,12 @@ export function LandingActions({
77
77
  className={cn("flex flex-col gap-3 sm:flex-row", align === "center" && "sm:justify-center", className)}
78
78
  >
79
79
  {session === "user" ? (
80
- <Button href={openAppHref} variant="brand" size={buttonSize} className={extra}>
80
+ <Button href={openAppHref} variant="accent" size={buttonSize} className={extra}>
81
81
  {labels.openApp}
82
82
  </Button>
83
83
  ) : (
84
84
  <>
85
- <Button href={accessLink.href} variant="brand" size={buttonSize} className={extra}>
85
+ <Button href={accessLink.href} variant="accent" size={buttonSize} className={extra}>
86
86
  {accessLink.label}
87
87
  </Button>
88
88
  {demoHref !== null && (
@@ -82,7 +82,7 @@ export function PublicHeader({
82
82
 
83
83
  const actions =
84
84
  session === "user" ? (
85
- <Button href={openAppHref} variant="brand" size={size}>
85
+ <Button href={openAppHref} variant="accent" size={size}>
86
86
  {labels.openApp}
87
87
  </Button>
88
88
  ) : (
@@ -96,7 +96,7 @@ export function PublicHeader({
96
96
  {labels.signIn}
97
97
  </Button>
98
98
  )}
99
- <Button href={accessLink.href} variant="brand" size={size}>
99
+ <Button href={accessLink.href} variant="accent" size={size}>
100
100
  {accessLink.label}
101
101
  </Button>
102
102
  </>
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The copy handler for the one minus sign (0.49 KIT-1b, Marcel's follow-up to KIT-1).
3
+ *
4
+ * The kit prints a negative with U+2212 (lib/minus.ts). A spreadsheet does not read that
5
+ * as a number: a figure copied out of a kit table and pasted into Excel landed as text.
6
+ * So while a selection inside the page is copied, every U+2212 standing directly before a
7
+ * number — optionally after a currency code or symbol and spaces: "−CHF 1,250.50",
8
+ * "−€2,000", "−12%" — becomes the ASCII "-" in the clipboard, in `text/plain` AND in
9
+ * `text/html` (Excel pastes the HTML flavour, as cells). The page keeps "−".
10
+ *
11
+ * A copy with no such minus is left to the browser untouched, and so is any copy out of an
12
+ * input, a textarea or an editable element: what the person typed is theirs.
13
+ *
14
+ * Installed once per document by `UiKitProvider` (`copyAsciiMinus`, default `true`) —
15
+ * the one part every app mounts, the signed-out pages included.
16
+ */
17
+
18
+ /** U+2212, then (optionally) a currency code or symbol, then spaces, then a digit. */
19
+ const MINUS_BEFORE_NUMBER = /\u2212(?=(?:[A-Z]{3}|\p{Sc})?[\s\u00a0\u202f]*\d)/gu;
20
+
21
+ /**
22
+ * `text` with each U+2212 that stands before a number (in the sense above) as the ASCII
23
+ * hyphen-minus, every other character as it was — for text an app hands a machine that
24
+ * did not come from a formatter's `minus: "ascii"`.
25
+ */
26
+ export function asciiMinusInNumbers(text: string): string {
27
+ return text.replace(MINUS_BEFORE_NUMBER, "-");
28
+ }
29
+
30
+ function hasMinusBeforeNumber(text: string): boolean {
31
+ MINUS_BEFORE_NUMBER.lastIndex = 0;
32
+ const found = MINUS_BEFORE_NUMBER.test(text);
33
+ MINUS_BEFORE_NUMBER.lastIndex = 0;
34
+ return found;
35
+ }
36
+
37
+ /** An input, a textarea or an editable element: a copy there is the person's own text. */
38
+ function isEditable(node: Node | null | undefined): boolean {
39
+ const el = node instanceof Element ? node : node?.parentElement;
40
+ if (!el) return false;
41
+ if (el.closest("input, textarea, select")) return true;
42
+ return (el as HTMLElement).isContentEditable === true || el.closest('[contenteditable=""], [contenteditable="true"]') !== null;
43
+ }
44
+
45
+ /** Table parts copied alone are wrapped back into a table, which Excel reads as cells. */
46
+ function wrapTableParts(container: HTMLElement): string {
47
+ const first = container.firstElementChild?.tagName;
48
+ const html = container.innerHTML;
49
+ if (first === "TD" || first === "TH") return `<table><tbody><tr>${html}</tr></tbody></table>`;
50
+ if (first === "TR") return `<table><tbody>${html}</tbody></table>`;
51
+ if (first === "TBODY" || first === "THEAD" || first === "TFOOT") return `<table>${html}</table>`;
52
+ return html;
53
+ }
54
+
55
+ function onCopy(event: ClipboardEvent): void {
56
+ if (event.defaultPrevented || !event.clipboardData) return;
57
+ const doc = (event.target as Node | null)?.ownerDocument ?? document;
58
+ if (isEditable(event.target as Node | null) || isEditable(doc.activeElement)) return;
59
+ const selection = doc.getSelection();
60
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
61
+ if (isEditable(selection.anchorNode) || isEditable(selection.focusNode)) return;
62
+ const text = selection.toString();
63
+ if (!hasMinusBeforeNumber(text)) return;
64
+
65
+ const container = doc.createElement("div");
66
+ for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents());
67
+ const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);
68
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
69
+ node.nodeValue = asciiMinusInNumbers(node.nodeValue ?? "");
70
+ }
71
+ event.clipboardData.setData("text/plain", asciiMinusInNumbers(text));
72
+ event.clipboardData.setData("text/html", wrapTableParts(container));
73
+ event.preventDefault();
74
+ }
75
+
76
+ const installs = new WeakMap<Document, number>();
77
+
78
+ /**
79
+ * Listen for copies on `doc` until the returned function is called. Counted: nested
80
+ * providers install it once, and the last one to go removes it.
81
+ */
82
+ export function installClipboardMinus(doc: Document = document): () => void {
83
+ const count = installs.get(doc) ?? 0;
84
+ if (count === 0) doc.addEventListener("copy", onCopy);
85
+ installs.set(doc, count + 1);
86
+ let done = false;
87
+ return () => {
88
+ if (done) return;
89
+ done = true;
90
+ const left = (installs.get(doc) ?? 1) - 1;
91
+ installs.set(doc, left);
92
+ if (left === 0) doc.removeEventListener("copy", onCopy);
93
+ };
94
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The window's title from its parts (0.49 KIT-9): `{page}`, `{section}` and `{app}` in a
3
+ * format, by default "{page} · {section} · {app}" — "Invoices · Finance · Kastlan".
4
+ *
5
+ * A part that is missing (undefined, null, empty or blank) is dropped WITH the separator
6
+ * before it, so "Invoices · Kastlan" never reads "Invoices · · Kastlan". The text between
7
+ * two parts is the separator; text before the first part and after the last is kept as
8
+ * written ("[{page} – {app}]" → "[KS]"). No part at all gives "".
9
+ */
10
+ export const DEFAULT_DOCUMENT_TITLE_FORMAT = "{page} · {section} · {app}";
11
+
12
+ /** The parts {@link formatDocumentTitle} fills in. */
13
+ export interface DocumentTitleParts {
14
+ page?: string | null;
15
+ section?: string | null;
16
+ app?: string | null;
17
+ }
18
+
19
+ const PLACEHOLDER = /\{(page|section|app)\}/g;
20
+
21
+ export function formatDocumentTitle(format: string, parts: DocumentTitleParts): string {
22
+ let prefix = "";
23
+ let out = "";
24
+ let pending = "";
25
+ let last = 0;
26
+ let seen = false;
27
+ for (const m of format.matchAll(PLACEHOLDER)) {
28
+ const literal = format.slice(last, m.index!);
29
+ last = m.index! + m[0].length;
30
+ if (!seen) prefix = literal;
31
+ else pending = out === "" ? "" : literal || pending;
32
+ seen = true;
33
+ const value = parts[m[1] as keyof DocumentTitleParts]?.trim();
34
+ if (!value) continue;
35
+ out += (out === "" ? "" : pending) + value;
36
+ pending = "";
37
+ }
38
+ if (!seen) return format;
39
+ if (out === "") return "";
40
+ return prefix + out + format.slice(last);
41
+ }
@@ -2,7 +2,8 @@ import { useEffect } from "react";
2
2
 
3
3
  /**
4
4
  * The room the floating launchers take above the bottom nav, published as
5
- * `--app-float-clearance` on `<html>` so `AppShell` can pad `<main>` past them.
5
+ * `--app-float-clearance` on `<html>` so `AppShell` can pad `<main>` past them, and with
6
+ * the nav included as `--kit-floating-inset` (0.49 KIT-11, below).
6
7
  *
7
8
  * keksdose dev#612 (UX-1.3, "the help button covers content"): the shell padded `<main>`
8
9
  * for the nav plus 1.5rem, so on a phone the last row of every page ended under the help
@@ -23,12 +24,27 @@ import { useEffect } from "react";
23
24
 
24
25
  const claims = new Map<symbol, string>();
25
26
 
27
+ /**
28
+ * 0.49 KIT-11: `--kit-floating-inset` is the same room measured from the screen's bottom
29
+ * edge — the phone's bottom nav (`--app-nav-h`, 0px from `md` up) or the home indicator,
30
+ * whichever is taller, plus `--app-float-clearance` — so a page's own fixed or sticky
31
+ * footer, or its last row, can clear the launchers with one length at every width.
32
+ * Unset while no launcher is mounted, like `--app-float-clearance`: write
33
+ * `var(--kit-floating-inset, var(--app-nav-h, 0px))`.
34
+ */
35
+ const NAV_OR_HOME_INDICATOR = "max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px))";
36
+
26
37
  function publish() {
27
38
  if (typeof document === "undefined") return;
28
39
  const root = document.documentElement;
29
40
  const values = [...claims.values()];
30
- if (values.length === 0) root.style.removeProperty("--app-float-clearance");
31
- else root.style.setProperty("--app-float-clearance", values.length === 1 ? values[0] : `max(${values.join(", ")})`);
41
+ if (values.length === 0) {
42
+ root.style.removeProperty("--app-float-clearance");
43
+ root.style.removeProperty("--kit-floating-inset");
44
+ return;
45
+ }
46
+ root.style.setProperty("--app-float-clearance", values.length === 1 ? values[0] : `max(${values.join(", ")})`);
47
+ root.style.setProperty("--kit-floating-inset", `calc(${NAV_OR_HOME_INDICATOR} + var(--app-float-clearance))`);
32
48
  }
33
49
 
34
50
  /** Claim `value` (a CSS length: the launcher's top edge above the nav, plus a gap) until
package/src/lib/format.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { useMemo } from "react";
2
2
  import { useKitLocale, useKitTimeZone } from "../i18n/kit-labels";
3
3
  import { parseIsoDate } from "./dates";
4
+ import { formatWithMinus, type MinusStyle } from "./minus";
4
5
  import { roundHalfAway } from "./round";
5
6
 
7
+ export type { MinusStyle } from "./minus";
8
+
6
9
  /**
7
10
  * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,
8
11
  * for the figures an app prints OUTSIDE a kit component.
@@ -19,9 +22,18 @@ import { roundHalfAway } from "./round";
19
22
  * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the
20
23
  * kit components on it.
21
24
  *
22
- * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as
23
- * `empty` (default "—", the dash `StatTile` shows for no value), never as "NaN" or
24
- * "Invalid Date".
25
+ * A missing value — `null`, `undefined`, an unparseable date — formats as `empty`
26
+ * (default "—", the dash `StatTile` shows for no value), never as "NaN" or "Invalid
27
+ * Date". A number may also arrive as a numeric STRING (0.49, KIT-2) — "1850.00", "-12.5",
28
+ * "0": a Decimal field off a Python API — and is formatted like the number. Anything
29
+ * else (a non-numeric string, an object, `NaN`, ±`Infinity`) prints `empty` too, but is
30
+ * a bug upstream rather than a gap in the data, so a development build says so on the
31
+ * console, once per call site. Zero is a figure: `0` and `"0"` print "CHF 0.00", never
32
+ * the dash.
33
+ *
34
+ * A negative figure carries the typographic minus, U+2212 (0.49, KIT-1), in every
35
+ * locale — `Intl` writes a hyphen in most. `minus: "ascii"` for text a machine reads
36
+ * back (a CSV, the clipboard).
25
37
  */
26
38
 
27
39
  /** What every formatter here prints for a missing value, unless told otherwise. */
@@ -47,6 +59,14 @@ export interface FormatNumberBaseOptions extends BaseOptions {
47
59
  signDisplay?: Intl.NumberFormatOptions["signDisplay"];
48
60
  /** Any further `Intl.NumberFormat` options, merged over the ones above. */
49
61
  options?: Intl.NumberFormatOptions;
62
+ /**
63
+ * The minus sign of a negative figure (0.49, KIT-1). `"typographic"` (default):
64
+ * U+2212, as wide as "+" and the digits, as `SignedAmount` prints it — in every
65
+ * locale, also where `Intl` writes a hyphen. `"ascii"`: U+002D, for an export a
66
+ * machine parses back (a CSV download, the clipboard) — there, also pass
67
+ * `options: { useGrouping: false }` or write the raw number.
68
+ */
69
+ minus?: MinusStyle;
50
70
  }
51
71
 
52
72
  /** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */
@@ -115,44 +135,113 @@ function numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {
115
135
  };
116
136
  }
117
137
 
118
- function isMissing(value: number | null | undefined): value is null | undefined {
119
- // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing
120
- // figure, and "∞" printed in a money or measurement column reads like data.
121
- return value === null || value === undefined || !Number.isFinite(value);
138
+ /**
139
+ * What the number formatters take (0.49, KIT-2): a number, a numeric string ("1850.00",
140
+ * "-12.5", "1e3" — what a Decimal arrives as in JSON), or `null` / `undefined` for no
141
+ * value. A string is read with `Number()`, so a figure with more significant digits than
142
+ * a double holds (about 15–17) is printed as its nearest double.
143
+ */
144
+ export type FormattableNumber = number | string | null | undefined;
145
+
146
+ /** A plain decimal: optional sign, digits with an optional point, an optional exponent.
147
+ * No grouping marks, no decimal comma, no currency — those are formatted text, and
148
+ * reading them back is a locale's guess. */
149
+ const NUMERIC_STRING = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
150
+
151
+ /** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
152
+ * production one (the consumer's bundler makes `import.meta.env.DEV` false) and never
153
+ * under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */
154
+ function devWarnings(): boolean {
155
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
156
+ }
157
+
158
+ const warnedSites = new Set<string>();
159
+ /** This module's own frames, skipped to find the caller ("Object.formatMoney" is a
160
+ * `useKitFormat` wrapper). */
161
+ const OWN_FRAME = /\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\b/;
162
+
163
+ /** The first stack frame outside the formatters: where the bad value was passed. */
164
+ function callSite(): string {
165
+ const frames = (new Error().stack ?? "").split("\n").slice(1);
166
+ return frames.find((frame) => !OWN_FRAME.test(frame))?.trim() ?? "";
167
+ }
168
+
169
+ function describeValue(value: unknown): string {
170
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
171
+ try {
172
+ return JSON.stringify(value) ?? String(value);
173
+ } catch {
174
+ return Object.prototype.toString.call(value);
175
+ }
176
+ }
177
+
178
+ /** A development build's console note about an invalid value — once per call site. */
179
+ function warnInvalid(fn: string, value: unknown): void {
180
+ if (!devWarnings()) return;
181
+ const site = callSite();
182
+ const key = `${fn}|${site || typeof value}`;
183
+ if (warnedSites.has(key)) return;
184
+ warnedSites.add(key);
185
+ const type = Array.isArray(value) ? "array" : typeof value;
186
+ console.warn(
187
+ `[ui-kit] ${fn} got ${describeValue(value)} (${type}), which is not a number: printed the empty value. ` +
188
+ "Pass a number, a numeric string, or null/undefined for a missing value." +
189
+ (site ? `\n at ${site.replace(/^at /, "")}` : ""),
190
+ );
191
+ }
192
+
193
+ /**
194
+ * `value` as a finite number to format, or `undefined` to print `empty`. `null` and
195
+ * `undefined` are a missing value and pass quietly; anything that is neither a finite
196
+ * number nor a numeric string is an invalid one — ±Infinity included (lenkbank, adopting
197
+ * 0.13: a division by zero upstream; "∞" in a money column reads like data) — and a
198
+ * development build warns. `-0` is folded into `0`: zero carries no sign.
199
+ */
200
+ function toFormattable(fn: string, value: unknown): number | undefined {
201
+ if (value === null || value === undefined) return undefined;
202
+ let n: number | undefined;
203
+ if (typeof value === "number") n = value;
204
+ else if (typeof value === "string" && NUMERIC_STRING.test(value.trim())) n = Number(value.trim());
205
+ if (n === undefined || !Number.isFinite(n)) {
206
+ warnInvalid(fn, value);
207
+ return undefined;
208
+ }
209
+ return n + 0;
122
210
  }
123
211
 
124
212
  /** A number in `locale`: "1,234.5", "1.234,5", "1’234.5" — with `unit`, "3.400 N". */
125
- export function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {
126
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
127
- const figure = numberFormatter(options.locale, numberOptions(options)).format(value);
213
+ export function formatNumber(value: FormattableNumber, options: FormatNumberOptions = {}): string {
214
+ const n = toFormattable("formatNumber", value);
215
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
216
+ const figure = formatWithMinus(numberFormatter(options.locale, numberOptions(options)), n, options.minus);
128
217
  return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;
129
218
  }
130
219
 
131
220
  /** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
132
- export function formatMoney(
133
- value: number | null | undefined,
134
- currency: string,
135
- options: FormatMoneyOptions = {},
136
- ): string {
137
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
138
- return numberFormatter(options.locale, {
221
+ export function formatMoney(value: FormattableNumber, currency: string, options: FormatMoneyOptions = {}): string {
222
+ const n = toFormattable("formatMoney", value);
223
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
224
+ const formatter = numberFormatter(options.locale, {
139
225
  style: "currency",
140
226
  currency,
141
227
  ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),
142
228
  ...numberOptions(options),
143
- }).format(value);
229
+ });
230
+ return formatWithMinus(formatter, n, options.minus);
144
231
  }
145
232
 
146
233
  /** A percentage in `locale` ("12%", "12 %", "+1.25%"). See {@link FormatPercentOptions.ratio}
147
234
  * for what `value` means. Default digits: at most one. */
148
- export function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {
149
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
150
- const ratio = options.ratio === false ? value / 100 : value;
151
- return numberFormatter(options.locale, {
235
+ export function formatPercent(value: FormattableNumber, options: FormatPercentOptions = {}): string {
236
+ const n = toFormattable("formatPercent", value);
237
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
238
+ const ratio = options.ratio === false ? n / 100 : n;
239
+ const formatter = numberFormatter(options.locale, {
152
240
  style: "percent",
153
241
  ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),
154
242
  ...numberOptions(options),
155
- }).format(ratio);
243
+ });
244
+ return formatWithMinus(formatter, ratio, options.minus);
156
245
  }
157
246
 
158
247
  /* ── Dates ─────────────────────────────────────────────────────────────────── */
@@ -327,13 +416,9 @@ type WithoutLocale<T> = Omit<T, "locale"> & { locale?: string };
327
416
  export interface KitFormat {
328
417
  /** The locale they format in — the provider's, or `undefined` (runtime default). */
329
418
  locale: string | undefined;
330
- formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;
331
- formatMoney: (
332
- value: number | null | undefined,
333
- currency: string,
334
- options?: WithoutLocale<FormatMoneyOptions>,
335
- ) => string;
336
- formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;
419
+ formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;
420
+ formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;
421
+ formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;
337
422
  formatDate: (
338
423
  value: DateInput,
339
424
  style?: FormatDateStyle | Intl.DateTimeFormatOptions,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The one minus sign (0.49, KIT-1). `Intl.NumberFormat` writes a negative with the
3
+ * HYPHEN-MINUS (U+002D) in nearly every locale — en, de-CH, fr, it, es, hu, zh — and
4
+ * with the typographic MINUS SIGN (U+2212) in a few (sv, fi). The kit prints U+2212
5
+ * everywhere, as `SignedAmount` always has: it is as wide as "+" and the tabular
6
+ * digits, so a column of signed figures lines up, and it does not break a line the way
7
+ * a hyphen may.
8
+ *
9
+ * `"ascii"` is the other way round, for text a machine reads back — a CSV, the
10
+ * clipboard: every minus becomes U+002D, whatever the locale wrote, so `Number()` and a
11
+ * spreadsheet parse it.
12
+ */
13
+ export type MinusStyle = "typographic" | "ascii";
14
+
15
+ /** U+2212 MINUS SIGN. */
16
+ export const TYPOGRAPHIC_MINUS = "\u2212";
17
+
18
+ /** The parts `formatToParts` writes a minus in (the exponent's, under `notation:
19
+ * "scientific"`, too). */
20
+ const MINUS_PARTS: ReadonlySet<string> = new Set(["minusSign", "exponentMinusSign"]);
21
+
22
+ /**
23
+ * `formatter.format(value)` with every minus sign in `minus` style. Only the parts
24
+ * `Intl` marks as a minus are touched — a hyphen inside a currency's name
25
+ * ("Bosnia-Herzegovina convertible marks") stays — and only when the text holds the
26
+ * other sign at all, so a positive figure costs no `formatToParts`.
27
+ */
28
+ export function formatWithMinus(
29
+ formatter: Intl.NumberFormat,
30
+ value: number,
31
+ minus: MinusStyle = "typographic",
32
+ ): string {
33
+ const text = formatter.format(value);
34
+ const [wanted, other] = minus === "ascii" ? ["-", TYPOGRAPHIC_MINUS] : [TYPOGRAPHIC_MINUS, "-"];
35
+ if (!text.includes(other)) return text;
36
+ return formatter
37
+ .formatToParts(value)
38
+ .map((part) => (MINUS_PARTS.has(part.type) ? wanted : part.value))
39
+ .join("");
40
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What a status means, in five domain-free words, and the tone that says it (0.49 KIT-8).
3
+ *
4
+ * Every app paints its statuses by hand: kastlan's ~15 enums each carry a Chip tone
5
+ * (`shared/enums/enum-meta.ts`), keksdose's job and transaction states theirs, and the
6
+ * same meaning drifted into different colours — "unpaid" amber in one table, brand in
7
+ * the next. The kind is the decision ("does the reader have to act?"), the tone only
8
+ * its colour, so an app maps each of its states to a kind once and the colours agree
9
+ * everywhere:
10
+ *
11
+ * | kind | tone | for |
12
+ * |---|---|---|
13
+ * | `good` | `success` | done, settled, healthy: nothing to do (Paid, Verified) |
14
+ * | `attention` | `warning` | needs a look or an action soon, nothing broken yet (Unpaid, Due soon) |
15
+ * | `problem` | `danger` | something is wrong or late and needs action now (Overdue, Failed) |
16
+ * | `active` | `info` | in progress or current, neither good nor bad (Running, Sent) |
17
+ * | `inactive` | `neutral` | over, paused or not started: out of the way (Draft, Archived) |
18
+ *
19
+ * `brand` is not a status tone: it is the app's own colour, and a status in it reads as
20
+ * "this is the app" rather than as a state. Money (`income` / `expense`) is not one
21
+ * either: it says which way an amount flows, not how a record stands.
22
+ */
23
+ export type StatusKind = "good" | "attention" | "problem" | "active" | "inactive";
24
+
25
+ /** The tones a {@link StatusKind} maps to — a subset of `ChipTone` and `StatusDotTone`. */
26
+ export type StatusTone = "success" | "warning" | "danger" | "info" | "neutral";
27
+
28
+ /** The five kinds, in the order the table above lists them. */
29
+ export const STATUS_KINDS: readonly StatusKind[] = ["good", "attention", "problem", "active", "inactive"];
30
+
31
+ const TONES: Readonly<Record<StatusKind, StatusTone>> = {
32
+ good: "success",
33
+ attention: "warning",
34
+ problem: "danger",
35
+ active: "info",
36
+ inactive: "neutral",
37
+ };
38
+
39
+ /** The tone for a status kind: good → success, attention → warning, problem → danger,
40
+ * active → info, inactive → neutral. See {@link StatusKind}. */
41
+ export function statusTone(kind: StatusKind): StatusTone {
42
+ return TONES[kind];
43
+ }
@@ -323,6 +323,7 @@ export function SettingsLayout<G extends string>({
323
323
  <div className={pageClass} data-settings-view="group">
324
324
  <PageHeader
325
325
  size="compact"
326
+ documentTitle={false}
326
327
  title={open.title}
327
328
  description={open.help}
328
329
  breadcrumbs={
@@ -347,7 +348,7 @@ export function SettingsLayout<G extends string>({
347
348
  </div>
348
349
  ) : (
349
350
  <div className={pageClass} data-settings-view="list">
350
- <PageHeader size="compact" title={pageTitle} />
351
+ <PageHeader size="compact" documentTitle={false} title={pageTitle} />
351
352
  {searchField}
352
353
  {searching ? (
353
354
  results
@@ -409,7 +410,7 @@ export function SettingsLayout<G extends string>({
409
410
  );
410
411
  body = (
411
412
  <div className={pageClass} data-settings-view="desktop">
412
- <PageHeader size="compact" title={pageTitle} />
413
+ <PageHeader size="compact" documentTitle={false} title={pageTitle} />
413
414
  {searchField}
414
415
  <div className="md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6">
415
416
  {withSidebarFooter(