@eifi1/ui-kit 0.7.0 → 0.8.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 (233) hide show
  1. package/README.md +32 -17
  2. package/dist/chart.d.ts +3 -2
  3. package/dist/components/alert-banner.d.ts +59 -10
  4. package/dist/components/alert-banner.js +114 -8
  5. package/dist/components/alert-banner.js.map +1 -1
  6. package/dist/components/amount-input.d.ts +5 -0
  7. package/dist/components/button-group.d.ts +26 -0
  8. package/dist/components/button-group.js +49 -0
  9. package/dist/components/button-group.js.map +1 -0
  10. package/dist/components/calculator.d.ts +5 -0
  11. package/dist/components/chart-zoom.d.ts +63 -14
  12. package/dist/components/chart-zoom.js +82 -16
  13. package/dist/components/chart-zoom.js.map +1 -1
  14. package/dist/components/chip.d.ts +67 -2
  15. package/dist/components/chip.js +73 -8
  16. package/dist/components/chip.js.map +1 -1
  17. package/dist/components/confirm-dialog.d.ts +120 -0
  18. package/dist/components/confirm-dialog.js +96 -0
  19. package/dist/components/confirm-dialog.js.map +1 -0
  20. package/dist/components/copy-button.d.ts +62 -0
  21. package/dist/components/copy-button.js +95 -0
  22. package/dist/components/copy-button.js.map +1 -0
  23. package/dist/components/data-table-filter-popover.d.ts +1 -1
  24. package/dist/components/data-table-filters.d.ts +1 -1
  25. package/dist/components/data-table-pagination.d.ts +5 -1
  26. package/dist/components/data-table-pagination.js +71 -58
  27. package/dist/components/data-table-pagination.js.map +1 -1
  28. package/dist/components/data-table-sort.d.ts +28 -5
  29. package/dist/components/data-table-sort.js +11 -7
  30. package/dist/components/data-table-sort.js.map +1 -1
  31. package/dist/components/data-table.d.ts +1 -1
  32. package/dist/components/data-table.js +544 -423
  33. package/dist/components/data-table.js.map +1 -1
  34. package/dist/components/date-picker.d.ts +51 -5
  35. package/dist/components/date-picker.js +152 -40
  36. package/dist/components/date-picker.js.map +1 -1
  37. package/dist/components/description-list.d.ts +60 -0
  38. package/dist/components/description-list.js +112 -0
  39. package/dist/components/description-list.js.map +1 -0
  40. package/dist/components/disclosure.d.ts +37 -2
  41. package/dist/components/disclosure.js +14 -5
  42. package/dist/components/disclosure.js.map +1 -1
  43. package/dist/components/facing-pair.d.ts +2 -0
  44. package/dist/components/file-button.d.ts +19 -1
  45. package/dist/components/file-button.js +8 -1
  46. package/dist/components/file-button.js.map +1 -1
  47. package/dist/components/file-dropzone.d.ts +56 -9
  48. package/dist/components/file-dropzone.js +100 -70
  49. package/dist/components/file-dropzone.js.map +1 -1
  50. package/dist/components/floating-panel.d.ts +107 -0
  51. package/dist/components/floating-panel.js +206 -0
  52. package/dist/components/floating-panel.js.map +1 -0
  53. package/dist/components/full-bleed-dialog.d.ts +12 -1
  54. package/dist/components/full-bleed-dialog.js +26 -5
  55. package/dist/components/full-bleed-dialog.js.map +1 -1
  56. package/dist/components/modal.d.ts +6 -2
  57. package/dist/components/modal.js +2 -1
  58. package/dist/components/modal.js.map +1 -1
  59. package/dist/components/number-field.d.ts +5 -0
  60. package/dist/components/number-input.d.ts +5 -0
  61. package/dist/components/numpad-sheet.d.ts +5 -0
  62. package/dist/components/progress-bar.d.ts +54 -0
  63. package/dist/components/progress-bar.js +94 -0
  64. package/dist/components/progress-bar.js.map +1 -0
  65. package/dist/components/scroll-area.d.ts +53 -0
  66. package/dist/components/scroll-area.js +86 -0
  67. package/dist/components/scroll-area.js.map +1 -0
  68. package/dist/components/separator.d.ts +23 -0
  69. package/dist/components/separator.js +24 -0
  70. package/dist/components/separator.js.map +1 -0
  71. package/dist/components/series-chart-ticks.d.ts +34 -1
  72. package/dist/components/series-chart-ticks.js +83 -1
  73. package/dist/components/series-chart-ticks.js.map +1 -1
  74. package/dist/components/series-chart.d.ts +314 -19
  75. package/dist/components/series-chart.js +510 -120
  76. package/dist/components/series-chart.js.map +1 -1
  77. package/dist/components/settings-fields.d.ts +2 -0
  78. package/dist/components/skeleton.d.ts +36 -0
  79. package/dist/components/skeleton.js +35 -0
  80. package/dist/components/skeleton.js.map +1 -0
  81. package/dist/components/stat-tile.d.ts +60 -6
  82. package/dist/components/stat-tile.js +60 -26
  83. package/dist/components/stat-tile.js.map +1 -1
  84. package/dist/components/table.d.ts +83 -0
  85. package/dist/components/table.js +166 -0
  86. package/dist/components/table.js.map +1 -0
  87. package/dist/components/time-input.d.ts +2 -0
  88. package/dist/components/toggle-legend.d.ts +3 -2
  89. package/dist/components/toggle-legend.js +1 -1
  90. package/dist/components/toggle-legend.js.map +1 -1
  91. package/dist/components/tooltip.d.ts +10 -4
  92. package/dist/components/tooltip.js +23 -6
  93. package/dist/components/tooltip.js.map +1 -1
  94. package/dist/components/tree-view.d.ts +129 -0
  95. package/dist/components/tree-view.js +376 -0
  96. package/dist/components/tree-view.js.map +1 -0
  97. package/dist/components/treemap.d.ts +9 -2
  98. package/dist/components/treemap.js +2 -1
  99. package/dist/components/treemap.js.map +1 -1
  100. package/dist/components/ui.d.ts +92 -14
  101. package/dist/components/ui.js +112 -30
  102. package/dist/components/ui.js.map +1 -1
  103. package/dist/components/use-table-state.d.ts +1 -1
  104. package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
  105. package/dist/data-table.d.ts +2 -2
  106. package/dist/data-table.js.map +1 -1
  107. package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
  108. package/dist/hooks/use-copy-to-clipboard.js +81 -0
  109. package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
  110. package/dist/hooks/use-debounce.d.ts +43 -0
  111. package/dist/hooks/use-debounce.js +56 -0
  112. package/dist/hooks/use-debounce.js.map +1 -0
  113. package/dist/hooks/use-file-drop.d.ts +72 -0
  114. package/dist/hooks/use-file-drop.js +57 -0
  115. package/dist/hooks/use-file-drop.js.map +1 -0
  116. package/dist/i18n/defaults.d.ts +5 -0
  117. package/dist/i18n/defaults.js +7 -1
  118. package/dist/i18n/defaults.js.map +1 -1
  119. package/dist/i18n/kit-labels.d.ts +17 -0
  120. package/dist/i18n/kit-labels.js +6 -2
  121. package/dist/i18n/kit-labels.js.map +1 -1
  122. package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
  123. package/dist/i18n/locales/de-CH-informal.js +8 -0
  124. package/dist/i18n/locales/de-CH-informal.js.map +1 -0
  125. package/dist/i18n/locales/de-CH.d.ts +5 -0
  126. package/dist/i18n/locales/de-CH.js +1 -14
  127. package/dist/i18n/locales/de-CH.js.map +1 -1
  128. package/dist/i18n/locales/de-informal.d.ts +64 -0
  129. package/dist/i18n/locales/de-informal.js +37 -0
  130. package/dist/i18n/locales/de-informal.js.map +1 -0
  131. package/dist/i18n/locales/de.d.ts +5 -0
  132. package/dist/i18n/locales/de.js +25 -0
  133. package/dist/i18n/locales/de.js.map +1 -1
  134. package/dist/i18n/locales/es.d.ts +5 -0
  135. package/dist/i18n/locales/es.js +25 -0
  136. package/dist/i18n/locales/es.js.map +1 -1
  137. package/dist/i18n/locales/fr.d.ts +5 -0
  138. package/dist/i18n/locales/fr.js +25 -0
  139. package/dist/i18n/locales/fr.js.map +1 -1
  140. package/dist/i18n/locales/hu.d.ts +5 -0
  141. package/dist/i18n/locales/hu.js +25 -0
  142. package/dist/i18n/locales/hu.js.map +1 -1
  143. package/dist/i18n/locales/it.d.ts +5 -0
  144. package/dist/i18n/locales/it.js +25 -0
  145. package/dist/i18n/locales/it.js.map +1 -1
  146. package/dist/i18n/locales/zh.d.ts +5 -0
  147. package/dist/i18n/locales/zh.js +25 -0
  148. package/dist/i18n/locales/zh.js.map +1 -1
  149. package/dist/i18n/swiss.d.ts +13 -0
  150. package/dist/i18n/swiss.js +19 -0
  151. package/dist/i18n/swiss.js.map +1 -0
  152. package/dist/index.d.ts +26 -11
  153. package/dist/index.js +22 -0
  154. package/dist/index.js.map +1 -1
  155. package/dist/lib/dates.d.ts +37 -1
  156. package/dist/lib/dates.js +19 -0
  157. package/dist/lib/dates.js.map +1 -1
  158. package/dist/rhf/form.d.ts +2 -0
  159. package/dist/rhf.d.ts +2 -0
  160. package/dist/search/command-palette.d.ts +15 -1
  161. package/dist/search/command-palette.js +10 -2
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/shell/app-shell.js +2 -2
  164. package/dist/shell/app-shell.js.map +1 -1
  165. package/dist/wizard/stepper-nav.d.ts +35 -2
  166. package/dist/wizard/stepper-nav.js +54 -22
  167. package/dist/wizard/stepper-nav.js.map +1 -1
  168. package/dist/wizard/types.d.ts +107 -5
  169. package/dist/wizard/types.js +2 -1
  170. package/dist/wizard/types.js.map +1 -1
  171. package/dist/wizard/use-wizard.js +141 -21
  172. package/dist/wizard/use-wizard.js.map +1 -1
  173. package/dist/wizard/wizard-summary.d.ts +10 -2
  174. package/dist/wizard/wizard-summary.js +3 -1
  175. package/dist/wizard/wizard-summary.js.map +1 -1
  176. package/dist/wizard.d.ts +4 -1
  177. package/dist/wizard.js.map +1 -1
  178. package/package.json +1 -1
  179. package/src/components/alert-banner.tsx +211 -20
  180. package/src/components/button-group.tsx +75 -0
  181. package/src/components/chart-zoom.tsx +158 -20
  182. package/src/components/chip.tsx +160 -9
  183. package/src/components/confirm-dialog.tsx +242 -0
  184. package/src/components/copy-button.tsx +158 -0
  185. package/src/components/data-table-pagination.tsx +19 -6
  186. package/src/components/data-table-sort.ts +49 -10
  187. package/src/components/data-table.tsx +293 -24
  188. package/src/components/date-picker.tsx +262 -57
  189. package/src/components/description-list.tsx +174 -0
  190. package/src/components/disclosure.tsx +58 -5
  191. package/src/components/file-button.tsx +22 -2
  192. package/src/components/file-dropzone.tsx +198 -100
  193. package/src/components/floating-panel.tsx +341 -0
  194. package/src/components/full-bleed-dialog.tsx +48 -5
  195. package/src/components/modal.tsx +9 -2
  196. package/src/components/progress-bar.tsx +163 -0
  197. package/src/components/scroll-area.tsx +129 -0
  198. package/src/components/separator.tsx +37 -0
  199. package/src/components/series-chart-ticks.ts +135 -0
  200. package/src/components/series-chart.tsx +934 -67
  201. package/src/components/skeleton.tsx +65 -0
  202. package/src/components/stat-tile.tsx +160 -27
  203. package/src/components/table.tsx +263 -0
  204. package/src/components/toggle-legend.tsx +4 -3
  205. package/src/components/tooltip.tsx +40 -12
  206. package/src/components/tree-view.tsx +589 -0
  207. package/src/components/treemap.tsx +10 -2
  208. package/src/components/ui.tsx +268 -42
  209. package/src/data-table.ts +2 -0
  210. package/src/hooks/use-copy-to-clipboard.ts +122 -0
  211. package/src/hooks/use-debounce.ts +105 -0
  212. package/src/hooks/use-file-drop.ts +123 -0
  213. package/src/i18n/defaults.ts +6 -0
  214. package/src/i18n/kit-labels.tsx +19 -0
  215. package/src/i18n/locales/de-CH-informal.ts +10 -0
  216. package/src/i18n/locales/de-CH.ts +1 -16
  217. package/src/i18n/locales/de-informal.ts +61 -0
  218. package/src/i18n/locales/de.ts +25 -0
  219. package/src/i18n/locales/es.ts +25 -0
  220. package/src/i18n/locales/fr.ts +25 -0
  221. package/src/i18n/locales/hu.ts +25 -0
  222. package/src/i18n/locales/it.ts +25 -0
  223. package/src/i18n/locales/zh.ts +25 -0
  224. package/src/i18n/swiss.ts +25 -0
  225. package/src/index.ts +29 -0
  226. package/src/lib/dates.ts +48 -0
  227. package/src/search/command-palette.tsx +29 -3
  228. package/src/shell/app-shell.tsx +2 -2
  229. package/src/wizard/stepper-nav.tsx +102 -28
  230. package/src/wizard/types.ts +106 -4
  231. package/src/wizard/use-wizard.ts +211 -31
  232. package/src/wizard/wizard-summary.tsx +24 -12
  233. package/src/wizard.ts +3 -2
@@ -42,6 +42,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
42
42
  },
43
43
  pageContents: { title: "In questa pagina" },
44
44
  common: {
45
+ dismiss: "Chiudi",
45
46
  close: "Chiudi",
46
47
  clear: "Cancella",
47
48
  search: "Cerca",
@@ -125,6 +126,9 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
125
126
  month: (monthYear) => monthYear,
126
127
  },
127
128
  datePicker: {
129
+ apply: "Applica",
130
+ cancel: "Annulla",
131
+ presets: "Intervalli rapidi",
128
132
  panel: "Scegli una data",
129
133
  rangePanel: "Scegli un intervallo di date",
130
134
  clear: "Cancella",
@@ -241,6 +245,12 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
241
245
  size: (bytes) => formatFileSize(bytes, numberLocale),
242
246
  },
243
247
  filePicker: {
248
+ dropzone: "Caricamento file",
249
+ browse: "Sfoglia",
250
+ empty: "Trascina qui un file",
251
+ emptyMultiple: "Trascina qui i file",
252
+ hint: (accept) => (accept ? `Accettati: ${accept}` : "Qualsiasi tipo di file"),
253
+ busy: "Caricamento…",
244
254
  rejectedPick: (count) =>
245
255
  count === 1 ? "Il file non è stato aggiunto" : `Nessuno dei ${count} file è stato aggiunto`,
246
256
  // "file" is invariable in Italian ("1 file", "3 file"); only the verb agrees.
@@ -260,6 +270,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
260
270
  cleared: "Tutti i file rimossi",
261
271
  },
262
272
  wizard: {
273
+ done: "Fine",
263
274
  cancel: "Annulla",
264
275
  back: "Indietro",
265
276
  next: "Avanti",
@@ -354,6 +365,20 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
354
365
  empty: "Nessun dato",
355
366
  legend: "Serie",
356
367
  },
368
+ confirmDialog: {
369
+ confirm: "Conferma",
370
+ cancel: "Annulla",
371
+ },
372
+ floatingPanel: {
373
+ close: "Chiudi",
374
+ },
375
+ copyButton: {
376
+ copy: "Copia",
377
+ copied: "Copiato",
378
+ failed: "Copia non riuscita",
379
+ copiedAnnouncement: "Copiato negli appunti",
380
+ failedAnnouncement: "Impossibile copiare negli appunti",
381
+ },
357
382
  };
358
383
  }
359
384
 
@@ -40,6 +40,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
40
40
  },
41
41
  pageContents: { title: "本页目录" },
42
42
  common: {
43
+ dismiss: "关闭",
43
44
  close: "关闭",
44
45
  clear: "清除",
45
46
  search: "搜索",
@@ -120,6 +121,9 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
120
121
  month: (monthYear) => monthYear,
121
122
  },
122
123
  datePicker: {
124
+ apply: "应用",
125
+ cancel: "取消",
126
+ presets: "快捷范围",
123
127
  panel: "选择日期",
124
128
  rangePanel: "选择日期范围",
125
129
  clear: "清除",
@@ -235,6 +239,12 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
235
239
  size: (bytes) => formatFileSize(bytes, numberLocale),
236
240
  },
237
241
  filePicker: {
242
+ dropzone: "文件上传",
243
+ browse: "浏览",
244
+ empty: "将文件拖放到此处",
245
+ emptyMultiple: "将文件拖放到此处",
246
+ hint: (accept) => (accept ? `支持:${accept}` : "任意文件类型"),
247
+ busy: "正在上传…",
238
248
  rejectedPick: (count) => (count === 1 ? "未添加该文件" : `${count} 个文件均未添加`),
239
249
  rejectedType: (name) => `“${name}”的文件类型不受支持`,
240
250
  rejectedTypeOnly: (accept) => `仅限 ${accept} 文件`,
@@ -250,6 +260,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
250
260
  cleared: "已移除所有文件",
251
261
  },
252
262
  wizard: {
263
+ done: "完成",
253
264
  cancel: "取消",
254
265
  back: "上一步",
255
266
  next: "下一步",
@@ -342,6 +353,20 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
342
353
  empty: "无数据",
343
354
  legend: "数据系列",
344
355
  },
356
+ confirmDialog: {
357
+ confirm: "确认",
358
+ cancel: "取消",
359
+ },
360
+ floatingPanel: {
361
+ close: "关闭",
362
+ },
363
+ copyButton: {
364
+ copy: "复制",
365
+ copied: "已复制",
366
+ failed: "复制失败",
367
+ copiedAnnouncement: "已复制到剪贴板",
368
+ failedAnnouncement: "无法复制到剪贴板",
369
+ },
345
370
  };
346
371
  }
347
372
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Swiss Standard German respelling, shared by the `de-CH` and `de-CH-informal`
3
+ * translations. Internal: it lives outside `src/i18n/locales/` because every file there
4
+ * becomes a public `@eifi1/ui-kit/i18n/<code>` subpath.
5
+ *
6
+ * "ss" for every "ß" (Schliessen, Grösse) is the one systematic difference in UI text.
7
+ * Function labels are wrapped: their RESULT is respelled, so an argument the app passes
8
+ * in (a file name, a phrase to type) is respelled too — which is what a Swiss app
9
+ * writing "ss" everywhere wants.
10
+ */
11
+ export function swiss<T>(value: T): T {
12
+ if (typeof value === "string") return respell(value) as T;
13
+ if (typeof value === "function") {
14
+ const fn = value as (...args: unknown[]) => unknown;
15
+ return ((...args: unknown[]) => swiss(fn(...args))) as T;
16
+ }
17
+ if (value && typeof value === "object") {
18
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, swiss(v)])) as T;
19
+ }
20
+ return value;
21
+ }
22
+
23
+ function respell(text: string): string {
24
+ return text.replace(/ß/g, "ss").replace(/ẞ/g, "SS");
25
+ }
package/src/index.ts CHANGED
@@ -32,6 +32,15 @@ export { useOverlayHistory } from "./hooks/use-overlay-history";
32
32
  export { useCloseTransition, OVERLAY_EXIT_MS } from "./hooks/use-close-transition";
33
33
  export { useRowSwipe } from "./hooks/use-row-swipe";
34
34
  export type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from "./hooks/use-row-swipe";
35
+ // Any element as a file drop target, screened like FileButton (kastlan).
36
+ export { useFileDrop, dragHasFiles } from "./hooks/use-file-drop";
37
+ export type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from "./hooks/use-file-drop";
38
+ // kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.
39
+ export { useDebounce, useDebouncedCallback } from "./hooks/use-debounce";
40
+ export type { DebouncedCallbackOptions, DebouncedFunction } from "./hooks/use-debounce";
41
+ // Copy that reports whether it worked — keksdose's copy buttons said "Copied" when it had not.
42
+ export { useCopyToClipboard, copyToClipboard } from "./hooks/use-copy-to-clipboard";
43
+ export type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from "./hooks/use-copy-to-clipboard";
35
44
 
36
45
  // ── theme / palettes ─────────────────────────────────────────────────────────
37
46
  export * from "./theme/chart-palette";
@@ -83,6 +92,10 @@ export * from "./components/page-contents";
83
92
  export * from "./components/disclosure";
84
93
  export * from "./components/dialog-frame";
85
94
  export * from "./components/danger-confirm";
95
+ // `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.
96
+ export * from "./components/confirm-dialog";
97
+ // A non-modal corner panel and its round trigger (keksdose's assistant launcher).
98
+ export * from "./components/floating-panel";
86
99
  export * from "./components/swatch-picker";
87
100
  export * from "./components/icon-picker";
88
101
  export * from "./components/choice-card";
@@ -123,7 +136,21 @@ export * from "./components/grouped-picker";
123
136
  export * from "./components/file-dropzone";
124
137
  export * from "./components/mini-calendar";
125
138
  export * from "./components/date-picker";
139
+ export * from "./components/tree-view";
126
140
  export * from "./components/chart";
141
+ // 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.
142
+ export * from "./components/description-list";
143
+ export * from "./components/progress-bar";
144
+ // Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.
145
+ export { Skeleton } from "./components/skeleton";
146
+ export type { SkeletonProps, SkeletonShape } from "./components/skeleton";
147
+ export * from "./components/copy-button";
148
+ export * from "./components/button-group";
149
+ export * from "./components/table";
150
+ export * from "./components/separator";
151
+ // Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.
152
+ export { ScrollArea } from "./components/scroll-area";
153
+ export type { ScrollAreaProps } from "./components/scroll-area";
127
154
 
128
155
  // ── data-table suite ─────────────────────────────────────────────────────────
129
156
  export * from "./components/data-table-labels";
@@ -140,6 +167,8 @@ export type {
140
167
  ServerPagination,
141
168
  FilterState,
142
169
  MobileSwipeActions,
170
+ DataTableDensity,
171
+ DataTableChrome,
143
172
  } from "./components/data-table";
144
173
 
145
174
  // ── shell (composable app chrome) ────────────────────────────────────────────
package/src/lib/dates.ts CHANGED
@@ -185,3 +185,51 @@ export function dateRangePresets(): DateRangePreset[] {
185
185
  { key: "last_year", from: startOfYearIso(-1), to: endOfYearIso(-1) },
186
186
  ];
187
187
  }
188
+
189
+ /** The last `n` WHOLE calendar months before the current one, as an inclusive
190
+ * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like
191
+ * everything here; `now` is for tests and for a host with its own clock. */
192
+ export function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {
193
+ const from = new Date(now.getFullYear(), now.getMonth() - n, 1);
194
+ // Day 0 of the current month is the last day of the previous one.
195
+ const to = new Date(now.getFullYear(), now.getMonth(), 0);
196
+ return { from: toLocalIso(from), to: toLocalIso(to) };
197
+ }
198
+
199
+ /** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is
200
+ * 1 January 2024 … 31 December 2025. */
201
+ export function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {
202
+ const year = now.getFullYear();
203
+ return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };
204
+ }
205
+
206
+ export interface CalendarMonthPresetOptions {
207
+ /** How many whole months each month preset spans. Default `[3, 6]`. */
208
+ months?: readonly number[];
209
+ /** How many whole years each year preset spans. Default `[2]`. */
210
+ years?: readonly number[];
211
+ /** The day to count back from. Default: now, on the local calendar. */
212
+ now?: Date;
213
+ }
214
+
215
+ /**
216
+ * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.
217
+ *
218
+ * kastlan's accounting reports are closed by the month: "the last three months" there
219
+ * means February, March and April, never "the 90 days up to this morning", which
220
+ * starts mid-month and ends with a day that is still being booked. `last_3_months`
221
+ * above is the rolling kind and stays that way for the data table's filter, so these
222
+ * are separate keys rather than a changed meaning under an old one.
223
+ *
224
+ * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the
225
+ * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With
226
+ * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its
227
+ * identity while the dates are recomputed on every render.
228
+ */
229
+ export function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {
230
+ const { months = [3, 6], years = [2], now = new Date() } = options;
231
+ return [
232
+ ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),
233
+ ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),
234
+ ];
235
+ }
@@ -91,6 +91,20 @@ interface CommandPaletteProps {
91
91
  * another keystroke. That is the ordinary first open on a cold cache.
92
92
  */
93
93
  revision?: unknown;
94
+ /**
95
+ * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search
96
+ * survives a reload, can be shared as a link, and Back walks through it — none of
97
+ * which state inside the palette can do.
98
+ *
99
+ * Leave it out and the palette keeps its own, as before, starting empty on every
100
+ * open. Given, it is never reset on open: the owner decides what an open shows, and
101
+ * a palette that blanked the URL's `q` each time would fight its own address bar.
102
+ * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.
103
+ */
104
+ query?: string;
105
+ /** Every edit of the field's text. Fires in both modes — uncontrolled, it only
106
+ * observes. */
107
+ onQueryChange?: (query: string) => void;
94
108
  labels?: Partial<CommandPaletteLabels>;
95
109
  }
96
110
 
@@ -119,6 +133,8 @@ export function CommandPalette({
119
133
  onClose,
120
134
  search,
121
135
  revision,
136
+ query: queryProp,
137
+ onQueryChange,
122
138
  labels,
123
139
  }: CommandPaletteProps) {
124
140
  const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
@@ -129,7 +145,13 @@ export function CommandPalette({
129
145
  const dialogName =
130
146
  l.dialog ??
131
147
  (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);
132
- const [query, setQuery] = useState("");
148
+ const [ownQuery, setOwnQuery] = useState("");
149
+ const controlled = queryProp !== undefined;
150
+ const query = controlled ? queryProp : ownQuery;
151
+ const setQuery = (next: string) => {
152
+ if (!controlled) setOwnQuery(next);
153
+ onQueryChange?.(next);
154
+ };
133
155
  const [results, setResults] = useState<CommandItem[]>([]);
134
156
  const [loading, setLoading] = useState(false);
135
157
  const [failed, setFailed] = useState(false);
@@ -160,11 +182,15 @@ export function CommandPalette({
160
182
  useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });
161
183
  useBodyScrollLock(open);
162
184
 
163
- // Reset when opened.
185
+ // Reset when opened — the palette's OWN text only; a controlled query belongs to
186
+ // its owner (see the prop).
164
187
  useEffect(() => {
165
188
  if (!open) return;
166
- setQuery("");
189
+ if (!controlled) setOwnQuery("");
167
190
  setActive(0);
191
+ // `controlled` is read at the open, not watched: a caller switching modes while
192
+ // the palette is open is not a reason to wipe what is typed.
193
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
194
  }, [open]);
169
195
 
170
196
  // Debounced, race-safe search.
@@ -299,7 +299,7 @@ export function AppShell({
299
299
  {sidebarFooter?.(collapsed)}
300
300
  <div className="border-t border-[var(--border)] p-2">
301
301
  {collapsed ? (
302
- <Tooltip label={labels.expand} side="right" portal className="block">
302
+ <Tooltip label={labels.expand} side="end" portal className="block">
303
303
  <button
304
304
  type="button"
305
305
  onClick={() => setCollapsed(false)}
@@ -569,7 +569,7 @@ function SidebarNavItem({
569
569
 
570
570
  if (hasSub) return <SidebarFlyout item={item}>{link}</SidebarFlyout>;
571
571
  return collapsed ? (
572
- <Tooltip label={item.label} side="right" portal className="block">
572
+ <Tooltip label={item.label} side="end" portal className="block">
573
573
  {link}
574
574
  </Tooltip>
575
575
  ) : (
@@ -1,8 +1,9 @@
1
1
  import { useId, useMemo } from "react";
2
- import type { ReactNode } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
3
  import { Check } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
- import { Button } from "../components/ui";
5
+ import { Button, Spinner } from "../components/ui";
6
+ import type { ButtonVariant } from "../components/ui";
6
7
  import { Modal } from "../components/modal";
7
8
  import { alertFrameClass } from "../components/alert-banner";
8
9
  import { WizardContextProvider } from "./wizard-context";
@@ -25,6 +26,12 @@ import type { UseWizardReturn, WizardLabels } from "./types";
25
26
  * The `<StepperNav>` is also what publishes {@link WizardContextProvider}, which
26
27
  * is how a mounted step registers its own validator. A step's
27
28
  * `useRhfWizardStep` / `useWizardStepValidate` therefore only works inside one.
29
+ *
30
+ * Which buttons show is read off `wizard` (`canCancel`, `canGoBack`, `canDone`,
31
+ * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,
32
+ * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button
33
+ * looks and what wraps it, because that is presentation and usually depends on
34
+ * state the app holds (a "replace everything" toggle, a write lock).
28
35
  */
29
36
  export function StepperNav<TData extends Record<string, unknown>>({
30
37
  wizard,
@@ -33,6 +40,9 @@ export function StepperNav<TData extends Record<string, unknown>>({
33
40
  description,
34
41
  className,
35
42
  labels,
43
+ finishVariant = "brand",
44
+ finishDisabled = false,
45
+ renderFinish,
36
46
  }: {
37
47
  wizard: UseWizardReturn<TData>;
38
48
  children: ReactNode;
@@ -40,6 +50,30 @@ export function StepperNav<TData extends Record<string, unknown>>({
40
50
  description?: ReactNode;
41
51
  className?: string;
42
52
  labels?: Partial<WizardLabels>;
53
+ /** The Finish button's variant — `"danger"` for a destructive commit (an import
54
+ * that replaces what is there). Default `"brand"`. Next keeps `"brand"`. */
55
+ finishVariant?: ButtonVariant;
56
+ /** Disable Finish for a reason of the app's own, on top of the kit's gates
57
+ * (`canFinish`, submitting, validating). */
58
+ finishDisabled?: boolean;
59
+ /**
60
+ * Wrap the Finish button: receives the kit's button element — already labelled,
61
+ * gated and wired to `wizard.finish` — and returns what to render in its place.
62
+ *
63
+ * ```tsx
64
+ * renderFinish={(button) => <SaveGuard lock={lock}>{button}</SaveGuard>}
65
+ * ```
66
+ *
67
+ * A wrapper and not a replacement (`finishButton: (props) => …`) or a hook
68
+ * (`onBeforeFinish`): the app's need is to put something AROUND the commit — a
69
+ * write-lock tooltip, which has to sit on a wrapping element because a disabled
70
+ * button receives no pointer events — while the kit keeps owning what the button
71
+ * says and when it is enabled. A replacement would hand every app the gating to
72
+ * re-derive (the problem this component exists to end); `onBeforeFinish` can veto
73
+ * a click but cannot explain a disabled button. The element is a plain `<Button>`,
74
+ * so a wrapper that clones it with `disabled` works as expected.
75
+ */
76
+ renderFinish?: (button: ReactElement) => ReactNode;
43
77
  }) {
44
78
  const l = useKitLabels("wizard", DEFAULT_WIZARD_LABELS, labels);
45
79
  const cancelTitleId = useId();
@@ -57,6 +91,51 @@ export function StepperNav<TData extends Record<string, unknown>>({
57
91
  // (empty data after a reload) from firing an empty submit.
58
92
  const canFinish = wizard.canFinish;
59
93
 
94
+ // Pending while the step's validators run (an async server check): a spinner and
95
+ // `aria-busy`, and disabled — the hook's own re-entry guard still refuses a second
96
+ // press, this just stops it looking like the first one did nothing.
97
+ const pending = wizard.isValidating;
98
+ const stepNextLabel = wizard.currentStep.nextLabel;
99
+
100
+ let forwardButton: ReactNode = null;
101
+ if (wizard.isCommitStep) {
102
+ const finishButton = (
103
+ <Button
104
+ variant={finishVariant}
105
+ data-tour="wizard-finish"
106
+ onClick={wizard.finish}
107
+ disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}
108
+ aria-busy={wizard.isSubmitting || pending || undefined}
109
+ >
110
+ {/* Decorative: the button's text is what a reader hears. */}
111
+ {pending && <Spinner label={null} className="size-4" />}
112
+ {wizard.isSubmitting ? l.submitting : (stepNextLabel ?? l.finish)}
113
+ </Button>
114
+ );
115
+ forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;
116
+ } else if (wizard.isLastStep) {
117
+ // The last step after a commit. Nothing to finish; Done only when the app gave
118
+ // an `onDone` — otherwise the step carries its own way out.
119
+ forwardButton = wizard.canDone ? (
120
+ <Button variant="brand" data-tour="wizard-done" onClick={wizard.done}>
121
+ {stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done}
122
+ </Button>
123
+ ) : null;
124
+ } else {
125
+ forwardButton = (
126
+ <Button
127
+ variant="brand"
128
+ data-tour="wizard-next"
129
+ onClick={wizard.goNext}
130
+ disabled={wizard.nextBlocked || pending}
131
+ aria-busy={pending || undefined}
132
+ >
133
+ {pending && <Spinner label={null} className="size-4" />}
134
+ {stepNextLabel ?? l.next}
135
+ </Button>
136
+ );
137
+ }
138
+
60
139
  return (
61
140
  <div className={cn("flex flex-col gap-6", className)}>
62
141
  {(title || description) && (
@@ -84,7 +163,9 @@ export function StepperNav<TData extends Record<string, unknown>>({
84
163
  <button
85
164
  type="button"
86
165
  onClick={() => wizard.goToStep(index)}
87
- disabled={status === "upcoming"}
166
+ // `canGoToStep` adds the commit lock: once past a committing step,
167
+ // the steps up to it are done with and cannot be reopened.
168
+ disabled={status === "upcoming" || !wizard.canGoToStep(index)}
88
169
  aria-current={status === "active" ? "step" : undefined}
89
170
  className={cn(
90
171
  "group flex flex-col items-center gap-1.5",
@@ -161,39 +242,32 @@ export function StepperNav<TData extends Record<string, unknown>>({
161
242
 
162
243
  {/* Navigation bar */}
163
244
  <div className="flex items-center justify-between border-t border-[var(--border)] pt-4">
164
- <Button variant="secondary" onClick={wizard.cancel}>
165
- {l.cancel}
166
- </Button>
245
+ {/* The empty span keeps the right-hand group on the right when Cancel is off. */}
246
+ {wizard.canCancel ? (
247
+ <Button variant="secondary" onClick={wizard.cancel}>
248
+ {l.cancel}
249
+ </Button>
250
+ ) : (
251
+ <span />
252
+ )}
167
253
  <div className="flex items-center gap-2">
168
- {!wizard.isFirstStep && (
169
- <Button variant="secondary" onClick={wizard.goBack}>
254
+ {wizard.canGoBack && (
255
+ // Disabled, not hidden, while the commit runs: it comes back if the commit
256
+ // fails, and a button that vanishes mid-click is its own kind of surprise.
257
+ <Button variant="secondary" onClick={wizard.goBack} disabled={wizard.isSubmitting}>
170
258
  {l.back}
171
259
  </Button>
172
260
  )}
173
- {wizard.currentStep.optional && !wizard.isLastStep && (
174
- <Button variant="ghost" onClick={wizard.skip} disabled={wizard.nextBlocked}>
175
- {l.skip}
176
- </Button>
177
- )}
178
- {wizard.isLastStep ? (
261
+ {wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && (
179
262
  <Button
180
- variant="brand"
181
- data-tour="wizard-finish"
182
- onClick={wizard.finish}
183
- disabled={wizard.isSubmitting || !canFinish}
263
+ variant="ghost"
264
+ onClick={wizard.skip}
265
+ disabled={wizard.nextBlocked || wizard.isValidating}
184
266
  >
185
- {wizard.isSubmitting ? l.submitting : l.finish}
186
- </Button>
187
- ) : (
188
- <Button
189
- variant="brand"
190
- data-tour="wizard-next"
191
- onClick={wizard.goNext}
192
- disabled={wizard.nextBlocked}
193
- >
194
- {l.next}
267
+ {l.skip}
195
268
  </Button>
196
269
  )}
270
+ {forwardButton}
197
271
  </div>
198
272
  </div>
199
273