@eifi1/ui-kit 0.22.0 → 0.23.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 (205) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +11 -8
  3. package/dist/components/amount-input.d.ts +28 -8
  4. package/dist/components/amount-input.js +3 -1
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +21 -0
  7. package/dist/components/autocomplete.js +128 -103
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +11 -8
  10. package/dist/components/calculator.d.ts +11 -8
  11. package/dist/components/column-mapper.d.ts +182 -0
  12. package/dist/components/column-mapper.js +375 -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 +80 -2
  18. package/dist/components/combobox.js +448 -311
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +11 -8
  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 +8 -5
  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 +11 -8
  28. package/dist/components/danger-confirm.js +13 -3
  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 +35 -1
  34. package/dist/components/entity-combobox.js +148 -112
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +11 -8
  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 +11 -8
  44. package/dist/components/file-dropzone.d.ts +11 -8
  45. package/dist/components/form-actions.d.ts +10 -7
  46. package/dist/components/form-actions.js +8 -2
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +11 -8
  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 +11 -8
  53. package/dist/components/money-field.d.ts +14 -8
  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 +26 -1
  58. package/dist/components/multi-entity-combobox.js +143 -102
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +11 -8
  61. package/dist/components/number-input.d.ts +11 -8
  62. package/dist/components/numpad-sheet.d.ts +11 -8
  63. package/dist/components/phone-input.d.ts +11 -8
  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 +11 -8
  68. package/dist/components/settings-fields.d.ts +11 -8
  69. package/dist/components/share-card.d.ts +10 -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 +10 -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 +11 -8
  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 +8 -5
  84. package/dist/components/ui.js +5 -11
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +1 -1
  87. package/dist/feedback/feedback-attachment.js +156 -56
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +1 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +3 -2
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +68 -287
  94. package/dist/feedback/feedback-thread.js +8 -1
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
  97. package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
  98. package/dist/feedback.d.ts +66 -2
  99. package/dist/hooks/use-file-drop.d.ts +11 -8
  100. package/dist/i18n/defaults.d.ts +11 -8
  101. package/dist/i18n/defaults.js +3 -1
  102. package/dist/i18n/defaults.js.map +1 -1
  103. package/dist/i18n/german.d.ts +11 -8
  104. package/dist/i18n/german.js +36 -2
  105. package/dist/i18n/german.js.map +1 -1
  106. package/dist/i18n/kit-labels.d.ts +7 -4
  107. package/dist/i18n/kit-labels.js.map +1 -1
  108. package/dist/i18n/languages.d.ts +11 -8
  109. package/dist/i18n/locales/de-CH.d.ts +11 -8
  110. package/dist/i18n/locales/en.d.ts +11 -8
  111. package/dist/i18n/locales/en.js +7 -0
  112. package/dist/i18n/locales/en.js.map +1 -1
  113. package/dist/i18n/locales/es.d.ts +11 -8
  114. package/dist/i18n/locales/es.js +37 -2
  115. package/dist/i18n/locales/es.js.map +1 -1
  116. package/dist/i18n/locales/fr.d.ts +11 -8
  117. package/dist/i18n/locales/fr.js +36 -2
  118. package/dist/i18n/locales/fr.js.map +1 -1
  119. package/dist/i18n/locales/hu.d.ts +11 -8
  120. package/dist/i18n/locales/hu.js +38 -2
  121. package/dist/i18n/locales/hu.js.map +1 -1
  122. package/dist/i18n/locales/it.d.ts +11 -8
  123. package/dist/i18n/locales/it.js +37 -2
  124. package/dist/i18n/locales/it.js.map +1 -1
  125. package/dist/i18n/locales/zh.d.ts +11 -8
  126. package/dist/i18n/locales/zh.js +34 -2
  127. package/dist/i18n/locales/zh.js.map +1 -1
  128. package/dist/i18n/review.d.ts +11 -8
  129. package/dist/i18n/review.js +20 -1
  130. package/dist/i18n/review.js.map +1 -1
  131. package/dist/index.d.ts +7 -3
  132. package/dist/index.js +16 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/lib/column-mapping.d.ts +81 -0
  135. package/dist/lib/column-mapping.js +108 -0
  136. package/dist/lib/column-mapping.js.map +1 -0
  137. package/dist/lib/table-text.d.ts +109 -1
  138. package/dist/lib/table-text.js +122 -1
  139. package/dist/lib/table-text.js.map +1 -1
  140. package/dist/rhf/fields.d.ts +89 -11
  141. package/dist/rhf/fields.js +124 -0
  142. package/dist/rhf/fields.js.map +1 -1
  143. package/dist/rhf/form.d.ts +11 -8
  144. package/dist/rhf.d.ts +12 -9
  145. package/dist/rhf.js.map +1 -1
  146. package/dist/shell/app-shell.d.ts +10 -7
  147. package/dist/shell/top-bar-brand.d.ts +11 -8
  148. package/dist/shell/topbar-action-menu.d.ts +36 -3
  149. package/dist/shell/topbar-action-menu.js +74 -33
  150. package/dist/shell/topbar-action-menu.js.map +1 -1
  151. package/dist/shell.d.ts +10 -7
  152. package/dist/table-text.d.ts +1 -1
  153. package/dist/wizard/stepper-nav.d.ts +41 -10
  154. package/dist/wizard/stepper-nav.js +4 -0
  155. package/dist/wizard/stepper-nav.js.map +1 -1
  156. package/dist/wizard.d.ts +11 -8
  157. package/package.json +1 -1
  158. package/src/components/amount-input.tsx +20 -1
  159. package/src/components/autocomplete.tsx +172 -111
  160. package/src/components/column-mapper.tsx +666 -0
  161. package/src/components/combobox-core.tsx +82 -9
  162. package/src/components/combobox.tsx +614 -332
  163. package/src/components/confirm-dialog.tsx +12 -8
  164. package/src/components/country-select.tsx +135 -22
  165. package/src/components/danger-confirm.tsx +91 -12
  166. package/src/components/date-picker.tsx +423 -10
  167. package/src/components/entity-combobox.tsx +217 -126
  168. package/src/components/field-parts.tsx +224 -5
  169. package/src/components/field-strip.tsx +149 -0
  170. package/src/components/form-actions.tsx +32 -4
  171. package/src/components/icon-picker.tsx +23 -4
  172. package/src/components/money-field.tsx +3 -0
  173. package/src/components/month-picker.tsx +2 -1
  174. package/src/components/multi-entity-combobox.tsx +207 -120
  175. package/src/components/reauth-dialog.tsx +17 -18
  176. package/src/components/swatch-picker.tsx +29 -5
  177. package/src/components/tile-radio.tsx +74 -13
  178. package/src/components/toggle-group.tsx +2 -2
  179. package/src/components/trigger-aria.ts +5 -0
  180. package/src/components/ui.tsx +7 -33
  181. package/src/feedback/feedback-attachment.tsx +308 -61
  182. package/src/feedback/feedback-dialog.tsx +7 -3
  183. package/src/feedback/feedback-inbox.tsx +3 -2
  184. package/src/feedback/feedback-thread.tsx +47 -4
  185. package/src/i18n/defaults.ts +2 -0
  186. package/src/i18n/german.ts +42 -0
  187. package/src/i18n/kit-labels.tsx +4 -0
  188. package/src/i18n/locales/en.ts +27 -5
  189. package/src/i18n/locales/es.ts +41 -0
  190. package/src/i18n/locales/fr.ts +42 -0
  191. package/src/i18n/locales/hu.ts +37 -0
  192. package/src/i18n/locales/it.ts +41 -0
  193. package/src/i18n/locales/zh.ts +32 -0
  194. package/src/i18n/review.ts +19 -0
  195. package/src/index.ts +18 -0
  196. package/src/lib/column-mapping.ts +234 -0
  197. package/src/lib/table-text.ts +271 -0
  198. package/src/rhf/fields.tsx +254 -0
  199. package/src/rhf.ts +2 -1
  200. package/src/shell/topbar-action-menu.tsx +134 -38
  201. package/src/wizard/stepper-nav.tsx +35 -1
  202. package/dist/components/field-anatomy.d.ts +0 -95
  203. package/dist/components/field-anatomy.js +0 -84
  204. package/dist/components/field-anatomy.js.map +0 -1
  205. package/src/components/field-anatomy.tsx +0 -190
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/table-text.ts"],"sourcesContent":["/**\n * One lexer for \"paste the table out of a spreadsheet\", and the three rules for the\n * comma. `@eifi1/ui-kit/table-text` — pure functions over strings, no React.\n *\n * Moved here from Lenkbank (`shared/lib/parse-table.ts`), where three screens took a\n * table off somebody's clipboard — a measured-table grid, the text view beside it, and\n * a page that reads a recorder's setpoint/actual files — and each had grown its own\n * reader. They disagreed, and not in the harmless way: a whitespace-separated German\n * export was read as six columns of nonsense by two of them and as nothing at all by\n * the third, with no message either way. The lexing is one problem (lines, a\n * separator, fields, a header, numbers) and it is solved once here, so the next app\n * that imports a CSV does not write the comma rules wrong a second time.\n *\n * **What is genuinely different between callers is where the decision about the\n * comma is taken**, and that is a parameter rather than a constant. A comma is the\n * decimal mark in every German export and the column separator in every English CSV,\n * and no single rule serves both without regressing one of them. So the rules are\n * named instead of one being picked:\n *\n * * **`\"whole-text\"`** — the *file* door. A recorder writes one file in one\n * convention, so the separator and the decimal mark are decided once, from the\n * whole text, and stated back to the reader (`decimalComma`) where they can see\n * what was assumed. Deciding once is what lets one damaged line be *counted*\n * rather than re-guessed into a different convention than the lines around it.\n * * **`\"per-line\"`** — the *paste* door. What arrives on a clipboard is a few rows\n * out of a spreadsheet, sometimes one row, sometimes a line somebody typed. There\n * is no file to be consistent with, and a line must not change meaning because of\n * another line somewhere else in the box — so each line is read on its own\n * evidence.\n * * **unconditional** — one cell (`cellNumber` / `isCellNumber`). A single cell has\n * no columns in it, so a comma in one cannot be separating anything and is always\n * a decimal mark. That is not a table rule at all, which is why it is a pair of\n * one-liners beside the others rather than an option on them.\n *\n * What this deliberately does not do:\n *\n * * **Thousands separators.** `1.234,5` and `1,234.5` are both a number with two\n * marks in it, and telling the grouping mark from the decimal mark needs a rule\n * about digit runs that would then have to be right for a three-digit integer\n * too. A field carrying one is reported as a line that could not be read, with\n * its line number, which is something a reader can act on. Guessing is not.\n * * **Empty cells.** An empty field is dropped rather than held open as a hole, so a\n * row with a dropped channel in the middle of it shifts left. Keeping the hole\n * would mean reading `1;2;3;` — a trailing separator, which every spreadsheet\n * writes — as a four-column row. It is a known gap, not an oversight.\n * * **Quoted fields.** No `\"a;b\"` escaping. These are numbers; the only text in them\n * is the header line, and a header field with a separator in it costs a column\n * name, not a measurement.\n */\n\n/** A digit, a comma, a digit — a comma that can only be a decimal mark or a\n * grouping mark, never a separator with a number on each side of it. `1, 2`\n * deliberately does not match: a comma followed by a space is how a human\n * writes a list, and reading it as a decimal mark would turn a hand-typed\n * `1, 2, 3` into one number. */\nconst DECIMAL_COMMA = /\\d,\\d/;\n\n/** What separates the columns of one line. A regular expression only for the\n * whitespace case, because a run of spaces is one separator. */\ntype Separator = string | RegExp;\n\n/** Where the decision about the comma is taken. See the module docstring: the\n * file door decides once for the whole text, the paste door decides per line. */\nexport type DecimalRule = \"per-line\" | \"whole-text\";\n\nexport interface ParseTableOptions {\n decimal: DecimalRule;\n /**\n * How many columns the caller already knows the table has. A grid with fixed\n * columns knows its width before a number arrives: anything past it is sliced\n * off — an export with a trailing timestamp column is not an error worth\n * refusing — and a line with fewer is reported. Omitted where the width is the\n * file's own answer.\n */\n columns?: number;\n /**\n * How many leading lines that are not rows of numbers may be a header.\n * Default 1, the name row. {@link parseRows} passes 2, because a bench export\n * writes the units on a second line under the names.\n */\n headerLines?: number;\n}\n\nexport interface ParsedTable {\n rows: number[][];\n /** The first header line's fields — the column *names*. A units line under it\n * is absorbed and not returned: what a column is measured in is the caller's to\n * say (its column definitions), and a second list of strings here would be a\n * second answer to that. */\n header: string[] | null;\n /** Whether a comma was read as a decimal mark. Under `\"per-line\"` this is true\n * if any line was read that way, and it exists for the reader: a guess\n * nobody can see is a guess nobody can correct. */\n decimalComma: boolean;\n /** Line numbers — 1-based, counted in the text as it was handed over, blank\n * lines included — that held something which is not a row of numbers. A list\n * rather than a count, because one door names the first one and the other\n * counts them. */\n skipped: number[];\n}\n\n/**\n * The columns of one line, as text, with the decimal mark normalised to a point.\n *\n * Text rather than numbers because a grid holds cells as strings: a pasted\n * field that is not a number belongs in the cell it landed in, marked, where the\n * person who pasted it can see which column went wrong. Dropping it here would\n * shift every column after it and lose the evidence.\n */\nexport function splitRow(line: string): string[] {\n const separator = separatorOf(line);\n const decimalComma = separator !== \",\";\n return fieldsOf(line, separator).map((field) => (decimalComma ? point(field) : field));\n}\n\n/**\n * A pasted table, or the lines of it that are not one.\n *\n * The lexer both doors are built on. It reads lines, drops blanks and `#`\n * banners, decides what separates the columns, takes the header off the top and\n * converts what is left.\n */\nexport function parseTable(text: string, options: ParseTableOptions): ParsedTable {\n const headerLimit = options.headerLines ?? 1;\n const lines = contentLines(text);\n if (!lines.length) return { rows: [], header: null, decimalComma: false, skipped: [] };\n const fixed = options.decimal === \"whole-text\" ? wholeTextRule(lines) : null;\n\n let header: string[] | null = null;\n let headerCount = 0;\n const rows: number[][] = [];\n const skipped: number[] = [];\n let sawDecimalComma = false;\n\n for (const { line, number } of lines) {\n const separator = fixed ? fixed.separator : separatorOf(line);\n const decimalComma = fixed ? fixed.decimalComma : separator !== \",\";\n const fields = fieldsOf(line, separator);\n const numbers = fields.map((field) => Number(decimalComma ? point(field) : field));\n const readable = fields.length > 0 && numbers.every(Number.isFinite);\n\n if (!readable) {\n // A header is only a header at the top, and only before any data: a stray\n // word further down is a broken row, and dropping it silently would lose a\n // measurement without saying so.\n if (rows.length === 0 && headerCount < headerLimit) {\n header ??= fields;\n headerCount += 1;\n continue;\n }\n skipped.push(number);\n continue;\n }\n if (options.columns !== undefined && fields.length < options.columns) {\n skipped.push(number);\n continue;\n }\n if (decimalComma && DECIMAL_COMMA.test(line)) sawDecimalComma = true;\n rows.push(options.columns === undefined ? numbers : numbers.slice(0, options.columns));\n }\n\n return {\n rows,\n header,\n decimalComma: fixed ? fixed.decimalComma : sawDecimalComma,\n skipped,\n };\n}\n\n/**\n * The paste door: a fixed number of columns, and the line that stopped it.\n *\n * Read per line, with up to two header lines (names, then units). It answers with a\n * line number rather than with what it managed to read, because half a pasted\n * sweep saved as a measurement is worse than a refused paste — and because a\n * table that comes back **empty** is the one answer a reader cannot act on: it\n * looks exactly like a paste that did not happen. So text that has content and\n * produced no row at all is an error naming its first unreadable line, never\n * `{ rows: [] }`.\n */\nexport function parseRows(text: string, columns: number): { rows: number[][] } | { error: number } {\n const lines = contentLines(text);\n if (!lines.length) return { rows: [] };\n const parsed = parseTable(text, { decimal: \"per-line\", columns, headerLines: 2 });\n if (parsed.skipped.length) return { error: parsed.skipped[0] };\n if (!parsed.rows.length) return { error: lines[0].number };\n return { rows: parsed.rows };\n}\n\n/** One cell as a number. Unconditional: a cell has no columns in it, so a comma\n * in one is a decimal mark and nothing else. */\nexport const cellNumber = (cell: string) => Number(cell.replace(\",\", \".\"));\n\n/** Whether one cell holds a number yet. An empty cell is not one — it is a hole\n * in the measurement, which is a different thing from a zero. */\nexport const isCellNumber = (cell: string) =>\n cell.trim() !== \"\" && !Number.isNaN(cellNumber(cell));\n\n/**\n * The one separator and the one decimal mark a whole file is read with.\n *\n * The separator is taken from the line most likely to be data — the second,\n * where there is one, because the first is usually the names.\n *\n * A comma between two digits is then a decimal mark whenever something else is\n * already separating the columns: there is nothing else it could be. The rule\n * this replaces also demanded that no decimal POINT appear anywhere in the file,\n * which read a semicolon-separated export with one column written by a different\n * tool — an entirely ordinary thing for a bench to produce — as a table with no\n * rows in it, and said nothing.\n */\nfunction wholeTextRule(lines: { line: string }[]): { separator: Separator; decimalComma: boolean } {\n const separator = separatorOf(lines[lines.length > 1 ? 1 : 0].line);\n const body = lines.map((entry) => entry.line).join(\"\\n\");\n return { separator, decimalComma: separator !== \",\" && DECIMAL_COMMA.test(body) };\n}\n\n/** Lines with something on them, numbered as they stand in the original text so\n * a message can point at one. `#` banners are a recorder's, not a measurement. */\nfunction contentLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) => line !== \"\" && !line.startsWith(\"#\"));\n}\n\n/**\n * What separates this line's columns.\n *\n * In order of trust, not of count: a semicolon-separated line with decimal\n * commas has more commas than semicolons, and a tab-separated one may have both.\n * Whichever of tab and semicolon appears wins outright, because neither is ever\n * anything but a separator.\n *\n * The comma is the one that has to be argued about. A line that has whitespace\n * between its fields *and* a comma between two digits has already said what\n * separates it — so the comma is a decimal mark, and the whitespace separates.\n * That is a German export printed or dumped from MATLAB (`0,5 1,25 2,75`), and\n * reading its commas as separators produced six columns from three: each number\n * split into a pair of small integers, accepted as a row, and saved as a\n * measurement with nothing on screen to say so. A comma with a space after it is\n * not that shape (`DECIMAL_COMMA` does not match `1, 2`), so a hand-typed list\n * still separates on the comma.\n */\nfunction separatorOf(line: string): Separator {\n if (line.includes(\"\\t\")) return \"\\t\";\n if (line.includes(\";\")) return \";\";\n if (line.includes(\",\")) return /\\s/.test(line) && DECIMAL_COMMA.test(line) ? /\\s+/ : \",\";\n return /\\s+/;\n}\n\n/** One line into trimmed, non-empty fields, as they were written. The header is\n * returned out of these, so a column called `Weg,X` keeps its name even where\n * the numbers under it are being read with a decimal comma. */\nfunction fieldsOf(line: string, separator: Separator): string[] {\n return line\n .split(separator)\n .map((field) => field.trim())\n .filter((field) => field !== \"\");\n}\n\n/** The decimal mark this language of `Number` understands. One comma, because a\n * field with two of them is a grouped number and this module does not read\n * those — it reports the line instead. */\nconst point = (field: string) => field.replace(\",\", \".\");\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/table-text.ts"],"sourcesContent":["/**\n * One lexer for \"paste the table out of a spreadsheet\", and the three rules for the\n * comma. `@eifi1/ui-kit/table-text` — pure functions over strings, no React.\n *\n * Moved here from Lenkbank (`shared/lib/parse-table.ts`), where three screens took a\n * table off somebody's clipboard — a measured-table grid, the text view beside it, and\n * a page that reads a recorder's setpoint/actual files — and each had grown its own\n * reader. They disagreed, and not in the harmless way: a whitespace-separated German\n * export was read as six columns of nonsense by two of them and as nothing at all by\n * the third, with no message either way. The lexing is one problem (lines, a\n * separator, fields, a header, numbers) and it is solved once here, so the next app\n * that imports a CSV does not write the comma rules wrong a second time.\n *\n * **What is genuinely different between callers is where the decision about the\n * comma is taken**, and that is a parameter rather than a constant. A comma is the\n * decimal mark in every German export and the column separator in every English CSV,\n * and no single rule serves both without regressing one of them. So the rules are\n * named instead of one being picked:\n *\n * * **`\"whole-text\"`** — the *file* door. A recorder writes one file in one\n * convention, so the separator and the decimal mark are decided once, from the\n * whole text, and stated back to the reader (`decimalComma`) where they can see\n * what was assumed. Deciding once is what lets one damaged line be *counted*\n * rather than re-guessed into a different convention than the lines around it.\n * * **`\"per-line\"`** — the *paste* door. What arrives on a clipboard is a few rows\n * out of a spreadsheet, sometimes one row, sometimes a line somebody typed. There\n * is no file to be consistent with, and a line must not change meaning because of\n * another line somewhere else in the box — so each line is read on its own\n * evidence.\n * * **unconditional** — one cell (`cellNumber` / `isCellNumber`). A single cell has\n * no columns in it, so a comma in one cannot be separating anything and is always\n * a decimal mark. That is not a table rule at all, which is why it is a pair of\n * one-liners beside the others rather than an option on them.\n *\n * What this deliberately does not do:\n *\n * * **Thousands separators.** `1.234,5` and `1,234.5` are both a number with two\n * marks in it, and telling the grouping mark from the decimal mark needs a rule\n * about digit runs that would then have to be right for a three-digit integer\n * too. A field carrying one is reported as a line that could not be read, with\n * its line number, which is something a reader can act on. Guessing is not.\n * * **Empty cells.** An empty field is dropped rather than held open as a hole, so a\n * row with a dropped channel in the middle of it shifts left. Keeping the hole\n * would mean reading `1;2;3;` — a trailing separator, which every spreadsheet\n * writes — as a four-column row. It is a known gap, not an oversight.\n * * **Quoted fields.** No `\"a;b\"` escaping. These are numbers; the only text in them\n * is the header line, and a header field with a separator in it costs a column\n * name, not a measurement.\n *\n * Both of those are the NUMERIC lexer's limits. A table whose cells are text — a bank\n * export, read for a column mapper — needs both, and {@link parseTextTable} (0.23) is\n * that door: empty cells held in place, quoted cells, and the same separator and comma\n * rules underneath.\n */\n\n/** A digit, a comma, a digit — a comma that can only be a decimal mark or a\n * grouping mark, never a separator with a number on each side of it. `1, 2`\n * deliberately does not match: a comma followed by a space is how a human\n * writes a list, and reading it as a decimal mark would turn a hand-typed\n * `1, 2, 3` into one number. */\nconst DECIMAL_COMMA = /\\d,\\d/;\n\n/** What separates the columns of one line. A regular expression only for the\n * whitespace case, because a run of spaces is one separator. */\ntype Separator = string | RegExp;\n\n/** Where the decision about the comma is taken. See the module docstring: the\n * file door decides once for the whole text, the paste door decides per line. */\nexport type DecimalRule = \"per-line\" | \"whole-text\";\n\nexport interface ParseTableOptions {\n decimal: DecimalRule;\n /**\n * How many columns the caller already knows the table has. A grid with fixed\n * columns knows its width before a number arrives: anything past it is sliced\n * off — an export with a trailing timestamp column is not an error worth\n * refusing — and a line with fewer is reported. Omitted where the width is the\n * file's own answer.\n */\n columns?: number;\n /**\n * How many leading lines that are not rows of numbers may be a header.\n * Default 1, the name row. {@link parseRows} passes 2, because a bench export\n * writes the units on a second line under the names.\n */\n headerLines?: number;\n}\n\nexport interface ParsedTable {\n rows: number[][];\n /** The first header line's fields — the column *names*. A units line under it\n * is absorbed and not returned: what a column is measured in is the caller's to\n * say (its column definitions), and a second list of strings here would be a\n * second answer to that. */\n header: string[] | null;\n /** Whether a comma was read as a decimal mark. Under `\"per-line\"` this is true\n * if any line was read that way, and it exists for the reader: a guess\n * nobody can see is a guess nobody can correct. */\n decimalComma: boolean;\n /** Line numbers — 1-based, counted in the text as it was handed over, blank\n * lines included — that held something which is not a row of numbers. A list\n * rather than a count, because one door names the first one and the other\n * counts them. */\n skipped: number[];\n}\n\n/**\n * The columns of one line, as text, with the decimal mark normalised to a point.\n *\n * Text rather than numbers because a grid holds cells as strings: a pasted\n * field that is not a number belongs in the cell it landed in, marked, where the\n * person who pasted it can see which column went wrong. Dropping it here would\n * shift every column after it and lose the evidence.\n */\nexport function splitRow(line: string): string[] {\n const separator = separatorOf(line);\n const decimalComma = separator !== \",\";\n return fieldsOf(line, separator).map((field) => (decimalComma ? point(field) : field));\n}\n\n/**\n * A pasted table, or the lines of it that are not one.\n *\n * The lexer both doors are built on. It reads lines, drops blanks and `#`\n * banners, decides what separates the columns, takes the header off the top and\n * converts what is left.\n */\nexport function parseTable(text: string, options: ParseTableOptions): ParsedTable {\n const headerLimit = options.headerLines ?? 1;\n const lines = contentLines(text);\n if (!lines.length) return { rows: [], header: null, decimalComma: false, skipped: [] };\n const fixed = options.decimal === \"whole-text\" ? wholeTextRule(lines) : null;\n\n let header: string[] | null = null;\n let headerCount = 0;\n const rows: number[][] = [];\n const skipped: number[] = [];\n let sawDecimalComma = false;\n\n for (const { line, number } of lines) {\n const separator = fixed ? fixed.separator : separatorOf(line);\n const decimalComma = fixed ? fixed.decimalComma : separator !== \",\";\n const fields = fieldsOf(line, separator);\n const numbers = fields.map((field) => Number(decimalComma ? point(field) : field));\n const readable = fields.length > 0 && numbers.every(Number.isFinite);\n\n if (!readable) {\n // A header is only a header at the top, and only before any data: a stray\n // word further down is a broken row, and dropping it silently would lose a\n // measurement without saying so.\n if (rows.length === 0 && headerCount < headerLimit) {\n header ??= fields;\n headerCount += 1;\n continue;\n }\n skipped.push(number);\n continue;\n }\n if (options.columns !== undefined && fields.length < options.columns) {\n skipped.push(number);\n continue;\n }\n if (decimalComma && DECIMAL_COMMA.test(line)) sawDecimalComma = true;\n rows.push(options.columns === undefined ? numbers : numbers.slice(0, options.columns));\n }\n\n return {\n rows,\n header,\n decimalComma: fixed ? fixed.decimalComma : sawDecimalComma,\n skipped,\n };\n}\n\n/**\n * The paste door: a fixed number of columns, and the line that stopped it.\n *\n * Read per line, with up to two header lines (names, then units). It answers with a\n * line number rather than with what it managed to read, because half a pasted\n * sweep saved as a measurement is worse than a refused paste — and because a\n * table that comes back **empty** is the one answer a reader cannot act on: it\n * looks exactly like a paste that did not happen. So text that has content and\n * produced no row at all is an error naming its first unreadable line, never\n * `{ rows: [] }`.\n */\nexport function parseRows(text: string, columns: number): { rows: number[][] } | { error: number } {\n const lines = contentLines(text);\n if (!lines.length) return { rows: [] };\n const parsed = parseTable(text, { decimal: \"per-line\", columns, headerLines: 2 });\n if (parsed.skipped.length) return { error: parsed.skipped[0] };\n if (!parsed.rows.length) return { error: lines[0].number };\n return { rows: parsed.rows };\n}\n\n/** One cell as a number. Unconditional: a cell has no columns in it, so a comma\n * in one is a decimal mark and nothing else. */\nexport const cellNumber = (cell: string) => Number(cell.replace(\",\", \".\"));\n\n/** Whether one cell holds a number yet. An empty cell is not one — it is a hole\n * in the measurement, which is a different thing from a zero. */\nexport const isCellNumber = (cell: string) =>\n cell.trim() !== \"\" && !Number.isNaN(cellNumber(cell));\n\n/**\n * The one separator and the one decimal mark a whole file is read with.\n *\n * The separator is taken from the line most likely to be data — the second,\n * where there is one, because the first is usually the names.\n *\n * A comma between two digits is then a decimal mark whenever something else is\n * already separating the columns: there is nothing else it could be. The rule\n * this replaces also demanded that no decimal POINT appear anywhere in the file,\n * which read a semicolon-separated export with one column written by a different\n * tool — an entirely ordinary thing for a bench to produce — as a table with no\n * rows in it, and said nothing.\n */\nfunction wholeTextRule(lines: { line: string }[]): { separator: Separator; decimalComma: boolean } {\n const separator = separatorOf(lines[lines.length > 1 ? 1 : 0].line);\n const body = lines.map((entry) => entry.line).join(\"\\n\");\n return { separator, decimalComma: separator !== \",\" && DECIMAL_COMMA.test(body) };\n}\n\n/** Lines with something on them, numbered as they stand in the original text so\n * a message can point at one. `#` banners are a recorder's, not a measurement. */\nfunction contentLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) => line !== \"\" && !line.startsWith(\"#\"));\n}\n\n/**\n * What separates this line's columns.\n *\n * In order of trust, not of count: a semicolon-separated line with decimal\n * commas has more commas than semicolons, and a tab-separated one may have both.\n * Whichever of tab and semicolon appears wins outright, because neither is ever\n * anything but a separator.\n *\n * The comma is the one that has to be argued about. A line that has whitespace\n * between its fields *and* a comma between two digits has already said what\n * separates it — so the comma is a decimal mark, and the whitespace separates.\n * That is a German export printed or dumped from MATLAB (`0,5 1,25 2,75`), and\n * reading its commas as separators produced six columns from three: each number\n * split into a pair of small integers, accepted as a row, and saved as a\n * measurement with nothing on screen to say so. A comma with a space after it is\n * not that shape (`DECIMAL_COMMA` does not match `1, 2`), so a hand-typed list\n * still separates on the comma.\n */\nfunction separatorOf(line: string): Separator {\n if (line.includes(\"\\t\")) return \"\\t\";\n if (line.includes(\";\")) return \";\";\n if (line.includes(\",\")) return /\\s/.test(line) && DECIMAL_COMMA.test(line) ? /\\s+/ : \",\";\n return /\\s+/;\n}\n\n/** One line into trimmed, non-empty fields, as they were written. The header is\n * returned out of these, so a column called `Weg,X` keeps its name even where\n * the numbers under it are being read with a decimal comma. */\nfunction fieldsOf(line: string, separator: Separator): string[] {\n return line\n .split(separator)\n .map((field) => field.trim())\n .filter((field) => field !== \"\");\n}\n\n/** The decimal mark this language of `Number` understands. One comma, because a\n * field with two of them is a grouped number and this module does not read\n * those — it reports the line instead. */\nconst point = (field: string) => field.replace(\",\", \".\");\n\n// ── A table of text ────────────────────────────────────────────────────────────\n\n/**\n * What separates the columns of a {@link TextTable}, named. `\" \"` is a run of\n * whitespace — one separator however many spaces or tabs it is made of — as in a\n * printed or MATLAB-dumped table.\n */\nexport type TableSeparator = \"\\t\" | \";\" | \",\" | \" \";\n\n/** One line of the text as it was handed over: its 1-based number (blank lines\n * counted, so a reader can count to it in the box they pasted into) and its text,\n * trimmed. */\nexport interface TableLine {\n line: number;\n text: string;\n}\n\nexport interface ParseTextTableOptions {\n /** The separator, decided by the caller — a \"separated by\" override, a format\n * the app already knows. Default: detected (see {@link parseTextTable}). */\n separator?: TableSeparator;\n /** Whether a comma between two digits is a decimal mark. Default: the\n * `\"whole-text\"` rule, decided once for the file. */\n decimalComma?: boolean;\n /** Whether the table's first line names its columns. Default: detected — a\n * word over a column of values (\"Amount\" over `-12,50`) is a header. */\n header?: boolean;\n}\n\n/**\n * A table read as text: every cell a string, as written.\n *\n * The *file* door's decisions, for a table that is not only numbers. Everything a\n * reader has to know to trust it is in the answer — the separator and the decimal\n * mark that were assumed, and the lines that were not read, with their number and\n * their text — because a guess nobody can see is a guess nobody can correct.\n */\nexport interface TextTable {\n /** The first line's cells, when it names the columns; `null` when it is data. */\n header: string[] | null;\n /** Each read row, `width` cells long. An empty cell is `\"\"`, kept in its place. */\n rows: string[][];\n /** Where each row came from — row-aligned with `rows`. */\n lines: TableLine[];\n /** How many columns the table has. */\n width: number;\n separator: TableSeparator;\n /** Whether a comma is this table's decimal mark — the convention its numbers are\n * written in, to read them with {@link tableNumber} and to say back to the reader. */\n decimalComma: boolean;\n /** Lines that are not a row of this table: a title above it, a total under it, a\n * row with a cell too many or too few. */\n unread: TableLine[];\n}\n\n/**\n * A pasted or dropped table, read as text — the lexer's file door for a table whose\n * cells are not all numbers (a bank export: a date, a payee, an amount), and the\n * parse under {@link ColumnMapper}.\n *\n * Moved under the same roof as {@link parseTable} because it has the same three\n * questions — what separates the columns, what the comma is, whether the first\n * line is a header — and answering them a second time somewhere else is how the\n * three readers this module replaced came to disagree. Kurvenschmiede's columns\n * input and keksdose's bank-file import both read a table whose columns the reader\n * then assigns, and both state what was assumed; this is that reading, once.\n *\n * Where it differs from {@link parseTable}, and why:\n *\n * * **Cells stay text.** Which cell is a number is the caller's question (its\n * column roles), so nothing here converts. {@link tableNumber} reads one in the\n * table's own convention.\n * * **An empty cell keeps its place.** A bank row with no memo has `;;` in it, and\n * dropping that field — which the numeric lexer does — would move the amount into\n * the memo's column. A trailing separator that ends most lines (`1;2;3;`, which\n * spreadsheets write) is a line ending, not a column.\n * * **Quoted cells.** `\"Example Ltd; branch 2\"` is one cell, and `\"\"` inside quotes\n * is a quote. Text has separators in it where numbers never do. A quoted cell that\n * runs over a line break is not joined — the line then has the wrong number of\n * cells and is reported.\n * * **The width is the file's.** The column count most lines share. A line with\n * another count is reported in `unread` rather than padded or cut: a row with a\n * cell too many is a row whose columns have slid, and reading it would put a payee\n * in the amount.\n * * **The separator is voted on**, over the first lines, rather than taken from the\n * second: an export that opens with a title block (a bank's account name and\n * period over the table) would otherwise be read with the title's separator.\n * * **The header is detected by its words.** The numeric lexer's rule — \"a line\n * that is not numbers\" — cannot work where the rows are text too. A first line is\n * a header when one of its cells is a word and every cell under it, as far as the\n * first rows go, is a value (digits and the marks numbers and dates are written\n * with): \"Amount\" over `-12,50`, \"Date\" over `01.02.2026`. A table of words only\n * is read without one; `options.header` says otherwise.\n *\n * Blank lines and `#` banners are skipped, as by the numeric lexer.\n */\nexport function parseTextTable(text: string, options: ParseTextTableOptions = {}): TextTable {\n const content = textLines(text);\n if (!content.length) {\n return { header: null, rows: [], lines: [], width: 0, separator: options.separator ?? \",\", decimalComma: false, unread: [] };\n }\n const separator = options.separator ?? votedSeparator(content);\n const decimalComma =\n options.decimalComma ?? (separator !== \",\" && DECIMAL_COMMA.test(content.map((entry) => entry.line).join(\"\\n\")));\n\n // A trailing separator is a line ending only where it is the file's habit: in a\n // file where most lines end in one. Elsewhere it closes an empty last cell.\n const trailing =\n separator !== \" \" && content.filter(({ line }) => line.endsWith(separator)).length * 2 > content.length;\n const split = content.map(({ line, number }) => {\n const cells = cellsOf(line, separator);\n if (trailing && line.endsWith(separator) && cells[cells.length - 1] === \"\") cells.pop();\n return { cells, entry: { line: number, text: line } };\n });\n\n const width = commonWidth(split.map(({ cells }) => cells.length));\n const first = split.findIndex(({ cells }) => cells.length === width);\n const unread: TableLine[] = split.slice(0, first).map(({ entry }) => entry);\n const body = split.slice(first);\n const fitting = body.filter(({ cells }) => cells.length === width);\n const header = options.header ?? looksLikeHeader(fitting[0].cells, fitting.slice(1, 21).map(({ cells }) => cells));\n\n const rows: string[][] = [];\n const lines: TableLine[] = [];\n body.forEach(({ cells, entry }, index) => {\n if (index === 0 && header) return;\n if (cells.length !== width) {\n unread.push(entry);\n return;\n }\n rows.push(cells);\n lines.push(entry);\n });\n\n return { header: header ? body[0].cells : null, rows, lines, width, separator, decimalComma, unread };\n}\n\n/**\n * One cell of a {@link TextTable} as a number, read in the table's convention.\n *\n * Not {@link cellNumber}, which reads every comma as a decimal mark — right for a\n * cell typed on its own, wrong for a cell of a file written with decimal points,\n * where `1,234` is a grouped thousand and reading it as 1.234 would be off by a\n * factor of a thousand without a word. Here a comma is a decimal mark only in a\n * table whose convention it is, and a cell with both marks is `NaN` either way: the\n * lexer does not read grouped numbers, it reports them. An empty cell is `NaN` too —\n * a hole in the table, not a zero.\n */\nexport function tableNumber(cell: string, decimalComma: boolean): number {\n const trimmed = cell.trim();\n if (trimmed === \"\") return Number.NaN;\n return Number(decimalComma ? point(trimmed) : trimmed);\n}\n\nconst SEPARATOR_TRUST: readonly TableSeparator[] = [\"\\t\", \";\", \",\", \" \"];\n\n/** {@link contentLines} for a table of text: spaces come off the ends, but a TAB\n * stays — in a tab-separated export a leading tab is an empty first cell, and\n * trimming it would slide the whole row one column to the left. */\nfunction textLines(text: string): { line: string; number: number }[] {\n return text\n .split(/\\r?\\n/)\n .map((line, index) => ({ line: line.replace(/^ +| +$/g, \"\"), number: index + 1 }))\n .filter(({ line }) => line.trim() !== \"\" && !line.trimStart().startsWith(\"#\"));\n}\n\n/** The separator most of the first lines are written with — a title block above the\n * table loses the vote to the table under it. A tie goes to the more trusted one. */\nfunction votedSeparator(lines: { line: string }[]): TableSeparator {\n const votes = new Map<TableSeparator, number>();\n for (const { line } of lines.slice(0, 50)) {\n const found = separatorOf(withoutQuoted(line));\n const named: TableSeparator = typeof found === \"string\" ? (found as TableSeparator) : \" \";\n votes.set(named, (votes.get(named) ?? 0) + 1);\n }\n let best: TableSeparator = SEPARATOR_TRUST[0];\n let bestCount = -1;\n for (const candidate of SEPARATOR_TRUST) {\n const count = votes.get(candidate) ?? 0;\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return best;\n}\n\n/** A line with its quoted parts emptied, so a separator inside quotes does not vote. */\nconst withoutQuoted = (line: string) => line.replace(/\"(?:[^\"]|\"\")*\"/g, '\"\"');\n\n/**\n * One line into cells: trimmed, quotes taken off, empty cells kept in their place.\n * A run of whitespace is one separator and makes no empty cells.\n */\nfunction cellsOf(line: string, separator: TableSeparator): string[] {\n if (separator === \" \") return line.split(/\\s+/).filter((field) => field !== \"\").map(unquote);\n const cells: string[] = [];\n let cell = \"\";\n let quoted = false;\n let started = false;\n for (let index = 0; index < line.length; index += 1) {\n const char = line[index];\n if (quoted) {\n if (char === '\"' && line[index + 1] === '\"') {\n cell += '\"';\n index += 1;\n } else if (char === '\"') {\n quoted = false;\n } else {\n cell += char;\n }\n } else if (char === separator) {\n cells.push(cell.trim());\n cell = \"\";\n started = false;\n } else if (char === '\"' && !started) {\n quoted = true;\n started = true;\n cell = \"\";\n } else {\n if (char.trim() !== \"\") started = true;\n cell += char;\n }\n }\n cells.push(cell.trim());\n return cells;\n}\n\n/** A whitespace-separated field with its quotes off. */\nfunction unquote(field: string): string {\n return field.length >= 2 && field.startsWith('\"') && field.endsWith('\"') ? field.slice(1, -1).replace(/\"\"/g, '\"') : field;\n}\n\n/** The column count most lines share; a tie goes to the wider table. */\nfunction commonWidth(counts: number[]): number {\n const tally = new Map<number, number>();\n for (const count of counts) tally.set(count, (tally.get(count) ?? 0) + 1);\n let width = 0;\n let best = 0;\n for (const [count, times] of tally) {\n if (times > best || (times === best && count > width)) {\n width = count;\n best = times;\n }\n }\n return width;\n}\n\n/** A cell with a letter in it — a name, not a value. */\nconst isWord = (cell: string) => /\\p{L}/u.test(cell);\n/** A cell made of digits and the marks numbers, dates and times are written with. */\nconst isValue = (cell: string) => /\\d/.test(cell) && !isWord(cell);\n\n/**\n * Whether a first line names the columns under it. A word over a column of values\n * says so; with no rows to compare against, a line of words alone does.\n */\nfunction looksLikeHeader(first: string[], below: string[][]): boolean {\n if (!below.length) return first.some((cell) => cell !== \"\") && first.every((cell) => cell === \"\" || isWord(cell));\n return first.some((cell, column) => {\n if (!isWord(cell)) return false;\n const values = below.map((row) => row[column]).filter((value) => value !== \"\");\n return values.length > 0 && values.every(isValue);\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,15 +1,23 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { FieldValues, FieldPath, ControllerProps, ControllerRenderProps, ControllerFieldState } from 'react-hook-form';
4
- import { aR as IbanInputProps, bc as PhoneInputProps, bg as SelectProps, a$ as InputProps, bB as TextareaProps } from '../kit-labels-R8mIAc9N.js';
4
+ import { b5 as IbanInputProps, bs as PhoneInputProps, bw as SelectProps, bf as InputProps, bR as TextareaProps } from '../feedback-BxeQVzwq.js';
5
5
  import { NumberFieldProps } from '../components/number-field.js';
6
6
  import { DatePickerProps, DateRangePickerProps } from '../components/date-picker.js';
7
7
  import { TimeInputProps } from '../components/time-input.js';
8
+ import { CountrySelectProps } from '../components/country-select.js';
9
+ import { MonthPickerProps } from '../components/month-picker.js';
8
10
  import { ToggleGroupBaseProps, ToggleGroupRequiredProps, ToggleGroupClearableProps } from '../components/toggle-group.js';
9
11
  import { CheckboxProps } from '../components/checkbox.js';
10
12
  import { EntityComboboxProps } from '../components/entity-combobox.js';
11
13
  import { ComboboxProps } from '../components/combobox.js';
12
14
  import { ComboClearValue } from '../components/combobox-core.js';
15
+ import '../feedback-attachment-fGAzZPf0.js';
16
+ import '../feedback/feedback-inbox.js';
17
+ import 'lucide-react';
18
+ import '../components/tooltip.js';
19
+ import '../hooks/use-anchored-rect.js';
20
+ import '../lib/clipping.js';
13
21
  import '../data-table-labels-B7OdnM0S.js';
14
22
  import '../components/data-table-sort.js';
15
23
  import '../components/swipeable-row.js';
@@ -17,17 +25,12 @@ import '../components/mini-calendar.js';
17
25
  import '../components/calendar-heatmap.js';
18
26
  import '../components/popover.js';
19
27
  import '../components/chip.js';
20
- import 'lucide-react';
21
28
  import '../components/field-sync.js';
22
- import '../components/tooltip.js';
23
- import '../hooks/use-anchored-rect.js';
24
- import '../lib/clipping.js';
25
29
  import '../wizard/types.js';
26
30
  import '../tour/tour.js';
27
31
  import '../search/command-palette.js';
28
32
  import '../search/global-search.js';
29
33
  import '../search/search-index.js';
30
- import '../components/month-picker.js';
31
34
  import '../components/page-contents.js';
32
35
  import '../components/series-chart-labels.js';
33
36
  import '../components/pie-chart-labels.js';
@@ -42,9 +45,6 @@ import '../components/dialog-frame.js';
42
45
  import '../components/modal.js';
43
46
  import '../hooks/use-search-param-state.js';
44
47
  import '../components/measured-grid.js';
45
- import '../feedback-attachment-NhUm7Zga.js';
46
- import '../feedback/feedback-thread.js';
47
- import '../feedback/feedback-inbox.js';
48
48
  import '../components/account-settings-labels.js';
49
49
  import '../components/floating-panel.js';
50
50
  import '../components/bulk-action-bar.js';
@@ -69,11 +69,14 @@ import '../components/server-wake.js';
69
69
  import '../lib/server-wake.js';
70
70
  import '../components/translation-review-labels.js';
71
71
  import '../lib/translation-review.js';
72
- import '../components/country-select.js';
73
72
  import '../components/inline-edit-field.js';
74
73
  import '../lib/iban.js';
75
74
  import '../lib/phone.js';
76
75
  import '../components/sign-chip.js';
76
+ import '../components/column-mapper.js';
77
+ import '../lib/column-mapping.js';
78
+ import '../lib/table-text.js';
79
+ import '../components/field-parts.js';
77
80
  import '../components/number-input.js';
78
81
  import '../components/calculator.js';
79
82
  import '../components/numpad-sheet.js';
@@ -232,6 +235,10 @@ type RhfMoneyFieldProps<TFieldValues extends FieldValues = FieldValues, TName ex
232
235
  * with `rules` as well if a clamp needs explaining. */
233
236
  min?: number;
234
237
  max?: number;
238
+ /** Classes for the `<input>` — `className` is the item's box. {@link RhfTextField},
239
+ * {@link RhfTextarea} and {@link RhfNumberField} take it; since 0.23 AmountInput
240
+ * does too (keksdose G7: `text-end` for a money column). */
241
+ inputClassName?: string;
235
242
  };
236
243
  /**
237
244
  * The kit's {@link AmountInput}: a money figure with its currency chip and the
@@ -339,6 +346,77 @@ type RhfPhoneInputProps<TFieldValues extends FieldValues = FieldValues, TName ex
339
346
  * schema's or `rules`'.
340
347
  */
341
348
  declare function RhfPhoneInput<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues>({ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className, ...phoneProps }: RhfPhoneInputProps<TFieldValues, TName, TTransformed>): react.JSX.Element;
349
+ type OwnCountrySelectProps = Omit<CountrySelectProps<boolean>, "value" | "onChange" | "onBlur" | "label" | "hint" | "error" | "invalid" | "disabled" | "className" | "id" | "ref" | "clearable">;
350
+ type RhfCountrySelectProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnCountrySelectProps & {
351
+ /** Offer the picker's clear "×" (and Delete on the trigger) while a country is
352
+ * chosen. A clear stores `clearValue`. */
353
+ clearable?: boolean;
354
+ /** What a clear stores. Default `null`; `""` for a schema that spells "no
355
+ * country" as an empty string (a zod `z.string().max(2)`, kastlan's). The picker
356
+ * reads either back as empty. */
357
+ clearValue?: ComboClearValue;
358
+ /** Classes for the picker — `className` is the item's box. */
359
+ selectClassName?: string;
360
+ };
361
+ /**
362
+ * {@link CountrySelect} bound to one field, which stores the ISO 3166-1 alpha-2 code
363
+ * ("CH", always upper-case), or `clearValue` once cleared (`clearable`). kastlan asked
364
+ * for it (0.22 adoption): its address form wired a `CountrySelect` through
365
+ * {@link RhfField}'s render, and with no ref on the picker react-hook-form's
366
+ * focus-on-error had nothing to focus — the one field on the form a failed submit
367
+ * could not take you to.
368
+ *
369
+ * Bound the way {@link RhfIbanInput} and {@link RhfPhoneInput} are: `field.ref` reaches
370
+ * the trigger (the picker's `ref`, 0.23), the form's error paints the trigger and is
371
+ * the shell's message under it, the hint is the shell's description — so neither is
372
+ * ever handed to the picker, and its box never changes when one comes or goes. The
373
+ * form's label names the trigger together with the chosen country, and is the phone
374
+ * sheet's title. `commit` and `disabledReason` are the picker's own: a country that
375
+ * saves itself stays focusable under a lock and says why.
376
+ *
377
+ * ```tsx
378
+ * <RhfCountrySelect name="country" label="Country" required
379
+ * preferred={["CH", "LI", "DE", "AT", "FR", "IT"]} rules={{ required: "Choose a country" }} />
380
+ * ```
381
+ */
382
+ declare function RhfCountrySelect<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues>({ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className, clearable, clearValue, selectClassName, labels, ...countryProps }: RhfCountrySelectProps<TFieldValues, TName, TTransformed>): react.JSX.Element;
383
+ type OwnMonthPickerProps = Omit<MonthPickerProps, "value" | "onChange" | "onBlur" | "label" | "hint" | "error" | "invalid" | "disabled" | "className" | "id" | "mode">;
384
+ /** `mode` picks what the form stores, and `valueAsNumber` is only offered for a year. */
385
+ type RhfMonthPickerMode = {
386
+ /** A month, stored as `"YYYY-MM"` (the default). */
387
+ mode?: "month";
388
+ valueAsNumber?: undefined;
389
+ } | {
390
+ /** A year, stored as `"YYYY"` — or as a number with `valueAsNumber`. */
391
+ mode: "year";
392
+ /** Store the year as a number (`2026`) rather than `"2026"` — kastlan's budget
393
+ * `fiscal_year` is an integer column. A number in the form reads back as its
394
+ * year. */
395
+ valueAsNumber?: boolean;
396
+ };
397
+ type RhfMonthPickerProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnMonthPickerProps & {
398
+ /** Classes for the picker — `className` is the item's box. As {@link RhfDateField}'s. */
399
+ inputClassName?: string;
400
+ } & RhfMonthPickerMode;
401
+ /**
402
+ * {@link MonthPicker} bound to one field: a month as `"YYYY-MM"`, or with `mode="year"` a
403
+ * year as `"YYYY"` (`valueAsNumber` stores `2026`). kastlan asked for it with
404
+ * {@link RhfCountrySelect}: its budget wizard's fiscal year was a `MonthPicker
405
+ * mode="year"` inside {@link RhfField}'s render — the `String()` in, the `Number()`
406
+ * out, `invalid` by hand — and a failed submit could not focus it, because the picker
407
+ * forwards no ref.
408
+ *
409
+ * Bound as {@link RhfDateField} is: a focus handle finds the trigger by its id, so
410
+ * focus-on-error lands there; the form's error paints the trigger and is the shell's
411
+ * message; the hint is the shell's description; neither is handed to the picker, so
412
+ * its box never changes. Picking marks the field touched.
413
+ *
414
+ * ```tsx
415
+ * <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber
416
+ * min="2020" max="2029" rules={{ required: "Choose a year" }} />
417
+ * ```
418
+ */
419
+ declare function RhfMonthPicker<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues>({ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className, inputClassName, mode, valueAsNumber, ...pickerProps }: RhfMonthPickerProps<TFieldValues, TName, TTransformed>): react.JSX.Element;
342
420
  interface RhfSelectOption {
343
421
  value: string | number;
344
422
  label: ReactNode;
@@ -430,4 +508,4 @@ type RhfTextComboboxProps<TFieldValues extends FieldValues = FieldValues, TName
430
508
  */
431
509
  declare function RhfTextCombobox<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues>({ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className, comboClassName, ...comboProps }: RhfTextComboboxProps<TFieldValues, TName, TTransformed>): react.JSX.Element;
432
510
 
433
- export { RhfCheckbox, type RhfCheckboxProps, RhfCombobox, type RhfComboboxProps, RhfDateField, type RhfDateFieldProps, RhfDateRangePicker, type RhfDateRangePickerProps, RhfField, type RhfFieldBaseProps, type RhfFieldProps, type RhfFieldRenderContext, RhfIbanInput, type RhfIbanInputProps, RhfIntegerField, type RhfIntegerFieldProps, RhfMoneyField, type RhfMoneyFieldProps, RhfNumberField, type RhfNumberFieldProps, RhfPhoneInput, type RhfPhoneInputProps, RhfSelect, type RhfSelectOption, type RhfSelectProps, RhfTextCombobox, type RhfTextComboboxProps, RhfTextField, type RhfTextFieldProps, RhfTextarea, type RhfTextareaProps, RhfTimeInput, type RhfTimeInputProps, RhfToggleGroup, type RhfToggleGroupProps };
511
+ export { RhfCheckbox, type RhfCheckboxProps, RhfCombobox, type RhfComboboxProps, RhfCountrySelect, type RhfCountrySelectProps, RhfDateField, type RhfDateFieldProps, RhfDateRangePicker, type RhfDateRangePickerProps, RhfField, type RhfFieldBaseProps, type RhfFieldProps, type RhfFieldRenderContext, RhfIbanInput, type RhfIbanInputProps, RhfIntegerField, type RhfIntegerFieldProps, RhfMoneyField, type RhfMoneyFieldProps, RhfMonthPicker, type RhfMonthPickerProps, RhfNumberField, type RhfNumberFieldProps, RhfPhoneInput, type RhfPhoneInputProps, RhfSelect, type RhfSelectOption, type RhfSelectProps, RhfTextCombobox, type RhfTextComboboxProps, RhfTextField, type RhfTextFieldProps, RhfTextarea, type RhfTextareaProps, RhfTimeInput, type RhfTimeInputProps, RhfToggleGroup, type RhfToggleGroupProps };
@@ -15,6 +15,8 @@ import {
15
15
  import { TimeInput } from "../components/time-input.js";
16
16
  import { IbanInput } from "../components/iban-input.js";
17
17
  import { PhoneInput } from "../components/phone-input.js";
18
+ import { CountrySelect } from "../components/country-select.js";
19
+ import { MonthPicker } from "../components/month-picker.js";
18
20
  import {
19
21
  ToggleGroup
20
22
  } from "../components/toggle-group.js";
@@ -631,6 +633,126 @@ function RhfPhoneInput({
631
633
  }
632
634
  );
633
635
  }
636
+ function RhfCountrySelect({
637
+ name,
638
+ control,
639
+ rules,
640
+ label,
641
+ hint,
642
+ required,
643
+ disabled,
644
+ excludeWhenDisabled,
645
+ className,
646
+ clearable,
647
+ clearValue = null,
648
+ selectClassName,
649
+ labels,
650
+ ...countryProps
651
+ }) {
652
+ const labelled = hasContent(label);
653
+ const ownLabels = typeof label === "string" ? { country: label, ...labels } : labels;
654
+ return /* @__PURE__ */ jsx(
655
+ RhfField,
656
+ {
657
+ ...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className },
658
+ render: ({ field, invalid, labelId }) => {
659
+ const value = field.value;
660
+ return (
661
+ // FormControl clones this element with `id` / `aria-describedby` /
662
+ // `aria-invalid`, which the picker routes to its trigger.
663
+ /* @__PURE__ */ jsx(
664
+ CountrySelect,
665
+ {
666
+ "aria-required": required || void 0,
667
+ "aria-labelledby": labelled ? labelId : void 0,
668
+ ...countryProps,
669
+ labels: ownLabels,
670
+ className: selectClassName,
671
+ ref: field.ref,
672
+ value: typeof value === "string" ? value : null,
673
+ clearable,
674
+ onChange: (code) => field.onChange(code ?? clearValue),
675
+ onBlur: field.onBlur,
676
+ disabled: field.disabled,
677
+ invalid
678
+ }
679
+ )
680
+ );
681
+ }
682
+ }
683
+ );
684
+ }
685
+ function MonthControl({
686
+ field,
687
+ invalid,
688
+ id,
689
+ valueAsNumber,
690
+ mode,
691
+ inputClassName,
692
+ pickerProps,
693
+ required,
694
+ ...aria
695
+ }) {
696
+ useFocusHandle(field.ref, () => document.getElementById(id));
697
+ const value = field.value;
698
+ return /* @__PURE__ */ jsx(
699
+ MonthPicker,
700
+ {
701
+ ...pickerProps,
702
+ ...aria,
703
+ "aria-required": required || void 0,
704
+ id,
705
+ mode,
706
+ value: value === null || value === void 0 ? "" : String(value),
707
+ onChange: (key) => {
708
+ field.onChange(mode === "year" && valueAsNumber ? Number(key) : key);
709
+ field.onBlur();
710
+ },
711
+ disabled: field.disabled,
712
+ invalid,
713
+ className: inputClassName
714
+ }
715
+ );
716
+ }
717
+ function RhfMonthPicker({
718
+ name,
719
+ control,
720
+ rules,
721
+ label,
722
+ hint,
723
+ required,
724
+ disabled,
725
+ excludeWhenDisabled,
726
+ className,
727
+ inputClassName,
728
+ mode,
729
+ valueAsNumber,
730
+ ...pickerProps
731
+ }) {
732
+ return /* @__PURE__ */ jsx(
733
+ RhfField,
734
+ {
735
+ ...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className },
736
+ render: ({ field, invalid, id }) => (
737
+ // FormControl clones this element with `aria-describedby` / `aria-invalid`,
738
+ // which MonthControl hands on to the picker's trigger.
739
+ /* @__PURE__ */ jsx(
740
+ MonthControl,
741
+ {
742
+ field,
743
+ invalid,
744
+ id,
745
+ mode,
746
+ valueAsNumber,
747
+ inputClassName,
748
+ pickerProps,
749
+ required
750
+ }
751
+ )
752
+ )
753
+ }
754
+ );
755
+ }
634
756
  function RhfSelect({
635
757
  name,
636
758
  control,
@@ -942,12 +1064,14 @@ function TextComboControl({
942
1064
  export {
943
1065
  RhfCheckbox,
944
1066
  RhfCombobox,
1067
+ RhfCountrySelect,
945
1068
  RhfDateField,
946
1069
  RhfDateRangePicker,
947
1070
  RhfField,
948
1071
  RhfIbanInput,
949
1072
  RhfIntegerField,
950
1073
  RhfMoneyField,
1074
+ RhfMonthPicker,
951
1075
  RhfNumberField,
952
1076
  RhfPhoneInput,
953
1077
  RhfSelect,