@svadmin/ui 0.0.2 → 0.0.6
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 +2 -2
- package/src/components/AdminApp.svelte +35 -15
- package/src/components/Authenticated.svelte +26 -0
- package/src/components/AutoForm.svelte +57 -5
- package/src/components/CanAccess.svelte +21 -0
- package/src/components/ConfigErrorScreen.svelte +224 -0
- package/src/components/DevTools.svelte +280 -0
- package/src/components/DrawerForm.svelte +26 -0
- package/src/components/ForgotPasswordPage.svelte +203 -0
- package/src/components/InferencerPanel.svelte +166 -0
- package/src/components/LoginPage.svelte +236 -0
- package/src/components/ModalForm.svelte +34 -0
- package/src/components/RegisterPage.svelte +241 -0
- package/src/components/StatsCard.svelte +23 -2
- package/src/components/UndoableNotification.svelte +132 -0
- package/src/components/UpdatePasswordPage.svelte +217 -0
- package/src/components/buttons/CloneButton.svelte +30 -0
- package/src/components/buttons/CreateButton.svelte +29 -0
- package/src/components/buttons/DeleteButton.svelte +60 -0
- package/src/components/buttons/EditButton.svelte +30 -0
- package/src/components/buttons/ExportButton.svelte +24 -0
- package/src/components/buttons/ImportButton.svelte +28 -0
- package/src/components/buttons/ListButton.svelte +23 -0
- package/src/components/buttons/RefreshButton.svelte +30 -0
- package/src/components/buttons/SaveButton.svelte +27 -0
- package/src/components/buttons/ShowButton.svelte +24 -0
- package/src/components/buttons/index.ts +10 -0
- package/src/index.ts +18 -0
- package/src/router-state.svelte.ts +26 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"svelte": "^5.0.0",
|
|
24
|
-
"@svadmin/core": "0.
|
|
24
|
+
"@svadmin/core": "0.0.5",
|
|
25
25
|
"bits-ui": "^2.0.0",
|
|
26
26
|
"tailwind-variants": "^3.0.0",
|
|
27
27
|
"clsx": "^2.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode } from '@svadmin/core';
|
|
4
|
-
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme } from '@svadmin/core';
|
|
3
|
+
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider } from '@svadmin/core';
|
|
4
|
+
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider } from '@svadmin/core';
|
|
5
5
|
import { navigate } from '@svadmin/core/router';
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
7
7
|
import Layout from './Layout.svelte';
|
|
@@ -9,11 +9,18 @@
|
|
|
9
9
|
import AutoForm from './AutoForm.svelte';
|
|
10
10
|
import ShowPage from './ShowPage.svelte';
|
|
11
11
|
import Toast from './Toast.svelte';
|
|
12
|
+
import LoginPage from './LoginPage.svelte';
|
|
13
|
+
import RegisterPage from './RegisterPage.svelte';
|
|
14
|
+
import ForgotPasswordPage from './ForgotPasswordPage.svelte';
|
|
15
|
+
import UpdatePasswordPage from './UpdatePasswordPage.svelte';
|
|
16
|
+
import ConfigErrorScreen from './ConfigErrorScreen.svelte';
|
|
17
|
+
import DevTools from './DevTools.svelte';
|
|
12
18
|
import { initRouter, getRoute, getParams } from '../router-state.svelte.js';
|
|
13
19
|
|
|
14
20
|
interface Props {
|
|
15
21
|
dataProvider: DataProvider;
|
|
16
22
|
authProvider?: AuthProvider;
|
|
23
|
+
routerProvider?: RouterProvider;
|
|
17
24
|
resources: ResourceDefinition[];
|
|
18
25
|
locale?: string;
|
|
19
26
|
title?: string;
|
|
@@ -25,6 +32,7 @@
|
|
|
25
32
|
let {
|
|
26
33
|
dataProvider,
|
|
27
34
|
authProvider,
|
|
35
|
+
routerProvider,
|
|
28
36
|
resources,
|
|
29
37
|
locale,
|
|
30
38
|
title = 'Admin',
|
|
@@ -33,10 +41,14 @@
|
|
|
33
41
|
loginPage,
|
|
34
42
|
}: Props = $props();
|
|
35
43
|
|
|
44
|
+
// Resolve router provider (default to hash)
|
|
45
|
+
const resolvedRouter = routerProvider ?? createHashRouterProvider();
|
|
46
|
+
|
|
36
47
|
// Set up context
|
|
37
48
|
setDataProvider(dataProvider);
|
|
38
49
|
if (authProvider) setAuthProvider(authProvider);
|
|
39
50
|
setResources(resources);
|
|
51
|
+
setRouterProvider(resolvedRouter);
|
|
40
52
|
if (locale) setLocale(locale);
|
|
41
53
|
if (defaultTheme) setTheme(defaultTheme);
|
|
42
54
|
|
|
@@ -46,23 +58,27 @@
|
|
|
46
58
|
},
|
|
47
59
|
});
|
|
48
60
|
|
|
49
|
-
// Initialize
|
|
50
|
-
initRouter();
|
|
61
|
+
// Initialize router with provider
|
|
62
|
+
initRouter(resolvedRouter);
|
|
51
63
|
|
|
52
64
|
// Reactive getters for route state
|
|
53
65
|
const route = $derived(getRoute());
|
|
54
66
|
const params = $derived(getParams());
|
|
55
67
|
|
|
56
68
|
// Auth check
|
|
57
|
-
let isAuthenticated = $state(
|
|
58
|
-
let authChecked = $state(
|
|
69
|
+
let isAuthenticated = $state(false);
|
|
70
|
+
let authChecked = $state(false);
|
|
59
71
|
|
|
60
72
|
$effect(() => {
|
|
61
|
-
if (!authProvider)
|
|
73
|
+
if (!authProvider) {
|
|
74
|
+
isAuthenticated = true;
|
|
75
|
+
authChecked = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
62
78
|
authProvider.check().then(result => {
|
|
63
79
|
isAuthenticated = result.authenticated;
|
|
64
80
|
authChecked = true;
|
|
65
|
-
if (!result.authenticated && route !== '/login') {
|
|
81
|
+
if (!result.authenticated && route !== '/login' && route !== '/register' && route !== '/forgot-password' && route !== '/update-password') {
|
|
66
82
|
navigate(result.redirectTo ?? '/login');
|
|
67
83
|
}
|
|
68
84
|
});
|
|
@@ -76,13 +92,16 @@
|
|
|
76
92
|
</div>
|
|
77
93
|
{:else if route === '/login' && loginPage}
|
|
78
94
|
{@render loginPage()}
|
|
79
|
-
{:else if route === '/login'}
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
{:else if route === '/login' && authProvider}
|
|
96
|
+
<LoginPage {title} onSuccess={() => { isAuthenticated = true; navigate('/'); }} />
|
|
97
|
+
{:else if route === '/register' && authProvider?.register}
|
|
98
|
+
<RegisterPage {title} />
|
|
99
|
+
{:else if route === '/forgot-password' && authProvider?.forgotPassword}
|
|
100
|
+
<ForgotPasswordPage {title} />
|
|
101
|
+
{:else if route === '/update-password' && authProvider?.updatePassword}
|
|
102
|
+
<UpdatePasswordPage {title} />
|
|
103
|
+
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
104
|
+
<ConfigErrorScreen title="{title} — Configuration Required" />
|
|
86
105
|
{:else if isAuthenticated || !authProvider}
|
|
87
106
|
<Layout {title}>
|
|
88
107
|
{#if route === '/'}
|
|
@@ -118,4 +137,5 @@
|
|
|
118
137
|
</div>
|
|
119
138
|
{/if}
|
|
120
139
|
<Toast />
|
|
140
|
+
<DevTools />
|
|
121
141
|
</QueryClientProvider>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { useIsAuthenticated } from '@svadmin/core';
|
|
4
|
+
|
|
5
|
+
let { children, fallback, loading } = $props<{
|
|
6
|
+
children: Snippet;
|
|
7
|
+
fallback?: Snippet;
|
|
8
|
+
loading?: Snippet;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const auth = useIsAuthenticated();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
{#if auth.isLoading}
|
|
15
|
+
{#if loading}
|
|
16
|
+
{@render loading()}
|
|
17
|
+
{:else}
|
|
18
|
+
<div class="flex items-center justify-center min-h-[200px]">
|
|
19
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
20
|
+
</div>
|
|
21
|
+
{/if}
|
|
22
|
+
{:else if auth.isAuthenticated}
|
|
23
|
+
{@render children()}
|
|
24
|
+
{:else if fallback}
|
|
25
|
+
{@render fallback()}
|
|
26
|
+
{/if}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
// Form state
|
|
36
36
|
let formData = $state<Record<string, unknown>>({});
|
|
37
|
+
let fieldErrors = $state<Record<string, string>>({});
|
|
37
38
|
let submitting = $state(false);
|
|
38
39
|
let error = $state<string | null>(null);
|
|
39
40
|
let initialized = $state(false);
|
|
@@ -84,10 +85,36 @@
|
|
|
84
85
|
const createMut = useCreate(resourceName);
|
|
85
86
|
const updateMut = useUpdate(resourceName);
|
|
86
87
|
|
|
88
|
+
function validateFields(): boolean {
|
|
89
|
+
const errors: Record<string, string> = {};
|
|
90
|
+
for (const field of formFields) {
|
|
91
|
+
const value = formData[field.key];
|
|
92
|
+
// Required check
|
|
93
|
+
if (field.required) {
|
|
94
|
+
if (value === undefined || value === null || value === '') {
|
|
95
|
+
errors[field.key] = t('validation.required');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Custom per-field validator
|
|
100
|
+
if (field.validate) {
|
|
101
|
+
const msg = field.validate(value);
|
|
102
|
+
if (msg) { errors[field.key] = msg; }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fieldErrors = errors;
|
|
106
|
+
return Object.keys(errors).length === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
87
109
|
async function handleSubmit() {
|
|
88
110
|
submitting = true;
|
|
89
111
|
error = null;
|
|
90
112
|
|
|
113
|
+
if (!validateFields()) {
|
|
114
|
+
submitting = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
try {
|
|
92
119
|
const cleanData: Record<string, unknown> = {};
|
|
93
120
|
for (const field of formFields) {
|
|
@@ -115,6 +142,12 @@
|
|
|
115
142
|
function handleFieldChange(key: string, val: unknown) {
|
|
116
143
|
formData[key] = val;
|
|
117
144
|
isDirty = true;
|
|
145
|
+
// Clear field error when user starts typing
|
|
146
|
+
if (fieldErrors[key]) {
|
|
147
|
+
const next = { ...fieldErrors };
|
|
148
|
+
delete next[key];
|
|
149
|
+
fieldErrors = next;
|
|
150
|
+
}
|
|
118
151
|
}
|
|
119
152
|
|
|
120
153
|
const isLoading = $derived(mode === 'edit' && existingQuery ? existingQuery?.isLoading : false);
|
|
@@ -158,11 +191,16 @@
|
|
|
158
191
|
<Card.Root>
|
|
159
192
|
<Card.Content class="space-y-5">
|
|
160
193
|
{#each formFields as field (field.key)}
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
<div class="field-wrapper" class:has-error={fieldErrors[field.key]}>
|
|
195
|
+
<FieldRenderer
|
|
196
|
+
{field}
|
|
197
|
+
value={formData[field.key]}
|
|
198
|
+
onchange={(val: unknown) => handleFieldChange(field.key, val)}
|
|
199
|
+
/>
|
|
200
|
+
{#if fieldErrors[field.key]}
|
|
201
|
+
<p class="field-error">{fieldErrors[field.key]}</p>
|
|
202
|
+
{/if}
|
|
203
|
+
</div>
|
|
166
204
|
{/each}
|
|
167
205
|
</Card.Content>
|
|
168
206
|
</Card.Root>
|
|
@@ -190,3 +228,17 @@
|
|
|
190
228
|
</form>
|
|
191
229
|
{/if}
|
|
192
230
|
</div>
|
|
231
|
+
|
|
232
|
+
<style>
|
|
233
|
+
.field-error {
|
|
234
|
+
color: hsl(var(--destructive));
|
|
235
|
+
font-size: 0.8125rem;
|
|
236
|
+
margin-top: 0.25rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.field-wrapper.has-error :global(input),
|
|
240
|
+
.field-wrapper.has-error :global(textarea),
|
|
241
|
+
.field-wrapper.has-error :global(select) {
|
|
242
|
+
border-color: hsl(var(--destructive));
|
|
243
|
+
}
|
|
244
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { canAccess } from '@svadmin/core/permissions';
|
|
4
|
+
import type { Action } from '@svadmin/core/permissions';
|
|
5
|
+
|
|
6
|
+
let { resource, action, params, children, fallback } = $props<{
|
|
7
|
+
resource: string;
|
|
8
|
+
action: Action;
|
|
9
|
+
params?: Record<string, unknown>;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
fallback?: Snippet;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const result = $derived(canAccess(resource, action, params));
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#if result.can}
|
|
18
|
+
{@render children()}
|
|
19
|
+
{:else if fallback}
|
|
20
|
+
{@render fallback()}
|
|
21
|
+
{/if}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import { AlertTriangle, Copy, CheckCircle } from 'lucide-svelte';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import * as Card from './ui/card/index.js';
|
|
6
|
+
|
|
7
|
+
let { title = 'Configuration Required', missingVars = [], envTemplate = '' } = $props<{
|
|
8
|
+
title?: string;
|
|
9
|
+
missingVars?: { key: string; description?: string }[];
|
|
10
|
+
envTemplate?: string;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
let copied = $state<Record<string, boolean>>({});
|
|
14
|
+
|
|
15
|
+
async function copyToClipboard(text: string, key: string) {
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(text);
|
|
18
|
+
copied = { ...copied, [key]: true };
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
copied = { ...copied, [key]: false };
|
|
21
|
+
}, 2000);
|
|
22
|
+
} catch {
|
|
23
|
+
console.warn('[svadmin] clipboard API unavailable');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function copyAll() {
|
|
28
|
+
const text = envTemplate || missingVars.map((v: { key: string }) => `${v.key}=`).join('\n');
|
|
29
|
+
await copyToClipboard(text, '__all__');
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
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">
|
|
38
|
+
<AlertTriangle class="h-7 w-7" />
|
|
39
|
+
</div>
|
|
40
|
+
<Card.CardTitle class="text-xl font-bold">{title}</Card.CardTitle>
|
|
41
|
+
<p class="text-sm text-muted-foreground">
|
|
42
|
+
{t('config.missingEnvDescription')}
|
|
43
|
+
</p>
|
|
44
|
+
</Card.CardHeader>
|
|
45
|
+
<Card.CardContent class="space-y-4">
|
|
46
|
+
{#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>
|
|
52
|
+
{#if v.description}
|
|
53
|
+
<span class="env-var-desc">{v.description}</span>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
<button
|
|
57
|
+
class="copy-btn"
|
|
58
|
+
onclick={() => copyToClipboard(`${v.key}=`, v.key)}
|
|
59
|
+
title="Copy"
|
|
60
|
+
>
|
|
61
|
+
{#if copied[v.key]}
|
|
62
|
+
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
63
|
+
{:else}
|
|
64
|
+
<Copy class="h-3.5 w-3.5" />
|
|
65
|
+
{/if}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
{/each}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
{#if envTemplate}
|
|
73
|
+
<div class="env-template">
|
|
74
|
+
<div class="env-template-header">
|
|
75
|
+
<span class="text-xs font-medium text-muted-foreground">{t('config.envFilePath')}</span>
|
|
76
|
+
<button
|
|
77
|
+
class="copy-btn"
|
|
78
|
+
onclick={copyAll}
|
|
79
|
+
>
|
|
80
|
+
{#if copied['__all__']}
|
|
81
|
+
<CheckCircle class="h-3.5 w-3.5 text-green-500" />
|
|
82
|
+
<span class="text-xs">Copied!</span>
|
|
83
|
+
{:else}
|
|
84
|
+
<Copy class="h-3.5 w-3.5" />
|
|
85
|
+
<span class="text-xs">Copy All</span>
|
|
86
|
+
{/if}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
<pre class="env-template-code">{envTemplate}</pre>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<p class="text-xs text-muted-foreground text-center mt-4">
|
|
94
|
+
{t('config.reload')}
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
<Button variant="outline" class="w-full" onclick={() => window.location.reload()}>
|
|
98
|
+
Reload Page
|
|
99
|
+
</Button>
|
|
100
|
+
</Card.CardContent>
|
|
101
|
+
</Card.Card>
|
|
102
|
+
</div>
|
|
103
|
+
</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>
|