@svadmin/ui 0.31.0 → 0.32.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.
- package/dist/app.css +7 -6
- package/dist/components/AdminApp.svelte +19 -24
- package/dist/components/AdminApp.svelte.d.ts +3 -1
- package/dist/components/AnimatedCounter.svelte +8 -2
- package/dist/components/AnomalyBadge.svelte +5 -1
- package/dist/components/AuditLogViewer.svelte +9 -2
- package/dist/components/AutoForm.svelte +29 -8
- package/dist/components/AutoForm.svelte.d.ts +1 -1
- package/dist/components/AutoTable.svelte +35 -15
- package/dist/components/Breadcrumbs.svelte +3 -3
- package/dist/components/ChatDialog.svelte +39 -1
- package/dist/components/CommandPalette.svelte +3 -3
- package/dist/components/ConfirmDialog.svelte +16 -2
- package/dist/components/DraggableHeader.svelte +1 -2
- package/dist/components/FieldRenderer.svelte +37 -10
- package/dist/components/FieldRenderer.svelte.d.ts +1 -1
- package/dist/components/Header.svelte +5 -6
- package/dist/components/Layout.svelte +10 -5
- package/dist/components/LiveIndicator.svelte +7 -1
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/NavigateToResource.svelte +4 -2
- package/dist/components/PermissionMatrix.svelte +6 -1
- package/dist/components/ShowPage.svelte +9 -0
- package/dist/components/Sidebar.svelte +16 -9
- package/dist/components/SidebarItem.svelte +2 -2
- package/dist/components/SmartSuggest.svelte +12 -2
- package/dist/components/StepsForm.svelte +22 -26
- package/dist/components/TenantSwitcher.svelte +2 -2
- package/dist/components/VirtualTable.svelte +8 -6
- package/dist/components/VoiceInput.svelte +12 -0
- package/dist/components/ui/dialog/dialog-content.svelte +1 -1
- package/dist/components/ui/dialog/dialog-overlay.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -1
- package/dist/router-state.svelte.js +5 -3
- package/package.json +2 -2
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
import type { Snippet } from 'svelte';
|
|
21
21
|
import { getRichTextEditor } from '../editor-config.svelte.js';
|
|
22
22
|
|
|
23
|
-
let { field, value, onchange, children } = $props<{
|
|
23
|
+
let { field, value, onchange, disabled, children } = $props<{
|
|
24
24
|
field: FieldDefinition;
|
|
25
25
|
value: unknown;
|
|
26
26
|
onchange: (val: unknown) => void;
|
|
27
|
-
|
|
27
|
+
disabled?: boolean;
|
|
28
28
|
children?: Snippet;
|
|
29
29
|
}>();
|
|
30
30
|
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
value={strVal}
|
|
99
99
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
100
100
|
required={field.required}
|
|
101
|
+
{disabled}
|
|
101
102
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
102
103
|
/>
|
|
103
104
|
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
value={strVal}
|
|
109
110
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
110
111
|
required={field.required}
|
|
112
|
+
{disabled}
|
|
111
113
|
placeholder="name@example.com"
|
|
112
114
|
/>
|
|
113
115
|
|
|
@@ -118,6 +120,7 @@
|
|
|
118
120
|
value={strVal}
|
|
119
121
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
120
122
|
required={field.required}
|
|
123
|
+
{disabled}
|
|
121
124
|
placeholder="https://"
|
|
122
125
|
/>
|
|
123
126
|
|
|
@@ -128,6 +131,7 @@
|
|
|
128
131
|
value={strVal}
|
|
129
132
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
130
133
|
required={field.required}
|
|
134
|
+
{disabled}
|
|
131
135
|
placeholder="+1 (555) 000-0000"
|
|
132
136
|
/>
|
|
133
137
|
|
|
@@ -139,6 +143,7 @@
|
|
|
139
143
|
value={strVal || '#000000'}
|
|
140
144
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
141
145
|
class="h-10 w-14 cursor-pointer rounded-md border border-input bg-background p-1"
|
|
146
|
+
disabled={disabled}
|
|
142
147
|
/>
|
|
143
148
|
<Input
|
|
144
149
|
type="text"
|
|
@@ -146,6 +151,7 @@
|
|
|
146
151
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
147
152
|
placeholder="#000000"
|
|
148
153
|
class="max-w-32 font-mono text-sm"
|
|
154
|
+
{disabled}
|
|
149
155
|
/>
|
|
150
156
|
{#if strVal}
|
|
151
157
|
<span
|
|
@@ -160,8 +166,12 @@
|
|
|
160
166
|
id={field.key}
|
|
161
167
|
type="number"
|
|
162
168
|
value={String(numVal)}
|
|
163
|
-
oninput={(e) =>
|
|
169
|
+
oninput={(e) => {
|
|
170
|
+
const v = (e.target as HTMLInputElement).value;
|
|
171
|
+
onchange(v === '' ? null : Number(v));
|
|
172
|
+
}}
|
|
164
173
|
required={field.required}
|
|
174
|
+
{disabled}
|
|
165
175
|
/>
|
|
166
176
|
|
|
167
177
|
{:else if field.type === 'richtext'}
|
|
@@ -173,6 +183,7 @@
|
|
|
173
183
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
174
184
|
preset="full"
|
|
175
185
|
onchange={(html: string) => onchange(html)}
|
|
186
|
+
{disabled}
|
|
176
187
|
/>
|
|
177
188
|
{:else}
|
|
178
189
|
<Textarea
|
|
@@ -180,6 +191,7 @@
|
|
|
180
191
|
value={strVal}
|
|
181
192
|
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
182
193
|
required={field.required}
|
|
194
|
+
{disabled}
|
|
183
195
|
rows={10}
|
|
184
196
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
185
197
|
class="resize-y"
|
|
@@ -192,6 +204,7 @@
|
|
|
192
204
|
value={strVal}
|
|
193
205
|
oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
|
|
194
206
|
required={field.required}
|
|
207
|
+
{disabled}
|
|
195
208
|
rows={4}
|
|
196
209
|
placeholder={t('field.enterValue', { label: field.label })}
|
|
197
210
|
class="resize-y"
|
|
@@ -206,24 +219,31 @@
|
|
|
206
219
|
optionLabel={field.optionLabel ?? 'title'}
|
|
207
220
|
optionValue={field.optionValue ?? 'id'}
|
|
208
221
|
placeholder={t('field.selectPlaceholder')}
|
|
222
|
+
{disabled}
|
|
209
223
|
/>
|
|
210
224
|
|
|
211
225
|
{:else if field.type === 'select'}
|
|
212
226
|
{#if (field.options?.length ?? 0) > 8}
|
|
213
|
-
|
|
214
|
-
<ComboboxField
|
|
227
|
+
<select
|
|
215
228
|
id={field.key}
|
|
216
|
-
|
|
217
|
-
value={
|
|
218
|
-
onchange={(
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
230
|
+
value={strVal}
|
|
231
|
+
onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
|
|
232
|
+
required={field.required}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
>
|
|
235
|
+
<option value="">{t('field.selectPlaceholder')}</option>
|
|
236
|
+
{#each field.options ?? [] as opt}
|
|
237
|
+
<option value={opt.value}>{opt.label}</option>
|
|
238
|
+
{/each}
|
|
239
|
+
</select>
|
|
221
240
|
{:else}
|
|
222
241
|
<Select
|
|
223
242
|
id={field.key}
|
|
224
243
|
value={strVal}
|
|
225
244
|
onchange={(e) => onchange((e.target as HTMLSelectElement).value)}
|
|
226
245
|
required={field.required}
|
|
246
|
+
{disabled}
|
|
227
247
|
placeholder={t('field.selectPlaceholder')}
|
|
228
248
|
>
|
|
229
249
|
{#each field.options ?? [] as opt}
|
|
@@ -244,6 +264,7 @@
|
|
|
244
264
|
id={`${field.key}-${opt.value}`}
|
|
245
265
|
checked={multiVal.includes(opt.value)}
|
|
246
266
|
onCheckedChange={() => toggleMulti(opt.value)}
|
|
267
|
+
disabled={disabled}
|
|
247
268
|
/>
|
|
248
269
|
{opt.label}
|
|
249
270
|
</label>
|
|
@@ -270,6 +291,7 @@
|
|
|
270
291
|
id={field.key}
|
|
271
292
|
checked={boolVal}
|
|
272
293
|
onCheckedChange={(v) => onchange(v)}
|
|
294
|
+
disabled={disabled}
|
|
273
295
|
/>
|
|
274
296
|
<span class="text-sm text-muted-foreground">{boolVal ? t('common.yes') : t('common.no')}</span>
|
|
275
297
|
</div>
|
|
@@ -294,6 +316,7 @@
|
|
|
294
316
|
type="text"
|
|
295
317
|
placeholder={t('field.tagsPlaceholder')}
|
|
296
318
|
onkeydown={handleTagKeydown}
|
|
319
|
+
{disabled}
|
|
297
320
|
/>
|
|
298
321
|
</div>
|
|
299
322
|
|
|
@@ -304,6 +327,7 @@
|
|
|
304
327
|
value={strVal}
|
|
305
328
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
306
329
|
required={field.required}
|
|
330
|
+
{disabled}
|
|
307
331
|
/>
|
|
308
332
|
|
|
309
333
|
{:else if field.type === 'images'}
|
|
@@ -317,6 +341,7 @@
|
|
|
317
341
|
oninput={(e) => updateImage(i, (e.target as HTMLInputElement).value)}
|
|
318
342
|
placeholder="https://example.com/image.jpg"
|
|
319
343
|
class="flex-1"
|
|
344
|
+
{disabled}
|
|
320
345
|
/>
|
|
321
346
|
{#if url}
|
|
322
347
|
<img src={url} alt="preview" class="h-9 w-9 rounded object-cover border" />
|
|
@@ -342,6 +367,7 @@
|
|
|
342
367
|
// keep raw text until valid JSON
|
|
343
368
|
}
|
|
344
369
|
}}
|
|
370
|
+
{disabled}
|
|
345
371
|
rows={6}
|
|
346
372
|
class="resize-y font-mono text-xs"
|
|
347
373
|
/>
|
|
@@ -352,6 +378,7 @@
|
|
|
352
378
|
type="text"
|
|
353
379
|
value={strVal}
|
|
354
380
|
oninput={(e) => onchange((e.target as HTMLInputElement).value)}
|
|
381
|
+
{disabled}
|
|
355
382
|
/>
|
|
356
383
|
{/if}
|
|
357
384
|
</div>
|
|
@@ -5,7 +5,7 @@ type $$ComponentProps = {
|
|
|
5
5
|
field: FieldDefinition;
|
|
6
6
|
value: unknown;
|
|
7
7
|
onchange: (val: unknown) => void;
|
|
8
|
-
|
|
8
|
+
disabled?: boolean;
|
|
9
9
|
children?: Snippet;
|
|
10
10
|
};
|
|
11
11
|
declare const FieldRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -26,12 +26,11 @@
|
|
|
26
26
|
siteUrl?: string;
|
|
27
27
|
}>();
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const CustomNotificationPanel = registry.NotificationPanel;
|
|
29
|
+
const registry = $derived(getComponentRegistry() || {} as any);
|
|
30
|
+
const CustomBreadcrumbs = $derived(registry.Breadcrumbs);
|
|
31
|
+
const CustomThemeToggle = $derived(registry.ThemeToggle);
|
|
32
|
+
const CustomUserMenu = $derived(registry.UserMenu);
|
|
33
|
+
const CustomNotificationPanel = $derived(registry.NotificationPanel);
|
|
35
34
|
</script>
|
|
36
35
|
|
|
37
36
|
<header class="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between bg-card/60 backdrop-blur-md px-4 md:px-6">
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { fade, fly } from 'svelte/transition';
|
|
4
4
|
import Sidebar from './Sidebar.svelte';
|
|
5
|
-
import Toast from './Toast.svelte';
|
|
6
5
|
import Header from './Header.svelte';
|
|
7
6
|
import CommandPalette from './CommandPalette.svelte';
|
|
8
7
|
import KeyboardShortcuts from './KeyboardShortcuts.svelte';
|
|
@@ -12,7 +11,7 @@
|
|
|
12
11
|
import { getAuthProvider } from '@svadmin/core';
|
|
13
12
|
import type { Identity, MenuItem } from '@svadmin/core';
|
|
14
13
|
import { navigate } from '@svadmin/core/router';
|
|
15
|
-
import {
|
|
14
|
+
import { getPath } from '../router-state.svelte.js';
|
|
16
15
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
17
16
|
import { Button } from './ui/button/index.js';
|
|
18
17
|
import * as Sheet from './ui/sheet/index.js';
|
|
@@ -150,7 +149,7 @@
|
|
|
150
149
|
|
|
151
150
|
<!-- Content area -->
|
|
152
151
|
<main class="flex-1 overflow-y-auto p-6 md:p-8">
|
|
153
|
-
{#key
|
|
152
|
+
{#key getPath()}
|
|
154
153
|
<div in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
|
|
155
154
|
{@render children()}
|
|
156
155
|
</div>
|
|
@@ -158,8 +157,14 @@
|
|
|
158
157
|
</main>
|
|
159
158
|
</div>
|
|
160
159
|
</div>
|
|
161
|
-
<CommandPalette
|
|
160
|
+
<CommandPalette
|
|
161
|
+
bind:open={commandOpen}
|
|
162
|
+
onAskAI={(q) => {
|
|
163
|
+
if (typeof window !== 'undefined') {
|
|
164
|
+
window.dispatchEvent(new CustomEvent('svadmin:ask-ai', { detail: q }));
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
162
168
|
<KeyboardShortcuts bind:open={shortcutsOpen} />
|
|
163
169
|
<ChatDialog />
|
|
164
|
-
<Toast />
|
|
165
170
|
{/if}
|
|
@@ -23,9 +23,15 @@
|
|
|
23
23
|
type StatusKey = 'connected' | 'connecting' | 'disconnected';
|
|
24
24
|
const cfg = $derived(statusConfigs[status as StatusKey]);
|
|
25
25
|
|
|
26
|
+
let now = $state(Date.now());
|
|
27
|
+
$effect(() => {
|
|
28
|
+
const itv = setInterval(() => { now = Date.now(); }, 1000);
|
|
29
|
+
return () => clearInterval(itv);
|
|
30
|
+
});
|
|
31
|
+
|
|
26
32
|
const timeSinceEvent = $derived(
|
|
27
33
|
lastEvent?.timestamp
|
|
28
|
-
? Math.floor((
|
|
34
|
+
? Math.floor((now - lastEvent.timestamp) / 1000)
|
|
29
35
|
: null
|
|
30
36
|
);
|
|
31
37
|
</script>
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
const { menuItems } = useMenu();
|
|
6
6
|
|
|
7
|
+
let navigated = false;
|
|
7
8
|
$effect(() => {
|
|
8
|
-
//
|
|
9
|
-
if (menuItems.length > 0) {
|
|
9
|
+
// Navigate to first resource ONCE — avoid infinite re-triggers
|
|
10
|
+
if (!navigated && menuItems.length > 0) {
|
|
11
|
+
navigated = true;
|
|
10
12
|
navigate(menuItems[0].route);
|
|
11
13
|
}
|
|
12
14
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
isGranted,
|
|
16
16
|
|
|
17
17
|
// Interactive states
|
|
18
|
-
selectedRole = $bindable(
|
|
18
|
+
selectedRole = $bindable(''),
|
|
19
19
|
loading = false,
|
|
20
20
|
|
|
21
21
|
// Feedback and alerts (passed down from upper business logic)
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
onToggle: (role: string, resource: string, action: string, grant: boolean) => void;
|
|
45
45
|
} = $props();
|
|
46
46
|
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (!selectedRole && roles.length > 0) {
|
|
49
|
+
selectedRole = roles[0].code;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
47
52
|
</script>
|
|
48
53
|
|
|
49
54
|
<div class="h-full flex flex-col pt-2">
|
|
@@ -73,5 +73,14 @@
|
|
|
73
73
|
{/each}
|
|
74
74
|
</Card.Content>
|
|
75
75
|
</Card.Root>
|
|
76
|
+
{:else}
|
|
77
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
78
|
+
<Card.Content class="p-8 text-center">
|
|
79
|
+
<p class="text-muted-foreground">{t('common.noData')}</p>
|
|
80
|
+
{#if query.isError}
|
|
81
|
+
<p class="text-sm text-destructive mt-2">{(query.error as Error)?.message ?? t('common.operationFailed')}</p>
|
|
82
|
+
{/if}
|
|
83
|
+
</Card.Content>
|
|
84
|
+
</Card.Root>
|
|
76
85
|
{/if}
|
|
77
86
|
</div>
|
|
@@ -80,10 +80,11 @@
|
|
|
80
80
|
let navItems = $state.raw<NavItem[]>([]);
|
|
81
81
|
|
|
82
82
|
$effect(() => {
|
|
83
|
-
// Re-run when locale or resources change
|
|
84
83
|
const localeVal = getLocale();
|
|
84
|
+
const currentResources = getResources();
|
|
85
|
+
let cancelled = false;
|
|
85
86
|
|
|
86
|
-
Promise.all(
|
|
87
|
+
Promise.all(currentResources.map(async (r) => {
|
|
87
88
|
try {
|
|
88
89
|
const { can } = await canAccessAsync(r.name, 'list');
|
|
89
90
|
return { r, can };
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
return { r, can: false };
|
|
92
93
|
}
|
|
93
94
|
})).then(results => {
|
|
95
|
+
if (cancelled) return;
|
|
94
96
|
const items: NavItem[] = [{ path: '/', label: t('common.home'), Icon: LayoutDashboard }];
|
|
95
97
|
for (const { r, can } of results) {
|
|
96
98
|
if (can) {
|
|
@@ -104,6 +106,8 @@
|
|
|
104
106
|
}
|
|
105
107
|
navItems = items;
|
|
106
108
|
});
|
|
109
|
+
|
|
110
|
+
return () => { cancelled = true; };
|
|
107
111
|
});
|
|
108
112
|
|
|
109
113
|
/** Toggle between available locales */
|
|
@@ -254,9 +258,10 @@
|
|
|
254
258
|
{@const active = isActive(item.path)}
|
|
255
259
|
<a
|
|
256
260
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
257
|
-
|
|
261
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
262
|
+
class="flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-300
|
|
258
263
|
{active
|
|
259
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
264
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
|
|
260
265
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
261
266
|
>
|
|
262
267
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -277,10 +282,11 @@
|
|
|
277
282
|
<a
|
|
278
283
|
{...props}
|
|
279
284
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
280
|
-
|
|
285
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
286
|
+
class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
|
|
281
287
|
{active
|
|
282
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
283
|
-
: 'text-sidebar-foreground/
|
|
288
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20'
|
|
289
|
+
: 'text-sidebar-foreground/60 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground'}"
|
|
284
290
|
>
|
|
285
291
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
286
292
|
</a>
|
|
@@ -293,9 +299,10 @@
|
|
|
293
299
|
{:else}
|
|
294
300
|
<a
|
|
295
301
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
296
|
-
|
|
302
|
+
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
303
|
+
class="flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
|
|
297
304
|
{active
|
|
298
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
305
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
|
|
299
306
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
300
307
|
>
|
|
301
308
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
103
103
|
class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
104
104
|
{active
|
|
105
|
-
? 'bg-
|
|
105
|
+
? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5'
|
|
106
106
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
107
107
|
>
|
|
108
108
|
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
122
122
|
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
|
|
123
123
|
{active
|
|
124
|
-
? 'bg-
|
|
124
|
+
? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5 font-semibold before:absolute before:left-0 before:top-2 before:bottom-2 before:w-[3px] before:bg-primary before:rounded-r-md overflow-hidden relative'
|
|
125
125
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
126
126
|
style="padding-left: {12 + depth * 12}px"
|
|
127
127
|
>
|
|
@@ -49,10 +49,20 @@
|
|
|
49
49
|
return '';
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
import { getChatProvider } from '@svadmin/core';
|
|
53
|
+
|
|
52
54
|
const suffixText = $derived(ghostText ? ghostText.slice(value.length) : '');
|
|
53
55
|
|
|
56
|
+
const resolvedProvider = $derived(provider ?? (() => {
|
|
57
|
+
try {
|
|
58
|
+
return getChatProvider();
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
})());
|
|
63
|
+
|
|
54
64
|
async function predict() {
|
|
55
|
-
if (!
|
|
65
|
+
if (!resolvedProvider || !value.trim()) {
|
|
56
66
|
suggestion = '';
|
|
57
67
|
return;
|
|
58
68
|
}
|
|
@@ -70,7 +80,7 @@ Provide ONLY the most likely completion of the exact phrase the user is typing.
|
|
|
70
80
|
Include the user's input in your returned string.
|
|
71
81
|
Do not include any other text, Markdown formatting, quotes, or explanations.`;
|
|
72
82
|
|
|
73
|
-
const result =
|
|
83
|
+
const result = resolvedProvider.sendMessage(
|
|
74
84
|
[{ id: 'prompt', role: 'user', content: prompt, timestamp: Date.now() }],
|
|
75
85
|
{ signal: abortController.signal }
|
|
76
86
|
);
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
const totalSteps = $derived(steps.length);
|
|
70
|
-
const progressValue = $derived(((form.currentStep + 1) / totalSteps) * 100);
|
|
71
|
-
const isLastStep = $derived(form.currentStep >= totalSteps - 1);
|
|
70
|
+
const progressValue = $derived(((form.steps.currentStep + 1) / totalSteps) * 100);
|
|
71
|
+
const isLastStep = $derived(form.steps.currentStep >= totalSteps - 1);
|
|
72
72
|
|
|
73
73
|
// Fields for the current step
|
|
74
74
|
const currentFields = $derived(
|
|
75
|
-
steps[form.currentStep]?.fields
|
|
75
|
+
steps[form.steps.currentStep]?.fields
|
|
76
76
|
.map(key => resource.fields.find(f => f.key === key))
|
|
77
77
|
.filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
|
|
78
78
|
);
|
|
@@ -81,15 +81,11 @@
|
|
|
81
81
|
function validateCurrentStep(): boolean {
|
|
82
82
|
let valid = true;
|
|
83
83
|
form.clearErrors();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else if (field.validate) {
|
|
90
|
-
const msg = field.validate(value);
|
|
91
|
-
if (msg) {
|
|
92
|
-
form.setFieldError(field.key, msg);
|
|
84
|
+
const errors = validator(form.values);
|
|
85
|
+
if (errors) {
|
|
86
|
+
for (const field of currentFields) {
|
|
87
|
+
if (errors[field.key]) {
|
|
88
|
+
form.setFieldError(field.key, errors[field.key]);
|
|
93
89
|
valid = false;
|
|
94
90
|
}
|
|
95
91
|
}
|
|
@@ -101,7 +97,7 @@
|
|
|
101
97
|
if (!validateCurrentStep()) return;
|
|
102
98
|
|
|
103
99
|
if (!isLastStep) {
|
|
104
|
-
form.
|
|
100
|
+
form.steps.nextStep();
|
|
105
101
|
return;
|
|
106
102
|
}
|
|
107
103
|
|
|
@@ -118,17 +114,17 @@
|
|
|
118
114
|
<Button
|
|
119
115
|
variant="ghost"
|
|
120
116
|
type="button"
|
|
121
|
-
class="flex items-center gap-2 transition-colors h-auto py-1 px-2 {i === form.currentStep ? 'text-primary font-semibold' : 'text-muted-foreground'}"
|
|
122
|
-
onclick={() => form.gotoStep(i)}
|
|
117
|
+
class="flex items-center gap-2 transition-colors h-auto py-1 px-2 {i === form.steps.currentStep ? 'text-primary font-semibold' : 'text-muted-foreground'}"
|
|
118
|
+
onclick={() => form.steps.gotoStep(i)}
|
|
123
119
|
>
|
|
124
120
|
<span
|
|
125
121
|
class="flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium transition-colors"
|
|
126
|
-
class:bg-primary={i <= form.currentStep}
|
|
127
|
-
class:text-primary-foreground={i <= form.currentStep}
|
|
128
|
-
class:bg-muted={i > form.currentStep}
|
|
129
|
-
class:text-muted-foreground={i > form.currentStep}
|
|
122
|
+
class:bg-primary={i <= form.steps.currentStep}
|
|
123
|
+
class:text-primary-foreground={i <= form.steps.currentStep}
|
|
124
|
+
class:bg-muted={i > form.steps.currentStep}
|
|
125
|
+
class:text-muted-foreground={i > form.steps.currentStep}
|
|
130
126
|
>
|
|
131
|
-
{#if i < form.currentStep}
|
|
127
|
+
{#if i < form.steps.currentStep}
|
|
132
128
|
<Check class="h-3.5 w-3.5" />
|
|
133
129
|
{:else}
|
|
134
130
|
{i + 1}
|
|
@@ -137,7 +133,7 @@
|
|
|
137
133
|
<span class="hidden sm:inline">{step.title}</span>
|
|
138
134
|
</Button>
|
|
139
135
|
{#if i < steps.length - 1}
|
|
140
|
-
<div class="flex-1 mx-2 h-px bg-border" class:bg-primary={i < form.currentStep}></div>
|
|
136
|
+
<div class="flex-1 mx-2 h-px bg-border" class:bg-primary={i < form.steps.currentStep}></div>
|
|
141
137
|
{/if}
|
|
142
138
|
{/each}
|
|
143
139
|
</div>
|
|
@@ -147,13 +143,13 @@
|
|
|
147
143
|
<!-- Step content -->
|
|
148
144
|
<Card.Root>
|
|
149
145
|
<Card.CardHeader>
|
|
150
|
-
<Card.CardTitle>{steps[form.currentStep]?.title ?? ''}</Card.CardTitle>
|
|
146
|
+
<Card.CardTitle>{steps[form.steps.currentStep]?.title ?? ''}</Card.CardTitle>
|
|
151
147
|
<Badge variant="outline" class="w-fit">
|
|
152
|
-
{t('common.step')} {form.currentStep + 1} / {totalSteps}
|
|
148
|
+
{t('common.step')} {form.steps.currentStep + 1} / {totalSteps}
|
|
153
149
|
</Badge>
|
|
154
150
|
</Card.CardHeader>
|
|
155
151
|
<Card.CardContent>
|
|
156
|
-
{#key form.currentStep}
|
|
152
|
+
{#key form.steps.currentStep}
|
|
157
153
|
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} in:fly={{ x: 30, duration: 250 }}>
|
|
158
154
|
{#each currentFields as field (field.key)}
|
|
159
155
|
<FieldRenderer
|
|
@@ -170,10 +166,10 @@
|
|
|
170
166
|
<Button
|
|
171
167
|
type="button"
|
|
172
168
|
variant="outline"
|
|
173
|
-
onclick={() => form.currentStep === 0 ? navigate(`/${resourceName}`) : form.
|
|
169
|
+
onclick={() => form.steps.currentStep === 0 ? navigate(`/${resourceName}`) : form.steps.prevStep()}
|
|
174
170
|
>
|
|
175
171
|
<ArrowLeft class="h-4 w-4" data-icon="inline-start" />
|
|
176
|
-
{form.currentStep === 0 ? t('common.cancel') : t('common.back')}
|
|
172
|
+
{form.steps.currentStep === 0 ? t('common.cancel') : t('common.back')}
|
|
177
173
|
</Button>
|
|
178
174
|
|
|
179
175
|
<Button type="submit" disabled={form.submitting}>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
9
|
tenants = [],
|
|
10
|
-
currentTenantId = $bindable(
|
|
10
|
+
currentTenantId = $bindable(undefined as string | undefined),
|
|
11
11
|
collapsed = false,
|
|
12
12
|
onSwitch,
|
|
13
13
|
headerSnippet,
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
{/if}
|
|
75
75
|
</div>
|
|
76
76
|
<span class="flex-1 truncate">{tenant.name}</span>
|
|
77
|
-
{#if tenant.id ===
|
|
77
|
+
{#if tenant.id === currentTenant?.id}
|
|
78
78
|
<Check class="h-4 w-4 text-primary" />
|
|
79
79
|
{/if}
|
|
80
80
|
</DropdownMenu.Item>
|
|
@@ -66,11 +66,13 @@
|
|
|
66
66
|
style="height: {containerHeight}px;"
|
|
67
67
|
onscroll={handleScroll}
|
|
68
68
|
>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
<
|
|
69
|
+
<table class="w-full relative" style="border-spacing: 0;">
|
|
70
|
+
<tbody style="display: block;">
|
|
71
|
+
<tr style="height: {visibleRange.start * rowHeight}px; border: none; padding: 0; display: block;"></tr>
|
|
72
|
+
{#each visibleItems as { item, index } (index)}
|
|
72
73
|
{@render row(item, index)}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
{/each}
|
|
75
|
+
<tr style="height: {Math.max(0, (items.length - visibleRange.end - 1) * rowHeight)}px; border: none; padding: 0; display: block;"></tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
76
78
|
</div>
|
|
@@ -89,6 +89,18 @@
|
|
|
89
89
|
isListening = false;
|
|
90
90
|
if (onend) onend();
|
|
91
91
|
};
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
if (recognition) {
|
|
95
|
+
isListening = false;
|
|
96
|
+
recognition.onend = () => {};
|
|
97
|
+
recognition.stop();
|
|
98
|
+
recognition.onstart = null;
|
|
99
|
+
recognition.onresult = null;
|
|
100
|
+
recognition.onerror = null;
|
|
101
|
+
recognition.onend = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
92
104
|
});
|
|
93
105
|
|
|
94
106
|
function toggleListening(e?: MouseEvent) {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
bind:ref
|
|
29
29
|
data-slot="dialog-content"
|
|
30
30
|
class={cn(
|
|
31
|
-
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-
|
|
31
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-98 data-[state=open]:zoom-in-98 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-6 text-sm ring-1 duration-300 sm:max-w-lg fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none shadow-2xl",
|
|
32
32
|
className
|
|
33
33
|
)}
|
|
34
34
|
{...restProps}
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
<DialogPrimitive.Overlay
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="dialog-overlay"
|
|
15
|
-
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/
|
|
15
|
+
class={cn("data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/20 duration-200 backdrop-blur-sm fixed inset-0 isolate z-50", className)}
|
|
16
16
|
{...restProps}
|
|
17
17
|
/>
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
...restProps
|
|
16
16
|
}: Props = $props();
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const getOpen = getContext<() => boolean>("svadmin-dropdown-open");
|
|
19
|
+
let isOpen = $derived(getOpen?.() ?? true);
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
22
|
{#if isOpen}
|