@svadmin/ui 0.10.0 → 0.11.1
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/package.json +1 -1
- package/src/app.css +18 -0
- package/src/component-registry.svelte.ts +46 -0
- package/src/components/AICommandBar.svelte +5 -5
- package/src/components/AdminApp.svelte +27 -1
- package/src/components/AnomalyBadge.svelte +1 -1
- package/src/components/AuditLogDrawer.svelte +4 -4
- package/src/components/AutoForm.svelte +1 -1
- package/src/components/AutoTable.svelte +2 -2
- package/src/components/CopilotPanel.svelte +4 -4
- package/src/components/FieldRenderer.svelte +1 -1
- package/src/components/Header.svelte +1 -1
- package/src/components/InsightCard.svelte +2 -2
- package/src/components/Layout.svelte +1 -1
- package/src/components/LiveIndicator.svelte +3 -3
- package/src/components/MarkdownRenderer.svelte +1 -1
- package/src/components/PasswordInput.svelte +2 -2
- package/src/components/ProfilePage.svelte +1 -1
- package/src/components/SmartSuggest.svelte +2 -2
- package/src/components/StatsCard.svelte +5 -5
- package/src/components/VoiceInput.svelte +30 -11
- package/src/components/fields/BooleanField.svelte +2 -2
- package/src/components/fields/SelectField.svelte +1 -1
- package/src/components/ui/alert/alert.svelte +2 -2
- package/src/index.ts +6 -0
package/package.json
CHANGED
package/src/app.css
CHANGED
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
--chart-3: oklch(0.398 0.07 227.392);
|
|
35
35
|
--chart-4: oklch(0.828 0.189 84.429);
|
|
36
36
|
--chart-5: oklch(0.769 0.188 70.08);
|
|
37
|
+
--success: oklch(0.6 0.2 145);
|
|
38
|
+
--success-foreground: oklch(0.985 0 0);
|
|
39
|
+
--warning: oklch(0.75 0.18 85);
|
|
40
|
+
--warning-foreground: oklch(0.3 0.08 60);
|
|
41
|
+
--info: oklch(0.6 0.15 250);
|
|
42
|
+
--info-foreground: oklch(0.985 0 0);
|
|
37
43
|
--sidebar: oklch(1 0 0 / 80%);
|
|
38
44
|
--sidebar-foreground: oklch(0.13 0.02 265);
|
|
39
45
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
@@ -69,6 +75,12 @@
|
|
|
69
75
|
--chart-3: oklch(0.769 0.188 70.08);
|
|
70
76
|
--chart-4: oklch(0.627 0.265 303.9);
|
|
71
77
|
--chart-5: oklch(0.645 0.246 16.439);
|
|
78
|
+
--success: oklch(0.65 0.2 150);
|
|
79
|
+
--success-foreground: oklch(0.985 0 0);
|
|
80
|
+
--warning: oklch(0.8 0.15 85);
|
|
81
|
+
--warning-foreground: oklch(0.25 0.05 60);
|
|
82
|
+
--info: oklch(0.65 0.15 250);
|
|
83
|
+
--info-foreground: oklch(0.985 0 0);
|
|
72
84
|
--sidebar: oklch(0.16 0.015 265 / 80%);
|
|
73
85
|
--sidebar-foreground: oklch(0.985 0 0);
|
|
74
86
|
--sidebar-primary: oklch(0.585 0.233 264);
|
|
@@ -146,6 +158,12 @@ body {
|
|
|
146
158
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
147
159
|
--color-sidebar-border: var(--sidebar-border);
|
|
148
160
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
161
|
+
--color-success: var(--success);
|
|
162
|
+
--color-success-foreground: var(--success-foreground);
|
|
163
|
+
--color-warning: var(--warning);
|
|
164
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
165
|
+
--color-info: var(--info);
|
|
166
|
+
--color-info-foreground: var(--info-foreground);
|
|
149
167
|
--radius-sm: calc(var(--radius) - 4px);
|
|
150
168
|
--radius-md: calc(var(--radius) - 2px);
|
|
151
169
|
--radius-lg: var(--radius);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Registry — Svelte Context-based DI for component injection.
|
|
3
|
+
*
|
|
4
|
+
* Allows users to override any registered component via the `components`
|
|
5
|
+
* prop on `AdminApp`, while providing sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { getContext, setContext, type Component } from 'svelte';
|
|
8
|
+
|
|
9
|
+
// ─── Overridable component slots ────────────────────────────────
|
|
10
|
+
export interface ComponentRegistry {
|
|
11
|
+
// Layout primitives
|
|
12
|
+
Layout: Component<any>;
|
|
13
|
+
Sidebar: Component<any>;
|
|
14
|
+
Header: Component<any>;
|
|
15
|
+
|
|
16
|
+
// Pages
|
|
17
|
+
LoginPage: Component<any>;
|
|
18
|
+
AutoTable: Component<any>;
|
|
19
|
+
AutoForm: Component<any>;
|
|
20
|
+
ShowPage: Component<any>;
|
|
21
|
+
|
|
22
|
+
// Shadcn primitives
|
|
23
|
+
Button: Component<any>;
|
|
24
|
+
Input: Component<any>;
|
|
25
|
+
Badge: Component<any>;
|
|
26
|
+
Skeleton: Component<any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const REGISTRY_KEY = 'svadmin:components';
|
|
30
|
+
|
|
31
|
+
/** Set the component registry in context (called by AdminApp). */
|
|
32
|
+
export function setComponentRegistry(registry: ComponentRegistry): void {
|
|
33
|
+
setContext(REGISTRY_KEY, registry);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Retrieve the full component registry from context. */
|
|
37
|
+
export function getComponentRegistry(): ComponentRegistry {
|
|
38
|
+
return getContext<ComponentRegistry>(REGISTRY_KEY);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Retrieve a single component by name from the registry. */
|
|
42
|
+
export function useComponent<K extends keyof ComponentRegistry>(
|
|
43
|
+
name: K,
|
|
44
|
+
): ComponentRegistry[K] {
|
|
45
|
+
return getComponentRegistry()[name];
|
|
46
|
+
}
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
} else {
|
|
60
60
|
aiResponse = await result;
|
|
61
61
|
}
|
|
62
|
-
} catch (err:
|
|
63
|
-
if (err.name !== 'AbortError') {
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
if (err instanceof Error && err.name !== 'AbortError') {
|
|
64
64
|
aiResponse = 'Failed to get response from AI.';
|
|
65
65
|
}
|
|
66
66
|
} finally {
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
>
|
|
92
92
|
<div class="flex items-center border-b px-3 h-12 relative" data-cmdk-input-wrapper="">
|
|
93
93
|
{#if aiMode}
|
|
94
|
-
<Sparkles class="mr-2 h-4 w-4 shrink-0 text-
|
|
94
|
+
<Sparkles class="mr-2 h-4 w-4 shrink-0 text-warning animate-pulse" />
|
|
95
95
|
{:else}
|
|
96
96
|
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
97
97
|
{/if}
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
/>
|
|
104
104
|
{#if searchValue.trim() && !aiMode}
|
|
105
105
|
<button
|
|
106
|
-
class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-
|
|
106
|
+
class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-warning bg-warning/10 hover:bg-warning/20 px-2 py-1 rounded transition-colors"
|
|
107
107
|
onclick={askAI}
|
|
108
108
|
>
|
|
109
109
|
<Sparkles class="h-3 w-3" />
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
<!-- AI Response Area -->
|
|
153
153
|
<div class="h-[360px] max-h-[360px] overflow-y-auto p-4 bg-muted/20">
|
|
154
154
|
<div class="flex items-start gap-3">
|
|
155
|
-
<div class="mt-0.5 w-8 h-8 rounded-full bg-
|
|
155
|
+
<div class="mt-0.5 w-8 h-8 rounded-full bg-warning/10 text-warning flex items-center justify-center shrink-0">
|
|
156
156
|
<Sparkles class="h-4 w-4" />
|
|
157
157
|
</div>
|
|
158
158
|
<div class="flex-1 w-full text-sm">
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { t } from '@svadmin/core/i18n';
|
|
8
8
|
import { navigate } from '@svadmin/core/router';
|
|
9
9
|
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
10
|
+
import { setComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
|
|
10
11
|
import Layout from './Layout.svelte';
|
|
11
12
|
import AutoTable from './AutoTable.svelte';
|
|
12
13
|
import AutoForm from './AutoForm.svelte';
|
|
@@ -18,6 +19,12 @@
|
|
|
18
19
|
import UpdatePasswordPage from './UpdatePasswordPage.svelte';
|
|
19
20
|
import ConfigErrorScreen from './ConfigErrorScreen.svelte';
|
|
20
21
|
import DevTools from './DevTools.svelte';
|
|
22
|
+
import Sidebar from './Sidebar.svelte';
|
|
23
|
+
import Header from './Header.svelte';
|
|
24
|
+
import { Button } from './ui/button/index.js';
|
|
25
|
+
import { Input } from './ui/input/index.js';
|
|
26
|
+
import { Badge } from './ui/badge/index.js';
|
|
27
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
21
28
|
import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
|
|
22
29
|
|
|
23
30
|
interface Props {
|
|
@@ -30,6 +37,8 @@
|
|
|
30
37
|
defaultTheme?: ThemeMode;
|
|
31
38
|
dashboard?: Snippet;
|
|
32
39
|
loginPage?: Snippet;
|
|
40
|
+
/** Override default components via DI */
|
|
41
|
+
components?: Partial<ComponentRegistry>;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
let {
|
|
@@ -42,8 +51,22 @@
|
|
|
42
51
|
defaultTheme,
|
|
43
52
|
dashboard,
|
|
44
53
|
loginPage,
|
|
54
|
+
components: userComponents,
|
|
45
55
|
}: Props = $props();
|
|
46
56
|
|
|
57
|
+
// Default component registry
|
|
58
|
+
const defaultComponents: ComponentRegistry = {
|
|
59
|
+
Layout, Sidebar, Header,
|
|
60
|
+
LoginPage, AutoTable, AutoForm, ShowPage,
|
|
61
|
+
Button: Button as unknown as ComponentRegistry['Button'],
|
|
62
|
+
Input: Input as unknown as ComponentRegistry['Input'],
|
|
63
|
+
Badge: Badge as unknown as ComponentRegistry['Badge'],
|
|
64
|
+
Skeleton: Skeleton as unknown as ComponentRegistry['Skeleton'],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Merge user overrides and set context
|
|
68
|
+
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
69
|
+
|
|
47
70
|
// Resolve router provider (default to hash)
|
|
48
71
|
const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
|
|
49
72
|
|
|
@@ -77,6 +100,9 @@
|
|
|
77
100
|
let authChecked = $state(false);
|
|
78
101
|
|
|
79
102
|
$effect(() => {
|
|
103
|
+
// Explicitly track route so auth is re-checked on navigation
|
|
104
|
+
const _route = route;
|
|
105
|
+
|
|
80
106
|
if (!authProvider) {
|
|
81
107
|
isAuthenticated = true;
|
|
82
108
|
authChecked = true;
|
|
@@ -85,7 +111,7 @@
|
|
|
85
111
|
authProvider.check().then(result => {
|
|
86
112
|
isAuthenticated = result.authenticated;
|
|
87
113
|
authChecked = true;
|
|
88
|
-
if (!result.authenticated &&
|
|
114
|
+
if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
|
|
89
115
|
navigate(result.redirectTo ?? '/login');
|
|
90
116
|
}
|
|
91
117
|
});
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
>
|
|
53
53
|
<Badge
|
|
54
54
|
variant={isGood ? 'default' : 'destructive'}
|
|
55
|
-
class="flex items-center gap-1 font-mono text-xs px-1.5 py-0 {isGood ? 'bg-
|
|
55
|
+
class="flex items-center gap-1 font-mono text-xs px-1.5 py-0 {isGood ? 'bg-success hover:bg-success/90 text-success-foreground' : ''} {className}"
|
|
56
56
|
>
|
|
57
57
|
{#if diff > 0}
|
|
58
58
|
<TrendingUp class="h-3 w-3" />
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
// Action semantic color mapping
|
|
35
35
|
function getActionColor(action: string) {
|
|
36
36
|
switch (action) {
|
|
37
|
-
case 'create': return 'bg-
|
|
38
|
-
case 'update': return 'bg-
|
|
39
|
-
case 'delete': return 'bg-
|
|
40
|
-
default: return 'bg-
|
|
37
|
+
case 'create': return 'bg-success';
|
|
38
|
+
case 'update': return 'bg-info';
|
|
39
|
+
case 'delete': return 'bg-destructive';
|
|
40
|
+
default: return 'bg-muted-foreground';
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
</script>
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
</TooltipButton>
|
|
225
225
|
<h1 class="text-xl font-semibold text-foreground">{pageTitle}</h1>
|
|
226
226
|
{#if isDirty}
|
|
227
|
-
<Badge variant="outline" class="border-
|
|
227
|
+
<Badge variant="outline" class="border-warning/30 bg-warning/10 text-warning-foreground">{t('common.unsaved')}</Badge>
|
|
228
228
|
{/if}
|
|
229
229
|
</div>
|
|
230
230
|
|
|
@@ -523,7 +523,7 @@
|
|
|
523
523
|
{#if cellRenderer && field}
|
|
524
524
|
{@render cellRenderer({ field, value: cell.getValue(), record })}
|
|
525
525
|
{:else if field?.type === 'boolean'}
|
|
526
|
-
<span class="inline-block h-2 w-2 rounded-full {cell.getValue() ? 'bg-
|
|
526
|
+
<span class="inline-block h-2 w-2 rounded-full {cell.getValue() ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
|
|
527
527
|
{:else if field?.type === 'date' && cell.getValue()}
|
|
528
528
|
{new Date(cell.getValue() as string).toLocaleDateString()}
|
|
529
529
|
{:else if field?.type === 'tags' && Array.isArray(cell.getValue())}
|
|
@@ -635,7 +635,7 @@
|
|
|
635
635
|
{#if cellRenderer}
|
|
636
636
|
{@render cellRenderer({ field, value, record })}
|
|
637
637
|
{:else if field.type === 'boolean'}
|
|
638
|
-
<span class="inline-block h-2 w-2 rounded-full {value ? 'bg-
|
|
638
|
+
<span class="inline-block h-2 w-2 rounded-full {value ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
|
|
639
639
|
{:else if field.type === 'date' && value}
|
|
640
640
|
{new Date(value as string).toLocaleDateString()}
|
|
641
641
|
{:else if field.type === 'tags' && Array.isArray(value)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getChatProvider, getChatContext } from '@svadmin/core';
|
|
3
|
-
import { Sparkles, X, ChevronRight, Check } from 'lucide-svelte';
|
|
3
|
+
import { Sparkles, X, ChevronRight, Check, RefreshCcw } from 'lucide-svelte';
|
|
4
4
|
import { fade, slide } from 'svelte/transition';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import MarkdownRenderer from './MarkdownRenderer.svelte';
|
|
@@ -56,8 +56,8 @@ Keep it concise, professional, and use bullet points.`;
|
|
|
56
56
|
} else {
|
|
57
57
|
insightText = await result;
|
|
58
58
|
}
|
|
59
|
-
} catch (err:
|
|
60
|
-
if (err.name !== 'AbortError') {
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
if (err instanceof Error && err.name !== 'AbortError') {
|
|
61
61
|
insightText = 'Failed to generate insights for this context.';
|
|
62
62
|
}
|
|
63
63
|
} finally {
|
|
@@ -115,7 +115,7 @@ Keep it concise, professional, and use bullet points.`;
|
|
|
115
115
|
</span>
|
|
116
116
|
{:else}
|
|
117
117
|
<Button variant="ghost" size="icon-sm" class="h-5 w-5 hover:bg-muted" onclick={getInsights}>
|
|
118
|
-
<
|
|
118
|
+
<RefreshCcw class="h-3 w-3" />
|
|
119
119
|
</Button>
|
|
120
120
|
{/if}
|
|
121
121
|
</h4>
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
{#if multiVal.length > 0}
|
|
222
222
|
<div class="flex flex-wrap gap-1 mt-1">
|
|
223
223
|
{#each multiVal as v}
|
|
224
|
-
{@const label = field.options?.find((o:
|
|
224
|
+
{@const label = field.options?.find((o: { label: string; value: string | number }) => o.value === v)?.label ?? String(v)}
|
|
225
225
|
<Badge variant="secondary" class="gap-1">
|
|
226
226
|
{label}
|
|
227
227
|
<button type="button" onclick={() => toggleMulti(v)} class="ml-0.5 rounded-sm hover:text-destructive hover:bg-destructive/10 transition-colors" aria-label={t('common.clear')}>×</button>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}>();
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<header class="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between bg-
|
|
24
|
+
<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">
|
|
25
25
|
<div class="flex items-center gap-4">
|
|
26
26
|
{#if children}
|
|
27
27
|
{@render children()}
|
|
@@ -55,8 +55,8 @@ ${context}`;
|
|
|
55
55
|
} else {
|
|
56
56
|
insightText = await result;
|
|
57
57
|
}
|
|
58
|
-
} catch (err:
|
|
59
|
-
if (err.name !== 'AbortError') {
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
if (err instanceof Error && err.name !== 'AbortError') {
|
|
60
60
|
insightText = 'Failed to generate insights.';
|
|
61
61
|
}
|
|
62
62
|
} finally {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
</div>
|
|
84
84
|
</div>
|
|
85
85
|
{:else}
|
|
86
|
-
<div class="flex h-screen bg-
|
|
86
|
+
<div class="flex h-screen bg-background" in:fade={{ duration: 200, delay: 50 }}>
|
|
87
87
|
<!-- Desktop sidebar -->
|
|
88
88
|
<div class="hidden md:block">
|
|
89
89
|
<Sidebar {collapsed} {identity} {title} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
}>();
|
|
17
17
|
|
|
18
18
|
const statusConfigs = {
|
|
19
|
-
connected: { variant: 'default' as const, dotClass: 'bg-
|
|
20
|
-
connecting: { variant: 'secondary' as const, dotClass: 'bg-
|
|
21
|
-
disconnected: { variant: 'destructive' as const, dotClass: 'bg-
|
|
19
|
+
connected: { variant: 'default' as const, dotClass: 'bg-success', label: 'Live', icon: Wifi },
|
|
20
|
+
connecting: { variant: 'secondary' as const, dotClass: 'bg-warning', label: 'Connecting...', icon: Loader2 as typeof Wifi },
|
|
21
|
+
disconnected: { variant: 'destructive' as const, dotClass: 'bg-destructive', label: 'Offline', icon: WifiOff },
|
|
22
22
|
} as const;
|
|
23
23
|
type StatusKey = 'connected' | 'connecting' | 'disconnected';
|
|
24
24
|
const cfg = $derived(statusConfigs[status as StatusKey]);
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
pres.forEach(pre => {
|
|
70
70
|
pre.classList.add('enhanced');
|
|
71
71
|
const wrapper = document.createElement('div');
|
|
72
|
-
wrapper.className = 'code-block-wrapper group relative my-4 rounded-md bg-
|
|
72
|
+
wrapper.className = 'code-block-wrapper group relative my-4 rounded-md bg-foreground/95';
|
|
73
73
|
|
|
74
74
|
const header = document.createElement('div');
|
|
75
75
|
header.className = 'flex items-center justify-between px-4 py-2 text-xs text-zinc-400 border-b border-zinc-800';
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
|
|
44
44
|
const strengthColor = $derived(
|
|
45
45
|
strength < 30 ? 'bg-destructive' :
|
|
46
|
-
strength < 60 ? 'bg-
|
|
47
|
-
strength < 80 ? 'bg-
|
|
46
|
+
strength < 60 ? 'bg-warning' :
|
|
47
|
+
strength < 80 ? 'bg-info' : 'bg-success'
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
const strengthLabel = $derived(
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
{/if}
|
|
131
131
|
|
|
132
132
|
{#if pwSuccess}
|
|
133
|
-
<Alert.Root class="border-
|
|
133
|
+
<Alert.Root class="border-success/30 bg-success/5 text-success">
|
|
134
134
|
<CheckCircle class="h-4 w-4" />
|
|
135
135
|
<Alert.Description>{t('profile.passwordChanged')}</Alert.Description>
|
|
136
136
|
</Alert.Root>
|
|
@@ -88,8 +88,8 @@ Do not include any other text, Markdown formatting, quotes, or explanations.`;
|
|
|
88
88
|
|
|
89
89
|
// Clean up the suggestion
|
|
90
90
|
suggestion = suggestion.trim().replace(/^["']|["']$/g, '');
|
|
91
|
-
} catch (err:
|
|
92
|
-
if (err.name !== 'AbortError') {
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
if (err instanceof Error && err.name !== 'AbortError') {
|
|
93
93
|
suggestion = '';
|
|
94
94
|
}
|
|
95
95
|
} finally {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
const colorMap: Record<ColorVariant, string> = {
|
|
9
9
|
primary: 'bg-primary/10 text-primary',
|
|
10
|
-
success: 'bg-
|
|
11
|
-
warning: 'bg-
|
|
12
|
-
danger: 'bg-
|
|
13
|
-
info: 'bg-
|
|
10
|
+
success: 'bg-success/10 text-success',
|
|
11
|
+
warning: 'bg-warning/10 text-warning',
|
|
12
|
+
danger: 'bg-destructive/10 text-destructive',
|
|
13
|
+
info: 'bg-info/10 text-info',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const variantMap: Record<StyleVariant, string> = {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<p class="text-xl font-semibold text-foreground">{value}</p>
|
|
61
61
|
{#if trend}
|
|
62
62
|
<span
|
|
63
|
-
class="text-xs font-medium {trend.value >= 0 ? 'text-
|
|
63
|
+
class="text-xs font-medium {trend.value >= 0 ? 'text-success' : 'text-destructive'}"
|
|
64
64
|
>
|
|
65
65
|
{trend.value >= 0 ? '↑' : '↓'}{Math.abs(trend.value)}%
|
|
66
66
|
{#if trend.label}
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
import { Mic, MicOff, Loader2 } from 'lucide-svelte';
|
|
3
3
|
import TooltipButton from './TooltipButton.svelte';
|
|
4
4
|
|
|
5
|
+
// Web Speech API type declarations
|
|
6
|
+
interface SpeechRecognitionEvent extends Event {
|
|
7
|
+
results: SpeechRecognitionResultList;
|
|
8
|
+
}
|
|
9
|
+
interface SpeechRecognitionErrorEvent extends Event {
|
|
10
|
+
error: string;
|
|
11
|
+
}
|
|
12
|
+
interface SpeechRecognitionInstance extends EventTarget {
|
|
13
|
+
continuous: boolean;
|
|
14
|
+
interimResults: boolean;
|
|
15
|
+
lang: string;
|
|
16
|
+
onstart: (() => void) | null;
|
|
17
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null;
|
|
18
|
+
onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;
|
|
19
|
+
onend: (() => void) | null;
|
|
20
|
+
start(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
interface Props {
|
|
6
25
|
/** Called when a speech recognition result is finalized */
|
|
7
26
|
onresult?: (text: string) => void;
|
|
@@ -25,23 +44,23 @@
|
|
|
25
44
|
|
|
26
45
|
let isListening = $state(false);
|
|
27
46
|
let notSupported = $state(false);
|
|
28
|
-
let recognition:
|
|
47
|
+
let recognition: SpeechRecognitionInstance | null = null;
|
|
29
48
|
|
|
30
49
|
// Initialize SpeechRecognition on mount
|
|
31
50
|
$effect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
const SpeechRecognitionCtor = (
|
|
52
|
+
(globalThis as Record<string, unknown>).SpeechRecognition ||
|
|
53
|
+
(globalThis as Record<string, unknown>).webkitSpeechRecognition
|
|
54
|
+
) as (new () => SpeechRecognitionInstance) | undefined;
|
|
34
55
|
|
|
35
|
-
if (!
|
|
56
|
+
if (!SpeechRecognitionCtor) {
|
|
36
57
|
notSupported = true;
|
|
37
58
|
return;
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
recognition = new
|
|
61
|
+
recognition = new SpeechRecognitionCtor();
|
|
41
62
|
recognition.continuous = false;
|
|
42
|
-
// Tries to return interim results, but we mainly care about final
|
|
43
63
|
recognition.interimResults = false;
|
|
44
|
-
// Uses the browser locale by default
|
|
45
64
|
recognition.lang = navigator.language || 'en-US';
|
|
46
65
|
|
|
47
66
|
recognition.onstart = () => {
|
|
@@ -49,10 +68,10 @@
|
|
|
49
68
|
if (onstart) onstart();
|
|
50
69
|
};
|
|
51
70
|
|
|
52
|
-
recognition.onresult = (event:
|
|
71
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
53
72
|
const transcript = Array.from(event.results)
|
|
54
|
-
.map((result:
|
|
55
|
-
.map((result:
|
|
73
|
+
.map((result: SpeechRecognitionResult) => result[0])
|
|
74
|
+
.map((result: SpeechRecognitionAlternative) => result.transcript)
|
|
56
75
|
.join('');
|
|
57
76
|
|
|
58
77
|
if (onresult && transcript) {
|
|
@@ -60,7 +79,7 @@
|
|
|
60
79
|
}
|
|
61
80
|
};
|
|
62
81
|
|
|
63
|
-
recognition.onerror = (event:
|
|
82
|
+
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
|
64
83
|
console.warn('SpeechRecognition error:', event.error);
|
|
65
84
|
isListening = false;
|
|
66
85
|
if (onend) onend();
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
let { value, trueIcon = '✓', falseIcon = '✗' }: Props = $props();
|
|
4
4
|
</script>
|
|
5
5
|
{#if value}
|
|
6
|
-
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-
|
|
6
|
+
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-success/10 text-success">
|
|
7
7
|
{trueIcon}
|
|
8
8
|
</span>
|
|
9
9
|
{:else}
|
|
10
|
-
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-
|
|
10
|
+
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-destructive/10 text-destructive">
|
|
11
11
|
{falseIcon}
|
|
12
12
|
</span>
|
|
13
13
|
{/if}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
}>();
|
|
8
8
|
|
|
9
9
|
const label = $derived(
|
|
10
|
-
options?.find((o:
|
|
10
|
+
options?.find((o: { label: string; value: string | number }) => o.value === value)?.label ?? (value != null ? String(value) : '—')
|
|
11
11
|
);
|
|
12
12
|
</script>
|
|
13
13
|
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
variant: {
|
|
10
10
|
default: "bg-card text-foreground",
|
|
11
11
|
destructive: "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
|
12
|
-
warning: "border-
|
|
13
|
-
success: "border-
|
|
12
|
+
warning: "border-warning/50 text-warning-foreground bg-warning/10 [&>svg]:text-current",
|
|
13
|
+
success: "border-success/50 text-success bg-success/10 [&>svg]:text-current",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: { variant: "default" },
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,12 @@ export { Command } from './components/ui/command/index.js';
|
|
|
109
109
|
// Utils
|
|
110
110
|
export { cn } from './utils';
|
|
111
111
|
|
|
112
|
+
// Component Registry (DI)
|
|
113
|
+
export {
|
|
114
|
+
setComponentRegistry, getComponentRegistry, useComponent,
|
|
115
|
+
type ComponentRegistry,
|
|
116
|
+
} from './component-registry.svelte';
|
|
117
|
+
|
|
112
118
|
// Field component registry
|
|
113
119
|
export {
|
|
114
120
|
builtinDisplayComponents, registerDisplayComponent,
|