@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,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Tooltip from './ui/tooltip/index.js';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
tooltip: string;
|
|
9
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
10
|
+
size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm';
|
|
11
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
12
|
+
class?: string;
|
|
13
|
+
onclick?: (e: MouseEvent) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
type?: 'button' | 'submit' | 'reset';
|
|
16
|
+
tabindex?: number;
|
|
17
|
+
children: Snippet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
tooltip,
|
|
22
|
+
variant = 'ghost',
|
|
23
|
+
size = 'icon',
|
|
24
|
+
side = 'top',
|
|
25
|
+
class: className = '',
|
|
26
|
+
onclick,
|
|
27
|
+
disabled = false,
|
|
28
|
+
type = 'button',
|
|
29
|
+
tabindex,
|
|
30
|
+
children,
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<Tooltip.Root>
|
|
35
|
+
<Tooltip.Trigger>
|
|
36
|
+
{#snippet child({ props })}
|
|
37
|
+
<Button
|
|
38
|
+
{...props}
|
|
39
|
+
{variant}
|
|
40
|
+
{size}
|
|
41
|
+
{type}
|
|
42
|
+
class={className}
|
|
43
|
+
{onclick}
|
|
44
|
+
{disabled}
|
|
45
|
+
{tabindex}
|
|
46
|
+
>
|
|
47
|
+
{@render children()}
|
|
48
|
+
</Button>
|
|
49
|
+
{/snippet}
|
|
50
|
+
</Tooltip.Trigger>
|
|
51
|
+
<Tooltip.Content {side}>{tooltip}</Tooltip.Content>
|
|
52
|
+
</Tooltip.Root>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Button } from './ui/button/index.js';
|
|
3
|
+
import { Progress } from './ui/progress/index.js';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import { slide } from 'svelte/transition';
|
|
3
6
|
import { X, Undo2 } from 'lucide-svelte';
|
|
4
7
|
|
|
5
8
|
let { message, duration = 5000, onUndo, onTimeout } = $props<{
|
|
@@ -12,121 +15,50 @@
|
|
|
12
15
|
let remaining = $state(duration);
|
|
13
16
|
let dismissed = $state(false);
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dismissed
|
|
21
|
-
|
|
18
|
+
$effect(() => {
|
|
19
|
+
const interval = setInterval(() => {
|
|
20
|
+
remaining -= 100;
|
|
21
|
+
if (remaining <= 0) {
|
|
22
|
+
clearInterval(interval);
|
|
23
|
+
if (!dismissed) {
|
|
24
|
+
dismissed = true;
|
|
25
|
+
onTimeout();
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
28
|
+
}, 100);
|
|
29
|
+
return () => clearInterval(interval);
|
|
30
|
+
});
|
|
25
31
|
|
|
26
32
|
function handleUndo() {
|
|
27
|
-
clearInterval(interval);
|
|
28
33
|
dismissed = true;
|
|
29
34
|
onUndo();
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
function handleDismiss() {
|
|
33
|
-
clearInterval(interval);
|
|
34
38
|
dismissed = true;
|
|
35
39
|
onTimeout();
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
const
|
|
42
|
+
const progressValue = $derived(remaining / duration * 100);
|
|
39
43
|
</script>
|
|
40
44
|
|
|
41
45
|
{#if !dismissed}
|
|
42
|
-
<div
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
<div
|
|
47
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-[100] min-w-80 max-w-lg rounded-xl border bg-card shadow-2xl overflow-hidden"
|
|
48
|
+
transition:slide={{ duration: 250 }}
|
|
49
|
+
>
|
|
50
|
+
<div class="flex items-center justify-between px-4 py-3 gap-3">
|
|
51
|
+
<p class="text-sm text-foreground">{message}</p>
|
|
52
|
+
<div class="flex items-center gap-1 flex-shrink-0">
|
|
53
|
+
<Button variant="ghost" size="sm" onclick={handleUndo} class="font-semibold text-primary">
|
|
47
54
|
<Undo2 class="h-3.5 w-3.5 mr-1" />
|
|
48
|
-
|
|
55
|
+
{t('common.undo')}
|
|
49
56
|
</Button>
|
|
50
|
-
<
|
|
57
|
+
<Button variant="ghost" size="icon" class="h-7 w-7" onclick={handleDismiss}>
|
|
51
58
|
<X class="h-3.5 w-3.5" />
|
|
52
|
-
</
|
|
59
|
+
</Button>
|
|
53
60
|
</div>
|
|
54
61
|
</div>
|
|
55
|
-
<
|
|
56
|
-
<div class="undoable-progress-bar" style="width: {progress}%"></div>
|
|
57
|
-
</div>
|
|
62
|
+
<Progress value={progressValue} class="h-1 rounded-none" />
|
|
58
63
|
</div>
|
|
59
64
|
{/if}
|
|
60
|
-
|
|
61
|
-
<style>
|
|
62
|
-
.undoable-notification {
|
|
63
|
-
position: fixed;
|
|
64
|
-
bottom: 1.5rem;
|
|
65
|
-
left: 50%;
|
|
66
|
-
transform: translateX(-50%);
|
|
67
|
-
z-index: 100;
|
|
68
|
-
min-width: 320px;
|
|
69
|
-
max-width: 480px;
|
|
70
|
-
background: hsl(var(--card));
|
|
71
|
-
border: 1px solid hsl(var(--border));
|
|
72
|
-
border-radius: 0.75rem;
|
|
73
|
-
box-shadow: 0 8px 32px hsl(0 0% 0% / 0.15);
|
|
74
|
-
overflow: hidden;
|
|
75
|
-
animation: slideUp 0.3s ease;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.undoable-content {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
justify-content: space-between;
|
|
82
|
-
padding: 0.75rem 1rem;
|
|
83
|
-
gap: 0.75rem;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.undoable-message {
|
|
87
|
-
font-size: 0.875rem;
|
|
88
|
-
color: hsl(var(--foreground));
|
|
89
|
-
margin: 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.undoable-actions {
|
|
93
|
-
display: flex;
|
|
94
|
-
align-items: center;
|
|
95
|
-
gap: 0.25rem;
|
|
96
|
-
flex-shrink: 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
:global(.undoable-undo-btn) {
|
|
100
|
-
font-weight: 600;
|
|
101
|
-
color: hsl(var(--primary)) !important;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.undoable-close {
|
|
105
|
-
background: none;
|
|
106
|
-
border: none;
|
|
107
|
-
cursor: pointer;
|
|
108
|
-
padding: 0.25rem;
|
|
109
|
-
color: hsl(var(--muted-foreground));
|
|
110
|
-
border-radius: 0.25rem;
|
|
111
|
-
}
|
|
112
|
-
.undoable-close:hover {
|
|
113
|
-
color: hsl(var(--foreground));
|
|
114
|
-
background: hsl(var(--muted));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.undoable-progress {
|
|
118
|
-
height: 3px;
|
|
119
|
-
background: hsl(var(--muted));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.undoable-progress-bar {
|
|
123
|
-
height: 100%;
|
|
124
|
-
background: hsl(var(--primary));
|
|
125
|
-
transition: width 100ms linear;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@keyframes slideUp {
|
|
129
|
-
from { transform: translateX(-50%) translateY(100%); opacity: 0; }
|
|
130
|
-
to { transform: translateX(-50%) translateY(0); opacity: 1; }
|
|
131
|
-
}
|
|
132
|
-
</style>
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { t } from '@svadmin/core/i18n';
|
|
4
4
|
import { navigate } from '@svadmin/core/router';
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
|
-
import { Input } from './ui/input/index.js';
|
|
7
6
|
import * as Card from './ui/card/index.js';
|
|
8
|
-
import
|
|
7
|
+
import * as Alert from './ui/alert/index.js';
|
|
8
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
9
|
+
import { ShieldCheck, Loader2, AlertCircle } from 'lucide-svelte';
|
|
9
10
|
|
|
10
11
|
let { title = 'Admin' } = $props<{
|
|
11
12
|
title?: string;
|
|
@@ -15,7 +16,6 @@
|
|
|
15
16
|
|
|
16
17
|
let password = $state('');
|
|
17
18
|
let confirmPassword = $state('');
|
|
18
|
-
let showPassword = $state(false);
|
|
19
19
|
let error = $state('');
|
|
20
20
|
|
|
21
21
|
async function handleSubmit(e: SubmitEvent) {
|
|
@@ -32,79 +32,51 @@
|
|
|
32
32
|
}
|
|
33
33
|
</script>
|
|
34
34
|
|
|
35
|
-
<div class="
|
|
36
|
-
<div class="
|
|
37
|
-
<Card.Card class="
|
|
38
|
-
<Card.CardHeader class="
|
|
39
|
-
<div class="
|
|
35
|
+
<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">
|
|
36
|
+
<div class="w-full max-w-[420px]">
|
|
37
|
+
<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)]">
|
|
38
|
+
<Card.CardHeader class="text-center pb-2">
|
|
39
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mx-auto mb-3">
|
|
40
40
|
<ShieldCheck class="h-6 w-6" />
|
|
41
41
|
</div>
|
|
42
42
|
<Card.CardTitle class="text-2xl font-bold">{t('auth.resetPassword')}</Card.CardTitle>
|
|
43
|
-
<p class="text-sm text-muted-foreground">
|
|
43
|
+
<p class="text-sm text-muted-foreground">{t('auth.resetPasswordDescription')}</p>
|
|
44
44
|
</Card.CardHeader>
|
|
45
45
|
<Card.CardContent>
|
|
46
46
|
<form onsubmit={handleSubmit} class="space-y-4">
|
|
47
47
|
{#if error}
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
48
|
+
<Alert.Root variant="destructive">
|
|
49
|
+
<AlertCircle class="h-4 w-4" />
|
|
50
|
+
<Alert.Description>{error}</Alert.Description>
|
|
51
|
+
</Alert.Root>
|
|
51
52
|
{/if}
|
|
52
53
|
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class="password-toggle"
|
|
68
|
-
onclick={() => showPassword = !showPassword}
|
|
69
|
-
tabindex={-1}
|
|
70
|
-
>
|
|
71
|
-
{#if showPassword}
|
|
72
|
-
<EyeOff class="h-4 w-4" />
|
|
73
|
-
{:else}
|
|
74
|
-
<Eye class="h-4 w-4" />
|
|
75
|
-
{/if}
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div class="space-y-2">
|
|
81
|
-
<label for="confirm-password" class="text-sm font-medium text-foreground">{t('auth.confirmPassword')}</label>
|
|
82
|
-
<div class="input-with-icon">
|
|
83
|
-
<Lock class="input-icon h-4 w-4" />
|
|
84
|
-
<Input
|
|
85
|
-
id="confirm-password"
|
|
86
|
-
type={showPassword ? 'text' : 'password'}
|
|
87
|
-
placeholder="••••••••"
|
|
88
|
-
bind:value={confirmPassword}
|
|
89
|
-
class="pl-9"
|
|
90
|
-
autocomplete="new-password"
|
|
91
|
-
/>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
54
|
+
<PasswordInput
|
|
55
|
+
id="new-password"
|
|
56
|
+
label={t('auth.password')}
|
|
57
|
+
bind:value={password}
|
|
58
|
+
autocomplete="new-password"
|
|
59
|
+
showStrength
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<PasswordInput
|
|
63
|
+
id="confirm-password"
|
|
64
|
+
label={t('auth.confirmPassword')}
|
|
65
|
+
bind:value={confirmPassword}
|
|
66
|
+
autocomplete="new-password"
|
|
67
|
+
/>
|
|
94
68
|
|
|
95
69
|
<Button type="submit" class="w-full" disabled={updatePw.isLoading}>
|
|
96
70
|
{#if updatePw.isLoading}
|
|
97
|
-
<
|
|
71
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
98
72
|
{/if}
|
|
99
73
|
{t('auth.resetPassword')}
|
|
100
74
|
</Button>
|
|
101
75
|
|
|
102
|
-
<div class="
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onclick={() => navigate('/login')}
|
|
107
|
-
>{t('auth.backToLogin')}</button>
|
|
76
|
+
<div class="flex items-center justify-center mt-2">
|
|
77
|
+
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
|
|
78
|
+
{t('auth.backToLogin')}
|
|
79
|
+
</Button>
|
|
108
80
|
</div>
|
|
109
81
|
</form>
|
|
110
82
|
</Card.CardContent>
|
|
@@ -115,103 +87,3 @@
|
|
|
115
87
|
</p>
|
|
116
88
|
</div>
|
|
117
89
|
</div>
|
|
118
|
-
|
|
119
|
-
<style>
|
|
120
|
-
.auth-page {
|
|
121
|
-
min-height: 100vh;
|
|
122
|
-
display: flex;
|
|
123
|
-
align-items: center;
|
|
124
|
-
justify-content: center;
|
|
125
|
-
background: linear-gradient(135deg, hsl(var(--primary) / 0.08) 0%, hsl(var(--background)) 50%, hsl(var(--primary) / 0.04) 100%);
|
|
126
|
-
padding: 1rem;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.auth-container {
|
|
130
|
-
width: 100%;
|
|
131
|
-
max-width: 420px;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
:global(.auth-card) {
|
|
135
|
-
backdrop-filter: blur(20px);
|
|
136
|
-
border: 1px solid hsl(var(--border) / 0.5);
|
|
137
|
-
box-shadow: 0 8px 32px hsl(var(--primary) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
:global(.auth-header) {
|
|
141
|
-
text-align: center;
|
|
142
|
-
padding-bottom: 0.5rem;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.auth-icon {
|
|
146
|
-
display: inline-flex;
|
|
147
|
-
align-items: center;
|
|
148
|
-
justify-content: center;
|
|
149
|
-
width: 48px;
|
|
150
|
-
height: 48px;
|
|
151
|
-
border-radius: 12px;
|
|
152
|
-
background: hsl(var(--primary) / 0.1);
|
|
153
|
-
color: hsl(var(--primary));
|
|
154
|
-
margin: 0 auto 0.75rem;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.input-with-icon {
|
|
158
|
-
position: relative;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
:global(.input-icon) {
|
|
162
|
-
position: absolute;
|
|
163
|
-
left: 0.75rem;
|
|
164
|
-
top: 50%;
|
|
165
|
-
transform: translateY(-50%);
|
|
166
|
-
color: hsl(var(--muted-foreground));
|
|
167
|
-
pointer-events: none;
|
|
168
|
-
z-index: 1;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.password-toggle {
|
|
172
|
-
position: absolute;
|
|
173
|
-
right: 0.75rem;
|
|
174
|
-
top: 50%;
|
|
175
|
-
transform: translateY(-50%);
|
|
176
|
-
color: hsl(var(--muted-foreground));
|
|
177
|
-
background: none;
|
|
178
|
-
border: none;
|
|
179
|
-
cursor: pointer;
|
|
180
|
-
padding: 2px;
|
|
181
|
-
z-index: 1;
|
|
182
|
-
}
|
|
183
|
-
.password-toggle:hover {
|
|
184
|
-
color: hsl(var(--foreground));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.error-alert {
|
|
188
|
-
padding: 0.75rem;
|
|
189
|
-
border-radius: 0.5rem;
|
|
190
|
-
background: hsl(var(--destructive) / 0.1);
|
|
191
|
-
border: 1px solid hsl(var(--destructive) / 0.3);
|
|
192
|
-
color: hsl(var(--destructive));
|
|
193
|
-
font-size: 0.875rem;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.loading-spinner {
|
|
197
|
-
display: inline-block;
|
|
198
|
-
width: 16px;
|
|
199
|
-
height: 16px;
|
|
200
|
-
border: 2px solid transparent;
|
|
201
|
-
border-top-color: currentColor;
|
|
202
|
-
border-radius: 50%;
|
|
203
|
-
animation: spin 0.6s linear infinite;
|
|
204
|
-
margin-right: 0.5rem;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
@keyframes spin {
|
|
208
|
-
to { transform: rotate(360deg); }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.auth-link {
|
|
212
|
-
display: flex;
|
|
213
|
-
align-items: center;
|
|
214
|
-
justify-content: center;
|
|
215
|
-
margin-top: 0.5rem;
|
|
216
|
-
}
|
|
217
|
-
</style>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
14
|
const nav = useNavigation();
|
|
15
|
-
const can = accessControl?.enabled ? useCan(
|
|
15
|
+
const can = accessControl?.enabled ? useCan(resource, 'create') : null;
|
|
16
16
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
17
17
|
</script>
|
|
18
18
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}>();
|
|
12
12
|
|
|
13
13
|
const nav = useNavigation();
|
|
14
|
-
const can = accessControl?.enabled ? useCan(
|
|
14
|
+
const can = accessControl?.enabled ? useCan(resource, 'create') : null;
|
|
15
15
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
16
16
|
</script>
|
|
17
17
|
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
class?: string;
|
|
17
17
|
}>();
|
|
18
18
|
|
|
19
|
-
const deleteMut = useDelete(resource,
|
|
20
|
-
const can = accessControl?.enabled ? useCan(
|
|
19
|
+
const deleteMut = useDelete({ resource, mutationMode: undoable ? 'undoable' : 'pessimistic' });
|
|
20
|
+
const can = accessControl?.enabled ? useCan(resource, 'delete') : null;
|
|
21
21
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
22
22
|
let confirming = $state(false);
|
|
23
23
|
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
confirming = false;
|
|
30
|
-
|
|
31
|
-
await $deleteMut.mutateAsync(recordItemId);
|
|
30
|
+
await deleteMut.mutation.mutateAsync({ id: recordItemId, resource });
|
|
32
31
|
onSuccess?.();
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
14
|
const nav = useNavigation();
|
|
15
|
-
const can = accessControl?.enabled ? useCan(
|
|
15
|
+
const can = accessControl?.enabled ? useCan(resource, 'edit') : null;
|
|
16
16
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
17
17
|
</script>
|
|
18
18
|
|
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { useImport, t } from '@svadmin/core';
|
|
2
|
+
import { useImport, t, type UseImportOptions } from '@svadmin/core';
|
|
3
3
|
import { Button } from '../ui/button/index.js';
|
|
4
4
|
import { Upload } from 'lucide-svelte';
|
|
5
5
|
|
|
6
|
-
let { resource, hideText = false,
|
|
6
|
+
let { resource, hideText = false, onFinish, class: className = '' } = $props<{
|
|
7
7
|
resource: string;
|
|
8
8
|
hideText?: boolean;
|
|
9
|
-
|
|
9
|
+
onFinish?: (result: { succeeded: unknown[]; errored: { request: unknown; error: unknown }[] }) => void;
|
|
10
10
|
class?: string;
|
|
11
11
|
}>();
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
let fileInput: HTMLInputElement | undefined = $state();
|
|
14
|
+
|
|
15
|
+
const importHook = useImport({
|
|
16
|
+
get resource() { return resource; },
|
|
17
|
+
onFinish: (result) => onFinish?.(result),
|
|
16
18
|
});
|
|
19
|
+
|
|
20
|
+
function triggerImport() {
|
|
21
|
+
fileInput?.click();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleFileChange(e: Event) {
|
|
25
|
+
const input = e.target as HTMLInputElement;
|
|
26
|
+
const file = input.files?.[0];
|
|
27
|
+
if (file) {
|
|
28
|
+
importHook.handleChange({ file });
|
|
29
|
+
input.value = ''; // reset for re-import
|
|
30
|
+
}
|
|
31
|
+
}
|
|
17
32
|
</script>
|
|
18
33
|
|
|
34
|
+
<input
|
|
35
|
+
bind:this={fileInput}
|
|
36
|
+
type="file"
|
|
37
|
+
accept=".csv"
|
|
38
|
+
style="display:none"
|
|
39
|
+
onchange={handleFileChange}
|
|
40
|
+
/>
|
|
19
41
|
<Button
|
|
20
42
|
variant="outline"
|
|
21
43
|
size={hideText ? 'icon' : 'sm'}
|
|
22
44
|
class={className}
|
|
23
|
-
disabled={isLoading}
|
|
45
|
+
disabled={importHook.isLoading}
|
|
24
46
|
onclick={triggerImport}
|
|
25
47
|
>
|
|
26
48
|
<Upload class="h-4 w-4" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useInvalidate, t } from '@svadmin/core';
|
|
3
3
|
import { Button } from '../ui/button/index.js';
|
|
4
4
|
import { RefreshCw } from 'lucide-svelte';
|
|
5
5
|
|
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
class?: string;
|
|
10
10
|
}>();
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const invalidate = useInvalidate();
|
|
13
13
|
let spinning = $state(false);
|
|
14
|
+
let spinTimer: ReturnType<typeof setTimeout> | undefined;
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
return () => { if (spinTimer) clearTimeout(spinTimer); };
|
|
18
|
+
});
|
|
14
19
|
|
|
15
20
|
function refresh() {
|
|
16
21
|
spinning = true;
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
invalidate({ resource, invalidates: ['list', 'many'] });
|
|
23
|
+
if (spinTimer) clearTimeout(spinTimer);
|
|
24
|
+
spinTimer = setTimeout(() => { spinning = false; }, 600);
|
|
19
25
|
}
|
|
20
26
|
</script>
|
|
21
27
|
|
|
@@ -26,5 +32,5 @@
|
|
|
26
32
|
onclick={refresh}
|
|
27
33
|
>
|
|
28
34
|
<RefreshCw class="h-4 w-4 {spinning ? 'animate-spin' : ''}" />
|
|
29
|
-
{#if !hideText}<span class="ml-1">
|
|
35
|
+
{#if !hideText}<span class="ml-1">{t('common.retry')}</span>{/if}
|
|
30
36
|
</Button>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Shared field type → display component mapping
|
|
2
|
+
// Used by both ShowPage and FieldRenderer to avoid duplicate if/else chains.
|
|
3
|
+
// Components are lazily resolved by field type at runtime.
|
|
4
|
+
|
|
5
|
+
import type { Component } from 'svelte';
|
|
6
|
+
|
|
7
|
+
export interface FieldComponentMap {
|
|
8
|
+
[fieldType: string]: Component<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Default display component map (read-only field display)
|
|
12
|
+
// Dynamically imports components only when first used
|
|
13
|
+
const displayCache = new Map<string, Component<Record<string, unknown>>>();
|
|
14
|
+
|
|
15
|
+
// Pre-import all field display components (tree-shaken by bundler)
|
|
16
|
+
import BooleanField from './fields/BooleanField.svelte';
|
|
17
|
+
import ImageField from './fields/ImageField.svelte';
|
|
18
|
+
import TagField from './fields/TagField.svelte';
|
|
19
|
+
import DateField from './fields/DateField.svelte';
|
|
20
|
+
import EmailField from './fields/EmailField.svelte';
|
|
21
|
+
import UrlField from './fields/UrlField.svelte';
|
|
22
|
+
import SelectField from './fields/SelectField.svelte';
|
|
23
|
+
import MultiSelectField from './fields/MultiSelectField.svelte';
|
|
24
|
+
import RelationField from './fields/RelationField.svelte';
|
|
25
|
+
import JsonField from './fields/JsonField.svelte';
|
|
26
|
+
import RichTextField from './fields/RichTextField.svelte';
|
|
27
|
+
import NumberField from './fields/NumberField.svelte';
|
|
28
|
+
import FileField from './fields/FileField.svelte';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Built-in display components for show/detail pages.
|
|
32
|
+
* Keys match FieldDefinition.type values.
|
|
33
|
+
*/
|
|
34
|
+
export const builtinDisplayComponents: FieldComponentMap = {
|
|
35
|
+
boolean: BooleanField,
|
|
36
|
+
image: ImageField,
|
|
37
|
+
images: ImageField,
|
|
38
|
+
tags: TagField,
|
|
39
|
+
date: DateField,
|
|
40
|
+
email: EmailField,
|
|
41
|
+
url: UrlField,
|
|
42
|
+
select: SelectField,
|
|
43
|
+
multiselect: MultiSelectField,
|
|
44
|
+
relation: RelationField,
|
|
45
|
+
json: JsonField,
|
|
46
|
+
richtext: RichTextField,
|
|
47
|
+
textarea: RichTextField,
|
|
48
|
+
number: NumberField,
|
|
49
|
+
file: FileField,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── Custom component registry ─────────────────────────────────
|
|
53
|
+
// Users can register custom field display components at runtime
|
|
54
|
+
|
|
55
|
+
const customDisplayComponents = new Map<string, Component<Record<string, unknown>>>();
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register a custom field display component for a given field type.
|
|
59
|
+
* The component will be used by ShowPage and any other display contexts.
|
|
60
|
+
*/
|
|
61
|
+
export function registerDisplayComponent(fieldType: string, component: Component<Record<string, unknown>>): void {
|
|
62
|
+
customDisplayComponents.set(fieldType, component);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the display component for a field type.
|
|
67
|
+
* Custom components take precedence over built-in ones.
|
|
68
|
+
* Returns undefined if no component is registered for the type.
|
|
69
|
+
*/
|
|
70
|
+
export function getDisplayComponent(fieldType: string): Component<Record<string, unknown>> | undefined {
|
|
71
|
+
return customDisplayComponents.get(fieldType) ?? builtinDisplayComponents[fieldType];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a field type has a display component (custom or built-in).
|
|
76
|
+
*/
|
|
77
|
+
export function hasDisplayComponent(fieldType: string): boolean {
|
|
78
|
+
return customDisplayComponents.has(fieldType) || fieldType in builtinDisplayComponents;
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props { value: boolean | null | undefined; trueIcon?: string; falseIcon?: string }
|
|
3
|
+
let { value, trueIcon = '✓', falseIcon = '✗' }: Props = $props();
|
|
4
|
+
</script>
|
|
5
|
+
{#if value}
|
|
6
|
+
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400">
|
|
7
|
+
{trueIcon}
|
|
8
|
+
</span>
|
|
9
|
+
{:else}
|
|
10
|
+
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full text-sm bg-red-50 text-red-600 dark:bg-red-500/20 dark:text-red-400">
|
|
11
|
+
{falseIcon}
|
|
12
|
+
</span>
|
|
13
|
+
{/if}
|