@svadmin/ui 0.0.6 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/src/actions.ts +110 -0
- package/src/components/AdminApp.svelte +8 -4
- package/src/components/Authenticated.svelte +38 -13
- package/src/components/AutoForm.svelte +163 -74
- package/src/components/AutoSaveIndicator.svelte +35 -0
- package/src/components/AutoTable.svelte +476 -183
- package/src/components/Breadcrumbs.svelte +22 -17
- package/src/components/CatchAllNavigate.svelte +21 -0
- package/src/components/ComboboxField.svelte +122 -0
- package/src/components/CommandPalette.svelte +93 -0
- package/src/components/ConfigErrorScreen.svelte +53 -151
- package/src/components/ConfirmDialog.svelte +25 -17
- package/src/components/CreatePage.svelte +43 -0
- package/src/components/DevTools.svelte +99 -230
- package/src/components/DrawerForm.svelte +19 -10
- package/src/components/EditPage.svelte +71 -0
- package/src/components/EmptyState.svelte +21 -31
- package/src/components/ErrorBoundary.svelte +38 -40
- package/src/components/ErrorComponent.svelte +36 -0
- package/src/components/FieldRenderer.svelte +240 -75
- package/src/components/ForgotPasswordPage.svelte +20 -111
- package/src/components/Header.svelte +46 -57
- package/src/components/InferencerPanel.svelte +136 -110
- package/src/components/InfiniteList.svelte +93 -0
- package/src/components/Layout.svelte +85 -22
- package/src/components/ListPage.svelte +71 -0
- package/src/components/LiveIndicator.svelte +58 -0
- package/src/components/LoginPage.svelte +33 -154
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/NavigateToResource.svelte +20 -0
- package/src/components/PageSkeleton.svelte +71 -0
- package/src/components/PasswordInput.svelte +95 -0
- package/src/components/RegisterPage.svelte +35 -164
- package/src/components/ShowPage.svelte +35 -36
- package/src/components/Sidebar.svelte +212 -81
- package/src/components/StatsCard.svelte +12 -3
- package/src/components/StepsForm.svelte +139 -0
- package/src/components/ThemedTitle.svelte +32 -0
- package/src/components/Toast.svelte +15 -44
- package/src/components/TooltipButton.svelte +52 -0
- package/src/components/UndoableNotification.svelte +28 -96
- package/src/components/UpdatePasswordPage.svelte +32 -160
- package/src/components/buttons/CloneButton.svelte +1 -1
- package/src/components/buttons/CreateButton.svelte +1 -1
- package/src/components/buttons/DeleteButton.svelte +3 -4
- package/src/components/buttons/EditButton.svelte +1 -1
- package/src/components/buttons/ImportButton.svelte +29 -7
- package/src/components/buttons/RefreshButton.svelte +11 -5
- package/src/components/fieldComponentMap.ts +79 -0
- package/src/components/fields/BooleanField.svelte +13 -0
- package/src/components/fields/DateField.svelte +6 -0
- package/src/components/fields/EmailField.svelte +9 -0
- package/src/components/fields/FileField.svelte +14 -0
- package/src/components/fields/ImageField.svelte +9 -0
- package/src/components/fields/JsonField.svelte +72 -0
- package/src/components/fields/MarkdownField.svelte +9 -0
- package/src/components/fields/MultiSelectField.svelte +20 -0
- package/src/components/fields/NumberField.svelte +6 -0
- package/src/components/fields/RelationField.svelte +24 -0
- package/src/components/fields/RichTextField.svelte +35 -0
- package/src/components/fields/SelectField.svelte +14 -0
- package/src/components/fields/TagField.svelte +12 -0
- package/src/components/fields/TextField.svelte +11 -0
- package/src/components/fields/UrlField.svelte +14 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
- package/src/components/ui/alert-dialog/index.ts +20 -0
- package/src/components/ui/avatar/avatar.svelte +2 -1
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
- package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/collapsible/index.ts +7 -0
- package/src/components/ui/command/command-dialog.svelte +31 -0
- package/src/components/ui/command/index.ts +2 -0
- package/src/components/ui/context-menu/index.ts +11 -0
- package/src/components/ui/drawer/drawer-content.svelte +21 -0
- package/src/components/ui/drawer/drawer-description.svelte +11 -0
- package/src/components/ui/drawer/drawer-footer.svelte +11 -0
- package/src/components/ui/drawer/drawer-header.svelte +11 -0
- package/src/components/ui/drawer/drawer-title.svelte +11 -0
- package/src/components/ui/drawer/drawer.svelte +18 -0
- package/src/components/ui/drawer/index.ts +6 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
- package/src/components/ui/dropdown-menu/index.ts +6 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/label/label.svelte +18 -0
- package/src/components/ui/pagination/index.ts +7 -0
- package/src/components/ui/pagination/pagination-content.svelte +11 -0
- package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
- package/src/components/ui/pagination/pagination-item.svelte +11 -0
- package/src/components/ui/pagination/pagination-link.svelte +26 -0
- package/src/components/ui/pagination/pagination-next.svelte +22 -0
- package/src/components/ui/pagination/pagination-previous.svelte +22 -0
- package/src/components/ui/pagination/pagination.svelte +11 -0
- package/src/components/ui/popover/index.ts +12 -0
- package/src/components/ui/popover/popover-content.svelte +25 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/progress/progress.svelte +22 -0
- package/src/components/ui/scroll-area/index.ts +1 -0
- package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
- package/src/components/ui/sheet/index.ts +15 -1
- package/src/components/ui/sheet/sheet-content.svelte +28 -0
- package/src/components/ui/sheet/sheet-description.svelte +22 -0
- package/src/components/ui/sheet/sheet-footer.svelte +22 -0
- package/src/components/ui/sheet/sheet-header.svelte +22 -0
- package/src/components/ui/sheet/sheet-title.svelte +22 -0
- package/src/index.ts +52 -1
- package/src/router-state.svelte.ts +16 -9
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getResource } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import type { FieldDefinition } from '@svadmin/core';
|
|
7
|
+
import PageHeader from './PageHeader.svelte';
|
|
8
|
+
import AutoTable from './AutoTable.svelte';
|
|
9
|
+
import { Button } from './ui/button/index.js';
|
|
10
|
+
import { Plus } from 'lucide-svelte';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
resourceName: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
canCreate?: boolean;
|
|
16
|
+
/** Passthrough: enable row selection checkboxes */
|
|
17
|
+
selectable?: boolean;
|
|
18
|
+
/** Passthrough: custom header actions */
|
|
19
|
+
headerActions?: Snippet;
|
|
20
|
+
/** Passthrough: custom cell renderer per field */
|
|
21
|
+
cellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
|
|
22
|
+
/** Passthrough: custom row actions */
|
|
23
|
+
rowActions?: Snippet<[{ record: Record<string, unknown>; id: string | number }]>;
|
|
24
|
+
/** Passthrough: custom empty state */
|
|
25
|
+
emptyState?: Snippet;
|
|
26
|
+
/** Passthrough: expandable row content */
|
|
27
|
+
expandedRowRender?: Snippet<[{ record: Record<string, unknown> }]>;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
resourceName,
|
|
33
|
+
title,
|
|
34
|
+
canCreate,
|
|
35
|
+
selectable,
|
|
36
|
+
headerActions,
|
|
37
|
+
cellRenderer,
|
|
38
|
+
rowActions,
|
|
39
|
+
emptyState,
|
|
40
|
+
expandedRowRender,
|
|
41
|
+
class: className = '',
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const resource = getResource(resourceName);
|
|
45
|
+
const pageTitle = title ?? resource.label;
|
|
46
|
+
const showCreate = canCreate ?? resource.canCreate !== false;
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="space-y-6 {className}">
|
|
50
|
+
<PageHeader title={pageTitle}>
|
|
51
|
+
{#snippet actions()}
|
|
52
|
+
{#if showCreate}
|
|
53
|
+
<Button onclick={() => navigate(`/${resourceName}/create`)}>
|
|
54
|
+
<Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
|
|
55
|
+
</Button>
|
|
56
|
+
{/if}
|
|
57
|
+
{#if headerActions}
|
|
58
|
+
{@render headerActions()}
|
|
59
|
+
{/if}
|
|
60
|
+
{/snippet}
|
|
61
|
+
</PageHeader>
|
|
62
|
+
|
|
63
|
+
<AutoTable
|
|
64
|
+
{resourceName}
|
|
65
|
+
{selectable}
|
|
66
|
+
{cellRenderer}
|
|
67
|
+
{rowActions}
|
|
68
|
+
{emptyState}
|
|
69
|
+
{expandedRowRender}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
|
+
import { Wifi, WifiOff, Loader2 } from 'lucide-svelte';
|
|
4
|
+
import { Badge } from './ui/badge/index.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
status = 'disconnected',
|
|
8
|
+
resource = '',
|
|
9
|
+
lastEvent = null,
|
|
10
|
+
showDetails = false,
|
|
11
|
+
} = $props<{
|
|
12
|
+
status: 'connecting' | 'connected' | 'disconnected';
|
|
13
|
+
resource?: string;
|
|
14
|
+
lastEvent?: { type: string; timestamp?: number } | null;
|
|
15
|
+
showDetails?: boolean;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const statusConfigs = {
|
|
19
|
+
connected: { variant: 'default' as const, dotClass: 'bg-emerald-500', label: 'Live', icon: Wifi },
|
|
20
|
+
connecting: { variant: 'secondary' as const, dotClass: 'bg-amber-500', label: 'Connecting...', icon: Loader2 as typeof Wifi },
|
|
21
|
+
disconnected: { variant: 'destructive' as const, dotClass: 'bg-red-500', label: 'Offline', icon: WifiOff },
|
|
22
|
+
} as const;
|
|
23
|
+
type StatusKey = 'connected' | 'connecting' | 'disconnected';
|
|
24
|
+
const cfg = $derived(statusConfigs[status as StatusKey]);
|
|
25
|
+
|
|
26
|
+
const timeSinceEvent = $derived(
|
|
27
|
+
lastEvent?.timestamp
|
|
28
|
+
? Math.floor((Date.now() - lastEvent.timestamp) / 1000)
|
|
29
|
+
: null
|
|
30
|
+
);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#key status}
|
|
34
|
+
<div transition:fade={{ duration: 200 }}>
|
|
35
|
+
<Badge variant={cfg.variant} class="gap-1.5 select-none" title={cfg.label}>
|
|
36
|
+
<span class="relative flex h-2 w-2">
|
|
37
|
+
<span class="{cfg.dotClass} rounded-full h-2 w-2"></span>
|
|
38
|
+
{#if status === 'connected'}
|
|
39
|
+
<span class="absolute inset-0 rounded-full {cfg.dotClass} opacity-40 animate-ping"></span>
|
|
40
|
+
{/if}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="text-[0.6875rem] font-semibold uppercase tracking-wide">{cfg.label}</span>
|
|
43
|
+
|
|
44
|
+
{#if showDetails && resource}
|
|
45
|
+
<span class="text-[0.625rem] font-mono opacity-70">{resource}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
{#if showDetails && lastEvent}
|
|
49
|
+
<span class="text-[0.625rem] opacity-70">
|
|
50
|
+
{lastEvent.type}
|
|
51
|
+
{#if timeSinceEvent !== null && timeSinceEvent < 60}
|
|
52
|
+
<span class="opacity-60">{timeSinceEvent}s ago</span>
|
|
53
|
+
{/if}
|
|
54
|
+
</span>
|
|
55
|
+
{/if}
|
|
56
|
+
</Badge>
|
|
57
|
+
</div>
|
|
58
|
+
{/key}
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
import { navigate } from '@svadmin/core/router';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
7
|
+
import { Label } from './ui/label/index.js';
|
|
7
8
|
import * as Card from './ui/card/index.js';
|
|
8
|
-
import
|
|
9
|
+
import * as Alert from './ui/alert/index.js';
|
|
10
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
import { LogIn, Mail, Loader2, AlertCircle } from 'lucide-svelte';
|
|
9
12
|
|
|
10
13
|
let { title = 'Admin', onSuccess } = $props<{
|
|
11
14
|
title?: string;
|
|
@@ -17,7 +20,6 @@
|
|
|
17
20
|
|
|
18
21
|
let email = $state('');
|
|
19
22
|
let password = $state('');
|
|
20
|
-
let showPassword = $state(false);
|
|
21
23
|
let error = $state('');
|
|
22
24
|
|
|
23
25
|
async function handleSubmit(e: SubmitEvent) {
|
|
@@ -36,11 +38,11 @@
|
|
|
36
38
|
}
|
|
37
39
|
</script>
|
|
38
40
|
|
|
39
|
-
<div class="
|
|
40
|
-
<div class="
|
|
41
|
-
<Card.Card class="
|
|
42
|
-
<Card.CardHeader class="
|
|
43
|
-
<div class="
|
|
41
|
+
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/[0.08] via-background to-primary/[0.04] p-4">
|
|
42
|
+
<div class="w-full max-w-[420px]">
|
|
43
|
+
<Card.Card class="backdrop-blur-xl border-border/50 shadow-[0_8px_32px_hsl(var(--primary)/0.08),0_2px_8px_hsl(0_0%_0%/0.06)]">
|
|
44
|
+
<Card.CardHeader class="text-center pb-2">
|
|
45
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mx-auto mb-3">
|
|
44
46
|
<LogIn class="h-6 w-6" />
|
|
45
47
|
</div>
|
|
46
48
|
<Card.CardTitle class="text-2xl font-bold">{t('auth.welcomeBack')}</Card.CardTitle>
|
|
@@ -49,15 +51,16 @@
|
|
|
49
51
|
<Card.CardContent>
|
|
50
52
|
<form onsubmit={handleSubmit} class="space-y-4">
|
|
51
53
|
{#if error}
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
54
|
+
<Alert.Root variant="destructive">
|
|
55
|
+
<AlertCircle class="h-4 w-4" />
|
|
56
|
+
<Alert.Description>{error}</Alert.Description>
|
|
57
|
+
</Alert.Root>
|
|
55
58
|
{/if}
|
|
56
59
|
|
|
57
60
|
<div class="space-y-2">
|
|
58
|
-
<
|
|
59
|
-
<div class="
|
|
60
|
-
<Mail class="
|
|
61
|
+
<Label for="login-email">{t('auth.email')}</Label>
|
|
62
|
+
<div class="relative">
|
|
63
|
+
<Mail class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
61
64
|
<Input
|
|
62
65
|
id="login-email"
|
|
63
66
|
type="email"
|
|
@@ -69,58 +72,37 @@
|
|
|
69
72
|
</div>
|
|
70
73
|
</div>
|
|
71
74
|
|
|
72
|
-
<div
|
|
73
|
-
<div class="flex items-center justify-between">
|
|
74
|
-
<
|
|
75
|
+
<div>
|
|
76
|
+
<div class="flex items-center justify-between mb-2">
|
|
77
|
+
<Label for="login-password">{t('auth.password')}</Label>
|
|
75
78
|
{#if authProvider.forgotPassword}
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
onclick={() => navigate('/forgot-password')}
|
|
80
|
-
>{t('auth.forgotPasswordLink')}</button>
|
|
79
|
+
<Button variant="link" class="text-xs h-auto p-0" onclick={() => navigate('/forgot-password')}>
|
|
80
|
+
{t('auth.forgotPasswordLink')}
|
|
81
|
+
</Button>
|
|
81
82
|
{/if}
|
|
82
83
|
</div>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
bind:value={password}
|
|
90
|
-
class="pl-9 pr-9"
|
|
91
|
-
autocomplete="current-password"
|
|
92
|
-
/>
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
95
|
-
class="password-toggle"
|
|
96
|
-
onclick={() => showPassword = !showPassword}
|
|
97
|
-
tabindex={-1}
|
|
98
|
-
>
|
|
99
|
-
{#if showPassword}
|
|
100
|
-
<EyeOff class="h-4 w-4" />
|
|
101
|
-
{:else}
|
|
102
|
-
<Eye class="h-4 w-4" />
|
|
103
|
-
{/if}
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
84
|
+
<PasswordInput
|
|
85
|
+
id="login-password"
|
|
86
|
+
label=""
|
|
87
|
+
bind:value={password}
|
|
88
|
+
autocomplete="current-password"
|
|
89
|
+
/>
|
|
106
90
|
</div>
|
|
107
91
|
|
|
108
92
|
<Button type="submit" class="w-full" disabled={login.isLoading}>
|
|
109
93
|
{#if login.isLoading}
|
|
110
|
-
<
|
|
94
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
111
95
|
{/if}
|
|
112
96
|
{t('auth.loginButton')}
|
|
113
97
|
</Button>
|
|
114
98
|
</form>
|
|
115
99
|
|
|
116
100
|
{#if authProvider.register}
|
|
117
|
-
<div class="
|
|
101
|
+
<div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
|
|
118
102
|
<span class="text-sm text-muted-foreground">{t('auth.noAccount')}</span>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
onclick={() => navigate('/register')}
|
|
123
|
-
>{t('auth.register')}</button>
|
|
103
|
+
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/register')}>
|
|
104
|
+
{t('auth.register')}
|
|
105
|
+
</Button>
|
|
124
106
|
</div>
|
|
125
107
|
{/if}
|
|
126
108
|
</Card.CardContent>
|
|
@@ -131,106 +113,3 @@
|
|
|
131
113
|
</p>
|
|
132
114
|
</div>
|
|
133
115
|
</div>
|
|
134
|
-
|
|
135
|
-
<style>
|
|
136
|
-
.login-page {
|
|
137
|
-
min-height: 100vh;
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
justify-content: center;
|
|
141
|
-
background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
|
|
142
|
-
padding: 1rem;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.login-container {
|
|
146
|
-
width: 100%;
|
|
147
|
-
max-width: 420px;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
:global(.login-card) {
|
|
151
|
-
backdrop-filter: blur(20px);
|
|
152
|
-
border: 1px solid hsl(var(--border) / 0.5);
|
|
153
|
-
box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
:global(.login-header) {
|
|
157
|
-
text-align: center;
|
|
158
|
-
padding-bottom: 0.5rem;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.login-icon {
|
|
162
|
-
display: inline-flex;
|
|
163
|
-
align-items: center;
|
|
164
|
-
justify-content: center;
|
|
165
|
-
width: 48px;
|
|
166
|
-
height: 48px;
|
|
167
|
-
border-radius: 12px;
|
|
168
|
-
background: hsl(var(--primary) / 0.1);
|
|
169
|
-
color: hsl(var(--primary));
|
|
170
|
-
margin: 0 auto 0.75rem;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.input-with-icon {
|
|
174
|
-
position: relative;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
:global(.input-icon) {
|
|
178
|
-
position: absolute;
|
|
179
|
-
left: 0.75rem;
|
|
180
|
-
top: 50%;
|
|
181
|
-
transform: translateY(-50%);
|
|
182
|
-
color: hsl(var(--muted-foreground));
|
|
183
|
-
pointer-events: none;
|
|
184
|
-
z-index: 1;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.password-toggle {
|
|
188
|
-
position: absolute;
|
|
189
|
-
right: 0.75rem;
|
|
190
|
-
top: 50%;
|
|
191
|
-
transform: translateY(-50%);
|
|
192
|
-
color: hsl(var(--muted-foreground));
|
|
193
|
-
background: none;
|
|
194
|
-
border: none;
|
|
195
|
-
cursor: pointer;
|
|
196
|
-
padding: 2px;
|
|
197
|
-
z-index: 1;
|
|
198
|
-
}
|
|
199
|
-
.password-toggle:hover {
|
|
200
|
-
color: hsl(var(--foreground));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.error-alert {
|
|
204
|
-
padding: 0.75rem;
|
|
205
|
-
border-radius: 0.5rem;
|
|
206
|
-
background: hsl(var(--destructive) / 0.1);
|
|
207
|
-
border: 1px solid hsl(var(--destructive) / 0.3);
|
|
208
|
-
color: hsl(var(--destructive));
|
|
209
|
-
font-size: 0.875rem;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.loading-spinner {
|
|
213
|
-
display: inline-block;
|
|
214
|
-
width: 16px;
|
|
215
|
-
height: 16px;
|
|
216
|
-
border: 2px solid transparent;
|
|
217
|
-
border-top-color: currentColor;
|
|
218
|
-
border-radius: 50%;
|
|
219
|
-
animation: spin 0.6s linear infinite;
|
|
220
|
-
margin-right: 0.5rem;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
@keyframes spin {
|
|
224
|
-
to { transform: rotate(360deg); }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.auth-link {
|
|
228
|
-
display: flex;
|
|
229
|
-
align-items: center;
|
|
230
|
-
justify-content: center;
|
|
231
|
-
gap: 0.25rem;
|
|
232
|
-
margin-top: 1.25rem;
|
|
233
|
-
padding-top: 1.25rem;
|
|
234
|
-
border-top: 1px solid hsl(var(--border));
|
|
235
|
-
}
|
|
236
|
-
</style>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { FieldDefinition } from '@svadmin/core';
|
|
4
4
|
import { getResource, useForm } from '@svadmin/core';
|
|
5
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
6
|
import * as Dialog from './ui/dialog/index.js';
|
|
6
7
|
import AutoForm from './AutoForm.svelte';
|
|
7
8
|
|
|
@@ -14,18 +15,21 @@
|
|
|
14
15
|
}>();
|
|
15
16
|
|
|
16
17
|
const resource = $derived(getResource(resourceName));
|
|
18
|
+
const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
|
|
19
|
+
const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
|
|
17
20
|
|
|
18
21
|
function handleClose() {
|
|
19
22
|
open = false;
|
|
23
|
+
onSuccess?.();
|
|
20
24
|
}
|
|
21
25
|
</script>
|
|
22
26
|
|
|
23
27
|
{#if open}
|
|
24
28
|
<Dialog.Dialog bind:open>
|
|
25
|
-
<Dialog.DialogContent class="
|
|
29
|
+
<Dialog.DialogContent class="{dialogWidth} max-h-[85vh] overflow-y-auto">
|
|
26
30
|
<Dialog.DialogHeader>
|
|
27
31
|
<Dialog.DialogTitle>
|
|
28
|
-
{mode === 'create' ?
|
|
32
|
+
{mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
|
|
29
33
|
</Dialog.DialogTitle>
|
|
30
34
|
</Dialog.DialogHeader>
|
|
31
35
|
<AutoForm {resourceName} {mode} {id} />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useMenu } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
|
|
5
|
+
const { menuItems } = useMenu();
|
|
6
|
+
|
|
7
|
+
$effect(() => {
|
|
8
|
+
// Automatically navigate to the first defined resource
|
|
9
|
+
if (menuItems.length > 0) {
|
|
10
|
+
navigate(menuItems[0].route);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="flex h-screen w-screen items-center justify-center p-4 text-muted-foreground">
|
|
16
|
+
<div class="text-center">
|
|
17
|
+
<div class="mb-4 inline-block h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"></div>
|
|
18
|
+
<p>Navigating to default resource...</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
|
+
import { Skeleton } from './ui/skeleton';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
type = 'list',
|
|
7
|
+
rows = 5,
|
|
8
|
+
} = $props<{
|
|
9
|
+
type?: 'list' | 'show' | 'form';
|
|
10
|
+
rows?: number;
|
|
11
|
+
}>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="w-full space-y-6" out:fade={{ duration: 200 }}>
|
|
15
|
+
<!-- Header Skeleton -->
|
|
16
|
+
<div class="flex items-center justify-between">
|
|
17
|
+
<div class="space-y-2">
|
|
18
|
+
<Skeleton class="h-8 w-[200px]" />
|
|
19
|
+
<Skeleton class="h-4 w-[300px]" />
|
|
20
|
+
</div>
|
|
21
|
+
{#if type === 'list'}
|
|
22
|
+
<Skeleton class="h-10 w-[120px]" />
|
|
23
|
+
{/if}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="rounded-md border p-4 space-y-4">
|
|
27
|
+
{#if type === 'list'}
|
|
28
|
+
<!-- Table Header -->
|
|
29
|
+
<div class="flex gap-4 border-b pb-4">
|
|
30
|
+
{#each Array(4) as _}
|
|
31
|
+
<Skeleton class="h-6 flex-1" />
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
<!-- Table Rows -->
|
|
35
|
+
<div class="space-y-4">
|
|
36
|
+
{#each Array(rows) as _}
|
|
37
|
+
<div class="flex gap-4">
|
|
38
|
+
<Skeleton class="h-6 flex-1" />
|
|
39
|
+
<Skeleton class="h-6 flex-1" />
|
|
40
|
+
<Skeleton class="h-6 flex-1" />
|
|
41
|
+
<Skeleton class="h-6 flex-1" />
|
|
42
|
+
</div>
|
|
43
|
+
{/each}
|
|
44
|
+
</div>
|
|
45
|
+
{:else if type === 'show'}
|
|
46
|
+
<!-- Key-Value pairs -->
|
|
47
|
+
<div class="space-y-6">
|
|
48
|
+
{#each Array(rows) as _}
|
|
49
|
+
<div class="space-y-2">
|
|
50
|
+
<Skeleton class="h-4 w-[100px]" />
|
|
51
|
+
<Skeleton class="h-6 w-[250px]" />
|
|
52
|
+
</div>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
55
|
+
{:else if type === 'form'}
|
|
56
|
+
<!-- Form Inputs -->
|
|
57
|
+
<div class="space-y-6">
|
|
58
|
+
{#each Array(rows) as _}
|
|
59
|
+
<div class="space-y-2">
|
|
60
|
+
<Skeleton class="h-4 w-[100px]" />
|
|
61
|
+
<Skeleton class="h-10 w-full max-w-md" />
|
|
62
|
+
</div>
|
|
63
|
+
{/each}
|
|
64
|
+
<div class="pt-4 flex gap-2">
|
|
65
|
+
<Skeleton class="h-10 w-[100px]" />
|
|
66
|
+
<Skeleton class="h-10 w-[100px]" />
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Input } from './ui/input/index.js';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
5
|
+
import { Label } from './ui/label/index.js';
|
|
6
|
+
import { Progress } from './ui/progress/index.js';
|
|
7
|
+
import { Lock, Eye, EyeOff } from 'lucide-svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
autocomplete?: string;
|
|
15
|
+
showStrength?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
id,
|
|
21
|
+
label,
|
|
22
|
+
value = $bindable(''),
|
|
23
|
+
placeholder = '••••••••',
|
|
24
|
+
autocomplete = 'current-password',
|
|
25
|
+
showStrength = false,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let showPassword = $state(false);
|
|
30
|
+
|
|
31
|
+
// Password strength calculation
|
|
32
|
+
const strength = $derived.by(() => {
|
|
33
|
+
if (!showStrength || !value) return 0;
|
|
34
|
+
let score = 0;
|
|
35
|
+
if (value.length >= 8) score += 25;
|
|
36
|
+
if (value.length >= 12) score += 10;
|
|
37
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 25;
|
|
38
|
+
if (/\d/.test(value)) score += 20;
|
|
39
|
+
if (/[^a-zA-Z0-9]/.test(value)) score += 20;
|
|
40
|
+
return Math.min(score, 100);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const strengthColor = $derived(
|
|
44
|
+
strength < 30 ? 'bg-destructive' :
|
|
45
|
+
strength < 60 ? 'bg-amber-500' :
|
|
46
|
+
strength < 80 ? 'bg-blue-500' : 'bg-emerald-500'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const strengthLabel = $derived(
|
|
50
|
+
strength < 30 ? 'Weak' :
|
|
51
|
+
strength < 60 ? 'Fair' :
|
|
52
|
+
strength < 80 ? 'Good' : 'Strong'
|
|
53
|
+
);
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="space-y-2 {className}">
|
|
57
|
+
<Label for={id}>{label}</Label>
|
|
58
|
+
<div class="relative">
|
|
59
|
+
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
60
|
+
<Input
|
|
61
|
+
{id}
|
|
62
|
+
type={showPassword ? 'text' : 'password'}
|
|
63
|
+
{placeholder}
|
|
64
|
+
bind:value
|
|
65
|
+
class="pl-9 pr-9"
|
|
66
|
+
{autocomplete}
|
|
67
|
+
/>
|
|
68
|
+
<TooltipButton
|
|
69
|
+
tooltip={showPassword ? 'Hide password' : 'Show password'}
|
|
70
|
+
variant="ghost"
|
|
71
|
+
size="icon"
|
|
72
|
+
type="button"
|
|
73
|
+
class="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 z-[1]"
|
|
74
|
+
onclick={() => showPassword = !showPassword}
|
|
75
|
+
tabindex={-1}
|
|
76
|
+
>
|
|
77
|
+
{#if showPassword}
|
|
78
|
+
<EyeOff class="h-4 w-4" />
|
|
79
|
+
{:else}
|
|
80
|
+
<Eye class="h-4 w-4" />
|
|
81
|
+
{/if}
|
|
82
|
+
</TooltipButton>
|
|
83
|
+
</div>
|
|
84
|
+
{#if showStrength && value}
|
|
85
|
+
<div class="flex items-center gap-2">
|
|
86
|
+
<div class="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
|
87
|
+
<div
|
|
88
|
+
class="h-full rounded-full transition-all duration-300 {strengthColor}"
|
|
89
|
+
style="width: {strength}%"
|
|
90
|
+
></div>
|
|
91
|
+
</div>
|
|
92
|
+
<span class="text-xs text-muted-foreground w-12 text-right">{strengthLabel}</span>
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|