@svadmin/ui 0.29.1 → 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 -673
- 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,158 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Input } from './ui/input/index.js';
|
|
3
|
-
import type { ChatProvider } from '@svadmin/core';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
/** The bound value of the input */
|
|
7
|
-
value: string;
|
|
8
|
-
/** The chat provider to generate suggestions */
|
|
9
|
-
provider?: ChatProvider;
|
|
10
|
-
/** Context given to the AI to help it predict the text */
|
|
11
|
-
context?: string;
|
|
12
|
-
/** Standard input props */
|
|
13
|
-
placeholder?: string;
|
|
14
|
-
class?: string;
|
|
15
|
-
id?: string;
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
name?: string;
|
|
18
|
-
type?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let {
|
|
22
|
-
value = $bindable(),
|
|
23
|
-
provider,
|
|
24
|
-
context = '',
|
|
25
|
-
placeholder = '',
|
|
26
|
-
class: className = '',
|
|
27
|
-
id,
|
|
28
|
-
disabled = false,
|
|
29
|
-
name,
|
|
30
|
-
type = 'text'
|
|
31
|
-
}: Props = $props();
|
|
32
|
-
|
|
33
|
-
let suggestion = $state('');
|
|
34
|
-
let isPredicting = $state(false);
|
|
35
|
-
let abortController: AbortController | null = null;
|
|
36
|
-
let predictTimer: ReturnType<typeof setTimeout> | null = null;
|
|
37
|
-
|
|
38
|
-
// The ghost text shows the suggestion, but only the part *after* what the user has typed
|
|
39
|
-
// It only displays if the suggestion actually starts with the current value
|
|
40
|
-
const ghostText = $derived.by(() => {
|
|
41
|
-
if (!suggestion || !value) return '';
|
|
42
|
-
const lowerVal = value.toLowerCase();
|
|
43
|
-
const lowerSugg = suggestion.toLowerCase();
|
|
44
|
-
|
|
45
|
-
if (lowerSugg.startsWith(lowerVal) && suggestion.length > value.length) {
|
|
46
|
-
// Preserve original casing from the suggestion but append only the suffix
|
|
47
|
-
return value + suggestion.slice(value.length);
|
|
48
|
-
}
|
|
49
|
-
return '';
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const suffixText = $derived(ghostText ? ghostText.slice(value.length) : '');
|
|
53
|
-
|
|
54
|
-
async function predict() {
|
|
55
|
-
if (!provider || !value.trim()) {
|
|
56
|
-
suggestion = '';
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
isPredicting = true;
|
|
61
|
-
if (abortController) {
|
|
62
|
-
abortController.abort();
|
|
63
|
-
}
|
|
64
|
-
abortController = new AbortController();
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const prompt = `You are an autocomplete engine. The user is typing: "${value}".
|
|
68
|
-
Context: ${context}.
|
|
69
|
-
Provide ONLY the most likely completion of the exact phrase the user is typing.
|
|
70
|
-
Include the user's input in your returned string.
|
|
71
|
-
Do not include any other text, Markdown formatting, quotes, or explanations.`;
|
|
72
|
-
|
|
73
|
-
const result = provider.sendMessage(
|
|
74
|
-
[{ id: 'prompt', role: 'user', content: prompt, timestamp: Date.now() }],
|
|
75
|
-
{ signal: abortController.signal }
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
suggestion = '';
|
|
79
|
-
|
|
80
|
-
// Handle streaming or Promise responses seamlessly
|
|
81
|
-
if (Symbol.asyncIterator in result) {
|
|
82
|
-
for await (const chunk of result as AsyncIterable<string>) {
|
|
83
|
-
suggestion += chunk;
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
suggestion = await result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Clean up the suggestion
|
|
90
|
-
suggestion = suggestion.trim().replace(/^["']|["']$/g, '');
|
|
91
|
-
} catch (err: unknown) {
|
|
92
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
93
|
-
suggestion = '';
|
|
94
|
-
}
|
|
95
|
-
} finally {
|
|
96
|
-
isPredicting = false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function handleInput(e: Event) {
|
|
101
|
-
const target = e.target as HTMLInputElement;
|
|
102
|
-
value = target.value;
|
|
103
|
-
|
|
104
|
-
// Reset suggestion immediately if it no longer matches
|
|
105
|
-
if (suggestion && !suggestion.toLowerCase().startsWith(value.toLowerCase())) {
|
|
106
|
-
suggestion = '';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (predictTimer) clearTimeout(predictTimer);
|
|
110
|
-
|
|
111
|
-
// Debounce prediction
|
|
112
|
-
predictTimer = setTimeout(() => {
|
|
113
|
-
predict();
|
|
114
|
-
}, 500);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
118
|
-
if (e.key === 'Tab' && suffixText) {
|
|
119
|
-
e.preventDefault(); // Prevent standard tab navigation
|
|
120
|
-
value = ghostText;
|
|
121
|
-
suggestion = ''; // Clear suggestion once accepted
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
</script>
|
|
126
|
-
|
|
127
|
-
<div class="relative w-full {className}">
|
|
128
|
-
<!-- Ghost Text Layer (positioned exactly beneath the actual text) -->
|
|
129
|
-
{#if suffixText}
|
|
130
|
-
<div
|
|
131
|
-
class="absolute inset-0 pointer-events-none flex items-center px-3 text-sm font-medium"
|
|
132
|
-
aria-hidden="true"
|
|
133
|
-
>
|
|
134
|
-
<span class="opacity-0">{value}</span>
|
|
135
|
-
<span class="text-muted-foreground/40">{suffixText}</span>
|
|
136
|
-
</div>
|
|
137
|
-
{/if}
|
|
138
|
-
|
|
139
|
-
<!-- Actual Input Layer -->
|
|
140
|
-
<Input
|
|
141
|
-
{id}
|
|
142
|
-
{name}
|
|
143
|
-
{type}
|
|
144
|
-
{disabled}
|
|
145
|
-
{placeholder}
|
|
146
|
-
value={value}
|
|
147
|
-
oninput={handleInput}
|
|
148
|
-
onkeydown={handleKeyDown}
|
|
149
|
-
class="bg-transparent relative z-10 w-full"
|
|
150
|
-
/>
|
|
151
|
-
|
|
152
|
-
<!-- Loading indicator -->
|
|
153
|
-
{#if isPredicting}
|
|
154
|
-
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
155
|
-
<div class="h-1.5 w-1.5 rounded-full bg-primary/50 animate-pulse"></div>
|
|
156
|
-
</div>
|
|
157
|
-
{/if}
|
|
158
|
-
</div>
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Skeleton } from './ui/skeleton/index.js';
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
|
|
5
|
-
type ColorVariant = 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
6
|
-
type StyleVariant = 'default' | 'outline' | 'filled';
|
|
7
|
-
|
|
8
|
-
const colorMap: Record<ColorVariant, string> = {
|
|
9
|
-
primary: 'bg-primary/10 text-primary',
|
|
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
|
-
};
|
|
15
|
-
|
|
16
|
-
const variantMap: Record<StyleVariant, string> = {
|
|
17
|
-
default: 'bg-card shadow-sm hover:shadow-md transition-shadow duration-300',
|
|
18
|
-
outline: 'border-2 border-border bg-transparent',
|
|
19
|
-
filled: 'border-0 bg-muted/50 shadow-sm',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
interface Props {
|
|
23
|
-
label: string;
|
|
24
|
-
value: string | number;
|
|
25
|
-
icon?: import('svelte').Component<{ class?: string }>;
|
|
26
|
-
trend?: { value: number; label?: string };
|
|
27
|
-
loading?: boolean;
|
|
28
|
-
color?: ColorVariant;
|
|
29
|
-
variant?: StyleVariant;
|
|
30
|
-
/** Optional footer snippet for additional content below the value */
|
|
31
|
-
footer?: Snippet;
|
|
32
|
-
class?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let {
|
|
36
|
-
label,
|
|
37
|
-
value,
|
|
38
|
-
icon: Icon,
|
|
39
|
-
trend,
|
|
40
|
-
loading = false,
|
|
41
|
-
color = 'primary',
|
|
42
|
-
variant = 'default',
|
|
43
|
-
footer,
|
|
44
|
-
class: className = '',
|
|
45
|
-
}: Props = $props();
|
|
46
|
-
</script>
|
|
47
|
-
|
|
48
|
-
<div class="flex items-center gap-4 rounded-lg p-5 {variantMap[variant]} {className}">
|
|
49
|
-
{#if Icon}
|
|
50
|
-
<div class="flex h-12 w-12 items-center justify-center rounded-lg {colorMap[color]}">
|
|
51
|
-
<Icon class="h-6 w-6" />
|
|
52
|
-
</div>
|
|
53
|
-
{/if}
|
|
54
|
-
<div class="flex-1 min-w-0">
|
|
55
|
-
<p class="text-sm text-muted-foreground">{label}</p>
|
|
56
|
-
{#if loading}
|
|
57
|
-
<Skeleton class="mt-1 h-7 w-20" />
|
|
58
|
-
{:else}
|
|
59
|
-
<div class="flex items-baseline gap-2">
|
|
60
|
-
<p class="text-xl font-mono font-semibold text-foreground tracking-tight">{value}</p>
|
|
61
|
-
{#if trend}
|
|
62
|
-
<span
|
|
63
|
-
class="text-xs font-medium {trend.value >= 0 ? 'text-success' : 'text-destructive'}"
|
|
64
|
-
>
|
|
65
|
-
{trend.value >= 0 ? '↑' : '↓'}{Math.abs(trend.value)}%
|
|
66
|
-
{#if trend.label}
|
|
67
|
-
<span class="text-muted-foreground">{trend.label}</span>
|
|
68
|
-
{/if}
|
|
69
|
-
</span>
|
|
70
|
-
{/if}
|
|
71
|
-
</div>
|
|
72
|
-
{/if}
|
|
73
|
-
{#if footer}
|
|
74
|
-
<div class="mt-2">
|
|
75
|
-
{@render footer()}
|
|
76
|
-
</div>
|
|
77
|
-
{/if}
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { fly } from 'svelte/transition';
|
|
3
|
-
import { useStepsForm, deriveValidator } from '@svadmin/core';
|
|
4
|
-
import type { FieldDefinition } from '@svadmin/core';
|
|
5
|
-
import { getResource } from '@svadmin/core';
|
|
6
|
-
import { navigate } from '@svadmin/core/router';
|
|
7
|
-
import { t } from '@svadmin/core/i18n';
|
|
8
|
-
import { Button } from './ui/button/index.js';
|
|
9
|
-
import * as Card from './ui/card/index.js';
|
|
10
|
-
import { Badge } from './ui/badge/index.js';
|
|
11
|
-
import { Progress } from './ui/progress/index.js';
|
|
12
|
-
import { Save, ArrowLeft, ArrowRight, Check, Loader2 } from '@lucide/svelte';
|
|
13
|
-
import FieldRenderer from './FieldRenderer.svelte';
|
|
14
|
-
|
|
15
|
-
interface StepDef {
|
|
16
|
-
title: string;
|
|
17
|
-
fields: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface Props {
|
|
21
|
-
resourceName: string;
|
|
22
|
-
id?: string | number;
|
|
23
|
-
mode?: 'create' | 'edit';
|
|
24
|
-
steps: StepDef[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let { resourceName, id, mode = 'create', steps }: Props = $props();
|
|
28
|
-
|
|
29
|
-
const resource = $derived(getResource(resourceName));
|
|
30
|
-
const primaryKey = $derived(resource.primaryKey ?? 'id');
|
|
31
|
-
|
|
32
|
-
// All fields used across all steps
|
|
33
|
-
const flatStepsFields = $derived(steps.flatMap(s => s.fields)
|
|
34
|
-
.map(key => resource.fields.find(f => f.key === key))
|
|
35
|
-
.filter((f): f is FieldDefinition => f != null));
|
|
36
|
-
|
|
37
|
-
// Default values from field metadata
|
|
38
|
-
function getDefaultForType(field: FieldDefinition): unknown {
|
|
39
|
-
switch (field.type) {
|
|
40
|
-
case 'text': case 'textarea': case 'richtext': case 'image': return '';
|
|
41
|
-
case 'number': return 0;
|
|
42
|
-
case 'boolean': return false;
|
|
43
|
-
case 'tags': case 'images': case 'multiselect': return [];
|
|
44
|
-
case 'select': return field.options?.[0]?.value ?? '';
|
|
45
|
-
case 'json': return {};
|
|
46
|
-
default: return '';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const defaults = $derived((() => {
|
|
51
|
-
const d: Record<string, unknown> = {};
|
|
52
|
-
for (const f of flatStepsFields) d[f.key] = f.defaultValue ?? getDefaultForType(f);
|
|
53
|
-
return d;
|
|
54
|
-
})());
|
|
55
|
-
|
|
56
|
-
// ─── useStepsForm: form state + step navigation ──────────────────
|
|
57
|
-
const form = useStepsForm({
|
|
58
|
-
get resource() { return resourceName; },
|
|
59
|
-
get action() { return mode; },
|
|
60
|
-
get id() { return id; },
|
|
61
|
-
defaultValues: defaults,
|
|
62
|
-
redirect: 'list',
|
|
63
|
-
warnWhenUnsavedChanges: true,
|
|
64
|
-
validate: deriveValidator(flatStepsFields),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const totalSteps = $derived(steps.length);
|
|
68
|
-
const progressValue = $derived(((form.currentStep + 1) / totalSteps) * 100);
|
|
69
|
-
const isLastStep = $derived(form.currentStep >= totalSteps - 1);
|
|
70
|
-
|
|
71
|
-
// Fields for the current step
|
|
72
|
-
const currentFields = $derived(
|
|
73
|
-
steps[form.currentStep]?.fields
|
|
74
|
-
.map(key => resource.fields.find(f => f.key === key))
|
|
75
|
-
.filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Validate only fields in the current step
|
|
79
|
-
function validateCurrentStep(): boolean {
|
|
80
|
-
let valid = true;
|
|
81
|
-
form.clearErrors();
|
|
82
|
-
for (const field of currentFields) {
|
|
83
|
-
const value = form.values[field.key];
|
|
84
|
-
if (field.required && (value === undefined || value === null || value === '')) {
|
|
85
|
-
form.setFieldError(field.key, t('validation.required'));
|
|
86
|
-
valid = false;
|
|
87
|
-
} else if (field.validate) {
|
|
88
|
-
const msg = field.validate(value);
|
|
89
|
-
if (msg) {
|
|
90
|
-
form.setFieldError(field.key, msg);
|
|
91
|
-
valid = false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return valid;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function handleSubmit() {
|
|
99
|
-
if (!validateCurrentStep()) return;
|
|
100
|
-
|
|
101
|
-
if (!isLastStep) {
|
|
102
|
-
form.next();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Last step — submit the full form
|
|
107
|
-
await form.submit({ redirect: 'list' });
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<div class="space-y-6">
|
|
112
|
-
<!-- Step indicators -->
|
|
113
|
-
<div class="space-y-3">
|
|
114
|
-
<div class="flex items-center justify-between text-sm">
|
|
115
|
-
{#each steps as step, i}
|
|
116
|
-
<Button
|
|
117
|
-
variant="ghost"
|
|
118
|
-
type="button"
|
|
119
|
-
class="flex items-center gap-2 transition-colors h-auto py-1 px-2 {i === form.currentStep ? 'text-primary font-semibold' : 'text-muted-foreground'}"
|
|
120
|
-
onclick={() => form.gotoStep(i)}
|
|
121
|
-
>
|
|
122
|
-
<span
|
|
123
|
-
class="flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium transition-colors"
|
|
124
|
-
class:bg-primary={i <= form.currentStep}
|
|
125
|
-
class:text-primary-foreground={i <= form.currentStep}
|
|
126
|
-
class:bg-muted={i > form.currentStep}
|
|
127
|
-
class:text-muted-foreground={i > form.currentStep}
|
|
128
|
-
>
|
|
129
|
-
{#if i < form.currentStep}
|
|
130
|
-
<Check class="h-3.5 w-3.5" />
|
|
131
|
-
{:else}
|
|
132
|
-
{i + 1}
|
|
133
|
-
{/if}
|
|
134
|
-
</span>
|
|
135
|
-
<span class="hidden sm:inline">{step.title}</span>
|
|
136
|
-
</Button>
|
|
137
|
-
{#if i < steps.length - 1}
|
|
138
|
-
<div class="flex-1 mx-2 h-px bg-border" class:bg-primary={i < form.currentStep}></div>
|
|
139
|
-
{/if}
|
|
140
|
-
{/each}
|
|
141
|
-
</div>
|
|
142
|
-
<Progress value={progressValue} class="h-1.5" />
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<!-- Step content -->
|
|
146
|
-
<Card.Root>
|
|
147
|
-
<Card.CardHeader>
|
|
148
|
-
<Card.CardTitle>{steps[form.currentStep]?.title ?? ''}</Card.CardTitle>
|
|
149
|
-
<Badge variant="outline" class="w-fit">
|
|
150
|
-
{t('common.step')} {form.currentStep + 1} / {totalSteps}
|
|
151
|
-
</Badge>
|
|
152
|
-
</Card.CardHeader>
|
|
153
|
-
<Card.CardContent>
|
|
154
|
-
{#key form.currentStep}
|
|
155
|
-
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} in:fly={{ x: 30, duration: 250 }}>
|
|
156
|
-
{#each currentFields as field (field.key)}
|
|
157
|
-
<FieldRenderer
|
|
158
|
-
{field}
|
|
159
|
-
value={form.values[field.key]}
|
|
160
|
-
onchange={(v) => form.setFieldValue(field.key, v)}
|
|
161
|
-
/>
|
|
162
|
-
{#if form.errors[field.key]}
|
|
163
|
-
<p class="text-destructive text-[0.8125rem] mt-1">{form.errors[field.key]}</p>
|
|
164
|
-
{/if}
|
|
165
|
-
{/each}
|
|
166
|
-
|
|
167
|
-
<div class="flex items-center justify-between pt-4">
|
|
168
|
-
<Button
|
|
169
|
-
type="button"
|
|
170
|
-
variant="outline"
|
|
171
|
-
onclick={() => form.currentStep === 0 ? navigate(`/${resourceName}`) : form.prev()}
|
|
172
|
-
>
|
|
173
|
-
<ArrowLeft class="h-4 w-4" data-icon="inline-start" />
|
|
174
|
-
{form.currentStep === 0 ? t('common.cancel') : t('common.back')}
|
|
175
|
-
</Button>
|
|
176
|
-
|
|
177
|
-
<Button type="submit" disabled={form.submitting}>
|
|
178
|
-
{#if form.submitting}
|
|
179
|
-
<Loader2 class="h-4 w-4 animate-spin" data-icon="inline-start" />
|
|
180
|
-
{:else if isLastStep}
|
|
181
|
-
<Save class="h-4 w-4" data-icon="inline-start" />
|
|
182
|
-
{:else}
|
|
183
|
-
<ArrowRight class="h-4 w-4" data-icon="inline-start" />
|
|
184
|
-
{/if}
|
|
185
|
-
{isLastStep ? t('common.save') : t('common.next')}
|
|
186
|
-
</Button>
|
|
187
|
-
</div>
|
|
188
|
-
</form>
|
|
189
|
-
{/key}
|
|
190
|
-
</Card.CardContent>
|
|
191
|
-
</Card.Root>
|
|
192
|
-
</div>
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { Loader2, CheckCircle2, XCircle, Clock, Download, X, ListTodo } from '@lucide/svelte';
|
|
4
|
-
import { Button } from './ui/button/index.js';
|
|
5
|
-
import { Badge } from './ui/badge/index.js';
|
|
6
|
-
import type { BackgroundTask, TaskStatus } from '../types.js';
|
|
7
|
-
|
|
8
|
-
let {
|
|
9
|
-
open = $bindable(false),
|
|
10
|
-
tasks = $bindable<BackgroundTask[]>([]),
|
|
11
|
-
onDismiss,
|
|
12
|
-
onRetry,
|
|
13
|
-
onDownload,
|
|
14
|
-
}: {
|
|
15
|
-
open?: boolean;
|
|
16
|
-
tasks?: BackgroundTask[];
|
|
17
|
-
onDismiss?: (taskId: string) => void;
|
|
18
|
-
onRetry?: (taskId: string) => void;
|
|
19
|
-
onDownload?: (taskId: string, url: string) => void;
|
|
20
|
-
} = $props();
|
|
21
|
-
|
|
22
|
-
let runningCount = $derived(tasks.filter(t => t.status === 'running' || t.status === 'pending').length);
|
|
23
|
-
|
|
24
|
-
const statusIcon: Record<TaskStatus, typeof Loader2> = {
|
|
25
|
-
pending: Clock,
|
|
26
|
-
running: Loader2,
|
|
27
|
-
completed: CheckCircle2,
|
|
28
|
-
failed: XCircle,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const statusColor: Record<TaskStatus, string> = {
|
|
32
|
-
pending: 'text-muted-foreground',
|
|
33
|
-
running: 'text-primary',
|
|
34
|
-
completed: 'text-green-500',
|
|
35
|
-
failed: 'text-destructive',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function formatTime(d: string | Date) {
|
|
39
|
-
return new Date(d).toLocaleTimeString();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function dismissTask(id: string) {
|
|
43
|
-
tasks = tasks.filter(t => t.id !== id);
|
|
44
|
-
onDismiss?.(id);
|
|
45
|
-
}
|
|
46
|
-
</script>
|
|
47
|
-
|
|
48
|
-
<!-- Trigger Button (to be placed in Header) -->
|
|
49
|
-
<Button
|
|
50
|
-
variant="ghost"
|
|
51
|
-
size="icon"
|
|
52
|
-
class="relative"
|
|
53
|
-
onclick={() => open = !open}
|
|
54
|
-
>
|
|
55
|
-
<ListTodo class="h-4 w-4" />
|
|
56
|
-
{#if runningCount > 0}
|
|
57
|
-
<span class="absolute -top-0.5 -right-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground">
|
|
58
|
-
{runningCount}
|
|
59
|
-
</span>
|
|
60
|
-
{/if}
|
|
61
|
-
</Button>
|
|
62
|
-
|
|
63
|
-
<!-- Task Queue Drawer -->
|
|
64
|
-
{#if open}
|
|
65
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
66
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
67
|
-
<div class="fixed inset-0 z-[90] flex justify-end" onclick={() => open = false}>
|
|
68
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
69
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
70
|
-
<div
|
|
71
|
-
class="bg-card shadow-2xl w-full max-w-sm h-full flex flex-col border-l border-border animate-in slide-in-from-right duration-200"
|
|
72
|
-
onclick={(e) => e.stopPropagation()}
|
|
73
|
-
>
|
|
74
|
-
<!-- Header -->
|
|
75
|
-
<div class="p-4 border-b border-border flex items-center justify-between bg-muted/30">
|
|
76
|
-
<div class="flex items-center gap-2">
|
|
77
|
-
<ListTodo class="h-5 w-5 text-muted-foreground" />
|
|
78
|
-
<h2 class="text-sm font-semibold">Background Tasks</h2>
|
|
79
|
-
{#if runningCount > 0}
|
|
80
|
-
<Badge variant="secondary" class="text-xs">{runningCount} active</Badge>
|
|
81
|
-
{/if}
|
|
82
|
-
</div>
|
|
83
|
-
<Button variant="ghost" size="icon" class="h-7 w-7" onclick={() => open = false}>
|
|
84
|
-
<X class="h-4 w-4" />
|
|
85
|
-
</Button>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<!-- Task List -->
|
|
89
|
-
<div class="flex-1 overflow-y-auto">
|
|
90
|
-
{#each tasks as task}
|
|
91
|
-
{@const Icon = statusIcon[task.status]}
|
|
92
|
-
<div class="p-4 border-b border-border hover:bg-muted/30 transition-colors">
|
|
93
|
-
<div class="flex items-start justify-between gap-2">
|
|
94
|
-
<div class="flex items-start gap-3 flex-1 min-w-0">
|
|
95
|
-
<div class="{statusColor[task.status]} mt-0.5 shrink-0">
|
|
96
|
-
<Icon class="h-4 w-4 {task.status === 'running' ? 'animate-spin' : ''}" />
|
|
97
|
-
</div>
|
|
98
|
-
<div class="flex-1 min-w-0">
|
|
99
|
-
<p class="text-sm font-medium truncate text-foreground">{task.title}</p>
|
|
100
|
-
{#if task.message}
|
|
101
|
-
<p class="text-xs text-muted-foreground mt-0.5">{task.message}</p>
|
|
102
|
-
{/if}
|
|
103
|
-
<p class="text-[10px] text-muted-foreground/60 mt-1 font-mono">{formatTime(task.createdAt)}</p>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
<button
|
|
107
|
-
class="text-muted-foreground/40 hover:text-muted-foreground transition shrink-0"
|
|
108
|
-
onclick={() => dismissTask(task.id)}
|
|
109
|
-
>
|
|
110
|
-
<X class="h-3.5 w-3.5" />
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<!-- Progress bar -->
|
|
115
|
-
{#if task.status === 'running' && task.progress != null}
|
|
116
|
-
<div class="mt-3 ml-7">
|
|
117
|
-
<div class="h-1.5 w-full bg-muted rounded-full overflow-hidden">
|
|
118
|
-
<div
|
|
119
|
-
class="h-full bg-primary rounded-full transition-all duration-500 ease-out"
|
|
120
|
-
style="width: {task.progress}%"
|
|
121
|
-
></div>
|
|
122
|
-
</div>
|
|
123
|
-
<p class="text-[10px] text-muted-foreground mt-1">{task.progress}%</p>
|
|
124
|
-
</div>
|
|
125
|
-
{/if}
|
|
126
|
-
|
|
127
|
-
<!-- Actions -->
|
|
128
|
-
{#if task.status === 'completed' && task.downloadUrl}
|
|
129
|
-
<div class="mt-2 ml-7">
|
|
130
|
-
<Button
|
|
131
|
-
variant="outline" size="sm" class="h-7 text-xs"
|
|
132
|
-
onclick={() => onDownload?.(task.id, task.downloadUrl!)}
|
|
133
|
-
>
|
|
134
|
-
<Download class="h-3 w-3 mr-1" />
|
|
135
|
-
Download
|
|
136
|
-
</Button>
|
|
137
|
-
</div>
|
|
138
|
-
{:else if task.status === 'failed' && onRetry}
|
|
139
|
-
<div class="mt-2 ml-7">
|
|
140
|
-
<Button
|
|
141
|
-
variant="outline" size="sm" class="h-7 text-xs text-destructive"
|
|
142
|
-
onclick={() => onRetry?.(task.id)}
|
|
143
|
-
>
|
|
144
|
-
Retry
|
|
145
|
-
</Button>
|
|
146
|
-
</div>
|
|
147
|
-
{/if}
|
|
148
|
-
</div>
|
|
149
|
-
{:else}
|
|
150
|
-
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
|
151
|
-
<ListTodo class="h-8 w-8 mb-2 opacity-30" />
|
|
152
|
-
<p class="text-sm">No background tasks</p>
|
|
153
|
-
</div>
|
|
154
|
-
{/each}
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
{/if}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { ChevronsUpDown, Building2, Check } from '@lucide/svelte';
|
|
4
|
-
import { Button } from './ui/button/index.js';
|
|
5
|
-
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
6
|
-
import type { Tenant } from '../types.js';
|
|
7
|
-
|
|
8
|
-
let {
|
|
9
|
-
tenants = [],
|
|
10
|
-
currentTenantId = $bindable(''),
|
|
11
|
-
collapsed = false,
|
|
12
|
-
onSwitch,
|
|
13
|
-
headerSnippet,
|
|
14
|
-
}: {
|
|
15
|
-
tenants: Tenant[];
|
|
16
|
-
currentTenantId?: string;
|
|
17
|
-
collapsed?: boolean;
|
|
18
|
-
onSwitch?: (tenantId: string) => void;
|
|
19
|
-
headerSnippet?: Snippet;
|
|
20
|
-
} = $props();
|
|
21
|
-
|
|
22
|
-
let currentTenant = $derived(tenants.find(t => t.id === currentTenantId) ?? tenants[0]);
|
|
23
|
-
|
|
24
|
-
function selectTenant(id: string) {
|
|
25
|
-
currentTenantId = id;
|
|
26
|
-
onSwitch?.(id);
|
|
27
|
-
}
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
{#if tenants.length > 0}
|
|
31
|
-
<DropdownMenu.Root>
|
|
32
|
-
<DropdownMenu.Trigger>
|
|
33
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
34
|
-
<button
|
|
35
|
-
{...props}
|
|
36
|
-
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-sidebar-accent/50
|
|
37
|
-
{collapsed ? 'justify-center' : ''}"
|
|
38
|
-
>
|
|
39
|
-
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary text-sm font-bold">
|
|
40
|
-
{#if currentTenant?.logo}
|
|
41
|
-
<img src={currentTenant.logo} alt={currentTenant.name} class="h-full w-full rounded-lg object-cover" />
|
|
42
|
-
{:else}
|
|
43
|
-
<Building2 class="h-4 w-4" />
|
|
44
|
-
{/if}
|
|
45
|
-
</div>
|
|
46
|
-
{#if !collapsed}
|
|
47
|
-
<div class="flex-1 min-w-0">
|
|
48
|
-
<p class="truncate text-sm font-semibold text-sidebar-foreground">{currentTenant?.name ?? 'Select'}</p>
|
|
49
|
-
<p class="truncate text-[10px] text-sidebar-foreground/50">Workspace</p>
|
|
50
|
-
</div>
|
|
51
|
-
<ChevronsUpDown class="h-4 w-4 shrink-0 text-sidebar-foreground/40" />
|
|
52
|
-
{/if}
|
|
53
|
-
</button>
|
|
54
|
-
{/snippet}
|
|
55
|
-
</DropdownMenu.Trigger>
|
|
56
|
-
<DropdownMenu.Content class="w-56">
|
|
57
|
-
{#if headerSnippet}
|
|
58
|
-
<div class="px-2 py-1.5">
|
|
59
|
-
{@render headerSnippet()}
|
|
60
|
-
</div>
|
|
61
|
-
<DropdownMenu.Separator />
|
|
62
|
-
{/if}
|
|
63
|
-
<div class="px-2 py-1.5 text-xs text-muted-foreground">Workspaces</div>
|
|
64
|
-
{#each tenants as tenant}
|
|
65
|
-
<DropdownMenu.Item
|
|
66
|
-
class="flex items-center gap-3 cursor-pointer"
|
|
67
|
-
onclick={() => selectTenant(tenant.id)}
|
|
68
|
-
>
|
|
69
|
-
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-primary/10 text-primary text-xs font-bold">
|
|
70
|
-
{#if tenant.logo}
|
|
71
|
-
<img src={tenant.logo} alt={tenant.name} class="h-full w-full rounded object-cover" />
|
|
72
|
-
{:else}
|
|
73
|
-
{tenant.name.charAt(0).toUpperCase()}
|
|
74
|
-
{/if}
|
|
75
|
-
</div>
|
|
76
|
-
<span class="flex-1 truncate">{tenant.name}</span>
|
|
77
|
-
{#if tenant.id === currentTenantId}
|
|
78
|
-
<Check class="h-4 w-4 text-primary" />
|
|
79
|
-
{/if}
|
|
80
|
-
</DropdownMenu.Item>
|
|
81
|
-
{/each}
|
|
82
|
-
</DropdownMenu.Content>
|
|
83
|
-
</DropdownMenu.Root>
|
|
84
|
-
{/if}
|