@eifi1/ui-kit 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/dist/components/amount-input.d.ts +7 -0
- package/dist/components/autocomplete.d.ts +103 -0
- package/dist/components/autocomplete.js +260 -0
- package/dist/components/autocomplete.js.map +1 -0
- package/dist/components/calculator.d.ts +7 -0
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +7 -2
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +3 -2
- package/dist/components/chip.js +14 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +100 -0
- package/dist/components/choice-card.js +170 -0
- package/dist/components/choice-card.js.map +1 -0
- package/dist/components/combobox-core.d.ts +76 -6
- package/dist/components/combobox-core.js +119 -49
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +12 -2
- package/dist/components/combobox.js +42 -17
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +91 -0
- package/dist/components/danger-confirm.js +181 -0
- package/dist/components/danger-confirm.js.map +1 -0
- package/dist/components/dialog-frame.d.ts +84 -0
- package/dist/components/dialog-frame.js +86 -0
- package/dist/components/dialog-frame.js.map +1 -0
- package/dist/components/disclosure.d.ts +108 -0
- package/dist/components/disclosure.js +127 -0
- package/dist/components/disclosure.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +17 -3
- package/dist/components/entity-combobox.js +25 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +161 -0
- package/dist/components/file-button.js +243 -0
- package/dist/components/file-button.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +72 -23
- package/dist/components/file-dropzone.js +219 -94
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/icon-picker.d.ts +72 -0
- package/dist/components/icon-picker.js +104 -0
- package/dist/components/icon-picker.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +3 -0
- package/dist/components/mini-calendar.js +4 -3
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +8 -1
- package/dist/components/modal.js +4 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +16 -3
- package/dist/components/multi-entity-combobox.js +25 -5
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +41 -1
- package/dist/components/number-field.js +42 -10
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +35 -2
- package/dist/components/number-input.js +35 -4
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +7 -0
- package/dist/components/search-field.d.ts +16 -0
- package/dist/components/search-field.js +29 -7
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/signature-pad.d.ts +43 -1
- package/dist/components/signature-pad.js +74 -2
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +69 -0
- package/dist/components/swatch-picker.js +75 -0
- package/dist/components/swatch-picker.js.map +1 -0
- package/dist/components/switch.d.ts +9 -0
- package/dist/components/switch.js +7 -2
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tile-radio.d.ts +50 -0
- package/dist/components/tile-radio.js +140 -0
- package/dist/components/tile-radio.js.map +1 -0
- package/dist/components/toggle-group.d.ts +27 -5
- package/dist/components/toggle-group.js +22 -15
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +150 -12
- package/dist/components/ui.js +196 -22
- package/dist/components/ui.js.map +1 -1
- package/dist/i18n/defaults.d.ts +7 -0
- package/dist/i18n/defaults.js +13 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +38 -5
- package/dist/i18n/kit-labels.js +12 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/table-text.d.ts +127 -0
- package/dist/lib/table-text.js +82 -0
- package/dist/lib/table-text.js.map +1 -0
- package/dist/rhf/form.d.ts +79 -0
- package/dist/rhf/form.js +143 -0
- package/dist/rhf/form.js.map +1 -0
- package/dist/rhf.d.ts +4 -0
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -0
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/table-text.d.ts +1 -0
- package/dist/table-text.js +3 -0
- package/dist/table-text.js.map +1 -0
- package/package.json +14 -1
- package/src/components/autocomplete.tsx +429 -0
- package/src/components/checkbox.tsx +16 -0
- package/src/components/chip.tsx +24 -2
- package/src/components/choice-card.tsx +305 -0
- package/src/components/combobox-core.tsx +228 -58
- package/src/components/combobox.tsx +58 -21
- package/src/components/danger-confirm.tsx +286 -0
- package/src/components/dialog-frame.tsx +179 -0
- package/src/components/disclosure.tsx +259 -0
- package/src/components/entity-combobox.tsx +41 -6
- package/src/components/file-button.tsx +458 -0
- package/src/components/file-dropzone.tsx +323 -117
- package/src/components/icon-picker.tsx +181 -0
- package/src/components/mini-calendar.tsx +7 -3
- package/src/components/modal.tsx +10 -2
- package/src/components/multi-entity-combobox.tsx +40 -6
- package/src/components/number-field.tsx +86 -10
- package/src/components/number-input.tsx +79 -2
- package/src/components/search-field.tsx +49 -6
- package/src/components/signature-pad.tsx +112 -0
- package/src/components/swatch-picker.tsx +141 -0
- package/src/components/switch.tsx +16 -0
- package/src/components/tile-radio.tsx +228 -0
- package/src/components/toggle-group.tsx +54 -18
- package/src/components/ui.tsx +400 -24
- package/src/i18n/defaults.ts +12 -1
- package/src/i18n/kit-labels.tsx +45 -5
- package/src/index.ts +19 -0
- package/src/lib/table-text.ts +265 -0
- package/src/rhf/form.tsx +300 -0
- package/src/rhf.ts +9 -0
- package/src/shell/app-shell.tsx +3 -1
- package/src/table-text.ts +8 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DECIMAL_COMMA = /\d,\d/;
|
|
3
|
+
function splitRow(line) {
|
|
4
|
+
const separator = separatorOf(line);
|
|
5
|
+
const decimalComma = separator !== ",";
|
|
6
|
+
return fieldsOf(line, separator).map((field) => decimalComma ? point(field) : field);
|
|
7
|
+
}
|
|
8
|
+
function parseTable(text, options) {
|
|
9
|
+
const headerLimit = options.headerLines ?? 1;
|
|
10
|
+
const lines = contentLines(text);
|
|
11
|
+
if (!lines.length) return { rows: [], header: null, decimalComma: false, skipped: [] };
|
|
12
|
+
const fixed = options.decimal === "whole-text" ? wholeTextRule(lines) : null;
|
|
13
|
+
let header = null;
|
|
14
|
+
let headerCount = 0;
|
|
15
|
+
const rows = [];
|
|
16
|
+
const skipped = [];
|
|
17
|
+
let sawDecimalComma = false;
|
|
18
|
+
for (const { line, number } of lines) {
|
|
19
|
+
const separator = fixed ? fixed.separator : separatorOf(line);
|
|
20
|
+
const decimalComma = fixed ? fixed.decimalComma : separator !== ",";
|
|
21
|
+
const fields = fieldsOf(line, separator);
|
|
22
|
+
const numbers = fields.map((field) => Number(decimalComma ? point(field) : field));
|
|
23
|
+
const readable = fields.length > 0 && numbers.every(Number.isFinite);
|
|
24
|
+
if (!readable) {
|
|
25
|
+
if (rows.length === 0 && headerCount < headerLimit) {
|
|
26
|
+
header ??= fields;
|
|
27
|
+
headerCount += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
skipped.push(number);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (options.columns !== void 0 && fields.length < options.columns) {
|
|
34
|
+
skipped.push(number);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (decimalComma && DECIMAL_COMMA.test(line)) sawDecimalComma = true;
|
|
38
|
+
rows.push(options.columns === void 0 ? numbers : numbers.slice(0, options.columns));
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
rows,
|
|
42
|
+
header,
|
|
43
|
+
decimalComma: fixed ? fixed.decimalComma : sawDecimalComma,
|
|
44
|
+
skipped
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function parseRows(text, columns) {
|
|
48
|
+
const lines = contentLines(text);
|
|
49
|
+
if (!lines.length) return { rows: [] };
|
|
50
|
+
const parsed = parseTable(text, { decimal: "per-line", columns, headerLines: 2 });
|
|
51
|
+
if (parsed.skipped.length) return { error: parsed.skipped[0] };
|
|
52
|
+
if (!parsed.rows.length) return { error: lines[0].number };
|
|
53
|
+
return { rows: parsed.rows };
|
|
54
|
+
}
|
|
55
|
+
const cellNumber = (cell) => Number(cell.replace(",", "."));
|
|
56
|
+
const isCellNumber = (cell) => cell.trim() !== "" && !Number.isNaN(cellNumber(cell));
|
|
57
|
+
function wholeTextRule(lines) {
|
|
58
|
+
const separator = separatorOf(lines[lines.length > 1 ? 1 : 0].line);
|
|
59
|
+
const body = lines.map((entry) => entry.line).join("\n");
|
|
60
|
+
return { separator, decimalComma: separator !== "," && DECIMAL_COMMA.test(body) };
|
|
61
|
+
}
|
|
62
|
+
function contentLines(text) {
|
|
63
|
+
return text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(({ line }) => line !== "" && !line.startsWith("#"));
|
|
64
|
+
}
|
|
65
|
+
function separatorOf(line) {
|
|
66
|
+
if (line.includes(" ")) return " ";
|
|
67
|
+
if (line.includes(";")) return ";";
|
|
68
|
+
if (line.includes(",")) return /\s/.test(line) && DECIMAL_COMMA.test(line) ? /\s+/ : ",";
|
|
69
|
+
return /\s+/;
|
|
70
|
+
}
|
|
71
|
+
function fieldsOf(line, separator) {
|
|
72
|
+
return line.split(separator).map((field) => field.trim()).filter((field) => field !== "");
|
|
73
|
+
}
|
|
74
|
+
const point = (field) => field.replace(",", ".");
|
|
75
|
+
export {
|
|
76
|
+
cellNumber,
|
|
77
|
+
isCellNumber,
|
|
78
|
+
parseRows,
|
|
79
|
+
parseTable,
|
|
80
|
+
splitRow
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=table-text.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactElement, ComponentProps } from 'react';
|
|
3
|
+
import * as react_hook_form from 'react-hook-form';
|
|
4
|
+
import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
|
|
5
|
+
import { LabelProps } from '../components/ui.js';
|
|
6
|
+
|
|
7
|
+
/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own
|
|
8
|
+
* `FormProvider`, under the name the shadcn file gave it. */
|
|
9
|
+
declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>({ children, watch, getValues, getErrors, getFieldState, setError, clearErrors, setValue, setValues, trigger, formState, resetField, reset, resetDefaultValues, handleSubmit, unregister, control, register, setFocus, subscribe, }: react_hook_form.FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React.JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* react-hook-form's `Controller`, with the field's name put in context for the parts
|
|
12
|
+
* below it. Takes exactly `Controller`'s props.
|
|
13
|
+
*/
|
|
14
|
+
declare function FormField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformedValues = TFieldValues>(props: ControllerProps<TFieldValues, TName, TTransformedValues>): react.JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* The ids and the state of the field around the caller: for a part of your own that
|
|
17
|
+
* sits beside the kit's (a character counter that belongs in `aria-describedby`, a
|
|
18
|
+
* label with a different look).
|
|
19
|
+
*
|
|
20
|
+
* Throws outside a `FormField` + `FormItem` pair. shadcn's version answered there with
|
|
21
|
+
* ids like `undefined-form-item`, which label nothing and fail nowhere.
|
|
22
|
+
*/
|
|
23
|
+
declare function useFormField(): {
|
|
24
|
+
invalid: boolean;
|
|
25
|
+
isDirty: boolean;
|
|
26
|
+
isTouched: boolean;
|
|
27
|
+
isValidating: boolean;
|
|
28
|
+
error?: react_hook_form.FieldError | undefined;
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
formItemId: string;
|
|
32
|
+
formDescriptionId: string;
|
|
33
|
+
formMessageId: string;
|
|
34
|
+
/** The ids of the description and message that are actually rendered, or
|
|
35
|
+
* `undefined` when neither is. */
|
|
36
|
+
describedBy: string | undefined;
|
|
37
|
+
};
|
|
38
|
+
type FormItemProps = ComponentProps<"div">;
|
|
39
|
+
/** One field's box: mints the id its label, control, description and message share. */
|
|
40
|
+
declare function FormItem({ className, ...rest }: FormItemProps): react.JSX.Element;
|
|
41
|
+
type FormLabelProps = LabelProps;
|
|
42
|
+
/**
|
|
43
|
+
* The kit's {@link Label}, bound to the control by `htmlFor` and turned to the danger
|
|
44
|
+
* colour while the field is invalid. `required` draws the kit's `aria-hidden` mark:
|
|
45
|
+
* whether the field is required is the control's to announce.
|
|
46
|
+
*/
|
|
47
|
+
declare function FormLabel({ className, ...rest }: FormLabelProps): react.JSX.Element;
|
|
48
|
+
interface FormControlProps {
|
|
49
|
+
/** Exactly one element: the kit field, or a native one. It receives `id`,
|
|
50
|
+
* `aria-describedby` and `aria-invalid`. */
|
|
51
|
+
children: ReactElement;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Wires its one child to the item: the `id` the label points at, `aria-describedby`
|
|
55
|
+
* naming the rendered description and message, and `aria-invalid` while the field has
|
|
56
|
+
* an error.
|
|
57
|
+
*
|
|
58
|
+
* The child's own values win where it has them (an explicit `id`, a set
|
|
59
|
+
* `aria-invalid`), as they did under Radix `Slot`; its own `aria-describedby` is
|
|
60
|
+
* merged in front of the item's rather than replaced.
|
|
61
|
+
*/
|
|
62
|
+
declare function FormControl({ children }: FormControlProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
63
|
+
type FormDescriptionProps = ComponentProps<"p">;
|
|
64
|
+
/** Standing advice under the field ("at least twelve characters"). */
|
|
65
|
+
declare function FormDescription({ className, ...rest }: FormDescriptionProps): react.JSX.Element;
|
|
66
|
+
type FormMessageProps = ComponentProps<"p">;
|
|
67
|
+
/**
|
|
68
|
+
* The field's error message, from the form — the kit renders no string of its own
|
|
69
|
+
* here; the schema or the `rules` carry the words. Without an error it renders its
|
|
70
|
+
* `children`, if any, and otherwise nothing.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately NOT `role="alert"`, for the reason the kit's `error` prop gives: the
|
|
73
|
+
* message is read when focus reaches the control, which is where a field's own error
|
|
74
|
+
* is wanted, and an alert would interrupt on every keystroke of a form that
|
|
75
|
+
* re-validates as you type.
|
|
76
|
+
*/
|
|
77
|
+
declare function FormMessage({ className, children, ...rest }: FormMessageProps): react.JSX.Element | null;
|
|
78
|
+
|
|
79
|
+
export { Form, FormControl, type FormControlProps, FormDescription, type FormDescriptionProps, FormField, FormItem, type FormItemProps, FormLabel, type FormLabelProps, FormMessage, type FormMessageProps, useFormField };
|
package/dist/rhf/form.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
createContext,
|
|
7
|
+
isValidElement,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useId,
|
|
11
|
+
useLayoutEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useState
|
|
14
|
+
} from "react";
|
|
15
|
+
import {
|
|
16
|
+
Controller,
|
|
17
|
+
FormProvider,
|
|
18
|
+
useFormContext,
|
|
19
|
+
useFormState
|
|
20
|
+
} from "react-hook-form";
|
|
21
|
+
import { Label } from "../components/ui.js";
|
|
22
|
+
import { cn } from "../lib/cn.js";
|
|
23
|
+
const Form = FormProvider;
|
|
24
|
+
const FormFieldContext = createContext(null);
|
|
25
|
+
const FormItemContext = createContext(null);
|
|
26
|
+
function FormField(props) {
|
|
27
|
+
const value = useMemo(() => ({ name: props.name }), [props.name]);
|
|
28
|
+
return /* @__PURE__ */ jsx(FormFieldContext.Provider, { value, children: /* @__PURE__ */ jsx(Controller, { ...props }) });
|
|
29
|
+
}
|
|
30
|
+
function useFormField() {
|
|
31
|
+
const field = useContext(FormFieldContext);
|
|
32
|
+
const item = useContext(FormItemContext);
|
|
33
|
+
if (!field) throw new Error("useFormField must be used inside <FormField>.");
|
|
34
|
+
if (!item) throw new Error("useFormField must be used inside <FormItem>.");
|
|
35
|
+
const { getFieldState } = useFormContext();
|
|
36
|
+
const formState = useFormState({ name: field.name });
|
|
37
|
+
const state = getFieldState(field.name, formState);
|
|
38
|
+
const formDescriptionId = `${item.id}-description`;
|
|
39
|
+
const formMessageId = `${item.id}-message`;
|
|
40
|
+
const describedBy = [
|
|
41
|
+
item.mounted.description && formDescriptionId,
|
|
42
|
+
item.mounted.message && formMessageId
|
|
43
|
+
].filter(Boolean).join(" ");
|
|
44
|
+
return {
|
|
45
|
+
id: item.id,
|
|
46
|
+
name: field.name,
|
|
47
|
+
formItemId: `${item.id}-control`,
|
|
48
|
+
formDescriptionId,
|
|
49
|
+
formMessageId,
|
|
50
|
+
/** The ids of the description and message that are actually rendered, or
|
|
51
|
+
* `undefined` when neither is. */
|
|
52
|
+
describedBy: describedBy || void 0,
|
|
53
|
+
...state
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function FormItem({ className, ...rest }) {
|
|
57
|
+
const id = useId();
|
|
58
|
+
const [mounted, setMounted] = useState({
|
|
59
|
+
description: false,
|
|
60
|
+
message: false
|
|
61
|
+
});
|
|
62
|
+
const mark = useCallback(
|
|
63
|
+
(slot, on) => setMounted((prev) => prev[slot] === on ? prev : { ...prev, [slot]: on }),
|
|
64
|
+
[]
|
|
65
|
+
);
|
|
66
|
+
const value = useMemo(() => ({ id, mounted, mark }), [id, mounted, mark]);
|
|
67
|
+
return /* @__PURE__ */ jsx(FormItemContext.Provider, { value, children: /* @__PURE__ */ jsx("div", { "data-slot": "form-item", ...rest, className: cn("grid gap-1", className) }) });
|
|
68
|
+
}
|
|
69
|
+
function useDescribes(slot, on) {
|
|
70
|
+
const item = useContext(FormItemContext);
|
|
71
|
+
const mark = item?.mark;
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
if (!on || !mark) return;
|
|
74
|
+
mark(slot, true);
|
|
75
|
+
return () => mark(slot, false);
|
|
76
|
+
}, [slot, on, mark]);
|
|
77
|
+
}
|
|
78
|
+
function FormLabel({ className, ...rest }) {
|
|
79
|
+
const { error, formItemId } = useFormField();
|
|
80
|
+
return /* @__PURE__ */ jsx(
|
|
81
|
+
Label,
|
|
82
|
+
{
|
|
83
|
+
"data-slot": "form-label",
|
|
84
|
+
"data-error": error ? true : void 0,
|
|
85
|
+
htmlFor: formItemId,
|
|
86
|
+
...rest,
|
|
87
|
+
className: cn("data-[error=true]:text-[var(--danger)]", className)
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function FormControl({ children }) {
|
|
92
|
+
const { invalid, formItemId, describedBy } = useFormField();
|
|
93
|
+
const child = Children.only(children);
|
|
94
|
+
if (!isValidElement(child)) return child;
|
|
95
|
+
const own = child.props;
|
|
96
|
+
const merged = [own["aria-describedby"], describedBy].filter(Boolean).join(" ");
|
|
97
|
+
return cloneElement(child, {
|
|
98
|
+
id: own.id ?? formItemId,
|
|
99
|
+
"aria-describedby": merged || void 0,
|
|
100
|
+
"aria-invalid": own["aria-invalid"] ?? (invalid || void 0)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function FormDescription({ className, ...rest }) {
|
|
104
|
+
const { formDescriptionId } = useFormField();
|
|
105
|
+
useDescribes("description", true);
|
|
106
|
+
return /* @__PURE__ */ jsx(
|
|
107
|
+
"p",
|
|
108
|
+
{
|
|
109
|
+
"data-slot": "form-description",
|
|
110
|
+
...rest,
|
|
111
|
+
id: formDescriptionId,
|
|
112
|
+
className: cn("text-[11px] leading-tight text-[var(--text-muted)]", className)
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function FormMessage({ className, children, ...rest }) {
|
|
117
|
+
const { error, formMessageId } = useFormField();
|
|
118
|
+
const body = error ? String(error.message ?? "") : children;
|
|
119
|
+
const shown = body !== void 0 && body !== null && body !== false && body !== "";
|
|
120
|
+
useDescribes("message", shown);
|
|
121
|
+
if (!shown) return null;
|
|
122
|
+
return /* @__PURE__ */ jsx(
|
|
123
|
+
"p",
|
|
124
|
+
{
|
|
125
|
+
"data-slot": "form-message",
|
|
126
|
+
...rest,
|
|
127
|
+
id: formMessageId,
|
|
128
|
+
className: cn("text-[11px] leading-tight text-[var(--danger)]", className),
|
|
129
|
+
children: body
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
Form,
|
|
135
|
+
FormControl,
|
|
136
|
+
FormDescription,
|
|
137
|
+
FormField,
|
|
138
|
+
FormItem,
|
|
139
|
+
FormLabel,
|
|
140
|
+
FormMessage,
|
|
141
|
+
useFormField
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/rhf/form.tsx"],"sourcesContent":["/**\n * The react-hook-form adapter: the five parts every form is built from, and the hook\n * they share. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <Form {...form}>\n * <FormField control={form.control} name=\"amount\" render={({ field }) => (\n * <FormItem>\n * <FormLabel required>Amount</FormLabel>\n * <FormControl>\n * <NumberField value={field.value} onCommit={field.onChange} />\n * </FormControl>\n * <FormMessage />\n * </FormItem>\n * )} />\n * </Form>\n * ```\n *\n * The shape is shadcn's `form.tsx`, on purpose: Kastlan builds every form from that\n * file, so moving onto this one is an import line rather than a rewrite. What differs\n * is what the shadcn file gets wrong:\n *\n * - **`aria-describedby` names only what is on screen.** shadcn always points the\n * control at a description id, rendered or not, and at the message id whenever the\n * field has an error, whether a message was rendered for it or not. A reference to\n * a node that does not exist describes the field as nothing. Here the description\n * and the message say when they are mounted, and the control points at exactly\n * those.\n * - **Merged, never replaced.** A control that already carries its own\n * `aria-describedby` keeps it, first — the same rule as the kit's `error` prop\n * (see `useFieldError` in components/ui.tsx).\n * - **No Radix.** `FormControl` is a one-child `cloneElement`, not `Slot`, and the\n * label is the kit's own `Label` — the kit carries no Radix dependency and this\n * entry is not the place to start.\n *\n * ## Why this is its own entry\n *\n * The main barrel once shipped a react-hook-form hook (`use-rhf-wizard-step`) and\n * removed it: it made react-hook-form an install every consumer's typecheck paid for,\n * for a hook nobody called (see the wizard section of src/index.ts). This is the\n * answer to that, not a reversal of it. Nothing outside `src/rhf*` imports\n * react-hook-form — `__tests__/packaging-contract.test.ts` holds that — and the peer is\n * optional, so an app that does not use it installs nothing and imports nothing.\n *\n * ## Painting the field\n *\n * `FormControl` sets `aria-invalid` on its child. `NumberField` and the pickers paint\n * from that attribute; `Input`, `Select` and `Textarea` paint only from their\n * `invalid` prop (see its note in components/ui.tsx: a bare attribute announces but\n * does not paint), so pass it from the render prop —\n * `<Input {...field} invalid={fieldState.invalid} />`. `FormControl` does not inject\n * `invalid` itself because its child may be a native element, which would print it\n * into the DOM as an unknown attribute.\n */\nimport {\n Children,\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useState,\n type ComponentProps,\n type ReactElement,\n} from \"react\";\nimport {\n Controller,\n FormProvider,\n useFormContext,\n useFormState,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\nimport { Label, type LabelProps } from \"../components/ui\";\nimport { cn } from \"../lib/cn\";\n\n/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own\n * `FormProvider`, under the name the shadcn file gave it. */\nexport const Form = FormProvider;\n\n// ── contexts ─────────────────────────────────────────────────────────────────\n\nconst FormFieldContext = createContext<{ name: string } | null>(null);\n\n/** The two optional nodes a control can be described by. */\ntype Described = \"description\" | \"message\";\n\ninterface FormItemContextValue {\n id: string;\n mounted: Record<Described, boolean>;\n mark: (slot: Described, on: boolean) => void;\n}\n\nconst FormItemContext = createContext<FormItemContextValue | null>(null);\n\n// ── FormField ────────────────────────────────────────────────────────────────\n\n/**\n * react-hook-form's `Controller`, with the field's name put in context for the parts\n * below it. Takes exactly `Controller`'s props.\n */\nexport function FormField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformedValues = TFieldValues,\n>(props: ControllerProps<TFieldValues, TName, TTransformedValues>) {\n const value = useMemo(() => ({ name: props.name as string }), [props.name]);\n return (\n <FormFieldContext.Provider value={value}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n );\n}\n\n// ── useFormField ─────────────────────────────────────────────────────────────\n\n/**\n * The ids and the state of the field around the caller: for a part of your own that\n * sits beside the kit's (a character counter that belongs in `aria-describedby`, a\n * label with a different look).\n *\n * Throws outside a `FormField` + `FormItem` pair. shadcn's version answered there with\n * ids like `undefined-form-item`, which label nothing and fail nowhere.\n */\nexport function useFormField() {\n const field = useContext(FormFieldContext);\n const item = useContext(FormItemContext);\n if (!field) throw new Error(\"useFormField must be used inside <FormField>.\");\n if (!item) throw new Error(\"useFormField must be used inside <FormItem>.\");\n const { getFieldState } = useFormContext();\n // Subscribes this part to the field's state, so a label or message re-renders when\n // the field's error changes without the whole form doing so.\n const formState = useFormState({ name: field.name });\n const state = getFieldState(field.name, formState);\n const formDescriptionId = `${item.id}-description`;\n const formMessageId = `${item.id}-message`;\n const describedBy = [\n item.mounted.description && formDescriptionId,\n item.mounted.message && formMessageId,\n ]\n .filter(Boolean)\n .join(\" \");\n return {\n id: item.id,\n name: field.name,\n formItemId: `${item.id}-control`,\n formDescriptionId,\n formMessageId,\n /** The ids of the description and message that are actually rendered, or\n * `undefined` when neither is. */\n describedBy: describedBy || undefined,\n ...state,\n };\n}\n\n// ── FormItem ─────────────────────────────────────────────────────────────────\n\nexport type FormItemProps = ComponentProps<\"div\">;\n\n/** One field's box: mints the id its label, control, description and message share. */\nexport function FormItem({ className, ...rest }: FormItemProps) {\n const id = useId();\n const [mounted, setMounted] = useState<Record<Described, boolean>>({\n description: false,\n message: false,\n });\n const mark = useCallback(\n (slot: Described, on: boolean) =>\n setMounted((prev) => (prev[slot] === on ? prev : { ...prev, [slot]: on })),\n [],\n );\n const value = useMemo(() => ({ id, mounted, mark }), [id, mounted, mark]);\n return (\n <FormItemContext.Provider value={value}>\n <div data-slot=\"form-item\" {...rest} className={cn(\"grid gap-1\", className)} />\n </FormItemContext.Provider>\n );\n}\n\n/** Report a describing node's presence to the item, so the control points at it only\n * while it is on screen. A layout effect, so the reference lands before paint. */\nfunction useDescribes(slot: Described, on: boolean) {\n const item = useContext(FormItemContext);\n const mark = item?.mark;\n useLayoutEffect(() => {\n if (!on || !mark) return;\n mark(slot, true);\n return () => mark(slot, false);\n }, [slot, on, mark]);\n}\n\n// ── FormLabel ────────────────────────────────────────────────────────────────\n\nexport type FormLabelProps = LabelProps;\n\n/**\n * The kit's {@link Label}, bound to the control by `htmlFor` and turned to the danger\n * colour while the field is invalid. `required` draws the kit's `aria-hidden` mark:\n * whether the field is required is the control's to announce.\n */\nexport function FormLabel({ className, ...rest }: FormLabelProps) {\n const { error, formItemId } = useFormField();\n return (\n <Label\n data-slot=\"form-label\"\n data-error={error ? true : undefined}\n htmlFor={formItemId}\n {...rest}\n className={cn(\"data-[error=true]:text-[var(--danger)]\", className)}\n />\n );\n}\n\n// ── FormControl ──────────────────────────────────────────────────────────────\n\nexport interface FormControlProps {\n /** Exactly one element: the kit field, or a native one. It receives `id`,\n * `aria-describedby` and `aria-invalid`. */\n children: ReactElement;\n}\n\ntype AriaProps = {\n id?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: ComponentProps<\"input\">[\"aria-invalid\"];\n};\n\n/**\n * Wires its one child to the item: the `id` the label points at, `aria-describedby`\n * naming the rendered description and message, and `aria-invalid` while the field has\n * an error.\n *\n * The child's own values win where it has them (an explicit `id`, a set\n * `aria-invalid`), as they did under Radix `Slot`; its own `aria-describedby` is\n * merged in front of the item's rather than replaced.\n */\nexport function FormControl({ children }: FormControlProps) {\n const { invalid, formItemId, describedBy } = useFormField();\n const child = Children.only(children);\n if (!isValidElement<AriaProps>(child)) return child;\n const own = child.props;\n const merged = [own[\"aria-describedby\"], describedBy].filter(Boolean).join(\" \");\n return cloneElement(child, {\n id: own.id ?? formItemId,\n \"aria-describedby\": merged || undefined,\n \"aria-invalid\": own[\"aria-invalid\"] ?? (invalid || undefined),\n });\n}\n\n// ── FormDescription / FormMessage ────────────────────────────────────────────\n\nexport type FormDescriptionProps = ComponentProps<\"p\">;\n\n/** Standing advice under the field (\"at least twelve characters\"). */\nexport function FormDescription({ className, ...rest }: FormDescriptionProps) {\n const { formDescriptionId } = useFormField();\n useDescribes(\"description\", true);\n return (\n <p\n data-slot=\"form-description\"\n {...rest}\n id={formDescriptionId}\n className={cn(\"text-[11px] leading-tight text-[var(--text-muted)]\", className)}\n />\n );\n}\n\nexport type FormMessageProps = ComponentProps<\"p\">;\n\n/**\n * The field's error message, from the form — the kit renders no string of its own\n * here; the schema or the `rules` carry the words. Without an error it renders its\n * `children`, if any, and otherwise nothing.\n *\n * Deliberately NOT `role=\"alert\"`, for the reason the kit's `error` prop gives: the\n * message is read when focus reaches the control, which is where a field's own error\n * is wanted, and an alert would interrupt on every keystroke of a form that\n * re-validates as you type.\n */\nexport function FormMessage({ className, children, ...rest }: FormMessageProps) {\n const { error, formMessageId } = useFormField();\n const body = error ? String(error.message ?? \"\") : children;\n const shown = body !== undefined && body !== null && body !== false && body !== \"\";\n useDescribes(\"message\", shown);\n if (!shown) return null;\n return (\n <p\n data-slot=\"form-message\"\n {...rest}\n id={formMessageId}\n className={cn(\"text-[11px] leading-tight text-[var(--danger)]\", className)}\n >\n {body}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/rhf.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Form, FormControl, FormControlProps, FormDescription, FormDescriptionProps, FormField, FormItem, FormItemProps, FormLabel, FormLabelProps, FormMessage, FormMessageProps, useFormField } from './rhf/form.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import 'react-hook-form';
|
|
4
|
+
import './components/ui.js';
|
package/dist/rhf.js
ADDED
package/dist/rhf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form, FormField, FormItem,\n// FormLabel, FormControl, FormDescription, FormMessage and useFormField.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n"],"mappings":";AAQA,cAAc;","names":[]}
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -290,7 +290,9 @@ function SidebarGroup({
|
|
|
290
290
|
"aria-hidden": true,
|
|
291
291
|
className: cn(
|
|
292
292
|
"size-3.5 transition-transform duration-200 ease-out motion-reduce:transition-none rtl:-scale-x-100",
|
|
293
|
-
|
|
293
|
+
// Under dir="rtl" the chevron is MIRRORED first (scale applies before rotate), so
|
|
294
|
+
// it starts pointing left and a +90° turn would point it UP — hence −90° there.
|
|
295
|
+
open && "rotate-90 rtl:-rotate-90"
|
|
294
296
|
)
|
|
295
297
|
}
|
|
296
298
|
)
|