@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
|
-
|
|
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…</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
|
>{"{"} {entries.map(([k]) => k).join(", ")} {"}"}</span
|
|
238
303
|
>
|
|
304
|
+
{:else if depth >= previewMaxDepth}
|
|
305
|
+
<!-- Too deep to render legibly — collapse to keys + hint -->
|
|
306
|
+
<span class="opacity-50"
|
|
307
|
+
>{"{"} {entries.map(([k]) => k).join(", ")} {"}"}</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">;
|