@svadmin/ui 0.65.1 → 0.67.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 (34) hide show
  1. package/dist/app.css +0 -51
  2. package/dist/app.theme.css +0 -51
  3. package/dist/components/AdminApp.svelte +5 -2
  4. package/dist/components/AdminApp.svelte.d.ts +3 -0
  5. package/dist/components/CommandPalette.svelte +3 -1
  6. package/dist/components/Layout.svelte +20 -12
  7. package/dist/components/Layout.svelte.d.ts +6 -0
  8. package/dist/components/MetricStrip.svelte +149 -0
  9. package/dist/components/MetricStrip.svelte.d.ts +32 -0
  10. package/dist/components/ui/badge/badge.svelte +5 -0
  11. package/dist/components/ui/badge/badge.svelte.d.ts +15 -0
  12. package/dist/components/ui/table/table-cell.svelte +14 -2
  13. package/dist/components/ui/table/table-cell.svelte.d.ts +4 -1
  14. package/dist/components/ui/table/table-head.svelte +14 -2
  15. package/dist/components/ui/table/table-head.svelte.d.ts +4 -1
  16. package/dist/components/ui/table/table-row.svelte +1 -1
  17. package/dist/index.d.ts +3 -7
  18. package/dist/index.js +1 -7
  19. package/package.json +3 -14
  20. package/dist/components/AICommandBar.svelte +0 -228
  21. package/dist/components/AICommandBar.svelte.d.ts +0 -6
  22. package/dist/components/ChatDialog.svelte +0 -947
  23. package/dist/components/ChatDialog.svelte.d.ts +0 -24
  24. package/dist/components/CopilotPanel.svelte +0 -204
  25. package/dist/components/CopilotPanel.svelte.d.ts +0 -6
  26. package/dist/components/InsightCard.svelte +0 -139
  27. package/dist/components/InsightCard.svelte.d.ts +0 -11
  28. package/dist/components/MarkdownRenderer.svelte +0 -245
  29. package/dist/components/MarkdownRenderer.svelte.d.ts +0 -11
  30. package/dist/components/SmartSuggest.svelte +0 -207
  31. package/dist/components/SmartSuggest.svelte.d.ts +0 -19
  32. package/dist/components/VoiceInput.svelte +0 -133
  33. package/dist/components/VoiceInput.svelte.d.ts +0 -15
  34. package/dist/markdown-deps.d.ts +0 -21
package/dist/app.css CHANGED
@@ -573,57 +573,6 @@ main {
573
573
  /* ─── svadmin UI Components CSS ─────────────────────────────── */
574
574
  /* These rules were moved from .svelte files to eliminate <style> tags */
575
575
 
576
- /* MarkdownRenderer Styles */
577
- .prose {
578
- line-height: 1.6;
579
- }
580
- .prose p {
581
- margin-top: 0.5em;
582
- margin-bottom: 0.5em;
583
- }
584
-
585
- /* Streaming cursor effect */
586
- .streaming > *:last-child::after {
587
- content: '▋';
588
- display: inline-block;
589
- animation: svadmin-blink 1s step-start infinite;
590
- margin-left: 2px;
591
- vertical-align: baseline;
592
- color: currentColor;
593
- }
594
-
595
- @keyframes svadmin-blink {
596
- 50% { opacity: 0; }
597
- }
598
-
599
- /* Handle copied state for dynamically injected buttons.
600
- * The check icon itself is swapped in by MarkdownRenderer (createCheckIcon)
601
- * with stroke="currentColor" so it inherits this semantic token color. */
602
- .copy-btn[data-copied-id] {
603
- color: var(--success);
604
- }
605
-
606
- /* ChatDialog Styles */
607
- .chat-markdown pre.chat-code-block {
608
- background: var(--color-muted, hsl(var(--muted)));
609
- border-radius: 0.5rem;
610
- padding: 0.75rem;
611
- margin: 0.5rem 0;
612
- overflow-x: auto;
613
- font-size: 0.75rem;
614
- line-height: 1.5;
615
- }
616
- .chat-markdown code.chat-inline-code {
617
- background: var(--color-muted, hsl(var(--muted)));
618
- padding: 0.125rem 0.375rem;
619
- border-radius: 0.25rem;
620
- font-size: 0.8125em;
621
- font-family: ui-monospace, monospace;
622
- }
623
- .chat-markdown strong {
624
- font-weight: 600;
625
- }
626
-
627
576
  /* Collapsible details normalization stays scoped to svadmin-owned hooks. */
628
577
  details[data-svadmin-filter]:not([open]) > :not(summary),
629
578
  details.svadmin-collapsible-filter:not([open]) > :not(summary),
@@ -572,57 +572,6 @@ main {
572
572
  /* ─── svadmin UI Components CSS ─────────────────────────────── */
573
573
  /* These rules were moved from .svelte files to eliminate <style> tags */
574
574
 
575
- /* MarkdownRenderer Styles */
576
- .prose {
577
- line-height: 1.6;
578
- }
579
- .prose p {
580
- margin-top: 0.5em;
581
- margin-bottom: 0.5em;
582
- }
583
-
584
- /* Streaming cursor effect */
585
- .streaming > *:last-child::after {
586
- content: '▋';
587
- display: inline-block;
588
- animation: svadmin-blink 1s step-start infinite;
589
- margin-left: 2px;
590
- vertical-align: baseline;
591
- color: currentColor;
592
- }
593
-
594
- @keyframes svadmin-blink {
595
- 50% { opacity: 0; }
596
- }
597
-
598
- /* Handle copied state for dynamically injected buttons.
599
- * The check icon itself is swapped in by MarkdownRenderer (createCheckIcon)
600
- * with stroke="currentColor" so it inherits this semantic token color. */
601
- .copy-btn[data-copied-id] {
602
- color: var(--success);
603
- }
604
-
605
- /* ChatDialog Styles */
606
- .chat-markdown pre.chat-code-block {
607
- background: var(--color-muted, hsl(var(--muted)));
608
- border-radius: 0.5rem;
609
- padding: 0.75rem;
610
- margin: 0.5rem 0;
611
- overflow-x: auto;
612
- font-size: 0.75rem;
613
- line-height: 1.5;
614
- }
615
- .chat-markdown code.chat-inline-code {
616
- background: var(--color-muted, hsl(var(--muted)));
617
- padding: 0.125rem 0.375rem;
618
- border-radius: 0.25rem;
619
- font-size: 0.8125em;
620
- font-family: ui-monospace, monospace;
621
- }
622
- .chat-markdown strong {
623
- font-weight: 600;
624
- }
625
-
626
575
  /* Collapsible details normalization stays scoped to svadmin-owned hooks. */
627
576
  details[data-svadmin-filter]:not([open]) > :not(summary),
628
577
  details.svadmin-collapsible-filter:not([open]) > :not(summary),
@@ -56,7 +56,7 @@
56
56
  import { useTranslation } from '@svadmin/core/i18n';
57
57
  import { QueryClient, QueryClientProvider, type DefaultOptions } from '@tanstack/svelte-query';
58
58
  import { setComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
59
- import Layout from './Layout.svelte';
59
+ import Layout, { type LayoutAIAssistantProps } from './Layout.svelte';
60
60
  import AutoTable from './AutoTable.svelte';
61
61
  import AutoForm from './AutoForm.svelte';
62
62
  import ShowPage from './ShowPage.svelte';
@@ -114,6 +114,8 @@
114
114
  /** Theme configuration for dark-first mode, CSS overrides, etc. */
115
115
  themeConfig?: ThemeConfig;
116
116
  dashboard?: Snippet;
117
+ /** Optional AI assistant rendered by Layout. Keep AI UI in @svadmin/ai-elements. */
118
+ aiAssistant?: Snippet<[LayoutAIAssistantProps]>;
117
119
  loginPage?: Snippet;
118
120
  /** Prefill and display credentials on the default login page, useful for demos/examples. */
119
121
  loginDefaults?: {
@@ -169,6 +171,7 @@
169
171
  defaultTheme,
170
172
  themeConfig: userThemeConfig,
171
173
  dashboard,
174
+ aiAssistant,
172
175
  loginPage,
173
176
  loginDefaults,
174
177
  components: userComponents,
@@ -548,7 +551,7 @@
548
551
  <ErrorComp status={standaloneErrorStatus} />
549
552
  </SystemPageShell>
550
553
  {:else if isAuthenticated || !resolvedAuthProvider}
551
- <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
554
+ <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode} {aiAssistant}>
552
555
  <div class="relative min-h-40" aria-busy={authRechecking}>
553
556
  {#if authRechecking}
554
557
  <div class="absolute inset-0 z-10 flex min-h-40 items-center justify-center bg-background/80" role="status" aria-live="polite">
@@ -3,6 +3,7 @@ import { type Component, type Snippet } from 'svelte';
3
3
  import type { AccessControlProvider, AgentProvider, AuditLogProvider, AuthProvider, ChatProvider, CredentialProvider, DataProviderInput, IdentityGovernanceProvider, I18nProvider, LiveProvider, MenuItem, NotificationProvider, OrganizationProvider, ResourceDefinition, RouterProvider, SessionProvider, TaskProvider, TenantAdapter, TenantContext, ThemeConfig, ThemeMode } from '@svadmin/core';
4
4
  import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
5
5
  import { type ComponentRegistry } from '../component-registry.svelte.js';
6
+ import { type LayoutAIAssistantProps } from './Layout.svelte';
6
7
  import type { AdminProviderBundle } from '../types.js';
7
8
  interface Props {
8
9
  /** Single provider, or named providers such as `{ default, analytics }`. */
@@ -37,6 +38,8 @@ interface Props {
37
38
  /** Theme configuration for dark-first mode, CSS overrides, etc. */
38
39
  themeConfig?: ThemeConfig;
39
40
  dashboard?: Snippet;
41
+ /** Optional AI assistant rendered by Layout. Keep AI UI in @svadmin/ai-elements. */
42
+ aiAssistant?: Snippet<[LayoutAIAssistantProps]>;
40
43
  loginPage?: Snippet;
41
44
  /** Prefill and display credentials on the default login page, useful for demos/examples. */
42
45
  loginDefaults?: {
@@ -20,7 +20,9 @@
20
20
  let searchValue = $state('');
21
21
 
22
22
  const resources = $derived(getResources());
23
- const hasAI = $derived(!!(adminContext.agentProvider || adminContext.chatProvider));
23
+ const hasAI = $derived(
24
+ !!(adminContext.agentProvider || adminContext.chatProvider) && typeof onAskAI === 'function',
25
+ );
24
26
 
25
27
  function close() {
26
28
  open = false;
@@ -1,3 +1,11 @@
1
+ <script module lang="ts">
2
+ export interface LayoutAIAssistantProps {
3
+ docked: boolean;
4
+ scope: string;
5
+ ownerScope: string;
6
+ }
7
+ </script>
8
+
1
9
  <script lang="ts">
2
10
  /* eslint-disable svelte/no-useless-children-snippet */
3
11
  import type { Snippet } from 'svelte';
@@ -6,7 +14,6 @@
6
14
  import Header from './Header.svelte';
7
15
  import CommandPalette from './CommandPalette.svelte';
8
16
  import KeyboardShortcuts from './KeyboardShortcuts.svelte';
9
- import ChatDialog from './ChatDialog.svelte';
10
17
  import DevTools from './DevTools.svelte';
11
18
  import { useTranslation } from '@svadmin/core/i18n';
12
19
 
@@ -25,7 +32,7 @@
25
32
  let shortcutsOpen = $state(false);
26
33
  let mobileMenuOpen = $state(false);
27
34
 
28
- let { children, title = 'Admin', menu, siteUrl, routeMode = 'auto' }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string; routeMode?: 'hash' | 'path' | 'auto' } = $props();
35
+ let { children, title = 'Admin', menu, siteUrl, routeMode = 'auto', aiAssistant }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string; routeMode?: 'hash' | 'path' | 'auto'; aiAssistant?: Snippet<[LayoutAIAssistantProps]> } = $props();
29
36
  const layoutId = $props.id();
30
37
  const layoutScope = `svadmin-layout-${layoutId}`;
31
38
  const mainContentId = `${layoutScope}-main`;
@@ -73,6 +80,13 @@
73
80
  document.getElementById(mainContentId)?.focus();
74
81
  }
75
82
 
83
+ function handleAskAI(query: string) {
84
+ if (!aiAssistant || typeof window === 'undefined') return;
85
+ window.dispatchEvent(new CustomEvent('svadmin:ask-ai', {
86
+ detail: { query, scope: chatScope },
87
+ }));
88
+ }
89
+
76
90
  let collapsed = $state(false);
77
91
 
78
92
  // Swipe gesture for mobile menu
@@ -235,19 +249,13 @@
235
249
 
236
250
  <footer class="flex min-h-14 shrink-0 items-center justify-end gap-2 border-t border-border/60 bg-background px-4 empty:hidden">
237
251
  <DevTools docked />
238
- <ChatDialog docked scope={chatScope} ownerScope={layoutScope} />
252
+ {@render aiAssistant?.({ docked: true, scope: chatScope, ownerScope: layoutScope })}
239
253
  </footer>
240
254
  </div>
241
255
  </div>
242
- <CommandPalette
243
- bind:open={commandOpen}
244
- onAskAI={(q) => {
245
- if (typeof window !== 'undefined') {
246
- window.dispatchEvent(new CustomEvent('svadmin:ask-ai', {
247
- detail: { query: q, scope: chatScope },
248
- }));
249
- }
250
- }}
256
+ <CommandPalette
257
+ bind:open={commandOpen}
258
+ onAskAI={aiAssistant ? handleAskAI : undefined}
251
259
  />
252
260
  <KeyboardShortcuts bind:open={shortcutsOpen} />
253
261
  {/if}
@@ -1,3 +1,8 @@
1
+ export interface LayoutAIAssistantProps {
2
+ docked: boolean;
3
+ scope: string;
4
+ ownerScope: string;
5
+ }
1
6
  import type { Snippet } from 'svelte';
2
7
  import type { MenuItem } from '@svadmin/core';
3
8
  type $$ComponentProps = {
@@ -6,6 +11,7 @@ type $$ComponentProps = {
6
11
  menu?: MenuItem[];
7
12
  siteUrl?: string;
8
13
  routeMode?: 'hash' | 'path' | 'auto';
14
+ aiAssistant?: Snippet<[LayoutAIAssistantProps]>;
9
15
  };
10
16
  declare const Layout: import("svelte").Component<$$ComponentProps, {}, "">;
11
17
  type Layout = ReturnType<typeof Layout>;
@@ -0,0 +1,149 @@
1
+ <script lang="ts">
2
+ import { Skeleton } from './ui/skeleton/index.js';
3
+ import { cn } from '../utils.js';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ export type MetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
7
+
8
+ export interface MetricStripItem {
9
+ id?: string;
10
+ label: string;
11
+ value: string | number;
12
+ tone?: MetricTone;
13
+ badge?: {
14
+ text: string;
15
+ tone?: 'default' | 'success' | 'warning' | 'danger' | 'info';
16
+ };
17
+ icon?: import('svelte').Component<{ class?: string }>;
18
+ href?: string;
19
+ trend?: {
20
+ value: number;
21
+ label?: string;
22
+ };
23
+ loading?: boolean;
24
+ class?: string;
25
+ }
26
+
27
+ interface Props {
28
+ items?: MetricStripItem[];
29
+ columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
30
+ ariaLabel?: string;
31
+ class?: string;
32
+ children?: Snippet;
33
+ }
34
+
35
+ let {
36
+ items = [],
37
+ columns = 'auto',
38
+ ariaLabel = 'Metrics overview',
39
+ class: className = '',
40
+ children,
41
+ }: Props = $props();
42
+
43
+ const columnClassMap = {
44
+ 2: 'grid-cols-1 sm:grid-cols-2',
45
+ 3: 'grid-cols-1 sm:grid-cols-3',
46
+ 4: 'grid-cols-2 sm:grid-cols-4',
47
+ 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
48
+ 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
49
+ auto: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
50
+ };
51
+
52
+ const toneValueColorMap: Record<MetricTone, string> = {
53
+ default: 'text-foreground',
54
+ primary: 'text-primary',
55
+ success: 'text-success',
56
+ warning: 'text-warning',
57
+ danger: 'text-destructive',
58
+ info: 'text-info',
59
+ };
60
+
61
+ const badgeToneMap: Record<
62
+ NonNullable<NonNullable<MetricStripItem['badge']>['tone']>,
63
+ string
64
+ > = {
65
+ default: 'bg-muted text-muted-foreground border-border',
66
+ success: 'bg-success/10 text-success border-success/20',
67
+ warning: 'bg-warning/10 text-warning border-warning/20',
68
+ danger: 'bg-destructive/10 text-destructive border-destructive/20',
69
+ info: 'bg-info/10 text-info border-info/20',
70
+ };
71
+ </script>
72
+
73
+ <div
74
+ role="region"
75
+ aria-label={ariaLabel}
76
+ class={cn(
77
+ 'grid gap-px overflow-hidden rounded-lg border border-border bg-border shadow-xs',
78
+ columnClassMap[columns] || columnClassMap.auto,
79
+ className
80
+ )}
81
+ >
82
+ {#if children}
83
+ {@render children()}
84
+ {:else}
85
+ {#each items as item, index (item.id || index)}
86
+ {@const isInteractive = Boolean(item.href)}
87
+ <svelte:element
88
+ this={isInteractive ? 'a' : 'div'}
89
+ href={item.href}
90
+ class={cn(
91
+ 'group relative flex flex-col justify-between gap-1 bg-card p-3.5 text-left transition-colors duration-150 sm:p-4',
92
+ isInteractive && 'cursor-pointer text-inherit no-underline hover:bg-muted/40',
93
+ item.class
94
+ )}
95
+ >
96
+ <div class="flex items-center justify-between gap-2">
97
+ <span class="truncate text-xs font-semibold text-muted-foreground">{item.label}</span>
98
+ {#if item.icon}
99
+ <span class="shrink-0 text-muted-foreground/70">
100
+ <item.icon class="h-3.5 w-3.5" />
101
+ </span>
102
+ {/if}
103
+ </div>
104
+
105
+ {#if item.loading}
106
+ <div class="mt-1 flex items-center justify-between">
107
+ <Skeleton class="h-6 w-16" />
108
+ </div>
109
+ {:else}
110
+ <div class="mt-0.5 flex items-baseline justify-between gap-2">
111
+ <strong
112
+ class={cn(
113
+ 'text-xl font-bold tracking-tight tabular-nums sm:text-2xl',
114
+ toneValueColorMap[item.tone || 'default']
115
+ )}
116
+ >
117
+ {item.value}
118
+ </strong>
119
+
120
+ {#if item.badge}
121
+ <span
122
+ class={cn(
123
+ 'inline-flex items-center rounded-full border px-1.5 py-0.5 text-[11px] font-semibold',
124
+ badgeToneMap[item.badge.tone || 'default']
125
+ )}
126
+ >
127
+ {item.badge.text}
128
+ </span>
129
+ {:else if item.trend}
130
+ <span
131
+ class={cn(
132
+ 'inline-flex items-center gap-0.5 text-xs font-medium tabular-nums',
133
+ item.trend.value >= 0 ? 'text-success' : 'text-destructive'
134
+ )}
135
+ aria-label="{item.trend.value >= 0 ? '上升' : '下降'} {Math.abs(item.trend.value)}%"
136
+ >
137
+ <span aria-hidden="true">{item.trend.value >= 0 ? '↑' : '↓'}</span>
138
+ <span>{Math.abs(item.trend.value)}%</span>
139
+ {#if item.trend.label}
140
+ <span class="ml-0.5 text-muted-foreground">{item.trend.label}</span>
141
+ {/if}
142
+ </span>
143
+ {/if}
144
+ </div>
145
+ {/if}
146
+ </svelte:element>
147
+ {/each}
148
+ {/if}
149
+ </div>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type MetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
3
+ export interface MetricStripItem {
4
+ id?: string;
5
+ label: string;
6
+ value: string | number;
7
+ tone?: MetricTone;
8
+ badge?: {
9
+ text: string;
10
+ tone?: 'default' | 'success' | 'warning' | 'danger' | 'info';
11
+ };
12
+ icon?: import('svelte').Component<{
13
+ class?: string;
14
+ }>;
15
+ href?: string;
16
+ trend?: {
17
+ value: number;
18
+ label?: string;
19
+ };
20
+ loading?: boolean;
21
+ class?: string;
22
+ }
23
+ interface Props {
24
+ items?: MetricStripItem[];
25
+ columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
26
+ ariaLabel?: string;
27
+ class?: string;
28
+ children?: Snippet;
29
+ }
30
+ declare const MetricStrip: import("svelte").Component<Props, {}, "">;
31
+ type MetricStrip = ReturnType<typeof MetricStrip>;
32
+ export default MetricStrip;
@@ -8,6 +8,11 @@
8
8
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
9
9
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
10
10
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
11
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15",
12
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15",
13
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15",
14
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15",
15
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium",
11
16
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
12
17
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
13
18
  link: "text-primary underline-offset-4 hover:underline",
@@ -4,6 +4,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
4
4
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
5
5
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
6
6
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
7
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
8
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
9
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
10
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
11
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
7
12
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
8
13
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
9
14
  link: "text-primary underline-offset-4 hover:underline";
@@ -13,6 +18,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
13
18
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
14
19
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
15
20
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
21
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
22
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
23
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
24
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
25
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
16
26
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
17
27
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
18
28
  link: "text-primary underline-offset-4 hover:underline";
@@ -22,6 +32,11 @@ export declare const badgeVariants: import("tailwind-variants").TVReturnType<{
22
32
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80";
23
33
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80";
24
34
  destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20";
35
+ subtle: "bg-primary/10 text-primary border-primary/20 [a]:hover:bg-primary/15";
36
+ "subtle-success": "bg-success/10 text-success border-success/20 [a]:hover:bg-success/15";
37
+ "subtle-warning": "bg-warning/10 text-warning border-warning/20 [a]:hover:bg-warning/15";
38
+ "subtle-destructive": "bg-destructive/10 text-destructive border-destructive/20 [a]:hover:bg-destructive/15";
39
+ "subtle-pill": "rounded-full bg-muted/70 text-muted-foreground border-border/80 font-medium";
25
40
  outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground";
26
41
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50";
27
42
  link: "text-primary underline-offset-4 hover:underline";
@@ -4,19 +4,31 @@
4
4
  import type { HTMLTdAttributes } from "svelte/elements";
5
5
  import type { TableDensity } from "./table.svelte";
6
6
 
7
+ type Props = WithElementRef<HTMLTdAttributes, HTMLTableCellElement> & {
8
+ sticky?: "left" | "right" | boolean;
9
+ };
10
+
7
11
  let {
8
12
  ref = $bindable(null),
13
+ sticky = false,
9
14
  class: className,
10
15
  children,
11
16
  ...restProps
12
- }: WithElementRef<HTMLTdAttributes, HTMLTableCellElement> = $props();
17
+ }: Props = $props();
13
18
 
14
19
  const getDensity = getContext<() => TableDensity>("svadmin-table-density");
15
20
  const densityClass = $derived(getDensity?.() === "compact"
16
21
  ? "h-8 px-2 py-1 text-xs"
17
22
  : "p-2");
23
+ const stickyClass = $derived(
24
+ sticky === "right" || sticky === true
25
+ ? "sticky right-0 z-10 bg-background shadow-[-1px_0_0_var(--border)] group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted"
26
+ : sticky === "left"
27
+ ? "sticky left-0 z-10 bg-background shadow-[1px_0_0_var(--border)] group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted"
28
+ : ""
29
+ );
18
30
  </script>
19
31
 
20
- <td bind:this={ref} data-slot="table-cell" class={cn("align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
32
+ <td bind:this={ref} data-slot="table-cell" data-sticky={sticky ? (typeof sticky === "string" ? sticky : "right") : undefined} class={cn("align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, stickyClass, className)} {...restProps}>
21
33
  {@render children?.()}
22
34
  </td>
@@ -1,5 +1,8 @@
1
1
  import { type WithElementRef } from "../../../utils.js";
2
2
  import type { HTMLTdAttributes } from "svelte/elements";
3
- declare const TableCell: import("svelte").Component<WithElementRef<HTMLTdAttributes, HTMLTableCellElement>, {}, "ref">;
3
+ type Props = WithElementRef<HTMLTdAttributes, HTMLTableCellElement> & {
4
+ sticky?: "left" | "right" | boolean;
5
+ };
6
+ declare const TableCell: import("svelte").Component<Props, {}, "ref">;
4
7
  type TableCell = ReturnType<typeof TableCell>;
5
8
  export default TableCell;
@@ -4,19 +4,31 @@
4
4
  import type { HTMLThAttributes } from "svelte/elements";
5
5
  import type { TableDensity } from "./table.svelte";
6
6
 
7
+ type Props = WithElementRef<HTMLThAttributes, HTMLTableCellElement> & {
8
+ sticky?: "left" | "right" | boolean;
9
+ };
10
+
7
11
  let {
8
12
  ref = $bindable(null),
13
+ sticky = false,
9
14
  class: className,
10
15
  children,
11
16
  ...restProps
12
- }: WithElementRef<HTMLThAttributes, HTMLTableCellElement> = $props();
17
+ }: Props = $props();
13
18
 
14
19
  const getDensity = getContext<() => TableDensity>("svadmin-table-density");
15
20
  const densityClass = $derived(getDensity?.() === "compact"
16
21
  ? "h-8 px-2 py-1 text-xs"
17
22
  : "h-9 px-2");
23
+ const stickyClass = $derived(
24
+ sticky === "right" || sticky === true
25
+ ? "sticky right-0 z-20 bg-background shadow-[-1px_0_0_var(--border)]"
26
+ : sticky === "left"
27
+ ? "sticky left-0 z-20 bg-background shadow-[1px_0_0_var(--border)]"
28
+ : ""
29
+ );
18
30
  </script>
19
31
 
20
- <th bind:this={ref} data-slot="table-head" class={cn("text-foreground text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, className)} {...restProps}>
32
+ <th bind:this={ref} data-slot="table-head" data-sticky={sticky ? (typeof sticky === "string" ? sticky : "right") : undefined} class={cn("text-foreground text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", densityClass, stickyClass, className)} {...restProps}>
21
33
  {@render children?.()}
22
34
  </th>
@@ -1,5 +1,8 @@
1
1
  import { type WithElementRef } from "../../../utils.js";
2
2
  import type { HTMLThAttributes } from "svelte/elements";
3
- declare const TableHead: import("svelte").Component<WithElementRef<HTMLThAttributes, HTMLTableCellElement>, {}, "ref">;
3
+ type Props = WithElementRef<HTMLThAttributes, HTMLTableCellElement> & {
4
+ sticky?: "left" | "right" | boolean;
5
+ };
6
+ declare const TableHead: import("svelte").Component<Props, {}, "ref">;
4
7
  type TableHead = ReturnType<typeof TableHead>;
5
8
  export default TableHead;
@@ -10,6 +10,6 @@
10
10
  }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
11
11
  </script>
12
12
 
13
- <tr bind:this={ref} data-slot="table-row" class={cn("hover:bg-muted/50 data-[state=selected]:bg-muted transition-colors", className)} {...restProps}>
13
+ <tr bind:this={ref} data-slot="table-row" class={cn("group/row hover:bg-muted/50 data-[state=selected]:bg-muted transition-colors", className)} {...restProps}>
14
14
  {@render children?.()}
15
15
  </tr>
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { default as ShowPage } from './components/ShowPage.svelte';
5
5
  export { default as ConfirmDialog } from './components/ConfirmDialog.svelte';
6
6
  export { default as Sidebar } from './components/Sidebar.svelte';
7
7
  export { default as Layout } from './components/Layout.svelte';
8
+ export type { LayoutAIAssistantProps } from './components/Layout.svelte';
8
9
  export { default as Header } from './components/Header.svelte';
9
10
  export { default as SvadminLogo } from './components/SvadminLogo.svelte';
10
11
  export { default as ErrorBoundary } from './components/ErrorBoundary.svelte';
@@ -13,6 +14,8 @@ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
13
14
  export { default as FieldRenderer } from './components/FieldRenderer.svelte';
14
15
  export { default as EmptyState } from './components/EmptyState.svelte';
15
16
  export { default as StatsCard } from './components/StatsCard.svelte';
17
+ export { default as MetricStrip } from './components/MetricStrip.svelte';
18
+ export type { MetricStripItem, MetricTone } from './components/MetricStrip.svelte';
16
19
  export { default as PageHeader } from './components/PageHeader.svelte';
17
20
  export { default as ResourceOperationsPage } from './components/ResourceOperationsPage.svelte';
18
21
  export { default as RecordDetailDrawer } from './components/RecordDetailDrawer.svelte';
@@ -117,14 +120,7 @@ export type { PendingMutation } from './components/OfflineSyncBanner.svelte';
117
120
  export { default as InfiniteList } from './components/InfiniteList.svelte';
118
121
  export { default as ComboboxField } from './components/ComboboxField.svelte';
119
122
  export { default as PasswordInput } from './components/PasswordInput.svelte';
120
- export { default as ChatDialog } from "./components/ChatDialog.svelte";
121
- export { default as MarkdownRenderer } from "./components/MarkdownRenderer.svelte";
122
- export { default as SmartSuggest } from "./components/SmartSuggest.svelte";
123
- export { default as AICommandBar } from "./components/AICommandBar.svelte";
124
- export { default as CopilotPanel } from "./components/CopilotPanel.svelte";
125
- export { default as InsightCard } from "./components/InsightCard.svelte";
126
123
  export { default as AnomalyBadge } from "./components/AnomalyBadge.svelte";
127
- export { default as VoiceInput } from "./components/VoiceInput.svelte";
128
124
  export { default as AnimatedCounter } from "./components/AnimatedCounter.svelte";
129
125
  export { default as KeyboardShortcuts } from "./components/KeyboardShortcuts.svelte";
130
126
  export { default as InlineEdit } from "./components/InlineEdit.svelte";
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
16
16
  export { default as FieldRenderer } from './components/FieldRenderer.svelte';
17
17
  export { default as EmptyState } from './components/EmptyState.svelte';
18
18
  export { default as StatsCard } from './components/StatsCard.svelte';
19
+ export { default as MetricStrip } from './components/MetricStrip.svelte';
19
20
  export { default as PageHeader } from './components/PageHeader.svelte';
20
21
  export { default as ResourceOperationsPage } from './components/ResourceOperationsPage.svelte';
21
22
  export { default as RecordDetailDrawer } from './components/RecordDetailDrawer.svelte';
@@ -94,14 +95,7 @@ export { default as OfflineSyncBanner } from './components/OfflineSyncBanner.sve
94
95
  export { default as InfiniteList } from './components/InfiniteList.svelte';
95
96
  export { default as ComboboxField } from './components/ComboboxField.svelte';
96
97
  export { default as PasswordInput } from './components/PasswordInput.svelte';
97
- export { default as ChatDialog } from "./components/ChatDialog.svelte";
98
- export { default as MarkdownRenderer } from "./components/MarkdownRenderer.svelte";
99
- export { default as SmartSuggest } from "./components/SmartSuggest.svelte";
100
- export { default as AICommandBar } from "./components/AICommandBar.svelte";
101
- export { default as CopilotPanel } from "./components/CopilotPanel.svelte";
102
- export { default as InsightCard } from "./components/InsightCard.svelte";
103
98
  export { default as AnomalyBadge } from "./components/AnomalyBadge.svelte";
104
- export { default as VoiceInput } from "./components/VoiceInput.svelte";
105
99
  export { default as AnimatedCounter } from "./components/AnimatedCounter.svelte";
106
100
  export { default as KeyboardShortcuts } from "./components/KeyboardShortcuts.svelte";
107
101
  export { default as InlineEdit } from "./components/InlineEdit.svelte";