@eifi1/ui-kit 0.22.0 → 0.24.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 (208) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +8 -7
  3. package/dist/components/amount-input.d.ts +34 -7
  4. package/dist/components/amount-input.js +14 -10
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +26 -0
  7. package/dist/components/autocomplete.js +164 -137
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +8 -7
  10. package/dist/components/calculator.d.ts +8 -7
  11. package/dist/components/column-mapper.d.ts +235 -0
  12. package/dist/components/column-mapper.js +411 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +113 -3
  18. package/dist/components/combobox.js +456 -313
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +8 -7
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +5 -4
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +8 -7
  28. package/dist/components/danger-confirm.js +64 -7
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +48 -2
  34. package/dist/components/entity-combobox.js +159 -114
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +8 -7
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +8 -7
  44. package/dist/components/file-dropzone.d.ts +8 -7
  45. package/dist/components/form-actions.d.ts +8 -7
  46. package/dist/components/form-actions.js +23 -3
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +8 -7
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +8 -7
  53. package/dist/components/money-field.d.ts +11 -7
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +31 -2
  58. package/dist/components/multi-entity-combobox.js +152 -103
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +8 -7
  61. package/dist/components/number-input.d.ts +8 -7
  62. package/dist/components/numpad-sheet.d.ts +8 -7
  63. package/dist/components/phone-input.d.ts +8 -7
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +8 -7
  68. package/dist/components/settings-fields.d.ts +8 -7
  69. package/dist/components/share-card.d.ts +8 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +8 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +8 -7
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +5 -4
  84. package/dist/components/ui.js +60 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +66 -1
  87. package/dist/feedback/feedback-attachment.js +185 -65
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +66 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +67 -252
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +66 -287
  94. package/dist/feedback/feedback-thread.js +16 -3
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
  97. package/dist/feedback.d.ts +65 -3
  98. package/dist/hooks/use-file-drop.d.ts +8 -7
  99. package/dist/i18n/defaults.d.ts +8 -7
  100. package/dist/i18n/defaults.js +3 -1
  101. package/dist/i18n/defaults.js.map +1 -1
  102. package/dist/i18n/german.d.ts +8 -7
  103. package/dist/i18n/german.js +36 -2
  104. package/dist/i18n/german.js.map +1 -1
  105. package/dist/i18n/kit-labels.d.ts +5 -4
  106. package/dist/i18n/kit-labels.js.map +1 -1
  107. package/dist/i18n/languages.d.ts +8 -7
  108. package/dist/i18n/locales/de-CH.d.ts +8 -7
  109. package/dist/i18n/locales/en.d.ts +8 -7
  110. package/dist/i18n/locales/en.js +7 -0
  111. package/dist/i18n/locales/en.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +8 -7
  113. package/dist/i18n/locales/es.js +37 -2
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +8 -7
  116. package/dist/i18n/locales/fr.js +36 -2
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +8 -7
  119. package/dist/i18n/locales/hu.js +38 -2
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +8 -7
  122. package/dist/i18n/locales/it.js +37 -2
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +8 -7
  125. package/dist/i18n/locales/zh.js +34 -2
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/i18n/review.d.ts +8 -7
  128. package/dist/i18n/review.js +20 -1
  129. package/dist/i18n/review.js.map +1 -1
  130. package/dist/index.d.ts +6 -4
  131. package/dist/index.js +16 -0
  132. package/dist/index.js.map +1 -1
  133. package/dist/lib/column-mapping.d.ts +81 -0
  134. package/dist/lib/column-mapping.js +108 -0
  135. package/dist/lib/column-mapping.js.map +1 -0
  136. package/dist/lib/strip-fade.d.ts +2 -2
  137. package/dist/lib/strip-fade.js.map +1 -1
  138. package/dist/lib/table-text.d.ts +109 -1
  139. package/dist/lib/table-text.js +122 -1
  140. package/dist/lib/table-text.js.map +1 -1
  141. package/dist/rhf/fields.d.ts +142 -13
  142. package/dist/rhf/fields.js +185 -3
  143. package/dist/rhf/fields.js.map +1 -1
  144. package/dist/rhf/form.d.ts +8 -7
  145. package/dist/rhf.d.ts +9 -8
  146. package/dist/rhf.js.map +1 -1
  147. package/dist/shell/app-shell.d.ts +8 -7
  148. package/dist/shell/top-bar-brand.d.ts +8 -7
  149. package/dist/shell/topbar-action-menu.d.ts +36 -3
  150. package/dist/shell/topbar-action-menu.js +74 -33
  151. package/dist/shell/topbar-action-menu.js.map +1 -1
  152. package/dist/shell.d.ts +8 -7
  153. package/dist/table-text.d.ts +1 -1
  154. package/dist/wizard/stepper-nav.d.ts +38 -9
  155. package/dist/wizard/stepper-nav.js +4 -0
  156. package/dist/wizard/stepper-nav.js.map +1 -1
  157. package/dist/wizard.d.ts +8 -7
  158. package/package.json +1 -1
  159. package/src/components/amount-input.tsx +50 -14
  160. package/src/components/autocomplete.tsx +188 -116
  161. package/src/components/column-mapper.tsx +768 -0
  162. package/src/components/combobox-core.tsx +82 -9
  163. package/src/components/combobox.tsx +655 -335
  164. package/src/components/confirm-dialog.tsx +12 -8
  165. package/src/components/country-select.tsx +135 -22
  166. package/src/components/danger-confirm.tsx +274 -24
  167. package/src/components/date-picker.tsx +423 -10
  168. package/src/components/entity-combobox.tsx +243 -129
  169. package/src/components/field-parts.tsx +224 -5
  170. package/src/components/field-strip.tsx +149 -0
  171. package/src/components/form-actions.tsx +52 -5
  172. package/src/components/icon-picker.tsx +23 -4
  173. package/src/components/money-field.tsx +3 -0
  174. package/src/components/month-picker.tsx +2 -1
  175. package/src/components/multi-entity-combobox.tsx +221 -121
  176. package/src/components/reauth-dialog.tsx +17 -18
  177. package/src/components/swatch-picker.tsx +29 -5
  178. package/src/components/tile-radio.tsx +74 -13
  179. package/src/components/toggle-group.tsx +2 -2
  180. package/src/components/trigger-aria.ts +5 -0
  181. package/src/components/ui.tsx +114 -33
  182. package/src/feedback/feedback-attachment.tsx +372 -72
  183. package/src/feedback/feedback-dialog.tsx +7 -3
  184. package/src/feedback/feedback-inbox.tsx +3 -2
  185. package/src/feedback/feedback-thread.tsx +112 -9
  186. package/src/i18n/defaults.ts +2 -0
  187. package/src/i18n/german.ts +42 -0
  188. package/src/i18n/kit-labels.tsx +4 -0
  189. package/src/i18n/locales/en.ts +27 -5
  190. package/src/i18n/locales/es.ts +41 -0
  191. package/src/i18n/locales/fr.ts +42 -0
  192. package/src/i18n/locales/hu.ts +37 -0
  193. package/src/i18n/locales/it.ts +41 -0
  194. package/src/i18n/locales/zh.ts +32 -0
  195. package/src/i18n/review.ts +19 -0
  196. package/src/index.ts +18 -0
  197. package/src/lib/column-mapping.ts +234 -0
  198. package/src/lib/strip-fade.ts +2 -2
  199. package/src/lib/table-text.ts +271 -0
  200. package/src/rhf/fields.tsx +421 -6
  201. package/src/rhf.ts +2 -1
  202. package/src/shell/topbar-action-menu.tsx +134 -38
  203. package/src/wizard/stepper-nav.tsx +35 -1
  204. package/dist/components/field-anatomy.d.ts +0 -95
  205. package/dist/components/field-anatomy.js +0 -84
  206. package/dist/components/field-anatomy.js.map +0 -1
  207. package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
  208. package/src/components/field-anatomy.tsx +0 -190
@@ -1,5 +1,8 @@
1
1
  import * as react from 'react';
2
- import { C as CalculatorLabels } from '../kit-labels-R8mIAc9N.js';
2
+ import { C as CalculatorLabels } from '../feedback-DOwPu-Il.js';
3
+ import './tooltip.js';
4
+ import '../hooks/use-anchored-rect.js';
5
+ import '../lib/clipping.js';
3
6
  import '../data-table-labels-B7OdnM0S.js';
4
7
  import './data-table-sort.js';
5
8
  import './swipeable-row.js';
@@ -9,9 +12,6 @@ import './popover.js';
9
12
  import './chip.js';
10
13
  import 'lucide-react';
11
14
  import './field-sync.js';
12
- import './tooltip.js';
13
- import '../hooks/use-anchored-rect.js';
14
- import '../lib/clipping.js';
15
15
  import '../wizard/types.js';
16
16
  import '../tour/tour.js';
17
17
  import '../search/command-palette.js';
@@ -32,9 +32,6 @@ import './dialog-frame.js';
32
32
  import './modal.js';
33
33
  import '../hooks/use-search-param-state.js';
34
34
  import './measured-grid.js';
35
- import '../feedback-attachment-NhUm7Zga.js';
36
- import '../feedback/feedback-thread.js';
37
- import '../feedback/feedback-inbox.js';
38
35
  import './account-settings-labels.js';
39
36
  import './floating-panel.js';
40
37
  import './bulk-action-bar.js';
@@ -64,6 +61,10 @@ import './inline-edit-field.js';
64
61
  import '../lib/iban.js';
65
62
  import '../lib/phone.js';
66
63
  import './sign-chip.js';
64
+ import './column-mapper.js';
65
+ import '../lib/column-mapping.js';
66
+ import '../lib/table-text.js';
67
+ import './field-parts.js';
67
68
 
68
69
  /** Screen-reader names for the calculator panel's own controls — any part of the
69
70
  * kit's `calculator` namespace. Once three optional keys of its own; widened, not
@@ -0,0 +1,235 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, HTMLAttributes } from 'react';
3
+ import { ColumnRole, ColumnMapperResult, ColumnMapping } from '../lib/column-mapping.js';
4
+ import { TextTable } from '../lib/table-text.js';
5
+
6
+ /**
7
+ * A table pasted, dropped or chosen as a file, and which of its columns plays which
8
+ * part — Kurvenschmiede's columns input and keksdose's bank-file mapping step, once.
9
+ *
10
+ * Kurvenschmiede (`features/control/columns-input.tsx` + `csv.ts`) reads a recorder's
11
+ * export into a playground signal (time, value) and a comparison (time, setpoint,
12
+ * actual): a textarea, a file button, a parse, a select per role, and a line that says
13
+ * which decimal convention was assumed and how many lines were skipped. keksdose
14
+ * (`import-map-step.tsx`) shows a bank's CSV as a table with a role select on every
15
+ * column's header, the file's real values under it, ignored columns dimmed, and what
16
+ * the import still needs named under it. Same problem, two hand-built answers.
17
+ *
18
+ * Two layers, so both shapes fit:
19
+ *
20
+ * * {@link ColumnMapper} — the whole single panel: paste, drop or choose → parse
21
+ * ({@link parseTextTable}: separator, decimal convention, header line, all stated
22
+ * back) → preview with a role per column → `onChange(result)`. Kurvenschmiede's.
23
+ * * {@link ColumnRoleTable} — the preview with the role selects alone, controlled, over
24
+ * rows the caller already has. keksdose's map step, whose rows come from its server's
25
+ * sniff (encodings, quoted multi-line cells, skipped title rows) and whose mapping
26
+ * lives in the wizard's state between steps.
27
+ *
28
+ * The rules under both are headless in `lib/column-mapping.ts`: a role moves rather
29
+ * than doubling up, what is missing is named, and the rows a numeric role cannot read
30
+ * are reported by line number rather than dropped.
31
+ */
32
+ interface ColumnMapperLabels {
33
+ /** The paste box's label. */
34
+ paste: string;
35
+ /** The caption under the paste box. */
36
+ pasteHint: string;
37
+ /** The file button. */
38
+ chooseFile: string;
39
+ /** A chosen or dropped file whose bytes could not be read at all. */
40
+ readError: (name: string) => string;
41
+ /** The checkbox that says whether the first line names the columns. */
42
+ headerRow: string;
43
+ /** What was read: so many columns and rows. */
44
+ summary: (columns: number, rows: number) => string;
45
+ /** The separator that was assumed. */
46
+ separatorSemicolon: string;
47
+ separatorComma: string;
48
+ separatorTab: string;
49
+ separatorSpace: string;
50
+ /** The decimal convention that was assumed — with an example, because "decimal
51
+ * comma" alone is a term, and `1,5` is something a reader recognises. */
52
+ decimalComma: string;
53
+ decimalPoint: string;
54
+ /** The title over the lines that could not be read. */
55
+ unreadCount: (count: number) => string;
56
+ /** One of them. */
57
+ unreadLine: (line: number) => string;
58
+ /** Past the first hundred listed. */
59
+ unreadMore: (count: number) => string;
60
+ /** The text has content, and none of it is a row. */
61
+ noRows: string;
62
+ /** The preview table's name. */
63
+ table: string;
64
+ /** A column the table names no header for. */
65
+ columnN: (n: number) => string;
66
+ /** The role select on a column's header — its accessible name. */
67
+ roleOf: (column: string) => string;
68
+ /** The role select's "no role" option. */
69
+ ignore: string;
70
+ /** A required role in the select — only while some role is NOT required: when
71
+ * every one is, the mark tells no role from another (0.24, Kurvenschmiede). */
72
+ requiredRole: (role: string) => string;
73
+ /** Under the preview, when it shows fewer rows than were read. */
74
+ previewOf: (shown: number, total: number) => string;
75
+ /** A required group's roles, already joined as a list with "or". */
76
+ oneOf: (roles: string) => string;
77
+ /** What the table still needs, already joined as a list with "and". */
78
+ missing: (roles: string) => string;
79
+ }
80
+ declare const DEFAULT_COLUMN_MAPPER_LABELS: ColumnMapperLabels;
81
+ /** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
82
+ type DataAttributes = {
83
+ [key: `data-${string}`]: string | number | boolean | undefined;
84
+ };
85
+ /**
86
+ * What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`
87
+ * hooks, an `id`, a class, a style.
88
+ */
89
+ type ColumnRoleTableBodyProps = Omit<HTMLAttributes<HTMLTableSectionElement>, "children" | "dangerouslySetInnerHTML"> & DataAttributes;
90
+ /**
91
+ * What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —
92
+ * the same shape as LineItems' `LineItemsRowProps`, on a table row.
93
+ */
94
+ type ColumnRoleTableRowProps = Omit<HTMLAttributes<HTMLTableRowElement>, "children" | "dangerouslySetInnerHTML"> & DataAttributes;
95
+ interface ColumnRoleTableProps<R extends string, M extends ColumnMapping<R> = ColumnMapping<R>> {
96
+ /** The columns' names — the file's header line — or `null` for a table without one
97
+ * (each column is then "Column 1", "Column 2", …). */
98
+ header: readonly string[] | null;
99
+ /** The rows to preview, as text. Only the first {@link previewRows} are drawn. */
100
+ rows: readonly (readonly string[])[];
101
+ /** How many columns. Default: the header's length or the widest previewed row. */
102
+ width?: number;
103
+ /** The parts a column can play. */
104
+ roles: readonly ColumnRole<R>[];
105
+ /** Role → column. Any object with the roles as nullable number fields — keksdose's
106
+ * `CsvMapping` as it stands; the other fields are handed back untouched. */
107
+ mapping: M;
108
+ /** The next mapping, with the picked role moved onto its column. */
109
+ onMappingChange: (next: M) => void;
110
+ /** Default 5. */
111
+ previewRows?: number;
112
+ /** How many rows the table has in all, for "The first 5 of 120 rows". Default:
113
+ * `rows.length`. */
114
+ totalRows?: number;
115
+ /** Every role select disabled — keksdose locks them while a re-sniff replaces the
116
+ * table they point into. */
117
+ disabled?: boolean;
118
+ /**
119
+ * Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are
120
+ * the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly
121
+ * that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,
122
+ * its map step reached into the table with a `useLayoutEffect` that set it on the
123
+ * `<tbody>` after every commit; `bodyProps={{ "data-private": "" }}` declares it.
124
+ * There is deliberately no head counterpart: the column names and the role selects
125
+ * are what such a mode is there to show.
126
+ *
127
+ * Spread on the kit `TableBody`; a `className` is added to its own.
128
+ */
129
+ bodyProps?: ColumnRoleTableBodyProps;
130
+ /**
131
+ * Extra attributes for one preview row's `<tr>`, from its cells and its index in the
132
+ * preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:
133
+ * `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for
134
+ * the rows drawn, the first {@link previewRows}. The row has no attributes of its own
135
+ * to protect; a `className` is added after its classes.
136
+ */
137
+ rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
138
+ labels?: Partial<ColumnMapperLabels>;
139
+ className?: string;
140
+ }
141
+ /**
142
+ * A table's first rows, with a role select on every column's header.
143
+ *
144
+ * keksdose's mapping step (dev#462: "make it more table like, where the table columns
145
+ * can be chosen from the input and the column data changes with the chosen column
146
+ * header"): the file IS the form. The file's own column name sits on top, verbatim —
147
+ * it is how the reader recognises the column; under it the role, picked in place;
148
+ * under that the column's real values, so a column that is one off shows up here and
149
+ * not after the import. A column with no role says "Ignore" and its values are dimmed:
150
+ * the state that most needs to be visible in a twelve-column bank export.
151
+ *
152
+ * Picking a role another column holds MOVES it ({@link assignColumnRole}). What the
153
+ * table still needs is named under it — a required group as "either Amount, Debit or
154
+ * Credit". Required roles say "(required)" in the list only where some role is not
155
+ * (0.24): Kurvenschmiede's roles are all required, so the mark told no option from
156
+ * another, and on a phone it cut the role's own name short ("Zeit (s) (erforderl…")
157
+ * — the part of the option the reader has to read. The "Still needed" line already
158
+ * says what is missing; a mark that distinguishes nothing is only noise.
159
+ *
160
+ * The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a
161
+ * named, keyboard-reachable region while it overflows), so a wide export never widens
162
+ * the page at 390px. An edge with columns behind it fades out (0.24, Kurvenschmiede:
163
+ * on a phone the fourth column's select sat off-screen and nothing said there was more
164
+ * to the right; phone scrollbars are overlays that show only while you drag). It is
165
+ * the kit Tabs strip's measured fade: drawn only while something is hidden on that
166
+ * side, so a table that fits is painted as before; both ends once scrolled into the
167
+ * middle; the reading direction's own in RTL; never animated, so there is nothing for
168
+ * reduced motion to turn off; and a `mask-image`, which changes no layout and needs no
169
+ * background colour to fade to. A role select focused behind the fade is scrolled
170
+ * clear of it.
171
+ */
172
+ declare function ColumnRoleTable<R extends string, M extends ColumnMapping<R> = ColumnMapping<R>>({ header, rows, width: widthProp, roles, mapping, onMappingChange, previewRows, totalRows, disabled, bodyProps, rowProps, labels: labelsProp, className, }: ColumnRoleTableProps<R, M>): react.JSX.Element;
173
+ interface ColumnMapperProps<R extends string> {
174
+ /** The parts the columns play — translated labels, which are required, which must
175
+ * be numbers. See {@link ColumnRole}. */
176
+ roles: readonly ColumnRole<R>[];
177
+ /**
178
+ * The table read through the current mapping, on every change: the text, the header
179
+ * checkbox, a role. `null` while there is no table at all (an empty box). Use it when
180
+ * `result.complete`; until then `result.missing` says what is not placed yet.
181
+ *
182
+ * The rows are strings. A numeric role's cells are guaranteed to read with
183
+ * `tableNumber(cell, result.decimalComma)`; anything else is the caller's to convert.
184
+ */
185
+ onChange: (result: ColumnMapperResult<R> | null) => void;
186
+ /** The paste box's label. Default: `labels.paste`. */
187
+ label?: ReactNode;
188
+ /** The caption under the paste box. Default: `labels.pasteHint`. */
189
+ hint?: ReactNode;
190
+ /** What the file dialog offers, and what a drop is checked against. Default:
191
+ * `.csv,.tsv,.txt,.dat,.asc`. */
192
+ accept?: string;
193
+ /** The largest file taken, in bytes. Default: no limit. */
194
+ maxSize?: number;
195
+ /** Text to start with. `onChange` is called with its result once, on mount. */
196
+ defaultText?: string;
197
+ /** Rows in the preview. Default 5. */
198
+ previewRows?: number;
199
+ /** The first mapping for a new table. Default {@link guessMapping}: a column named
200
+ * like a role gets it, and the required roles left over take the free columns in
201
+ * order. */
202
+ guess?: (table: TextTable) => ColumnMapping<R>;
203
+ disabled?: boolean;
204
+ /** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}
205
+ * (0.24): `{ "data-private": "" }` for a table of someone's own data. */
206
+ bodyProps?: ColumnRoleTableBodyProps;
207
+ /** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}
208
+ * (0.24). `row` is the row as read, `index` its place in the preview. */
209
+ rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;
210
+ labels?: Partial<ColumnMapperLabels>;
211
+ className?: string;
212
+ }
213
+ /**
214
+ * Paste, drop or choose a table, see what it was read as, and say which column is
215
+ * which — Kurvenschmiede's playground signal and comparison input as one kit part.
216
+ *
217
+ * Everything the parse assumed is stated, because a guess the reader cannot see is a
218
+ * guess they cannot correct: the shape it found, the separator, the decimal
219
+ * convention ("decimal comma (1,5)"), and every line it could not read, by number and
220
+ * with its text. The header line is a checkbox, since the detection is a guess either
221
+ * way and toggling it does not move a column (the mapping is kept, keksdose's
222
+ * `keepRoles`). A new file is a new table and is mapped afresh; typing that changes the
223
+ * column count re-guesses, because the old indexes point at nothing.
224
+ *
225
+ * A file can be chosen, dropped on the button, or dropped on the paste box — which,
226
+ * left to the browser, would open the file in place of the page. It is read as UTF-8,
227
+ * or as Windows-1252 when it is not UTF-8 ({@link readTextFile}).
228
+ *
229
+ * A two-step wizard that keeps the drop and the mapping apart (keksdose's) uses the
230
+ * parts: {@link parseTextTable} or its own server's sniff, then {@link ColumnRoleTable},
231
+ * then {@link readMappedTable}.
232
+ */
233
+ declare function ColumnMapper<R extends string>({ roles, onChange, label, hint, accept, maxSize, defaultText, previewRows, guess, disabled, bodyProps, rowProps, labels: labelsProp, className, }: ColumnMapperProps<R>): react.JSX.Element;
234
+
235
+ export { ColumnMapper, type ColumnMapperLabels, type ColumnMapperProps, ColumnRoleTable, type ColumnRoleTableBodyProps, type ColumnRoleTableProps, type ColumnRoleTableRowProps, DEFAULT_COLUMN_MAPPER_LABELS };
@@ -0,0 +1,411 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { createElement } from "react";
4
+ import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
5
+ import { Upload } from "lucide-react";
6
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import {
9
+ assignColumnRole,
10
+ guessMapping,
11
+ missingRoles,
12
+ readMappedTable,
13
+ readTextFile,
14
+ roleOfColumn
15
+ } from "../lib/column-mapping.js";
16
+ import { scrollIntoStrip, useStripFade } from "../lib/strip-fade.js";
17
+ import { parseTextTable } from "../lib/table-text.js";
18
+ import { AlertBanner } from "./alert-banner.js";
19
+ import { Checkbox } from "./checkbox.js";
20
+ import { Disclosure } from "./disclosure.js";
21
+ import { FileButton, useFilePicker } from "./file-button.js";
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableHead,
27
+ TableHeaderCell,
28
+ TableRow
29
+ } from "./table.js";
30
+ import { Select, Textarea } from "./ui.js";
31
+ const DEFAULT_COLUMN_MAPPER_LABELS = {
32
+ paste: "Paste a table",
33
+ pasteHint: "Copy the rows out of a spreadsheet and paste them here, or drop a CSV or text file.",
34
+ chooseFile: "Choose a file",
35
+ readError: (name) => `\u201C${name}\u201D could not be read`,
36
+ headerRow: "The first line names the columns",
37
+ summary: (columns, rows) => `${columns} ${columns === 1 ? "column" : "columns"}, ${rows} ${rows === 1 ? "row" : "rows"}`,
38
+ separatorSemicolon: "separated by semicolons",
39
+ separatorComma: "separated by commas",
40
+ separatorTab: "separated by tabs",
41
+ separatorSpace: "separated by spaces",
42
+ decimalComma: "decimal comma (1,5)",
43
+ decimalPoint: "decimal point (1.5)",
44
+ unreadCount: (count) => count === 1 ? "1 line could not be read" : `${count} lines could not be read`,
45
+ unreadLine: (line) => `Line ${line} could not be read`,
46
+ unreadMore: (count) => `\u2026and ${count} more`,
47
+ noRows: "No line of this text reads as a row of the table.",
48
+ table: "Columns and their roles",
49
+ columnN: (n) => `Column ${n}`,
50
+ roleOf: (column) => `What does column \u201C${column}\u201D hold?`,
51
+ ignore: "Ignore",
52
+ requiredRole: (role) => `${role} (required)`,
53
+ previewOf: (shown, total) => shown === 1 ? `The first of ${total} rows` : `The first ${shown} of ${total} rows`,
54
+ oneOf: (roles) => `either ${roles}`,
55
+ missing: (roles) => `Still needed: ${roles}.`
56
+ };
57
+ const SEPARATOR_LABEL = {
58
+ ";": "separatorSemicolon",
59
+ ",": "separatorComma",
60
+ " ": "separatorTab",
61
+ " ": "separatorSpace"
62
+ };
63
+ const UNREAD_LISTED = 100;
64
+ const DEFAULT_ACCEPT = ".csv,.tsv,.txt,.dat,.asc";
65
+ function formatList(items, type, locale) {
66
+ try {
67
+ return new Intl.ListFormat(locale, { type, style: "long" }).format(items);
68
+ } catch {
69
+ return items.join(", ");
70
+ }
71
+ }
72
+ function ColumnRoleTable({
73
+ header,
74
+ rows,
75
+ width: widthProp,
76
+ roles,
77
+ mapping,
78
+ onMappingChange,
79
+ previewRows = 5,
80
+ totalRows,
81
+ disabled,
82
+ bodyProps,
83
+ rowProps,
84
+ labels: labelsProp,
85
+ className
86
+ }) {
87
+ const labels = useKitLabels(
88
+ "columnMapper",
89
+ DEFAULT_COLUMN_MAPPER_LABELS,
90
+ labelsProp
91
+ );
92
+ const locale = useKitLocale();
93
+ const shown = rows.slice(0, previewRows);
94
+ const width = widthProp ?? shown.reduce(
95
+ (widest, row) => Math.max(widest, row.length),
96
+ header?.length ?? 0
97
+ );
98
+ const columns = Array.from({ length: width }, (_, index) => index);
99
+ const total = totalRows ?? rows.length;
100
+ const nameOf = (column) => header?.[column] || labels.columnN(column + 1);
101
+ const labelOf = (value) => roles.find((role) => role.value === value)?.label ?? value;
102
+ const gaps = missingRoles(roles, mapping, width).map(
103
+ (gap) => gap.length === 1 ? labelOf(gap[0]) : labels.oneOf(formatList(gap.map(labelOf), "disjunction", locale))
104
+ );
105
+ const markRequired = roles.some((role) => role.required !== true);
106
+ const scroller = useRef(null);
107
+ const tableRef = useCallback((table) => {
108
+ scroller.current = table?.parentElement ?? null;
109
+ }, []);
110
+ const fade = useStripFade(scroller);
111
+ return /* @__PURE__ */ jsxs(
112
+ "div",
113
+ {
114
+ className: cn("min-w-0 space-y-1.5", className),
115
+ "data-slot": "column-role-table",
116
+ children: [
117
+ /* @__PURE__ */ jsx(
118
+ "div",
119
+ {
120
+ "data-slot": "column-role-table-scroll",
121
+ "data-overflow": fade.overflow,
122
+ style: fade.mask ? { maskImage: fade.mask, WebkitMaskImage: fade.mask } : void 0,
123
+ children: /* @__PURE__ */ jsxs(
124
+ Table,
125
+ {
126
+ ref: tableRef,
127
+ density: "compact",
128
+ framed: true,
129
+ "aria-label": labels.table,
130
+ children: [
131
+ /* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => {
132
+ const role = roleOfColumn(roles, mapping, column);
133
+ const name = nameOf(column);
134
+ return /* @__PURE__ */ jsxs(
135
+ TableHeaderCell,
136
+ {
137
+ scope: "col",
138
+ className: "min-w-36 py-2 align-top",
139
+ children: [
140
+ /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]", children: name }),
141
+ /* @__PURE__ */ jsxs(
142
+ Select,
143
+ {
144
+ size: "sm",
145
+ className: "w-full",
146
+ "aria-label": labels.roleOf(name),
147
+ value: role ?? "",
148
+ disabled,
149
+ onFocus: (event) => {
150
+ const cell = event.currentTarget.closest("th");
151
+ if (scroller.current && cell)
152
+ scrollIntoStrip(scroller.current, cell);
153
+ },
154
+ onChange: (event) => onMappingChange(
155
+ assignColumnRole(
156
+ roles,
157
+ mapping,
158
+ column,
159
+ event.target.value === "" ? null : event.target.value
160
+ )
161
+ ),
162
+ children: [
163
+ /* @__PURE__ */ jsx("option", { value: "", children: labels.ignore }),
164
+ roles.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.required === true && markRequired ? labels.requiredRole(option.label) : option.label }, option.value))
165
+ ]
166
+ }
167
+ )
168
+ ]
169
+ },
170
+ column
171
+ );
172
+ }) }) }),
173
+ /* @__PURE__ */ jsx(TableBody, { ...bodyProps, children: shown.map((row, index) => /* @__PURE__ */ createElement(TableRow, { ...rowProps?.(row, index), key: index }, columns.map((column) => /* @__PURE__ */ jsx(
174
+ TableCell,
175
+ {
176
+ "data-ignored": roleOfColumn(roles, mapping, column) === null || void 0,
177
+ className: cn(
178
+ "font-mono whitespace-nowrap",
179
+ // Dimmed, not hidden: its values are what tell you whether ignoring
180
+ // it was right.
181
+ roleOfColumn(roles, mapping, column) === null && "text-[var(--text-muted)]"
182
+ ),
183
+ children: /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate", children: row[column] ?? "" })
184
+ },
185
+ column
186
+ )))) })
187
+ ]
188
+ }
189
+ )
190
+ }
191
+ ),
192
+ total > shown.length && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.previewOf(shown.length, total) }),
193
+ gaps.length > 0 && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, children: labels.missing(formatList(gaps, "conjunction", locale)) })
194
+ ]
195
+ }
196
+ );
197
+ }
198
+ const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
199
+ function ColumnMapper({
200
+ roles,
201
+ onChange,
202
+ label,
203
+ hint,
204
+ accept = DEFAULT_ACCEPT,
205
+ maxSize,
206
+ defaultText = "",
207
+ previewRows = 5,
208
+ guess,
209
+ disabled = false,
210
+ bodyProps,
211
+ rowProps,
212
+ labels: labelsProp,
213
+ className
214
+ }) {
215
+ const labels = useKitLabels(
216
+ "columnMapper",
217
+ DEFAULT_COLUMN_MAPPER_LABELS,
218
+ labelsProp
219
+ );
220
+ const firstMapping = (table2) => guess ? guess(table2) : guessMapping(roles, table2.header, table2.width);
221
+ const [state, setState] = useState(() => {
222
+ const table2 = parseTextTable(defaultText);
223
+ return {
224
+ text: defaultText,
225
+ header: null,
226
+ table: table2,
227
+ mapping: firstMapping(table2)
228
+ };
229
+ });
230
+ const [reading, setReading] = useState(false);
231
+ const [refusal, setRefusal] = useState(null);
232
+ const [dragOver, setDragOver] = useState(false);
233
+ const result = readMappedTable(state.table, roles, state.mapping);
234
+ const commit = (next) => {
235
+ setState(next);
236
+ onChange(
237
+ next.table.width === 0 ? null : readMappedTable(next.table, roles, next.mapping)
238
+ );
239
+ };
240
+ const take = (text, fresh) => {
241
+ const header = fresh ? null : state.header;
242
+ const table2 = parseTextTable(text, header === null ? {} : { header });
243
+ const keep = !fresh && table2.width === state.table.width && state.table.width > 0;
244
+ commit({
245
+ text,
246
+ header,
247
+ table: table2,
248
+ mapping: keep ? state.mapping : firstMapping(table2)
249
+ });
250
+ };
251
+ const load = (file) => {
252
+ setRefusal(null);
253
+ setReading(true);
254
+ void readTextFile(file).then(
255
+ (text) => take(text, true),
256
+ // Only the read: an error thrown by the caller's onChange is not the file's.
257
+ () => setRefusal(labels.readError(file.name))
258
+ ).finally(() => setReading(false));
259
+ };
260
+ const refuse = (rejections) => setRefusal(rejections[0]?.message ?? null);
261
+ const drop = useFilePicker({
262
+ accept,
263
+ maxSize,
264
+ disabled,
265
+ onFiles: ([file]) => load(file),
266
+ onReject: refuse
267
+ });
268
+ const seeded = useEffectEvent(() => {
269
+ if (state.table.width > 0) onChange(result);
270
+ });
271
+ useEffect(() => {
272
+ seeded();
273
+ }, []);
274
+ const { table } = state;
275
+ const separator = labels[SEPARATOR_LABEL[table.separator]];
276
+ const unread = result.unread;
277
+ return /* @__PURE__ */ jsxs(
278
+ "div",
279
+ {
280
+ className: cn("min-w-0 space-y-3", className),
281
+ "data-slot": "column-mapper",
282
+ children: [
283
+ /* @__PURE__ */ jsx(
284
+ Textarea,
285
+ {
286
+ label: label ?? labels.paste,
287
+ hint: hint ?? labels.pasteHint,
288
+ rows: 5,
289
+ wrap: "off",
290
+ spellCheck: false,
291
+ autoCapitalize: "off",
292
+ autoCorrect: "off",
293
+ value: state.text,
294
+ disabled,
295
+ className: cn(
296
+ "font-mono text-xs",
297
+ dragOver && "rounded-md ring-2 ring-[var(--brand)]"
298
+ ),
299
+ "data-drag-over": dragOver || void 0,
300
+ onChange: (event) => {
301
+ setRefusal(null);
302
+ take(event.target.value, false);
303
+ },
304
+ onDragEnter: (event) => {
305
+ if (!hasFiles(event)) return;
306
+ event.preventDefault();
307
+ if (!disabled) setDragOver(true);
308
+ },
309
+ onDragOver: (event) => {
310
+ if (!hasFiles(event)) return;
311
+ event.preventDefault();
312
+ event.dataTransfer.dropEffect = disabled ? "none" : "copy";
313
+ },
314
+ onDragLeave: () => setDragOver(false),
315
+ onDrop: (event) => {
316
+ if (!hasFiles(event)) return;
317
+ event.preventDefault();
318
+ setDragOver(false);
319
+ drop.take(event.dataTransfer.files);
320
+ }
321
+ }
322
+ ),
323
+ drop.element,
324
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [
325
+ /* @__PURE__ */ jsxs(
326
+ FileButton,
327
+ {
328
+ variant: "secondary",
329
+ accept,
330
+ maxSize,
331
+ droppable: true,
332
+ pending: reading,
333
+ disabled,
334
+ onFiles: ([file]) => load(file),
335
+ onReject: refuse,
336
+ children: [
337
+ /* @__PURE__ */ jsx(Upload, { className: "size-4", "aria-hidden": true }),
338
+ labels.chooseFile
339
+ ]
340
+ }
341
+ ),
342
+ table.width > 0 && /* @__PURE__ */ jsx(
343
+ Checkbox,
344
+ {
345
+ label: labels.headerRow,
346
+ checked: table.header !== null,
347
+ disabled,
348
+ onCheckedChange: (checked) => {
349
+ const next = parseTextTable(state.text, { header: checked });
350
+ commit({ ...state, header: checked, table: next });
351
+ }
352
+ }
353
+ )
354
+ ] }),
355
+ refusal && /* @__PURE__ */ jsx(AlertBanner, { tone: "danger", variant: "inline", size: "sm", block: true, children: refusal }),
356
+ table.width > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
357
+ /* @__PURE__ */ jsx(
358
+ "p",
359
+ {
360
+ className: "text-xs text-[var(--text-secondary)]",
361
+ "data-slot": "column-mapper-summary",
362
+ children: [
363
+ labels.summary(table.width, result.rows.length),
364
+ separator,
365
+ table.decimalComma ? labels.decimalComma : labels.decimalPoint
366
+ ].join(" \xB7 ")
367
+ }
368
+ ),
369
+ result.rows.length === 0 && /* @__PURE__ */ jsx(AlertBanner, { tone: "warning", variant: "inline", size: "sm", block: true, children: labels.noRows }),
370
+ /* @__PURE__ */ jsx(
371
+ ColumnRoleTable,
372
+ {
373
+ header: table.header,
374
+ rows: result.rows,
375
+ width: table.width,
376
+ roles,
377
+ mapping: state.mapping,
378
+ onMappingChange: (mapping) => commit({ ...state, mapping }),
379
+ previewRows,
380
+ disabled,
381
+ bodyProps,
382
+ rowProps,
383
+ labels: labelsProp
384
+ }
385
+ ),
386
+ unread.length > 0 && /* @__PURE__ */ jsx(
387
+ Disclosure,
388
+ {
389
+ variant: "bare",
390
+ title: labels.unreadCount(unread.length),
391
+ "data-slot": "column-mapper-unread",
392
+ children: /* @__PURE__ */ jsxs("ul", { className: "space-y-1 text-xs", children: [
393
+ unread.slice(0, UNREAD_LISTED).map((line) => /* @__PURE__ */ jsxs("li", { className: "min-w-0", children: [
394
+ /* @__PURE__ */ jsx("span", { className: "block text-[var(--text-secondary)]", children: labels.unreadLine(line.line) }),
395
+ /* @__PURE__ */ jsx("code", { className: "block truncate font-mono text-[var(--text-muted)]", children: line.text })
396
+ ] }, line.line)),
397
+ unread.length > UNREAD_LISTED && /* @__PURE__ */ jsx("li", { className: "text-[var(--text-muted)]", children: labels.unreadMore(unread.length - UNREAD_LISTED) })
398
+ ] })
399
+ }
400
+ )
401
+ ] })
402
+ ]
403
+ }
404
+ );
405
+ }
406
+ export {
407
+ ColumnMapper,
408
+ ColumnRoleTable,
409
+ DEFAULT_COLUMN_MAPPER_LABELS
410
+ };
411
+ //# sourceMappingURL=column-mapper.js.map