@svadmin/ui 0.29.0 → 0.29.2
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 +20 -9
- package/src/README.md +0 -18
- package/src/actions.ts +0 -110
- package/src/app.css +0 -519
- package/src/component-registry.svelte.ts +0 -60
- package/src/components/AICommandBar.svelte +0 -180
- package/src/components/AboutSettings.svelte +0 -106
- package/src/components/AdminApp.svelte +0 -199
- package/src/components/AnimatedCounter.svelte +0 -46
- package/src/components/AnomalyBadge.svelte +0 -65
- package/src/components/AppearanceSettings.svelte +0 -183
- package/src/components/AuditLogDrawer.svelte +0 -93
- package/src/components/AuditLogViewer.svelte +0 -226
- package/src/components/Authenticated.svelte +0 -51
- package/src/components/AutoForm.svelte +0 -242
- package/src/components/AutoSaveIndicator.svelte +0 -35
- package/src/components/AutoTable.svelte +0 -839
- package/src/components/Breadcrumbs.svelte +0 -58
- package/src/components/CanAccess.svelte +0 -23
- package/src/components/CatchAllNavigate.svelte +0 -21
- package/src/components/ChatDialog.svelte +0 -535
- package/src/components/ComboboxField.svelte +0 -129
- package/src/components/CommandPalette.svelte +0 -135
- package/src/components/ConfigErrorScreen.svelte +0 -126
- package/src/components/ConfirmDialog.svelte +0 -53
- package/src/components/CopilotPanel.svelte +0 -163
- package/src/components/CreatePage.svelte +0 -39
- package/src/components/DevTools.svelte +0 -149
- package/src/components/DraggableGrid.svelte +0 -86
- package/src/components/DraggableHeader.svelte +0 -112
- package/src/components/DrawerForm.svelte +0 -35
- package/src/components/EditPage.svelte +0 -52
- package/src/components/EmptyState.svelte +0 -31
- package/src/components/ErrorBoundary.svelte +0 -49
- package/src/components/ErrorComponent.svelte +0 -36
- package/src/components/FieldRenderer.svelte +0 -357
- package/src/components/ForgotPasswordPage.svelte +0 -112
- package/src/components/Header.svelte +0 -94
- package/src/components/InferencerPanel.svelte +0 -192
- package/src/components/InfiniteList.svelte +0 -93
- package/src/components/InlineEdit.svelte +0 -88
- package/src/components/InsightCard.svelte +0 -98
- package/src/components/KeyboardShortcuts.svelte +0 -61
- package/src/components/Layout.svelte +0 -167
- package/src/components/ListPage.svelte +0 -71
- package/src/components/LiveIndicator.svelte +0 -58
- package/src/components/LoginPage.svelte +0 -163
- package/src/components/MarkdownRenderer.svelte +0 -127
- package/src/components/ModalForm.svelte +0 -38
- package/src/components/NavigateToResource.svelte +0 -20
- package/src/components/PageHeader.svelte +0 -39
- package/src/components/PageSkeleton.svelte +0 -71
- package/src/components/PasswordInput.svelte +0 -96
- package/src/components/PermissionMatrix.svelte +0 -185
- package/src/components/ProfilePage.svelte +0 -284
- package/src/components/RegisterPage.svelte +0 -112
- package/src/components/RolesSettings.svelte +0 -146
- package/src/components/SettingsPage.svelte +0 -113
- package/src/components/ShowPage.svelte +0 -73
- package/src/components/Sidebar.svelte +0 -417
- package/src/components/SidebarItem.svelte +0 -129
- package/src/components/SmartSuggest.svelte +0 -158
- package/src/components/StatsCard.svelte +0 -79
- package/src/components/StepsForm.svelte +0 -192
- package/src/components/TaskQueueDrawer.svelte +0 -158
- package/src/components/TenantSwitcher.svelte +0 -84
- package/src/components/ThemedTitle.svelte +0 -32
- package/src/components/Toast.svelte +0 -46
- package/src/components/TooltipButton.svelte +0 -42
- package/src/components/UndoableNotification.svelte +0 -69
- package/src/components/UpdatePasswordPage.svelte +0 -89
- package/src/components/VirtualTable.svelte +0 -76
- package/src/components/VoiceInput.svelte +0 -120
- package/src/components/buttons/CloneButton.svelte +0 -30
- package/src/components/buttons/CreateButton.svelte +0 -29
- package/src/components/buttons/DeleteButton.svelte +0 -59
- package/src/components/buttons/EditButton.svelte +0 -30
- package/src/components/buttons/ExportButton.svelte +0 -30
- package/src/components/buttons/ImportButton.svelte +0 -56
- package/src/components/buttons/ListButton.svelte +0 -23
- package/src/components/buttons/RefreshButton.svelte +0 -36
- package/src/components/buttons/SaveButton.svelte +0 -27
- package/src/components/buttons/ShowButton.svelte +0 -31
- package/src/components/buttons/index.ts +0 -10
- package/src/components/charts/BarChart.svelte +0 -69
- package/src/components/charts/LineChart.svelte +0 -115
- package/src/components/charts/PieChart.svelte +0 -95
- package/src/components/charts/index.ts +0 -3
- package/src/components/fieldComponentMap.ts +0 -79
- package/src/components/fields/ArrayField.svelte +0 -86
- package/src/components/fields/BooleanField.svelte +0 -13
- package/src/components/fields/DateField.svelte +0 -6
- package/src/components/fields/EmailField.svelte +0 -9
- package/src/components/fields/FileField.svelte +0 -14
- package/src/components/fields/ImageField.svelte +0 -9
- package/src/components/fields/JsonField.svelte +0 -72
- package/src/components/fields/MarkdownField.svelte +0 -9
- package/src/components/fields/MultiSelectField.svelte +0 -21
- package/src/components/fields/NumberField.svelte +0 -6
- package/src/components/fields/RelationField.svelte +0 -24
- package/src/components/fields/RichTextField.svelte +0 -35
- package/src/components/fields/SelectField.svelte +0 -14
- package/src/components/fields/TagField.svelte +0 -12
- package/src/components/fields/TextField.svelte +0 -11
- package/src/components/fields/UrlField.svelte +0 -14
- package/src/components/ui/alert/alert-description.svelte +0 -17
- package/src/components/ui/alert/alert-title.svelte +0 -17
- package/src/components/ui/alert/alert.svelte +0 -44
- package/src/components/ui/alert/index.ts +0 -15
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +0 -29
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +0 -6
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +0 -16
- package/src/components/ui/alert-dialog/index.ts +0 -20
- package/src/components/ui/avatar/avatar.svelte +0 -63
- package/src/components/ui/avatar/index.ts +0 -2
- package/src/components/ui/badge/badge.svelte +0 -49
- package/src/components/ui/badge/index.ts +0 -3
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +0 -22
- package/src/components/ui/breadcrumb/breadcrumb.svelte +0 -11
- package/src/components/ui/breadcrumb/index.ts +0 -7
- package/src/components/ui/button/button.svelte +0 -90
- package/src/components/ui/button/index.ts +0 -18
- package/src/components/ui/card/card-action.svelte +0 -23
- package/src/components/ui/card/card-content.svelte +0 -20
- package/src/components/ui/card/card-description.svelte +0 -20
- package/src/components/ui/card/card-footer.svelte +0 -20
- package/src/components/ui/card/card-header.svelte +0 -23
- package/src/components/ui/card/card-title.svelte +0 -15
- package/src/components/ui/card/card.svelte +0 -22
- package/src/components/ui/card/index.ts +0 -25
- package/src/components/ui/checkbox/checkbox.svelte +0 -39
- package/src/components/ui/checkbox/index.ts +0 -6
- package/src/components/ui/collapsible/index.ts +0 -7
- package/src/components/ui/command/command-dialog.svelte +0 -31
- package/src/components/ui/command/index.ts +0 -2
- package/src/components/ui/context-menu/index.ts +0 -11
- package/src/components/ui/dialog/dialog-close.svelte +0 -11
- package/src/components/ui/dialog/dialog-content.svelte +0 -48
- package/src/components/ui/dialog/dialog-description.svelte +0 -17
- package/src/components/ui/dialog/dialog-footer.svelte +0 -32
- package/src/components/ui/dialog/dialog-header.svelte +0 -20
- package/src/components/ui/dialog/dialog-overlay.svelte +0 -17
- package/src/components/ui/dialog/dialog-portal.svelte +0 -7
- package/src/components/ui/dialog/dialog-title.svelte +0 -17
- package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
- package/src/components/ui/dialog/dialog.svelte +0 -7
- package/src/components/ui/dialog/index.ts +0 -34
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -43
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -35
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -36
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -10
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -27
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +0 -45
- package/src/components/ui/dropdown-menu/index.ts +0 -21
- package/src/components/ui/input/index.ts +0 -7
- package/src/components/ui/input/input.svelte +0 -48
- package/src/components/ui/label/index.ts +0 -1
- package/src/components/ui/label/label.svelte +0 -18
- package/src/components/ui/pagination/index.ts +0 -7
- package/src/components/ui/pagination/pagination-content.svelte +0 -11
- package/src/components/ui/pagination/pagination-ellipsis.svelte +0 -12
- package/src/components/ui/pagination/pagination-item.svelte +0 -11
- package/src/components/ui/pagination/pagination-link.svelte +0 -26
- package/src/components/ui/pagination/pagination-next.svelte +0 -22
- package/src/components/ui/pagination/pagination-previous.svelte +0 -22
- package/src/components/ui/pagination/pagination.svelte +0 -11
- package/src/components/ui/popover/index.ts +0 -12
- package/src/components/ui/popover/popover-content.svelte +0 -25
- package/src/components/ui/progress/index.ts +0 -1
- package/src/components/ui/progress/progress.svelte +0 -22
- package/src/components/ui/scroll-area/index.ts +0 -1
- package/src/components/ui/scroll-area/scroll-area.svelte +0 -20
- package/src/components/ui/select/index.ts +0 -2
- package/src/components/ui/select/select.svelte +0 -36
- package/src/components/ui/separator/index.ts +0 -7
- package/src/components/ui/separator/separator.svelte +0 -23
- package/src/components/ui/sheet/index.ts +0 -16
- package/src/components/ui/sheet/sheet-content.svelte +0 -28
- package/src/components/ui/sheet/sheet-description.svelte +0 -22
- package/src/components/ui/sheet/sheet-footer.svelte +0 -22
- package/src/components/ui/sheet/sheet-header.svelte +0 -22
- package/src/components/ui/sheet/sheet-title.svelte +0 -22
- package/src/components/ui/sheet/sheet.svelte +0 -78
- package/src/components/ui/skeleton/index.ts +0 -2
- package/src/components/ui/skeleton/skeleton.svelte +0 -21
- package/src/components/ui/switch/index.ts +0 -2
- package/src/components/ui/switch/switch.svelte +0 -49
- package/src/components/ui/table/index.ts +0 -28
- package/src/components/ui/table/table-body.svelte +0 -15
- package/src/components/ui/table/table-caption.svelte +0 -20
- package/src/components/ui/table/table-cell.svelte +0 -15
- package/src/components/ui/table/table-footer.svelte +0 -20
- package/src/components/ui/table/table-head.svelte +0 -15
- package/src/components/ui/table/table-header.svelte +0 -20
- package/src/components/ui/table/table-row.svelte +0 -15
- package/src/components/ui/table/table.svelte +0 -17
- package/src/components/ui/tabs/index.ts +0 -15
- package/src/components/ui/tabs/tabs-content.svelte +0 -30
- package/src/components/ui/tabs/tabs-list.svelte +0 -26
- package/src/components/ui/tabs/tabs-trigger.svelte +0 -37
- package/src/components/ui/tabs/tabs.svelte +0 -27
- package/src/components/ui/textarea/index.ts +0 -2
- package/src/components/ui/textarea/textarea.svelte +0 -24
- package/src/components/ui/tooltip/index.ts +0 -19
- package/src/components/ui/tooltip/tooltip-content.svelte +0 -52
- package/src/components/ui/tooltip/tooltip-portal.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
- package/src/components/ui/tooltip/tooltip.svelte +0 -10
- package/src/editor-config.svelte.ts +0 -23
- package/src/index.ts +0 -155
- package/src/markdown-deps.d.ts +0 -21
- package/src/router-state.svelte.ts +0 -65
- package/src/types.ts +0 -33
- package/src/utils.ts +0 -28
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
// ─── Extended slots (optional) ──────────────────────────────
|
|
29
|
-
// These allow deeper customization without replacing entire Layout/Header.
|
|
30
|
-
|
|
31
|
-
/** Custom dashboard page component (replaces default welcome message) */
|
|
32
|
-
DashboardPage?: Component<any>;
|
|
33
|
-
/** Custom breadcrumbs component (replaces built-in Breadcrumbs) */
|
|
34
|
-
Breadcrumbs?: Component<any>;
|
|
35
|
-
/** Custom theme toggle button (replaces built-in Sun/Moon toggle) */
|
|
36
|
-
ThemeToggle?: Component<any>;
|
|
37
|
-
/** Custom user menu / avatar dropdown in the header */
|
|
38
|
-
UserMenu?: Component<any>;
|
|
39
|
-
/** Custom notification panel / bell icon in the header */
|
|
40
|
-
NotificationPanel?: Component<any>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const REGISTRY_KEY = 'svadmin:components';
|
|
44
|
-
|
|
45
|
-
/** Set the component registry in context (called by AdminApp). */
|
|
46
|
-
export function setComponentRegistry(registry: ComponentRegistry): void {
|
|
47
|
-
setContext(REGISTRY_KEY, registry);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Retrieve the full component registry from context. */
|
|
51
|
-
export function getComponentRegistry(): ComponentRegistry {
|
|
52
|
-
return getContext<ComponentRegistry>(REGISTRY_KEY);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Retrieve a single component by name from the registry. */
|
|
56
|
-
export function useComponent<K extends keyof ComponentRegistry>(
|
|
57
|
-
name: K,
|
|
58
|
-
): ComponentRegistry[K] {
|
|
59
|
-
return getComponentRegistry()[name];
|
|
60
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getResources, getChatProvider } from '@svadmin/core';
|
|
3
|
-
import { navigate } from '@svadmin/core/router';
|
|
4
|
-
import { t } from '@svadmin/core/i18n';
|
|
5
|
-
import { toggleTheme } from '@svadmin/core';
|
|
6
|
-
import { Command } from 'cmdk-sv';
|
|
7
|
-
import * as Dialog from './ui/dialog/index.js';
|
|
8
|
-
import { Search, LayoutDashboard, Plus, Sun, FileText, Sparkles, Loader2, Send } from '@lucide/svelte';
|
|
9
|
-
import MarkdownRenderer from './MarkdownRenderer.svelte';
|
|
10
|
-
|
|
11
|
-
let { open = $bindable(false) } = $props<{ open?: boolean }>();
|
|
12
|
-
let searchValue = $state('');
|
|
13
|
-
|
|
14
|
-
const resources = getResources();
|
|
15
|
-
const provider = $derived(getChatProvider());
|
|
16
|
-
|
|
17
|
-
// AI State
|
|
18
|
-
let aiMode = $state(false);
|
|
19
|
-
let aiResponse = $state('');
|
|
20
|
-
let isPredicting = $state(false);
|
|
21
|
-
let abortController: AbortController | null = null;
|
|
22
|
-
|
|
23
|
-
function close() {
|
|
24
|
-
open = false;
|
|
25
|
-
searchValue = '';
|
|
26
|
-
aiMode = false;
|
|
27
|
-
aiResponse = '';
|
|
28
|
-
if (abortController) {
|
|
29
|
-
abortController.abort();
|
|
30
|
-
abortController = null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function act(fn: () => void) {
|
|
35
|
-
fn();
|
|
36
|
-
close();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function askAI() {
|
|
40
|
-
if (!provider || !searchValue.trim()) return;
|
|
41
|
-
|
|
42
|
-
aiMode = true;
|
|
43
|
-
isPredicting = true;
|
|
44
|
-
aiResponse = '';
|
|
45
|
-
|
|
46
|
-
if (abortController) abortController.abort();
|
|
47
|
-
abortController = new AbortController();
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const result = provider.sendMessage(
|
|
51
|
-
[{ id: 'cmd', role: 'user', content: searchValue, timestamp: Date.now() }],
|
|
52
|
-
{ signal: abortController.signal }
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (Symbol.asyncIterator in result) {
|
|
56
|
-
for await (const chunk of result as AsyncIterable<string>) {
|
|
57
|
-
aiResponse += chunk;
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
aiResponse = await result;
|
|
61
|
-
}
|
|
62
|
-
} catch (err: unknown) {
|
|
63
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
64
|
-
aiResponse = 'Failed to get response from AI.';
|
|
65
|
-
}
|
|
66
|
-
} finally {
|
|
67
|
-
isPredicting = false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
72
|
-
if (e.key === 'Enter' && searchValue.trim() && !aiMode) {
|
|
73
|
-
// If pressing enter on an active search, cmdk usually handles selection.
|
|
74
|
-
// But if there's no selection or we want to force AI, we can intercept.
|
|
75
|
-
// For simplicity, we trigger AI on Ctrl+Enter
|
|
76
|
-
if (e.ctrlKey || e.metaKey) {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
askAI();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const itemClass = 'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50';
|
|
84
|
-
</script>
|
|
85
|
-
|
|
86
|
-
<Dialog.Dialog bind:open onOpenChange={(v: boolean) => !v && close()}>
|
|
87
|
-
<Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[600px] border-none shadow-2xl">
|
|
88
|
-
<Command.Root
|
|
89
|
-
shouldFilter={!aiMode}
|
|
90
|
-
class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-semibold [&_[data-cmdk-group-heading]]:uppercase [&_[data-cmdk-group-heading]]:tracking-wider"
|
|
91
|
-
>
|
|
92
|
-
<div class="flex items-center border-b px-3 h-12 relative" data-cmdk-input-wrapper="">
|
|
93
|
-
{#if aiMode}
|
|
94
|
-
<Sparkles class="mr-2 h-4 w-4 shrink-0 text-warning animate-pulse" />
|
|
95
|
-
{:else}
|
|
96
|
-
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
97
|
-
{/if}
|
|
98
|
-
<Command.Input
|
|
99
|
-
bind:value={searchValue}
|
|
100
|
-
onkeydown={handleKeyDown}
|
|
101
|
-
placeholder={aiMode ? "Ask AI anything..." : "Search commands or ask AI (Ctrl+Enter)..."}
|
|
102
|
-
class="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
103
|
-
/>
|
|
104
|
-
{#if searchValue.trim() && !aiMode}
|
|
105
|
-
<button
|
|
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
|
-
onclick={askAI}
|
|
108
|
-
>
|
|
109
|
-
<Sparkles class="h-3 w-3" />
|
|
110
|
-
<kbd class="font-sans">Ctrl+Enter</kbd> to Ask AI
|
|
111
|
-
</button>
|
|
112
|
-
{/if}
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
{#if !aiMode}
|
|
116
|
-
<Command.List class="max-h-[360px] overflow-y-auto overflow-x-hidden p-1">
|
|
117
|
-
<Command.Empty class="py-6 text-center text-sm text-muted-foreground">
|
|
118
|
-
No standard commands found. Press <kbd class="px-1 py-0.5 rounded bg-muted border font-sans text-[10px]">Ctrl+Enter</kbd> to ask AI.
|
|
119
|
-
</Command.Empty>
|
|
120
|
-
|
|
121
|
-
<!-- Navigation -->
|
|
122
|
-
<Command.Group heading={t('common.home')}>
|
|
123
|
-
<Command.Item value="home" onSelect={() => act(() => navigate('/'))} class={itemClass}>
|
|
124
|
-
<LayoutDashboard class="h-4 w-4 text-muted-foreground" />
|
|
125
|
-
{t('common.home')}
|
|
126
|
-
</Command.Item>
|
|
127
|
-
{#each resources as r}
|
|
128
|
-
<Command.Item value={r.name} onSelect={() => act(() => navigate(`/${r.name}`))} class={itemClass}>
|
|
129
|
-
<FileText class="h-4 w-4 text-muted-foreground" />
|
|
130
|
-
{r.label}
|
|
131
|
-
</Command.Item>
|
|
132
|
-
{/each}
|
|
133
|
-
</Command.Group>
|
|
134
|
-
|
|
135
|
-
<Command.Separator class="mx-1 my-1 h-px bg-border" />
|
|
136
|
-
|
|
137
|
-
<!-- Actions -->
|
|
138
|
-
<Command.Group heading={t('common.actions')}>
|
|
139
|
-
{#each resources as r}
|
|
140
|
-
<Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigate(`/${r.name}/create`))} class={itemClass}>
|
|
141
|
-
<Plus class="h-4 w-4 text-muted-foreground" />
|
|
142
|
-
{t('common.create')} {r.label}
|
|
143
|
-
</Command.Item>
|
|
144
|
-
{/each}
|
|
145
|
-
<Command.Item value="toggle-theme" onSelect={() => act(() => toggleTheme())} class={itemClass}>
|
|
146
|
-
<Sun class="h-4 w-4 text-muted-foreground" />
|
|
147
|
-
{t('common.toggleTheme')}
|
|
148
|
-
</Command.Item>
|
|
149
|
-
</Command.Group>
|
|
150
|
-
</Command.List>
|
|
151
|
-
{:else}
|
|
152
|
-
<!-- AI Response Area -->
|
|
153
|
-
<div class="h-[360px] max-h-[360px] overflow-y-auto p-4 bg-muted/20">
|
|
154
|
-
<div class="flex items-start gap-3">
|
|
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
|
-
<Sparkles class="h-4 w-4" />
|
|
157
|
-
</div>
|
|
158
|
-
<div class="flex-1 w-full text-sm">
|
|
159
|
-
{#if !aiResponse && isPredicting}
|
|
160
|
-
<div class="flex items-center gap-2 text-muted-foreground mt-1.5">
|
|
161
|
-
<Loader2 class="h-4 w-4 animate-spin" /> Thinking...
|
|
162
|
-
</div>
|
|
163
|
-
{:else}
|
|
164
|
-
<MarkdownRenderer content={aiResponse} streaming={isPredicting} />
|
|
165
|
-
{/if}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
<div class="p-2 border-t flex justify-end gap-2 bg-muted/10">
|
|
170
|
-
<button
|
|
171
|
-
class="text-xs px-3 py-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground"
|
|
172
|
-
onclick={() => { aiMode = false; searchValue = ''; }}
|
|
173
|
-
>
|
|
174
|
-
Clear & Return
|
|
175
|
-
</button>
|
|
176
|
-
</div>
|
|
177
|
-
{/if}
|
|
178
|
-
</Command.Root>
|
|
179
|
-
</Dialog.DialogContent>
|
|
180
|
-
</Dialog.Dialog>
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { t } from '@svadmin/core/i18n';
|
|
3
|
-
import { getResources, getDataProviderNames } from '@svadmin/core';
|
|
4
|
-
import { getLocale } from '@svadmin/core/i18n';
|
|
5
|
-
import { getResolvedTheme, getColorTheme } from '@svadmin/core';
|
|
6
|
-
import * as Card from './ui/card/index.js';
|
|
7
|
-
import { Badge } from './ui/badge/index.js';
|
|
8
|
-
import { Info, Database, Globe, Palette, Layers } from '@lucide/svelte';
|
|
9
|
-
|
|
10
|
-
const resources = getResources();
|
|
11
|
-
const providerNames = getDataProviderNames();
|
|
12
|
-
|
|
13
|
-
const version = '__SVADMIN_VERSION__'; // replaced at build time or shown as-is
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<div class="space-y-6">
|
|
17
|
-
<div>
|
|
18
|
-
<h2 class="text-xl font-semibold text-foreground">{t('settings.about')}</h2>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<!-- Version -->
|
|
22
|
-
<Card.Card>
|
|
23
|
-
<Card.CardHeader class="pb-3">
|
|
24
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
25
|
-
<Info class="h-4 w-4 text-muted-foreground" />
|
|
26
|
-
svadmin
|
|
27
|
-
</Card.CardTitle>
|
|
28
|
-
</Card.CardHeader>
|
|
29
|
-
<Card.CardContent>
|
|
30
|
-
<div class="grid gap-4 text-sm">
|
|
31
|
-
<div class="flex items-center justify-between">
|
|
32
|
-
<span class="text-muted-foreground">{t('settings.version')}</span>
|
|
33
|
-
<Badge variant="secondary" class="font-mono text-xs">{version}</Badge>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</Card.CardContent>
|
|
37
|
-
</Card.Card>
|
|
38
|
-
|
|
39
|
-
<!-- Registered Resources -->
|
|
40
|
-
<Card.Card>
|
|
41
|
-
<Card.CardHeader class="pb-3">
|
|
42
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
43
|
-
<Layers class="h-4 w-4 text-muted-foreground" />
|
|
44
|
-
{t('settings.registeredResources')}
|
|
45
|
-
</Card.CardTitle>
|
|
46
|
-
</Card.CardHeader>
|
|
47
|
-
<Card.CardContent>
|
|
48
|
-
<div class="flex flex-wrap gap-2">
|
|
49
|
-
{#each resources as resource}
|
|
50
|
-
<Badge variant="outline" class="font-mono text-xs">{resource.name}</Badge>
|
|
51
|
-
{/each}
|
|
52
|
-
{#if resources.length === 0}
|
|
53
|
-
<p class="text-sm text-muted-foreground">—</p>
|
|
54
|
-
{/if}
|
|
55
|
-
</div>
|
|
56
|
-
</Card.CardContent>
|
|
57
|
-
</Card.Card>
|
|
58
|
-
|
|
59
|
-
<!-- Data Providers -->
|
|
60
|
-
<Card.Card>
|
|
61
|
-
<Card.CardHeader class="pb-3">
|
|
62
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
63
|
-
<Database class="h-4 w-4 text-muted-foreground" />
|
|
64
|
-
{t('settings.dataProvider')}
|
|
65
|
-
</Card.CardTitle>
|
|
66
|
-
</Card.CardHeader>
|
|
67
|
-
<Card.CardContent>
|
|
68
|
-
<div class="flex flex-wrap gap-2">
|
|
69
|
-
{#each providerNames as name}
|
|
70
|
-
<Badge variant="outline" class="font-mono text-xs">{name}</Badge>
|
|
71
|
-
{/each}
|
|
72
|
-
</div>
|
|
73
|
-
</Card.CardContent>
|
|
74
|
-
</Card.Card>
|
|
75
|
-
|
|
76
|
-
<!-- Current Environment -->
|
|
77
|
-
<Card.Card>
|
|
78
|
-
<Card.CardHeader class="pb-3">
|
|
79
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
80
|
-
<Globe class="h-4 w-4 text-muted-foreground" />
|
|
81
|
-
{t('settings.currentEnvironment')}
|
|
82
|
-
</Card.CardTitle>
|
|
83
|
-
</Card.CardHeader>
|
|
84
|
-
<Card.CardContent>
|
|
85
|
-
<div class="grid gap-3 text-sm">
|
|
86
|
-
<div class="flex items-center justify-between">
|
|
87
|
-
<span class="text-muted-foreground">{t('settings.language')}</span>
|
|
88
|
-
<span class="font-mono text-xs">{getLocale()}</span>
|
|
89
|
-
</div>
|
|
90
|
-
<div class="h-px bg-border"></div>
|
|
91
|
-
<div class="flex items-center justify-between">
|
|
92
|
-
<span class="text-muted-foreground">{t('settings.themeMode')}</span>
|
|
93
|
-
<span class="font-mono text-xs">{getResolvedTheme()}</span>
|
|
94
|
-
</div>
|
|
95
|
-
<div class="h-px bg-border"></div>
|
|
96
|
-
<div class="flex items-center justify-between">
|
|
97
|
-
<span class="text-muted-foreground flex items-center gap-1.5">
|
|
98
|
-
<Palette class="h-3.5 w-3.5" />
|
|
99
|
-
{t('settings.colorAccent')}
|
|
100
|
-
</span>
|
|
101
|
-
<span class="font-mono text-xs">{getColorTheme()}</span>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</Card.CardContent>
|
|
105
|
-
</Card.Card>
|
|
106
|
-
</div>
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
// Auto-inject design tokens so consumers don't need to manually import
|
|
3
|
-
import '../app.css';
|
|
4
|
-
import type { Snippet } from 'svelte';
|
|
5
|
-
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem } from '@svadmin/core';
|
|
6
|
-
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
|
|
7
|
-
import { t } from '@svadmin/core/i18n';
|
|
8
|
-
import { navigate } from '@svadmin/core/router';
|
|
9
|
-
import { currentPath } from '@svadmin/core/router';
|
|
10
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
11
|
-
import { setComponentRegistry, getComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
|
|
12
|
-
import Layout from './Layout.svelte';
|
|
13
|
-
import AutoTable from './AutoTable.svelte';
|
|
14
|
-
import AutoForm from './AutoForm.svelte';
|
|
15
|
-
import ShowPage from './ShowPage.svelte';
|
|
16
|
-
import Toast from './Toast.svelte';
|
|
17
|
-
import LoginPage from './LoginPage.svelte';
|
|
18
|
-
import RegisterPage from './RegisterPage.svelte';
|
|
19
|
-
import ForgotPasswordPage from './ForgotPasswordPage.svelte';
|
|
20
|
-
import UpdatePasswordPage from './UpdatePasswordPage.svelte';
|
|
21
|
-
import ConfigErrorScreen from './ConfigErrorScreen.svelte';
|
|
22
|
-
import DevTools from './DevTools.svelte';
|
|
23
|
-
import Sidebar from './Sidebar.svelte';
|
|
24
|
-
import Header from './Header.svelte';
|
|
25
|
-
import SettingsPage from './SettingsPage.svelte';
|
|
26
|
-
import { Button } from './ui/button/index.js';
|
|
27
|
-
import { Input } from './ui/input/index.js';
|
|
28
|
-
import { Badge } from './ui/badge/index.js';
|
|
29
|
-
import { Skeleton } from './ui/skeleton/index.js';
|
|
30
|
-
import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
|
|
31
|
-
|
|
32
|
-
interface Props {
|
|
33
|
-
dataProvider: DataProvider;
|
|
34
|
-
authProvider?: AuthProvider;
|
|
35
|
-
routerProvider?: RouterProvider;
|
|
36
|
-
resources: ResourceDefinition[];
|
|
37
|
-
locale?: string;
|
|
38
|
-
title?: string;
|
|
39
|
-
defaultTheme?: ThemeMode;
|
|
40
|
-
/** Theme configuration for dark-first mode, CSS overrides, etc. */
|
|
41
|
-
themeConfig?: ThemeConfig;
|
|
42
|
-
dashboard?: Snippet;
|
|
43
|
-
loginPage?: Snippet;
|
|
44
|
-
/** Override default components via DI */
|
|
45
|
-
components?: Partial<ComponentRegistry>;
|
|
46
|
-
/** Custom multi-level menu configuration */
|
|
47
|
-
menu?: MenuItem[];
|
|
48
|
-
/** External URL to the main application or workspace (renders a shortcut in the header) */
|
|
49
|
-
siteUrl?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let {
|
|
53
|
-
dataProvider,
|
|
54
|
-
authProvider,
|
|
55
|
-
routerProvider,
|
|
56
|
-
resources,
|
|
57
|
-
locale,
|
|
58
|
-
title = 'Admin',
|
|
59
|
-
defaultTheme,
|
|
60
|
-
themeConfig: userThemeConfig,
|
|
61
|
-
dashboard,
|
|
62
|
-
loginPage,
|
|
63
|
-
components: userComponents,
|
|
64
|
-
menu,
|
|
65
|
-
siteUrl,
|
|
66
|
-
}: Props = $props();
|
|
67
|
-
|
|
68
|
-
// Default component registry
|
|
69
|
-
const defaultComponents: ComponentRegistry = {
|
|
70
|
-
Layout, Sidebar, Header,
|
|
71
|
-
LoginPage, AutoTable, AutoForm, ShowPage,
|
|
72
|
-
Button: Button as unknown as ComponentRegistry['Button'],
|
|
73
|
-
Input: Input as unknown as ComponentRegistry['Input'],
|
|
74
|
-
Badge: Badge as unknown as ComponentRegistry['Badge'],
|
|
75
|
-
Skeleton: Skeleton as unknown as ComponentRegistry['Skeleton'],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Merge user overrides and set context
|
|
79
|
-
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
80
|
-
|
|
81
|
-
// Resolve router provider (default to hash)
|
|
82
|
-
const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
|
|
83
|
-
|
|
84
|
-
// Set up context — use $effect so prop changes are tracked
|
|
85
|
-
$effect.pre(() => {
|
|
86
|
-
setDataProvider(dataProvider);
|
|
87
|
-
if (authProvider) setAuthProvider(authProvider);
|
|
88
|
-
setResources(resources);
|
|
89
|
-
setRouterProvider(resolvedRouter);
|
|
90
|
-
if (locale) setLocale(locale);
|
|
91
|
-
if (userThemeConfig) configureTheme(userThemeConfig);
|
|
92
|
-
if (defaultTheme) setTheme(defaultTheme);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const queryClient = new QueryClient({
|
|
96
|
-
defaultOptions: {
|
|
97
|
-
queries: { staleTime: 30_000, retry: 1 },
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Initialize router with provider
|
|
102
|
-
$effect.pre(() => {
|
|
103
|
-
initRouter(resolvedRouter);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Reactive getters for route state
|
|
107
|
-
const route = $derived(getRoute());
|
|
108
|
-
const params = $derived(getParams());
|
|
109
|
-
|
|
110
|
-
// Auth check
|
|
111
|
-
let isAuthenticated = $state(false);
|
|
112
|
-
let authChecked = $state(false);
|
|
113
|
-
|
|
114
|
-
$effect(() => {
|
|
115
|
-
// Explicitly track route so auth is re-checked on navigation
|
|
116
|
-
const _route = route;
|
|
117
|
-
|
|
118
|
-
if (!authProvider) {
|
|
119
|
-
isAuthenticated = true;
|
|
120
|
-
authChecked = true;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
authProvider.check().then(result => {
|
|
124
|
-
isAuthenticated = result.authenticated;
|
|
125
|
-
authChecked = true;
|
|
126
|
-
if (!result.authenticated && _route !== '/login' && _route !== '/register' && _route !== '/forgot-password' && _route !== '/update-password') {
|
|
127
|
-
navigate(result.redirectTo ?? '/login');
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
</script>
|
|
132
|
-
|
|
133
|
-
<QueryClientProvider client={queryClient}>
|
|
134
|
-
{#if !authChecked}
|
|
135
|
-
<div class="flex h-screen items-center justify-center">
|
|
136
|
-
<div class="space-y-4 text-center">
|
|
137
|
-
<div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary mx-auto"></div>
|
|
138
|
-
<p class="text-sm text-muted-foreground">Loading...</p>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
{:else if route === '/login' && loginPage}
|
|
142
|
-
{@render loginPage()}
|
|
143
|
-
{:else if route === '/login' && authProvider}
|
|
144
|
-
<LoginPage {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
|
|
145
|
-
{:else if route === '/register' && authProvider?.register}
|
|
146
|
-
<RegisterPage {title} />
|
|
147
|
-
{:else if route === '/forgot-password' && authProvider?.forgotPassword}
|
|
148
|
-
<ForgotPasswordPage {title} />
|
|
149
|
-
{:else if route === '/update-password' && authProvider?.updatePassword}
|
|
150
|
-
<UpdatePasswordPage {title} />
|
|
151
|
-
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
152
|
-
<ConfigErrorScreen title="{title} — {t('common.configRequired')}" />
|
|
153
|
-
{:else if isAuthenticated || !authProvider}
|
|
154
|
-
<Layout {title} {menu} {siteUrl}>
|
|
155
|
-
{#key route + (params.resource ?? '') + (params.id ?? '')}
|
|
156
|
-
<div class="svadmin-page-enter">
|
|
157
|
-
{#if currentPath().startsWith('/settings')}
|
|
158
|
-
<SettingsPage />
|
|
159
|
-
{:else if route === '/' || route === ''}
|
|
160
|
-
{#if dashboard}
|
|
161
|
-
{@render dashboard()}
|
|
162
|
-
{:else}
|
|
163
|
-
<div class="space-y-4">
|
|
164
|
-
<h1 class="text-xl font-semibold text-foreground">{t('common.welcome', { title })}</h1>
|
|
165
|
-
<p class="text-muted-foreground">{t('common.dashboardHint')}</p>
|
|
166
|
-
</div>
|
|
167
|
-
{/if}
|
|
168
|
-
{:else if route === '/:resource'}
|
|
169
|
-
{#key params.resource}
|
|
170
|
-
{@const Comp = getComponentRegistry().AutoTable}
|
|
171
|
-
<Comp resourceName={params.resource} />
|
|
172
|
-
{/key}
|
|
173
|
-
{:else if route === '/:resource/create'}
|
|
174
|
-
{#key params.resource}
|
|
175
|
-
{@const Comp = getComponentRegistry().AutoForm}
|
|
176
|
-
<Comp resourceName={params.resource} mode="create" />
|
|
177
|
-
{/key}
|
|
178
|
-
{:else if route === '/:resource/edit/:id'}
|
|
179
|
-
{#key `${params.resource}-${params.id}`}
|
|
180
|
-
{@const Comp = getComponentRegistry().AutoForm}
|
|
181
|
-
<Comp resourceName={params.resource} mode="edit" id={params.id} />
|
|
182
|
-
{/key}
|
|
183
|
-
{:else if route === '/:resource/show/:id'}
|
|
184
|
-
{#key `${params.resource}-${params.id}`}
|
|
185
|
-
{@const Comp = getComponentRegistry().ShowPage}
|
|
186
|
-
<Comp resourceName={params.resource} id={params.id} />
|
|
187
|
-
{/key}
|
|
188
|
-
{/if}
|
|
189
|
-
</div>
|
|
190
|
-
{/key}
|
|
191
|
-
</Layout>
|
|
192
|
-
{:else}
|
|
193
|
-
<div class="flex h-screen items-center justify-center">
|
|
194
|
-
<p class="text-muted-foreground">{t('common.redirecting')}</p>
|
|
195
|
-
</div>
|
|
196
|
-
{/if}
|
|
197
|
-
<Toast />
|
|
198
|
-
<DevTools />
|
|
199
|
-
</QueryClientProvider>
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
/** The target number to animate to */
|
|
6
|
-
value: number | string;
|
|
7
|
-
/** Animation duration in milliseconds */
|
|
8
|
-
duration?: number;
|
|
9
|
-
/** CSS class for the container */
|
|
10
|
-
class?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let { value, duration = 600, class: className = '' }: Props = $props();
|
|
14
|
-
|
|
15
|
-
let displayValue = $state('0');
|
|
16
|
-
let prevTarget = 0;
|
|
17
|
-
|
|
18
|
-
$effect(() => {
|
|
19
|
-
const target = typeof value === 'number' ? value : parseInt(value, 10);
|
|
20
|
-
if (isNaN(target)) {
|
|
21
|
-
displayValue = String(value);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const start = untrack(() => prevTarget);
|
|
26
|
-
prevTarget = target;
|
|
27
|
-
const startTime = performance.now();
|
|
28
|
-
|
|
29
|
-
function tick() {
|
|
30
|
-
const elapsed = performance.now() - startTime;
|
|
31
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
32
|
-
// Ease-out cubic
|
|
33
|
-
const eased = 1 - Math.pow(1 - progress, 3);
|
|
34
|
-
const current = Math.round(start + (target - start) * eased);
|
|
35
|
-
displayValue = current.toLocaleString();
|
|
36
|
-
|
|
37
|
-
if (progress < 1) {
|
|
38
|
-
requestAnimationFrame(tick);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
requestAnimationFrame(tick);
|
|
43
|
-
});
|
|
44
|
-
</script>
|
|
45
|
-
|
|
46
|
-
<span class={className}>{displayValue}</span>
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Badge } from './ui/badge/index.js';
|
|
3
|
-
import { TrendingUp, TrendingDown, AlertTriangle } from '@lucide/svelte';
|
|
4
|
-
import TooltipButton from './TooltipButton.svelte';
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
/** The actual current value */
|
|
8
|
-
value: number;
|
|
9
|
-
/** The baseline/expected value to compare against */
|
|
10
|
-
baseline: number;
|
|
11
|
-
/** Threshold percentage to trigger an anomaly (e.g. 0.2 means 20% diff) */
|
|
12
|
-
threshold?: number;
|
|
13
|
-
/** Optional label formatting */
|
|
14
|
-
formatter?: (val: number) => string;
|
|
15
|
-
/** If true, lower is better (e.g. latency). Default false (higher is better e.g. revenue). */
|
|
16
|
-
lowerIsBetter?: boolean;
|
|
17
|
-
class?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let {
|
|
21
|
-
value,
|
|
22
|
-
baseline,
|
|
23
|
-
threshold = 0.2,
|
|
24
|
-
formatter = (v) => v.toString(),
|
|
25
|
-
lowerIsBetter = false,
|
|
26
|
-
class: className = ''
|
|
27
|
-
}: Props = $props();
|
|
28
|
-
|
|
29
|
-
// Calculate percentage difference
|
|
30
|
-
const diff = $derived(baseline === 0 ? 0 : (value - baseline) / Math.abs(baseline));
|
|
31
|
-
const isAnomaly = $derived(Math.abs(diff) >= threshold);
|
|
32
|
-
|
|
33
|
-
// Determine sentiment (good, bad, neutral)
|
|
34
|
-
const isGood = $derived(
|
|
35
|
-
!isAnomaly ? null :
|
|
36
|
-
(lowerIsBetter ? diff < 0 : diff > 0)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const percentLabel = $derived(`${Math.abs(diff * 100).toFixed(1)}%`);
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
{#if !isAnomaly}
|
|
43
|
-
<span class="text-muted-foreground text-sm font-medium {className}">
|
|
44
|
-
{formatter(value)}
|
|
45
|
-
</span>
|
|
46
|
-
{:else}
|
|
47
|
-
<TooltipButton
|
|
48
|
-
tooltip={`Baseline: ${formatter(baseline)} (${diff > 0 ? '+' : '-'}${percentLabel})`}
|
|
49
|
-
variant="ghost"
|
|
50
|
-
size="sm"
|
|
51
|
-
class="p-0 h-auto hover:bg-transparent"
|
|
52
|
-
>
|
|
53
|
-
<Badge
|
|
54
|
-
variant={isGood ? 'default' : 'destructive'}
|
|
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
|
-
>
|
|
57
|
-
{#if diff > 0}
|
|
58
|
-
<TrendingUp class="h-3 w-3" />
|
|
59
|
-
{:else}
|
|
60
|
-
<TrendingDown class="h-3 w-3" />
|
|
61
|
-
{/if}
|
|
62
|
-
{formatter(value)}
|
|
63
|
-
</Badge>
|
|
64
|
-
</TooltipButton>
|
|
65
|
-
{/if}
|