@svadmin/ui 0.75.0 → 0.76.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 (41) hide show
  1. package/dist/app.css +14 -0
  2. package/dist/app.theme.css +14 -0
  3. package/dist/components/AppFooter.svelte +137 -0
  4. package/dist/components/AppFooter.svelte.d.ts +25 -0
  5. package/dist/components/CodeEditor.svelte +145 -0
  6. package/dist/components/CodeEditor.svelte.d.ts +26 -0
  7. package/dist/components/HighlightText.svelte +46 -0
  8. package/dist/components/HighlightText.svelte.d.ts +13 -0
  9. package/dist/components/JsonEditor.svelte +182 -0
  10. package/dist/components/JsonEditor.svelte.d.ts +22 -0
  11. package/dist/components/Marquee.svelte +104 -0
  12. package/dist/components/Marquee.svelte.d.ts +20 -0
  13. package/dist/components/MediaPlayer.svelte +104 -0
  14. package/dist/components/MediaPlayer.svelte.d.ts +21 -0
  15. package/dist/components/PhoneInput.svelte +157 -0
  16. package/dist/components/PhoneInput.svelte.d.ts +26 -0
  17. package/dist/components/QRCode.svelte +88 -0
  18. package/dist/components/QRCode.svelte.d.ts +22 -0
  19. package/dist/components/ResizableGrid.svelte +252 -0
  20. package/dist/components/ResizableGrid.svelte.d.ts +34 -0
  21. package/dist/components/TopProgressBar.svelte +90 -0
  22. package/dist/components/TopProgressBar.svelte.d.ts +15 -0
  23. package/dist/components/browser.svelte.d.ts +65 -0
  24. package/dist/components/browser.svelte.js +228 -0
  25. package/dist/components/charts/AreaChart.svelte +78 -0
  26. package/dist/components/charts/AreaChart.svelte.d.ts +17 -0
  27. package/dist/components/charts/ScatterChart.svelte +82 -0
  28. package/dist/components/charts/ScatterChart.svelte.d.ts +17 -0
  29. package/dist/components/charts/index.d.ts +2 -0
  30. package/dist/components/charts/index.js +2 -0
  31. package/dist/components/code-editor-presets.d.ts +14 -0
  32. package/dist/components/code-editor-presets.js +14 -0
  33. package/dist/components/highlight.d.ts +16 -0
  34. package/dist/components/highlight.js +68 -0
  35. package/dist/components/media.svelte.d.ts +27 -0
  36. package/dist/components/media.svelte.js +60 -0
  37. package/dist/components/phone-dial-codes.d.ts +19 -0
  38. package/dist/components/phone-dial-codes.js +112 -0
  39. package/dist/index.d.ts +19 -1
  40. package/dist/index.js +14 -1
  41. package/package.json +64 -4
package/dist/app.css CHANGED
@@ -5539,6 +5539,7 @@
5539
5539
  --leading-tight: 1.25;
5540
5540
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
5541
5541
  --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
5542
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
5542
5543
  --default-transition-duration: 150ms;
5543
5544
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
5544
5545
  }
@@ -6019,6 +6020,10 @@
6019
6020
  .opacity-50 {
6020
6021
  opacity: 50%;
6021
6022
  }
6023
+ .shadow {
6024
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
6025
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6026
+ }
6022
6027
  .shadow-sm {
6023
6028
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
6024
6029
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -6045,6 +6050,10 @@
6045
6050
  transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
6046
6051
  transition-duration: var(--tw-duration, var(--default-transition-duration));
6047
6052
  }
6053
+ .ease-in-out {
6054
+ --tw-ease: var(--ease-in-out);
6055
+ transition-timing-function: var(--ease-in-out);
6056
+ }
6048
6057
  .select-none {
6049
6058
  -webkit-user-select: none;
6050
6059
  user-select: none;
@@ -6389,6 +6398,10 @@
6389
6398
  syntax: "*";
6390
6399
  inherits: false;
6391
6400
  }
6401
+ @property --tw-ease {
6402
+ syntax: "*";
6403
+ inherits: false;
6404
+ }
6392
6405
  @layer properties {
6393
6406
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
6394
6407
  *, ::before, ::after, ::backdrop {
@@ -6434,6 +6447,7 @@
6434
6447
  --tw-drop-shadow-color: initial;
6435
6448
  --tw-drop-shadow-alpha: 100%;
6436
6449
  --tw-drop-shadow-size: initial;
6450
+ --tw-ease: initial;
6437
6451
  }
6438
6452
  }
6439
6453
  }
@@ -5539,6 +5539,7 @@
5539
5539
  --leading-tight: 1.25;
5540
5540
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
5541
5541
  --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
5542
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
5542
5543
  --default-transition-duration: 150ms;
5543
5544
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
5544
5545
  }
@@ -6019,6 +6020,10 @@
6019
6020
  .opacity-50 {
6020
6021
  opacity: 50%;
6021
6022
  }
6023
+ .shadow {
6024
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
6025
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6026
+ }
6022
6027
  .shadow-sm {
6023
6028
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
6024
6029
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -6045,6 +6050,10 @@
6045
6050
  transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
6046
6051
  transition-duration: var(--tw-duration, var(--default-transition-duration));
6047
6052
  }
6053
+ .ease-in-out {
6054
+ --tw-ease: var(--ease-in-out);
6055
+ transition-timing-function: var(--ease-in-out);
6056
+ }
6048
6057
  .select-none {
6049
6058
  -webkit-user-select: none;
6050
6059
  user-select: none;
@@ -6389,6 +6398,10 @@
6389
6398
  syntax: "*";
6390
6399
  inherits: false;
6391
6400
  }
6401
+ @property --tw-ease {
6402
+ syntax: "*";
6403
+ inherits: false;
6404
+ }
6392
6405
  @layer properties {
6393
6406
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
6394
6407
  *, ::before, ::after, ::backdrop {
@@ -6434,6 +6447,7 @@
6434
6447
  --tw-drop-shadow-color: initial;
6435
6448
  --tw-drop-shadow-alpha: 100%;
6436
6449
  --tw-drop-shadow-size: initial;
6450
+ --tw-ease: initial;
6437
6451
  }
6438
6452
  }
6439
6453
  }
@@ -0,0 +1,137 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils.js';
4
+
5
+ export interface FooterLink {
6
+ label: string;
7
+ href?: string;
8
+ external?: boolean;
9
+ onclick?: () => void;
10
+ }
11
+
12
+ export interface FooterColumn {
13
+ title: string;
14
+ links: FooterLink[];
15
+ }
16
+
17
+ interface Props {
18
+ /** Bottom-line copyright text. */
19
+ copyright?: string;
20
+ /** Flat links rendered in the bottom bar. */
21
+ links?: FooterLink[];
22
+ /** Link columns rendered above the bottom bar. */
23
+ columns?: FooterColumn[];
24
+ ariaLabel?: string;
25
+ class?: string;
26
+ children?: Snippet;
27
+ }
28
+
29
+ let {
30
+ copyright,
31
+ links = [],
32
+ columns = [],
33
+ ariaLabel = 'Footer',
34
+ class: className = '',
35
+ children,
36
+ }: Props = $props();
37
+ </script>
38
+
39
+ <footer class={cn('svadmin-app-footer', className)} data-slot="app-footer" aria-label={ariaLabel}>
40
+ {#if columns.length}
41
+ <div class="svadmin-app-footer__columns">
42
+ {#each columns as column (column.title)}
43
+ <nav class="svadmin-app-footer__column" aria-label={column.title}>
44
+ <h2 class="svadmin-app-footer__heading">{column.title}</h2>
45
+ <ul class="svadmin-app-footer__list">
46
+ {#each column.links as link (link.label)}
47
+ <li>
48
+ {#if link.href}
49
+ <a
50
+ class="svadmin-app-footer__link"
51
+ href={link.href}
52
+ rel={link.external ? 'noreferrer noopener' : undefined}
53
+ target={link.external ? '_blank' : undefined}
54
+ >{link.label}</a>
55
+ {:else}
56
+ <button class="svadmin-app-footer__link" type="button" onclick={() => link.onclick?.()}>{link.label}</button>
57
+ {/if}
58
+ </li>
59
+ {/each}
60
+ </ul>
61
+ </nav>
62
+ {/each}
63
+ </div>
64
+ {/if}
65
+
66
+ {#if children}<div class="svadmin-app-footer__content">{@render children()}</div>{/if}
67
+
68
+ {#if copyright || links.length}
69
+ <div class="svadmin-app-footer__bar">
70
+ {#if copyright}<span class="svadmin-app-footer__copyright">{copyright}</span>{/if}
71
+ {#if links.length}
72
+ <ul class="svadmin-app-footer__list svadmin-app-footer__list--inline">
73
+ {#each links as link (link.label)}
74
+ <li>
75
+ {#if link.href}
76
+ <a
77
+ class="svadmin-app-footer__link"
78
+ href={link.href}
79
+ rel={link.external ? 'noreferrer noopener' : undefined}
80
+ target={link.external ? '_blank' : undefined}
81
+ >{link.label}</a>
82
+ {:else}
83
+ <button class="svadmin-app-footer__link" type="button" onclick={() => link.onclick?.()}>{link.label}</button>
84
+ {/if}
85
+ </li>
86
+ {/each}
87
+ </ul>
88
+ {/if}
89
+ </div>
90
+ {/if}
91
+ </footer>
92
+
93
+ <style>
94
+ .svadmin-app-footer {
95
+ display: grid;
96
+ gap: 1.5rem;
97
+ border-block-start: 1px solid var(--border, currentColor);
98
+ padding: 1.5rem;
99
+ background: var(--card, var(--background, transparent));
100
+ color: var(--muted-foreground, currentColor);
101
+ font-size: 0.8125rem;
102
+ }
103
+
104
+ .svadmin-app-footer__columns {
105
+ display: grid;
106
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
107
+ gap: 1.5rem;
108
+ }
109
+
110
+ .svadmin-app-footer__column { display: grid; gap: 0.625rem; }
111
+ .svadmin-app-footer__heading { margin: 0; color: var(--foreground, currentColor); font-size: 0.8125rem; font-weight: 600; }
112
+ .svadmin-app-footer__list { display: grid; gap: 0.375rem; margin: 0; padding: 0; list-style: none; }
113
+ .svadmin-app-footer__list--inline { display: flex; flex-wrap: wrap; gap: 1rem; }
114
+
115
+ .svadmin-app-footer__link {
116
+ border: 0;
117
+ padding: 0;
118
+ background: transparent;
119
+ color: inherit;
120
+ font: inherit;
121
+ text-decoration: none;
122
+ cursor: pointer;
123
+ }
124
+ .svadmin-app-footer__link:hover { color: var(--foreground, currentColor); text-decoration: underline; text-underline-offset: 0.2em; }
125
+ .svadmin-app-footer__link:focus-visible { outline: 2px solid var(--ring, currentColor); outline-offset: 2px; }
126
+
127
+ .svadmin-app-footer__bar {
128
+ display: flex;
129
+ flex-wrap: wrap;
130
+ align-items: center;
131
+ justify-content: space-between;
132
+ gap: 0.75rem;
133
+ border-block-start: 1px solid var(--border, currentColor);
134
+ padding-block-start: 1rem;
135
+ color: var(--muted-foreground, currentColor);
136
+ }
137
+ </style>
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface FooterLink {
3
+ label: string;
4
+ href?: string;
5
+ external?: boolean;
6
+ onclick?: () => void;
7
+ }
8
+ export interface FooterColumn {
9
+ title: string;
10
+ links: FooterLink[];
11
+ }
12
+ interface Props {
13
+ /** Bottom-line copyright text. */
14
+ copyright?: string;
15
+ /** Flat links rendered in the bottom bar. */
16
+ links?: FooterLink[];
17
+ /** Link columns rendered above the bottom bar. */
18
+ columns?: FooterColumn[];
19
+ ariaLabel?: string;
20
+ class?: string;
21
+ children?: Snippet;
22
+ }
23
+ declare const AppFooter: import("svelte").Component<Props, {}, "">;
24
+ type AppFooter = ReturnType<typeof AppFooter>;
25
+ export default AppFooter;
@@ -0,0 +1,145 @@
1
+ <script lang="ts">
2
+ import type { Component } from 'svelte';
3
+ import { onMount } from 'svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ /**
7
+ * Thin adapter over the Svelte ecosystem `svelte-codemirror-editor` package.
8
+ * The dependency is optional: if it cannot be loaded, an accessible
9
+ * `<textarea>` fallback keeps the field usable.
10
+ */
11
+ interface Props {
12
+ value?: string;
13
+ placeholder?: string;
14
+ readonly?: boolean;
15
+ editable?: boolean;
16
+ lineNumbers?: boolean;
17
+ lineWrapping?: boolean;
18
+ tabSize?: number;
19
+ /** CodeMirror extensions supplied by the host (languages, themes, …). */
20
+ extensions?: readonly unknown[];
21
+ lang?: unknown;
22
+ theme?: unknown;
23
+ ariaLabel?: string;
24
+ class?: string;
25
+ onchange?: (value: string) => void;
26
+ onready?: (view: unknown) => void;
27
+ }
28
+
29
+ let {
30
+ value = $bindable(''),
31
+ placeholder = 'Write code…',
32
+ readonly = false,
33
+ editable = true,
34
+ lineNumbers = true,
35
+ lineWrapping = true,
36
+ tabSize = 2,
37
+ extensions,
38
+ lang,
39
+ theme,
40
+ ariaLabel = 'Code editor',
41
+ class: className = '',
42
+ onchange,
43
+ onready,
44
+ }: Props = $props();
45
+
46
+ let Editor = $state<Component<Record<string, unknown>> | null>(null);
47
+ let unavailable = $state(false);
48
+
49
+ onMount(() => {
50
+ let active = true;
51
+ import('svelte-codemirror-editor')
52
+ .then((module) => {
53
+ if (active) Editor = module.default as unknown as Component<Record<string, unknown>>;
54
+ })
55
+ .catch(() => {
56
+ if (active) unavailable = true;
57
+ });
58
+ return () => {
59
+ active = false;
60
+ };
61
+ });
62
+
63
+ const editorProps = $derived({
64
+ value,
65
+ placeholder,
66
+ readonly,
67
+ editable,
68
+ lineNumbers,
69
+ lineWrapping,
70
+ tabSize,
71
+ ...(extensions ? { extensions: [...extensions] } : {}),
72
+ ...(lang ? { lang } : {}),
73
+ ...(theme ? { theme } : {}),
74
+ ...(onready ? { onready } : {}),
75
+ onchange: (next: string) => {
76
+ value = next;
77
+ onchange?.(next);
78
+ },
79
+ });
80
+
81
+ const editorState = $derived(Editor ? 'ready' : unavailable ? 'unavailable' : 'loading');
82
+ </script>
83
+
84
+ <div
85
+ class={cn('svadmin-code-editor', className)}
86
+ data-slot="code-editor"
87
+ data-state={editorState}
88
+ >
89
+ {#if Editor}
90
+ <Editor {...editorProps} />
91
+ {:else}
92
+ <textarea
93
+ class="svadmin-code-editor__fallback"
94
+ bind:value
95
+ {placeholder}
96
+ {readonly}
97
+ disabled={!editable || readonly}
98
+ aria-label={ariaLabel}
99
+ spellcheck="false"
100
+ autocapitalize="off"
101
+ autocomplete="off"
102
+ wrap={lineWrapping ? 'soft' : 'off'}
103
+ oninput={(event) => onchange?.((event.currentTarget as HTMLTextAreaElement).value)}
104
+ ></textarea>
105
+ {#if unavailable}
106
+ <p class="svadmin-code-editor__hint" role="status">
107
+ Install <code>svelte-codemirror-editor</code> and <code>codemirror</code> to enable syntax highlighting.
108
+ </p>
109
+ {/if}
110
+ {/if}
111
+ </div>
112
+
113
+ <style>
114
+ .svadmin-code-editor {
115
+ overflow: hidden;
116
+ border: 1px solid var(--border, currentColor);
117
+ border-radius: min(var(--radius, 0.5rem), 0.5rem);
118
+ background: var(--card, var(--background, transparent));
119
+ color: var(--card-foreground, var(--foreground, currentColor));
120
+ }
121
+
122
+ .svadmin-code-editor:focus-within { outline: 2px solid var(--ring, currentColor); outline-offset: 1px; }
123
+ .svadmin-code-editor__fallback {
124
+ display: block;
125
+ width: 100%;
126
+ min-height: 8rem;
127
+ border: 0;
128
+ padding: 0.75rem;
129
+ background: transparent;
130
+ color: inherit;
131
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
132
+ font-size: 0.8125rem;
133
+ line-height: 1.6;
134
+ resize: vertical;
135
+ }
136
+ .svadmin-code-editor__fallback:focus { outline: none; }
137
+ .svadmin-code-editor__hint {
138
+ margin: 0;
139
+ border-top: 1px solid var(--border, currentColor);
140
+ padding: 0.5rem 0.75rem;
141
+ color: var(--muted-foreground, currentColor);
142
+ font-size: 0.75rem;
143
+ }
144
+ .svadmin-code-editor__hint code { color: var(--foreground, currentColor); }
145
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { Component } from 'svelte';
2
+ /**
3
+ * Thin adapter over the Svelte ecosystem `svelte-codemirror-editor` package.
4
+ * The dependency is optional: if it cannot be loaded, an accessible
5
+ * `<textarea>` fallback keeps the field usable.
6
+ */
7
+ interface Props {
8
+ value?: string;
9
+ placeholder?: string;
10
+ readonly?: boolean;
11
+ editable?: boolean;
12
+ lineNumbers?: boolean;
13
+ lineWrapping?: boolean;
14
+ tabSize?: number;
15
+ /** CodeMirror extensions supplied by the host (languages, themes, …). */
16
+ extensions?: readonly unknown[];
17
+ lang?: unknown;
18
+ theme?: unknown;
19
+ ariaLabel?: string;
20
+ class?: string;
21
+ onchange?: (value: string) => void;
22
+ onready?: (view: unknown) => void;
23
+ }
24
+ declare const CodeEditor: Component<Props, {}, "value">;
25
+ type CodeEditor = ReturnType<typeof CodeEditor>;
26
+ export default CodeEditor;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+ import { splitHighlights } from './highlight.js';
4
+
5
+ interface Props {
6
+ text: string;
7
+ /** One term or a list of terms to highlight. */
8
+ query?: string | readonly string[];
9
+ caseSensitive?: boolean;
10
+ wholeWord?: boolean;
11
+ class?: string;
12
+ /** Overrides the `<mark>` styling without leaving the semantic token system. */
13
+ highlightClass?: string;
14
+ }
15
+
16
+ let {
17
+ text,
18
+ query,
19
+ caseSensitive = false,
20
+ wholeWord = false,
21
+ class: className = '',
22
+ highlightClass = '',
23
+ }: Props = $props();
24
+
25
+ const segments = $derived(
26
+ splitHighlights(text, query, { caseSensitive, wholeWord }),
27
+ );
28
+ </script>
29
+
30
+ <span class={cn('svadmin-highlight-text', className)} data-slot="highlight-text">
31
+ {#each segments as segment, index (index)}{#if segment.match}<mark class={cn('svadmin-highlight-text__mark', highlightClass)}>{segment.text}</mark>{:else}{segment.text}{/if}{/each}
32
+ </span>
33
+
34
+ <style>
35
+ .svadmin-highlight-text {
36
+ color: inherit;
37
+ overflow-wrap: anywhere;
38
+ }
39
+
40
+ .svadmin-highlight-text__mark {
41
+ border-radius: 0.2rem;
42
+ padding-inline: 0.1rem;
43
+ background: color-mix(in oklch, var(--warning, var(--primary, currentColor)) 28%, transparent);
44
+ color: var(--foreground, currentColor);
45
+ }
46
+ </style>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ text: string;
3
+ /** One term or a list of terms to highlight. */
4
+ query?: string | readonly string[];
5
+ caseSensitive?: boolean;
6
+ wholeWord?: boolean;
7
+ class?: string;
8
+ /** Overrides the `<mark>` styling without leaving the semantic token system. */
9
+ highlightClass?: string;
10
+ }
11
+ declare const HighlightText: import("svelte").Component<Props, {}, "">;
12
+ type HighlightText = ReturnType<typeof HighlightText>;
13
+ export default HighlightText;
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import type { Component } from 'svelte';
3
+ import { onMount } from 'svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ /**
7
+ * Thin adapter over the Svelte ecosystem `svelte-jsoneditor` package. The
8
+ * dependency is optional: if it cannot be loaded, a validating `<textarea>`
9
+ * fallback keeps the field usable.
10
+ */
11
+ type JsonMode = 'tree' | 'text' | 'table';
12
+
13
+ interface Props {
14
+ /** Parsed JSON value. */
15
+ value?: unknown;
16
+ mode?: JsonMode;
17
+ readonly?: boolean;
18
+ /** Height of the editor surface (CSS length). */
19
+ height?: string;
20
+ ariaLabel?: string;
21
+ class?: string;
22
+ onchange?: (value: unknown) => void;
23
+ onerror?: (message: string) => void;
24
+ }
25
+
26
+ let {
27
+ value = $bindable(undefined),
28
+ mode = 'tree',
29
+ readonly = false,
30
+ height = '22rem',
31
+ ariaLabel = 'JSON editor',
32
+ class: className = '',
33
+ onchange,
34
+ onerror,
35
+ }: Props = $props();
36
+
37
+ const generatedId = $props.id();
38
+ const errorId = `${generatedId}-error`;
39
+
40
+ let Editor = $state<Component<Record<string, unknown>> | null>(null);
41
+ let unavailable = $state(false);
42
+ let error = $state('');
43
+
44
+ function format(input: unknown): string {
45
+ if (input === undefined) return '';
46
+ try {
47
+ return JSON.stringify(input, null, 2) ?? '';
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ let text = $state(format(value));
54
+
55
+ onMount(() => {
56
+ let active = true;
57
+ import('svelte-jsoneditor')
58
+ .then((module) => {
59
+ if (active) Editor = module.JSONEditor as unknown as Component<Record<string, unknown>>;
60
+ })
61
+ .catch(() => {
62
+ if (active) unavailable = true;
63
+ });
64
+ return () => {
65
+ active = false;
66
+ };
67
+ });
68
+
69
+ function handleEditorChange(updated: { content?: { json?: unknown; text?: string } }): void {
70
+ const content = updated?.content ?? {};
71
+ const next = 'json' in content ? content.json : content.text;
72
+ value = next;
73
+ error = '';
74
+ onchange?.(next);
75
+ }
76
+
77
+ function handleFallbackInput(next: string): void {
78
+ text = next;
79
+ if (next.trim() === '') {
80
+ error = '';
81
+ value = undefined;
82
+ onchange?.(undefined);
83
+ return;
84
+ }
85
+ try {
86
+ const parsed: unknown = JSON.parse(next);
87
+ error = '';
88
+ value = parsed;
89
+ onchange?.(parsed);
90
+ } catch (cause) {
91
+ const message = cause instanceof Error ? cause.message : 'Invalid JSON';
92
+ error = message;
93
+ onerror?.(message);
94
+ }
95
+ }
96
+
97
+ const editorProps = $derived({
98
+ content: { json: value },
99
+ mode,
100
+ readOnly: readonly,
101
+ ariaLabel,
102
+ mainMenuBar: !readonly,
103
+ navigationBar: !readonly,
104
+ statusBar: true,
105
+ onChange: handleEditorChange,
106
+ });
107
+
108
+ const editorState = $derived(Editor ? 'ready' : unavailable ? 'unavailable' : 'loading');
109
+ </script>
110
+
111
+ <div
112
+ class={cn('svadmin-json-editor', className)}
113
+ data-slot="json-editor"
114
+ data-state={editorState}
115
+ >
116
+ {#if Editor}
117
+ <div class="svadmin-json-editor__surface" style={`height: ${height};`}>
118
+ <Editor {...editorProps} />
119
+ </div>
120
+ {:else}
121
+ <textarea
122
+ class="svadmin-json-editor__fallback"
123
+ style={`min-height: ${height};`}
124
+ bind:value={text}
125
+ {readonly}
126
+ aria-label={ariaLabel}
127
+ spellcheck="false"
128
+ autocapitalize="off"
129
+ autocomplete="off"
130
+ aria-invalid={error ? 'true' : undefined}
131
+ aria-describedby={error ? errorId : undefined}
132
+ oninput={(event) => handleFallbackInput((event.currentTarget as HTMLTextAreaElement).value)}
133
+ ></textarea>
134
+ {#if error}<p id={errorId} class="svadmin-json-editor__error" role="alert">{error}</p>{/if}
135
+ {#if unavailable}
136
+ <p class="svadmin-json-editor__hint" role="status">
137
+ Install <code>svelte-jsoneditor</code> to enable the structured editor.
138
+ </p>
139
+ {/if}
140
+ {/if}
141
+ </div>
142
+
143
+ <style>
144
+ .svadmin-json-editor {
145
+ overflow: hidden;
146
+ border: 1px solid var(--border, currentColor);
147
+ border-radius: min(var(--radius, 0.5rem), 0.5rem);
148
+ background: var(--card, var(--background, transparent));
149
+ color: var(--card-foreground, var(--foreground, currentColor));
150
+ }
151
+
152
+ .svadmin-json-editor:focus-within { outline: 2px solid var(--ring, currentColor); outline-offset: 1px; }
153
+ .svadmin-json-editor__surface { width: 100%; }
154
+ .svadmin-json-editor__fallback {
155
+ display: block;
156
+ width: 100%;
157
+ border: 0;
158
+ padding: 0.75rem;
159
+ background: transparent;
160
+ color: inherit;
161
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
162
+ font-size: 0.8125rem;
163
+ line-height: 1.6;
164
+ resize: vertical;
165
+ }
166
+ .svadmin-json-editor__fallback:focus { outline: none; }
167
+ .svadmin-json-editor__error {
168
+ margin: 0;
169
+ border-top: 1px solid var(--destructive, currentColor);
170
+ padding: 0.5rem 0.75rem;
171
+ color: var(--destructive, currentColor);
172
+ font-size: 0.75rem;
173
+ }
174
+ .svadmin-json-editor__hint {
175
+ margin: 0;
176
+ border-top: 1px solid var(--border, currentColor);
177
+ padding: 0.5rem 0.75rem;
178
+ color: var(--muted-foreground, currentColor);
179
+ font-size: 0.75rem;
180
+ }
181
+ .svadmin-json-editor__hint code { color: var(--foreground, currentColor); }
182
+ </style>
@@ -0,0 +1,22 @@
1
+ import type { Component } from 'svelte';
2
+ /**
3
+ * Thin adapter over the Svelte ecosystem `svelte-jsoneditor` package. The
4
+ * dependency is optional: if it cannot be loaded, a validating `<textarea>`
5
+ * fallback keeps the field usable.
6
+ */
7
+ type JsonMode = 'tree' | 'text' | 'table';
8
+ interface Props {
9
+ /** Parsed JSON value. */
10
+ value?: unknown;
11
+ mode?: JsonMode;
12
+ readonly?: boolean;
13
+ /** Height of the editor surface (CSS length). */
14
+ height?: string;
15
+ ariaLabel?: string;
16
+ class?: string;
17
+ onchange?: (value: unknown) => void;
18
+ onerror?: (message: string) => void;
19
+ }
20
+ declare const JsonEditor: Component<Props, {}, "value">;
21
+ type JsonEditor = ReturnType<typeof JsonEditor>;
22
+ export default JsonEditor;