@viniaraujo68/plinth 0.3.1 → 0.5.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.
Files changed (103) hide show
  1. package/README.md +189 -19
  2. package/dist/components/ErrorDisplay.svelte +30 -16
  3. package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
  4. package/dist/components/JsonView.svelte +43 -0
  5. package/dist/components/JsonView.svelte.d.ts +4 -0
  6. package/dist/components/JsonViewFrame.svelte +654 -0
  7. package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
  8. package/dist/components/Paginator.svelte +34 -8
  9. package/dist/components/Paginator.svelte.d.ts +10 -0
  10. package/dist/components/RequestErrorDisplay.svelte +43 -0
  11. package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
  12. package/dist/components/Tabs.svelte +204 -0
  13. package/dist/components/Tabs.svelte.d.ts +76 -0
  14. package/dist/components/VirtualJsonView.svelte +67 -0
  15. package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
  16. package/dist/components/index.d.ts +8 -1
  17. package/dist/components/index.js +11 -5
  18. package/dist/components/json-view.d.ts +70 -0
  19. package/dist/components/json-view.js +302 -0
  20. package/dist/components/request-error.d.ts +8 -0
  21. package/dist/components/request-error.js +55 -0
  22. package/dist/components/tabs.d.ts +5 -0
  23. package/dist/components/tabs.js +1 -0
  24. package/dist/http.d.ts +10 -5
  25. package/dist/http.js +34 -6
  26. package/dist/i18n/LocaleToggle.svelte +45 -0
  27. package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
  28. package/dist/i18n/i18n.svelte.d.ts +56 -0
  29. package/dist/i18n/i18n.svelte.js +94 -0
  30. package/dist/i18n/index.d.ts +3 -0
  31. package/dist/i18n/index.js +7 -0
  32. package/dist/i18n/translate.d.ts +30 -0
  33. package/dist/i18n/translate.js +38 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/redirect.d.ts +40 -0
  37. package/dist/redirect.js +59 -0
  38. package/dist/routing.svelte.d.ts +1 -0
  39. package/dist/routing.svelte.js +3 -0
  40. package/dist/shell/AppShell.svelte +265 -93
  41. package/dist/shell/AppShell.svelte.d.ts +3 -2
  42. package/dist/shell/NavigationProgress.svelte +85 -0
  43. package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
  44. package/dist/shell/TopBar.svelte +191 -0
  45. package/dist/shell/TopBar.svelte.d.ts +41 -0
  46. package/dist/shell/UserAvatar.svelte +50 -0
  47. package/dist/shell/UserAvatar.svelte.d.ts +14 -0
  48. package/dist/shell/index.d.ts +2 -0
  49. package/dist/shell/index.js +7 -3
  50. package/dist/shell/initials.d.ts +6 -0
  51. package/dist/shell/initials.js +13 -0
  52. package/dist/table/DataTable.svelte +199 -9
  53. package/dist/table/DataTable.svelte.d.ts +28 -5
  54. package/dist/table/FilterBar.svelte +417 -0
  55. package/dist/table/FilterBar.svelte.d.ts +34 -0
  56. package/dist/table/FilterRuleEditor.svelte +290 -0
  57. package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
  58. package/dist/table/filter-labels.d.ts +23 -0
  59. package/dist/table/filter-labels.js +30 -0
  60. package/dist/table/filter.d.ts +27 -0
  61. package/dist/table/filter.js +279 -0
  62. package/dist/table/index.d.ts +4 -1
  63. package/dist/table/index.js +5 -2
  64. package/dist/table/types.d.ts +49 -0
  65. package/dist/toast/Toaster.svelte +7 -3
  66. package/dist/toast/index.d.ts +1 -1
  67. package/dist/toast/toast-manager.svelte.d.ts +29 -1
  68. package/dist/toast/toast-manager.svelte.js +61 -1
  69. package/dist/url-state/codec.d.ts +15 -0
  70. package/dist/url-state/codec.js +1 -0
  71. package/dist/url-state/index.d.ts +5 -0
  72. package/dist/url-state/index.js +4 -0
  73. package/dist/url-state/params-codec.d.ts +73 -0
  74. package/dist/url-state/params-codec.js +116 -0
  75. package/dist/url-state/search-params.d.ts +4 -0
  76. package/dist/url-state/search-params.js +28 -0
  77. package/dist/url-state/synced-state.svelte.d.ts +20 -0
  78. package/dist/url-state/synced-state.svelte.js +88 -0
  79. package/dist/url-state/url-store.svelte.d.ts +12 -0
  80. package/dist/url-state/url-store.svelte.js +56 -0
  81. package/dist/virtual/DataList.svelte +215 -0
  82. package/dist/virtual/DataList.svelte.d.ts +52 -0
  83. package/dist/virtual/VirtualList.svelte +150 -0
  84. package/dist/virtual/VirtualList.svelte.d.ts +42 -0
  85. package/dist/virtual/index.d.ts +7 -0
  86. package/dist/virtual/index.js +6 -0
  87. package/dist/virtual/keyed-cache.d.ts +7 -0
  88. package/dist/virtual/keyed-cache.js +20 -0
  89. package/dist/virtual/labels.d.ts +12 -0
  90. package/dist/virtual/labels.js +6 -0
  91. package/dist/virtual/paging.d.ts +5 -0
  92. package/dist/virtual/paging.js +11 -0
  93. package/dist/virtual/range.d.ts +9 -0
  94. package/dist/virtual/range.js +31 -0
  95. package/dist/virtual/size-tree.d.ts +10 -0
  96. package/dist/virtual/size-tree.js +56 -0
  97. package/dist/virtual/virtualizer.svelte.d.ts +38 -0
  98. package/dist/virtual/virtualizer.svelte.js +295 -0
  99. package/dist/virtual/windowed-data.svelte.d.ts +38 -0
  100. package/dist/virtual/windowed-data.svelte.js +139 -0
  101. package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
  102. package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
  103. package/package.json +13 -1
@@ -0,0 +1,654 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { ClassValue } from "svelte/elements";
4
+ import type { JsonMatch, JsonViewLabels } from "./json-view.js";
5
+
6
+ export interface JsonViewProps {
7
+ value: unknown;
8
+ class?: ClassValue;
9
+ labels?: Partial<JsonViewLabels>;
10
+ collapseDepth?: number;
11
+ onselect?: (path: string, value: unknown) => void;
12
+ }
13
+
14
+ export interface JsonViewViewport {
15
+ visible: readonly number[];
16
+ line: Snippet<[number]>;
17
+ rowId: (index: number) => string;
18
+ rowHeight: number;
19
+ widthCh: number;
20
+ }
21
+
22
+ export interface JsonViewRevealTarget {
23
+ line: number;
24
+ visibleIndex: number;
25
+ match: JsonMatch;
26
+ columnCh: number;
27
+ }
28
+
29
+ export const JSON_VIEW_ROW_HEIGHT = 22;
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ import { onDestroy, tick } from "svelte";
34
+ import { SvelteMap } from "svelte/reactivity";
35
+ import {
36
+ ancestorLines,
37
+ copyAllJson,
38
+ copyJson,
39
+ DEFAULT_JSON_VIEW_LABELS,
40
+ flattenJson,
41
+ isFoldable,
42
+ matchColumnCh,
43
+ matchSegments,
44
+ maxLineWidthCh,
45
+ searchJson,
46
+ subtreeHasMatch,
47
+ visibleLines,
48
+ type JsonLine,
49
+ type JsonPoint,
50
+ } from "./json-view.js";
51
+
52
+ type Props = JsonViewProps & {
53
+ viewport: Snippet<[JsonViewViewport]>;
54
+ reveal: (target: JsonViewRevealTarget) => void | Promise<void>;
55
+ };
56
+
57
+ let {
58
+ value,
59
+ class: className,
60
+ labels,
61
+ collapseDepth,
62
+ onselect,
63
+ viewport,
64
+ reveal,
65
+ }: Props = $props();
66
+
67
+ const text = $derived({ ...DEFAULT_JSON_VIEW_LABELS, ...labels });
68
+ const lines = $derived(flattenJson(value, { circular: text.circular }));
69
+
70
+ let baseline = $state<"depth" | "expanded" | "collapsed">("depth");
71
+ const overrides = new SvelteMap<string, boolean>();
72
+
73
+ const foldedByDefault = (line: JsonLine): boolean => {
74
+ if (baseline === "expanded") return false;
75
+ if (baseline === "collapsed") return line.depth > 0;
76
+ return collapseDepth !== undefined && line.depth >= collapseDepth;
77
+ };
78
+
79
+ const collapsed: ReadonlySet<string> = $derived(
80
+ new Set(
81
+ lines
82
+ .filter(
83
+ (line) => isFoldable(line) && (overrides.get(line.pointer) ?? foldedByDefault(line)),
84
+ )
85
+ .map((line) => line.pointer),
86
+ ),
87
+ );
88
+
89
+ const visible = $derived(visibleLines(lines, collapsed));
90
+ const widthCh = $derived(maxLineWidthCh(lines));
91
+ const rowId = (index: number) => lines[index].id;
92
+
93
+ const toggle = (line: JsonLine) => overrides.set(line.pointer, !collapsed.has(line.pointer));
94
+
95
+ const expandAll = () => {
96
+ overrides.clear();
97
+ baseline = "expanded";
98
+ };
99
+
100
+ const collapseAll = () => {
101
+ overrides.clear();
102
+ baseline = "collapsed";
103
+ };
104
+
105
+ let root = $state<HTMLDivElement>();
106
+ let searchField = $state<HTMLInputElement>();
107
+ let searching = $state(false);
108
+ let query = $state("");
109
+ let current = $state(0);
110
+
111
+ const matches = $derived(searchJson(lines, query));
112
+ const hitLines = $derived(new Set(matches.map((match) => match.line)));
113
+ const active = $derived(
114
+ matches.length === 0 ? undefined : matches[Math.min(current, matches.length - 1)],
115
+ );
116
+
117
+ const show = async (position: number) => {
118
+ if (matches.length === 0) return;
119
+ current = (position + matches.length) % matches.length;
120
+ const match = matches[current];
121
+ for (const ancestor of ancestorLines(lines, match.line))
122
+ overrides.set(lines[ancestor].pointer, false);
123
+ await tick();
124
+ await reveal({
125
+ line: match.line,
126
+ visibleIndex: visible.indexOf(match.line),
127
+ match,
128
+ columnCh: matchColumnCh(lines[match.line], match),
129
+ });
130
+ };
131
+
132
+ const step = (direction: 1 | -1) =>
133
+ show(Math.min(current, Math.max(matches.length - 1, 0)) + direction);
134
+
135
+ const openSearch = async () => {
136
+ searching = true;
137
+ await tick();
138
+ searchField?.focus();
139
+ searchField?.select();
140
+ };
141
+
142
+ const closeSearch = () => {
143
+ searching = false;
144
+ query = "";
145
+ current = 0;
146
+ root?.focus();
147
+ };
148
+
149
+ const onQueryInput = (event: Event & { currentTarget: HTMLInputElement }) => {
150
+ query = event.currentTarget.value;
151
+ current = 0;
152
+ void show(0);
153
+ };
154
+
155
+ const onQueryKeydown = (event: KeyboardEvent) => {
156
+ if (event.key === "Enter") {
157
+ event.preventDefault();
158
+ void step(event.shiftKey ? -1 : 1);
159
+ } else if (event.key === "Escape") {
160
+ event.preventDefault();
161
+ event.stopPropagation();
162
+ closeSearch();
163
+ }
164
+ };
165
+
166
+ const onRootKeydown = (event: KeyboardEvent) => {
167
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "f") {
168
+ event.preventDefault();
169
+ void openSearch();
170
+ }
171
+ };
172
+
173
+ let copied = $state(false);
174
+ let copiedTimer: ReturnType<typeof setTimeout> | undefined;
175
+
176
+ const copyAll = async () => {
177
+ try {
178
+ await navigator.clipboard.writeText(copyAllJson(lines));
179
+ } catch {
180
+ return;
181
+ }
182
+ copied = true;
183
+ clearTimeout(copiedTimer);
184
+ copiedTimer = setTimeout(() => (copied = false), 1200);
185
+ };
186
+
187
+ onDestroy(() => clearTimeout(copiedTimer));
188
+
189
+ const isBlank = (node: Text) => !/\S/.test(node.data);
190
+
191
+ const columnIn = (row: HTMLElement, node: Node, offset: number): number => {
192
+ const before = document.createRange();
193
+ before.setStart(row, 0);
194
+ before.setEnd(node, offset);
195
+ const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT);
196
+ let column = 0;
197
+ for (let next = walker.nextNode(); next !== null; next = walker.nextNode()) {
198
+ const textNode = next as Text;
199
+ if (textNode === node) return column + (isBlank(textNode) ? 0 : offset);
200
+ if (before.comparePoint(textNode, textNode.length) !== 0) break;
201
+ if (!isBlank(textNode)) column += textNode.length;
202
+ }
203
+ return column;
204
+ };
205
+
206
+ const lineOf = (row: HTMLElement) => Number(row.dataset.line);
207
+
208
+ const nearestRow = (node: Node, offset: number): JsonPoint | null => {
209
+ const rows = root?.querySelectorAll<HTMLElement>("[data-line]") ?? [];
210
+ let last: HTMLElement | undefined;
211
+ for (const row of rows) {
212
+ const range = document.createRange();
213
+ range.selectNode(row);
214
+ if (range.comparePoint(node, offset) < 0) return { line: lineOf(row), column: 0 };
215
+ last = row;
216
+ }
217
+ return last ? { line: lineOf(last), column: Number.POSITIVE_INFINITY } : null;
218
+ };
219
+
220
+ const pointAt = (node: Node, offset: number): JsonPoint | null => {
221
+ if (!root?.contains(node)) return null;
222
+ const element = node instanceof Element ? node : node.parentElement;
223
+ const row = element?.closest<HTMLElement>("[data-line]");
224
+ if (row && root.contains(row))
225
+ return { line: lineOf(row), column: columnIn(row, node, offset) };
226
+ return nearestRow(node, offset);
227
+ };
228
+
229
+ const onCopy = (event: ClipboardEvent) => {
230
+ if (event.target instanceof HTMLInputElement) return;
231
+ const selection = getSelection();
232
+ if (!selection || selection.isCollapsed || !selection.anchorNode || !selection.focusNode)
233
+ return;
234
+ const from = pointAt(selection.anchorNode, selection.anchorOffset);
235
+ const to = pointAt(selection.focusNode, selection.focusOffset);
236
+ if (!from || !to || !event.clipboardData) return;
237
+ event.clipboardData.setData("text/plain", copyJson(lines, collapsed, from, to));
238
+ event.preventDefault();
239
+ };
240
+
241
+ const onRootClick = (event: MouseEvent) => {
242
+ if (!onselect || !(event.target instanceof Element)) return;
243
+ const part = event.target.closest("[data-part]");
244
+ const row = part?.closest<HTMLElement>("[data-line]");
245
+ if (!row) return;
246
+ const selection = getSelection();
247
+ if (selection && !selection.isCollapsed) return;
248
+ const line = lines[lineOf(row)];
249
+ onselect(line.pointer, line.source);
250
+ };
251
+
252
+ const isActive = (index: number, field: JsonMatch["field"], start: number) =>
253
+ active !== undefined &&
254
+ active.line === index &&
255
+ active.field === field &&
256
+ active.start === start;
257
+
258
+ const closing = (line: JsonLine) => (line.text === "[" ? "]" : "}");
259
+
260
+ const tone = (line: JsonLine) =>
261
+ line.type === null ? "plinth-json-punct" : `plinth-json-${line.type}`;
262
+ </script>
263
+
264
+ {#snippet icon(paths: string[])}
265
+ <svg
266
+ class="size-3.5"
267
+ viewBox="0 0 24 24"
268
+ fill="none"
269
+ stroke="currentColor"
270
+ stroke-width="2"
271
+ stroke-linecap="round"
272
+ stroke-linejoin="round"
273
+ aria-hidden="true"
274
+ >
275
+ {#each paths as d (d)}
276
+ <path {d} />
277
+ {/each}
278
+ </svg>
279
+ {/snippet}
280
+
281
+ {#snippet tokens(index: number, field: JsonMatch["field"], content: string, toneClass: string)}
282
+ {#if hitLines.has(index)}
283
+ {#each matchSegments(content, query) as segment (segment.start)}
284
+ {#if segment.match}
285
+ <mark
286
+ class={["plinth-json-hit", toneClass]}
287
+ data-part={field}
288
+ data-active={isActive(index, field, segment.start) || undefined}>{segment.text}</mark
289
+ >
290
+ {:else}
291
+ <span class={toneClass} data-part={field}>{segment.text}</span>
292
+ {/if}
293
+ {/each}
294
+ {:else}
295
+ <span class={toneClass} data-part={field}>{content}</span>
296
+ {/if}
297
+ {/snippet}
298
+
299
+ {#snippet line(index: number)}
300
+ {@const row = lines[index]}
301
+ {@const foldable = isFoldable(row)}
302
+ {@const folded = foldable && collapsed.has(row.pointer)}
303
+ <div
304
+ class={[
305
+ "plinth-json-row",
306
+ hitLines.has(index) && "plinth-json-row-hit",
307
+ folded &&
308
+ query !== "" &&
309
+ subtreeHasMatch(matches, index, row.end) &&
310
+ "plinth-json-row-hidden-hit",
311
+ onselect && "plinth-json-selectable",
312
+ ]}
313
+ style:height="{JSON_VIEW_ROW_HEIGHT}px"
314
+ style:padding-inline-start="{row.depth * 2}ch"
315
+ data-line={index}
316
+ >
317
+ {#if foldable}
318
+ <button
319
+ type="button"
320
+ class="plinth-json-toggle"
321
+ aria-expanded={!folded}
322
+ aria-label={folded ? text.expand : text.collapse}
323
+ onclick={() => toggle(row)}
324
+ >
325
+ <svg
326
+ viewBox="0 0 24 24"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ stroke-width="2"
330
+ stroke-linecap="round"
331
+ stroke-linejoin="round"
332
+ aria-hidden="true"><path d="m9 6 6 6-6 6" /></svg
333
+ >
334
+ </button>
335
+ {:else}
336
+ <span class="plinth-json-gutter"></span>
337
+ {/if}
338
+ {#if row.name !== null}
339
+ {@render tokens(index, "name", row.name, "plinth-json-name")}
340
+ <span class="plinth-json-punct">:&nbsp;</span>
341
+ {/if}
342
+ {#if row.kind === "value"}
343
+ {@render tokens(index, "value", row.text, tone(row))}
344
+ {:else if row.kind === "open"}
345
+ <span class="plinth-json-punct" data-part="value">{row.text}</span>
346
+ {#if row.size === 0}
347
+ <span class="plinth-json-punct" data-part="value">{closing(row)}</span>
348
+ {:else if folded}
349
+ <button type="button" class="plinth-json-fold" tabindex="-1" onclick={() => toggle(row)}
350
+ >…{closing(row)}</button
351
+ >
352
+ {/if}
353
+ {:else}
354
+ <span class="plinth-json-punct">{row.text}</span>
355
+ {/if}
356
+ {#if row.comma && (row.kind !== "open" || folded || row.size === 0)}
357
+ <span class="plinth-json-punct">,</span>
358
+ {/if}
359
+ {#if folded}
360
+ <span class="plinth-json-count" title={text.items(row.size)}>&nbsp;{row.size}</span>
361
+ {/if}
362
+ </div>
363
+ {/snippet}
364
+
365
+ <div
366
+ bind:this={root}
367
+ class={["plinth-json-view rounded-box border border-base-content/10 bg-base-100", className]}
368
+ role="region"
369
+ aria-label={text.region}
370
+ tabindex="-1"
371
+ onkeydown={onRootKeydown}
372
+ oncopy={onCopy}
373
+ onclick={onRootClick}
374
+ >
375
+ <div class="plinth-json-toolbar">
376
+ {#if searching}
377
+ <div class="plinth-json-search">
378
+ <input
379
+ bind:this={searchField}
380
+ type="search"
381
+ class="input w-48 min-w-0 input-xs"
382
+ placeholder={text.searchField}
383
+ aria-label={text.searchField}
384
+ value={query}
385
+ oninput={onQueryInput}
386
+ onkeydown={onQueryKeydown}
387
+ />
388
+ {#if query !== ""}
389
+ <span class="plinth-json-count-label" role="status">
390
+ {matches.length === 0
391
+ ? text.noMatches
392
+ : text.matchCount(Math.min(current, matches.length - 1) + 1, matches.length)}
393
+ </span>
394
+ {/if}
395
+ <button
396
+ type="button"
397
+ class="btn btn-square btn-ghost btn-xs"
398
+ aria-label={text.previousMatch}
399
+ title={text.previousMatch}
400
+ disabled={matches.length === 0}
401
+ onclick={() => step(-1)}
402
+ >
403
+ {@render icon(["m6 15 6-6 6 6"])}
404
+ </button>
405
+ <button
406
+ type="button"
407
+ class="btn btn-square btn-ghost btn-xs"
408
+ aria-label={text.nextMatch}
409
+ title={text.nextMatch}
410
+ disabled={matches.length === 0}
411
+ onclick={() => step(1)}
412
+ >
413
+ {@render icon(["m6 9 6 6 6-6"])}
414
+ </button>
415
+ <button
416
+ type="button"
417
+ class="btn btn-square btn-ghost btn-xs"
418
+ aria-label={text.closeSearch}
419
+ title={text.closeSearch}
420
+ onclick={closeSearch}
421
+ >
422
+ {@render icon(["M18 6 6 18", "m6 6 12 12"])}
423
+ </button>
424
+ </div>
425
+ {:else}
426
+ <button
427
+ type="button"
428
+ class="btn btn-square btn-ghost btn-xs"
429
+ aria-label={text.search}
430
+ title={text.search}
431
+ onclick={openSearch}
432
+ >
433
+ {@render icon(["m20 20-4.2-4.2", "M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14"])}
434
+ </button>
435
+ {/if}
436
+ <div class="plinth-json-actions">
437
+ <button
438
+ type="button"
439
+ class="btn btn-square btn-ghost btn-xs"
440
+ aria-label={text.expandAll}
441
+ title={text.expandAll}
442
+ disabled={lines.length === 0}
443
+ onclick={expandAll}
444
+ >
445
+ {@render icon(["m7 15 5 5 5-5", "m7 9 5-5 5 5"])}
446
+ </button>
447
+ <button
448
+ type="button"
449
+ class="btn btn-square btn-ghost btn-xs"
450
+ aria-label={text.collapseAll}
451
+ title={text.collapseAll}
452
+ disabled={lines.length === 0}
453
+ onclick={collapseAll}
454
+ >
455
+ {@render icon(["m7 20 5-5 5 5", "m7 4 5 5 5-5"])}
456
+ </button>
457
+ <button
458
+ type="button"
459
+ class="btn btn-square btn-ghost btn-xs"
460
+ aria-label={copied ? text.copied : text.copy}
461
+ title={copied ? text.copied : text.copy}
462
+ disabled={lines.length === 0}
463
+ onclick={copyAll}
464
+ >
465
+ {#if copied}
466
+ {@render icon(["m4 12.5 5 5L20 6.5"])}
467
+ {:else}
468
+ {@render icon([
469
+ "M11 9h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2z",
470
+ "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1",
471
+ ])}
472
+ {/if}
473
+ </button>
474
+ </div>
475
+ </div>
476
+ {#if lines.length === 0}
477
+ <p class="plinth-json-empty">{text.empty}</p>
478
+ {:else}
479
+ {@render viewport({ visible, line, rowId, rowHeight: JSON_VIEW_ROW_HEIGHT, widthCh })}
480
+ {/if}
481
+ </div>
482
+
483
+ <style>
484
+ .plinth-json-view {
485
+ display: flex;
486
+ flex-direction: column;
487
+ min-height: 0;
488
+ overflow: hidden;
489
+ font-family: var(--font-mono, ui-monospace, monospace);
490
+ font-size: 0.75rem;
491
+ }
492
+
493
+ .plinth-json-view:focus-visible {
494
+ outline: 2px solid var(--color-primary);
495
+ outline-offset: 2px;
496
+ }
497
+
498
+ .plinth-json-toolbar {
499
+ display: flex;
500
+ flex: none;
501
+ align-items: center;
502
+ gap: 0.25rem;
503
+ padding: 0.25rem 0.375rem;
504
+ border-bottom: 1px solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
505
+ font-family: var(--font-sans, inherit);
506
+ }
507
+
508
+ .plinth-json-search {
509
+ display: flex;
510
+ flex: 1;
511
+ align-items: center;
512
+ gap: 0.25rem;
513
+ min-width: 0;
514
+ }
515
+
516
+ .plinth-json-search input::-webkit-search-cancel-button {
517
+ appearance: none;
518
+ display: none;
519
+ }
520
+
521
+ .plinth-json-count-label {
522
+ flex: none;
523
+ padding-inline: 0.25rem;
524
+ color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
525
+ font-variant-numeric: tabular-nums;
526
+ white-space: nowrap;
527
+ }
528
+
529
+ .plinth-json-actions {
530
+ display: flex;
531
+ align-items: center;
532
+ gap: 0.125rem;
533
+ margin-inline-start: auto;
534
+ }
535
+
536
+ .plinth-json-empty {
537
+ padding: 0.75rem;
538
+ color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
539
+ font-family: var(--font-sans, inherit);
540
+ }
541
+
542
+ .plinth-json-row {
543
+ display: flex;
544
+ align-items: center;
545
+ white-space: pre;
546
+ }
547
+
548
+ .plinth-json-row > :global(*) {
549
+ flex: none;
550
+ }
551
+
552
+ .plinth-json-row-hit {
553
+ background: color-mix(in oklch, var(--color-warning) 8%, transparent);
554
+ }
555
+
556
+ .plinth-json-row-hidden-hit .plinth-json-fold {
557
+ background: color-mix(in oklch, var(--color-warning) 35%, transparent);
558
+ }
559
+
560
+ .plinth-json-gutter,
561
+ .plinth-json-toggle {
562
+ width: 2ch;
563
+ height: 100%;
564
+ }
565
+
566
+ .plinth-json-toggle {
567
+ display: grid;
568
+ place-items: center;
569
+ padding: 0;
570
+ color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
571
+ cursor: pointer;
572
+ border-radius: var(--radius-field, 0.25rem);
573
+ }
574
+
575
+ .plinth-json-toggle:hover {
576
+ color: var(--color-base-content);
577
+ }
578
+
579
+ .plinth-json-toggle[aria-expanded="true"] :global(svg) {
580
+ transform: rotate(90deg);
581
+ }
582
+
583
+ .plinth-json-toggle :global(svg) {
584
+ width: 1.1em;
585
+ height: 1.1em;
586
+ transition: transform 120ms ease;
587
+ }
588
+
589
+ .plinth-json-fold {
590
+ padding: 0;
591
+ border-radius: 0.25rem;
592
+ color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
593
+ background: color-mix(in oklch, var(--color-base-content) 7%, transparent);
594
+ font: inherit;
595
+ cursor: pointer;
596
+ }
597
+
598
+ .plinth-json-fold:hover {
599
+ color: var(--color-base-content);
600
+ }
601
+
602
+ .plinth-json-count {
603
+ color: color-mix(in oklch, var(--color-base-content) 40%, transparent);
604
+ }
605
+
606
+ .plinth-json-punct {
607
+ color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
608
+ }
609
+
610
+ .plinth-json-name {
611
+ color: var(--color-secondary);
612
+ }
613
+
614
+ .plinth-json-string {
615
+ color: var(--color-success);
616
+ }
617
+
618
+ .plinth-json-number {
619
+ color: var(--color-info);
620
+ }
621
+
622
+ .plinth-json-boolean {
623
+ color: var(--color-warning);
624
+ }
625
+
626
+ .plinth-json-null {
627
+ color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
628
+ font-style: italic;
629
+ }
630
+
631
+ .plinth-json-circular {
632
+ color: var(--color-error);
633
+ font-style: italic;
634
+ }
635
+
636
+ .plinth-json-selectable [data-part] {
637
+ cursor: pointer;
638
+ }
639
+
640
+ .plinth-json-selectable [data-part]:hover {
641
+ text-decoration: underline;
642
+ text-decoration-color: color-mix(in oklch, currentColor 50%, transparent);
643
+ }
644
+
645
+ .plinth-json-hit {
646
+ border-radius: 0.125rem;
647
+ background: color-mix(in oklch, var(--color-warning) 30%, transparent);
648
+ }
649
+
650
+ .plinth-json-hit[data-active] {
651
+ background: var(--color-warning);
652
+ color: var(--color-warning-content);
653
+ }
654
+ </style>
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ClassValue } from "svelte/elements";
3
+ import type { JsonMatch, JsonViewLabels } from "./json-view.js";
4
+ export interface JsonViewProps {
5
+ value: unknown;
6
+ class?: ClassValue;
7
+ labels?: Partial<JsonViewLabels>;
8
+ collapseDepth?: number;
9
+ onselect?: (path: string, value: unknown) => void;
10
+ }
11
+ export interface JsonViewViewport {
12
+ visible: readonly number[];
13
+ line: Snippet<[number]>;
14
+ rowId: (index: number) => string;
15
+ rowHeight: number;
16
+ widthCh: number;
17
+ }
18
+ export interface JsonViewRevealTarget {
19
+ line: number;
20
+ visibleIndex: number;
21
+ match: JsonMatch;
22
+ columnCh: number;
23
+ }
24
+ export declare const JSON_VIEW_ROW_HEIGHT = 22;
25
+ type Props = JsonViewProps & {
26
+ viewport: Snippet<[JsonViewViewport]>;
27
+ reveal: (target: JsonViewRevealTarget) => void | Promise<void>;
28
+ };
29
+ declare const JsonViewFrame: import("svelte").Component<Props, {}, "">;
30
+ type JsonViewFrame = ReturnType<typeof JsonViewFrame>;
31
+ export default JsonViewFrame;