@marianmeres/stuic 3.181.0 → 3.182.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.
- package/README.md +1 -1
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte +101 -150
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +10 -1
- package/dist/components/FieldsBuilder/README.md +82 -3
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte +624 -0
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte.d.ts +41 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte +174 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte.d.ts +20 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.d.ts +27 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.js +15 -0
- package/dist/components/FieldsBuilder/i18n-sk.js +17 -0
- package/dist/components/FieldsBuilder/i18n.d.ts +17 -0
- package/dist/components/FieldsBuilder/i18n.js +17 -0
- package/dist/components/FieldsBuilder/index.css +20 -0
- package/dist/components/FieldsBuilder/index.d.ts +2 -2
- package/dist/components/FieldsBuilder/types.d.ts +39 -0
- package/dist/components/FieldsBuilder/utils.d.ts +36 -1
- package/dist/components/FieldsBuilder/utils.js +180 -55
- package/dist/components/Input/FieldTable.svelte +907 -0
- package/dist/components/Input/FieldTable.svelte.d.ts +98 -0
- package/dist/components/Input/README.md +251 -0
- package/dist/components/Input/field-table-i18n-sk.d.ts +21 -0
- package/dist/components/Input/field-table-i18n-sk.js +41 -0
- package/dist/components/Input/field-table-i18n.d.ts +60 -0
- package/dist/components/Input/field-table-i18n.js +66 -0
- package/dist/components/Input/field-table-number.d.ts +48 -0
- package/dist/components/Input/field-table-number.js +86 -0
- package/dist/components/Input/index.css +597 -0
- package/dist/components/Input/index.d.ts +4 -0
- package/dist/components/Input/index.js +4 -0
- package/docs/domains/components.md +24 -18
- package/package.json +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions, ValidationResult } from "../../actions/validate.svelte.js";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { MaybeLocalized } from "../../utils/tr.js";
|
|
5
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
6
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
7
|
+
type SnippetWithId = Snippet<[{
|
|
8
|
+
id: string;
|
|
9
|
+
}]>;
|
|
10
|
+
/** The cell types `FieldTable` renders itself. */
|
|
11
|
+
export type FieldTableCellType = "text" | "number" | "select" | "checkbox" | "date" | "url";
|
|
12
|
+
export interface FieldTableColumn {
|
|
13
|
+
/** The row-object key this cell reads and writes. */
|
|
14
|
+
key: string;
|
|
15
|
+
/** A built-in cell type, or anything else for the `cell` snippet. */
|
|
16
|
+
type: FieldTableCellType | (string & {});
|
|
17
|
+
label: MaybeLocalized;
|
|
18
|
+
/** `select` cells: the choices. */
|
|
19
|
+
options?: {
|
|
20
|
+
value: string;
|
|
21
|
+
label: MaybeLocalized;
|
|
22
|
+
}[];
|
|
23
|
+
/** `number` cells: a display unit — in the header ("Qty (pcs)") and after the input. */
|
|
24
|
+
unit?: string;
|
|
25
|
+
placeholder?: MaybeLocalized;
|
|
26
|
+
/** `text` / `url` cells: the input's `maxlength`, re-checked by validation. */
|
|
27
|
+
maxLength?: number;
|
|
28
|
+
/** Extra per-cell rule, run after the built-in one. Return a message to fail. */
|
|
29
|
+
validate?: (value: unknown, row: Record<string, unknown>) => string | undefined | void;
|
|
30
|
+
}
|
|
31
|
+
/** What the `cell` snippet receives for a column whose `type` is not built in. */
|
|
32
|
+
export interface FieldTableCellContext {
|
|
33
|
+
column: FieldTableColumn;
|
|
34
|
+
row: Record<string, unknown>;
|
|
35
|
+
rowIndex: number;
|
|
36
|
+
value: unknown;
|
|
37
|
+
setValue: (next: unknown) => void;
|
|
38
|
+
/** The id the cell's `<label for>` points at. Put it on your control. */
|
|
39
|
+
id: string;
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
invalid: boolean;
|
|
42
|
+
describedby: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
export type FieldTableRow = Record<string, unknown>;
|
|
45
|
+
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
46
|
+
/** Bindable. The rows — a live array, no string round trip. */
|
|
47
|
+
value: FieldTableRow[];
|
|
48
|
+
/** The hidden input carrying `JSON.stringify(value)`. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** One entry per cell, in order. */
|
|
51
|
+
columns: FieldTableColumn[];
|
|
52
|
+
/** "Add row" is disabled at the cap; a seeded list above it is a validation error. */
|
|
53
|
+
maxRows?: number;
|
|
54
|
+
/** At least one row. */
|
|
55
|
+
required?: boolean;
|
|
56
|
+
/** The `tr()` fallback chain used to resolve column and option labels. */
|
|
57
|
+
displayLanguage?: string | string[];
|
|
58
|
+
/** Decimal separator of number cells (input and display). Default: the browser's. */
|
|
59
|
+
locale?: string;
|
|
60
|
+
/** `auto` switches on the component's OWN width (container query). */
|
|
61
|
+
layout?: "auto" | "table" | "stacked";
|
|
62
|
+
/** `sm` 32rem, `md` 40rem, `lg` 48rem, `xl` 56rem; ignored unless `layout="auto"`. */
|
|
63
|
+
tableFrom?: "sm" | "md" | "lg" | "xl";
|
|
64
|
+
/** Move up / move down buttons per row. Default `true`. */
|
|
65
|
+
reorderable?: boolean;
|
|
66
|
+
/** What "Add row" inserts. Default: one empty value per column. */
|
|
67
|
+
newRow?: () => FieldTableRow;
|
|
68
|
+
/** Renders any column whose `type` is not built in. */
|
|
69
|
+
cell?: Snippet<[FieldTableCellContext]>;
|
|
70
|
+
addLabel?: string;
|
|
71
|
+
emptyMessage?: string;
|
|
72
|
+
/** Fired after every change. */
|
|
73
|
+
onChange?: (value: FieldTableRow[]) => void;
|
|
74
|
+
label?: SnippetWithId | THC;
|
|
75
|
+
description?: SnippetWithId | THC;
|
|
76
|
+
class?: string;
|
|
77
|
+
id?: string;
|
|
78
|
+
tabindex?: number;
|
|
79
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
80
|
+
disabled?: boolean;
|
|
81
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
82
|
+
labelAfter?: SnippetWithId | THC;
|
|
83
|
+
below?: SnippetWithId | THC;
|
|
84
|
+
labelLeft?: boolean;
|
|
85
|
+
labelLeftWidth?: "normal" | "wide";
|
|
86
|
+
labelLeftBreakpoint?: number;
|
|
87
|
+
style?: string;
|
|
88
|
+
t?: TranslateFn;
|
|
89
|
+
}
|
|
90
|
+
declare const FieldTable: import("svelte").Component<Props, {
|
|
91
|
+
validate: () => ValidationResult | undefined;
|
|
92
|
+
clearValidation: () => void;
|
|
93
|
+
getValidation: () => ValidationResult | undefined;
|
|
94
|
+
focus: () => void;
|
|
95
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
96
|
+
}, "value">;
|
|
97
|
+
type FieldTable = ReturnType<typeof FieldTable>;
|
|
98
|
+
export default FieldTable;
|
|
@@ -18,6 +18,7 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
18
18
|
| `FieldFile` | File upload input |
|
|
19
19
|
| `FieldAssets` | Asset/image upload with preview |
|
|
20
20
|
| `FieldKeyValues` | Key-value pairs editor with JSON serialization |
|
|
21
|
+
| `FieldTable` | Rows × typed columns editor (a list of records) — see below |
|
|
21
22
|
| `FieldLikeButton` | Like/favorite toggle button |
|
|
22
23
|
| `Fieldset` | Fieldset with legend |
|
|
23
24
|
| `Honeypot` | Hidden anti-bot trap field (server-less) |
|
|
@@ -886,6 +887,256 @@ field shell adds:
|
|
|
886
887
|
The trigger follows the `--stuic-input-*` size tokens (`renderSize`), the dialog card the
|
|
887
888
|
`--stuic-modal-dialog-bg` / `-text` tokens.
|
|
888
889
|
|
|
890
|
+
## FieldTable
|
|
891
|
+
|
|
892
|
+
A form control whose value is **a list of records with a fixed, typed set of columns** —
|
|
893
|
+
a bill of materials, a price list, opening hours, contacts. Each row is a line of typed
|
|
894
|
+
inputs; the bound `value` is a live array of plain row objects, no string round trip:
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
[
|
|
898
|
+
{ part: "M6 bolt", qty: 12, material: "steel", rohs: true, since: "2026-01-15" },
|
|
899
|
+
{ part: "Washer", qty: 24, material: "brass", rohs: false, since: "" },
|
|
900
|
+
];
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
It follows the hidden-input `Field*` contract (like `FieldKeyValues` and `FieldsBuilder`):
|
|
904
|
+
one `<input type="hidden" name>` carries `JSON.stringify(value)`, every inner control is
|
|
905
|
+
nameless, and `required` plus the per-cell rules are enforced by the component's own
|
|
906
|
+
validator. Columns are declared by data:
|
|
907
|
+
|
|
908
|
+
```svelte
|
|
909
|
+
<script lang="ts">
|
|
910
|
+
import {
|
|
911
|
+
FieldTable,
|
|
912
|
+
type FieldTableColumn,
|
|
913
|
+
type FieldTableRow,
|
|
914
|
+
} from "@marianmeres/stuic";
|
|
915
|
+
|
|
916
|
+
const columns: FieldTableColumn[] = [
|
|
917
|
+
{ key: "part", type: "text", label: "Part", maxLength: 60 },
|
|
918
|
+
{ key: "qty", type: "number", label: "Qty", unit: "pcs" },
|
|
919
|
+
{
|
|
920
|
+
key: "material",
|
|
921
|
+
type: "select",
|
|
922
|
+
label: "Material",
|
|
923
|
+
options: [
|
|
924
|
+
{ value: "steel", label: "Steel" },
|
|
925
|
+
{ value: "brass", label: "Brass" },
|
|
926
|
+
],
|
|
927
|
+
},
|
|
928
|
+
{ key: "rohs", type: "checkbox", label: "RoHS" },
|
|
929
|
+
{ key: "since", type: "date", label: "Since" },
|
|
930
|
+
{ key: "sheet", type: "url", label: "Datasheet" },
|
|
931
|
+
];
|
|
932
|
+
|
|
933
|
+
let bom = $state<FieldTableRow[]>([]);
|
|
934
|
+
</script>
|
|
935
|
+
|
|
936
|
+
<FieldTable
|
|
937
|
+
bind:value={bom}
|
|
938
|
+
name="bom"
|
|
939
|
+
label="Bill of materials"
|
|
940
|
+
{columns}
|
|
941
|
+
maxRows={100}
|
|
942
|
+
/>
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Column shape
|
|
946
|
+
|
|
947
|
+
```ts
|
|
948
|
+
type FieldTableCellType = "text" | "number" | "select" | "checkbox" | "date" | "url";
|
|
949
|
+
|
|
950
|
+
interface FieldTableColumn {
|
|
951
|
+
key: string; // the row-object key this cell reads and writes
|
|
952
|
+
type: FieldTableCellType | (string & {}); // a built-in type, or anything for `cell`
|
|
953
|
+
label: MaybeLocalized; // resolved through `tr()` with `displayLanguage`
|
|
954
|
+
options?: { value: string; label: MaybeLocalized }[]; // `select`
|
|
955
|
+
unit?: string; // `number`: shown in the header ("Qty (pcs)") and after the input
|
|
956
|
+
placeholder?: MaybeLocalized;
|
|
957
|
+
maxLength?: number; // `text` / `url`: the input's maxlength, re-checked by validation
|
|
958
|
+
validate?: (value: unknown, row: Record<string, unknown>) => string | undefined | void;
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### The built-in cells
|
|
963
|
+
|
|
964
|
+
| type | Control | Writes | Blank | A stored value that does not fit |
|
|
965
|
+
| ---------- | ------------------------------------------------ | ------------------------------------------------------ | ------- | ----------------------------------------------------------------------------- |
|
|
966
|
+
| `text` | `<input type="text">` (+ `maxlength`) | string; trimmed on commit (`change`), not while typing | `""` | over `maxLength` → `err_maxlength` |
|
|
967
|
+
| `number` | `<input type="text" inputmode="decimal">` + unit | `number` | `null` | unparseable text is **kept as typed** and flagged `err_number` |
|
|
968
|
+
| `select` | `<select>` with a blank first entry | the option value | `""` | shown as its own entry, round-trips, flagged `err_select_unknown` |
|
|
969
|
+
| `checkbox` | `<input type="checkbox">` | boolean | `false` | checked iff `=== true`; rewritten only when toggled |
|
|
970
|
+
| `date` | `<input type="date">` | `YYYY-MM-DD` | `""` | input shows empty, value untouched until a date is picked, flagged `err_date` |
|
|
971
|
+
| `url` | `<input type="text" inputmode="url">` | string; trimmed on commit | `""` | not an absolute `http:` / `https:` URL → `err_url` |
|
|
972
|
+
|
|
973
|
+
Why no `type="number"` / `type="url"`: a number input defaults to `step=1`, so a decimal
|
|
974
|
+
fails **native** constraint validation and the browser refuses the whole form's submit —
|
|
975
|
+
silently, when the bubble belongs to a control the user cannot see. A url input accepts
|
|
976
|
+
`mailto:` while refusing a bare domain, which matches nobody's rule. Both stay text
|
|
977
|
+
inputs (with the numeric / url keypad) and are validated by the component.
|
|
978
|
+
|
|
979
|
+
**Number parsing is locale-aware and never guesses.** Whitespace (incl. NBSP) is
|
|
980
|
+
stripped, so `1 000` gives `1000`; `.` is always the decimal separator; `,` is one only
|
|
981
|
+
when `locale` uses it (`Intl.NumberFormat(locale)`), so `4,2` under `sk` is `4.2` and
|
|
982
|
+
under `en` an error — never silently read as grouping. Display uses the locale's decimal
|
|
983
|
+
separator without grouping, so a Slovak user sees `4,2`. While a cell is being typed in,
|
|
984
|
+
the input shows exactly the typed text; blur re-formats it. The two helpers,
|
|
985
|
+
`parseCellNumber(raw, locale)` and `formatCellNumber(n, locale)`, are exported.
|
|
986
|
+
|
|
987
|
+
**Unknown types** render through the `cell` snippet, which receives
|
|
988
|
+
`{ column, row, rowIndex, value, setValue, id, disabled, invalid, describedby }` — put
|
|
989
|
+
`id` on your control so the row's `<label for>` reaches it. Without the snippet the cell
|
|
990
|
+
shows the raw value read-only and round-trips it.
|
|
991
|
+
|
|
992
|
+
```svelte
|
|
993
|
+
{#snippet cell(ctx)}
|
|
994
|
+
{#if ctx.column.type === "color"}
|
|
995
|
+
<input
|
|
996
|
+
type="color"
|
|
997
|
+
id={ctx.id}
|
|
998
|
+
value={ctx.value}
|
|
999
|
+
oninput={(e) => ctx.setValue(e.currentTarget.value)}
|
|
1000
|
+
/>
|
|
1001
|
+
{/if}
|
|
1002
|
+
{/snippet}
|
|
1003
|
+
|
|
1004
|
+
<FieldTable bind:value name="items" {columns} {cell} />
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
### Value semantics
|
|
1008
|
+
|
|
1009
|
+
- **Row objects never gain a key the component invented.** Row identity lives in an
|
|
1010
|
+
internal index-aligned list, so a backend with `additionalProperties: false` on the row
|
|
1011
|
+
never sees a stray id.
|
|
1012
|
+
- **Keys not in `columns` are preserved, untouched and unrendered.** The library never
|
|
1013
|
+
silently drops data; a consumer whose backend is closed prunes at its save boundary.
|
|
1014
|
+
- **"Add row"** inserts `newRow?.()`, or else one empty value per column (`""` / `null` /
|
|
1015
|
+
`false` as in the table), and focuses the new row's first cell.
|
|
1016
|
+
- **A loaded row missing a cell** renders that control empty and writes nothing until the
|
|
1017
|
+
cell is edited.
|
|
1018
|
+
- **Change → emit.** `value` is replaced by a JSON copy of the rows, then `onChange`
|
|
1019
|
+
fires, then a bubbling `change` on the hidden input. **External reassignment** is
|
|
1020
|
+
detected by a JSON comparison and rebuilds the rows.
|
|
1021
|
+
- **A non-array `value`** (`""`, `null`, `undefined`) renders as zero rows and is not
|
|
1022
|
+
rewritten on mount — a mount never dirties the host form. The first edit emits an array.
|
|
1023
|
+
- **A non-object entry** renders as a degraded read-only row (`unknown_row_warning`). It
|
|
1024
|
+
can be moved and removed, is never edited, and round-trips.
|
|
1025
|
+
- **Duplicate column keys**: the first occurrence wins, later ones are skipped with a
|
|
1026
|
+
`console.warn`. **An empty column label** falls back to the key.
|
|
1027
|
+
|
|
1028
|
+
### Layout
|
|
1029
|
+
|
|
1030
|
+
One DOM for both modes — a real `<table>` — restyled by CSS. `layout="auto"` (default)
|
|
1031
|
+
switches on the **component's own width** through a container query, not the viewport's:
|
|
1032
|
+
a 480px table inside a side panel of a 1440px window stacks. `tableFrom` picks the
|
|
1033
|
+
threshold on a fixed scale (`sm` 32rem, `md` 40rem (default), `lg` 48rem, `xl` 56rem);
|
|
1034
|
+
`layout="table"` / `"stacked"` are static.
|
|
1035
|
+
|
|
1036
|
+
- **table**: `<th scope="col">` headings (with the unit), one row per record, an actions
|
|
1037
|
+
cell (move up / down, remove). Each cell type has a `min-width` token; the table
|
|
1038
|
+
scrolls horizontally inside its own wrapper instead of squashing inputs.
|
|
1039
|
+
- **stacked**: each row is a card titled "Row N" with the actions on top, each cell a
|
|
1040
|
+
label + control line.
|
|
1041
|
+
|
|
1042
|
+
Every cell carries a real `<label for>` in both modes — "Qty (pcs), row 3" — visually
|
|
1043
|
+
hidden in table mode (the heading is visible there), visible and clickable in stacked
|
|
1044
|
+
mode. Crossing the breakpoint keeps focus and caret, because nothing remounts.
|
|
1045
|
+
|
|
1046
|
+
### Validation and the host `validate()` rule
|
|
1047
|
+
|
|
1048
|
+
The validator runs, in order: `required` with zero rows (`err_rows_required`), `maxRows`
|
|
1049
|
+
exceeded (`err_max_rows` — a seeded list above the cap is an error, never truncated),
|
|
1050
|
+
the first invalid cell in row-major order (`err_cell`: "Row 3, Qty: not a number" —
|
|
1051
|
+
built-in rule, then `column.maxLength`, then `column.validate`), then your
|
|
1052
|
+
`validate.customValidator`. Each invalid cell is `aria-invalid` with its message inline
|
|
1053
|
+
once `validate()` has run or once that cell was blurred; `validate()` scrolls to and
|
|
1054
|
+
focuses the first offender.
|
|
1055
|
+
|
|
1056
|
+
No cell control carries `required`, `pattern`, `min` / `max` or a validating `type`, so
|
|
1057
|
+
a cell scrolled out of view can never make the browser refuse a submit without a
|
|
1058
|
+
message. The flip side — the rule for every hidden-input `Field*` — is that **a host that
|
|
1059
|
+
submits natively must call `validate()`** (or use `use:onSubmitValidityCheck`) before
|
|
1060
|
+
saving.
|
|
1061
|
+
|
|
1062
|
+
### Props
|
|
1063
|
+
|
|
1064
|
+
| Prop | Type | Default | Description |
|
|
1065
|
+
| -------------------------- | ---------------------------------- | ------------------ | ----------------------------------------------------------------- |
|
|
1066
|
+
| `value` | `FieldTableRow[]` | required, bindable | The rows |
|
|
1067
|
+
| `name` | `string` | required | The hidden input's name |
|
|
1068
|
+
| `columns` | `FieldTableColumn[]` | required | One entry per cell, in order |
|
|
1069
|
+
| `maxRows` | `number` | - | Cap; shows an `n / max` counter |
|
|
1070
|
+
| `required` | `boolean` | `false` | At least one row |
|
|
1071
|
+
| `displayLanguage` | `string \| string[]` | - | `tr()` fallback chain for column / option labels |
|
|
1072
|
+
| `locale` | `string` | browser | Decimal separator of number cells |
|
|
1073
|
+
| `layout` | `"auto" \| "table" \| "stacked"` | `"auto"` | See Layout |
|
|
1074
|
+
| `tableFrom` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Container width at which `auto` renders the table |
|
|
1075
|
+
| `reorderable` | `boolean` | `true` | Move up / down buttons |
|
|
1076
|
+
| `newRow` | `() => FieldTableRow` | per-column empties | What "Add row" inserts |
|
|
1077
|
+
| `cell` | `Snippet<[FieldTableCellContext]>` | - | Renders columns whose `type` is not built in |
|
|
1078
|
+
| `addLabel`, `emptyMessage` | `string` | `t(...)` | Text overrides |
|
|
1079
|
+
| `onChange` | `(value: FieldTableRow[]) => void` | - | After every change |
|
|
1080
|
+
| `t` | `TranslateFn` | English | See i18n |
|
|
1081
|
+
| `renderSize` | `"sm" \| "md" \| "lg"` | `"sm"` | Control size (the cells follow the `--stuic-input-*` size tokens) |
|
|
1082
|
+
|
|
1083
|
+
Plus the usual field props: `label`, `description`, `labelAfter`, `below`, `id`,
|
|
1084
|
+
`tabindex`, `disabled`, `validate`, `labelLeft*`, `style`, `class`, and the shared
|
|
1085
|
+
`InputWrapClassProps`. The imperative API is the standard one: `validate()`,
|
|
1086
|
+
`clearValidation()`, `getValidation()`, `focus()` (first cell, or "Add row"),
|
|
1087
|
+
`scrollIntoView()`.
|
|
1088
|
+
|
|
1089
|
+
### i18n
|
|
1090
|
+
|
|
1091
|
+
All UI texts go through `t`. English is built in; Slovak ships bundled and opt-in
|
|
1092
|
+
(importing it is what pulls it into your bundle):
|
|
1093
|
+
|
|
1094
|
+
```svelte
|
|
1095
|
+
<script>
|
|
1096
|
+
import {
|
|
1097
|
+
FieldTable,
|
|
1098
|
+
createFieldTableT,
|
|
1099
|
+
FIELD_TABLE_MESSAGES_SK,
|
|
1100
|
+
} from "@marianmeres/stuic";
|
|
1101
|
+
const t = createFieldTableT(FIELD_TABLE_MESSAGES_SK);
|
|
1102
|
+
</script>
|
|
1103
|
+
|
|
1104
|
+
<FieldTable bind:value name="items" {columns} displayLanguage="sk" locale="sk" {t} />
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
`createFieldTableT(messages, fallbackMessages?)` falls back to `FIELD_TABLE_MESSAGES_EN`
|
|
1108
|
+
for any key the catalog does not define, so a partial catalog is fine and a raw key is
|
|
1109
|
+
never rendered. Column and option labels are consumer data: pass them as
|
|
1110
|
+
`{ en: "...", sk: "..." }` records and set `displayLanguage`.
|
|
1111
|
+
|
|
1112
|
+
### Accessibility
|
|
1113
|
+
|
|
1114
|
+
- Every cell has an accessible name of the form "Column, row N", in both layouts.
|
|
1115
|
+
- The actions are real buttons ("Move row 3 up", "Remove row 3"); a move keeps focus on
|
|
1116
|
+
the moved row's button, a remove focuses the row now at that index (else the previous
|
|
1117
|
+
one, else "Add row"); add, move and remove are announced through a polite live region.
|
|
1118
|
+
- "Enter" in a cell submits the host form, like any `FieldInput` in it — there is no
|
|
1119
|
+
"Enter adds a row".
|
|
1120
|
+
|
|
1121
|
+
### CSS Variables
|
|
1122
|
+
|
|
1123
|
+
Cell controls reuse the `--stuic-input-*` colour and size tokens, so a themed `FieldInput`
|
|
1124
|
+
and a themed cell look alike. The table adds:
|
|
1125
|
+
|
|
1126
|
+
| Variable | Default | Description |
|
|
1127
|
+
| -------------------------------------------------------------------- | -------------------------------- | ----------------------------------------- |
|
|
1128
|
+
| `--stuic-field-table-border-color` | `--stuic-color-border` | Stacked card border |
|
|
1129
|
+
| `--stuic-field-table-header-bg` | `--stuic-color-muted` | `<th>` background |
|
|
1130
|
+
| `--stuic-field-table-header-text` | `--stuic-color-muted-foreground` | `<th>` and stacked cell-label color |
|
|
1131
|
+
| `--stuic-field-table-row-divider-color` | `--stuic-color-border` | Line between rows / under a card's title |
|
|
1132
|
+
| `--stuic-field-table-cell-padding-x` / `-y` | `0.375rem` / `0.25rem` | Cell padding in table mode |
|
|
1133
|
+
| `--stuic-field-table-card-bg` | `transparent` | Stacked card background |
|
|
1134
|
+
| `--stuic-field-table-card-gap` | `0.5rem` | Gap between stacked cards |
|
|
1135
|
+
| `--stuic-field-table-card-radius` | `--stuic-radius` | Stacked card radius (usage-site fallback) |
|
|
1136
|
+
| `--stuic-field-table-invalid-color` | `--stuic-input-accent-error` | Invalid cell border + inline message |
|
|
1137
|
+
| `--stuic-field-table-unit-text` | `--stuic-color-muted-foreground` | The unit suffix |
|
|
1138
|
+
| `--stuic-field-table-col-min-{text,number,select,checkbox,date,url}` | `10 / 6 / 8 / 3 / 9.5 / 12rem` | Minimum cell width per type in table mode |
|
|
1139
|
+
|
|
889
1140
|
## Honeypot & TimeTrap (anti-bot primitives)
|
|
890
1141
|
|
|
891
1142
|
Two small, **client-side, server-less** primitives for cheap spam mitigation. They produce **signals** — they do not block anything. Read the signal, then enforce on your server (the only place enforcement is trustworthy). Both are reusable on any form; [`ContactUsForm`](../ContactUsForm/README.md) composes them by default.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FieldTableMessages } from "./field-table-i18n.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slovak message catalog for `FieldTable`. Opt-in — English stays the built-in
|
|
4
|
+
* default, and this module is only pulled into a bundle when it is actually imported
|
|
5
|
+
* (the component itself never references it).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import {
|
|
11
|
+
* FieldTable,
|
|
12
|
+
* createFieldTableT,
|
|
13
|
+
* FIELD_TABLE_MESSAGES_SK,
|
|
14
|
+
* } from "@marianmeres/stuic";
|
|
15
|
+
* const t = createFieldTableT(FIELD_TABLE_MESSAGES_SK);
|
|
16
|
+
* </script>
|
|
17
|
+
*
|
|
18
|
+
* <FieldTable name="items" bind:value {columns} locale="sk" {t} />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const FIELD_TABLE_MESSAGES_SK: FieldTableMessages;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slovak message catalog for `FieldTable`. Opt-in — English stays the built-in
|
|
3
|
+
* default, and this module is only pulled into a bundle when it is actually imported
|
|
4
|
+
* (the component itself never references it).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script>
|
|
9
|
+
* import {
|
|
10
|
+
* FieldTable,
|
|
11
|
+
* createFieldTableT,
|
|
12
|
+
* FIELD_TABLE_MESSAGES_SK,
|
|
13
|
+
* } from "@marianmeres/stuic";
|
|
14
|
+
* const t = createFieldTableT(FIELD_TABLE_MESSAGES_SK);
|
|
15
|
+
* </script>
|
|
16
|
+
*
|
|
17
|
+
* <FieldTable name="items" bind:value {columns} locale="sk" {t} />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const FIELD_TABLE_MESSAGES_SK = {
|
|
21
|
+
field_req_att: "Toto pole vyžaduje pozornosť. Skontrolujte ho a skúste to znova.",
|
|
22
|
+
add_row: "Pridať riadok",
|
|
23
|
+
empty_message: "Zatiaľ žiadne riadky",
|
|
24
|
+
row_label: "Riadok {{row}}",
|
|
25
|
+
actions_label: "Akcie",
|
|
26
|
+
move_row_up: "Posunúť riadok {{row}} vyššie",
|
|
27
|
+
move_row_down: "Posunúť riadok {{row}} nižšie",
|
|
28
|
+
remove_row: "Odstrániť riadok {{row}}",
|
|
29
|
+
added_row: "Pridaný riadok {{row}}",
|
|
30
|
+
removed_row: "Odstránený riadok {{row}}",
|
|
31
|
+
moved_row: "Riadok presunutý na pozíciu {{position}} z {{total}}",
|
|
32
|
+
unknown_row_warning: "Tento riadok nie je záznam, ktorý vie tento editor zobraziť. Zostáva nezmenený.",
|
|
33
|
+
err_rows_required: "Vyžaduje sa aspoň jeden riadok",
|
|
34
|
+
err_max_rows: "Maximálny počet riadkov je {{max}}",
|
|
35
|
+
err_cell: "Riadok {{row}}, {{column}}: {{message}}",
|
|
36
|
+
err_number: "nie je číslo",
|
|
37
|
+
err_url: "musí byť webová adresa začínajúca na http:// alebo https://",
|
|
38
|
+
err_date: "nie je platný dátum",
|
|
39
|
+
err_select_unknown: "„{{value}}“ nie je medzi možnosťami",
|
|
40
|
+
err_maxlength: "príliš dlhé (max. {{max}} znakov)",
|
|
41
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TranslateFn } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The built-in (English) message catalog of `FieldTable`. Also the fallback of every
|
|
4
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
5
|
+
*
|
|
6
|
+
* Placeholders are mustache-style (`{{row}}`, `{{max}}`, ...).
|
|
7
|
+
*/
|
|
8
|
+
export declare const FIELD_TABLE_MESSAGES_EN: {
|
|
9
|
+
field_req_att: string;
|
|
10
|
+
add_row: string;
|
|
11
|
+
empty_message: string;
|
|
12
|
+
row_label: string;
|
|
13
|
+
actions_label: string;
|
|
14
|
+
move_row_up: string;
|
|
15
|
+
move_row_down: string;
|
|
16
|
+
remove_row: string;
|
|
17
|
+
added_row: string;
|
|
18
|
+
removed_row: string;
|
|
19
|
+
moved_row: string;
|
|
20
|
+
unknown_row_warning: string;
|
|
21
|
+
err_rows_required: string;
|
|
22
|
+
err_max_rows: string;
|
|
23
|
+
err_cell: string;
|
|
24
|
+
err_number: string;
|
|
25
|
+
err_url: string;
|
|
26
|
+
err_date: string;
|
|
27
|
+
err_select_unknown: string;
|
|
28
|
+
err_maxlength: string;
|
|
29
|
+
};
|
|
30
|
+
/** Every message key `FieldTable` may look up. */
|
|
31
|
+
export type FieldTableMessageKey = keyof typeof FIELD_TABLE_MESSAGES_EN;
|
|
32
|
+
/** A (possibly partial) catalog for one locale. */
|
|
33
|
+
export type FieldTableMessages = Record<FieldTableMessageKey, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Builds the `t` prop of `FieldTable` from a message catalog. Unknown or untranslated
|
|
36
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
37
|
+
* partial and never renders a raw key.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```svelte
|
|
41
|
+
* <script>
|
|
42
|
+
* import {
|
|
43
|
+
* FieldTable,
|
|
44
|
+
* createFieldTableT,
|
|
45
|
+
* FIELD_TABLE_MESSAGES_SK,
|
|
46
|
+
* } from "@marianmeres/stuic";
|
|
47
|
+
*
|
|
48
|
+
* // full locale, plus a field specific tweak
|
|
49
|
+
* const t = createFieldTableT({
|
|
50
|
+
* ...FIELD_TABLE_MESSAGES_SK,
|
|
51
|
+
* add_row: "Pridať položku",
|
|
52
|
+
* });
|
|
53
|
+
* </script>
|
|
54
|
+
*
|
|
55
|
+
* <FieldTable name="items" bind:value {columns} {t} />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function createFieldTableT(messages: Partial<FieldTableMessages> | Record<string, string>, fallbackMessages?: Partial<FieldTableMessages> | Record<string, string>): TranslateFn;
|
|
59
|
+
/** The component's built-in English `t`. */
|
|
60
|
+
export declare const t_default: TranslateFn;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
2
|
+
import { replaceMap } from "../../utils/replace-map.js";
|
|
3
|
+
/**
|
|
4
|
+
* The built-in (English) message catalog of `FieldTable`. Also the fallback of every
|
|
5
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
6
|
+
*
|
|
7
|
+
* Placeholders are mustache-style (`{{row}}`, `{{max}}`, ...).
|
|
8
|
+
*/
|
|
9
|
+
export const FIELD_TABLE_MESSAGES_EN = {
|
|
10
|
+
field_req_att: "This field requires attention. Please review and try again.",
|
|
11
|
+
add_row: "Add row",
|
|
12
|
+
empty_message: "No rows yet",
|
|
13
|
+
row_label: "Row {{row}}",
|
|
14
|
+
actions_label: "Actions",
|
|
15
|
+
move_row_up: "Move row {{row}} up",
|
|
16
|
+
move_row_down: "Move row {{row}} down",
|
|
17
|
+
remove_row: "Remove row {{row}}",
|
|
18
|
+
added_row: "Row {{row}} added",
|
|
19
|
+
removed_row: "Row {{row}} removed",
|
|
20
|
+
moved_row: "Row moved to position {{position}} of {{total}}",
|
|
21
|
+
unknown_row_warning: "This row is not a record this editor can show. It is kept unchanged.",
|
|
22
|
+
err_rows_required: "At least one row is required",
|
|
23
|
+
err_max_rows: "Maximum number of rows is {{max}}",
|
|
24
|
+
err_cell: "Row {{row}}, {{column}}: {{message}}",
|
|
25
|
+
err_number: "not a number",
|
|
26
|
+
err_url: "must be a web address starting with http:// or https://",
|
|
27
|
+
err_date: "not a valid date",
|
|
28
|
+
err_select_unknown: "“{{value}}” is not one of the choices",
|
|
29
|
+
err_maxlength: "too long (max {{max}} characters)",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Builds the `t` prop of `FieldTable` from a message catalog. Unknown or untranslated
|
|
33
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
34
|
+
* partial and never renders a raw key.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```svelte
|
|
38
|
+
* <script>
|
|
39
|
+
* import {
|
|
40
|
+
* FieldTable,
|
|
41
|
+
* createFieldTableT,
|
|
42
|
+
* FIELD_TABLE_MESSAGES_SK,
|
|
43
|
+
* } from "@marianmeres/stuic";
|
|
44
|
+
*
|
|
45
|
+
* // full locale, plus a field specific tweak
|
|
46
|
+
* const t = createFieldTableT({
|
|
47
|
+
* ...FIELD_TABLE_MESSAGES_SK,
|
|
48
|
+
* add_row: "Pridať položku",
|
|
49
|
+
* });
|
|
50
|
+
* </script>
|
|
51
|
+
*
|
|
52
|
+
* <FieldTable name="items" bind:value {columns} {t} />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function createFieldTableT(messages, fallbackMessages = FIELD_TABLE_MESSAGES_EN) {
|
|
56
|
+
return (k, values = null, fallback = "") => {
|
|
57
|
+
const out = messages[k] ??
|
|
58
|
+
fallbackMessages[k] ??
|
|
59
|
+
(typeof fallback === "string" ? fallback : k);
|
|
60
|
+
return isPlainObject(values)
|
|
61
|
+
? replaceMap(out, values)
|
|
62
|
+
: out;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The component's built-in English `t`. */
|
|
66
|
+
export const t_default = createFieldTableT(FIELD_TABLE_MESSAGES_EN);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware number parsing and formatting for `FieldTable`'s `number` cells.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions (no DOM), so they can be unit-tested in the fast node project.
|
|
5
|
+
* The rule they implement is "never guess": a typed value is either read as one
|
|
6
|
+
* unambiguous number, or rejected.
|
|
7
|
+
*/
|
|
8
|
+
/** A parse outcome — `null` is "blank" (an empty cell), never an error. */
|
|
9
|
+
export type ParsedCellNumber = {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: number | null;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The decimal separator `Intl` uses for `locale` (`"."` for `en`, `","` for `sk`).
|
|
17
|
+
* `undefined` means the runtime's default locale. An unknown locale falls back to `"."`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decimalSeparator(locale?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Parses what a user typed into a number cell.
|
|
22
|
+
*
|
|
23
|
+
* - Whitespace anywhere (including NBSP / narrow NBSP) is stripped: `"1 000"` → `1000`.
|
|
24
|
+
* - `"."` is always accepted as the decimal separator.
|
|
25
|
+
* - `","` is accepted as the decimal separator **only** when `locale` uses it. Under an
|
|
26
|
+
* English locale `"1,000"` is rejected — it is never silently read as a thousands group.
|
|
27
|
+
* - Two separators (`"1.2.3"`, `"1,2,3"`, `"1,000.5"`) are rejected.
|
|
28
|
+
* - A Unicode minus (`"−5"`, what `Intl` formats in some locales) reads as a minus sign.
|
|
29
|
+
* - Blank is `{ ok: true, value: null }`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* parseCellNumber("4.2", "sk"); // { ok: true, value: 4.2 }
|
|
34
|
+
* parseCellNumber("4,2", "sk"); // { ok: true, value: 4.2 }
|
|
35
|
+
* parseCellNumber("4,2", "en"); // { ok: false }
|
|
36
|
+
* parseCellNumber("1 000", "en"); // { ok: true, value: 1000 }
|
|
37
|
+
* parseCellNumber("", "en"); // { ok: true, value: null }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseCellNumber(raw: string, locale?: string): ParsedCellNumber;
|
|
41
|
+
/**
|
|
42
|
+
* Formats a number for display in a number cell: no grouping, up to 20 fraction
|
|
43
|
+
* digits (so `4.2` is never rounded away), Latin digits, the locale's decimal
|
|
44
|
+
* separator. A Slovak user sees `4,2` for `4.2`; an English one `4.2`.
|
|
45
|
+
*
|
|
46
|
+
* A non-finite input yields `""`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatCellNumber(n: number, locale?: string): string;
|