@marianmeres/stuic 2.8.1 → 2.10.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/Circle/Circle.svelte +5 -2
- package/dist/components/Input/FieldAssets.svelte +72 -63
- package/dist/components/Input/FieldAssets.svelte.d.ts +1 -0
- package/dist/components/Input/FieldKeyValues.svelte +29 -6
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +1 -0
- package/package.json +1 -1
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
|
|
25
25
|
let container: HTMLDivElement = $state()!;
|
|
26
26
|
|
|
27
|
+
// Note: completeness and rotate are NOT included here - they're updated via setters
|
|
28
|
+
// to avoid recreating the SVG on every change
|
|
27
29
|
let circle = $derived(
|
|
28
30
|
svgCircle({
|
|
29
31
|
strokeWidth,
|
|
30
|
-
completeness,
|
|
31
32
|
bgStrokeColor,
|
|
32
33
|
roundedEdges,
|
|
33
|
-
rotate,
|
|
34
34
|
strokeWidthRatio,
|
|
35
35
|
class: circleClass,
|
|
36
36
|
circleStyle:
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
$effect(() => {
|
|
45
45
|
container.appendChild(circle.svg);
|
|
46
|
+
return () => {
|
|
47
|
+
circle.svg.remove();
|
|
48
|
+
};
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
$effect(() => {
|
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
) => Promise<FieldAssetWithBlobUrl[]>;
|
|
175
175
|
withOnProgress?: boolean;
|
|
176
176
|
classControls?: string;
|
|
177
|
+
isLoading?: boolean;
|
|
177
178
|
}
|
|
178
179
|
</script>
|
|
179
180
|
|
|
@@ -231,6 +232,7 @@
|
|
|
231
232
|
processAssets,
|
|
232
233
|
withOnProgress,
|
|
233
234
|
classControls = "",
|
|
235
|
+
isLoading = false,
|
|
234
236
|
parseValue = (strigifiedModels: string) => {
|
|
235
237
|
const val = strigifiedModels ?? "[]";
|
|
236
238
|
try {
|
|
@@ -280,7 +282,8 @@
|
|
|
280
282
|
// return value and uses it directly as the error message.
|
|
281
283
|
|
|
282
284
|
if (required && !assets.length) return t("field_req_att");
|
|
283
|
-
if (assets.length > cardinality)
|
|
285
|
+
if (assets.length > cardinality)
|
|
286
|
+
return t("cardinality_reached", { max: cardinality });
|
|
284
287
|
|
|
285
288
|
// normally, with other fieldtypes, we would continue with provided validator:
|
|
286
289
|
// return (validate as any)?.customValidator?.(value, context, el) || "";
|
|
@@ -369,72 +372,78 @@
|
|
|
369
372
|
/>
|
|
370
373
|
|
|
371
374
|
{#snippet default_render()}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
class={[objectSize, objectFitStyle, "hover:saturate-150"]}
|
|
392
|
-
/>
|
|
393
|
-
{:else}
|
|
394
|
-
{@html getAssetIcon((asset.name ?? "").split(".").at(-1))({
|
|
395
|
-
size: 32,
|
|
396
|
-
class: "mx-auto",
|
|
397
|
-
})}
|
|
398
|
-
{/if}
|
|
399
|
-
<span
|
|
400
|
-
class="absolute bottom-1 left-1 right-1 grid bg-white/50 rounded"
|
|
401
|
-
use:tooltip={() => ({ content: asset.name })}
|
|
375
|
+
{#if isLoading}
|
|
376
|
+
<div class="p-2 pl-8 flex items-center justify-center min-h-24">
|
|
377
|
+
<SpinnerCircle />
|
|
378
|
+
</div>
|
|
379
|
+
{:else}
|
|
380
|
+
<div class={["p-2 flex items-center gap-0.5 flex-wrap"]}>
|
|
381
|
+
{#each assets as asset, idx}
|
|
382
|
+
{@const { thumb, full, original } = asset_urls(asset)}
|
|
383
|
+
{@const _is_img = isImage(asset.type ?? thumb)}
|
|
384
|
+
<div class="relative group">
|
|
385
|
+
<button
|
|
386
|
+
class={[objectSize, "bg-black/10 grid place-content-center", classControls]}
|
|
387
|
+
onclick={(e) => {
|
|
388
|
+
e.stopPropagation();
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
previewIdx = idx;
|
|
391
|
+
modal.open();
|
|
392
|
+
}}
|
|
393
|
+
type="button"
|
|
402
394
|
>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
395
|
+
{#if _is_img}
|
|
396
|
+
<img
|
|
397
|
+
src={thumb}
|
|
398
|
+
alt={asset.name}
|
|
399
|
+
class={[objectSize, objectFitStyle, "hover:saturate-150"]}
|
|
400
|
+
/>
|
|
401
|
+
{:else}
|
|
402
|
+
{@html getAssetIcon((asset.name ?? "").split(".").at(-1))({
|
|
403
|
+
size: 32,
|
|
404
|
+
class: "mx-auto",
|
|
405
|
+
})}
|
|
406
|
+
{/if}
|
|
407
407
|
<span
|
|
408
|
-
class="absolute
|
|
408
|
+
class="absolute bottom-1 left-1 right-1 grid bg-white/50 rounded"
|
|
409
|
+
use:tooltip={() => ({ content: asset.name })}
|
|
409
410
|
>
|
|
410
|
-
{
|
|
411
|
-
<Circle
|
|
412
|
-
class="text-white"
|
|
413
|
-
animateCompletenessMs={300}
|
|
414
|
-
bgStrokeColor="rgba(0 0 0 / 0.2)"
|
|
415
|
-
completeness={progress[asset.id] / 100}
|
|
416
|
-
rotate={-90}
|
|
417
|
-
/>
|
|
418
|
-
{:else}
|
|
419
|
-
<SpinnerCircle bgStrokeColor="gray" />
|
|
420
|
-
{/if}
|
|
411
|
+
<span class="truncate px-2 text-xs">{asset.name}</span>
|
|
421
412
|
</span>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
413
|
+
|
|
414
|
+
{#if asset.meta?.isUploading}
|
|
415
|
+
<span
|
|
416
|
+
class="absolute inset-0 grid place-content-center pointer-events-none text-white"
|
|
417
|
+
>
|
|
418
|
+
{#if withOnProgress}
|
|
419
|
+
<Circle
|
|
420
|
+
class="text-white"
|
|
421
|
+
animateCompletenessMs={300}
|
|
422
|
+
bgStrokeColor="rgba(0 0 0 / 0.2)"
|
|
423
|
+
completeness={progress[asset.id] / 100}
|
|
424
|
+
rotate={-90}
|
|
425
|
+
/>
|
|
426
|
+
{:else}
|
|
427
|
+
<SpinnerCircle bgStrokeColor="gray" />
|
|
428
|
+
{/if}
|
|
429
|
+
</span>
|
|
430
|
+
{/if}
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
{/each}
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
onclick={(e) => {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
e.stopPropagation();
|
|
439
|
+
inputEl.click();
|
|
440
|
+
}}
|
|
441
|
+
class={[objectSize, " grid place-content-center group", classControls]}
|
|
442
|
+
>
|
|
443
|
+
{@html iconAdd({ size: 32, class: "opacity-75 group-hover:opacity-100" })}
|
|
444
|
+
</button>
|
|
445
|
+
</div>
|
|
446
|
+
{/if}
|
|
438
447
|
{/snippet}
|
|
439
448
|
|
|
440
449
|
<div
|
|
@@ -64,6 +64,7 @@ export interface Props extends Record<string, any> {
|
|
|
64
64
|
processAssets?: (assets: FieldAsset[], onProgress?: (blobUrl: string, progress: number) => any) => Promise<FieldAssetWithBlobUrl[]>;
|
|
65
65
|
withOnProgress?: boolean;
|
|
66
66
|
classControls?: string;
|
|
67
|
+
isLoading?: boolean;
|
|
67
68
|
}
|
|
68
69
|
declare const FieldAssets: import("svelte").Component<Props, {}, "value">;
|
|
69
70
|
type FieldAssets = ReturnType<typeof FieldAssets>;
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
addLabel?: string;
|
|
48
48
|
emptyMessage?: string;
|
|
49
49
|
onChange?: (value: string) => void;
|
|
50
|
+
strictJsonValidation?: boolean;
|
|
50
51
|
t?: TranslateFn;
|
|
51
52
|
}
|
|
52
53
|
</script>
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
empty_message: "No entries",
|
|
80
81
|
remove_entry: "Remove entry",
|
|
81
82
|
duplicate_keys: "Duplicate keys are not allowed",
|
|
83
|
+
invalid_json_syntax: "Invalid JSON syntax. Check for missing quotes or brackets.",
|
|
82
84
|
};
|
|
83
85
|
let out = m[k] ?? fallback ?? k;
|
|
84
86
|
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
@@ -118,26 +120,37 @@
|
|
|
118
120
|
addLabel,
|
|
119
121
|
emptyMessage,
|
|
120
122
|
onChange,
|
|
123
|
+
strictJsonValidation = true,
|
|
121
124
|
t = t_default,
|
|
122
125
|
}: Props = $props();
|
|
123
126
|
|
|
124
127
|
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
125
128
|
let keyInputRefs: HTMLInputElement[] = $state([]);
|
|
129
|
+
let entryJsonErrors: boolean[] = $state([]);
|
|
126
130
|
|
|
127
131
|
// Internal state - handle undefined value from async form initialization
|
|
128
132
|
let entries: KeyValueEntry[] = $state(parseValue(value ?? SERIALIZED_DEFAULT));
|
|
129
133
|
|
|
130
134
|
// Parse JSON value with auto-detect: try JSON first, fallback to plain string
|
|
131
|
-
|
|
135
|
+
// Returns intendedJson=true if input looks like JSON (starts with { or [)
|
|
136
|
+
// Returns parseError=true if JSON parse failed
|
|
137
|
+
function parseJsonValue(input: string): {
|
|
138
|
+
value: unknown;
|
|
139
|
+
intendedJson: boolean;
|
|
140
|
+
parseError: boolean;
|
|
141
|
+
} {
|
|
132
142
|
const trimmed = input.trim();
|
|
133
|
-
if (trimmed === "") return { value: "" };
|
|
143
|
+
if (trimmed === "") return { value: "", intendedJson: false, parseError: false };
|
|
144
|
+
|
|
145
|
+
// Heuristics: input looks like it's trying to be JSON
|
|
146
|
+
const intendedJson = trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
134
147
|
|
|
135
148
|
try {
|
|
136
149
|
const parsed = JSON.parse(trimmed);
|
|
137
|
-
return { value: parsed };
|
|
150
|
+
return { value: parsed, intendedJson, parseError: false };
|
|
138
151
|
} catch {
|
|
139
|
-
// If parse fails, treat as plain string
|
|
140
|
-
return { value: trimmed };
|
|
152
|
+
// If parse fails, treat as plain string but flag the error
|
|
153
|
+
return { value: trimmed, intendedJson, parseError: intendedJson };
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
|
|
@@ -183,6 +196,8 @@
|
|
|
183
196
|
const newSerialized = serializeValue(newEntries);
|
|
184
197
|
if (currentSerialized !== newSerialized) {
|
|
185
198
|
entries = newEntries;
|
|
199
|
+
// Reset JSON errors when external value changes (assume valid JSON from external source)
|
|
200
|
+
entryJsonErrors = new Array(newEntries.length).fill(false);
|
|
186
201
|
}
|
|
187
202
|
});
|
|
188
203
|
|
|
@@ -190,6 +205,7 @@
|
|
|
190
205
|
function addEntry() {
|
|
191
206
|
const newEntry: KeyValueEntry = { key: "", value: "", parsedValue: "" };
|
|
192
207
|
entries = [...entries, newEntry];
|
|
208
|
+
entryJsonErrors = [...entryJsonErrors, false];
|
|
193
209
|
syncToValue();
|
|
194
210
|
// Focus the new key input after render
|
|
195
211
|
tick().then(() => {
|
|
@@ -201,15 +217,17 @@
|
|
|
201
217
|
// Remove entry by index
|
|
202
218
|
function removeEntry(idx: number) {
|
|
203
219
|
entries = entries.filter((_, i) => i !== idx);
|
|
220
|
+
entryJsonErrors = entryJsonErrors.filter((_, i) => i !== idx);
|
|
204
221
|
syncToValue();
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
// Update entry field
|
|
208
225
|
function updateEntry(idx: number, field: "key" | "value", newValue: string) {
|
|
209
226
|
if (field === "value") {
|
|
210
|
-
const { value: parsed } = parseJsonValue(newValue);
|
|
227
|
+
const { value: parsed, parseError } = parseJsonValue(newValue);
|
|
211
228
|
entries[idx].value = newValue;
|
|
212
229
|
entries[idx].parsedValue = parsed;
|
|
230
|
+
entryJsonErrors[idx] = parseError;
|
|
213
231
|
} else {
|
|
214
232
|
entries[idx].key = newValue;
|
|
215
233
|
}
|
|
@@ -243,6 +261,11 @@
|
|
|
243
261
|
return t("duplicate_keys");
|
|
244
262
|
}
|
|
245
263
|
|
|
264
|
+
// Check for JSON syntax errors when strictJsonValidation is enabled
|
|
265
|
+
if (strictJsonValidation && entryJsonErrors.some(Boolean)) {
|
|
266
|
+
return t("invalid_json_syntax");
|
|
267
|
+
}
|
|
268
|
+
|
|
246
269
|
// Continue with provided validator
|
|
247
270
|
return (validate as any)?.customValidator?.(val, context, el) || "";
|
|
248
271
|
},
|
|
@@ -42,6 +42,7 @@ export interface Props extends Record<string, any> {
|
|
|
42
42
|
addLabel?: string;
|
|
43
43
|
emptyMessage?: string;
|
|
44
44
|
onChange?: (value: string) => void;
|
|
45
|
+
strictJsonValidation?: boolean;
|
|
45
46
|
t?: TranslateFn;
|
|
46
47
|
}
|
|
47
48
|
declare const FieldKeyValues: import("svelte").Component<Props, {}, "value">;
|