@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,192 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getDataProvider, getResources, inferResource } from '@svadmin/core';
|
|
3
|
-
import type { InferResult } from '@svadmin/core';
|
|
4
|
-
import { Button } from './ui/button/index.js';
|
|
5
|
-
import { Input } from './ui/input/index.js';
|
|
6
|
-
import { Badge } from './ui/badge/index.js';
|
|
7
|
-
import * as Card from './ui/card/index.js';
|
|
8
|
-
import * as Table from './ui/table/index.js';
|
|
9
|
-
import * as Alert from './ui/alert/index.js';
|
|
10
|
-
import { Skeleton } from './ui/skeleton/index.js';
|
|
11
|
-
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
12
|
-
import { Select } from './ui/select/index.js';
|
|
13
|
-
import { Wand2, Copy, Check, RefreshCw, Loader2, AlertCircle } from '@lucide/svelte';
|
|
14
|
-
|
|
15
|
-
const dataProvider = getDataProvider();
|
|
16
|
-
const resources = getResources();
|
|
17
|
-
|
|
18
|
-
let selectedResource = $state('');
|
|
19
|
-
let inferResult = $state<InferResult | null>(null);
|
|
20
|
-
let loading = $state(false);
|
|
21
|
-
let error = $state<string | null>(null);
|
|
22
|
-
let copied = $state(false);
|
|
23
|
-
let customEndpoint = $state('');
|
|
24
|
-
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
-
|
|
26
|
-
$effect(() => {
|
|
27
|
-
return () => { if (copyTimer) clearTimeout(copyTimer); };
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
async function runInference() {
|
|
31
|
-
const resourceName = customEndpoint.trim() || selectedResource;
|
|
32
|
-
if (!resourceName) return;
|
|
33
|
-
|
|
34
|
-
loading = true;
|
|
35
|
-
error = null;
|
|
36
|
-
inferResult = null;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const response = await dataProvider.getList({
|
|
40
|
-
resource: resourceName,
|
|
41
|
-
pagination: { current: 1, pageSize: 25 },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!response.data || response.data.length === 0) {
|
|
45
|
-
error = `No data returned from "${resourceName}". The API must return at least one record to infer fields.`;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
inferResult = inferResource(
|
|
50
|
-
resourceName,
|
|
51
|
-
response.data as Record<string, unknown>[],
|
|
52
|
-
);
|
|
53
|
-
} catch (e: unknown) {
|
|
54
|
-
error = e instanceof Error ? e.message : 'Failed to fetch data for inference.';
|
|
55
|
-
} finally {
|
|
56
|
-
loading = false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function copyCode() {
|
|
61
|
-
if (!inferResult) return;
|
|
62
|
-
navigator.clipboard.writeText(inferResult.code);
|
|
63
|
-
copied = true;
|
|
64
|
-
if (copyTimer) clearTimeout(copyTimer);
|
|
65
|
-
copyTimer = setTimeout(() => { copied = false; }, 2000);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const typeVariants: Record<string, 'default' | 'secondary' | 'outline' | 'destructive'> = {
|
|
69
|
-
text: 'secondary',
|
|
70
|
-
number: 'default',
|
|
71
|
-
boolean: 'outline',
|
|
72
|
-
date: 'secondary',
|
|
73
|
-
email: 'default',
|
|
74
|
-
url: 'outline',
|
|
75
|
-
image: 'secondary',
|
|
76
|
-
images: 'secondary',
|
|
77
|
-
textarea: 'outline',
|
|
78
|
-
json: 'secondary',
|
|
79
|
-
tags: 'default',
|
|
80
|
-
select: 'outline',
|
|
81
|
-
relation: 'destructive',
|
|
82
|
-
color: 'secondary',
|
|
83
|
-
phone: 'default',
|
|
84
|
-
};
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<Card.Root>
|
|
88
|
-
<Card.CardHeader class="pb-3">
|
|
89
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
90
|
-
<Wand2 class="h-4 w-4" />
|
|
91
|
-
Resource Inferencer
|
|
92
|
-
</Card.CardTitle>
|
|
93
|
-
</Card.CardHeader>
|
|
94
|
-
<Card.CardContent class="space-y-4">
|
|
95
|
-
<!-- Resource selector -->
|
|
96
|
-
<div class="flex gap-2">
|
|
97
|
-
<Select
|
|
98
|
-
class="flex-1"
|
|
99
|
-
bind:value={selectedResource}
|
|
100
|
-
placeholder="— Select a resource —"
|
|
101
|
-
>
|
|
102
|
-
{#each resources as res}
|
|
103
|
-
<option value={res.name}>{res.label} ({res.name})</option>
|
|
104
|
-
{/each}
|
|
105
|
-
</Select>
|
|
106
|
-
<span class="flex items-center text-xs text-muted-foreground">or</span>
|
|
107
|
-
<Input
|
|
108
|
-
type="text"
|
|
109
|
-
class="w-36"
|
|
110
|
-
placeholder="custom endpoint"
|
|
111
|
-
bind:value={customEndpoint}
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<Button size="sm" onclick={runInference} disabled={loading || (!selectedResource && !customEndpoint.trim())}>
|
|
116
|
-
{#if loading}
|
|
117
|
-
<Loader2 class="h-3 w-3 animate-spin" data-icon="inline-start" />
|
|
118
|
-
Inferring...
|
|
119
|
-
{:else}
|
|
120
|
-
<RefreshCw class="h-3 w-3" data-icon="inline-start" />
|
|
121
|
-
Infer Fields
|
|
122
|
-
{/if}
|
|
123
|
-
</Button>
|
|
124
|
-
|
|
125
|
-
{#if error}
|
|
126
|
-
<Alert.Root variant="destructive">
|
|
127
|
-
<AlertCircle class="h-4 w-4" />
|
|
128
|
-
<Alert.Description>{error}</Alert.Description>
|
|
129
|
-
</Alert.Root>
|
|
130
|
-
{/if}
|
|
131
|
-
|
|
132
|
-
{#if loading}
|
|
133
|
-
<div class="space-y-2">
|
|
134
|
-
{#each Array(4) as _}
|
|
135
|
-
<Skeleton class="h-8 w-full" />
|
|
136
|
-
{/each}
|
|
137
|
-
</div>
|
|
138
|
-
{/if}
|
|
139
|
-
|
|
140
|
-
{#if inferResult}
|
|
141
|
-
<!-- Field table -->
|
|
142
|
-
<ScrollArea class="max-h-80">
|
|
143
|
-
<Table.Root>
|
|
144
|
-
<Table.Header>
|
|
145
|
-
<Table.Row class="bg-muted/50">
|
|
146
|
-
<Table.Head>Field</Table.Head>
|
|
147
|
-
<Table.Head>Type</Table.Head>
|
|
148
|
-
<Table.Head class="text-center">List</Table.Head>
|
|
149
|
-
<Table.Head class="text-center">Form</Table.Head>
|
|
150
|
-
</Table.Row>
|
|
151
|
-
</Table.Header>
|
|
152
|
-
<Table.Body>
|
|
153
|
-
{#each inferResult.fields as field}
|
|
154
|
-
<Table.Row>
|
|
155
|
-
<Table.Cell class="font-mono text-xs">{field.key}</Table.Cell>
|
|
156
|
-
<Table.Cell>
|
|
157
|
-
<Badge variant={typeVariants[field.type] ?? 'secondary'}>
|
|
158
|
-
{field.type}
|
|
159
|
-
{#if field.resource}→ {field.resource}{/if}
|
|
160
|
-
</Badge>
|
|
161
|
-
</Table.Cell>
|
|
162
|
-
<Table.Cell class="text-center">{field.showInList ? '✓' : '—'}</Table.Cell>
|
|
163
|
-
<Table.Cell class="text-center">{field.showInForm ? '✓' : '—'}</Table.Cell>
|
|
164
|
-
</Table.Row>
|
|
165
|
-
{/each}
|
|
166
|
-
</Table.Body>
|
|
167
|
-
</Table.Root>
|
|
168
|
-
</ScrollArea>
|
|
169
|
-
|
|
170
|
-
<!-- Generated code -->
|
|
171
|
-
<div class="rounded-lg border overflow-hidden">
|
|
172
|
-
<div class="flex items-center justify-between bg-muted px-3 py-1.5 text-xs text-muted-foreground">
|
|
173
|
-
<span>Generated ResourceDefinition</span>
|
|
174
|
-
<Button variant="ghost" size="sm" class="h-6 text-xs gap-1" onclick={copyCode}>
|
|
175
|
-
{#if copied}
|
|
176
|
-
<Check class="h-3 w-3 text-emerald-500" />
|
|
177
|
-
<span class="text-emerald-500">Copied!</span>
|
|
178
|
-
{:else}
|
|
179
|
-
<Copy class="h-3 w-3" />
|
|
180
|
-
Copy
|
|
181
|
-
{/if}
|
|
182
|
-
</Button>
|
|
183
|
-
</div>
|
|
184
|
-
<pre class="max-h-64 overflow-auto bg-muted/30 p-3 text-xs text-foreground font-mono">{inferResult.code}</pre>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<p class="text-xs text-muted-foreground">
|
|
188
|
-
Inferred {inferResult.fields.length} fields from sample data. Copy the code above into your <code class="bg-muted px-1 rounded text-foreground">resources.ts</code> file.
|
|
189
|
-
</p>
|
|
190
|
-
{/if}
|
|
191
|
-
</Card.CardContent>
|
|
192
|
-
</Card.Root>
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { fade, fly } from 'svelte/transition';
|
|
3
|
-
import { useInfiniteList } from '@svadmin/core';
|
|
4
|
-
import type { BaseRecord, Sort, Filter } from '@svadmin/core';
|
|
5
|
-
import type { Snippet } from 'svelte';
|
|
6
|
-
import { t } from '@svadmin/core/i18n';
|
|
7
|
-
import { Skeleton } from './ui/skeleton/index.js';
|
|
8
|
-
import { Button } from './ui/button/index.js';
|
|
9
|
-
import { Loader2 } from '@lucide/svelte';
|
|
10
|
-
import { intersect } from '../actions.js';
|
|
11
|
-
|
|
12
|
-
interface Props<T extends BaseRecord = BaseRecord> {
|
|
13
|
-
resource: string;
|
|
14
|
-
pageSize?: number;
|
|
15
|
-
sorters?: Sort[];
|
|
16
|
-
filters?: Filter[];
|
|
17
|
-
/** Render each item */
|
|
18
|
-
children: Snippet<[{ item: T; index: number }]>;
|
|
19
|
-
/** Custom empty state */
|
|
20
|
-
empty?: Snippet;
|
|
21
|
-
/** Custom loading skeleton */
|
|
22
|
-
loadingSkeleton?: Snippet;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let {
|
|
26
|
-
resource,
|
|
27
|
-
pageSize = 20,
|
|
28
|
-
sorters,
|
|
29
|
-
filters,
|
|
30
|
-
children,
|
|
31
|
-
empty,
|
|
32
|
-
loadingSkeleton,
|
|
33
|
-
}: Props = $props();
|
|
34
|
-
|
|
35
|
-
const { query } = useInfiniteList({
|
|
36
|
-
get resource() { return resource; },
|
|
37
|
-
get pagination() { return { pageSize }; },
|
|
38
|
-
get sorters() { return sorters; },
|
|
39
|
-
get filters() { return filters; },
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const allItems = $derived(
|
|
43
|
-
(query.data as { pages?: { data: BaseRecord[] }[] })?.pages?.flatMap(p => p.data) ?? []
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const hasNextPage = $derived(query.hasNextPage ?? false);
|
|
47
|
-
const isFetchingNextPage = $derived(query.isFetchingNextPage ?? false);
|
|
48
|
-
|
|
49
|
-
function loadMore() {
|
|
50
|
-
if (hasNextPage && !isFetchingNextPage) {
|
|
51
|
-
query.fetchNextPage();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
</script>
|
|
55
|
-
|
|
56
|
-
<div class="space-y-2">
|
|
57
|
-
{#if query.isLoading}
|
|
58
|
-
{#if loadingSkeleton}
|
|
59
|
-
{@render loadingSkeleton()}
|
|
60
|
-
{:else}
|
|
61
|
-
<div class="space-y-3">
|
|
62
|
-
{#each Array(5) as _}
|
|
63
|
-
<Skeleton class="h-16 w-full rounded-lg" />
|
|
64
|
-
{/each}
|
|
65
|
-
</div>
|
|
66
|
-
{/if}
|
|
67
|
-
{:else if allItems.length === 0}
|
|
68
|
-
{#if empty}
|
|
69
|
-
{@render empty()}
|
|
70
|
-
{:else}
|
|
71
|
-
<p class="text-center text-sm text-muted-foreground py-8">{t('empty.title')}</p>
|
|
72
|
-
{/if}
|
|
73
|
-
{:else}
|
|
74
|
-
{#each allItems as item, index}
|
|
75
|
-
<div in:fade={{ duration: 200, delay: index < 20 ? index * 30 : 0 }}>
|
|
76
|
-
{@render children({ item, index })}
|
|
77
|
-
</div>
|
|
78
|
-
{/each}
|
|
79
|
-
|
|
80
|
-
{#if isFetchingNextPage}
|
|
81
|
-
<div class="flex justify-center py-4">
|
|
82
|
-
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
|
|
83
|
-
</div>
|
|
84
|
-
{/if}
|
|
85
|
-
|
|
86
|
-
{#if hasNextPage}
|
|
87
|
-
<!-- Sentinel for infinite scroll -->
|
|
88
|
-
<div use:intersect={loadMore} class="h-1"></div>
|
|
89
|
-
{:else}
|
|
90
|
-
<p class="text-center text-xs text-muted-foreground py-2">{t('empty.noMore')}</p>
|
|
91
|
-
{/if}
|
|
92
|
-
{/if}
|
|
93
|
-
</div>
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { useUpdate } from '@svadmin/core';
|
|
3
|
-
import type { FieldDefinition } from '@svadmin/core';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
resourceName: string;
|
|
7
|
-
recordId: string | number;
|
|
8
|
-
field: FieldDefinition;
|
|
9
|
-
value: unknown;
|
|
10
|
-
/** Callback after successful save */
|
|
11
|
-
onSave?: (newValue: unknown) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let { resourceName, recordId, field, value, onSave }: Props = $props();
|
|
15
|
-
|
|
16
|
-
let editing = $state(false);
|
|
17
|
-
let editValue = $state('');
|
|
18
|
-
let inputRef = $state<HTMLInputElement | null>(null);
|
|
19
|
-
let saving = $state(false);
|
|
20
|
-
const displayValue = $derived(String(value ?? ''));
|
|
21
|
-
|
|
22
|
-
const { mutation } = useUpdate({ get resource() { return resourceName; } });
|
|
23
|
-
|
|
24
|
-
function startEdit() {
|
|
25
|
-
if (field.type === 'number' || field.type === 'text' || field.type === 'url' || field.type === 'email') {
|
|
26
|
-
editing = true;
|
|
27
|
-
editValue = displayValue;
|
|
28
|
-
// Focus input on next tick
|
|
29
|
-
setTimeout(() => inputRef?.focus(), 0);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function save() {
|
|
34
|
-
if (!editing || saving) return;
|
|
35
|
-
const newValue = field.type === 'number' ? Number(editValue) : editValue;
|
|
36
|
-
if (newValue === value) {
|
|
37
|
-
editing = false;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
saving = true;
|
|
41
|
-
try {
|
|
42
|
-
await mutation.mutateAsync({
|
|
43
|
-
id: recordId,
|
|
44
|
-
resource: resourceName,
|
|
45
|
-
variables: { [field.key]: newValue },
|
|
46
|
-
});
|
|
47
|
-
editing = false;
|
|
48
|
-
onSave?.(newValue);
|
|
49
|
-
} catch {
|
|
50
|
-
// Keep editing on error
|
|
51
|
-
} finally {
|
|
52
|
-
saving = false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function cancel() {
|
|
57
|
-
editing = false;
|
|
58
|
-
editValue = displayValue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function handleKeydown(e: KeyboardEvent) {
|
|
62
|
-
if (e.key === 'Enter') save();
|
|
63
|
-
if (e.key === 'Escape') cancel();
|
|
64
|
-
}
|
|
65
|
-
</script>
|
|
66
|
-
|
|
67
|
-
{#if editing}
|
|
68
|
-
<input
|
|
69
|
-
bind:this={inputRef}
|
|
70
|
-
type={field.type === 'number' ? 'number' : 'text'}
|
|
71
|
-
bind:value={editValue}
|
|
72
|
-
onkeydown={handleKeydown}
|
|
73
|
-
onblur={save}
|
|
74
|
-
disabled={saving}
|
|
75
|
-
class="h-7 w-full rounded border bg-background px-2 text-sm outline-none ring-1 ring-primary/50 focus:ring-2 focus:ring-primary transition-all"
|
|
76
|
-
/>
|
|
77
|
-
{:else}
|
|
78
|
-
<span
|
|
79
|
-
class="inline-block cursor-pointer rounded px-1 py-0.5 -mx-1 hover:bg-accent/50 transition-colors"
|
|
80
|
-
ondblclick={startEdit}
|
|
81
|
-
title="Double-click to edit"
|
|
82
|
-
role="button"
|
|
83
|
-
tabindex="0"
|
|
84
|
-
onkeydown={(e) => { if (e.key === 'Enter' || e.key === 'F2') startEdit(); }}
|
|
85
|
-
>
|
|
86
|
-
{value ?? '—'}
|
|
87
|
-
</span>
|
|
88
|
-
{/if}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getChatProvider } from '@svadmin/core';
|
|
3
|
-
import { Sparkles, RefreshCcw, Loader2 } from '@lucide/svelte';
|
|
4
|
-
import { Button } from './ui/button/index.js';
|
|
5
|
-
import MarkdownRenderer from './MarkdownRenderer.svelte';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
title?: string;
|
|
9
|
-
context: string;
|
|
10
|
-
autoFetch?: boolean;
|
|
11
|
-
class?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let { title = 'AI Insights', context, autoFetch = true, class: className = '' }: Props = $props();
|
|
15
|
-
|
|
16
|
-
const provider = $derived(getChatProvider());
|
|
17
|
-
|
|
18
|
-
let isPredicting = $state(false);
|
|
19
|
-
let insightText = $state('');
|
|
20
|
-
let abortController: AbortController | null = null;
|
|
21
|
-
let hasFetchedForContext = $state('');
|
|
22
|
-
|
|
23
|
-
$effect(() => {
|
|
24
|
-
if (autoFetch && context && provider && hasFetchedForContext !== context) {
|
|
25
|
-
hasFetchedForContext = context;
|
|
26
|
-
getInsights();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export async function getInsights() {
|
|
31
|
-
if (!provider || !context) return;
|
|
32
|
-
|
|
33
|
-
isPredicting = true;
|
|
34
|
-
insightText = '';
|
|
35
|
-
|
|
36
|
-
if (abortController) abortController.abort();
|
|
37
|
-
abortController = new AbortController();
|
|
38
|
-
|
|
39
|
-
const prompt = `Based on the following data/context, generate 3 concise, highly professional insights.
|
|
40
|
-
Use bullet points. Do not include introductory text like "Here are the insights".
|
|
41
|
-
|
|
42
|
-
Data Context:
|
|
43
|
-
${context}`;
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const result = provider.sendMessage(
|
|
47
|
-
[{ id: 'insights', role: 'user', content: prompt, timestamp: Date.now() }],
|
|
48
|
-
{ signal: abortController.signal }
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
if (Symbol.asyncIterator in result) {
|
|
52
|
-
for await (const chunk of result as AsyncIterable<string>) {
|
|
53
|
-
insightText += chunk;
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
insightText = await result;
|
|
57
|
-
}
|
|
58
|
-
} catch (err: unknown) {
|
|
59
|
-
if (err instanceof Error && err.name !== 'AbortError') {
|
|
60
|
-
insightText = 'Failed to generate insights.';
|
|
61
|
-
}
|
|
62
|
-
} finally {
|
|
63
|
-
isPredicting = false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
<div class="rounded-xl border bg-card text-card-foreground shadow-sm flex flex-col {className}">
|
|
69
|
-
<div class="flex items-center justify-between p-4 border-b">
|
|
70
|
-
<div class="flex items-center gap-2 font-medium">
|
|
71
|
-
<Sparkles class="h-4 w-4 text-amber-500" />
|
|
72
|
-
{title}
|
|
73
|
-
</div>
|
|
74
|
-
<Button variant="ghost" size="icon-sm" class="h-6 w-6" onclick={getInsights} disabled={isPredicting || !provider}>
|
|
75
|
-
<RefreshCcw class="h-3 w-3 {isPredicting ? 'animate-spin' : ''}" />
|
|
76
|
-
</Button>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div class="p-4 flex-1">
|
|
80
|
-
{#if !provider}
|
|
81
|
-
<div class="text-sm text-muted-foreground italic flex items-center justify-center h-full">
|
|
82
|
-
ChatProvider not configured.
|
|
83
|
-
</div>
|
|
84
|
-
{:else if isPredicting && !insightText}
|
|
85
|
-
<div class="flex items-center justify-center gap-2 text-sm text-muted-foreground h-full py-4">
|
|
86
|
-
<Loader2 class="h-4 w-4 animate-spin" /> Analyzing data...
|
|
87
|
-
</div>
|
|
88
|
-
{:else if !insightText}
|
|
89
|
-
<div class="text-sm text-muted-foreground italic flex items-center justify-center h-full">
|
|
90
|
-
No insights available. Click refresh to analyze.
|
|
91
|
-
</div>
|
|
92
|
-
{:else}
|
|
93
|
-
<div class="text-sm prose prose-sm dark:prose-invert prose-p:my-1 prose-ul:my-1 prose-li:my-0.5">
|
|
94
|
-
<MarkdownRenderer content={insightText} streaming={isPredicting} />
|
|
95
|
-
</div>
|
|
96
|
-
{/if}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { t } from '@svadmin/core/i18n';
|
|
3
|
-
import * as Dialog from './ui/dialog/index.js';
|
|
4
|
-
|
|
5
|
-
let { open = $bindable(false) }: { open?: boolean } = $props();
|
|
6
|
-
|
|
7
|
-
interface Shortcut {
|
|
8
|
-
keys: string[];
|
|
9
|
-
label: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const shortcuts: { group: string; items: Shortcut[] }[] = [
|
|
13
|
-
{
|
|
14
|
-
group: 'Navigation',
|
|
15
|
-
items: [
|
|
16
|
-
{ keys: ['⌘', 'K'], label: 'Open command palette' },
|
|
17
|
-
{ keys: ['?'], label: 'Show keyboard shortcuts' },
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
group: 'Table',
|
|
22
|
-
items: [
|
|
23
|
-
{ keys: ['⌘', 'F'], label: 'Focus search' },
|
|
24
|
-
{ keys: ['Esc'], label: 'Close dialog / cancel' },
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
group: 'Theme',
|
|
29
|
-
items: [
|
|
30
|
-
{ keys: ['⌘', 'D'], label: 'Toggle dark mode' },
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<Dialog.Root bind:open>
|
|
37
|
-
<Dialog.Content class="max-w-md">
|
|
38
|
-
<Dialog.Header>
|
|
39
|
-
<Dialog.Title>{t('common.keyboardShortcuts') || 'Keyboard Shortcuts'}</Dialog.Title>
|
|
40
|
-
</Dialog.Header>
|
|
41
|
-
<div class="space-y-4 py-2">
|
|
42
|
-
{#each shortcuts as group}
|
|
43
|
-
<div>
|
|
44
|
-
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">{group.group}</h4>
|
|
45
|
-
<div class="space-y-1.5">
|
|
46
|
-
{#each group.items as shortcut}
|
|
47
|
-
<div class="flex items-center justify-between py-1">
|
|
48
|
-
<span class="text-sm text-foreground">{shortcut.label}</span>
|
|
49
|
-
<div class="flex items-center gap-1">
|
|
50
|
-
{#each shortcut.keys as key}
|
|
51
|
-
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1.5 text-[10px] font-medium font-mono text-muted-foreground">{key}</kbd>
|
|
52
|
-
{/each}
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
{/each}
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
{/each}
|
|
59
|
-
</div>
|
|
60
|
-
</Dialog.Content>
|
|
61
|
-
</Dialog.Root>
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { fade, fly } from 'svelte/transition';
|
|
4
|
-
import Sidebar from './Sidebar.svelte';
|
|
5
|
-
import Toast from './Toast.svelte';
|
|
6
|
-
import Header from './Header.svelte';
|
|
7
|
-
import CommandPalette from './CommandPalette.svelte';
|
|
8
|
-
import KeyboardShortcuts from './KeyboardShortcuts.svelte';
|
|
9
|
-
import ChatDialog from './ChatDialog.svelte';
|
|
10
|
-
import TooltipButton from './TooltipButton.svelte';
|
|
11
|
-
import { t } from '@svadmin/core/i18n';
|
|
12
|
-
import { getAuthProvider } from '@svadmin/core';
|
|
13
|
-
import type { Identity, MenuItem } from '@svadmin/core';
|
|
14
|
-
import { navigate } from '@svadmin/core/router';
|
|
15
|
-
import { currentPath } from '@svadmin/core/router';
|
|
16
|
-
import { Skeleton } from './ui/skeleton/index.js';
|
|
17
|
-
import { Button } from './ui/button/index.js';
|
|
18
|
-
import * as Sheet from './ui/sheet/index.js';
|
|
19
|
-
import { Menu } from '@lucide/svelte';
|
|
20
|
-
|
|
21
|
-
let commandOpen = $state(false);
|
|
22
|
-
let shortcutsOpen = $state(false);
|
|
23
|
-
let mobileMenuOpen = $state(false);
|
|
24
|
-
|
|
25
|
-
let { children, title = 'Admin', menu, siteUrl }: { children: Snippet; title?: string; menu?: MenuItem[]; siteUrl?: string } = $props();
|
|
26
|
-
|
|
27
|
-
let hasAuth = true;
|
|
28
|
-
let auth: ReturnType<typeof getAuthProvider> | null = null;
|
|
29
|
-
try {
|
|
30
|
-
auth = getAuthProvider();
|
|
31
|
-
} catch {
|
|
32
|
-
hasAuth = false;
|
|
33
|
-
}
|
|
34
|
-
let loading = $state(true);
|
|
35
|
-
let identity = $state<Identity | null>(null);
|
|
36
|
-
|
|
37
|
-
$effect(() => {
|
|
38
|
-
if (!auth) {
|
|
39
|
-
loading = false;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
(async () => {
|
|
43
|
-
try {
|
|
44
|
-
const { authenticated, redirectTo } = await auth.check();
|
|
45
|
-
if (!authenticated) {
|
|
46
|
-
navigate(redirectTo ?? '/login');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
identity = await auth.getIdentity();
|
|
50
|
-
} finally {
|
|
51
|
-
loading = false;
|
|
52
|
-
}
|
|
53
|
-
})();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
async function handleLogout() {
|
|
57
|
-
if (!auth) return;
|
|
58
|
-
const result = await auth.logout();
|
|
59
|
-
if (result.success) {
|
|
60
|
-
navigate(result.redirectTo ?? '/login');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let collapsed = $state(false);
|
|
65
|
-
|
|
66
|
-
// Swipe gesture for mobile menu
|
|
67
|
-
let touchStartX = $state(0);
|
|
68
|
-
let touchEndX = $state(0);
|
|
69
|
-
|
|
70
|
-
function handleTouchStart(e: TouchEvent) {
|
|
71
|
-
if (e.touches.length === 1) {
|
|
72
|
-
touchStartX = e.touches[0].clientX;
|
|
73
|
-
touchEndX = e.touches[0].clientX;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function handleTouchMove(e: TouchEvent) {
|
|
78
|
-
if (e.touches.length === 1) {
|
|
79
|
-
touchEndX = e.touches[0].clientX;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function handleTouchEnd() {
|
|
84
|
-
// Only trigger swipe-to-open if starting near the left edge (e.g., within 30px)
|
|
85
|
-
// and swiping right by at least 50px
|
|
86
|
-
if (touchStartX < 30 && touchEndX - touchStartX > 50) {
|
|
87
|
-
mobileMenuOpen = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
</script>
|
|
91
|
-
|
|
92
|
-
<svelte:window
|
|
93
|
-
onkeydown={(e) => {
|
|
94
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); commandOpen = true; }
|
|
95
|
-
if (e.key === '?' && !['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as HTMLElement)?.tagName)) { e.preventDefault(); shortcutsOpen = true; }
|
|
96
|
-
}}
|
|
97
|
-
ontouchstart={handleTouchStart}
|
|
98
|
-
ontouchmove={handleTouchMove}
|
|
99
|
-
ontouchend={handleTouchEnd}
|
|
100
|
-
/>
|
|
101
|
-
{#if loading}
|
|
102
|
-
<div class="flex h-screen" in:fade={{ duration: 150 }}>
|
|
103
|
-
<div class="hidden md:block w-64 bg-sidebar/80 p-4 space-y-4">
|
|
104
|
-
<Skeleton class="h-8 w-32" />
|
|
105
|
-
<div class="space-y-2 mt-6">
|
|
106
|
-
{#each Array(5) as _}
|
|
107
|
-
<Skeleton class="h-9 w-full rounded-lg" />
|
|
108
|
-
{/each}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="flex-1 p-8 space-y-6">
|
|
112
|
-
<Skeleton class="h-8 w-48" />
|
|
113
|
-
<div class="space-y-3">
|
|
114
|
-
{#each Array(4) as _}
|
|
115
|
-
<Skeleton class="h-12 w-full" />
|
|
116
|
-
{/each}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
{:else}
|
|
121
|
-
<div class="flex h-screen bg-background" in:fade={{ duration: 200, delay: 50 }}>
|
|
122
|
-
<!-- Desktop sidebar -->
|
|
123
|
-
<div class="hidden md:block">
|
|
124
|
-
<Sidebar {collapsed} {identity} {title} {menu} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<!-- Mobile sidebar via Sheet -->
|
|
128
|
-
<Sheet.Root bind:open={mobileMenuOpen} side="left">
|
|
129
|
-
<div class="md:hidden">
|
|
130
|
-
<Sidebar collapsed={false} {identity} {title} {menu} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
|
|
131
|
-
</div>
|
|
132
|
-
</Sheet.Root>
|
|
133
|
-
|
|
134
|
-
<div
|
|
135
|
-
class="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
|
136
|
-
class:md:ml-64={!collapsed}
|
|
137
|
-
class:md:ml-16={collapsed}
|
|
138
|
-
>
|
|
139
|
-
<!-- Header with mobile hamburger -->
|
|
140
|
-
<Header
|
|
141
|
-
{siteUrl}
|
|
142
|
-
showSearch={true}
|
|
143
|
-
onSearchClick={() => { commandOpen = true; }}
|
|
144
|
-
>
|
|
145
|
-
{#snippet children()}
|
|
146
|
-
<!-- Mobile hamburger -->
|
|
147
|
-
<TooltipButton tooltip={t('common.menu')} class="md:hidden" onclick={() => { mobileMenuOpen = true; }}>
|
|
148
|
-
<Menu class="h-5 w-5" />
|
|
149
|
-
</TooltipButton>
|
|
150
|
-
{/snippet}
|
|
151
|
-
</Header>
|
|
152
|
-
|
|
153
|
-
<!-- Content area -->
|
|
154
|
-
<main class="flex-1 overflow-y-auto p-6 md:p-8">
|
|
155
|
-
{#key currentPath()}
|
|
156
|
-
<div in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
|
|
157
|
-
{@render children()}
|
|
158
|
-
</div>
|
|
159
|
-
{/key}
|
|
160
|
-
</main>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
<CommandPalette bind:open={commandOpen} />
|
|
164
|
-
<KeyboardShortcuts bind:open={shortcutsOpen} />
|
|
165
|
-
<ChatDialog />
|
|
166
|
-
<Toast />
|
|
167
|
-
{/if}
|