@marianmeres/stuic 3.123.0 → 3.124.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.
@@ -18,6 +18,19 @@
18
18
  id?: string;
19
19
  tabindex?: number;
20
20
  renderSize?: "sm" | "md" | "lg" | string;
21
+ /**
22
+ * Max nesting depth rendered in (read-only) preview mode before a node is
23
+ * collapsed to a keys-only / `[n]` summary with a "more…" hint. Prevents
24
+ * deeply nested objects from blowing out horizontally and becoming
25
+ * unreadable. Has no effect on raw edit mode. Defaults to 4.
26
+ */
27
+ previewMaxDepth?: number;
28
+ /**
29
+ * When true (the default), the edit toggle opens the raw JSON editor in a
30
+ * (near) full-screen modal — comfortable for large / deeply nested objects.
31
+ * Set `false` to edit inline in a textarea below the preview instead.
32
+ */
33
+ fullscreenEdit?: boolean;
21
34
  required?: boolean;
22
35
  disabled?: boolean;
23
36
  validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
@@ -38,6 +51,9 @@
38
51
  import { validate as validateAction } from "../../actions/validate.svelte.js";
39
52
  import { getId } from "../../utils/get-id.js";
40
53
  import { twMerge } from "../../utils/tw-merge.js";
54
+ import Button from "../Button/Button.svelte";
55
+ import Modal from "../Modal/Modal.svelte";
56
+ import { Thc, isTHCNotEmpty } from "../Thc/index.js";
41
57
  import InputWrap from "./_internal/InputWrap.svelte";
42
58
 
43
59
  let {
@@ -49,6 +65,8 @@
49
65
  class: classProp,
50
66
  tabindex = 0,
51
67
  renderSize = "sm",
68
+ previewMaxDepth = 4,
69
+ fullscreenEdit = true,
52
70
  required = false,
53
71
  disabled = false,
54
72
  // Renamed local binding to avoid collision with `export function validate()` below.
@@ -113,17 +131,54 @@
113
131
  }
114
132
  } else {
115
133
  validation = undefined;
116
- // Pretty-print the JSON before entering edit mode
117
- try {
118
- const obj = JSON.parse(value || "null");
119
- value = JSON.stringify(obj, null, "\t");
120
- } catch {
121
- // leave value as-is if not parseable
122
- }
134
+ prettyPrint();
123
135
  editMode = true;
124
136
  }
125
137
  }
126
138
 
139
+ /** Pretty-print `value` in place; leaves it untouched if not parseable. */
140
+ function prettyPrint() {
141
+ try {
142
+ value = JSON.stringify(JSON.parse(value || "null"), null, "\t");
143
+ } catch {
144
+ // not parseable — leave as-is
145
+ }
146
+ }
147
+
148
+ // --- Fullscreen modal editor (opt-in via `fullscreenEdit`) ---
149
+ let fsModal: Modal | undefined = $state();
150
+ // Snapshot of `value` captured when the modal opens, so Cancel / Escape can
151
+ // discard edits and restore the original (the inline editor has no cancel).
152
+ let fsSnapshot = "";
153
+
154
+ function openFullscreen(openerOrEvent?: MouseEvent) {
155
+ validation = undefined;
156
+ fsSnapshot = value;
157
+ prettyPrint();
158
+ fsModal?.open(openerOrEvent ?? null);
159
+ }
160
+
161
+ function applyFullscreen() {
162
+ // Same rule as the inline toggle: invalid JSON keeps the editor open.
163
+ try {
164
+ JSON.parse(value || "null");
165
+ validation = undefined;
166
+ fsModal?.close();
167
+ } catch {
168
+ validation = {
169
+ valid: false,
170
+ message: "This field requires attention. Please review and try again.",
171
+ };
172
+ }
173
+ }
174
+
175
+ function cancelFullscreen() {
176
+ // discard edits — restore the value as it was when the modal opened
177
+ value = fsSnapshot;
178
+ validation = undefined;
179
+ fsModal?.close();
180
+ }
181
+
127
182
  // Validation
128
183
  let validation: ValidationResult | undefined = $state();
129
184
  const setValidationResult = (res: ValidationResult) => (validation = res);
@@ -167,7 +222,7 @@
167
222
  }
168
223
 
169
224
  const TEXTAREA_CLS =
170
- "w-full min-h-16 p-2 font-mono text-sm focus:outline-none focus:ring-0";
225
+ "w-full min-h-16 p-2 font-mono text-sm focus:outline-none focus:ring-0 scrollbar-thin";
171
226
 
172
227
  const BTN_CLS = [
173
228
  "toggle-btn",
@@ -198,6 +253,13 @@
198
253
  {/if}
199
254
  {/snippet}
200
255
 
256
+ {#snippet moreHint()}
257
+ <span
258
+ class="ml-1 align-middle text-xs italic opacity-40"
259
+ title="Open the editor to view the full structure">more&hellip;</span
260
+ >
261
+ {/snippet}
262
+
201
263
  {#snippet renderValue(val: unknown, depth: number)}
202
264
  {#if val === null || val === undefined || typeof val === "boolean" || typeof val === "number" || typeof val === "string"}
203
265
  {@render renderPrimitive(val)}
@@ -210,6 +272,9 @@
210
272
  <span class="break-words"
211
273
  >{val.map((v) => (v === null ? "null" : String(v))).join(", ")}</span
212
274
  >
275
+ {:else if depth >= previewMaxDepth}
276
+ <!-- Too deep to render legibly — collapse to count + hint -->
277
+ <span class="opacity-50">[{val.length}]</span>{@render moreHint()}
213
278
  {:else}
214
279
  <div class="flex flex-col gap-2">
215
280
  {#each val as item, i (i)}
@@ -236,6 +301,11 @@
236
301
  <span class="opacity-50"
237
302
  >{"{"}&#8239;{entries.map(([k]) => k).join(", ")}&#8239;{"}"}</span
238
303
  >
304
+ {:else if depth >= previewMaxDepth}
305
+ <!-- Too deep to render legibly — collapse to keys + hint -->
306
+ <span class="opacity-50"
307
+ >{"{"}&#8239;{entries.map(([k]) => k).join(", ")}&#8239;{"}"}</span
308
+ >{@render moreHint()}
239
309
  {:else}
240
310
  <div class={twMerge("flex flex-col", depth > 0 && "ml-2")}>
241
311
  {#each entries as [key, v], i (key)}
@@ -310,6 +380,7 @@
310
380
  class={TEXTAREA_CLS}
311
381
  {tabindex}
312
382
  {disabled}
383
+ wrap="off"
313
384
  use:autogrow={() => ({ enabled: true, value })}
314
385
  ></textarea>
315
386
  {:else}
@@ -331,7 +402,7 @@
331
402
  type="button"
332
403
  class={BTN_CLS}
333
404
  bind:this={toggleBtnEl}
334
- onclick={toggleMode}
405
+ onclick={(e) => (fullscreenEdit ? openFullscreen(e) : toggleMode())}
335
406
  {disabled}
336
407
  use:tooltip={() => ({
337
408
  enabled: true,
@@ -374,3 +445,52 @@
374
445
  };
375
446
  }}
376
447
  />
448
+
449
+ {#if fullscreenEdit}
450
+ <!-- Full-screen raw JSON editor (opened by the edit toggle when `fullscreenEdit`) -->
451
+ <Modal
452
+ bind:this={fsModal}
453
+ noClickOutsideClose
454
+ onEscape={() => {
455
+ // Escape behaves like Cancel — discard edits (the dialog closes itself)
456
+ value = fsSnapshot;
457
+ validation = undefined;
458
+ }}
459
+ classDialog="md:size-full"
460
+ classInner="md:h-full md:max-h-full md:min-h-0 md:w-full md:max-w-full"
461
+ classHeader="p-3 flex items-center gap-2 border-b border-(--stuic-color-border)"
462
+ classMain="p-0 flex flex-col overflow-hidden"
463
+ classFooter="p-3 flex items-center border-t border-(--stuic-color-border)"
464
+ >
465
+ {#snippet header()}
466
+ <span class="flex-1 truncate font-semibold">
467
+ {#if typeof label === "function"}
468
+ {@render label({ id })}
469
+ {:else if isTHCNotEmpty(label)}
470
+ <Thc thc={label as THC} />
471
+ {:else}
472
+ Edit JSON
473
+ {/if}
474
+ </span>
475
+ {/snippet}
476
+
477
+ <textarea
478
+ bind:value
479
+ {disabled}
480
+ wrap="off"
481
+ class="min-h-0 w-full flex-1 resize-none p-3 font-mono text-sm scrollbar-thin focus:outline-none focus:ring-0"
482
+ ></textarea>
483
+
484
+ {#snippet footer()}
485
+ <div class="flex w-full items-center justify-between gap-3">
486
+ <span class="text-sm text-red-600 dark:text-red-400">
487
+ {#if validation && !validation.valid}{validation.message}{/if}
488
+ </span>
489
+ <div class="flex gap-2">
490
+ <Button type="button" variant="ghost" onclick={cancelFullscreen}>Cancel</Button>
491
+ <Button type="button" onclick={applyFullscreen}>Apply</Button>
492
+ </div>
493
+ </div>
494
+ {/snippet}
495
+ </Modal>
496
+ {/if}
@@ -14,6 +14,19 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
14
14
  id?: string;
15
15
  tabindex?: number;
16
16
  renderSize?: "sm" | "md" | "lg" | string;
17
+ /**
18
+ * Max nesting depth rendered in (read-only) preview mode before a node is
19
+ * collapsed to a keys-only / `[n]` summary with a "more…" hint. Prevents
20
+ * deeply nested objects from blowing out horizontally and becoming
21
+ * unreadable. Has no effect on raw edit mode. Defaults to 4.
22
+ */
23
+ previewMaxDepth?: number;
24
+ /**
25
+ * When true (the default), the edit toggle opens the raw JSON editor in a
26
+ * (near) full-screen modal — comfortable for large / deeply nested objects.
27
+ * Set `false` to edit inline in a textarea below the preview instead.
28
+ */
29
+ fullscreenEdit?: boolean;
17
30
  required?: boolean;
18
31
  disabled?: boolean;
19
32
  validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.123.0",
3
+ "version": "3.124.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",