@marianmeres/stuic 3.178.0 → 3.180.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/dist/components/ContactUsForm/ContactUsForm.svelte +78 -15
- package/dist/components/ContactUsForm/ContactUsForm.svelte.d.ts +22 -0
- package/dist/components/ContactUsForm/README.md +49 -0
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte +43 -18
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +15 -1
- package/dist/components/FieldsBuilder/README.md +63 -30
- package/dist/components/FieldsBuilder/_internal/OptionsEditor.svelte +10 -1
- package/dist/components/FieldsBuilder/_internal/OptionsEditor.svelte.d.ts +3 -0
- package/dist/components/FieldsBuilder/utils.d.ts +12 -3
- package/dist/components/FieldsBuilder/utils.js +12 -5
- package/dist/components/Input/README.md +19 -0
- package/dist/components/Input/index.css +25 -0
- package/package.json +2 -2
|
@@ -60,6 +60,29 @@
|
|
|
60
60
|
/** Minimum message length. 0 (default) disables the check. */
|
|
61
61
|
messageMinLength?: number;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Names of fields rendered **read-only**: the value is visible and still
|
|
65
|
+
* reaches `onSubmit`, but the user cannot change it. The typical case is a
|
|
66
|
+
* signed-in visitor whose name/email the server already knows — prefill
|
|
67
|
+
* them through `formData` and list them here.
|
|
68
|
+
*
|
|
69
|
+
* Accepts the built-in names (`"name"`, `"email"`, `"phone"`, `"subject"`,
|
|
70
|
+
* `"company"`, `"message"`) and any `extraFields` name. Unknown names are
|
|
71
|
+
* ignored. Listing a field does NOT show it — pair with the matching
|
|
72
|
+
* `show*` toggle.
|
|
73
|
+
*
|
|
74
|
+
* Read-only is not the same as `disabled`: the value keeps full contrast
|
|
75
|
+
* and stays focusable/selectable/copyable. A read-only Subject rendered
|
|
76
|
+
* from `subjectValues` falls back to a read-only text input, because
|
|
77
|
+
* `<select>` has no read-only counterpart.
|
|
78
|
+
*
|
|
79
|
+
* Prefill whatever you lock: a read-only control is barred from *native*
|
|
80
|
+
* constraint validation, so an empty required one is a dead end for the
|
|
81
|
+
* user. The form still reports it (see `validateContactForm`), it just
|
|
82
|
+
* cannot be fixed in the UI.
|
|
83
|
+
*/
|
|
84
|
+
readonlyFields?: string[];
|
|
85
|
+
|
|
63
86
|
/**
|
|
64
87
|
* Declarative extra fields rendered as FieldInput entries.
|
|
65
88
|
* Values bind into `formData.extra[name]`.
|
|
@@ -153,6 +176,7 @@
|
|
|
153
176
|
showCompany = false,
|
|
154
177
|
requireCompany = false,
|
|
155
178
|
messageMinLength = 0,
|
|
179
|
+
readonlyFields,
|
|
156
180
|
extraFields = [],
|
|
157
181
|
extraFieldsSlot,
|
|
158
182
|
useHoneypot = true,
|
|
@@ -184,12 +208,24 @@
|
|
|
184
208
|
let topFields = $derived(extraFields.filter((f) => f.position === "top"));
|
|
185
209
|
let bottomFields = $derived(extraFields.filter((f) => f.position !== "top"));
|
|
186
210
|
|
|
211
|
+
let readonlySet = $derived(new Set(readonlyFields ?? []));
|
|
212
|
+
function isReadonly(field: string): boolean {
|
|
213
|
+
return readonlySet.has(field);
|
|
214
|
+
}
|
|
215
|
+
|
|
187
216
|
// Subject: render a <select> when subjectValues is non-empty (which also shows
|
|
188
217
|
// the field regardless of showSubject); otherwise a free-text input gated by
|
|
189
218
|
// showSubject. The select gets a prepended blank "prompt" option so the initial
|
|
190
219
|
// empty subject isn't silently auto-selected to the first real value.
|
|
191
|
-
|
|
192
|
-
|
|
220
|
+
//
|
|
221
|
+
// A read-only subject downgrades to a read-only text input — <select> has no
|
|
222
|
+
// readonly counterpart, and `disabled` would grey out the very value we mean
|
|
223
|
+
// to show. Hence `subjectShown` keys off *having* values rather than off
|
|
224
|
+
// rendering a select: sharing one flag would have made `subjectValues` +
|
|
225
|
+
// readonly hide the field outright.
|
|
226
|
+
let subjectHasValues = $derived((subjectValues?.length ?? 0) > 0);
|
|
227
|
+
let subjectAsSelect = $derived(subjectHasValues && !isReadonly("subject"));
|
|
228
|
+
let subjectShown = $derived(showSubject || subjectHasValues);
|
|
193
229
|
let subjectOptions = $derived([
|
|
194
230
|
{ label: t("contact_form.subject_select_prompt"), value: "" },
|
|
195
231
|
...(subjectValues ?? []).map((v) => ({ label: v, value: v })),
|
|
@@ -280,6 +316,21 @@
|
|
|
280
316
|
},
|
|
281
317
|
});
|
|
282
318
|
|
|
319
|
+
// Single source for `validateContactForm`'s options — the submit path and
|
|
320
|
+
// `readonlyMissing()` below must agree on which fields are shown/required.
|
|
321
|
+
let validationOptions = $derived({
|
|
322
|
+
showName,
|
|
323
|
+
requireName,
|
|
324
|
+
showPhone,
|
|
325
|
+
requirePhone,
|
|
326
|
+
showSubject: subjectShown,
|
|
327
|
+
requireSubject,
|
|
328
|
+
showCompany,
|
|
329
|
+
requireCompany,
|
|
330
|
+
messageMinLength,
|
|
331
|
+
extraFields,
|
|
332
|
+
});
|
|
333
|
+
|
|
283
334
|
// Merge internal + external errors; external takes precedence per field.
|
|
284
335
|
let allErrors = $derived.by(() => {
|
|
285
336
|
const map = new Map<string, string>();
|
|
@@ -289,7 +340,22 @@
|
|
|
289
340
|
});
|
|
290
341
|
|
|
291
342
|
function fieldError(field: string): string | undefined {
|
|
292
|
-
return allErrors.find((e) => e.field === field)?.message;
|
|
343
|
+
return allErrors.find((e) => e.field === field)?.message ?? readonlyMissing(field);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// A read-only control is barred from *native* constraint validation:
|
|
347
|
+
// `validity.valueMissing` stays false however empty it is (the spec requires
|
|
348
|
+
// the control to be "mutable"). Without this the field walk behind the
|
|
349
|
+
// exported `validate()` would green-light an empty read-only required field,
|
|
350
|
+
// and the consumer would post it. Run the form's own validator for that one
|
|
351
|
+
// field instead — same rules, same messages, no duplicated logic. The
|
|
352
|
+
// built-in submit path reaches `validateContactForm` anyway; this only
|
|
353
|
+
// closes the pre-submit / imperative window, and only for read-only fields.
|
|
354
|
+
function readonlyMissing(field: string): string | undefined {
|
|
355
|
+
if (!readonlySet.size || !isReadonly(field)) return;
|
|
356
|
+
return validateContactForm(formData, t, validationOptions).find(
|
|
357
|
+
(e) => e.field === field
|
|
358
|
+
)?.message;
|
|
293
359
|
}
|
|
294
360
|
|
|
295
361
|
function extraValue(cfg: ContactFieldConfig): string {
|
|
@@ -321,18 +387,7 @@
|
|
|
321
387
|
}
|
|
322
388
|
|
|
323
389
|
function handleSubmitValid() {
|
|
324
|
-
const validationErrors = validateContactForm(formData, t,
|
|
325
|
-
showName,
|
|
326
|
-
requireName,
|
|
327
|
-
showPhone,
|
|
328
|
-
requirePhone,
|
|
329
|
-
showSubject: subjectShown,
|
|
330
|
-
requireSubject,
|
|
331
|
-
showCompany,
|
|
332
|
-
requireCompany,
|
|
333
|
-
messageMinLength,
|
|
334
|
-
extraFields,
|
|
335
|
-
});
|
|
390
|
+
const validationErrors = validateContactForm(formData, t, validationOptions);
|
|
336
391
|
internalErrors = validationErrors;
|
|
337
392
|
|
|
338
393
|
// Report-only on bot signals: we still submit when field validation passes
|
|
@@ -452,6 +507,7 @@
|
|
|
452
507
|
placeholder={cfg.placeholder}
|
|
453
508
|
autocomplete={cfg.autocomplete}
|
|
454
509
|
required={cfg.required}
|
|
510
|
+
readonly={isReadonly(cfg.name)}
|
|
455
511
|
name={`contact-extra-${cfg.name}`}
|
|
456
512
|
labelLeftBreakpoint={0}
|
|
457
513
|
validate={{
|
|
@@ -481,6 +537,7 @@
|
|
|
481
537
|
placeholder={t("contact_form.name_placeholder")}
|
|
482
538
|
autocomplete="name"
|
|
483
539
|
required={requireName}
|
|
540
|
+
readonly={isReadonly("name")}
|
|
484
541
|
name="contact-name"
|
|
485
542
|
labelLeftBreakpoint={0}
|
|
486
543
|
validate={{
|
|
@@ -501,6 +558,7 @@
|
|
|
501
558
|
placeholder={t("contact_form.email_placeholder")}
|
|
502
559
|
autocomplete="email"
|
|
503
560
|
required
|
|
561
|
+
readonly={isReadonly("email")}
|
|
504
562
|
name="contact-email"
|
|
505
563
|
labelLeftBreakpoint={0}
|
|
506
564
|
validate={{
|
|
@@ -521,6 +579,7 @@
|
|
|
521
579
|
placeholder={t("contact_form.phone_placeholder")}
|
|
522
580
|
autocomplete="tel"
|
|
523
581
|
required={requirePhone}
|
|
582
|
+
readonly={isReadonly("phone")}
|
|
524
583
|
name="contact-phone"
|
|
525
584
|
labelLeftBreakpoint={0}
|
|
526
585
|
validate={{
|
|
@@ -542,6 +601,7 @@
|
|
|
542
601
|
placeholder={t("contact_form.company_placeholder")}
|
|
543
602
|
autocomplete="organization"
|
|
544
603
|
required={requireCompany}
|
|
604
|
+
readonly={isReadonly("company")}
|
|
545
605
|
name="contact-company"
|
|
546
606
|
labelLeftBreakpoint={0}
|
|
547
607
|
validate={{
|
|
@@ -579,6 +639,7 @@
|
|
|
579
639
|
type="text"
|
|
580
640
|
placeholder={t("contact_form.subject_placeholder")}
|
|
581
641
|
required={requireSubject}
|
|
642
|
+
readonly={isReadonly("subject")}
|
|
582
643
|
name="contact-subject"
|
|
583
644
|
labelLeftBreakpoint={0}
|
|
584
645
|
validate={{
|
|
@@ -598,6 +659,7 @@
|
|
|
598
659
|
label={t("contact_form.message_label")}
|
|
599
660
|
placeholder={t("contact_form.message_placeholder")}
|
|
600
661
|
required
|
|
662
|
+
readonly={isReadonly("message")}
|
|
601
663
|
name="contact-message"
|
|
602
664
|
labelLeftBreakpoint={0}
|
|
603
665
|
validate={{
|
|
@@ -619,6 +681,7 @@
|
|
|
619
681
|
placeholder={cfg.placeholder}
|
|
620
682
|
autocomplete={cfg.autocomplete}
|
|
621
683
|
required={cfg.required}
|
|
684
|
+
readonly={isReadonly(cfg.name)}
|
|
622
685
|
name={`contact-extra-${cfg.name}`}
|
|
623
686
|
labelLeftBreakpoint={0}
|
|
624
687
|
validate={{
|
|
@@ -47,6 +47,28 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
47
47
|
requireCompany?: boolean;
|
|
48
48
|
/** Minimum message length. 0 (default) disables the check. */
|
|
49
49
|
messageMinLength?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Names of fields rendered **read-only**: the value is visible and still
|
|
52
|
+
* reaches `onSubmit`, but the user cannot change it. The typical case is a
|
|
53
|
+
* signed-in visitor whose name/email the server already knows — prefill
|
|
54
|
+
* them through `formData` and list them here.
|
|
55
|
+
*
|
|
56
|
+
* Accepts the built-in names (`"name"`, `"email"`, `"phone"`, `"subject"`,
|
|
57
|
+
* `"company"`, `"message"`) and any `extraFields` name. Unknown names are
|
|
58
|
+
* ignored. Listing a field does NOT show it — pair with the matching
|
|
59
|
+
* `show*` toggle.
|
|
60
|
+
*
|
|
61
|
+
* Read-only is not the same as `disabled`: the value keeps full contrast
|
|
62
|
+
* and stays focusable/selectable/copyable. A read-only Subject rendered
|
|
63
|
+
* from `subjectValues` falls back to a read-only text input, because
|
|
64
|
+
* `<select>` has no read-only counterpart.
|
|
65
|
+
*
|
|
66
|
+
* Prefill whatever you lock: a read-only control is barred from *native*
|
|
67
|
+
* constraint validation, so an empty required one is a dead end for the
|
|
68
|
+
* user. The form still reports it (see `validateContactForm`), it just
|
|
69
|
+
* cannot be fixed in the UI.
|
|
70
|
+
*/
|
|
71
|
+
readonlyFields?: string[];
|
|
50
72
|
/**
|
|
51
73
|
* Declarative extra fields rendered as FieldInput entries.
|
|
52
74
|
* Values bind into `formData.extra[name]`.
|
|
@@ -75,6 +75,7 @@ interface ContactFieldConfig {
|
|
|
75
75
|
| `showCompany` | `boolean` | `false` | Render the Company field. |
|
|
76
76
|
| `requireCompany` | `boolean` | `false` | Require Company (only applies when shown). |
|
|
77
77
|
| `messageMinLength` | `number` | `0` | Minimum message length. `0` disables the check. |
|
|
78
|
+
| `readonlyFields` | `string[]` | - | Field names rendered read-only — prefilled, visible, submitted, not editable (see below). |
|
|
78
79
|
| `extraFields` | `ContactFieldConfig[]` | `[]` | Declarative extra fields, positioned top or bottom. |
|
|
79
80
|
| `extraFieldsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Escape hatch for non-FieldInput extras (consent checkbox, captcha widget). |
|
|
80
81
|
| `useHoneypot` | `boolean` | `true` | Render the hidden honeypot trap. |
|
|
@@ -182,6 +183,54 @@ automatically; the bound value is still the chosen string in `formData.subject`)
|
|
|
182
183
|
</ContactUsForm>
|
|
183
184
|
```
|
|
184
185
|
|
|
186
|
+
### Prefilled, read-only fields (signed-in visitor)
|
|
187
|
+
|
|
188
|
+
When the server already knows who is writing, prefill `formData` and list those
|
|
189
|
+
field names in `readonlyFields`. The values stay visible, keep full contrast, are
|
|
190
|
+
selectable/copyable, and still reach `onSubmit` — the user simply cannot change
|
|
191
|
+
them.
|
|
192
|
+
|
|
193
|
+
```svelte
|
|
194
|
+
<script lang="ts">
|
|
195
|
+
import { ContactUsForm, createEmptyContactFormData } from "@marianmeres/stuic";
|
|
196
|
+
|
|
197
|
+
let { user } = $props();
|
|
198
|
+
|
|
199
|
+
let formData = $state({
|
|
200
|
+
...createEmptyContactFormData(),
|
|
201
|
+
name: user.name,
|
|
202
|
+
email: user.email,
|
|
203
|
+
});
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<ContactUsForm
|
|
207
|
+
bind:formData
|
|
208
|
+
onSubmit={send}
|
|
209
|
+
showName
|
|
210
|
+
readonlyFields={["name", "email"]}
|
|
211
|
+
/>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Notes:
|
|
215
|
+
|
|
216
|
+
- `readonlyFields` does **not** show a field — pair it with the matching `show*` toggle.
|
|
217
|
+
- It accepts the built-in names (`name`, `email`, `phone`, `subject`, `company`,
|
|
218
|
+
`message`) and any `extraFields` name. Unknown names are ignored.
|
|
219
|
+
- Read-only is **not** `disabled`. `disabled` reads as "unavailable", greys the
|
|
220
|
+
value out and drops it from the tab order; read-only says "this is your value,
|
|
221
|
+
it just isn't yours to change here".
|
|
222
|
+
- A read-only Subject falls back to a read-only **text input** even when
|
|
223
|
+
`subjectValues` is set, because `<select>` has no read-only counterpart and
|
|
224
|
+
disabling it would grey out the very value you meant to show.
|
|
225
|
+
- **Prefill whatever you lock.** A read-only control is barred from _native_
|
|
226
|
+
constraint validation, so an empty required one is a dead end for the user.
|
|
227
|
+
The form still reports it (`validateContactForm` runs on submit, and
|
|
228
|
+
`validate()` covers the same ground for read-only fields), but nobody can fix
|
|
229
|
+
it from the UI.
|
|
230
|
+
- Read-only is a **UI** affordance, not a security boundary: anything the browser
|
|
231
|
+
holds can be edited from devtools. Re-derive the trusted values server-side
|
|
232
|
+
from the session instead of trusting the posted ones.
|
|
233
|
+
|
|
185
234
|
### Declarative extra field
|
|
186
235
|
|
|
187
236
|
```svelte
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { TranslateFn } from "../../types.js";
|
|
8
8
|
import type { THC } from "../Thc/Thc.svelte";
|
|
9
9
|
import type { InputWrapClassProps } from "../Input/types.js";
|
|
10
|
-
import type { FieldDef, FieldTypeDef } from "./types.js";
|
|
10
|
+
import type { FieldDef, FieldTypeDef, LocalizedText } from "./types.js";
|
|
11
11
|
|
|
12
12
|
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
13
13
|
|
|
@@ -46,8 +46,22 @@
|
|
|
46
46
|
* strings.
|
|
47
47
|
*/
|
|
48
48
|
languages?: string[];
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The canonical (authoring) language. Defaults to `languages[0]`. Drives
|
|
51
|
+
* key derivation, the "label required" rule and which input the label/
|
|
52
|
+
* description/option editors show collapsed. Also the display language
|
|
53
|
+
* unless `displayLanguage` is set.
|
|
54
|
+
*/
|
|
50
55
|
defaultLanguage?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Which translation of consumer-supplied localized data is DISPLAYED —
|
|
58
|
+
* field labels in the row list, announcements and the preview fallback;
|
|
59
|
+
* palette labels/descriptions; extras labels/placeholders/descriptions.
|
|
60
|
+
* Typically the current user's UI locale. Purely presentational: authoring
|
|
61
|
+
* stays on `defaultLanguage` (see above), which is also the fallback for a
|
|
62
|
+
* missing translation. Default: `defaultLanguage`.
|
|
63
|
+
*/
|
|
64
|
+
displayLanguage?: string;
|
|
51
65
|
languageLabels?: Record<string, string>;
|
|
52
66
|
/** Key policy. Default: lowercase snake_case starting with a letter. */
|
|
53
67
|
keyPattern?: RegExp;
|
|
@@ -165,6 +179,7 @@
|
|
|
165
179
|
style,
|
|
166
180
|
languages,
|
|
167
181
|
defaultLanguage,
|
|
182
|
+
displayLanguage,
|
|
168
183
|
languageLabels,
|
|
169
184
|
keyPattern,
|
|
170
185
|
keyMaxLength = DEFAULT_KEY_MAX_LENGTH,
|
|
@@ -244,6 +259,17 @@
|
|
|
244
259
|
let rows: Row[] = $state(fromValue(value ?? []));
|
|
245
260
|
|
|
246
261
|
const _defaultLanguage = $derived(defaultLanguage || languages?.[0]);
|
|
262
|
+
const _displayLanguage = $derived(displayLanguage || _defaultLanguage);
|
|
263
|
+
// display fallback chain: preferred → canonical → first non-empty entry
|
|
264
|
+
const _displayLanguages = $derived(
|
|
265
|
+
[_displayLanguage, _defaultLanguage].filter((l): l is string => !!l)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
/** Read-only rendering of consumer-supplied localized data. */
|
|
269
|
+
function displayText(text: LocalizedText | null | undefined): string {
|
|
270
|
+
return getLocalizedText(text, _displayLanguages);
|
|
271
|
+
}
|
|
272
|
+
|
|
247
273
|
const typeByName = $derived(new Map(types.map((td) => [td.type, td])));
|
|
248
274
|
const visibleRows = $derived(rows.filter((r) => !r.deleted));
|
|
249
275
|
const maxReached = $derived(!!maxFields && visibleRows.length >= maxFields);
|
|
@@ -569,7 +595,7 @@
|
|
|
569
595
|
}
|
|
570
596
|
|
|
571
597
|
function rowLabel(row: Row): string {
|
|
572
|
-
return
|
|
598
|
+
return displayText(row.def.label) || String(t("untitled"));
|
|
573
599
|
}
|
|
574
600
|
|
|
575
601
|
// clear-then-set so REPEATED identical announcements (e.g. pressing "Move
|
|
@@ -597,6 +623,7 @@
|
|
|
597
623
|
reservedKeys,
|
|
598
624
|
maxFields,
|
|
599
625
|
defaultLanguage: _defaultLanguage,
|
|
626
|
+
displayLanguage: _displayLanguage,
|
|
600
627
|
t: tUtils,
|
|
601
628
|
}
|
|
602
629
|
)
|
|
@@ -831,7 +858,7 @@
|
|
|
831
858
|
{:else if entry.icon}
|
|
832
859
|
{@render entry.icon()}
|
|
833
860
|
{/if}
|
|
834
|
-
{
|
|
861
|
+
{displayText(entry.label)}
|
|
835
862
|
</span>
|
|
836
863
|
{/if}
|
|
837
864
|
{#if (showLabelError || showKeyError || showOptionsError || showExtrasError) && !row.deleted}
|
|
@@ -994,7 +1021,7 @@
|
|
|
994
1021
|
>
|
|
995
1022
|
{#each types as td (td.type)}
|
|
996
1023
|
<option value={td.type}>
|
|
997
|
-
{
|
|
1024
|
+
{displayText(td.label)}
|
|
998
1025
|
</option>
|
|
999
1026
|
{/each}
|
|
1000
1027
|
</select>
|
|
@@ -1016,7 +1043,7 @@
|
|
|
1016
1043
|
</div>
|
|
1017
1044
|
{#if entry.description}
|
|
1018
1045
|
<div class="fb-hint text-xs mt-0.5">
|
|
1019
|
-
{
|
|
1046
|
+
{displayText(entry.description)}
|
|
1020
1047
|
</div>
|
|
1021
1048
|
{/if}
|
|
1022
1049
|
{#if typeChanged(row)}
|
|
@@ -1043,6 +1070,7 @@
|
|
|
1043
1070
|
bind:options={row.def.options}
|
|
1044
1071
|
{languages}
|
|
1045
1072
|
defaultLanguage={_defaultLanguage}
|
|
1073
|
+
displayLanguage={_displayLanguage}
|
|
1046
1074
|
{languageLabels}
|
|
1047
1075
|
{disabled}
|
|
1048
1076
|
locked={!!row.def.lock?.options}
|
|
@@ -1077,7 +1105,7 @@
|
|
|
1077
1105
|
{@const exValue = extraText(row, ex.key)}
|
|
1078
1106
|
<div class="fb-extra">
|
|
1079
1107
|
<label class="fb-sub-label" for={exId}>
|
|
1080
|
-
{
|
|
1108
|
+
{displayText(ex.label)}
|
|
1081
1109
|
</label>
|
|
1082
1110
|
{#if ex.type === "string"}
|
|
1083
1111
|
<input
|
|
@@ -1086,10 +1114,7 @@
|
|
|
1086
1114
|
class={twMerge(INPUT_CLS, "fb-extra-input w-full")}
|
|
1087
1115
|
value={exValue}
|
|
1088
1116
|
maxlength={ex.maxlength}
|
|
1089
|
-
placeholder={
|
|
1090
|
-
ex.placeholder,
|
|
1091
|
-
_defaultLanguage
|
|
1092
|
-
) || undefined}
|
|
1117
|
+
placeholder={displayText(ex.placeholder) || undefined}
|
|
1093
1118
|
oninput={(e) =>
|
|
1094
1119
|
onExtraStringInput(
|
|
1095
1120
|
row,
|
|
@@ -1124,11 +1149,11 @@
|
|
|
1124
1149
|
{tabindex}
|
|
1125
1150
|
>
|
|
1126
1151
|
<option value="">
|
|
1127
|
-
{
|
|
1152
|
+
{displayText(ex.placeholder)}
|
|
1128
1153
|
</option>
|
|
1129
1154
|
{#each ex.options as opt (opt.value)}
|
|
1130
1155
|
<option value={opt.value}>
|
|
1131
|
-
{
|
|
1156
|
+
{displayText(opt.label)}
|
|
1132
1157
|
</option>
|
|
1133
1158
|
{/each}
|
|
1134
1159
|
<!-- a stored value outside the declared list stays
|
|
@@ -1141,7 +1166,7 @@
|
|
|
1141
1166
|
{/if}
|
|
1142
1167
|
{#if ex.description}
|
|
1143
1168
|
<div class="fb-hint text-xs mt-0.5">
|
|
1144
|
-
{
|
|
1169
|
+
{displayText(ex.description)}
|
|
1145
1170
|
</div>
|
|
1146
1171
|
{/if}
|
|
1147
1172
|
</div>
|
|
@@ -1158,10 +1183,10 @@
|
|
|
1158
1183
|
{tabindex}
|
|
1159
1184
|
/>
|
|
1160
1185
|
<span class="text-sm">
|
|
1161
|
-
{
|
|
1186
|
+
{displayText(ex.label)}
|
|
1162
1187
|
{#if ex.description}
|
|
1163
1188
|
<span class="fb-hint block text-xs">
|
|
1164
|
-
{
|
|
1189
|
+
{displayText(ex.description)}
|
|
1165
1190
|
</span>
|
|
1166
1191
|
{/if}
|
|
1167
1192
|
</span>
|
|
@@ -1291,14 +1316,14 @@
|
|
|
1291
1316
|
{:else}
|
|
1292
1317
|
<div class="fb-preview-fallback flex items-center gap-1.5 text-sm">
|
|
1293
1318
|
<span>
|
|
1294
|
-
{
|
|
1319
|
+
{displayText(f.label) || t("untitled")}
|
|
1295
1320
|
</span>
|
|
1296
1321
|
{#if f.required}
|
|
1297
1322
|
<span class="fb-row-required" aria-hidden="true">*</span>
|
|
1298
1323
|
{/if}
|
|
1299
1324
|
{#if pentry}
|
|
1300
1325
|
<span class="fb-chip">
|
|
1301
|
-
{
|
|
1326
|
+
{displayText(pentry.label)}
|
|
1302
1327
|
</span>
|
|
1303
1328
|
{/if}
|
|
1304
1329
|
</div>
|
|
@@ -42,8 +42,22 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
42
42
|
* strings.
|
|
43
43
|
*/
|
|
44
44
|
languages?: string[];
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The canonical (authoring) language. Defaults to `languages[0]`. Drives
|
|
47
|
+
* key derivation, the "label required" rule and which input the label/
|
|
48
|
+
* description/option editors show collapsed. Also the display language
|
|
49
|
+
* unless `displayLanguage` is set.
|
|
50
|
+
*/
|
|
46
51
|
defaultLanguage?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Which translation of consumer-supplied localized data is DISPLAYED —
|
|
54
|
+
* field labels in the row list, announcements and the preview fallback;
|
|
55
|
+
* palette labels/descriptions; extras labels/placeholders/descriptions.
|
|
56
|
+
* Typically the current user's UI locale. Purely presentational: authoring
|
|
57
|
+
* stays on `defaultLanguage` (see above), which is also the fallback for a
|
|
58
|
+
* missing translation. Default: `defaultLanguage`.
|
|
59
|
+
*/
|
|
60
|
+
displayLanguage?: string;
|
|
47
61
|
languageLabels?: Record<string, string>;
|
|
48
62
|
/** Key policy. Default: lowercase snake_case starting with a letter. */
|
|
49
63
|
keyPattern?: RegExp;
|
|
@@ -187,35 +187,36 @@ not block validation. It is never silently dropped.
|
|
|
187
187
|
|
|
188
188
|
## Props
|
|
189
189
|
|
|
190
|
-
| Prop | Type | Default
|
|
191
|
-
| ------------------------------------------------------------- | ---------------------------------------------------- |
|
|
192
|
-
| `value` | `FieldDef[]` | required
|
|
193
|
-
| `name` | `string` | required
|
|
194
|
-
| `types` | `FieldTypeDef[]` | required
|
|
195
|
-
| `label` | `Snippet \| THC` | —
|
|
196
|
-
| `description` | `Snippet \| THC` | —
|
|
197
|
-
| `languages` | `string[]` | —
|
|
198
|
-
| `defaultLanguage` | `string` | `languages[0]`
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
190
|
+
| Prop | Type | Default | Description |
|
|
191
|
+
| ------------------------------------------------------------- | ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------- |
|
|
192
|
+
| `value` | `FieldDef[]` | required | Bindable ordered field list |
|
|
193
|
+
| `name` | `string` | required | Hidden-input name (form participation) |
|
|
194
|
+
| `types` | `FieldTypeDef[]` | required | The type palette |
|
|
195
|
+
| `label` | `Snippet \| THC` | — | Field label |
|
|
196
|
+
| `description` | `Snippet \| THC` | — | Help text below |
|
|
197
|
+
| `languages` | `string[]` | — | Enables multi-language label/description/option labels |
|
|
198
|
+
| `defaultLanguage` | `string` | `languages[0]` | Canonical language: key derivation, required-label rule, editors' primary input |
|
|
199
|
+
| `displayLanguage` | `string` | `defaultLanguage` | Translation shown in read-only spots (row list, palette, extras); see i18n |
|
|
200
|
+
| `languageLabels` | `Record<string, string>` | — | Display names for language codes |
|
|
201
|
+
| `keyPattern` | `RegExp` | snake_case | Key validation pattern |
|
|
202
|
+
| `keyMaxLength` | `number` | `63` | Key length limit |
|
|
203
|
+
| `reservedKeys` | `string[] \| (key) => boolean` | — | Keys the user may not use |
|
|
204
|
+
| `keysImmutable` | `boolean` | `true` | Freeze keys loaded from `value` |
|
|
205
|
+
| `deriveKeyFromLabel` | `boolean \| (label) => string` | `true` | Live key derivation (custom slugifier allowed) |
|
|
206
|
+
| `maxFields` | `number` | — | Disables adding beyond the limit |
|
|
207
|
+
| `deleteMode` | `"mark" \| "immediate"` | `"mark"` | Delete UX (see above) |
|
|
208
|
+
| `onBeforeDelete` | `(field) => void \| false \| Promise<void \| false>` | — | Delete veto hook |
|
|
209
|
+
| `onBeforeTypeChange` | `(field, newType) => void \| false \| Promise<...>` | — | Type-change veto hook (pre-existing fields) |
|
|
210
|
+
| `onChange` | `(value: FieldDef[]) => void` | — | Fired after every change |
|
|
211
|
+
| `preview` | `Snippet<[{ fields: FieldDef[] }]>` | — | Preview pane content (see below) |
|
|
212
|
+
| `previewBreakpoint` | `number` | `768` | Component width for side-by-side preview; `0` = below |
|
|
213
|
+
| `required` | `boolean` | `false` | At least one field required |
|
|
214
|
+
| `validate` | `boolean \| ValidateOptions` | `true` | Validate-action options |
|
|
215
|
+
| `renderSize` | `"sm" \| "md" \| "lg"` | `"sm"` | InputWrap size |
|
|
216
|
+
| `addLabel`, `emptyMessage` | `string` | — | Text overrides |
|
|
217
|
+
| `classRow`, `classRowHeader`, `classRowBody`, `classPreview` | `string` | — | Class hooks |
|
|
218
|
+
| `t` | `TranslateFn` | built-in (en) | i18n override for all texts (see below) |
|
|
219
|
+
| `disabled`, `id`, `tabindex`, `style`, `labelLeft*`, `class*` | | | Standard `Field*`/InputWrap pass-throughs |
|
|
219
220
|
|
|
220
221
|
Imperative API (via `bind:this`), same as every `Field*`:
|
|
221
222
|
`validate()`, `clearValidation()`, `getValidation()`, `focus()`, `scrollIntoView()`.
|
|
@@ -281,7 +282,39 @@ messages alone would leave the type select in English. `FIELDS_BUILDER_DEFAULT_T
|
|
|
281
282
|
is the Slovak twin of `FIELDS_BUILDER_DEFAULT_TYPES` (identical `type` values — the
|
|
282
283
|
stored defs are unaffected by which one you pass). A palette entry's `label` /
|
|
283
284
|
`description` also accept a per-language map (`{ en: "Text", sk: "Text" }`), resolved
|
|
284
|
-
against `
|
|
285
|
+
against `displayLanguage`.
|
|
286
|
+
|
|
287
|
+
### Display language vs. default language
|
|
288
|
+
|
|
289
|
+
With `languages` set, two languages play different roles:
|
|
290
|
+
|
|
291
|
+
- **`defaultLanguage`** (default `languages[0]`) is the _canonical_ one: the key is
|
|
292
|
+
derived from its label, the "label required" rule checks its entry, and the
|
|
293
|
+
label/description/option editors open on it (the other translations sit behind the
|
|
294
|
+
toggle).
|
|
295
|
+
- **`displayLanguage`** (default `defaultLanguage`) is what the component _shows_
|
|
296
|
+
wherever it renders localized data read-only: the collapsed row titles and the
|
|
297
|
+
reorder/delete announcements, the type chips and the type picker, the palette
|
|
298
|
+
descriptions, extras labels/placeholders/descriptions, the built-in preview fallback,
|
|
299
|
+
and the extra's name in `err_extra_maxlength`. A missing translation falls back to
|
|
300
|
+
`defaultLanguage`, then to the first non-empty entry.
|
|
301
|
+
|
|
302
|
+
So a Slovak user of a schema whose canonical language is English gets a Slovak list and
|
|
303
|
+
Slovak chrome (`t`), and still edits the canonical English label first:
|
|
304
|
+
|
|
305
|
+
```svelte
|
|
306
|
+
<FieldsBuilder
|
|
307
|
+
bind:value
|
|
308
|
+
name="fields"
|
|
309
|
+
types={FIELDS_BUILDER_DEFAULT_TYPES_SK}
|
|
310
|
+
languages={["en", "sk"]}
|
|
311
|
+
displayLanguage={user.locale}
|
|
312
|
+
t={createFieldsBuilderT(FIELDS_BUILDER_MESSAGES_SK)}
|
|
313
|
+
/>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`getLocalizedText(text, preferred)` — exported — accepts the same fallback chain as an
|
|
317
|
+
array: `getLocalizedText(label, ["sk", "en"])`.
|
|
285
318
|
|
|
286
319
|
## Accessibility
|
|
287
320
|
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
interface Props {
|
|
38
38
|
options?: FieldOptionDef[];
|
|
39
39
|
languages?: string[];
|
|
40
|
+
/** Canonical language: value derivation and the editors' collapsed input. */
|
|
40
41
|
defaultLanguage?: string;
|
|
42
|
+
/** Language the announcements read option labels in. Default: `defaultLanguage`. */
|
|
43
|
+
displayLanguage?: string;
|
|
41
44
|
languageLabels?: Record<string, string>;
|
|
42
45
|
disabled?: boolean;
|
|
43
46
|
/** `lock.options` — render the list read-only. */
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
options = $bindable(),
|
|
53
56
|
languages,
|
|
54
57
|
defaultLanguage,
|
|
58
|
+
displayLanguage,
|
|
55
59
|
languageLabels,
|
|
56
60
|
disabled = false,
|
|
57
61
|
locked = false,
|
|
@@ -61,6 +65,11 @@
|
|
|
61
65
|
}: Props = $props();
|
|
62
66
|
|
|
63
67
|
const _defaultLanguage = $derived(defaultLanguage || languages?.[0]);
|
|
68
|
+
const _displayLanguages = $derived(
|
|
69
|
+
[displayLanguage || _defaultLanguage, _defaultLanguage].filter(
|
|
70
|
+
(l): l is string => !!l
|
|
71
|
+
)
|
|
72
|
+
);
|
|
64
73
|
|
|
65
74
|
interface OptionMeta {
|
|
66
75
|
/** Stable render id (options themselves have no identity). */
|
|
@@ -92,7 +101,7 @@
|
|
|
92
101
|
) {
|
|
93
102
|
liveAnnouncement = "";
|
|
94
103
|
const msg = String(
|
|
95
|
-
t(key, { label: getLocalizedText(option.label,
|
|
104
|
+
t(key, { label: getLocalizedText(option.label, _displayLanguages), ...values })
|
|
96
105
|
);
|
|
97
106
|
tick().then(() => (liveAnnouncement = msg));
|
|
98
107
|
}
|
|
@@ -3,7 +3,10 @@ import type { FieldOptionDef } from "../types.js";
|
|
|
3
3
|
interface Props {
|
|
4
4
|
options?: FieldOptionDef[];
|
|
5
5
|
languages?: string[];
|
|
6
|
+
/** Canonical language: value derivation and the editors' collapsed input. */
|
|
6
7
|
defaultLanguage?: string;
|
|
8
|
+
/** Language the announcements read option labels in. Default: `defaultLanguage`. */
|
|
9
|
+
displayLanguage?: string;
|
|
7
10
|
languageLabels?: Record<string, string>;
|
|
8
11
|
disabled?: boolean;
|
|
9
12
|
/** `lock.options` — render the list read-only. */
|
|
@@ -5,10 +5,12 @@ export declare const DEFAULT_KEY_MAX_LENGTH = 63;
|
|
|
5
5
|
/** Minimal translate signature the pure helpers below need. */
|
|
6
6
|
export type FieldsBuilderTranslate = (key: string, values?: Record<string, string | number>) => string;
|
|
7
7
|
/**
|
|
8
|
-
* Read the display text of a `LocalizedText`: the string itself, the
|
|
9
|
-
* language
|
|
8
|
+
* Read the display text of a `LocalizedText`: the string itself, the entry of
|
|
9
|
+
* the first preferred language (a single one, or a fallback chain in order of
|
|
10
|
+
* preference) that is non-empty, or the first non-empty entry as a last
|
|
11
|
+
* resort.
|
|
10
12
|
*/
|
|
11
|
-
export declare function getLocalizedText(text: LocalizedText | null | undefined, preferredLanguage?: string): string;
|
|
13
|
+
export declare function getLocalizedText(text: LocalizedText | null | undefined, preferredLanguage?: string | string[]): string;
|
|
12
14
|
/**
|
|
13
15
|
* Derive a machine key from a human label: transliterates diacritics
|
|
14
16
|
* (`Ročník` → `rocnik`), lowercases, collapses everything else to `_`. A slug
|
|
@@ -48,7 +50,14 @@ export interface ValidateFieldDefsOptions {
|
|
|
48
50
|
keyMaxLength?: number;
|
|
49
51
|
reservedKeys?: string[] | ((key: string) => boolean);
|
|
50
52
|
maxFields?: number;
|
|
53
|
+
/** The canonical language: the "label required" rule reads this entry. */
|
|
51
54
|
defaultLanguage?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Language the texts interpolated into messages (an extra's `label`) are
|
|
57
|
+
* read in, falling back to `defaultLanguage`. The rules themselves are not
|
|
58
|
+
* affected. Default: `defaultLanguage`.
|
|
59
|
+
*/
|
|
60
|
+
displayLanguage?: string;
|
|
52
61
|
/** Translator for the error messages; defaults to returning the message key. */
|
|
53
62
|
t?: FieldsBuilderTranslate;
|
|
54
63
|
}
|
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
export const DEFAULT_KEY_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
|
|
3
3
|
export const DEFAULT_KEY_MAX_LENGTH = 63;
|
|
4
4
|
/**
|
|
5
|
-
* Read the display text of a `LocalizedText`: the string itself, the
|
|
6
|
-
* language
|
|
5
|
+
* Read the display text of a `LocalizedText`: the string itself, the entry of
|
|
6
|
+
* the first preferred language (a single one, or a fallback chain in order of
|
|
7
|
+
* preference) that is non-empty, or the first non-empty entry as a last
|
|
8
|
+
* resort.
|
|
7
9
|
*/
|
|
8
10
|
export function getLocalizedText(text, preferredLanguage) {
|
|
9
11
|
if (text == null)
|
|
10
12
|
return "";
|
|
11
13
|
if (typeof text === "string")
|
|
12
14
|
return text;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const preferred = typeof preferredLanguage === "string"
|
|
16
|
+
? [preferredLanguage]
|
|
17
|
+
: (preferredLanguage ?? []);
|
|
18
|
+
for (const lang of preferred)
|
|
19
|
+
if (lang && text[lang])
|
|
20
|
+
return text[lang];
|
|
15
21
|
for (const v of Object.values(text))
|
|
16
22
|
if (v)
|
|
17
23
|
return v;
|
|
@@ -75,6 +81,7 @@ export function validateFieldDefs(defs, opts = {}) {
|
|
|
75
81
|
const keyPattern = opts.keyPattern ?? DEFAULT_KEY_PATTERN;
|
|
76
82
|
const keyMaxLength = opts.keyMaxLength ?? DEFAULT_KEY_MAX_LENGTH;
|
|
77
83
|
const typeMap = opts.types ? new Map(opts.types.map((td) => [td.type, td])) : null;
|
|
84
|
+
const displayLanguages = [opts.displayLanguage, opts.defaultLanguage].filter((l) => !!l);
|
|
78
85
|
const rowErrors = defs.map(() => null);
|
|
79
86
|
const put = (i, field, msg) => {
|
|
80
87
|
rowErrors[i] ??= {};
|
|
@@ -133,7 +140,7 @@ export function validateFieldDefs(defs, opts = {}) {
|
|
|
133
140
|
const v = d.extras?.[ex.key];
|
|
134
141
|
if (typeof v === "string" && v.length > ex.maxlength) {
|
|
135
142
|
put(i, "extras", t("err_extra_maxlength", {
|
|
136
|
-
label: getLocalizedText(ex.label,
|
|
143
|
+
label: getLocalizedText(ex.label, displayLanguages),
|
|
137
144
|
max: ex.maxlength,
|
|
138
145
|
}));
|
|
139
146
|
}
|
|
@@ -314,6 +314,25 @@ Override globally in `:root` or locally via `style` prop:
|
|
|
314
314
|
| `--stuic-input-text` | `--stuic-color-foreground` | Text color |
|
|
315
315
|
| `--stuic-input-placeholder` | `--stuic-color-muted-foreground` | Placeholder color |
|
|
316
316
|
|
|
317
|
+
### Readonly Tokens
|
|
318
|
+
|
|
319
|
+
Any `Field*` control that reaches the DOM with a `readonly` attribute (via the
|
|
320
|
+
`readonly` prop passthrough) gets a muted wrapper — a "shown, but not yours to
|
|
321
|
+
change" treatment. Deliberately **not** the `disabled` treatment: `disabled`
|
|
322
|
+
fades the whole wrap to `opacity: 0.5` and leaves the tab order, while readonly
|
|
323
|
+
keeps full text contrast and stays focusable, selectable and copyable. Applies
|
|
324
|
+
to `<input>` and `<textarea>` only — `<select>` has no readonly counterpart.
|
|
325
|
+
|
|
326
|
+
| Variable | Default | Description |
|
|
327
|
+
| ------------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
|
|
328
|
+
| `--stuic-input-bg-readonly` | `--stuic-color-input` mixed 10% toward `--stuic-color-muted-foreground` | Wrapper background when readonly |
|
|
329
|
+
| `--stuic-input-border-readonly` | `--stuic-input-border` mixed 15% toward `--stuic-color-muted-foreground` | Wrapper border when readonly |
|
|
330
|
+
|
|
331
|
+
Both are _mixed off_ the editable values rather than pointing at
|
|
332
|
+
`--stuic-color-muted`: in several themes (stone, for one) `muted` and `input`
|
|
333
|
+
sit ~2% apart, which is no visual cue at all. Mixing toward the muted
|
|
334
|
+
_foreground_ also reverses direction on its own in dark themes.
|
|
335
|
+
|
|
317
336
|
### Size Tokens
|
|
318
337
|
|
|
319
338
|
Each size (sm, md, lg) has corresponding tokens:
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
--stuic-input-text: var(--stuic-color-foreground);
|
|
25
25
|
--stuic-input-placeholder: color-mix(in oklab, var(--stuic-color-muted-foreground) 75%, transparent);
|
|
26
26
|
|
|
27
|
+
/* Readonly: a value shown but not editable. Deliberately NOT the disabled
|
|
28
|
+
treatment (opacity 0.5) — the text must stay fully legible. Mixed off the
|
|
29
|
+
input bg rather than set to --stuic-color-muted: in several themes (stone,
|
|
30
|
+
the demo default) muted and input sit ~2% apart, which is no cue at all.
|
|
31
|
+
Mixing toward the muted *foreground* also flips direction on its own in
|
|
32
|
+
dark themes, so the field reads as "settled" either way. */
|
|
33
|
+
--stuic-input-bg-readonly: color-mix(in oklab, var(--stuic-color-muted-foreground) 10%, var(--stuic-color-input));
|
|
34
|
+
--stuic-input-border-readonly: color-mix(in oklab, var(--stuic-color-muted-foreground) 15%, var(--stuic-input-border));
|
|
35
|
+
|
|
27
36
|
/* Size: sm */
|
|
28
37
|
--stuic-input-padding-x-sm: calc(var(--spacing) * 2.5);
|
|
29
38
|
--stuic-input-padding-y-sm: calc(var(--spacing) * 2);
|
|
@@ -254,6 +263,22 @@
|
|
|
254
263
|
cursor: not-allowed;
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
/* Readonly state — "this value is known, you just can't change it".
|
|
267
|
+
Scoped to the wrap that directly contains the control (not the whole
|
|
268
|
+
.stuic-input) so a nested readonly control can't mute an editable parent.
|
|
269
|
+
Unlike disabled it keeps full text contrast and the focus ring: a readonly
|
|
270
|
+
control is still focusable, and hiding that would cost keyboard users. */
|
|
271
|
+
.stuic-input .input-wrap:has(input[readonly]),
|
|
272
|
+
.stuic-input .input-wrap:has(textarea[readonly]) {
|
|
273
|
+
background: var(--stuic-input-bg-readonly);
|
|
274
|
+
border-color: var(--stuic-input-border-readonly);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.stuic-input input[readonly],
|
|
278
|
+
.stuic-input textarea[readonly] {
|
|
279
|
+
cursor: default;
|
|
280
|
+
}
|
|
281
|
+
|
|
257
282
|
/* Transparent wrapper utility - for components like Switch that don't need wrapper styling */
|
|
258
283
|
.stuic-input .input-wrap.input-wrap-transparent,
|
|
259
284
|
.stuic-input.invalid .input-wrap.input-wrap-transparent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.180.0",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"tailwindcss": "^4.3.3",
|
|
178
178
|
"tsx": "^4.23.13",
|
|
179
179
|
"typescript": "^5.9.3",
|
|
180
|
-
"typescript-eslint": "^8.
|
|
180
|
+
"typescript-eslint": "^8.70.0",
|
|
181
181
|
"vite": "^7.3.6",
|
|
182
182
|
"vitest": "^4.1.11",
|
|
183
183
|
"vitest-browser-svelte": "^2.2.1"
|