@eifi1/ui-kit 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) 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 +3 -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 +3 -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 +17 -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 +54 -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 +28 -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 +3 -0
  60. package/dist/components/number-input.d.ts +3 -0
  61. package/dist/components/numpad-sheet.d.ts +3 -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/skeleton.d.ts +36 -0
  78. package/dist/components/skeleton.js +35 -0
  79. package/dist/components/skeleton.js.map +1 -0
  80. package/dist/components/stat-tile.d.ts +60 -6
  81. package/dist/components/stat-tile.js +60 -26
  82. package/dist/components/stat-tile.js.map +1 -1
  83. package/dist/components/table.d.ts +83 -0
  84. package/dist/components/table.js +166 -0
  85. package/dist/components/table.js.map +1 -0
  86. package/dist/components/toggle-legend.d.ts +3 -2
  87. package/dist/components/toggle-legend.js +1 -1
  88. package/dist/components/toggle-legend.js.map +1 -1
  89. package/dist/components/tree-view.d.ts +129 -0
  90. package/dist/components/tree-view.js +376 -0
  91. package/dist/components/tree-view.js.map +1 -0
  92. package/dist/components/treemap.d.ts +9 -2
  93. package/dist/components/treemap.js +6 -2
  94. package/dist/components/treemap.js.map +1 -1
  95. package/dist/components/ui.d.ts +94 -13
  96. package/dist/components/ui.js +111 -29
  97. package/dist/components/ui.js.map +1 -1
  98. package/dist/components/use-table-state.d.ts +1 -1
  99. package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
  100. package/dist/data-table.d.ts +2 -2
  101. package/dist/data-table.js.map +1 -1
  102. package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
  103. package/dist/hooks/use-copy-to-clipboard.js +81 -0
  104. package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
  105. package/dist/hooks/use-debounce.d.ts +43 -0
  106. package/dist/hooks/use-debounce.js +56 -0
  107. package/dist/hooks/use-debounce.js.map +1 -0
  108. package/dist/hooks/use-file-drop.d.ts +72 -0
  109. package/dist/hooks/use-file-drop.js +57 -0
  110. package/dist/hooks/use-file-drop.js.map +1 -0
  111. package/dist/i18n/defaults.d.ts +3 -0
  112. package/dist/i18n/defaults.js +7 -1
  113. package/dist/i18n/defaults.js.map +1 -1
  114. package/dist/i18n/kit-labels.d.ts +15 -0
  115. package/dist/i18n/kit-labels.js +6 -2
  116. package/dist/i18n/kit-labels.js.map +1 -1
  117. package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
  118. package/dist/i18n/locales/de-CH-informal.js +8 -0
  119. package/dist/i18n/locales/de-CH-informal.js.map +1 -0
  120. package/dist/i18n/locales/de-CH.d.ts +3 -0
  121. package/dist/i18n/locales/de-CH.js +1 -14
  122. package/dist/i18n/locales/de-CH.js.map +1 -1
  123. package/dist/i18n/locales/de-informal.d.ts +64 -0
  124. package/dist/i18n/locales/de-informal.js +37 -0
  125. package/dist/i18n/locales/de-informal.js.map +1 -0
  126. package/dist/i18n/locales/de.d.ts +3 -0
  127. package/dist/i18n/locales/de.js +25 -0
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +3 -0
  130. package/dist/i18n/locales/es.js +25 -0
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +3 -0
  133. package/dist/i18n/locales/fr.js +25 -0
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +3 -0
  136. package/dist/i18n/locales/hu.js +25 -0
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +3 -0
  139. package/dist/i18n/locales/it.js +25 -0
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +3 -0
  142. package/dist/i18n/locales/zh.js +25 -0
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/i18n/swiss.d.ts +13 -0
  145. package/dist/i18n/swiss.js +19 -0
  146. package/dist/i18n/swiss.js.map +1 -0
  147. package/dist/index.d.ts +25 -10
  148. package/dist/index.js +22 -0
  149. package/dist/index.js.map +1 -1
  150. package/dist/lib/dates.d.ts +37 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/search/command-palette.d.ts +15 -1
  154. package/dist/search/command-palette.js +10 -2
  155. package/dist/search/command-palette.js.map +1 -1
  156. package/dist/wizard/stepper-nav.d.ts +35 -2
  157. package/dist/wizard/stepper-nav.js +54 -22
  158. package/dist/wizard/stepper-nav.js.map +1 -1
  159. package/dist/wizard/types.d.ts +107 -5
  160. package/dist/wizard/types.js +2 -1
  161. package/dist/wizard/types.js.map +1 -1
  162. package/dist/wizard/use-wizard.js +141 -21
  163. package/dist/wizard/use-wizard.js.map +1 -1
  164. package/dist/wizard/wizard-summary.d.ts +10 -2
  165. package/dist/wizard/wizard-summary.js +3 -1
  166. package/dist/wizard/wizard-summary.js.map +1 -1
  167. package/dist/wizard.d.ts +4 -1
  168. package/dist/wizard.js.map +1 -1
  169. package/package.json +1 -1
  170. package/src/components/alert-banner.tsx +211 -20
  171. package/src/components/button-group.tsx +75 -0
  172. package/src/components/chart-zoom.tsx +158 -20
  173. package/src/components/chip.tsx +160 -9
  174. package/src/components/confirm-dialog.tsx +242 -0
  175. package/src/components/copy-button.tsx +158 -0
  176. package/src/components/data-table-pagination.tsx +19 -6
  177. package/src/components/data-table-sort.ts +49 -10
  178. package/src/components/data-table.tsx +293 -24
  179. package/src/components/date-picker.tsx +262 -57
  180. package/src/components/description-list.tsx +174 -0
  181. package/src/components/disclosure.tsx +58 -5
  182. package/src/components/file-button.tsx +22 -2
  183. package/src/components/file-dropzone.tsx +198 -100
  184. package/src/components/floating-panel.tsx +341 -0
  185. package/src/components/full-bleed-dialog.tsx +53 -5
  186. package/src/components/modal.tsx +9 -2
  187. package/src/components/progress-bar.tsx +163 -0
  188. package/src/components/scroll-area.tsx +129 -0
  189. package/src/components/separator.tsx +37 -0
  190. package/src/components/series-chart-ticks.ts +135 -0
  191. package/src/components/series-chart.tsx +934 -67
  192. package/src/components/skeleton.tsx +65 -0
  193. package/src/components/stat-tile.tsx +160 -27
  194. package/src/components/table.tsx +263 -0
  195. package/src/components/toggle-legend.tsx +4 -3
  196. package/src/components/tree-view.tsx +589 -0
  197. package/src/components/treemap.tsx +13 -2
  198. package/src/components/ui.tsx +270 -39
  199. package/src/data-table.ts +2 -0
  200. package/src/hooks/use-copy-to-clipboard.ts +122 -0
  201. package/src/hooks/use-debounce.ts +105 -0
  202. package/src/hooks/use-file-drop.ts +123 -0
  203. package/src/i18n/defaults.ts +6 -0
  204. package/src/i18n/kit-labels.tsx +19 -0
  205. package/src/i18n/locales/de-CH-informal.ts +10 -0
  206. package/src/i18n/locales/de-CH.ts +1 -16
  207. package/src/i18n/locales/de-informal.ts +61 -0
  208. package/src/i18n/locales/de.ts +25 -0
  209. package/src/i18n/locales/es.ts +25 -0
  210. package/src/i18n/locales/fr.ts +25 -0
  211. package/src/i18n/locales/hu.ts +25 -0
  212. package/src/i18n/locales/it.ts +25 -0
  213. package/src/i18n/locales/zh.ts +25 -0
  214. package/src/i18n/swiss.ts +25 -0
  215. package/src/index.ts +29 -0
  216. package/src/lib/dates.ts +48 -0
  217. package/src/search/command-palette.tsx +29 -3
  218. package/src/wizard/stepper-nav.tsx +102 -28
  219. package/src/wizard/types.ts +106 -4
  220. package/src/wizard/use-wizard.ts +211 -31
  221. package/src/wizard/wizard-summary.tsx +24 -12
  222. 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.
@@ -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
 
@@ -24,13 +24,83 @@ export interface WizardStepConfig {
24
24
  * per-field error messages via `wizard.fieldErrors`.
25
25
  */
26
26
  validate?: () => ValidateResult | Promise<ValidateResult>;
27
+ /**
28
+ * The forward button's label on THIS step, in place of `labels.next` — "Analyse",
29
+ * "Import", "Send code". On the committing step (see {@link commits}) it labels
30
+ * Finish instead of `labels.finish`; `labels.submitting` still replaces it while
31
+ * the commit runs. Like `label`, a node the app has already translated.
32
+ */
33
+ nextLabel?: ReactNode;
34
+ /**
35
+ * This step's forward button COMMITS: it runs `onComplete` (via `finish`) instead
36
+ * of just advancing. Without the flag the last step commits, as it always has.
37
+ *
38
+ * Flag a step before the last and every step after it becomes a post-commit step —
39
+ * an import's "here is what happened, set up recurring rules?" page:
40
+ * - a successful `onComplete` advances onto the next step (a failed one stays, with
41
+ * `wizard.error`, exactly like a last-step Finish);
42
+ * - from there Back, the indicator and `goBack`/`goToStep` cannot cross back over
43
+ * the commit, and Cancel is hidden — there is nothing left to discard;
44
+ * - the last post-commit step's button is Done, which calls `onDone` (and is not
45
+ * shown when there is no `onDone`: the step then carries its own way out).
46
+ *
47
+ * Only the FIRST flagged step counts. Steps after it may still validate, skip and
48
+ * use `nextLabel` like any other.
49
+ */
50
+ commits?: boolean;
27
51
  }
28
52
 
53
+ /**
54
+ * Where the active step lives. `true` (the default) mirrors it to `?step=N` through
55
+ * react-router, as every wizard before 0.8 did; `{ param }` renames that param (two
56
+ * wizards on one page, or a page that already uses `step`); `false` keeps it in
57
+ * memory only and needs no router at all.
58
+ *
59
+ * Read once, at mount: switching it on a mounted wizard has no effect.
60
+ */
61
+ export type WizardUrlSync = boolean | { param?: string };
62
+
29
63
  export interface UseWizardOptions<TData extends Record<string, unknown>> {
30
64
  steps: WizardStepConfig[];
31
65
  initialData?: Partial<TData>;
32
66
  onComplete?: (data: TData) => void | Promise<void>;
33
67
  onCancel?: () => void;
68
+ /**
69
+ * Leave the wizard BACKWARDS: when given, Back is shown on the first step too and
70
+ * calls this — keksdose's import panel, whose first step's Back returns to the
71
+ * dropzone the file came from. No confirm dialog: going back is not discarding.
72
+ */
73
+ onExit?: () => void;
74
+ /**
75
+ * The Done button on the last step after a committing step (see
76
+ * {@link WizardStepConfig.commits}). Without it no Done button is rendered, and the
77
+ * post-commit step is expected to carry its own way out.
78
+ */
79
+ onDone?: () => void;
80
+ /**
81
+ * Whether the chrome offers Cancel at all. Default `true`. `false` hides the button
82
+ * (`wizard.canCancel` is false); `wizard.cancel()` still works if the app calls it.
83
+ */
84
+ cancellable?: boolean;
85
+ /**
86
+ * Whether Cancel asks first ("Discard this form?"). Default `true`. `false` makes
87
+ * `cancel()` call `onCancel` straight away — for a wizard that has nothing to lose
88
+ * yet, or one whose data is kept elsewhere.
89
+ */
90
+ confirmCancel?: boolean;
91
+ /**
92
+ * Mirror the active step to the URL. Default `true` (`?step=N`, needs a router).
93
+ * See {@link WizardUrlSync}.
94
+ *
95
+ * The param is WRITTEN only, never read back (see the note in `use-wizard.ts`). It
96
+ * is removed again when the wizard is left through the kit: a confirmed Cancel,
97
+ * `onExit` from the first step's Back, and Done — each before the app's callback
98
+ * runs, so an app that navigates away navigates from a clean URL. It is NOT removed
99
+ * after `onComplete` on a last step: an app navigating away there would otherwise
100
+ * be pulled back by the kit's own URL write. An app that stays on the page after a
101
+ * final Finish drops the param itself, or uses `urlSync: false`.
102
+ */
103
+ urlSync?: WizardUrlSync;
34
104
  /**
35
105
  * Shown when a step blocks with no per-field detail — an RHF step surfacing
36
106
  * its own inline messages, say — so the user learns why the wizard did not
@@ -55,14 +125,35 @@ export interface UseWizardReturn<TData extends Record<string, unknown>> {
55
125
  steps: WizardStepConfig[];
56
126
  isFirstStep: boolean;
57
127
  isLastStep: boolean;
58
- /** True once every step before the last has been completed or skipped. */
128
+ /** True once every step before the committing one (the last, unless a step is
129
+ * flagged `commits`) has been completed or skipped. */
59
130
  canFinish: boolean;
131
+ /** The step whose forward button commits: the first `commits` step, else the last. */
132
+ commitStepIndex: number;
133
+ /** The active step is {@link commitStepIndex}: the chrome shows Finish, not Next. */
134
+ isCommitStep: boolean;
135
+ /** `onComplete` has resolved. Once the active step is past the committing one, Back
136
+ * cannot cross it and Cancel is hidden. */
137
+ committed: boolean;
138
+ /** Whether Back is offered here: not on the first step unless `onExit` was given,
139
+ * and never back across a commit. (Disabled, not hidden, while `isSubmitting`.) */
140
+ canGoBack: boolean;
141
+ /** Whether Cancel is offered: `cancellable`, and not after a commit. */
142
+ canCancel: boolean;
143
+ /** Whether Done is offered: the last step, past a commit, with an `onDone`. */
144
+ canDone: boolean;
145
+ /** Whether the indicator may jump to `index` — a completed/skipped/active step on
146
+ * the right side of any commit. `goToStep` refuses anything this rejects. */
147
+ canGoToStep: (index: number) => boolean;
60
148
  goNext: () => Promise<void>;
149
+ /** One step back — or, on the first step, `onExit`. Refused while submitting. */
61
150
  goBack: () => void;
62
151
  goToStep: (index: number) => void;
63
152
  skip: () => void;
64
153
  cancel: () => void;
65
154
  finish: () => Promise<void>;
155
+ /** Leave after the post-commit steps: drops the URL param, then calls `onDone`. */
156
+ done: () => void;
66
157
  /** Register a validator for the mounted step (used by useRhfWizardStep). */
67
158
  registerStepValidate: (
68
159
  fn: () => ValidateResult | Promise<ValidateResult>,
@@ -82,6 +173,9 @@ export interface UseWizardReturn<TData extends Record<string, unknown>> {
82
173
 
83
174
  // Loading / error
84
175
  isSubmitting: boolean;
176
+ /** A `goNext`/`finish` is running the step's validators (an async server check,
177
+ * say). The chrome shows Next pending — spinner, `aria-busy`, disabled. */
178
+ isValidating: boolean;
85
179
  error: string | null;
86
180
  clearError: () => void;
87
181
 
@@ -102,8 +196,12 @@ export interface SummaryItem {
102
196
 
103
197
  export interface SummarySection {
104
198
  label: ReactNode;
105
- /** Which step the section's edit button jumps back to. */
106
- stepIndex: number;
199
+ /**
200
+ * Which step the section's edit button jumps back to. Leave it out for a section
201
+ * that belongs to no step — file-level counts, a total — and the card has no edit
202
+ * button, instead of borrowing one from whichever step it was parked under.
203
+ */
204
+ stepIndex?: number;
107
205
  items: SummaryItem[];
108
206
  }
109
207
 
@@ -149,12 +247,15 @@ export interface WizardLabels {
149
247
  /** `wizard.error` when `onComplete` rejects with nothing readable — a thrown
150
248
  * non-Error, or an Error whose message is empty. Optional: see above. */
151
249
  genericError?: string;
250
+ /** The button on the last step after a committing step (`onDone`). Optional: see
251
+ * `missingRequired`. */
252
+ done?: string;
152
253
  }
153
254
 
154
255
  /** Typed with the two optional keys made required, so code reading the defaults
155
256
  * directly (the fallback in `useWizard`) needs no `!`. */
156
257
  export const DEFAULT_WIZARD_LABELS: WizardLabels &
157
- Required<Pick<WizardLabels, "missingRequired" | "genericError">> = {
258
+ Required<Pick<WizardLabels, "missingRequired" | "genericError" | "done">> = {
158
259
  cancel: "Cancel",
159
260
  back: "Back",
160
261
  next: "Next",
@@ -171,6 +272,7 @@ export const DEFAULT_WIZARD_LABELS: WizardLabels &
171
272
  edit: "Edit",
172
273
  missingRequired: "Please fill in all required fields.",
173
274
  genericError: "An error occurred",
275
+ done: "Done",
174
276
  };
175
277
 
176
278
  export function resolveWizardLabels(labels?: Partial<WizardLabels>): WizardLabels {