@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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/components/FieldsBuilder/FieldsBuilder.svelte +101 -150
  3. package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +10 -1
  4. package/dist/components/FieldsBuilder/README.md +82 -3
  5. package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte +624 -0
  6. package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte.d.ts +41 -0
  7. package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte +174 -0
  8. package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte.d.ts +20 -0
  9. package/dist/components/FieldsBuilder/_internal/column-meta.d.ts +27 -0
  10. package/dist/components/FieldsBuilder/_internal/column-meta.js +15 -0
  11. package/dist/components/FieldsBuilder/i18n-sk.js +17 -0
  12. package/dist/components/FieldsBuilder/i18n.d.ts +17 -0
  13. package/dist/components/FieldsBuilder/i18n.js +17 -0
  14. package/dist/components/FieldsBuilder/index.css +20 -0
  15. package/dist/components/FieldsBuilder/index.d.ts +2 -2
  16. package/dist/components/FieldsBuilder/types.d.ts +39 -0
  17. package/dist/components/FieldsBuilder/utils.d.ts +36 -1
  18. package/dist/components/FieldsBuilder/utils.js +180 -55
  19. package/dist/components/Input/FieldTable.svelte +907 -0
  20. package/dist/components/Input/FieldTable.svelte.d.ts +98 -0
  21. package/dist/components/Input/README.md +251 -0
  22. package/dist/components/Input/field-table-i18n-sk.d.ts +21 -0
  23. package/dist/components/Input/field-table-i18n-sk.js +41 -0
  24. package/dist/components/Input/field-table-i18n.d.ts +60 -0
  25. package/dist/components/Input/field-table-i18n.js +66 -0
  26. package/dist/components/Input/field-table-number.d.ts +48 -0
  27. package/dist/components/Input/field-table-number.js +86 -0
  28. package/dist/components/Input/index.css +597 -0
  29. package/dist/components/Input/index.d.ts +4 -0
  30. package/dist/components/Input/index.js +4 -0
  31. package/docs/domains/components.md +24 -18
  32. package/package.json +1 -1
@@ -57,10 +57,102 @@ export function isKeyReserved(key, reservedKeys) {
57
57
  ? !!reservedKeys(key)
58
58
  : reservedKeys.includes(key);
59
59
  }
60
+ /**
61
+ * The palette a column of `entry` may take: its own `columnTypes`, else the
62
+ * component's `types` without every entry that `supportsColumns` itself
63
+ * (columns never nest).
64
+ */
65
+ export function resolveColumnTypes(entry, types) {
66
+ return entry.columnTypes ?? (types ?? []).filter((td) => !td.supportsColumns);
67
+ }
68
+ /**
69
+ * Materialize the `default` of every declared extra that the bag does not
70
+ * hold yet. Returns the same bag when nothing was seeded (so `undefined`
71
+ * stays `undefined` for a type without defaults).
72
+ */
73
+ export function seedExtraDefaults(extras, entry) {
74
+ let next = extras;
75
+ for (const ex of entry?.extras ?? []) {
76
+ if (ex.default !== undefined && next?.[ex.key] === undefined) {
77
+ next = { ...(next ?? {}), [ex.key]: ex.default };
78
+ }
79
+ }
80
+ return next;
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // the single-value rules, shared by the field loop and the column loop so the
84
+ // two cannot drift; each returns the FIRST offending message or undefined
85
+ // ---------------------------------------------------------------------------
86
+ function labelError(label, defaultLanguage, t) {
87
+ return getLocalizedText(label, defaultLanguage).trim()
88
+ ? undefined
89
+ : t("err_label_required");
90
+ }
91
+ function keyError(key, rules, t) {
92
+ // presence is checked trimmed, but pattern/length run on the RAW key —
93
+ // a whitespace-padded key must fail here, not at the consumer's gate
94
+ const k = key ?? "";
95
+ if (!k.trim())
96
+ return t("err_key_required");
97
+ if (k.length > rules.maxLength)
98
+ return t("err_key_maxlength", { max: rules.maxLength });
99
+ if (!rules.pattern.test(k))
100
+ return t("err_key_pattern");
101
+ if (rules.isDuplicate(k.trim()))
102
+ return t(rules.duplicateMessageKey);
103
+ if (rules.isReserved?.(k))
104
+ return t("err_key_reserved");
105
+ return undefined;
106
+ }
107
+ function optionsError(options, t) {
108
+ const list = options ?? [];
109
+ const values = list.map((o) => (o.value ?? "").trim());
110
+ if (!list.length)
111
+ return t("err_options_required");
112
+ if (values.some((v) => !v))
113
+ return t("err_option_value_required");
114
+ if (new Set(values).size !== values.length)
115
+ return t("err_option_value_duplicate");
116
+ return undefined;
117
+ }
118
+ // `maxlength` is re-checked here on purpose: the input's attribute stops
119
+ // typing, it does not bound a seeded or pasted-then-mutated value
120
+ function extrasError(extras, entry, displayLanguages, t) {
121
+ for (const ex of entry?.extras ?? []) {
122
+ if (ex.type !== "string" || !ex.maxlength)
123
+ continue;
124
+ const v = extras?.[ex.key];
125
+ if (typeof v === "string" && v.length > ex.maxlength) {
126
+ return t("err_extra_maxlength", {
127
+ label: getLocalizedText(ex.label, displayLanguages),
128
+ max: ex.maxlength,
129
+ });
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+ function countKeys(items) {
135
+ const counts = new Map();
136
+ for (const it of items) {
137
+ const k = (it.key ?? "").trim();
138
+ if (k)
139
+ counts.set(k, (counts.get(k) ?? 0) + 1);
140
+ }
141
+ return counts;
142
+ }
143
+ /** First string member of an errors object (skips the `columnErrors` array). */
144
+ function firstMessage(errs) {
145
+ if (!errs)
146
+ return "";
147
+ return Object.values(errs).find((v) => typeof v === "string") ?? "";
148
+ }
60
149
  /**
61
150
  * Validate a list of field defs: label non-empty, key present/pattern/length/
62
151
  * unique/not-reserved, choice types have at least one option with non-empty
63
152
  * unique values, string extras within `maxlength`, `maxFields` not exceeded.
153
+ * A `supportsColumns` type additionally needs at least one column, at most
154
+ * `maxColumns`, and every column (of a known column type) passes the label /
155
+ * key (unique within the field) / options / extras rules above.
64
156
  *
65
157
  * This is client-side convenience only — a consumer persisting the list MUST
66
158
  * re-validate server-side; this function is not a security boundary.
@@ -73,76 +165,109 @@ export function validateFieldDefs(defs, opts = {}) {
73
165
  const displayLanguages = [opts.displayLanguage, opts.defaultLanguage].filter((l) => !!l);
74
166
  const rowErrors = defs.map(() => null);
75
167
  const put = (i, field, msg) => {
168
+ if (!msg)
169
+ return;
76
170
  rowErrors[i] ??= {};
77
171
  rowErrors[i][field] ??= msg;
78
172
  };
79
173
  // keys of ALL defs (incl. unknown types) occupy the key space
80
- const keyCounts = new Map();
81
- for (const d of defs) {
82
- const k = (d.key ?? "").trim();
83
- if (k)
84
- keyCounts.set(k, (keyCounts.get(k) ?? 0) + 1);
85
- }
174
+ const keyCounts = countKeys(defs);
86
175
  defs.forEach((d, i) => {
87
176
  // unknown type: keep as-is, do not block (see the doc comment above)
88
177
  if (typeMap && !typeMap.has(d.type))
89
178
  return;
90
- if (!getLocalizedText(d.label, opts.defaultLanguage).trim()) {
91
- put(i, "label", t("err_label_required"));
92
- }
93
- // presence is checked trimmed, but pattern/length run on the RAW key —
94
- // a whitespace-padded key must fail here, not at the consumer's gate
95
- const k = d.key ?? "";
96
- if (!k.trim()) {
97
- put(i, "key", t("err_key_required"));
98
- }
99
- else {
100
- if (k.length > keyMaxLength) {
101
- put(i, "key", t("err_key_maxlength", { max: keyMaxLength }));
102
- }
103
- else if (!keyPattern.test(k)) {
104
- put(i, "key", t("err_key_pattern"));
105
- }
106
- if ((keyCounts.get(k.trim()) ?? 0) > 1)
107
- put(i, "key", t("err_key_duplicate"));
108
- if (isKeyReserved(k, opts.reservedKeys))
109
- put(i, "key", t("err_key_reserved"));
110
- }
111
- if (typeMap?.get(d.type)?.supportsOptions) {
112
- const options = d.options ?? [];
113
- const values = options.map((o) => (o.value ?? "").trim());
114
- if (!options.length) {
115
- put(i, "options", t("err_options_required"));
116
- }
117
- else if (values.some((v) => !v)) {
118
- put(i, "options", t("err_option_value_required"));
119
- }
120
- else if (new Set(values).size !== values.length) {
121
- put(i, "options", t("err_option_value_duplicate"));
122
- }
123
- }
124
- // `maxlength` is re-checked here on purpose: the input's attribute stops
125
- // typing, it does not bound a seeded or pasted-then-mutated value
126
- for (const ex of typeMap?.get(d.type)?.extras ?? []) {
127
- if (ex.type !== "string" || !ex.maxlength)
128
- continue;
129
- const v = d.extras?.[ex.key];
130
- if (typeof v === "string" && v.length > ex.maxlength) {
131
- put(i, "extras", t("err_extra_maxlength", {
132
- label: getLocalizedText(ex.label, displayLanguages),
133
- max: ex.maxlength,
134
- }));
135
- }
179
+ const entry = typeMap?.get(d.type);
180
+ put(i, "label", labelError(d.label, opts.defaultLanguage, t));
181
+ put(i, "key", keyError(d.key, {
182
+ pattern: keyPattern,
183
+ maxLength: keyMaxLength,
184
+ isDuplicate: (k) => (keyCounts.get(k) ?? 0) > 1,
185
+ duplicateMessageKey: "err_key_duplicate",
186
+ isReserved: (k) => isKeyReserved(k, opts.reservedKeys),
187
+ }, t));
188
+ if (entry?.supportsOptions)
189
+ put(i, "options", optionsError(d.options, t));
190
+ if (entry?.supportsColumns) {
191
+ const { summary, columnErrors } = validateColumns(d.columns, entry, {
192
+ columnTypes: resolveColumnTypes(entry, opts.types),
193
+ keyPattern,
194
+ keyMaxLength,
195
+ defaultLanguage: opts.defaultLanguage,
196
+ displayLanguages,
197
+ t,
198
+ });
199
+ put(i, "columns", summary);
200
+ if (columnErrors)
201
+ rowErrors[i].columnErrors = columnErrors;
136
202
  }
203
+ put(i, "extras", extrasError(d.extras, entry, displayLanguages, t));
137
204
  });
138
- let message = rowErrors.find(Boolean)
139
- ? Object.values(rowErrors.find(Boolean))[0]
140
- : "";
205
+ // the summary is the first STRING member of the first erroneous row —
206
+ // `columnErrors` (an array) must never be picked here
207
+ let message = firstMessage(rowErrors.find(Boolean));
141
208
  if (!message && opts.maxFields && defs.length > opts.maxFields) {
142
209
  message = t("err_max_fields", { max: opts.maxFields });
143
210
  }
144
211
  return { valid: !message, message, rowErrors };
145
212
  }
213
+ /**
214
+ * The column rules of one `supportsColumns` def. A column whose type is not
215
+ * in the column palette is NOT validated (it round-trips untouched, like an
216
+ * unknown field type) but its key still counts toward uniqueness. `reservedKeys`
217
+ * does not apply: it guards the field-key namespace, and a column key lives
218
+ * inside one field's rows.
219
+ */
220
+ function validateColumns(columns, entry, opts) {
221
+ const { t } = opts;
222
+ const list = columns ?? [];
223
+ // a `supportsColumns` entry inside the column palette is a plain column
224
+ // type here — nothing nests — so only `type` / `supportsOptions` / `extras` matter
225
+ const colTypeMap = new Map(opts.columnTypes.map((td) => [td.type, td]));
226
+ const keyCounts = countKeys(list);
227
+ let summary;
228
+ if (!list.length)
229
+ summary = t("err_columns_required");
230
+ else if (entry.maxColumns && list.length > entry.maxColumns) {
231
+ summary = t("err_max_columns", { max: entry.maxColumns });
232
+ }
233
+ const columnErrors = list.map((c) => {
234
+ const ct = colTypeMap.get(c.type);
235
+ if (!ct)
236
+ return null;
237
+ const e = {};
238
+ const label = labelError(c.label, opts.defaultLanguage, t);
239
+ if (label)
240
+ e.label = label;
241
+ const key = keyError(c.key, {
242
+ pattern: opts.keyPattern,
243
+ maxLength: opts.keyMaxLength,
244
+ isDuplicate: (k) => (keyCounts.get(k) ?? 0) > 1,
245
+ duplicateMessageKey: "err_column_key_duplicate",
246
+ }, t);
247
+ if (key)
248
+ e.key = key;
249
+ if (ct.supportsOptions) {
250
+ const options = optionsError(c.options, t);
251
+ if (options)
252
+ e.options = options;
253
+ }
254
+ const extras = extrasError(c.extras, ct, opts.displayLanguages, t);
255
+ if (extras)
256
+ e.extras = extras;
257
+ return Object.keys(e).length ? e : null;
258
+ });
259
+ const firstIdx = columnErrors.findIndex(Boolean);
260
+ if (!summary && firstIdx >= 0) {
261
+ summary = t("err_column", {
262
+ position: firstIdx + 1,
263
+ message: firstMessage(columnErrors[firstIdx]),
264
+ });
265
+ }
266
+ return {
267
+ summary,
268
+ columnErrors: firstIdx >= 0 ? columnErrors : undefined,
269
+ };
270
+ }
146
271
  /**
147
272
  * A small general-purpose palette — handy for demos and for consumers with no
148
273
  * opinion. `types` is a required prop, so nobody gets this by accident.