@svadmin/ui 0.74.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 (65) hide show
  1. package/dist/app.css +14 -0
  2. package/dist/app.theme.css +14 -0
  3. package/dist/components/AdminApp.svelte +5 -5
  4. package/dist/components/AppFooter.svelte +137 -0
  5. package/dist/components/AppFooter.svelte.d.ts +25 -0
  6. package/dist/components/CodeEditor.svelte +145 -0
  7. package/dist/components/CodeEditor.svelte.d.ts +26 -0
  8. package/dist/components/DatePicker.svelte +2 -2
  9. package/dist/components/DatePicker.svelte.d.ts +1 -1
  10. package/dist/components/DateRangePicker.svelte +2 -2
  11. package/dist/components/DateRangePicker.svelte.d.ts +1 -1
  12. package/dist/components/DateTimeInput.svelte +18 -18
  13. package/dist/components/DateTimeInput.svelte.d.ts +18 -18
  14. package/dist/components/DateTimePicker.svelte +2 -2
  15. package/dist/components/DateTimePicker.svelte.d.ts +1 -1
  16. package/dist/components/FieldRenderer.svelte +1 -1
  17. package/dist/components/HighlightText.svelte +46 -0
  18. package/dist/components/HighlightText.svelte.d.ts +13 -0
  19. package/dist/components/JsonEditor.svelte +182 -0
  20. package/dist/components/JsonEditor.svelte.d.ts +22 -0
  21. package/dist/components/LazyPage.svelte +3 -3
  22. package/dist/components/LazyPage.svelte.d.ts +2 -2
  23. package/dist/components/Marquee.svelte +104 -0
  24. package/dist/components/Marquee.svelte.d.ts +20 -0
  25. package/dist/components/MediaPlayer.svelte +104 -0
  26. package/dist/components/MediaPlayer.svelte.d.ts +21 -0
  27. package/dist/components/PasswordInput.svelte +3 -3
  28. package/dist/components/PasswordInput.svelte.d.ts +2 -1
  29. package/dist/components/PhoneInput.svelte +157 -0
  30. package/dist/components/PhoneInput.svelte.d.ts +26 -0
  31. package/dist/components/QRCode.svelte +88 -0
  32. package/dist/components/QRCode.svelte.d.ts +22 -0
  33. package/dist/components/ResizableGrid.svelte +252 -0
  34. package/dist/components/ResizableGrid.svelte.d.ts +34 -0
  35. package/dist/components/ResourceOperationsPage.svelte +5 -1
  36. package/dist/components/TimePicker.svelte +2 -2
  37. package/dist/components/TimePicker.svelte.d.ts +1 -1
  38. package/dist/components/TooltipButton.svelte +3 -4
  39. package/dist/components/TopProgressBar.svelte +90 -0
  40. package/dist/components/TopProgressBar.svelte.d.ts +15 -0
  41. package/dist/components/browser.svelte.d.ts +65 -0
  42. package/dist/components/browser.svelte.js +228 -0
  43. package/dist/components/charts/AreaChart.svelte +78 -0
  44. package/dist/components/charts/AreaChart.svelte.d.ts +17 -0
  45. package/dist/components/charts/ScatterChart.svelte +82 -0
  46. package/dist/components/charts/ScatterChart.svelte.d.ts +17 -0
  47. package/dist/components/charts/index.d.ts +2 -0
  48. package/dist/components/charts/index.js +2 -0
  49. package/dist/components/code-editor-presets.d.ts +14 -0
  50. package/dist/components/code-editor-presets.js +14 -0
  51. package/dist/components/fields/MarkdownField.svelte +14 -5
  52. package/dist/components/highlight.d.ts +16 -0
  53. package/dist/components/highlight.js +68 -0
  54. package/dist/components/media.svelte.d.ts +27 -0
  55. package/dist/components/media.svelte.js +60 -0
  56. package/dist/components/phone-dial-codes.d.ts +19 -0
  57. package/dist/components/phone-dial-codes.js +112 -0
  58. package/dist/components/ui/DesignSystemShowcase.svelte +1 -1
  59. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -2
  60. package/dist/editor-config.svelte.d.ts +4 -2
  61. package/dist/index.d.ts +19 -1
  62. package/dist/index.js +14 -1
  63. package/dist/styles/recipes.css +1262 -0
  64. package/dist/types.d.ts +4 -4
  65. package/package.json +64 -4
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+ import {
4
+ findDialCodeByCountry,
5
+ PHONE_DIAL_CODES,
6
+ toE164,
7
+ type PhoneDialCode,
8
+ } from './phone-dial-codes.js';
9
+
10
+ interface Props {
11
+ /** ISO 3166-1 alpha-2 country code. */
12
+ country?: string;
13
+ /** National number without the dialling prefix. */
14
+ nationalNumber?: string;
15
+ countries?: readonly PhoneDialCode[];
16
+ id?: string;
17
+ name?: string;
18
+ placeholder?: string;
19
+ disabled?: boolean;
20
+ readonly?: boolean;
21
+ required?: boolean;
22
+ invalid?: boolean;
23
+ describedby?: string;
24
+ ariaLabel?: string;
25
+ class?: string;
26
+ onchange?: (payload: { country: string; nationalNumber: string; e164: string }) => void;
27
+ }
28
+
29
+ let {
30
+ country = $bindable('CN'),
31
+ nationalNumber = $bindable(''),
32
+ countries = PHONE_DIAL_CODES,
33
+ id,
34
+ name,
35
+ placeholder = 'Phone number',
36
+ disabled = false,
37
+ readonly = false,
38
+ required = false,
39
+ invalid = false,
40
+ describedby,
41
+ ariaLabel = 'Phone number',
42
+ class: className = '',
43
+ onchange,
44
+ }: Props = $props();
45
+
46
+ const entry = $derived(findDialCodeByCountry(country, countries));
47
+ const dial = $derived(entry?.dial ?? '');
48
+
49
+ function emit(): void {
50
+ onchange?.({
51
+ country,
52
+ nationalNumber,
53
+ e164: toE164(country, nationalNumber, countries),
54
+ });
55
+ }
56
+ </script>
57
+
58
+ <div
59
+ class={cn('svadmin-phone-input', className)}
60
+ data-slot="phone-input"
61
+ data-invalid={invalid ? 'true' : undefined}
62
+ data-disabled={disabled ? 'true' : undefined}
63
+ >
64
+ <select
65
+ class="svadmin-phone-input__country"
66
+ bind:value={country}
67
+ {disabled}
68
+ aria-label={`${ariaLabel}: country code`}
69
+ onchange={emit}
70
+ >
71
+ {#each countries as option (option.country)}
72
+ <option value={option.country}>{option.name} (+{option.dial})</option>
73
+ {/each}
74
+ </select>
75
+
76
+ <div class="svadmin-phone-input__control">
77
+ {#if dial}<span class="svadmin-phone-input__dial" aria-hidden="true">+{dial}</span>{/if}
78
+ <input
79
+ class="svadmin-phone-input__field"
80
+ type="tel"
81
+ inputmode="tel"
82
+ autocomplete="tel-national"
83
+ {id}
84
+ {name}
85
+ bind:value={nationalNumber}
86
+ {placeholder}
87
+ {disabled}
88
+ {readonly}
89
+ {required}
90
+ aria-label={ariaLabel}
91
+ aria-invalid={invalid ? 'true' : undefined}
92
+ aria-describedby={describedby}
93
+ oninput={emit}
94
+ />
95
+ </div>
96
+ </div>
97
+
98
+ <style>
99
+ .svadmin-phone-input {
100
+ display: flex;
101
+ align-items: stretch;
102
+ border: 1px solid var(--border, currentColor);
103
+ border-radius: min(var(--radius, 0.5rem), 0.5rem);
104
+ background: var(--background, transparent);
105
+ color: var(--foreground, currentColor);
106
+ }
107
+
108
+ .svadmin-phone-input:focus-within { outline: 2px solid var(--ring, currentColor); outline-offset: 1px; }
109
+ .svadmin-phone-input[data-invalid='true'] { border-color: var(--destructive, currentColor); }
110
+ .svadmin-phone-input[data-disabled='true'] { cursor: not-allowed; opacity: 0.6; }
111
+
112
+ .svadmin-phone-input__country {
113
+ flex: none;
114
+ max-width: 11rem;
115
+ border: 0;
116
+ border-inline-end: 1px solid var(--border, currentColor);
117
+ border-start-start-radius: inherit;
118
+ border-end-start-radius: inherit;
119
+ padding: 0.5rem 0.625rem;
120
+ background: var(--muted, transparent);
121
+ color: inherit;
122
+ font: inherit;
123
+ font-size: 0.8125rem;
124
+ cursor: pointer;
125
+ }
126
+
127
+ .svadmin-phone-input__country:focus-visible { outline: none; }
128
+
129
+ .svadmin-phone-input__control {
130
+ display: flex;
131
+ min-width: 0;
132
+ flex: 1;
133
+ align-items: center;
134
+ gap: 0.375rem;
135
+ padding-inline: 0.625rem;
136
+ }
137
+
138
+ .svadmin-phone-input__dial {
139
+ flex: none;
140
+ color: var(--muted-foreground, currentColor);
141
+ font-size: 0.8125rem;
142
+ font-variant-numeric: tabular-nums;
143
+ }
144
+
145
+ .svadmin-phone-input__field {
146
+ width: 100%;
147
+ min-width: 0;
148
+ border: 0;
149
+ padding-block: 0.5rem;
150
+ background: transparent;
151
+ color: inherit;
152
+ font: inherit;
153
+ font-size: 0.875rem;
154
+ }
155
+
156
+ .svadmin-phone-input__field:focus { outline: none; }
157
+ </style>
@@ -0,0 +1,26 @@
1
+ import { type PhoneDialCode } from './phone-dial-codes.js';
2
+ interface Props {
3
+ /** ISO 3166-1 alpha-2 country code. */
4
+ country?: string;
5
+ /** National number without the dialling prefix. */
6
+ nationalNumber?: string;
7
+ countries?: readonly PhoneDialCode[];
8
+ id?: string;
9
+ name?: string;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ readonly?: boolean;
13
+ required?: boolean;
14
+ invalid?: boolean;
15
+ describedby?: string;
16
+ ariaLabel?: string;
17
+ class?: string;
18
+ onchange?: (payload: {
19
+ country: string;
20
+ nationalNumber: string;
21
+ e164: string;
22
+ }) => void;
23
+ }
24
+ declare const PhoneInput: import("svelte").Component<Props, {}, "country" | "nationalNumber">;
25
+ type PhoneInput = ReturnType<typeof PhoneInput>;
26
+ export default PhoneInput;
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import QRCodeLib from 'qrcode';
3
+ import { cn } from '../utils.js';
4
+
5
+ /**
6
+ * Renders a QR code from the Svelte ecosystem `qrcode` package as a crisp
7
+ * SVG. No canvas is required, so it is safe under SSR and test DOMs.
8
+ */
9
+ interface Props {
10
+ /** Content to encode. Empty content renders a neutral placeholder. */
11
+ value: string;
12
+ /** Rendered size in pixels. */
13
+ size?: number;
14
+ /** Quiet-zone width in modules. Keep at least 4 for reliable scanning. */
15
+ margin?: number;
16
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
17
+ /** Module color; defaults to the semantic foreground token. */
18
+ color?: string;
19
+ /** Background color; defaults to the semantic background token. */
20
+ background?: string;
21
+ ariaLabel?: string;
22
+ class?: string;
23
+ }
24
+
25
+ let {
26
+ value,
27
+ size = 160,
28
+ margin = 4,
29
+ errorCorrectionLevel = 'M',
30
+ color = 'var(--foreground, black)',
31
+ background = 'var(--background, white)',
32
+ ariaLabel = 'QR code',
33
+ class: className = '',
34
+ }: Props = $props();
35
+
36
+ const matrix = $derived.by(() => {
37
+ if (!value) return null;
38
+ try {
39
+ const qr = QRCodeLib.create(value, { errorCorrectionLevel });
40
+ const moduleCount = qr.modules.size;
41
+ const data = qr.modules.data;
42
+ const total = moduleCount + margin * 2;
43
+ let path = '';
44
+ for (let y = 0; y < moduleCount; y += 1) {
45
+ for (let x = 0; x < moduleCount; x += 1) {
46
+ if (data[y * moduleCount + x]) path += `M${x + margin} ${y + margin}h1v1h-1z`;
47
+ }
48
+ }
49
+ return { total, path };
50
+ } catch {
51
+ return null;
52
+ }
53
+ });
54
+ </script>
55
+
56
+ <figure class={cn('svadmin-qr-code', className)} data-slot="qr-code">
57
+ {#if matrix}
58
+ <svg
59
+ class="svadmin-qr-code__svg"
60
+ width={size}
61
+ height={size}
62
+ viewBox={`0 0 ${matrix.total} ${matrix.total}`}
63
+ role="img"
64
+ aria-label={ariaLabel}
65
+ shape-rendering="crispEdges"
66
+ >
67
+ <rect width={matrix.total} height={matrix.total} fill={background} />
68
+ <path d={matrix.path} fill={color} />
69
+ </svg>
70
+ {:else}
71
+ <p class="svadmin-qr-code__empty" role="status">No QR content.</p>
72
+ {/if}
73
+ </figure>
74
+
75
+ <style>
76
+ .svadmin-qr-code {
77
+ display: inline-flex;
78
+ margin: 0;
79
+ border: 1px solid var(--border, currentColor);
80
+ border-radius: min(var(--radius, 0.5rem), 0.75rem);
81
+ padding: 0.5rem;
82
+ background: var(--card, var(--background, transparent));
83
+ color: var(--card-foreground, var(--foreground, currentColor));
84
+ }
85
+
86
+ .svadmin-qr-code__svg { display: block; border-radius: calc(min(var(--radius, 0.5rem), 0.75rem) - 0.25rem); }
87
+ .svadmin-qr-code__empty { margin: 0; padding: 1rem; color: var(--muted-foreground, currentColor); font-size: 0.8125rem; }
88
+ </style>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Renders a QR code from the Svelte ecosystem `qrcode` package as a crisp
3
+ * SVG. No canvas is required, so it is safe under SSR and test DOMs.
4
+ */
5
+ interface Props {
6
+ /** Content to encode. Empty content renders a neutral placeholder. */
7
+ value: string;
8
+ /** Rendered size in pixels. */
9
+ size?: number;
10
+ /** Quiet-zone width in modules. Keep at least 4 for reliable scanning. */
11
+ margin?: number;
12
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
13
+ /** Module color; defaults to the semantic foreground token. */
14
+ color?: string;
15
+ /** Background color; defaults to the semantic background token. */
16
+ background?: string;
17
+ ariaLabel?: string;
18
+ class?: string;
19
+ }
20
+ declare const QRCode: import("svelte").Component<Props, {}, "">;
21
+ type QRCode = ReturnType<typeof QRCode>;
22
+ export default QRCode;
@@ -0,0 +1,252 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils.js';
4
+
5
+ export interface GridItem {
6
+ id: string;
7
+ /** Column index (0-based). */
8
+ x: number;
9
+ /** Row index (0-based). */
10
+ y: number;
11
+ /** Width in columns. */
12
+ w: number;
13
+ /** Height in rows. */
14
+ h: number;
15
+ }
16
+
17
+ interface Props {
18
+ items: GridItem[];
19
+ /** Number of columns in the grid. */
20
+ cols?: number;
21
+ /** Height of a single row in pixels. */
22
+ rowHeight?: number;
23
+ /** Gap between cells in pixels. */
24
+ gap?: number;
25
+ minW?: number;
26
+ minH?: number;
27
+ /** Enables move/resize handles. */
28
+ editable?: boolean;
29
+ ariaLabel?: string;
30
+ class?: string;
31
+ /** Renders each cell's content. Falls back to `children`. */
32
+ item?: Snippet<[GridItem]>;
33
+ children?: Snippet;
34
+ onchange?: (items: GridItem[]) => void;
35
+ }
36
+
37
+ let {
38
+ items = $bindable([]),
39
+ cols = 12,
40
+ rowHeight = 80,
41
+ gap = 12,
42
+ minW = 1,
43
+ minH = 1,
44
+ editable = true,
45
+ ariaLabel = 'Grid layout',
46
+ class: className = '',
47
+ item: itemSnippet,
48
+ children,
49
+ onchange,
50
+ }: Props = $props();
51
+
52
+ let container = $state<HTMLElement | null>(null);
53
+
54
+ const rows = $derived(Math.max(...items.map((entry) => entry.y + entry.h), 1));
55
+ const height = $derived(`calc(${rows} * (var(--svadmin-grid-row) + ${gap}px) - ${gap}px)`);
56
+
57
+ const clamp = (value: number, min: number, max: number): number =>
58
+ Math.min(Math.max(value, min), Math.max(min, max));
59
+
60
+ function commit(next: GridItem[]): void {
61
+ items = next;
62
+ onchange?.(next);
63
+ }
64
+
65
+ function move(item: GridItem, dxUnits: number, dyUnits: number): void {
66
+ commit(items.map((entry) => entry.id === item.id
67
+ ? {
68
+ ...entry,
69
+ x: clamp(item.x + dxUnits, 0, cols - item.w),
70
+ y: Math.max(0, item.y + dyUnits),
71
+ }
72
+ : entry));
73
+ }
74
+
75
+ function resize(item: GridItem, dw: number, dh: number): void {
76
+ commit(items.map((entry) => entry.id === item.id
77
+ ? {
78
+ ...entry,
79
+ w: clamp(item.w + dw, minW, cols - item.x),
80
+ h: Math.max(minH, item.h + dh),
81
+ }
82
+ : entry));
83
+ }
84
+
85
+ function startMove(event: PointerEvent, item: GridItem): void {
86
+ if (!editable || event.button !== 0) return;
87
+ event.preventDefault();
88
+
89
+ const handle = event.currentTarget as HTMLElement;
90
+ const rect = container?.getBoundingClientRect();
91
+ const stepX = rect && rect.width > 0 ? (rect.width + gap) / cols : 80;
92
+ const stepY = rowHeight + gap;
93
+ const startX = event.clientX;
94
+ const startY = event.clientY;
95
+ const origin = { x: item.x, y: item.y };
96
+
97
+ const onMove = (moveEvent: PointerEvent): void => {
98
+ move(
99
+ { ...item, x: origin.x, y: origin.y },
100
+ Math.round((moveEvent.clientX - startX) / stepX),
101
+ Math.round((moveEvent.clientY - startY) / stepY),
102
+ );
103
+ };
104
+ onPointerGesture(handle, event.pointerId, onMove);
105
+ }
106
+
107
+ function startResize(event: PointerEvent, item: GridItem): void {
108
+ if (!editable || event.button !== 0) return;
109
+ event.preventDefault();
110
+
111
+ const handle = event.currentTarget as HTMLElement;
112
+ const rect = container?.getBoundingClientRect();
113
+ const stepX = rect && rect.width > 0 ? (rect.width + gap) / cols : 80;
114
+ const stepY = rowHeight + gap;
115
+ const startX = event.clientX;
116
+ const startY = event.clientY;
117
+ const origin = { w: item.w, h: item.h };
118
+
119
+ const onMove = (moveEvent: PointerEvent): void => {
120
+ resize(
121
+ { ...item, w: origin.w, h: origin.h },
122
+ Math.round((moveEvent.clientX - startX) / stepX),
123
+ Math.round((moveEvent.clientY - startY) / stepY),
124
+ );
125
+ };
126
+ onPointerGesture(handle, event.pointerId, onMove);
127
+ }
128
+
129
+ function onPointerGesture(
130
+ handle: HTMLElement,
131
+ pointerId: number,
132
+ onMove: (event: PointerEvent) => void,
133
+ ): void {
134
+ const onUp = (): void => {
135
+ window.removeEventListener('pointermove', onMove);
136
+ window.removeEventListener('pointerup', onUp);
137
+ try {
138
+ handle.releasePointerCapture?.(pointerId);
139
+ } catch {
140
+ // Pointer capture may be unavailable or already released.
141
+ }
142
+ };
143
+ try {
144
+ handle.setPointerCapture?.(pointerId);
145
+ } catch {
146
+ // Pointer capture is optional; window listeners still track the gesture.
147
+ }
148
+ window.addEventListener('pointermove', onMove);
149
+ window.addEventListener('pointerup', onUp);
150
+ }
151
+
152
+ function handleKeydown(event: KeyboardEvent, item: GridItem, mode: 'move' | 'resize'): void {
153
+ if (!editable) return;
154
+ let dx = 0;
155
+ let dy = 0;
156
+ if (event.key === 'ArrowRight') dx = 1;
157
+ else if (event.key === 'ArrowLeft') dx = -1;
158
+ else if (event.key === 'ArrowDown') dy = 1;
159
+ else if (event.key === 'ArrowUp') dy = -1;
160
+ else return;
161
+ event.preventDefault();
162
+ if (mode === 'move') move(item, dx, dy);
163
+ else resize(item, dx, dy);
164
+ }
165
+ </script>
166
+
167
+ <div
168
+ bind:this={container}
169
+ class={cn('svadmin-resizable-grid', className)}
170
+ data-slot="resizable-grid"
171
+ data-editable={editable ? 'true' : 'false'}
172
+ style={`--svadmin-grid-row: ${rowHeight}px; height: ${height};`}
173
+ role="group"
174
+ aria-label={ariaLabel}
175
+ >
176
+ {#each items as entry (entry.id)}
177
+ <div
178
+ class="svadmin-resizable-grid__item"
179
+ data-grid-id={entry.id}
180
+ style={`left: calc(${entry.x} * (100% + ${gap}px) / ${cols}); width: calc(${entry.w} * (100% + ${gap}px) / ${cols} - ${gap}px); top: calc(${entry.y} * (var(--svadmin-grid-row) + ${gap}px)); height: calc(${entry.h} * var(--svadmin-grid-row) + ${Math.max(entry.h - 1, 0) * gap}px);`}
181
+ >
182
+ <div class="svadmin-resizable-grid__content">
183
+ {#if itemSnippet}{@render itemSnippet(entry)}{:else if children}{@render children()}{/if}
184
+ </div>
185
+ {#if editable}
186
+ <button
187
+ type="button"
188
+ class="svadmin-resizable-grid__handle svadmin-resizable-grid__handle--move"
189
+ aria-label={`Move ${entry.id}`}
190
+ onpointerdown={(event) => startMove(event, entry)}
191
+ onkeydown={(event) => handleKeydown(event, entry, 'move')}
192
+ >⠿</button>
193
+ <button
194
+ type="button"
195
+ class="svadmin-resizable-grid__handle svadmin-resizable-grid__handle--resize"
196
+ aria-label={`Resize ${entry.id}`}
197
+ onpointerdown={(event) => startResize(event, entry)}
198
+ onkeydown={(event) => handleKeydown(event, entry, 'resize')}
199
+ ></button>
200
+ {/if}
201
+ </div>
202
+ {/each}
203
+ </div>
204
+
205
+ <style>
206
+ .svadmin-resizable-grid {
207
+ position: relative;
208
+ width: 100%;
209
+ }
210
+
211
+ .svadmin-resizable-grid__item {
212
+ position: absolute;
213
+ display: flex;
214
+ overflow: hidden;
215
+ border: 1px solid var(--border, currentColor);
216
+ border-radius: min(var(--radius, 0.5rem), 0.75rem);
217
+ background: var(--card, var(--background, transparent));
218
+ color: var(--card-foreground, var(--foreground, currentColor));
219
+ }
220
+
221
+ .svadmin-resizable-grid__content { min-width: 0; flex: 1; padding: 0.75rem; }
222
+
223
+ .svadmin-resizable-grid__handle {
224
+ position: absolute;
225
+ border: 0;
226
+ padding: 0;
227
+ background: transparent;
228
+ color: var(--muted-foreground, currentColor);
229
+ cursor: pointer;
230
+ }
231
+ .svadmin-resizable-grid__handle:focus-visible { outline: 2px solid var(--ring, currentColor); outline-offset: -2px; }
232
+
233
+ .svadmin-resizable-grid__handle--move {
234
+ top: 0;
235
+ right: 1.25rem;
236
+ width: 1.25rem;
237
+ height: 1.25rem;
238
+ font-size: 0.75rem;
239
+ line-height: 1;
240
+ cursor: grab;
241
+ }
242
+ .svadmin-resizable-grid__handle--move:active { cursor: grabbing; }
243
+
244
+ .svadmin-resizable-grid__handle--resize {
245
+ right: 0;
246
+ bottom: 0;
247
+ width: 1rem;
248
+ height: 1rem;
249
+ background: linear-gradient(135deg, transparent 45%, var(--border, currentColor) 45%, var(--border, currentColor) 55%, transparent 55%, transparent 70%, var(--border, currentColor) 70%, var(--border, currentColor) 80%, transparent 80%);
250
+ cursor: nwse-resize;
251
+ }
252
+ </style>
@@ -0,0 +1,34 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface GridItem {
3
+ id: string;
4
+ /** Column index (0-based). */
5
+ x: number;
6
+ /** Row index (0-based). */
7
+ y: number;
8
+ /** Width in columns. */
9
+ w: number;
10
+ /** Height in rows. */
11
+ h: number;
12
+ }
13
+ interface Props {
14
+ items: GridItem[];
15
+ /** Number of columns in the grid. */
16
+ cols?: number;
17
+ /** Height of a single row in pixels. */
18
+ rowHeight?: number;
19
+ /** Gap between cells in pixels. */
20
+ gap?: number;
21
+ minW?: number;
22
+ minH?: number;
23
+ /** Enables move/resize handles. */
24
+ editable?: boolean;
25
+ ariaLabel?: string;
26
+ class?: string;
27
+ /** Renders each cell's content. Falls back to `children`. */
28
+ item?: Snippet<[GridItem]>;
29
+ children?: Snippet;
30
+ onchange?: (items: GridItem[]) => void;
31
+ }
32
+ declare const ResizableGrid: import("svelte").Component<Props, {}, "items">;
33
+ type ResizableGrid = ReturnType<typeof ResizableGrid>;
34
+ export default ResizableGrid;
@@ -78,8 +78,12 @@
78
78
  emptyLanesText = 'No grouped records yet.',
79
79
  highlightsLabel = 'Focus queue',
80
80
  workspaceStyle = 'operations',
81
- tableProps = {},
81
+ tableProps: rawTableProps = {},
82
82
  }: Props = $props();
83
+ const tableProps = $derived({
84
+ ...rawTableProps,
85
+ ...(rendering === undefined ? {} : { rendering }),
86
+ });
83
87
  const navigation = useNavigation();
84
88
  const context = captureAdminContext();
85
89
  const resource = $derived(context.getResource(resourceName));
@@ -4,7 +4,7 @@
4
4
 
5
5
  export type TimePickerProps = Omit<ComponentProps<typeof DateTimeInput>, 'mode'>;
6
6
 
7
- let props: TimePickerProps = $props();
7
+ let { value = $bindable(null), ...props }: TimePickerProps = $props();
8
8
  </script>
9
9
 
10
- <DateTimeInput {...props} mode="time" />
10
+ <DateTimeInput {...props} bind:value mode="time" />
@@ -1,6 +1,6 @@
1
1
  import type { ComponentProps } from 'svelte';
2
2
  import DateTimeInput from './DateTimeInput.svelte';
3
3
  export type TimePickerProps = Omit<ComponentProps<typeof DateTimeInput>, 'mode'>;
4
- declare const TimePicker: import("svelte").Component<TimePickerProps, {}, "">;
4
+ declare const TimePicker: import("svelte").Component<TimePickerProps, {}, "value">;
5
5
  type TimePicker = ReturnType<typeof TimePicker>;
6
6
  export default TimePicker;
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import * as Tooltip from './ui/tooltip/index.js';
4
3
  import { Button } from './ui/button/index.js';
5
4
  import type { Snippet } from 'svelte';
@@ -43,8 +42,8 @@
43
42
  onclick?.(event);
44
43
  }
45
44
 
46
- function mergeButtonProps(primitiveProps: Record<string, any>) {
47
- const merged: Record<string, any> = { ...primitiveProps, ...restProps };
45
+ function mergeButtonProps(primitiveProps: Record<string, unknown>) {
46
+ const merged: Record<string, unknown> = { ...primitiveProps, ...restProps };
48
47
  for (const [key, userHandler] of Object.entries(restProps)) {
49
48
  const primitiveHandler = primitiveProps[key];
50
49
  if (!key.startsWith('on') || typeof userHandler !== 'function' || typeof primitiveHandler !== 'function') continue;
@@ -61,7 +60,7 @@
61
60
  <Tooltip.Trigger aria-describedby={restProps['aria-describedby']}>
62
61
  {#snippet child({ props })}
63
62
  {@const primitiveOnclick = props['onclick'] as ((event: MouseEvent) => void) | undefined}
64
- {@const buttonProps = { ...mergeButtonProps(props), href, target, rel, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, 'aria-describedby': props['aria-describedby'], onclick: (event: MouseEvent) => handleClick(event, primitiveOnclick), disabled, tabindex } as any}
63
+ {@const buttonProps = { ...mergeButtonProps(props), href, target, rel, variant, size, type, class: className, 'aria-label': typeof restProps['aria-label'] === 'string' ? restProps['aria-label'] : tooltip, 'aria-describedby': typeof props['aria-describedby'] === 'string' ? props['aria-describedby'] : undefined, onclick: (event: MouseEvent) => handleClick(event, primitiveOnclick), disabled, tabindex }}
65
64
  <Button {...buttonProps}>
66
65
  {@render children()}
67
66
  </Button>