@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.
Files changed (120) hide show
  1. package/package.json +8 -3
  2. package/src/actions.ts +110 -0
  3. package/src/components/AdminApp.svelte +8 -4
  4. package/src/components/Authenticated.svelte +38 -13
  5. package/src/components/AutoForm.svelte +163 -74
  6. package/src/components/AutoSaveIndicator.svelte +35 -0
  7. package/src/components/AutoTable.svelte +476 -183
  8. package/src/components/Breadcrumbs.svelte +22 -17
  9. package/src/components/CatchAllNavigate.svelte +21 -0
  10. package/src/components/ComboboxField.svelte +122 -0
  11. package/src/components/CommandPalette.svelte +93 -0
  12. package/src/components/ConfigErrorScreen.svelte +53 -151
  13. package/src/components/ConfirmDialog.svelte +25 -17
  14. package/src/components/CreatePage.svelte +43 -0
  15. package/src/components/DevTools.svelte +99 -230
  16. package/src/components/DrawerForm.svelte +19 -10
  17. package/src/components/EditPage.svelte +71 -0
  18. package/src/components/EmptyState.svelte +21 -31
  19. package/src/components/ErrorBoundary.svelte +38 -40
  20. package/src/components/ErrorComponent.svelte +36 -0
  21. package/src/components/FieldRenderer.svelte +240 -75
  22. package/src/components/ForgotPasswordPage.svelte +20 -111
  23. package/src/components/Header.svelte +46 -57
  24. package/src/components/InferencerPanel.svelte +136 -110
  25. package/src/components/InfiniteList.svelte +93 -0
  26. package/src/components/Layout.svelte +85 -22
  27. package/src/components/ListPage.svelte +71 -0
  28. package/src/components/LiveIndicator.svelte +58 -0
  29. package/src/components/LoginPage.svelte +33 -154
  30. package/src/components/ModalForm.svelte +6 -2
  31. package/src/components/NavigateToResource.svelte +20 -0
  32. package/src/components/PageSkeleton.svelte +71 -0
  33. package/src/components/PasswordInput.svelte +95 -0
  34. package/src/components/RegisterPage.svelte +35 -164
  35. package/src/components/ShowPage.svelte +35 -36
  36. package/src/components/Sidebar.svelte +212 -81
  37. package/src/components/StatsCard.svelte +12 -3
  38. package/src/components/StepsForm.svelte +139 -0
  39. package/src/components/ThemedTitle.svelte +32 -0
  40. package/src/components/Toast.svelte +15 -44
  41. package/src/components/TooltipButton.svelte +52 -0
  42. package/src/components/UndoableNotification.svelte +28 -96
  43. package/src/components/UpdatePasswordPage.svelte +32 -160
  44. package/src/components/buttons/CloneButton.svelte +1 -1
  45. package/src/components/buttons/CreateButton.svelte +1 -1
  46. package/src/components/buttons/DeleteButton.svelte +3 -4
  47. package/src/components/buttons/EditButton.svelte +1 -1
  48. package/src/components/buttons/ImportButton.svelte +29 -7
  49. package/src/components/buttons/RefreshButton.svelte +11 -5
  50. package/src/components/fieldComponentMap.ts +79 -0
  51. package/src/components/fields/BooleanField.svelte +13 -0
  52. package/src/components/fields/DateField.svelte +6 -0
  53. package/src/components/fields/EmailField.svelte +9 -0
  54. package/src/components/fields/FileField.svelte +14 -0
  55. package/src/components/fields/ImageField.svelte +9 -0
  56. package/src/components/fields/JsonField.svelte +72 -0
  57. package/src/components/fields/MarkdownField.svelte +9 -0
  58. package/src/components/fields/MultiSelectField.svelte +20 -0
  59. package/src/components/fields/NumberField.svelte +6 -0
  60. package/src/components/fields/RelationField.svelte +24 -0
  61. package/src/components/fields/RichTextField.svelte +35 -0
  62. package/src/components/fields/SelectField.svelte +14 -0
  63. package/src/components/fields/TagField.svelte +12 -0
  64. package/src/components/fields/TextField.svelte +11 -0
  65. package/src/components/fields/UrlField.svelte +14 -0
  66. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +25 -0
  67. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +16 -0
  68. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +8 -0
  69. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +8 -0
  70. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +16 -0
  71. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +6 -0
  72. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +16 -0
  73. package/src/components/ui/alert-dialog/index.ts +20 -0
  74. package/src/components/ui/avatar/avatar.svelte +2 -1
  75. package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +18 -0
  76. package/src/components/ui/breadcrumb/breadcrumb-item.svelte +15 -0
  77. package/src/components/ui/breadcrumb/breadcrumb-link.svelte +15 -0
  78. package/src/components/ui/breadcrumb/breadcrumb-list.svelte +15 -0
  79. package/src/components/ui/breadcrumb/breadcrumb-page.svelte +18 -0
  80. package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +22 -0
  81. package/src/components/ui/breadcrumb/breadcrumb.svelte +11 -0
  82. package/src/components/ui/breadcrumb/index.ts +7 -0
  83. package/src/components/ui/collapsible/index.ts +7 -0
  84. package/src/components/ui/command/command-dialog.svelte +31 -0
  85. package/src/components/ui/command/index.ts +2 -0
  86. package/src/components/ui/context-menu/index.ts +11 -0
  87. package/src/components/ui/drawer/drawer-content.svelte +21 -0
  88. package/src/components/ui/drawer/drawer-description.svelte +11 -0
  89. package/src/components/ui/drawer/drawer-footer.svelte +11 -0
  90. package/src/components/ui/drawer/drawer-header.svelte +11 -0
  91. package/src/components/ui/drawer/drawer-title.svelte +11 -0
  92. package/src/components/ui/drawer/drawer.svelte +18 -0
  93. package/src/components/ui/drawer/index.ts +6 -0
  94. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
  95. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +27 -0
  96. package/src/components/ui/dropdown-menu/index.ts +6 -0
  97. package/src/components/ui/label/index.ts +1 -0
  98. package/src/components/ui/label/label.svelte +18 -0
  99. package/src/components/ui/pagination/index.ts +7 -0
  100. package/src/components/ui/pagination/pagination-content.svelte +11 -0
  101. package/src/components/ui/pagination/pagination-ellipsis.svelte +12 -0
  102. package/src/components/ui/pagination/pagination-item.svelte +11 -0
  103. package/src/components/ui/pagination/pagination-link.svelte +26 -0
  104. package/src/components/ui/pagination/pagination-next.svelte +22 -0
  105. package/src/components/ui/pagination/pagination-previous.svelte +22 -0
  106. package/src/components/ui/pagination/pagination.svelte +11 -0
  107. package/src/components/ui/popover/index.ts +12 -0
  108. package/src/components/ui/popover/popover-content.svelte +25 -0
  109. package/src/components/ui/progress/index.ts +1 -0
  110. package/src/components/ui/progress/progress.svelte +22 -0
  111. package/src/components/ui/scroll-area/index.ts +1 -0
  112. package/src/components/ui/scroll-area/scroll-area.svelte +20 -0
  113. package/src/components/ui/sheet/index.ts +15 -1
  114. package/src/components/ui/sheet/sheet-content.svelte +28 -0
  115. package/src/components/ui/sheet/sheet-description.svelte +22 -0
  116. package/src/components/ui/sheet/sheet-footer.svelte +22 -0
  117. package/src/components/ui/sheet/sheet-header.svelte +22 -0
  118. package/src/components/ui/sheet/sheet-title.svelte +22 -0
  119. package/src/index.ts +52 -1
  120. package/src/router-state.svelte.ts +16 -9
@@ -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
- $effect(() => {
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
 
@@ -29,23 +26,31 @@
29
26
  result.push({ label: t('common.create'), href: `#/${res.name}/create` });
30
27
  } else if (segments[1] === 'edit' && segments[2]) {
31
28
  result.push({ label: `${t('common.edit')} #${segments[2]}`, href: `#/${res.name}/edit/${segments[2]}` });
29
+ } else if (segments[1] === 'show' && segments[2]) {
30
+ result.push({ label: `${t('common.detail')} #${segments[2]}`, href: `#/${res.name}/show/${segments[2]}` });
32
31
  }
33
32
  }
34
33
  return result;
35
34
  });
36
35
  </script>
37
36
 
37
+ <svelte:window onhashchange={onHashChange} />
38
+
38
39
  {#if crumbs.length > 1}
39
- <nav class="mb-4 flex items-center gap-1.5 text-sm text-gray-500">
40
- {#each crumbs as crumb, i}
41
- {#if i > 0}
42
- <span class="text-gray-300">/</span>
43
- {/if}
44
- {#if i === crumbs.length - 1}
45
- <span class="font-medium text-gray-700">{crumb.label}</span>
46
- {:else}
47
- <a href={crumb.href} class="hover:text-primary transition">{crumb.label}</a>
48
- {/if}
49
- {/each}
50
- </nav>
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>
51
56
  {/if}
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { useIsAuthenticated } from '@svadmin/core';
3
+ import { navigate } from '@svadmin/core/router';
4
+ import NavigateToResource from './NavigateToResource.svelte';
5
+
6
+ const auth = useIsAuthenticated();
7
+
8
+ $effect(() => {
9
+ if (!auth.isLoading && !auth.isAuthenticated) {
10
+ navigate('/login');
11
+ }
12
+ });
13
+ </script>
14
+
15
+ {#if auth.isLoading}
16
+ <div class="flex h-screen w-screen items-center justify-center">
17
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"></div>
18
+ </div>
19
+ {:else if auth.isAuthenticated}
20
+ <NavigateToResource />
21
+ {/if}
@@ -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 = 'Configuration Required', missingVars = [], envTemplate = '' } = $props<{
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
- copied = { ...copied, [key]: true };
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="config-error-page">
34
- <div class="config-error-container">
35
- <Card.Card class="config-error-card">
36
- <Card.CardHeader class="config-error-header">
37
- <div class="config-error-icon">
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">{title}</Card.CardTitle>
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="env-var-list">
48
- {#each missingVars as v (v.key)}
49
- <div class="env-var-row">
50
- <div class="env-var-info">
51
- <code class="env-var-key">{v.key}</code>
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="env-var-desc">{v.description}</span>
71
+ <span class="text-[0.6875rem] text-muted-foreground">{v.description}</span>
54
72
  {/if}
55
73
  </div>
56
- <button
57
- class="copy-btn"
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
- </button>
86
+ </TooltipButton>
67
87
  </div>
68
88
  {/each}
69
89
  </div>
70
90
  {/if}
71
91
 
72
92
  {#if envTemplate}
73
- <div class="env-template">
74
- <div class="env-template-header">
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
- <button
77
- class="copy-btn"
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">Copied!</span>
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">Copy All</span>
108
+ <span class="text-xs">{t('common.copyAll')}</span>
86
109
  {/if}
87
- </button>
110
+ </TooltipButton>
88
111
  </div>
89
- <pre class="env-template-code">{envTemplate}</pre>
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
- Reload Page
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 Dialog from './ui/dialog/index.js';
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
- <Dialog.Root bind:open onOpenChange={(v: boolean) => { if (!v) oncancel(); }}>
31
- <Dialog.Content showCloseButton={false} class="sm:max-w-md">
32
- <Dialog.Header>
33
- <Dialog.Title>{resolvedTitle}</Dialog.Title>
34
- <Dialog.Description>{message}</Dialog.Description>
35
- </Dialog.Header>
36
- <Dialog.Footer>
37
- <Button variant="outline" onclick={oncancel}>
38
- {resolvedCancelText}
39
- </Button>
40
- <Button variant={variantMap[variant]} onclick={onconfirm}>
41
- {resolvedConfirmText}
42
- </Button>
43
- </Dialog.Footer>
44
- </Dialog.Content>
45
- </Dialog.Root>
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>
@@ -0,0 +1,43 @@
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 PageHeader from './PageHeader.svelte';
7
+ import AutoForm from './AutoForm.svelte';
8
+ import { ArrowLeft } from 'lucide-svelte';
9
+ import { Button } from './ui/button/index.js';
10
+ import TooltipButton from './TooltipButton.svelte';
11
+
12
+ interface Props {
13
+ resourceName: string;
14
+ title?: string;
15
+ headerActions?: Snippet;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ resourceName,
21
+ title,
22
+ headerActions,
23
+ class: className = '',
24
+ }: Props = $props();
25
+
26
+ const resource = getResource(resourceName);
27
+ const pageTitle = title ?? `${t('common.create')}${resource.label}`;
28
+ </script>
29
+
30
+ <div class="space-y-6 {className}">
31
+ <PageHeader title={pageTitle}>
32
+ {#snippet actions()}
33
+ <TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
34
+ <ArrowLeft class="h-5 w-5" />
35
+ </TooltipButton>
36
+ {#if headerActions}
37
+ {@render headerActions()}
38
+ {/if}
39
+ {/snippet}
40
+ </PageHeader>
41
+
42
+ <AutoForm {resourceName} mode="create" />
43
+ </div>