@svadmin/ui 0.3.0 → 0.5.0
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 +9 -4
- package/src/actions.ts +110 -0
- package/src/components/AdminApp.svelte +7 -4
- package/src/components/Authenticated.svelte +1 -7
- package/src/components/AutoForm.svelte +158 -69
- package/src/components/AutoSaveIndicator.svelte +19 -14
- package/src/components/AutoTable.svelte +465 -243
- package/src/components/Breadcrumbs.svelte +20 -17
- package/src/components/CanAccess.svelte +6 -4
- 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 +3 -2
- package/src/components/DevTools.svelte +99 -230
- package/src/components/DrawerForm.svelte +19 -10
- package/src/components/EditPage.svelte +25 -6
- package/src/components/EmptyState.svelte +5 -1
- package/src/components/ErrorBoundary.svelte +38 -40
- package/src/components/ErrorComponent.svelte +18 -17
- package/src/components/FieldRenderer.svelte +240 -75
- package/src/components/ForgotPasswordPage.svelte +20 -111
- package/src/components/Header.svelte +28 -8
- 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 +25 -1
- package/src/components/LiveIndicator.svelte +30 -87
- package/src/components/LoginPage.svelte +81 -155
- package/src/components/ModalForm.svelte +6 -2
- package/src/components/PageSkeleton.svelte +2 -1
- package/src/components/PasswordInput.svelte +95 -0
- package/src/components/ProfilePage.svelte +166 -0
- package/src/components/RegisterPage.svelte +35 -164
- package/src/components/ShowPage.svelte +32 -33
- package/src/components/Sidebar.svelte +185 -73
- package/src/components/StatsCard.svelte +12 -3
- package/src/components/StepsForm.svelte +139 -0
- package/src/components/Toast.svelte +15 -54
- 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/RefreshButton.svelte +7 -2
- package/src/components/fieldComponentMap.ts +79 -0
- package/src/components/fields/BooleanField.svelte +9 -9
- package/src/components/fields/EmailField.svelte +2 -8
- package/src/components/fields/FileField.svelte +4 -9
- package/src/components/fields/ImageField.svelte +2 -7
- package/src/components/fields/JsonField.svelte +72 -0
- package/src/components/fields/MarkdownField.svelte +2 -12
- package/src/components/fields/MultiSelectField.svelte +20 -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 +6 -11
- package/src/components/fields/TextField.svelte +2 -7
- package/src/components/fields/UrlField.svelte +4 -10
- 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/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 +31 -1
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
import { getResources } from '@svadmin/core';
|
|
3
3
|
import { currentPath } from '@svadmin/core/router';
|
|
4
4
|
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import * as Breadcrumb from './ui/breadcrumb/index.js';
|
|
5
6
|
|
|
6
7
|
const resources = getResources();
|
|
7
8
|
let path = $state(currentPath());
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
function onHash() { path = currentPath(); }
|
|
11
|
-
window.addEventListener('hashchange', onHash);
|
|
12
|
-
return () => window.removeEventListener('hashchange', onHash);
|
|
13
|
-
});
|
|
10
|
+
function onHashChange() { path = currentPath(); }
|
|
14
11
|
|
|
15
12
|
interface Crumb { label: string; href: string; }
|
|
16
13
|
|
|
@@ -37,17 +34,23 @@
|
|
|
37
34
|
});
|
|
38
35
|
</script>
|
|
39
36
|
|
|
37
|
+
<svelte:window onhashchange={onHashChange} />
|
|
38
|
+
|
|
40
39
|
{#if crumbs.length > 1}
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
{#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
<Breadcrumb.Root class="mb-4">
|
|
41
|
+
<Breadcrumb.List>
|
|
42
|
+
{#each crumbs as crumb, i}
|
|
43
|
+
{#if i > 0}
|
|
44
|
+
<Breadcrumb.Separator />
|
|
45
|
+
{/if}
|
|
46
|
+
<Breadcrumb.Item>
|
|
47
|
+
{#if i === crumbs.length - 1}
|
|
48
|
+
<Breadcrumb.Page>{crumb.label}</Breadcrumb.Page>
|
|
49
|
+
{:else}
|
|
50
|
+
<Breadcrumb.Link href={crumb.href}>{crumb.label}</Breadcrumb.Link>
|
|
51
|
+
{/if}
|
|
52
|
+
</Breadcrumb.Item>
|
|
53
|
+
{/each}
|
|
54
|
+
</Breadcrumb.List>
|
|
55
|
+
</Breadcrumb.Root>
|
|
53
56
|
{/if}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import type { Action } from '@svadmin/core
|
|
3
|
+
import { useCan } from '@svadmin/core';
|
|
4
|
+
import type { Action } from '@svadmin/core';
|
|
5
5
|
|
|
6
6
|
let { resource, action, params, children, fallback } = $props<{
|
|
7
7
|
resource: string;
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
fallback?: Snippet;
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const can = $derived(useCan(resource, action, params));
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
|
-
{#if
|
|
17
|
+
{#if can.isLoading}
|
|
18
|
+
<!-- Loading: nothing rendered -->
|
|
19
|
+
{:else if can.allowed}
|
|
18
20
|
{@render children()}
|
|
19
21
|
{:else if fallback}
|
|
20
22
|
{@render fallback()}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useSelect } from '@svadmin/core';
|
|
3
|
+
import type { Filter } from '@svadmin/core';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
6
|
+
import { Badge } from './ui/badge/index.js';
|
|
7
|
+
import { Command } from 'cmdk-sv';
|
|
8
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
9
|
+
import { ChevronsUpDown, Check, X, Search } from 'lucide-svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
resource: string;
|
|
13
|
+
value?: string | number | null;
|
|
14
|
+
onchange?: (value: string | number | null) => void;
|
|
15
|
+
optionLabel?: string;
|
|
16
|
+
optionValue?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
searchable?: boolean;
|
|
19
|
+
onSearch?: (value: string) => Filter[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
resource,
|
|
24
|
+
value = null,
|
|
25
|
+
onchange,
|
|
26
|
+
optionLabel = 'title',
|
|
27
|
+
optionValue = 'id',
|
|
28
|
+
placeholder = 'Select...',
|
|
29
|
+
searchable = true,
|
|
30
|
+
onSearch,
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
const { options, query, onSearchChange } = useSelect({
|
|
34
|
+
resource,
|
|
35
|
+
optionLabel,
|
|
36
|
+
optionValue,
|
|
37
|
+
onSearch: onSearch ?? (searchable ? (v: string) => v ? [{ field: optionLabel, operator: 'contains' as const, value: v }] : [] : undefined),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let open = $state(false);
|
|
41
|
+
let searchInputValue = $state('');
|
|
42
|
+
|
|
43
|
+
const selectedLabel = $derived(
|
|
44
|
+
(options as { label: string; value: string | number }[])?.find(
|
|
45
|
+
(o: { label: string; value: string | number }) => o.value === value
|
|
46
|
+
)?.label ?? ''
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function handleSelect(optValue: string | number) {
|
|
50
|
+
onchange?.(optValue === value ? null : optValue);
|
|
51
|
+
open = false;
|
|
52
|
+
searchInputValue = '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleClear(e: Event) {
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
onchange?.(null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleSearchInput(v: string) {
|
|
61
|
+
searchInputValue = v;
|
|
62
|
+
onSearchChange(v);
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<div class="relative">
|
|
67
|
+
<Button
|
|
68
|
+
variant="outline"
|
|
69
|
+
type="button"
|
|
70
|
+
class="w-full justify-between font-normal"
|
|
71
|
+
onclick={() => { open = !open; }}
|
|
72
|
+
>
|
|
73
|
+
{#if selectedLabel}
|
|
74
|
+
<span class="truncate">{selectedLabel}</span>
|
|
75
|
+
<TooltipButton tooltip={t('common.clear')} size="icon-sm" class="h-5 w-5 ml-1" onclick={handleClear}>
|
|
76
|
+
<X class="h-3 w-3" />
|
|
77
|
+
</TooltipButton>
|
|
78
|
+
{:else}
|
|
79
|
+
<span class="text-muted-foreground">{placeholder}</span>
|
|
80
|
+
{/if}
|
|
81
|
+
<ChevronsUpDown class="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
|
82
|
+
</Button>
|
|
83
|
+
|
|
84
|
+
{#if open}
|
|
85
|
+
<div class="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg">
|
|
86
|
+
<Command.Root class="flex flex-col overflow-hidden rounded-md bg-popover text-popover-foreground">
|
|
87
|
+
{#if searchable}
|
|
88
|
+
<div class="flex items-center border-b px-2">
|
|
89
|
+
<Search class="mr-1.5 h-3.5 w-3.5 shrink-0 opacity-50" />
|
|
90
|
+
<Command.Input
|
|
91
|
+
value={searchInputValue}
|
|
92
|
+
onValueChange={handleSearchInput}
|
|
93
|
+
placeholder="Search..."
|
|
94
|
+
class="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
<Command.List class="max-h-48 overflow-y-auto p-1">
|
|
99
|
+
<Command.Empty class="py-4 text-center text-xs text-muted-foreground">
|
|
100
|
+
No results.
|
|
101
|
+
</Command.Empty>
|
|
102
|
+
{#if query.isLoading}
|
|
103
|
+
{#each Array(3) as _}
|
|
104
|
+
<div class="px-2 py-1.5"><Skeleton class="h-5 w-full" /></div>
|
|
105
|
+
{/each}
|
|
106
|
+
{:else}
|
|
107
|
+
{#each (options as { label: string; value: string | number }[]) ?? [] as opt}
|
|
108
|
+
<Command.Item
|
|
109
|
+
value={opt.label}
|
|
110
|
+
onSelect={() => handleSelect(opt.value)}
|
|
111
|
+
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
|
|
112
|
+
>
|
|
113
|
+
<Check class="mr-2 h-3.5 w-3.5 {value === opt.value ? 'opacity-100' : 'opacity-0'}" />
|
|
114
|
+
{opt.label}
|
|
115
|
+
</Command.Item>
|
|
116
|
+
{/each}
|
|
117
|
+
{/if}
|
|
118
|
+
</Command.List>
|
|
119
|
+
</Command.Root>
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getResources } from '@svadmin/core';
|
|
3
|
+
import { navigate } from '@svadmin/core/router';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import { toggleTheme } from '@svadmin/core';
|
|
6
|
+
import { Command } from 'cmdk-sv';
|
|
7
|
+
import * as Dialog from './ui/dialog/index.js';
|
|
8
|
+
import { Search, LayoutDashboard, Plus, Sun, FileText } from 'lucide-svelte';
|
|
9
|
+
|
|
10
|
+
let { open = $bindable(false) } = $props<{ open?: boolean }>();
|
|
11
|
+
let searchValue = $state('');
|
|
12
|
+
|
|
13
|
+
const resources = getResources();
|
|
14
|
+
|
|
15
|
+
function close() {
|
|
16
|
+
open = false;
|
|
17
|
+
searchValue = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function act(fn: () => void) {
|
|
21
|
+
fn();
|
|
22
|
+
close();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const itemClass = 'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50';
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<Dialog.Dialog bind:open>
|
|
29
|
+
<Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[560px]">
|
|
30
|
+
<Command.Root class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-semibold [&_[data-cmdk-group-heading]]:uppercase [&_[data-cmdk-group-heading]]:tracking-wider">
|
|
31
|
+
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
|
32
|
+
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
33
|
+
<Command.Input
|
|
34
|
+
bind:value={searchValue}
|
|
35
|
+
placeholder={t('common.search')}
|
|
36
|
+
class="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
37
|
+
/>
|
|
38
|
+
<kbd class="ml-2 text-[10px] font-medium text-muted-foreground bg-muted px-1.5 py-0.5 rounded border border-border">ESC</kbd>
|
|
39
|
+
</div>
|
|
40
|
+
<Command.List class="max-h-[320px] overflow-y-auto overflow-x-hidden p-1">
|
|
41
|
+
<Command.Empty class="py-6 text-center text-sm text-muted-foreground">
|
|
42
|
+
{t('common.noData')}
|
|
43
|
+
</Command.Empty>
|
|
44
|
+
|
|
45
|
+
<!-- Navigation -->
|
|
46
|
+
<Command.Group heading={t('common.home')}>
|
|
47
|
+
<Command.Item
|
|
48
|
+
value="home"
|
|
49
|
+
onSelect={() => act(() => navigate('/'))}
|
|
50
|
+
class={itemClass}
|
|
51
|
+
>
|
|
52
|
+
<LayoutDashboard class="h-4 w-4 text-muted-foreground" />
|
|
53
|
+
{t('common.home')}
|
|
54
|
+
</Command.Item>
|
|
55
|
+
{#each resources as r}
|
|
56
|
+
<Command.Item
|
|
57
|
+
value={r.name}
|
|
58
|
+
onSelect={() => act(() => navigate(`/${r.name}`))}
|
|
59
|
+
class={itemClass}
|
|
60
|
+
>
|
|
61
|
+
<FileText class="h-4 w-4 text-muted-foreground" />
|
|
62
|
+
{r.label}
|
|
63
|
+
</Command.Item>
|
|
64
|
+
{/each}
|
|
65
|
+
</Command.Group>
|
|
66
|
+
|
|
67
|
+
<Command.Separator class="mx-1 my-1 h-px bg-border" />
|
|
68
|
+
|
|
69
|
+
<!-- Actions -->
|
|
70
|
+
<Command.Group heading={t('common.actions')}>
|
|
71
|
+
{#each resources as r}
|
|
72
|
+
<Command.Item
|
|
73
|
+
value="create-{r.name}"
|
|
74
|
+
onSelect={() => act(() => navigate(`/${r.name}/create`))}
|
|
75
|
+
class={itemClass}
|
|
76
|
+
>
|
|
77
|
+
<Plus class="h-4 w-4 text-muted-foreground" />
|
|
78
|
+
{t('common.create')} {r.label}
|
|
79
|
+
</Command.Item>
|
|
80
|
+
{/each}
|
|
81
|
+
<Command.Item
|
|
82
|
+
value="toggle-theme"
|
|
83
|
+
onSelect={() => act(() => toggleTheme())}
|
|
84
|
+
class={itemClass}
|
|
85
|
+
>
|
|
86
|
+
<Sun class="h-4 w-4 text-muted-foreground" />
|
|
87
|
+
{t('common.toggleTheme')}
|
|
88
|
+
</Command.Item>
|
|
89
|
+
</Command.Group>
|
|
90
|
+
</Command.List>
|
|
91
|
+
</Command.Root>
|
|
92
|
+
</Dialog.DialogContent>
|
|
93
|
+
</Dialog.Dialog>
|
|
@@ -2,23 +2,41 @@
|
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
3
|
import { AlertTriangle, Copy, CheckCircle } from 'lucide-svelte';
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
5
6
|
import * as Card from './ui/card/index.js';
|
|
6
7
|
|
|
7
|
-
let { title
|
|
8
|
+
let { title, missingVars = [], envTemplate = '' } = $props<{
|
|
8
9
|
title?: string;
|
|
9
10
|
missingVars?: { key: string; description?: string }[];
|
|
10
11
|
envTemplate?: string;
|
|
11
12
|
}>();
|
|
12
13
|
|
|
14
|
+
const displayTitle = $derived(title ?? t('config.missingEnvTitle'));
|
|
15
|
+
|
|
13
16
|
let copied = $state<Record<string, boolean>>({});
|
|
17
|
+
let copyTimers = $state<Record<string, ReturnType<typeof setTimeout>>>({});
|
|
18
|
+
|
|
19
|
+
function setCopied(key: string) {
|
|
20
|
+
copied = { ...copied, [key]: true };
|
|
21
|
+
// Clear previous timer for this key if any
|
|
22
|
+
if (copyTimers[key]) clearTimeout(copyTimers[key]);
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
copied = { ...copied, [key]: false };
|
|
25
|
+
}, 2000);
|
|
26
|
+
copyTimers = { ...copyTimers, [key]: timer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Cleanup all timers on destroy
|
|
30
|
+
$effect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
Object.values(copyTimers).forEach(clearTimeout);
|
|
33
|
+
};
|
|
34
|
+
});
|
|
14
35
|
|
|
15
36
|
async function copyToClipboard(text: string, key: string) {
|
|
16
37
|
try {
|
|
17
38
|
await navigator.clipboard.writeText(text);
|
|
18
|
-
|
|
19
|
-
setTimeout(() => {
|
|
20
|
-
copied = { ...copied, [key]: false };
|
|
21
|
-
}, 2000);
|
|
39
|
+
setCopied(key);
|
|
22
40
|
} catch {
|
|
23
41
|
console.warn('[svadmin] clipboard API unavailable');
|
|
24
42
|
}
|
|
@@ -30,63 +48,68 @@
|
|
|
30
48
|
}
|
|
31
49
|
</script>
|
|
32
50
|
|
|
33
|
-
<div class="
|
|
34
|
-
<div class="
|
|
35
|
-
<Card.Card class="
|
|
36
|
-
<Card.CardHeader class="
|
|
37
|
-
<div class="
|
|
51
|
+
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-destructive/[0.05] via-background to-destructive/[0.03] p-4">
|
|
52
|
+
<div class="w-full max-w-[480px]">
|
|
53
|
+
<Card.Card class="backdrop-blur-xl border-border/50 shadow-[0_8px_32px_hsl(var(--destructive)/0.08),0_2px_8px_hsl(0_0%_0%/0.06)]">
|
|
54
|
+
<Card.CardHeader class="text-center pb-2">
|
|
55
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-[14px] bg-destructive/10 text-destructive mx-auto mb-3">
|
|
38
56
|
<AlertTriangle class="h-7 w-7" />
|
|
39
57
|
</div>
|
|
40
|
-
<Card.CardTitle class="text-xl font-bold">{
|
|
58
|
+
<Card.CardTitle class="text-xl font-bold">{displayTitle}</Card.CardTitle>
|
|
41
59
|
<p class="text-sm text-muted-foreground">
|
|
42
60
|
{t('config.missingEnvDescription')}
|
|
43
61
|
</p>
|
|
44
62
|
</Card.CardHeader>
|
|
45
63
|
<Card.CardContent class="space-y-4">
|
|
46
64
|
{#if missingVars.length > 0}
|
|
47
|
-
<div class="
|
|
48
|
-
{#each missingVars as v (v.key)}
|
|
49
|
-
<div class="
|
|
50
|
-
<div class="
|
|
51
|
-
<code class="
|
|
65
|
+
<div class="rounded-lg border overflow-hidden">
|
|
66
|
+
{#each missingVars as v, i (v.key)}
|
|
67
|
+
<div class="flex items-center justify-between px-3 py-2.5 gap-2 {i < missingVars.length - 1 ? 'border-b border-border/50' : ''}">
|
|
68
|
+
<div class="flex flex-col gap-0.5 min-w-0">
|
|
69
|
+
<code class="text-[0.8125rem] font-semibold text-foreground font-mono">{v.key}</code>
|
|
52
70
|
{#if v.description}
|
|
53
|
-
<span class="
|
|
71
|
+
<span class="text-[0.6875rem] text-muted-foreground">{v.description}</span>
|
|
54
72
|
{/if}
|
|
55
73
|
</div>
|
|
56
|
-
<
|
|
57
|
-
|
|
74
|
+
<TooltipButton
|
|
75
|
+
tooltip={t('common.copy')}
|
|
76
|
+
variant="ghost"
|
|
77
|
+
size="icon"
|
|
78
|
+
class="h-7 w-7 shrink-0"
|
|
58
79
|
onclick={() => copyToClipboard(`${v.key}=`, v.key)}
|
|
59
|
-
title="Copy"
|
|
60
80
|
>
|
|
61
81
|
{#if copied[v.key]}
|
|
62
82
|
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
63
83
|
{:else}
|
|
64
84
|
<Copy class="h-3.5 w-3.5" />
|
|
65
85
|
{/if}
|
|
66
|
-
</
|
|
86
|
+
</TooltipButton>
|
|
67
87
|
</div>
|
|
68
88
|
{/each}
|
|
69
89
|
</div>
|
|
70
90
|
{/if}
|
|
71
91
|
|
|
72
92
|
{#if envTemplate}
|
|
73
|
-
<div class="
|
|
74
|
-
<div class="
|
|
93
|
+
<div class="rounded-lg border overflow-hidden">
|
|
94
|
+
<div class="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-border/50">
|
|
75
95
|
<span class="text-xs font-medium text-muted-foreground">{t('config.envFilePath')}</span>
|
|
76
|
-
<
|
|
77
|
-
|
|
96
|
+
<TooltipButton
|
|
97
|
+
tooltip={t('common.copyAll')}
|
|
98
|
+
variant="ghost"
|
|
99
|
+
size="sm"
|
|
100
|
+
class="h-7 gap-1"
|
|
78
101
|
onclick={copyAll}
|
|
79
102
|
>
|
|
80
103
|
{#if copied['__all__']}
|
|
81
104
|
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
82
|
-
<span class="text-xs">
|
|
105
|
+
<span class="text-xs">{t('common.copied')}</span>
|
|
83
106
|
{:else}
|
|
84
107
|
<Copy class="h-3.5 w-3.5" />
|
|
85
|
-
<span class="text-xs">
|
|
108
|
+
<span class="text-xs">{t('common.copyAll')}</span>
|
|
86
109
|
{/if}
|
|
87
|
-
</
|
|
110
|
+
</TooltipButton>
|
|
88
111
|
</div>
|
|
89
|
-
<pre class="
|
|
112
|
+
<pre class="px-3 py-3 text-xs font-mono leading-relaxed text-foreground bg-muted/20 m-0 whitespace-pre-wrap break-all">{envTemplate}</pre>
|
|
90
113
|
</div>
|
|
91
114
|
{/if}
|
|
92
115
|
|
|
@@ -95,130 +118,9 @@
|
|
|
95
118
|
</p>
|
|
96
119
|
|
|
97
120
|
<Button variant="outline" class="w-full" onclick={() => window.location.reload()}>
|
|
98
|
-
|
|
121
|
+
{t('config.reloadButton')}
|
|
99
122
|
</Button>
|
|
100
123
|
</Card.CardContent>
|
|
101
124
|
</Card.Card>
|
|
102
125
|
</div>
|
|
103
126
|
</div>
|
|
104
|
-
|
|
105
|
-
<style>
|
|
106
|
-
.config-error-page {
|
|
107
|
-
min-height: 100vh;
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
justify-content: center;
|
|
111
|
-
background: linear-gradient(135deg, hsl(var(--destructive) / 0.05) 0%, hsl(var(--background)) 50%, hsl(var(--destructive) / 0.03) 100%);
|
|
112
|
-
padding: 1rem;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.config-error-container {
|
|
116
|
-
width: 100%;
|
|
117
|
-
max-width: 480px;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
:global(.config-error-card) {
|
|
121
|
-
backdrop-filter: blur(20px);
|
|
122
|
-
border: 1px solid hsl(var(--border) / 0.5);
|
|
123
|
-
box-shadow: 0 8px 32px hsl(var(--destructive) / 0.08), 0 2px 8px hsl(0 0% 0% / 0.06);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
:global(.config-error-header) {
|
|
127
|
-
text-align: center;
|
|
128
|
-
padding-bottom: 0.5rem;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.config-error-icon {
|
|
132
|
-
display: inline-flex;
|
|
133
|
-
align-items: center;
|
|
134
|
-
justify-content: center;
|
|
135
|
-
width: 56px;
|
|
136
|
-
height: 56px;
|
|
137
|
-
border-radius: 14px;
|
|
138
|
-
background: hsl(var(--destructive) / 0.1);
|
|
139
|
-
color: hsl(var(--destructive));
|
|
140
|
-
margin: 0 auto 0.75rem;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.env-var-list {
|
|
144
|
-
border: 1px solid hsl(var(--border));
|
|
145
|
-
border-radius: 0.5rem;
|
|
146
|
-
overflow: hidden;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.env-var-row {
|
|
150
|
-
display: flex;
|
|
151
|
-
align-items: center;
|
|
152
|
-
justify-content: space-between;
|
|
153
|
-
padding: 0.625rem 0.75rem;
|
|
154
|
-
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
|
155
|
-
gap: 0.5rem;
|
|
156
|
-
}
|
|
157
|
-
.env-var-row:last-child {
|
|
158
|
-
border-bottom: none;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.env-var-info {
|
|
162
|
-
display: flex;
|
|
163
|
-
flex-direction: column;
|
|
164
|
-
gap: 0.125rem;
|
|
165
|
-
min-width: 0;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.env-var-key {
|
|
169
|
-
font-size: 0.8125rem;
|
|
170
|
-
font-weight: 600;
|
|
171
|
-
color: hsl(var(--foreground));
|
|
172
|
-
font-family: monospace;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.env-var-desc {
|
|
176
|
-
font-size: 0.6875rem;
|
|
177
|
-
color: hsl(var(--muted-foreground));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.copy-btn {
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
gap: 0.25rem;
|
|
184
|
-
background: none;
|
|
185
|
-
border: none;
|
|
186
|
-
cursor: pointer;
|
|
187
|
-
padding: 0.25rem 0.5rem;
|
|
188
|
-
color: hsl(var(--muted-foreground));
|
|
189
|
-
border-radius: 0.25rem;
|
|
190
|
-
transition: all 0.15s;
|
|
191
|
-
flex-shrink: 0;
|
|
192
|
-
}
|
|
193
|
-
.copy-btn:hover {
|
|
194
|
-
color: hsl(var(--foreground));
|
|
195
|
-
background: hsl(var(--muted) / 0.5);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.env-template {
|
|
199
|
-
border: 1px solid hsl(var(--border));
|
|
200
|
-
border-radius: 0.5rem;
|
|
201
|
-
overflow: hidden;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.env-template-header {
|
|
205
|
-
display: flex;
|
|
206
|
-
align-items: center;
|
|
207
|
-
justify-content: space-between;
|
|
208
|
-
padding: 0.5rem 0.75rem;
|
|
209
|
-
background: hsl(var(--muted) / 0.5);
|
|
210
|
-
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.env-template-code {
|
|
214
|
-
padding: 0.75rem;
|
|
215
|
-
font-size: 0.75rem;
|
|
216
|
-
font-family: monospace;
|
|
217
|
-
line-height: 1.6;
|
|
218
|
-
color: hsl(var(--foreground));
|
|
219
|
-
background: hsl(var(--muted) / 0.2);
|
|
220
|
-
margin: 0;
|
|
221
|
-
white-space: pre-wrap;
|
|
222
|
-
word-break: break-all;
|
|
223
|
-
}
|
|
224
|
-
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import * as
|
|
2
|
+
import * as AlertDialog from './ui/alert-dialog/index.js';
|
|
3
3
|
import { Button } from './ui/button/index.js';
|
|
4
4
|
import { t } from '@svadmin/core/i18n';
|
|
5
5
|
|
|
@@ -27,19 +27,27 @@
|
|
|
27
27
|
const resolvedCancelText = $derived(cancelText ?? t('common.cancel'));
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
</
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
<AlertDialog.Root bind:open onOpenChange={(v: boolean) => { if (!v) oncancel(); }}>
|
|
31
|
+
<AlertDialog.Content>
|
|
32
|
+
<AlertDialog.Header>
|
|
33
|
+
<AlertDialog.Title>{resolvedTitle}</AlertDialog.Title>
|
|
34
|
+
<AlertDialog.Description>{message}</AlertDialog.Description>
|
|
35
|
+
</AlertDialog.Header>
|
|
36
|
+
<AlertDialog.Footer>
|
|
37
|
+
<AlertDialog.Cancel>
|
|
38
|
+
{#snippet child({ props })}
|
|
39
|
+
<Button variant="outline" {...props} onclick={oncancel}>
|
|
40
|
+
{resolvedCancelText}
|
|
41
|
+
</Button>
|
|
42
|
+
{/snippet}
|
|
43
|
+
</AlertDialog.Cancel>
|
|
44
|
+
<AlertDialog.Action>
|
|
45
|
+
{#snippet child({ props })}
|
|
46
|
+
<Button variant={variantMap[variant as keyof typeof variantMap]} {...props} onclick={onconfirm}>
|
|
47
|
+
{resolvedConfirmText}
|
|
48
|
+
</Button>
|
|
49
|
+
{/snippet}
|
|
50
|
+
</AlertDialog.Action>
|
|
51
|
+
</AlertDialog.Footer>
|
|
52
|
+
</AlertDialog.Content>
|
|
53
|
+
</AlertDialog.Root>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import AutoForm from './AutoForm.svelte';
|
|
8
8
|
import { ArrowLeft } from 'lucide-svelte';
|
|
9
9
|
import { Button } from './ui/button/index.js';
|
|
10
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
resourceName: string;
|
|
@@ -29,9 +30,9 @@
|
|
|
29
30
|
<div class="space-y-6 {className}">
|
|
30
31
|
<PageHeader title={pageTitle}>
|
|
31
32
|
{#snippet actions()}
|
|
32
|
-
<
|
|
33
|
+
<TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
|
|
33
34
|
<ArrowLeft class="h-5 w-5" />
|
|
34
|
-
</
|
|
35
|
+
</TooltipButton>
|
|
35
36
|
{#if headerActions}
|
|
36
37
|
{@render headerActions()}
|
|
37
38
|
{/if}
|