@eifi1/ui-kit 0.6.1 → 0.7.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 (263) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +3 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +33 -6
  9. package/dist/components/autocomplete.js +62 -16
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +3 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/chip.d.ts +34 -14
  20. package/dist/components/chip.js +63 -28
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +18 -1
  23. package/dist/components/choice-card.js +21 -8
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/combobox-core.d.ts +61 -2
  26. package/dist/components/combobox-core.js +68 -17
  27. package/dist/components/combobox-core.js.map +1 -1
  28. package/dist/components/combobox.d.ts +11 -7
  29. package/dist/components/combobox.js +51 -28
  30. package/dist/components/combobox.js.map +1 -1
  31. package/dist/components/currency-select.js +3 -3
  32. package/dist/components/currency-select.js.map +1 -1
  33. package/dist/components/danger-confirm.d.ts +30 -6
  34. package/dist/components/danger-confirm.js +51 -32
  35. package/dist/components/danger-confirm.js.map +1 -1
  36. package/dist/components/data-table-filter-popover.d.ts +1 -1
  37. package/dist/components/data-table-filters.d.ts +1 -1
  38. package/dist/components/data-table-pagination.d.ts +6 -3
  39. package/dist/components/data-table-pagination.js +6 -5
  40. package/dist/components/data-table-pagination.js.map +1 -1
  41. package/dist/components/data-table.d.ts +1 -1
  42. package/dist/components/data-table.js +332 -248
  43. package/dist/components/data-table.js.map +1 -1
  44. package/dist/components/date-picker.js +17 -7
  45. package/dist/components/date-picker.js.map +1 -1
  46. package/dist/components/dialog-frame.d.ts +28 -3
  47. package/dist/components/dialog-frame.js +47 -11
  48. package/dist/components/dialog-frame.js.map +1 -1
  49. package/dist/components/disclosure.d.ts +22 -2
  50. package/dist/components/disclosure.js +41 -8
  51. package/dist/components/disclosure.js.map +1 -1
  52. package/dist/components/dropdown.d.ts +7 -3
  53. package/dist/components/dropdown.js +6 -2
  54. package/dist/components/dropdown.js.map +1 -1
  55. package/dist/components/entity-combobox.d.ts +17 -8
  56. package/dist/components/entity-combobox.js +7 -5
  57. package/dist/components/entity-combobox.js.map +1 -1
  58. package/dist/components/file-button.d.ts +36 -5
  59. package/dist/components/file-button.js +23 -2
  60. package/dist/components/file-button.js.map +1 -1
  61. package/dist/components/file-dropzone.d.ts +9 -5
  62. package/dist/components/file-dropzone.js +14 -19
  63. package/dist/components/file-dropzone.js.map +1 -1
  64. package/dist/components/grouped-picker.js +2 -2
  65. package/dist/components/grouped-picker.js.map +1 -1
  66. package/dist/components/hover-menu.d.ts +8 -3
  67. package/dist/components/hover-menu.js +88 -4
  68. package/dist/components/hover-menu.js.map +1 -1
  69. package/dist/components/icon-picker.d.ts +10 -1
  70. package/dist/components/icon-picker.js +5 -1
  71. package/dist/components/icon-picker.js.map +1 -1
  72. package/dist/components/measured-grid.d.ts +165 -0
  73. package/dist/components/measured-grid.js +559 -0
  74. package/dist/components/measured-grid.js.map +1 -0
  75. package/dist/components/mini-calendar.js +7 -8
  76. package/dist/components/mini-calendar.js.map +1 -1
  77. package/dist/components/month-picker.js +13 -9
  78. package/dist/components/month-picker.js.map +1 -1
  79. package/dist/components/multi-entity-combobox.js +2 -2
  80. package/dist/components/multi-entity-combobox.js.map +1 -1
  81. package/dist/components/multi-select.d.ts +9 -1
  82. package/dist/components/multi-select.js +22 -8
  83. package/dist/components/multi-select.js.map +1 -1
  84. package/dist/components/number-field.d.ts +3 -0
  85. package/dist/components/number-input.d.ts +3 -0
  86. package/dist/components/number-input.js +3 -3
  87. package/dist/components/number-input.js.map +1 -1
  88. package/dist/components/numpad-sheet.d.ts +3 -0
  89. package/dist/components/numpad-sheet.js +3 -3
  90. package/dist/components/numpad-sheet.js.map +1 -1
  91. package/dist/components/picker-sheet.js +1 -1
  92. package/dist/components/picker-sheet.js.map +1 -1
  93. package/dist/components/popover.js +11 -2
  94. package/dist/components/popover.js.map +1 -1
  95. package/dist/components/series-chart-ticks.d.ts +13 -0
  96. package/dist/components/series-chart-ticks.js +29 -0
  97. package/dist/components/series-chart-ticks.js.map +1 -0
  98. package/dist/components/series-chart.js +33 -27
  99. package/dist/components/series-chart.js.map +1 -1
  100. package/dist/components/swatch-picker.d.ts +10 -1
  101. package/dist/components/swatch-picker.js +2 -0
  102. package/dist/components/swatch-picker.js.map +1 -1
  103. package/dist/components/swipeable-row.d.ts +11 -2
  104. package/dist/components/swipeable-row.js +5 -3
  105. package/dist/components/swipeable-row.js.map +1 -1
  106. package/dist/components/tile-radio.js +5 -7
  107. package/dist/components/tile-radio.js.map +1 -1
  108. package/dist/components/toggle-group.d.ts +13 -3
  109. package/dist/components/toggle-group.js +21 -1
  110. package/dist/components/toggle-group.js.map +1 -1
  111. package/dist/components/toggle-legend.js +1 -2
  112. package/dist/components/toggle-legend.js.map +1 -1
  113. package/dist/components/treemap.d.ts +6 -1
  114. package/dist/components/treemap.js +16 -6
  115. package/dist/components/treemap.js.map +1 -1
  116. package/dist/components/ui.d.ts +31 -7
  117. package/dist/components/ui.js +26 -18
  118. package/dist/components/ui.js.map +1 -1
  119. package/dist/components/use-anchor-dir.d.ts +15 -0
  120. package/dist/components/use-anchor-dir.js +14 -0
  121. package/dist/components/use-anchor-dir.js.map +1 -0
  122. package/dist/components/use-mobile-reveal.d.ts +8 -1
  123. package/dist/components/use-mobile-reveal.js +3 -2
  124. package/dist/components/use-mobile-reveal.js.map +1 -1
  125. package/dist/components/use-table-state.d.ts +4 -4
  126. package/dist/components/use-table-state.js +12 -5
  127. package/dist/components/use-table-state.js.map +1 -1
  128. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  129. package/dist/data-table.d.ts +1 -1
  130. package/dist/data-table.js.map +1 -1
  131. package/dist/feedback/feedback-attachment.d.ts +22 -3
  132. package/dist/feedback/feedback-attachment.js +15 -7
  133. package/dist/feedback/feedback-attachment.js.map +1 -1
  134. package/dist/feedback/feedback-dialog.d.ts +3 -2
  135. package/dist/feedback/feedback-dialog.js.map +1 -1
  136. package/dist/feedback.d.ts +1 -1
  137. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  138. package/dist/hooks/use-windowed-rows.js +31 -0
  139. package/dist/hooks/use-windowed-rows.js.map +1 -0
  140. package/dist/i18n/defaults.d.ts +3 -0
  141. package/dist/i18n/defaults.js +5 -1
  142. package/dist/i18n/defaults.js.map +1 -1
  143. package/dist/i18n/kit-labels.d.ts +5 -0
  144. package/dist/i18n/kit-labels.js.map +1 -1
  145. package/dist/i18n/locales/de-CH.d.ts +43 -0
  146. package/dist/i18n/locales/de-CH.js +21 -0
  147. package/dist/i18n/locales/de-CH.js.map +1 -0
  148. package/dist/i18n/locales/de.d.ts +43 -0
  149. package/dist/i18n/locales/de.js +344 -0
  150. package/dist/i18n/locales/de.js.map +1 -0
  151. package/dist/i18n/locales/es.d.ts +43 -0
  152. package/dist/i18n/locales/es.js +339 -0
  153. package/dist/i18n/locales/es.js.map +1 -0
  154. package/dist/i18n/locales/fr.d.ts +43 -0
  155. package/dist/i18n/locales/fr.js +344 -0
  156. package/dist/i18n/locales/fr.js.map +1 -0
  157. package/dist/i18n/locales/hu.d.ts +43 -0
  158. package/dist/i18n/locales/hu.js +343 -0
  159. package/dist/i18n/locales/hu.js.map +1 -0
  160. package/dist/i18n/locales/it.d.ts +43 -0
  161. package/dist/i18n/locales/it.js +342 -0
  162. package/dist/i18n/locales/it.js.map +1 -0
  163. package/dist/i18n/locales/zh.d.ts +43 -0
  164. package/dist/i18n/locales/zh.js +338 -0
  165. package/dist/i18n/locales/zh.js.map +1 -0
  166. package/dist/index.d.ts +6 -3
  167. package/dist/index.js +4 -1
  168. package/dist/index.js.map +1 -1
  169. package/dist/lib/direction.d.ts +24 -0
  170. package/dist/lib/direction.js +19 -0
  171. package/dist/lib/direction.js.map +1 -0
  172. package/dist/rhf/form.js.map +1 -1
  173. package/dist/search/command-palette.d.ts +5 -0
  174. package/dist/search/command-palette.js +80 -50
  175. package/dist/search/command-palette.js.map +1 -1
  176. package/dist/shell/app-shell.d.ts +13 -1
  177. package/dist/shell/app-shell.js +133 -84
  178. package/dist/shell/app-shell.js.map +1 -1
  179. package/dist/shell/topbar-action-menu.js +1 -1
  180. package/dist/shell/topbar-action-menu.js.map +1 -1
  181. package/dist/shell/topbar-controls.d.ts +7 -3
  182. package/dist/shell/topbar-controls.js +38 -31
  183. package/dist/shell/topbar-controls.js.map +1 -1
  184. package/dist/theme/chart-palette.d.ts +7 -1
  185. package/dist/theme/chart-palette.js +2 -1
  186. package/dist/theme/chart-palette.js.map +1 -1
  187. package/dist/tour/tour.d.ts +11 -5
  188. package/dist/tour/tour.js +124 -58
  189. package/dist/tour/tour.js.map +1 -1
  190. package/dist/wizard/use-wizard.js +30 -13
  191. package/dist/wizard/use-wizard.js.map +1 -1
  192. package/dist/wizard/wizard-summary.js +1 -1
  193. package/dist/wizard/wizard-summary.js.map +1 -1
  194. package/package.json +12 -2
  195. package/src/components/account-settings.tsx +6 -2
  196. package/src/components/amount-input.tsx +10 -8
  197. package/src/components/autocomplete.tsx +110 -24
  198. package/src/components/calculator.tsx +5 -1
  199. package/src/components/chart-zoom.tsx +8 -1
  200. package/src/components/chart.tsx +68 -7
  201. package/src/components/chip.tsx +131 -49
  202. package/src/components/choice-card.tsx +77 -36
  203. package/src/components/combobox-core.tsx +147 -20
  204. package/src/components/combobox.tsx +100 -44
  205. package/src/components/currency-select.tsx +6 -6
  206. package/src/components/danger-confirm.tsx +90 -36
  207. package/src/components/data-table-pagination.tsx +32 -19
  208. package/src/components/data-table.tsx +198 -36
  209. package/src/components/date-picker.tsx +24 -7
  210. package/src/components/dialog-frame.tsx +97 -22
  211. package/src/components/disclosure.tsx +75 -13
  212. package/src/components/dropdown.tsx +17 -5
  213. package/src/components/entity-combobox.tsx +26 -13
  214. package/src/components/file-button.tsx +74 -5
  215. package/src/components/file-dropzone.tsx +44 -35
  216. package/src/components/grouped-picker.tsx +2 -2
  217. package/src/components/hover-menu.tsx +139 -10
  218. package/src/components/icon-picker.tsx +14 -1
  219. package/src/components/measured-grid.tsx +852 -0
  220. package/src/components/mini-calendar.tsx +11 -8
  221. package/src/components/month-picker.tsx +18 -11
  222. package/src/components/multi-entity-combobox.tsx +2 -2
  223. package/src/components/multi-select.tsx +34 -7
  224. package/src/components/number-input.tsx +4 -4
  225. package/src/components/numpad-sheet.tsx +5 -3
  226. package/src/components/picker-sheet.tsx +1 -1
  227. package/src/components/popover.tsx +18 -3
  228. package/src/components/series-chart-ticks.ts +52 -0
  229. package/src/components/series-chart.tsx +39 -31
  230. package/src/components/swatch-picker.tsx +11 -0
  231. package/src/components/swipeable-row.tsx +21 -6
  232. package/src/components/tile-radio.tsx +5 -7
  233. package/src/components/toggle-group.tsx +39 -3
  234. package/src/components/toggle-legend.tsx +5 -2
  235. package/src/components/treemap.tsx +28 -5
  236. package/src/components/ui.tsx +74 -30
  237. package/src/components/use-anchor-dir.ts +20 -0
  238. package/src/components/use-mobile-reveal.ts +10 -2
  239. package/src/components/use-table-state.ts +44 -7
  240. package/src/data-table.ts +1 -0
  241. package/src/feedback/feedback-attachment.tsx +36 -9
  242. package/src/feedback/feedback-dialog.tsx +3 -2
  243. package/src/hooks/use-windowed-rows.ts +69 -0
  244. package/src/i18n/defaults.ts +4 -0
  245. package/src/i18n/kit-labels.tsx +4 -0
  246. package/src/i18n/locales/de-CH.ts +30 -0
  247. package/src/i18n/locales/de.ts +365 -0
  248. package/src/i18n/locales/es.ts +358 -0
  249. package/src/i18n/locales/fr.ts +371 -0
  250. package/src/i18n/locales/hu.ts +359 -0
  251. package/src/i18n/locales/it.ts +360 -0
  252. package/src/i18n/locales/zh.ts +348 -0
  253. package/src/index.ts +10 -1
  254. package/src/lib/direction.ts +37 -0
  255. package/src/rhf/form.tsx +21 -0
  256. package/src/search/command-palette.tsx +77 -19
  257. package/src/shell/app-shell.tsx +200 -129
  258. package/src/shell/topbar-action-menu.tsx +1 -1
  259. package/src/shell/topbar-controls.tsx +19 -8
  260. package/src/theme/chart-palette.ts +9 -2
  261. package/src/tour/tour.tsx +119 -18
  262. package/src/wizard/use-wizard.ts +55 -17
  263. package/src/wizard/wizard-summary.tsx +1 -1
@@ -0,0 +1,559 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { ClipboardPaste, Eraser, Plus, Trash2 } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { horizontalStep } from "../lib/direction.js";
7
+ import { cellNumber, isCellNumber, parseRows, splitRow } from "../lib/table-text.js";
8
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
9
+ import { useWindowedRows } from "../hooks/use-windowed-rows.js";
10
+ import { ToggleGroup } from "./toggle-group.js";
11
+ import { Button, FIELD_INVALID, Textarea } from "./ui.js";
12
+ const DEFAULT_MEASURED_GRID_LABELS = {
13
+ view: "Table view",
14
+ cellsView: "Cells",
15
+ textView: "Text",
16
+ addRow: "Add row",
17
+ removeRow: (row) => `Remove row ${row}`,
18
+ clear: "Clear table",
19
+ pasteHint: "Paste a block from a spreadsheet into any cell",
20
+ cell: (column, row) => `${column}, row ${row}`,
21
+ rowNumber: "Row",
22
+ rowActions: "Row actions",
23
+ keyboardHint: "Arrow keys move between cells. Type to replace a cell, F2 to edit it, Escape to undo the edit. Enter moves down and adds a row at the end.",
24
+ lineError: (line) => `Line ${line} could not be read`,
25
+ points: (count) => count === 1 ? "1 point" : `${count} points`,
26
+ problems: (count) => count === 1 ? "1 cell is not a number" : `${count} cells are not numbers`
27
+ };
28
+ const BLANK = "";
29
+ const DEFAULT_ROW_HEIGHT = 28;
30
+ const ALL_VIEWS = ["cells", "text"];
31
+ function decimalMark(locale) {
32
+ try {
33
+ const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
34
+ return part?.value === "," ? "," : ".";
35
+ } catch {
36
+ return ".";
37
+ }
38
+ }
39
+ const withMark = (cell, mark) => mark === "," && isCellNumber(cell) ? cell.replace(".", ",") : cell;
40
+ const formatCell = (value, digits, mark) => withMark(String(Number(value.toFixed(digits))), mark);
41
+ const hasContent = (row) => row.some((cell) => cell.trim() !== "");
42
+ function useMeasuredRows(initial, options = {}) {
43
+ const { digits = 4 } = options;
44
+ const mark = decimalMark(useKitLocale(options.locale));
45
+ const [cells, setCells] = useState(
46
+ () => (typeof initial === "function" ? initial() : initial).map(
47
+ (row) => row.map((value) => formatCell(value, digits, mark))
48
+ )
49
+ );
50
+ const { rows, ready, problems } = useMemo(() => {
51
+ const filled = cells.filter(hasContent);
52
+ const whole = filled.length >= 2 && filled.every((row) => row.every(isCellNumber));
53
+ return {
54
+ rows: whole ? filled.map((row) => row.map(cellNumber)) : [],
55
+ ready: whole,
56
+ problems: filled.flat().filter((cell) => cell.trim() !== "" && !isCellNumber(cell)).length
57
+ };
58
+ }, [cells]);
59
+ const setRows = useCallback(
60
+ (next) => setCells(next.map((row) => row.map((value) => formatCell(value, digits, mark)))),
61
+ [digits, mark]
62
+ );
63
+ const props = useMemo(() => ({ cells, onCells: setCells }), [cells]);
64
+ return { cells, setCells, rows, setRows, ready, problems, props };
65
+ }
66
+ function MeasuredGrid({
67
+ label,
68
+ hint,
69
+ columns,
70
+ cells,
71
+ onCells,
72
+ disabled,
73
+ views = ALL_VIEWS,
74
+ rowHeight = DEFAULT_ROW_HEIGHT,
75
+ locale: localeProp,
76
+ labels: labelsProp,
77
+ className,
78
+ ...rest
79
+ }) {
80
+ const labels = useKitLabels("measuredGrid", DEFAULT_MEASURED_GRID_LABELS, labelsProp);
81
+ const locale = useKitLocale(localeProp);
82
+ const mark = decimalMark(locale);
83
+ const id = useId();
84
+ const labelId = `${id}-label`;
85
+ const hintId = `${id}-hint`;
86
+ const keysId = `${id}-keys`;
87
+ const offered = views.length ? views : ALL_VIEWS;
88
+ const [chosen, setChosen] = useState(offered[0]);
89
+ const view = offered.includes(chosen) ? chosen : offered[0];
90
+ return (
91
+ // `relative` so the keyboard description below can be `sr-only` without escaping
92
+ // to the initial containing block (see sr-only-containment.test).
93
+ /* @__PURE__ */ jsxs("div", { ...rest, className: cn("relative", className), children: [
94
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex flex-wrap items-center justify-between gap-2", children: [
95
+ /* @__PURE__ */ jsx("span", { id: labelId, className: "text-[11px] font-medium text-[var(--text-muted)]", children: label }),
96
+ offered.length > 1 && /* @__PURE__ */ jsx(
97
+ ToggleGroup,
98
+ {
99
+ className: "w-auto",
100
+ "aria-label": labels.view,
101
+ value: view,
102
+ onChange: setChosen,
103
+ options: offered.map((value) => ({
104
+ value,
105
+ label: value === "cells" ? labels.cellsView : labels.textView
106
+ }))
107
+ }
108
+ )
109
+ ] }),
110
+ view === "cells" ? /* @__PURE__ */ jsx(
111
+ CellGrid,
112
+ {
113
+ columns,
114
+ cells,
115
+ onCells,
116
+ disabled,
117
+ rowHeight,
118
+ locale,
119
+ mark,
120
+ labels,
121
+ labelledBy: labelId,
122
+ describedBy: hint ? `${keysId} ${hintId}` : keysId
123
+ }
124
+ ) : /* @__PURE__ */ jsx(
125
+ TextView,
126
+ {
127
+ columns,
128
+ cells,
129
+ onCells,
130
+ disabled,
131
+ mark,
132
+ labels,
133
+ labelledBy: labelId,
134
+ describedBy: hint ? hintId : void 0
135
+ }
136
+ ),
137
+ hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "mt-1 px-1 text-[11px] text-[var(--text-muted)]", children: hint }),
138
+ view === "cells" && /* @__PURE__ */ jsx("span", { id: keysId, className: "sr-only", children: labels.keyboardHint })
139
+ ] })
140
+ );
141
+ }
142
+ function CellGrid({
143
+ columns,
144
+ cells,
145
+ onCells,
146
+ disabled,
147
+ rowHeight,
148
+ locale,
149
+ mark,
150
+ labels,
151
+ labelledBy,
152
+ describedBy
153
+ }) {
154
+ const scrollRef = useRef(null);
155
+ const headRef = useRef(null);
156
+ const width = columns.length;
157
+ const blankRow = () => columns.map(() => BLANK);
158
+ const grid = cells.length ? cells : [blankRow()];
159
+ const { first, last, totalHeight } = useWindowedRows(grid.length, rowHeight, scrollRef);
160
+ const rowNumber = useMemo(() => new Intl.NumberFormat(locale), [locale]);
161
+ const removable = !disabled && cells.length > 0;
162
+ const lastColumn = removable ? width : width - 1;
163
+ const [caret, setCaret] = useState({ row: 0, column: 0 });
164
+ const at = { row: Math.min(caret.row, grid.length - 1), column: Math.min(caret.column, lastColumn) };
165
+ const [editing, setEditing] = useState(false);
166
+ const pending = useRef(null);
167
+ const origin = useRef(BLANK);
168
+ const pointer = useRef(false);
169
+ useEffect(() => {
170
+ const target = pending.current;
171
+ if (!target) return;
172
+ const element = scrollRef.current?.querySelector(
173
+ `[data-cell="${target.row}:${target.column}"]`
174
+ );
175
+ if (!element) return;
176
+ pending.current = null;
177
+ element.focus({ preventScroll: true });
178
+ if (target.select && element instanceof HTMLInputElement) element.select();
179
+ });
180
+ const pad = (row) => Array.from({ length: width }, (_, index) => row?.[index] ?? BLANK);
181
+ const setCell = (row, column, value) => {
182
+ const next = grid.map(pad);
183
+ next[row][column] = value;
184
+ onCells(next);
185
+ };
186
+ const reveal = (row) => {
187
+ const element = scrollRef.current;
188
+ if (!element) return;
189
+ const room = element.clientHeight - (headRef.current?.offsetHeight ?? 0);
190
+ if (room <= 0) return;
191
+ const top = row * rowHeight;
192
+ if (top < element.scrollTop) element.scrollTop = top;
193
+ else if (top + rowHeight > element.scrollTop + room) element.scrollTop = top + rowHeight - room;
194
+ };
195
+ const focusCell = (row, column) => {
196
+ reveal(row);
197
+ pending.current = { row, column, select: true };
198
+ setCaret({ row, column });
199
+ setEditing(false);
200
+ };
201
+ const go = (row, column) => {
202
+ const target = {
203
+ row: Math.max(0, Math.min(row, grid.length - 1)),
204
+ column: Math.max(0, Math.min(column, lastColumn))
205
+ };
206
+ if (target.row === at.row && target.column === at.column) return;
207
+ focusCell(target.row, target.column);
208
+ };
209
+ const down = (row, column) => {
210
+ if (row + 1 < grid.length) return go(row + 1, column);
211
+ if (disabled || !hasContent(grid[row])) return;
212
+ onCells([...grid.map(pad), blankRow()]);
213
+ focusCell(row + 1, Math.min(column, width - 1));
214
+ };
215
+ const page = () => {
216
+ const element = scrollRef.current;
217
+ const room = element ? element.clientHeight - (headRef.current?.offsetHeight ?? 0) : 0;
218
+ return room > 0 ? Math.max(1, Math.floor(room / rowHeight) - 1) : 10;
219
+ };
220
+ const onCellKeyDown = (event, row, column) => {
221
+ const input = event.currentTarget instanceof HTMLInputElement ? event.currentTarget : null;
222
+ const inText = Boolean(input) && editing;
223
+ const modified = event.ctrlKey || event.metaKey;
224
+ switch (event.key) {
225
+ case "ArrowUp":
226
+ event.preventDefault();
227
+ return go(row - 1, column);
228
+ case "ArrowDown":
229
+ event.preventDefault();
230
+ return down(row, column);
231
+ case "Enter":
232
+ if (!input) return;
233
+ event.preventDefault();
234
+ return event.shiftKey ? go(row - 1, column) : down(row, column);
235
+ case "ArrowLeft":
236
+ case "ArrowRight": {
237
+ const forward = horizontalStep(event.key, event.currentTarget) === 1;
238
+ if (inText && input) {
239
+ const edge = forward ? input.value.length : 0;
240
+ if (input.selectionStart !== edge || input.selectionEnd !== edge) return;
241
+ }
242
+ event.preventDefault();
243
+ return go(row, column + (forward ? 1 : -1));
244
+ }
245
+ case "Home":
246
+ case "End": {
247
+ if (inText && !modified) return;
248
+ event.preventDefault();
249
+ const home = event.key === "Home";
250
+ if (modified) return go(home ? 0 : grid.length - 1, home ? 0 : width - 1);
251
+ return go(row, home ? 0 : width - 1);
252
+ }
253
+ case "PageUp":
254
+ event.preventDefault();
255
+ return go(row - page(), column);
256
+ case "PageDown":
257
+ event.preventDefault();
258
+ return go(row + page(), column);
259
+ case "F2":
260
+ if (!input || disabled) return;
261
+ event.preventDefault();
262
+ setEditing(true);
263
+ input.setSelectionRange(input.value.length, input.value.length);
264
+ return;
265
+ case "Escape":
266
+ if (!input || !editing) return;
267
+ event.preventDefault();
268
+ event.stopPropagation();
269
+ if (input.value !== origin.current) setCell(row, column, origin.current);
270
+ pending.current = { row, column, select: true };
271
+ setEditing(false);
272
+ return;
273
+ }
274
+ };
275
+ const paste = (row, column, text) => {
276
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
277
+ if (!lines.length) return;
278
+ const block = lines.map(splitRow);
279
+ if (block.length > 1 && block[0].some((cell) => !isCellNumber(cell))) block.shift();
280
+ const height = Math.max(grid.length, row + block.length);
281
+ const next = Array.from({ length: height }, (_, index) => pad(grid[index]));
282
+ block.forEach((line, downwards) => {
283
+ line.forEach((cell, across) => {
284
+ const target = column + across;
285
+ if (target < width) next[row + downwards][target] = withMark(cell.trim(), mark);
286
+ });
287
+ });
288
+ onCells(next);
289
+ pending.current = { row, column, select: true };
290
+ setEditing(false);
291
+ };
292
+ const template = `2.5rem repeat(${width}, minmax(5.5rem, 1fr)) 2rem`;
293
+ const mounted = [];
294
+ for (let index = first; index < last; index += 1) mounted.push(index);
295
+ if (at.row < first || at.row >= last) mounted.push(at.row);
296
+ const filled = cells.filter(hasContent);
297
+ const problems = filled.flat().filter((cell) => cell.trim() !== "" && !isCellNumber(cell)).length;
298
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-[var(--border)] bg-[var(--bg-surface)]", children: [
299
+ /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "max-h-72 overflow-auto", children: /* @__PURE__ */ jsxs(
300
+ "div",
301
+ {
302
+ role: "grid",
303
+ "aria-labelledby": labelledBy,
304
+ "aria-describedby": describedBy,
305
+ "aria-rowcount": grid.length + 1,
306
+ "aria-colcount": width + 2,
307
+ "aria-readonly": disabled || void 0,
308
+ className: "text-xs",
309
+ style: { minWidth: `calc(4.5rem + ${width} * 5.5rem)` },
310
+ children: [
311
+ /* @__PURE__ */ jsx("div", { ref: headRef, role: "rowgroup", className: "sticky top-0 z-10 bg-[var(--bg-surface-2)]", children: /* @__PURE__ */ jsxs(
312
+ "div",
313
+ {
314
+ role: "row",
315
+ "aria-rowindex": 1,
316
+ className: "grid border-b border-[var(--border)]",
317
+ style: { gridTemplateColumns: template },
318
+ children: [
319
+ /* @__PURE__ */ jsx(
320
+ "div",
321
+ {
322
+ role: "columnheader",
323
+ "aria-label": labels.rowNumber,
324
+ className: "flex items-end justify-end px-1 py-1 text-[10px] text-[var(--text-placeholder)]",
325
+ children: "#"
326
+ }
327
+ ),
328
+ columns.map((column, index) => /* @__PURE__ */ jsxs(
329
+ "div",
330
+ {
331
+ role: "columnheader",
332
+ className: "min-w-0 border-s border-[var(--border)] px-2 py-1 text-end",
333
+ children: [
334
+ /* @__PURE__ */ jsx("span", { className: "block truncate font-medium text-[var(--text-primary)]", children: column.label }),
335
+ column.unit && /* @__PURE__ */ jsx("span", { className: "block truncate text-[10px] text-[var(--text-muted)]", children: column.unit })
336
+ ]
337
+ },
338
+ index
339
+ )),
340
+ /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ role: "columnheader",
344
+ "aria-label": labels.rowActions,
345
+ className: "border-s border-[var(--border)]"
346
+ }
347
+ )
348
+ ]
349
+ }
350
+ ) }),
351
+ /* @__PURE__ */ jsx("div", { role: "rowgroup", className: "relative", style: { height: totalHeight }, children: mounted.map((index) => {
352
+ const row = grid[index];
353
+ return /* @__PURE__ */ jsxs(
354
+ "div",
355
+ {
356
+ role: "row",
357
+ "aria-rowindex": index + 2,
358
+ className: "absolute inset-x-0 grid hover:bg-[var(--bg-hover)]",
359
+ style: { top: index * rowHeight, height: rowHeight, gridTemplateColumns: template },
360
+ children: [
361
+ /* @__PURE__ */ jsx(
362
+ "div",
363
+ {
364
+ role: "rowheader",
365
+ className: "flex items-center justify-end px-1 text-[10px] tabular-nums text-[var(--text-placeholder)]",
366
+ children: rowNumber.format(index + 1)
367
+ }
368
+ ),
369
+ columns.map((column, columnIndex) => {
370
+ const value = row[columnIndex] ?? BLANK;
371
+ const bad = value.trim() !== "" && !isCellNumber(value);
372
+ const isCaret = at.row === index && at.column === columnIndex;
373
+ return /* @__PURE__ */ jsx("div", { role: "gridcell", className: "min-w-0 border-s border-[var(--border)]", children: /* @__PURE__ */ jsx(
374
+ "input",
375
+ {
376
+ "data-cell": `${index}:${columnIndex}`,
377
+ value,
378
+ readOnly: disabled,
379
+ tabIndex: isCaret ? 0 : -1,
380
+ inputMode: "decimal",
381
+ spellCheck: false,
382
+ autoComplete: "off",
383
+ "aria-label": labels.cell(column.label, index + 1),
384
+ "aria-invalid": bad || void 0,
385
+ className: cn(
386
+ "block size-full bg-transparent px-2 text-end tabular-nums text-[var(--text-primary)] outline-none",
387
+ // Arriving selects the text (typing replaces it); editing
388
+ // in place is the plain surface with a caret in it.
389
+ "focus:bg-[var(--brand-bg)] focus:ring-2 focus:ring-inset focus:ring-[var(--brand)]",
390
+ isCaret && editing && "focus:bg-[var(--bg-surface)]",
391
+ disabled && "text-[var(--text-muted)]",
392
+ bad && cn(FIELD_INVALID, "ring-inset")
393
+ ),
394
+ onMouseDown: (event) => {
395
+ if (document.activeElement === event.currentTarget) setEditing(!disabled);
396
+ else pointer.current = true;
397
+ },
398
+ onFocus: (event) => {
399
+ origin.current = event.currentTarget.value;
400
+ if (!isCaret) setCaret({ row: index, column: columnIndex });
401
+ const byPointer = pointer.current;
402
+ pointer.current = false;
403
+ setEditing(byPointer && !disabled);
404
+ if (!byPointer) event.currentTarget.select();
405
+ },
406
+ onBlur: () => setEditing(false),
407
+ onChange: (event) => {
408
+ setCell(index, columnIndex, event.target.value);
409
+ setEditing(true);
410
+ },
411
+ onPaste: (event) => {
412
+ if (disabled) return;
413
+ const text = event.clipboardData.getData("text/plain");
414
+ if (!/[\t\n;]/.test(text)) return;
415
+ event.preventDefault();
416
+ paste(index, columnIndex, text);
417
+ },
418
+ onKeyDown: (event) => onCellKeyDown(event, index, columnIndex)
419
+ }
420
+ ) }, columnIndex);
421
+ }),
422
+ /* @__PURE__ */ jsx(
423
+ "div",
424
+ {
425
+ role: "gridcell",
426
+ className: "flex items-center justify-center border-s border-[var(--border)]",
427
+ children: /* @__PURE__ */ jsx(
428
+ "button",
429
+ {
430
+ type: "button",
431
+ "data-cell": `${index}:${width}`,
432
+ disabled: !removable,
433
+ tabIndex: at.row === index && at.column === width ? 0 : -1,
434
+ "aria-label": labels.removeRow(index + 1),
435
+ className: "rounded p-1 text-[var(--text-placeholder)] outline-none hover:text-[var(--danger)] focus-visible:ring-2 focus-visible:ring-[var(--brand)] disabled:cursor-default disabled:hover:text-[var(--text-placeholder)]",
436
+ onKeyDown: (event) => onCellKeyDown(event, index, width),
437
+ onFocus: () => {
438
+ if (at.row !== index || at.column !== width) setCaret({ row: index, column: width });
439
+ },
440
+ onClick: () => {
441
+ const next = grid.filter((_, other) => other !== index);
442
+ onCells(next);
443
+ if (next.length) focusCell(Math.min(index, next.length - 1), width);
444
+ else focusCell(0, 0);
445
+ },
446
+ children: /* @__PURE__ */ jsx(Trash2, { "aria-hidden": true, className: "size-3.5" })
447
+ }
448
+ )
449
+ }
450
+ )
451
+ ]
452
+ },
453
+ index
454
+ );
455
+ }) })
456
+ ]
457
+ }
458
+ ) }),
459
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-[var(--border)] px-2 py-1.5", children: [
460
+ /* @__PURE__ */ jsxs(
461
+ Button,
462
+ {
463
+ variant: "ghost",
464
+ className: "px-2 py-1 text-xs",
465
+ disabled,
466
+ onClick: () => {
467
+ onCells([...grid.map(pad), blankRow()]);
468
+ focusCell(grid.length, 0);
469
+ },
470
+ children: [
471
+ /* @__PURE__ */ jsx(Plus, { "aria-hidden": true, className: "size-3.5" }),
472
+ " ",
473
+ labels.addRow
474
+ ]
475
+ }
476
+ ),
477
+ /* @__PURE__ */ jsxs(
478
+ Button,
479
+ {
480
+ variant: "ghost",
481
+ className: "px-2 py-1 text-xs text-[var(--text-muted)]",
482
+ disabled: disabled || !cells.length,
483
+ onClick: () => {
484
+ onCells([]);
485
+ focusCell(0, 0);
486
+ },
487
+ children: [
488
+ /* @__PURE__ */ jsx(Eraser, { "aria-hidden": true, className: "size-3.5" }),
489
+ " ",
490
+ labels.clear
491
+ ]
492
+ }
493
+ ),
494
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] tabular-nums text-[var(--text-muted)]", children: labels.points(filled.length) }),
495
+ problems > 0 && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-[var(--danger)]", children: labels.problems(problems) }),
496
+ /* @__PURE__ */ jsxs("span", { className: "ms-auto flex items-center gap-1 text-[11px] text-[var(--text-muted)]", children: [
497
+ /* @__PURE__ */ jsx(ClipboardPaste, { "aria-hidden": true, className: "size-3.5" }),
498
+ " ",
499
+ labels.pasteHint
500
+ ] })
501
+ ] })
502
+ ] });
503
+ }
504
+ const textOf = (cells) => cells.map((row) => row.join(" ")).join("\n");
505
+ function TextView({ columns, cells, onCells, disabled, mark, labels, labelledBy, describedBy }) {
506
+ const text = textOf(cells);
507
+ const [draft, setDraft] = useState(text);
508
+ const [errorLine, setErrorLine] = useState(null);
509
+ const [seen, setSeen] = useState(text);
510
+ if (seen !== text) {
511
+ setSeen(text);
512
+ setDraft(text);
513
+ setErrorLine(null);
514
+ }
515
+ const commit = () => {
516
+ if (draft === text) return;
517
+ if (!draft.trim()) {
518
+ setErrorLine(null);
519
+ onCells([]);
520
+ return;
521
+ }
522
+ const parsed = parseRows(draft, columns.length);
523
+ if ("error" in parsed) {
524
+ setErrorLine(parsed.error);
525
+ return;
526
+ }
527
+ setErrorLine(null);
528
+ onCells(parsed.rows.map((row) => row.map((value) => withMark(String(value), mark))));
529
+ };
530
+ const lines = draft.split("\n").filter((line) => line.trim()).length;
531
+ return /* @__PURE__ */ jsxs("div", { children: [
532
+ /* @__PURE__ */ jsx(
533
+ Textarea,
534
+ {
535
+ "aria-labelledby": labelledBy,
536
+ "aria-describedby": describedBy,
537
+ rows: 10,
538
+ value: draft,
539
+ readOnly: disabled,
540
+ spellCheck: false,
541
+ className: "font-mono text-xs",
542
+ error: errorLine !== null ? labels.lineError(errorLine) : void 0,
543
+ onChange: (event) => setDraft(event.target.value),
544
+ onBlur: commit
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 px-1 text-[11px] text-[var(--text-muted)]", children: [
548
+ columns.map((column) => column.label).join(" \xB7 "),
549
+ " \u2014 ",
550
+ labels.points(lines)
551
+ ] })
552
+ ] });
553
+ }
554
+ export {
555
+ DEFAULT_MEASURED_GRID_LABELS,
556
+ MeasuredGrid,
557
+ useMeasuredRows
558
+ };
559
+ //# sourceMappingURL=measured-grid.js.map