@marianmeres/stuic 2.7.1 → 2.8.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/actions/tooltip/tooltip.svelte.d.ts +1 -1
- package/dist/actions/tooltip/tooltip.svelte.js +4 -0
- package/dist/actions/validate.svelte.d.ts +5 -0
- package/dist/actions/validate.svelte.js +17 -7
- package/dist/components/Input/FieldAssets.svelte +24 -19
- package/dist/components/Input/FieldAssets.svelte.d.ts +1 -0
- package/dist/components/Input/FieldKeyValues.svelte +64 -64
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +1 -1
- package/dist/components/Input/FieldLikeButton.svelte +6 -7
- package/dist/components/Input/README.md +6 -6
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ export declare function isTooltipSupported(): boolean;
|
|
|
20
20
|
/**
|
|
21
21
|
* Valid positions for tooltip placement relative to the anchor element.
|
|
22
22
|
*/
|
|
23
|
-
export type TooltipPosition = "top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right" | "left" | "right";
|
|
23
|
+
export type TooltipPosition = "top" | "top-left" | "top-right" | "top-span-left" | "top-span-right" | "bottom" | "bottom-left" | "bottom-right" | "bottom-span-left" | "bottom-span-right" | "left" | "right";
|
|
24
24
|
/**
|
|
25
25
|
* A Svelte action that displays a tooltip anchored to an element using CSS Anchor Positioning.
|
|
26
26
|
*
|
|
@@ -37,9 +37,13 @@ const POSITION_MAP = {
|
|
|
37
37
|
top: "top",
|
|
38
38
|
"top-left": "top left",
|
|
39
39
|
"top-right": "top right",
|
|
40
|
+
"top-span-left": "top span-left",
|
|
41
|
+
"top-span-right": "top span-right",
|
|
40
42
|
bottom: "bottom",
|
|
41
43
|
"bottom-left": "bottom left",
|
|
42
44
|
"bottom-right": "bottom right",
|
|
45
|
+
"bottom-span-left": "bottom span-left",
|
|
46
|
+
"bottom-span-right": "bottom span-right",
|
|
43
47
|
left: "left",
|
|
44
48
|
right: "right",
|
|
45
49
|
};
|
|
@@ -145,6 +145,11 @@ export interface ValidateOptions {
|
|
|
145
145
|
* ```ts
|
|
146
146
|
* validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
|
|
147
147
|
* ```
|
|
148
|
+
*
|
|
149
|
+
* **Hidden Input Limitation**: Browsers don't populate `el.validationMessage`
|
|
150
|
+
* for hidden inputs (`type="hidden"`) even when `setCustomValidity()` is called.
|
|
151
|
+
* This action works around this by preserving the `customValidator` return value
|
|
152
|
+
* and using it directly as the error message fallback.
|
|
148
153
|
*/
|
|
149
154
|
export declare function validate(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, fn?: () => boolean | ValidateOptions): void;
|
|
150
155
|
export declare namespace validate {
|
|
@@ -127,6 +127,11 @@ const KNOWN_REASONS = [
|
|
|
127
127
|
* ```ts
|
|
128
128
|
* validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
|
|
129
129
|
* ```
|
|
130
|
+
*
|
|
131
|
+
* **Hidden Input Limitation**: Browsers don't populate `el.validationMessage`
|
|
132
|
+
* for hidden inputs (`type="hidden"`) even when `setCustomValidity()` is called.
|
|
133
|
+
* This action works around this by preserving the `customValidator` return value
|
|
134
|
+
* and using it directly as the error message fallback.
|
|
130
135
|
*/
|
|
131
136
|
export function validate(el, fn) {
|
|
132
137
|
$effect(() => {
|
|
@@ -153,8 +158,14 @@ export function validate(el, fn) {
|
|
|
153
158
|
if (!enabled)
|
|
154
159
|
return;
|
|
155
160
|
el.checkValidity();
|
|
161
|
+
// Store customValidator message directly - hidden inputs (type="hidden")
|
|
162
|
+
// don't populate el.validationMessage even when setCustomValidity() is called.
|
|
163
|
+
// This is a browser limitation. We preserve the message here and use it
|
|
164
|
+
// directly as the fallback in ValidationResult.message below.
|
|
165
|
+
let customValidatorMessage = "";
|
|
156
166
|
if (typeof customValidator === "function") {
|
|
157
|
-
|
|
167
|
+
customValidatorMessage = customValidator(el.value, context, el) || "";
|
|
168
|
+
el.setCustomValidity(customValidatorMessage);
|
|
158
169
|
}
|
|
159
170
|
// this triggers the bubble, which is not what we want
|
|
160
171
|
// el.reportValidity();
|
|
@@ -172,12 +183,11 @@ export function validate(el, fn) {
|
|
|
172
183
|
validity: validityState,
|
|
173
184
|
reasons,
|
|
174
185
|
valid: validityState?.valid,
|
|
175
|
-
//
|
|
176
|
-
// otherwise fallback to native msg
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// put only something generic here...
|
|
186
|
+
// Use translate fn for first reason (if fn provided and allowed),
|
|
187
|
+
// otherwise fallback to native msg. We use customValidatorMessage first
|
|
188
|
+
// because hidden inputs don't populate el.validationMessage (see above).
|
|
189
|
+
message: _t(reasons?.[0], el.value, customValidatorMessage ||
|
|
190
|
+
el.validationMessage ||
|
|
181
191
|
"This field is invalid. Please review and try again."),
|
|
182
192
|
});
|
|
183
193
|
// });
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
onProgress?: (blobUrl: string, progress: number) => any
|
|
174
174
|
) => Promise<FieldAssetWithBlobUrl[]>;
|
|
175
175
|
withOnProgress?: boolean;
|
|
176
|
+
classControls?: string;
|
|
176
177
|
}
|
|
177
178
|
</script>
|
|
178
179
|
|
|
@@ -229,6 +230,7 @@
|
|
|
229
230
|
// onChange,
|
|
230
231
|
processAssets,
|
|
231
232
|
withOnProgress,
|
|
233
|
+
classControls = "",
|
|
232
234
|
parseValue = (strigifiedModels: string) => {
|
|
233
235
|
const val = strigifiedModels ?? "[]";
|
|
234
236
|
try {
|
|
@@ -273,13 +275,12 @@
|
|
|
273
275
|
let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
|
|
274
276
|
enabled: true,
|
|
275
277
|
customValidator(value: any, context: Record<string, any> | undefined, el: any) {
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// - nor the hidden input's value
|
|
278
|
+
// Return actual translated messages (not reason names) because hidden inputs
|
|
279
|
+
// don't support el.validationMessage - the validate action preserves our
|
|
280
|
+
// return value and uses it directly as the error message.
|
|
280
281
|
|
|
281
|
-
if (required && !assets.length) return "
|
|
282
|
-
if (assets.length > cardinality) return "
|
|
282
|
+
if (required && !assets.length) return t("field_req_att");
|
|
283
|
+
if (assets.length > cardinality) return t("cardinality_reached", { max: cardinality });
|
|
283
284
|
|
|
284
285
|
// normally, with other fieldtypes, we would continue with provided validator:
|
|
285
286
|
// return (validate as any)?.customValidator?.(value, context, el) || "";
|
|
@@ -289,10 +290,6 @@
|
|
|
289
290
|
}
|
|
290
291
|
return "";
|
|
291
292
|
},
|
|
292
|
-
t(reason: keyof ValidityStateFlags, value: any, fallback: string) {
|
|
293
|
-
// Unfortunately, for hidden, everything is a `customError` reason. So, we must generalize...
|
|
294
|
-
return t("field_req_att");
|
|
295
|
-
},
|
|
296
293
|
setValidationResult,
|
|
297
294
|
});
|
|
298
295
|
|
|
@@ -378,7 +375,7 @@
|
|
|
378
375
|
{@const _is_img = isImage(asset.type ?? thumb)}
|
|
379
376
|
<div class="relative group">
|
|
380
377
|
<button
|
|
381
|
-
class={[objectSize, "bg-black/10 grid place-content-center"]}
|
|
378
|
+
class={[objectSize, "bg-black/10 grid place-content-center", classControls]}
|
|
382
379
|
onclick={(e) => {
|
|
383
380
|
e.stopPropagation();
|
|
384
381
|
e.preventDefault();
|
|
@@ -433,7 +430,7 @@
|
|
|
433
430
|
e.stopPropagation();
|
|
434
431
|
inputEl.click();
|
|
435
432
|
}}
|
|
436
|
-
class={[objectSize, " grid place-content-center group"]}
|
|
433
|
+
class={[objectSize, " grid place-content-center group", classControls]}
|
|
437
434
|
>
|
|
438
435
|
{@html iconAdd({ size: 32, class: "opacity-75 group-hover:opacity-100" })}
|
|
439
436
|
</button>
|
|
@@ -441,7 +438,7 @@
|
|
|
441
438
|
{/snippet}
|
|
442
439
|
|
|
443
440
|
<div
|
|
444
|
-
class="w-full"
|
|
441
|
+
class="w-full stuic-field-assets"
|
|
445
442
|
use:highlightDragover={() => ({
|
|
446
443
|
enabled: typeof processAssets === "function",
|
|
447
444
|
classes: ["outline-dashed outline-2 outline-neutral-300"],
|
|
@@ -559,7 +556,7 @@
|
|
|
559
556
|
classBackdrop="p-4 md:p-4"
|
|
560
557
|
classInner="max-w-full h-full"
|
|
561
558
|
class="max-h-full md:max-h-full"
|
|
562
|
-
classMain="flex items-center justify-center relative "
|
|
559
|
+
classMain="flex items-center justify-center relative stuic-field-assets stuic-field-assets-open"
|
|
563
560
|
>
|
|
564
561
|
{@const previewAsset = assets?.[previewIdx]}
|
|
565
562
|
{#if previewAsset}
|
|
@@ -585,13 +582,21 @@
|
|
|
585
582
|
|
|
586
583
|
{#if assets?.length > 1}
|
|
587
584
|
<div class={["absolute inset-0 flex items-center justify-between"]}>
|
|
588
|
-
<button
|
|
585
|
+
<button
|
|
586
|
+
class={twMerge("p-4", classControls)}
|
|
587
|
+
onclick={preview_previous}
|
|
588
|
+
type="button"
|
|
589
|
+
>
|
|
589
590
|
<span class="bg-white rounded-full p-3 block">
|
|
590
591
|
{@html iconPrevious()}
|
|
591
592
|
</span>
|
|
592
593
|
</button>
|
|
593
594
|
|
|
594
|
-
<button
|
|
595
|
+
<button
|
|
596
|
+
class={twMerge("p-4", classControls)}
|
|
597
|
+
onclick={preview_next}
|
|
598
|
+
type="button"
|
|
599
|
+
>
|
|
595
600
|
<span class="bg-white rounded-full p-3 block">
|
|
596
601
|
{@html iconNext()}
|
|
597
602
|
</span>
|
|
@@ -602,7 +607,7 @@
|
|
|
602
607
|
<!-- bg-white rounded-md p-2 -->
|
|
603
608
|
<div class="absolute top-4 right-4 flex items-center space-x-3">
|
|
604
609
|
<button
|
|
605
|
-
class={TOP_BUTTON_CLS}
|
|
610
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
606
611
|
onclick={(e) => {
|
|
607
612
|
e.preventDefault();
|
|
608
613
|
remove_by_idx(previewIdx);
|
|
@@ -615,7 +620,7 @@
|
|
|
615
620
|
{@html iconDelete({ class: "size-6" })}
|
|
616
621
|
</button>
|
|
617
622
|
<button
|
|
618
|
-
class={TOP_BUTTON_CLS}
|
|
623
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
619
624
|
type="button"
|
|
620
625
|
onclick={(e) => {
|
|
621
626
|
e.preventDefault();
|
|
@@ -627,7 +632,7 @@
|
|
|
627
632
|
{@html iconDownload({ class: "size-6" })}
|
|
628
633
|
</button>
|
|
629
634
|
<button
|
|
630
|
-
class={TOP_BUTTON_CLS}
|
|
635
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
631
636
|
onclick={modal.close}
|
|
632
637
|
aria-label={t("close")}
|
|
633
638
|
type="button"
|
|
@@ -63,6 +63,7 @@ export interface Props extends Record<string, any> {
|
|
|
63
63
|
accept?: string;
|
|
64
64
|
processAssets?: (assets: FieldAsset[], onProgress?: (blobUrl: string, progress: number) => any) => Promise<FieldAssetWithBlobUrl[]>;
|
|
65
65
|
withOnProgress?: boolean;
|
|
66
|
+
classControls?: string;
|
|
66
67
|
}
|
|
67
68
|
declare const FieldAssets: import("svelte").Component<Props, {}, "value">;
|
|
68
69
|
type FieldAssets = ReturnType<typeof FieldAssets>;
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
11
11
|
|
|
12
12
|
export interface KeyValueEntry {
|
|
13
|
-
id: string;
|
|
14
13
|
key: string;
|
|
15
|
-
value: string;
|
|
14
|
+
value: string; // Raw input from user
|
|
15
|
+
parsedValue: unknown; // Parsed JSON value
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface Props extends Record<string, any> {
|
|
@@ -52,8 +52,6 @@
|
|
|
52
52
|
</script>
|
|
53
53
|
|
|
54
54
|
<script lang="ts">
|
|
55
|
-
import { iconFeatherChevronDown } from "@marianmeres/icons-fns/feather/iconFeatherChevronDown.js";
|
|
56
|
-
import { iconFeatherChevronUp } from "@marianmeres/icons-fns/feather/iconFeatherChevronUp.js";
|
|
57
55
|
import { iconFeatherPlus } from "@marianmeres/icons-fns/feather/iconFeatherPlus.js";
|
|
58
56
|
import { iconFeatherTrash2 } from "@marianmeres/icons-fns/feather/iconFeatherTrash2.js";
|
|
59
57
|
import { tick } from "svelte";
|
|
@@ -74,27 +72,29 @@
|
|
|
74
72
|
) {
|
|
75
73
|
const m: Record<string, string> = {
|
|
76
74
|
field_req_att: "This field requires attention. Please review and try again.",
|
|
75
|
+
entry_required: "At least one entry is required",
|
|
77
76
|
key_placeholder: "Key",
|
|
78
77
|
value_placeholder: "Value",
|
|
79
78
|
add_label: "Add",
|
|
80
79
|
empty_message: "No entries",
|
|
81
80
|
remove_entry: "Remove entry",
|
|
82
|
-
|
|
83
|
-
move_down: "Move down",
|
|
81
|
+
duplicate_keys: "Duplicate keys are not allowed",
|
|
84
82
|
};
|
|
85
83
|
let out = m[k] ?? fallback ?? k;
|
|
86
84
|
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
87
85
|
}
|
|
88
86
|
|
|
87
|
+
const SERIALIZED_DEFAULT = "{}";
|
|
88
|
+
|
|
89
89
|
let {
|
|
90
|
-
value = $bindable(
|
|
90
|
+
value = $bindable(),
|
|
91
91
|
name,
|
|
92
92
|
id = getId(),
|
|
93
93
|
label,
|
|
94
94
|
description,
|
|
95
95
|
class: classProp,
|
|
96
96
|
tabindex = 0,
|
|
97
|
-
renderSize = "
|
|
97
|
+
renderSize = "sm",
|
|
98
98
|
required = false,
|
|
99
99
|
disabled = false,
|
|
100
100
|
validate,
|
|
@@ -124,18 +124,32 @@
|
|
|
124
124
|
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
125
125
|
let keyInputRefs: HTMLInputElement[] = $state([]);
|
|
126
126
|
|
|
127
|
-
// Internal state
|
|
128
|
-
let entries: KeyValueEntry[] = $state(parseValue(value));
|
|
127
|
+
// Internal state - handle undefined value from async form initialization
|
|
128
|
+
let entries: KeyValueEntry[] = $state(parseValue(value ?? SERIALIZED_DEFAULT));
|
|
129
|
+
|
|
130
|
+
// Parse JSON value with auto-detect: try JSON first, fallback to plain string
|
|
131
|
+
function parseJsonValue(input: string): { value: unknown } {
|
|
132
|
+
const trimmed = input.trim();
|
|
133
|
+
if (trimmed === "") return { value: "" };
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(trimmed);
|
|
137
|
+
return { value: parsed };
|
|
138
|
+
} catch {
|
|
139
|
+
// If parse fails, treat as plain string
|
|
140
|
+
return { value: trimmed };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
129
143
|
|
|
130
144
|
// Parse external JSON string to internal entries
|
|
131
145
|
function parseValue(jsonString: string): KeyValueEntry[] {
|
|
132
146
|
try {
|
|
133
|
-
const parsed = JSON.parse(jsonString ||
|
|
134
|
-
if (!
|
|
135
|
-
return parsed.map(([key, val]
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
const parsed = JSON.parse(jsonString || SERIALIZED_DEFAULT);
|
|
148
|
+
if (!isPlainObject(parsed)) return [];
|
|
149
|
+
return Object.entries(parsed).map(([key, val]) => ({
|
|
150
|
+
key,
|
|
151
|
+
value: typeof val === "string" ? val : JSON.stringify(val),
|
|
152
|
+
parsedValue: val,
|
|
139
153
|
}));
|
|
140
154
|
} catch (e) {
|
|
141
155
|
return [];
|
|
@@ -144,7 +158,13 @@
|
|
|
144
158
|
|
|
145
159
|
// Serialize internal entries to external JSON string
|
|
146
160
|
function serializeValue(entries: KeyValueEntry[]): string {
|
|
147
|
-
|
|
161
|
+
const obj: Record<string, unknown> = {};
|
|
162
|
+
for (const e of entries) {
|
|
163
|
+
if (e.key) {
|
|
164
|
+
obj[e.key] = e.parsedValue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return JSON.stringify(obj);
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
// Sync internal state to external value
|
|
@@ -158,7 +178,7 @@
|
|
|
158
178
|
|
|
159
179
|
// Sync external value to internal state when it changes externally
|
|
160
180
|
$effect(() => {
|
|
161
|
-
const newEntries = parseValue(value);
|
|
181
|
+
const newEntries = parseValue(value ?? SERIALIZED_DEFAULT);
|
|
162
182
|
const currentSerialized = serializeValue(entries);
|
|
163
183
|
const newSerialized = serializeValue(newEntries);
|
|
164
184
|
if (currentSerialized !== newSerialized) {
|
|
@@ -168,7 +188,7 @@
|
|
|
168
188
|
|
|
169
189
|
// Add new entry
|
|
170
190
|
function addEntry() {
|
|
171
|
-
const newEntry = {
|
|
191
|
+
const newEntry: KeyValueEntry = { key: "", value: "", parsedValue: "" };
|
|
172
192
|
entries = [...entries, newEntry];
|
|
173
193
|
syncToValue();
|
|
174
194
|
// Focus the new key input after render
|
|
@@ -184,20 +204,15 @@
|
|
|
184
204
|
syncToValue();
|
|
185
205
|
}
|
|
186
206
|
|
|
187
|
-
// Move entry up or down
|
|
188
|
-
function moveEntry(idx: number, direction: "up" | "down") {
|
|
189
|
-
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
190
|
-
if (newIdx < 0 || newIdx >= entries.length) return;
|
|
191
|
-
|
|
192
|
-
const newEntries = [...entries];
|
|
193
|
-
[newEntries[idx], newEntries[newIdx]] = [newEntries[newIdx], newEntries[idx]];
|
|
194
|
-
entries = newEntries;
|
|
195
|
-
syncToValue();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
207
|
// Update entry field
|
|
199
208
|
function updateEntry(idx: number, field: "key" | "value", newValue: string) {
|
|
200
|
-
|
|
209
|
+
if (field === "value") {
|
|
210
|
+
const { value: parsed } = parseJsonValue(newValue);
|
|
211
|
+
entries[idx].value = newValue;
|
|
212
|
+
entries[idx].parsedValue = parsed;
|
|
213
|
+
} else {
|
|
214
|
+
entries[idx].key = newValue;
|
|
215
|
+
}
|
|
201
216
|
syncToValue();
|
|
202
217
|
}
|
|
203
218
|
|
|
@@ -210,24 +225,27 @@
|
|
|
210
225
|
customValidator(val: any, context: Record<string, any> | undefined, el: any) {
|
|
211
226
|
// Validate JSON structure
|
|
212
227
|
try {
|
|
213
|
-
const parsed = JSON.parse(val ||
|
|
214
|
-
if (!
|
|
215
|
-
for (const entry of parsed) {
|
|
216
|
-
if (!Array.isArray(entry) || entry.length !== 2) return "typeMismatch";
|
|
217
|
-
}
|
|
228
|
+
const parsed = JSON.parse(val || SERIALIZED_DEFAULT);
|
|
229
|
+
if (!isPlainObject(parsed)) return t("field_req_att");
|
|
218
230
|
} catch (e) {
|
|
219
|
-
return "
|
|
231
|
+
return t("field_req_att");
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
//
|
|
223
|
-
|
|
234
|
+
// Get non-empty keys first (used for both required and duplicate checks)
|
|
235
|
+
const keys = entries.map((e) => e.key).filter((k) => k.trim() !== "");
|
|
236
|
+
|
|
237
|
+
// Required check FIRST - must have at least one entry with non-empty key
|
|
238
|
+
if (required && keys.length === 0) return t("entry_required");
|
|
239
|
+
|
|
240
|
+
// Then check for duplicate keys
|
|
241
|
+
const uniqueKeys = new Set(keys);
|
|
242
|
+
if (keys.length !== uniqueKeys.size) {
|
|
243
|
+
return t("duplicate_keys");
|
|
244
|
+
}
|
|
224
245
|
|
|
225
246
|
// Continue with provided validator
|
|
226
247
|
return (validate as any)?.customValidator?.(val, context, el) || "";
|
|
227
248
|
},
|
|
228
|
-
t(reason: keyof ValidityStateFlags, val: any, fallback: string) {
|
|
229
|
-
return t("field_req_att");
|
|
230
|
-
},
|
|
231
249
|
setValidationResult,
|
|
232
250
|
});
|
|
233
251
|
|
|
@@ -278,7 +296,7 @@
|
|
|
278
296
|
</div>
|
|
279
297
|
{:else}
|
|
280
298
|
<div class="p-2">
|
|
281
|
-
{#each entries as entry, idx (
|
|
299
|
+
{#each entries as entry, idx (idx)}
|
|
282
300
|
<div
|
|
283
301
|
class={twMerge(
|
|
284
302
|
"flex gap-2 items-start py-2",
|
|
@@ -323,26 +341,8 @@
|
|
|
323
341
|
<!-- use:autogrow={() => ({ enabled: true, value: entry.value })} -->
|
|
324
342
|
</div>
|
|
325
343
|
|
|
326
|
-
<!--
|
|
327
|
-
<div class="
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onclick={() => moveEntry(idx, "up")}
|
|
331
|
-
disabled={disabled || idx === 0}
|
|
332
|
-
class={BTN_CLS}
|
|
333
|
-
aria-label={t("move_up")}
|
|
334
|
-
>
|
|
335
|
-
{@html iconFeatherChevronUp({ size: 14 })}
|
|
336
|
-
</button>
|
|
337
|
-
<button
|
|
338
|
-
type="button"
|
|
339
|
-
onclick={() => moveEntry(idx, "down")}
|
|
340
|
-
disabled={disabled || idx === entries.length - 1}
|
|
341
|
-
class={BTN_CLS}
|
|
342
|
-
aria-label={t("move_down")}
|
|
343
|
-
>
|
|
344
|
-
{@html iconFeatherChevronDown({ size: 14 })}
|
|
345
|
-
</button>
|
|
344
|
+
<!-- Delete button -->
|
|
345
|
+
<div class="pt-0.5">
|
|
346
346
|
<button
|
|
347
347
|
type="button"
|
|
348
348
|
onclick={() => removeEntry(idx)}
|
|
@@ -386,7 +386,7 @@
|
|
|
386
386
|
<input
|
|
387
387
|
type="hidden"
|
|
388
388
|
{name}
|
|
389
|
-
{value}
|
|
389
|
+
value={value ?? SERIALIZED_DEFAULT}
|
|
390
390
|
bind:this={hiddenInputEl}
|
|
391
391
|
use:validateAction={() => wrappedValidate}
|
|
392
392
|
/>
|
|
@@ -185,20 +185,19 @@
|
|
|
185
185
|
enabled: !!validate,
|
|
186
186
|
...(typeof validate === "boolean"
|
|
187
187
|
? {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
188
|
+
// Return actual messages (not reason names) because hidden inputs
|
|
189
|
+
// don't support el.validationMessage - the validate action preserves
|
|
190
|
+
// our return value and uses it directly as the error message.
|
|
191
191
|
customValidator(val, ctx, el) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (required && !val) return "valueMissing";
|
|
192
|
+
if (required && !val)
|
|
193
|
+
return "This field requires attention. Please review and try again.";
|
|
195
194
|
|
|
196
195
|
// also, by default, JSON validation is built in
|
|
197
196
|
try {
|
|
198
197
|
JSON.parse(val as string);
|
|
199
198
|
return "";
|
|
200
199
|
} catch (e) {
|
|
201
|
-
return "
|
|
200
|
+
return "This field is invalid. Please review and try again.";
|
|
202
201
|
}
|
|
203
202
|
},
|
|
204
203
|
}
|
|
@@ -179,8 +179,8 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
179
179
|
<script lang="ts">
|
|
180
180
|
import { FieldKeyValues } from 'stuic';
|
|
181
181
|
|
|
182
|
-
// Value is a JSON string of
|
|
183
|
-
let headers = $state('
|
|
182
|
+
// Value is a JSON string of {key: value, key2: value2, ...}
|
|
183
|
+
let headers = $state('{}');
|
|
184
184
|
</script>
|
|
185
185
|
|
|
186
186
|
<FieldKeyValues
|
|
@@ -196,7 +196,7 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
196
196
|
|
|
197
197
|
| Prop | Type | Default | Description |
|
|
198
198
|
|------|------|---------|-------------|
|
|
199
|
-
| `value` | `string` | `"
|
|
199
|
+
| `value` | `string` | `"{}"` | JSON string of `{key: value, ...}` (bindable) |
|
|
200
200
|
| `name` | `string` | - | Form field name |
|
|
201
201
|
| `keyPlaceholder` | `string` | `"Key"` | Placeholder for key input |
|
|
202
202
|
| `valuePlaceholder` | `string` | `"Value"` | Placeholder for value textarea |
|
|
@@ -209,9 +209,9 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
209
209
|
|
|
210
210
|
Features:
|
|
211
211
|
- Add/remove key-value pairs with + and trash buttons
|
|
212
|
-
-
|
|
213
|
-
- Duplicate keys are
|
|
214
|
-
- Value is serialized as
|
|
212
|
+
- Values support any JSON type (auto-detected): plain text → string, `42` → number, `true` → boolean, `{"a":1}` → object
|
|
213
|
+
- Duplicate keys are validated and rejected on form submission
|
|
214
|
+
- Value is serialized as plain object: `{key: value, key2: value2}`
|
|
215
215
|
- Validation at top level only (not individual pairs)
|
|
216
216
|
|
|
217
217
|
## Validation
|