@marianmeres/stuic 3.112.0 → 3.113.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.
@@ -33,7 +33,14 @@ export function autogrow(el, fn) {
33
33
  // console.log(123, el.value);
34
34
  if (enabled) {
35
35
  el.style.height = "auto"; // Reset height to auto to correctly calculate scrollHeight
36
- el.style.height = Math.max(min, Math.min(el.scrollHeight, max)) + "px";
36
+ // `scrollHeight` excludes the border, but with `box-sizing: border-box` the
37
+ // height we set *includes* it — so without adding the vertical border back
38
+ // we undershoot by a couple px and a scrollbar lingers.
39
+ const cs = getComputedStyle(el);
40
+ const borderY = cs.boxSizing === "border-box"
41
+ ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)
42
+ : 0;
43
+ el.style.height = Math.max(min, Math.min(el.scrollHeight + borderY, max)) + "px";
37
44
  }
38
45
  }
39
46
  // eventlistener strategy (we're not passing value)
@@ -42,13 +42,23 @@
42
42
  addLabel?: string;
43
43
  emptyMessage?: string;
44
44
  onChange?: (value: string) => void;
45
+ /**
46
+ * When `true`, a value that *looks like* JSON (starts with `{`/`[`) but
47
+ * fails to parse becomes a blocking validation error on submit.
48
+ *
49
+ * Defaults to `false`: the component detects/parses JSON for convenience
50
+ * (pretty-print + the inline indicator) but does **not** enforce validity —
51
+ * whether a value must be valid JSON is a business rule the consumer owns.
52
+ * Unparseable values are simply stored as plain strings. Opt in to `true`
53
+ * only for a strictly JSON-only field.
54
+ */
45
55
  strictJsonValidation?: boolean;
46
56
  t?: TranslateFn;
47
57
  }
48
58
  </script>
49
59
 
50
60
  <script lang="ts">
51
- import { iconPlus, iconTrash } from "../../icons/index.js";
61
+ import { iconAlertWarning, iconCode, iconPlus, iconTrash } from "../../icons/index.js";
52
62
  import { tick } from "svelte";
53
63
  import { autogrow } from "../../actions/autogrow.svelte.js";
54
64
  import { validate as validateAction } from "../../actions/validate.svelte.js";
@@ -75,6 +85,7 @@
75
85
  remove_entry: "Remove entry",
76
86
  duplicate_keys: "Duplicate keys are not allowed",
77
87
  invalid_json_syntax: "Invalid JSON syntax. Check for missing quotes or brackets.",
88
+ json_detected: "Valid JSON",
78
89
  };
79
90
  let out = m[k] ?? fallback ?? k;
80
91
  return isPlainObject(values) ? replaceMap(out, values as any) : out;
@@ -118,7 +129,7 @@
118
129
  addLabel,
119
130
  emptyMessage,
120
131
  onChange,
121
- strictJsonValidation = true,
132
+ strictJsonValidation = false,
122
133
  t = t_default,
123
134
  }: Props = $props();
124
135
 
@@ -159,7 +170,7 @@
159
170
  if (!isPlainObject(parsed)) return [];
160
171
  return Object.entries(parsed).map(([key, val]) => ({
161
172
  key,
162
- value: typeof val === "string" ? val : JSON.stringify(val),
173
+ value: typeof val === "string" ? val : JSON.stringify(val, null, 2),
163
174
  parsedValue: val,
164
175
  }));
165
176
  } catch (e) {
@@ -232,6 +243,35 @@
232
243
  syncToValue();
233
244
  }
234
245
 
246
+ // Pretty-print structured JSON (objects/arrays) on blur. Display-only: the
247
+ // parsed value is unchanged, so the serialized external `value` is identical
248
+ // and `syncToValue()` is intentionally not called. Primitives, plain strings,
249
+ // and invalid JSON are left exactly as typed (no surprising normalization).
250
+ function formatValueEntry(idx: number) {
251
+ const raw = entries[idx].value;
252
+ const trimmed = raw.trim();
253
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return;
254
+ try {
255
+ const parsed = JSON.parse(trimmed);
256
+ const pretty = JSON.stringify(parsed, null, 2);
257
+ if (pretty !== raw) {
258
+ entries[idx].value = pretty;
259
+ entries[idx].parsedValue = parsed;
260
+ entryJsonErrors[idx] = false;
261
+ }
262
+ } catch {
263
+ // invalid JSON: leave as typed; indicator + validation handle it
264
+ }
265
+ }
266
+
267
+ // Per-entry JSON signal for the subtle inline indicator.
268
+ function jsonState(entry: KeyValueEntry, idx: number): "valid" | "error" | "none" {
269
+ if (entryJsonErrors[idx]) return "error";
270
+ const v = entry.parsedValue;
271
+ if (isPlainObject(v) || Array.isArray(v)) return "valid";
272
+ return "none";
273
+ }
274
+
235
275
  // Validation
236
276
  let validation: ValidationResult | undefined = $state();
237
277
  const setValidationResult = (res: ValidationResult) => (validation = res);
@@ -314,6 +354,7 @@
314
354
 
315
355
  const INPUT_CLS = [
316
356
  "rounded bg-(--stuic-color-input)",
357
+ "font-mono",
317
358
  "focus:outline-none focus:ring-0",
318
359
  "border border-(--stuic-color-border)",
319
360
  "focus:border-(--stuic-color-border-hover)",
@@ -355,6 +396,7 @@
355
396
  {:else}
356
397
  <div class="p-2">
357
398
  {#each entries as entry, idx (idx)}
399
+ {@const st = jsonState(entry, idx)}
358
400
  <div
359
401
  class={twMerge(
360
402
  "flex gap-2 items-start py-2",
@@ -382,15 +424,42 @@
382
424
  />
383
425
 
384
426
  <!-- Value textarea -->
385
- <textarea
386
- value={entry.value}
387
- oninput={(e) => updateEntry(idx, "value", e.currentTarget.value)}
388
- placeholder={valuePlaceholder ?? t("value_placeholder")}
389
- class={twMerge(INPUT_CLS, "min-h-10 flex-none", classValueInput)}
390
- {disabled}
391
- {tabindex}
392
- use:autogrow={() => ({ enabled: true, value: entry.value })}
393
- ></textarea>
427
+ <div class="relative">
428
+ <textarea
429
+ value={entry.value}
430
+ oninput={(e) => updateEntry(idx, "value", e.currentTarget.value)}
431
+ onblur={() => formatValueEntry(idx)}
432
+ placeholder={valuePlaceholder ?? t("value_placeholder")}
433
+ class={twMerge(
434
+ INPUT_CLS,
435
+ "w-full min-h-10 flex-none pr-6",
436
+ classValueInput
437
+ )}
438
+ {disabled}
439
+ {tabindex}
440
+ use:autogrow={() => ({ enabled: true, value: entry.value })}
441
+ ></textarea>
442
+
443
+ <!-- Subtle JSON state indicator -->
444
+ {#if st !== "none"}
445
+ <span
446
+ class={twMerge(
447
+ "pointer-events-none absolute top-1.5 right-1.5",
448
+ st === "valid"
449
+ ? "opacity-40"
450
+ : "text-amber-500 opacity-80"
451
+ )}
452
+ title={st === "valid"
453
+ ? t("json_detected")
454
+ : t("invalid_json_syntax")}
455
+ aria-hidden="true"
456
+ >
457
+ {@html st === "valid"
458
+ ? iconCode({ size: 14 })
459
+ : iconAlertWarning({ size: 14 })}
460
+ </span>
461
+ {/if}
462
+ </div>
394
463
  </div>
395
464
 
396
465
  <!-- Delete button -->
@@ -37,6 +37,16 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
37
37
  addLabel?: string;
38
38
  emptyMessage?: string;
39
39
  onChange?: (value: string) => void;
40
+ /**
41
+ * When `true`, a value that *looks like* JSON (starts with `{`/`[`) but
42
+ * fails to parse becomes a blocking validation error on submit.
43
+ *
44
+ * Defaults to `false`: the component detects/parses JSON for convenience
45
+ * (pretty-print + the inline indicator) but does **not** enforce validity —
46
+ * whether a value must be valid JSON is a business rule the consumer owns.
47
+ * Unparseable values are simply stored as plain strings. Opt in to `true`
48
+ * only for a strictly JSON-only field.
49
+ */
40
50
  strictJsonValidation?: boolean;
41
51
  t?: TranslateFn;
42
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.112.0",
3
+ "version": "3.113.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",