@svadmin/ui 0.32.1 → 0.32.4

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.
@@ -91,8 +91,8 @@
91
91
 
92
92
  setComponentRegistry({ ...defaultComponents, ...userComponents });
93
93
 
94
- // Resolve router provider (default to hash)
95
- const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
94
+ const defaultRouter = createHashRouterProvider();
95
+ const resolvedRouter = $derived(routerProvider ?? defaultRouter);
96
96
  const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
97
97
 
98
98
  // Set up context — use $effect.pre so prop changes are tracked and children can access during first render
@@ -23,7 +23,7 @@
23
23
  let drawerOpen = $state(false);
24
24
  let drawerLog = $state<AuditLog | null>(null);
25
25
 
26
- let cancelled = $state(false);
26
+ let requestId = 0;
27
27
 
28
28
  async function loadLogs() {
29
29
  if (!authProvider?.getAuditLogs) {
@@ -31,27 +31,25 @@
31
31
  loading = false;
32
32
  return;
33
33
  }
34
+ const currentId = ++requestId;
34
35
  loading = true;
35
36
  error = null;
36
37
  try {
37
38
  const result = await authProvider.getAuditLogs({ page, pageSize });
38
- if (cancelled) return;
39
+ if (currentId !== requestId) return;
39
40
  logs = result.data;
40
41
  total = result.total;
41
42
  } catch (e) {
42
- if (cancelled) return;
43
+ if (currentId !== requestId) return;
43
44
  toast.error((e as Error).message);
44
45
  } finally {
45
- if (!cancelled) loading = false;
46
+ if (currentId === requestId) loading = false;
46
47
  }
47
48
  }
48
49
 
49
50
  $effect(() => {
50
51
  void page;
51
- cancelled = true;
52
- cancelled = false;
53
52
  loadLogs();
54
- return () => { cancelled = true; };
55
53
  });
56
54
 
57
55
  let totalPages = $derived(Math.ceil(total / pageSize) || 1);
@@ -22,9 +22,10 @@
22
22
  fieldRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; onchange: (v: unknown) => void }]>;
23
23
  formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
24
24
  headerContent?: Snippet;
25
+ onSuccess?: () => void;
25
26
  }
26
27
 
27
- let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent }: Props = $props();
28
+ let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent, onSuccess }: Props = $props();
28
29
  const isReadonly = $derived(mode === 'show');
29
30
 
30
31
  // ─── Resource metadata ────────────────────────────────────────────
@@ -104,6 +105,7 @@
104
105
  submitError = null;
105
106
  try {
106
107
  await form.submit();
108
+ onSuccess?.();
107
109
  } catch (e) {
108
110
  submitError = e instanceof Error ? e.message : t('common.operationFailed');
109
111
  }
@@ -14,6 +14,7 @@ interface Props {
14
14
  onSubmit: () => void;
15
15
  }]>;
16
16
  headerContent?: Snippet;
17
+ onSuccess?: () => void;
17
18
  }
18
19
  declare const AutoForm: import("svelte").Component<Props, {}, "">;
19
20
  type AutoForm = ReturnType<typeof AutoForm>;
@@ -11,7 +11,7 @@
11
11
  } from '@tanstack/svelte-table';
12
12
  import { getCoreRowModel } from '@tanstack/table-core';
13
13
 
14
- import { useList, useDelete, getResource, notify } from '@svadmin/core';
14
+ import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
15
15
  import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
16
16
  import { navigate } from '@svadmin/core/router';
17
17
  import { useCan, getAccessControlProvider } from '@svadmin/core';
@@ -170,6 +170,8 @@
170
170
  const query = listResult;
171
171
  const deleteResult = useDelete({ get resource() { return resourceName; } });
172
172
  const deleteMutation = deleteResult.mutation;
173
+ const deleteManyResult = useDeleteMany({ get resource() { return resourceName; } });
174
+ const deleteManyMutation = deleteManyResult.mutation;
173
175
 
174
176
  // ─── Permissions ──────────────────────────────────────────────
175
177
  const acEnabled = $derived(!!getAccessControlProvider());
@@ -347,13 +349,11 @@
347
349
  const ids = Object.keys(rowSelection);
348
350
  confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
349
351
  confirmAction = async () => {
350
- const results = await Promise.allSettled(ids.map(id => deleteMutation.mutateAsync({ id, resource: resourceName, successNotification: false })));
351
- const succeeded = results.filter(r => r.status === 'fulfilled').length;
352
- const failed = results.filter(r => r.status === 'rejected');
353
- if (failed.length > 0) {
354
- notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: failed.length, total: ids.length }) });
355
- } else {
356
- notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: succeeded }) });
352
+ try {
353
+ await deleteManyMutation.mutateAsync({ ids, resource: resourceName, successNotification: false });
354
+ notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: ids.length }) });
355
+ } catch {
356
+ notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
357
357
  }
358
358
  rowSelection = {};
359
359
  confirmOpen = false;
@@ -374,13 +374,13 @@
374
374
  return String(val);
375
375
  })
376
376
  );
377
- const csv = [headers.join(','), ...rows.map(r => r.map(v => `"${v.replace(new RegExp('"', 'g'), '""')}"`).join(','))].join('\n');
377
+ const csv = [headers.map(h => `"${h.replace(/"/g, '""')}"`).join(','), ...rows.map(r => r.map(v => `"${v.replace(/"/g, '""')}"`).join(','))].join('\n');
378
378
  const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
379
379
  const link = document.createElement('a');
380
380
  link.href = URL.createObjectURL(blob);
381
381
  link.download = `${resourceName}_${new Date().toISOString().slice(0, 10)}.csv`;
382
382
  link.click();
383
- URL.revokeObjectURL(link.href);
383
+ setTimeout(() => URL.revokeObjectURL(link.href), 1000);
384
384
  }
385
385
 
386
386
  function goToPage(page: number) {
@@ -5,7 +5,7 @@
5
5
  import { t } from '@svadmin/core/i18n';
6
6
  import * as Breadcrumb from './ui/breadcrumb/index.js';
7
7
 
8
- const resources = getResources();
8
+ const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
9
9
 
10
10
  interface Crumb { label: string; href: string; }
11
11
 
@@ -89,26 +89,45 @@
89
89
 
90
90
  // ─── Persist on change (debounced) ──────────────────────────
91
91
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
92
+ let lastPersistSnapshot: ChatMessage[] | null = null;
93
+
94
+ function flushPersist(snapshot: ChatMessage[]) {
95
+ if (onPersist) {
96
+ onPersist(snapshot);
97
+ } else if (persistKey) {
98
+ try {
99
+ localStorage.setItem(persistKey, JSON.stringify(snapshot));
100
+ } catch {
101
+ // storage full or unavailable
102
+ }
103
+ }
104
+ }
92
105
 
93
106
  $effect(() => {
94
- // Access `messages` to subscribe to changes
95
107
  const snapshot = messages;
96
108
  if (!initialized) return;
97
109
 
98
110
  if (persistTimer) clearTimeout(persistTimer);
111
+ lastPersistSnapshot = snapshot;
99
112
  persistTimer = setTimeout(() => {
100
- if (onPersist) {
101
- onPersist(snapshot);
102
- } else if (persistKey) {
103
- try {
104
- localStorage.setItem(persistKey, JSON.stringify(snapshot));
105
- } catch {
106
- // storage full or unavailable
107
- }
108
- }
113
+ persistTimer = null;
114
+ flushPersist(snapshot);
109
115
  }, 300);
110
116
 
111
- return () => { if (persistTimer) clearTimeout(persistTimer); };
117
+ return () => {
118
+ if (persistTimer) {
119
+ clearTimeout(persistTimer);
120
+ persistTimer = null;
121
+ }
122
+ };
123
+ });
124
+
125
+ $effect(() => {
126
+ return () => {
127
+ if (lastPersistSnapshot != null) {
128
+ flushPersist(lastPersistSnapshot);
129
+ }
130
+ };
112
131
  });
113
132
 
114
133
  function genId(): string {
@@ -192,6 +211,7 @@
192
211
  const collectedActions: ChatAction[] = [];
193
212
 
194
213
  for await (const event of stream) {
214
+ if (!isStreaming) break;
195
215
  switch (event.type) {
196
216
  case 'text':
197
217
  assistantMsg.content += event.content;
@@ -216,7 +236,6 @@
216
236
  break;
217
237
 
218
238
  case 'approval_request': {
219
- // Register in the core approval system
220
239
  const reqId = event.id;
221
240
  pendingApprovalIds = [...pendingApprovalIds, reqId];
222
241
 
@@ -227,7 +246,9 @@
227
246
  content: approved ? `User approved execution of tool '${event.tool}'` : `User rejected execution of tool '${event.tool}'`,
228
247
  timestamp: Date.now()
229
248
  }];
230
- doSend();
249
+ if (agent?.approveToolCall) {
250
+ agent.approveToolCall(reqId, approved);
251
+ }
231
252
  });
232
253
 
233
254
  collectedActions.push(
@@ -269,6 +290,7 @@
269
290
 
270
291
  if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
271
292
  for await (const chunk of result as AsyncGenerator<string>) {
293
+ if (!isStreaming) break;
272
294
  assistantMsg.content += chunk;
273
295
  messages = [...messages.slice(0, -1), { ...assistantMsg }];
274
296
  scrollToBottom();
@@ -359,12 +381,22 @@
359
381
 
360
382
  /** Simple markdown→html: bold, italic, code blocks, inline code, line breaks */
361
383
  function renderMarkdown(text: string): string {
362
- return escapeHtml(text)
363
- .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="chat-code-block"><code>$2</code></pre>')
384
+ const escaped = escapeHtml(text);
385
+ const codeBlocks: string[] = [];
386
+ let processed = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
387
+ const idx = codeBlocks.length;
388
+ codeBlocks.push(`<pre class="chat-code-block"><code>${code}</code></pre>`);
389
+ return `\x00CODEBLOCK${idx}\x00`;
390
+ });
391
+ processed = processed
364
392
  .replace(/`([^`]+)`/g, '<code class="chat-inline-code">$1</code>')
365
393
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
366
394
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
367
395
  .replace(/\n/g, '<br>');
396
+ for (let i = 0; i < codeBlocks.length; i++) {
397
+ processed = processed.replace(`\x00CODEBLOCK${i}\x00`, codeBlocks[i]);
398
+ }
399
+ return processed;
368
400
  }
369
401
 
370
402
  const suggestions = $derived([
@@ -9,6 +9,14 @@
9
9
  import { Skeleton } from './ui/skeleton/index.js';
10
10
  import { ChevronsUpDown, Check, X, Search } from '@lucide/svelte';
11
11
 
12
+ function clickOutside(node: HTMLElement) {
13
+ const handler = (e: MouseEvent) => {
14
+ if (!node.contains(e.target as Node)) open = false;
15
+ };
16
+ document.addEventListener('click', handler, true);
17
+ return { destroy() { document.removeEventListener('click', handler, true); } };
18
+ }
19
+
12
20
  interface Props {
13
21
  resource: string;
14
22
  value?: string | number | null;
@@ -60,6 +68,8 @@
60
68
  function handleClear(e: Event) {
61
69
  e.stopPropagation();
62
70
  onchange?.(null);
71
+ open = false;
72
+ searchInputValue = '';
63
73
  }
64
74
 
65
75
  function handleSearchInput(e: Event) {
@@ -69,7 +79,7 @@
69
79
  }
70
80
  </script>
71
81
 
72
- <div class="relative">
82
+ <div class="relative" use:clickOutside>
73
83
  <Button
74
84
  variant="outline"
75
85
  type="button"
@@ -50,7 +50,7 @@
50
50
  <AlertDialog.Footer>
51
51
  <AlertDialog.Cancel>
52
52
  {#snippet child({ props })}
53
- <Button variant="outline" {...props} onclick={oncancel}>
53
+ <Button variant="outline" {...props}>
54
54
  {resolvedCancelText}
55
55
  </Button>
56
56
  {/snippet}
@@ -15,7 +15,7 @@
15
15
 
16
16
  const resource = $derived(getResource(resourceName));
17
17
 
18
- function handleClose() {
18
+ function handleSuccess() {
19
19
  open = false;
20
20
  onSuccess?.();
21
21
  }
@@ -29,7 +29,7 @@
29
29
  </Sheet.Title>
30
30
  </Sheet.Header>
31
31
  <div class="flex-1 overflow-y-auto">
32
- <AutoForm {resourceName} {mode} {id} />
32
+ <AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
33
33
  </div>
34
34
  </Sheet.Content>
35
35
  </Sheet.Root>
@@ -30,11 +30,18 @@
30
30
 
31
31
  // Typed accessors
32
32
  const strVal = $derived((value as string) ?? '');
33
- const numVal = $derived((value as number) ?? 0);
33
+ const numVal = $derived(value as number | null | undefined);
34
34
  const boolVal = $derived((value as boolean) ?? false);
35
35
  const tagsVal = $derived((value as string[]) ?? []);
36
36
  const multiVal = $derived((value as (string | number)[]) ?? []);
37
37
  const imagesVal = $derived((value as string[]) ?? []);
38
+ let jsonEditText = $state('');
39
+ let jsonEditing = false;
40
+
41
+ $effect(() => {
42
+ if (jsonEditing) return;
43
+ jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
44
+ });
38
45
 
39
46
  function handleTagKeydown(e: KeyboardEvent) {
40
47
  if (e.key === 'Enter') {
@@ -165,7 +172,7 @@
165
172
  <Input
166
173
  id={field.key}
167
174
  type="number"
168
- value={String(numVal)}
175
+ value={numVal == null ? '' : String(numVal)}
169
176
  oninput={(e) => {
170
177
  const v = (e.target as HTMLInputElement).value;
171
178
  onchange(v === '' ? null : Number(v));
@@ -359,10 +366,17 @@
359
366
  {:else if field.type === 'json'}
360
367
  <Textarea
361
368
  id={field.key}
362
- value={typeof value === 'string' ? strVal : JSON.stringify(value, null, 2)}
369
+ value={jsonEditText}
370
+ onfocus={() => { jsonEditing = true; }}
371
+ onblur={() => {
372
+ jsonEditing = false;
373
+ jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
374
+ }}
363
375
  oninput={(e) => {
376
+ const raw = (e.target as HTMLTextAreaElement).value;
377
+ jsonEditText = raw;
364
378
  try {
365
- onchange(JSON.parse((e.target as HTMLTextAreaElement).value));
379
+ onchange(JSON.parse(raw));
366
380
  } catch {
367
381
  // keep raw text until valid JSON
368
382
  }
@@ -17,6 +17,7 @@
17
17
  let editValue = $state('');
18
18
  let inputRef = $state<HTMLInputElement | null>(null);
19
19
  let saving = $state(false);
20
+ let savePending = false;
20
21
  const displayValue = $derived(String(value ?? ''));
21
22
 
22
23
  const { mutation } = useUpdate({ get resource() { return resourceName; } });
@@ -31,12 +32,13 @@
31
32
  }
32
33
 
33
34
  async function save() {
34
- if (!editing || saving) return;
35
- const newValue = field.type === 'number' ? Number(editValue) : editValue;
35
+ if (!editing || saving || savePending) return;
36
+ const newValue = field.type === 'number' ? (editValue.trim() === '' ? null : (() => { const n = Number(editValue); return isNaN(n) ? value : n; })()) : editValue;
36
37
  if (newValue === value) {
37
38
  editing = false;
38
39
  return;
39
40
  }
41
+ savePending = true;
40
42
  saving = true;
41
43
  try {
42
44
  await mutation.mutateAsync({
@@ -50,6 +52,7 @@
50
52
  // Keep editing on error
51
53
  } finally {
52
54
  saving = false;
55
+ savePending = false;
53
56
  }
54
57
  }
55
58
 
@@ -59,9 +62,16 @@
59
62
  }
60
63
 
61
64
  function handleKeydown(e: KeyboardEvent) {
62
- if (e.key === 'Enter') save();
65
+ if (e.key === 'Enter') {
66
+ e.preventDefault();
67
+ save();
68
+ }
63
69
  if (e.key === 'Escape') cancel();
64
70
  }
71
+
72
+ function handleBlur() {
73
+ if (!savePending) save();
74
+ }
65
75
  </script>
66
76
 
67
77
  {#if editing}
@@ -70,7 +80,7 @@
70
80
  type={field.type === 'number' ? 'number' : 'text'}
71
81
  bind:value={editValue}
72
82
  onkeydown={handleKeydown}
73
- onblur={save}
83
+ onblur={handleBlur}
74
84
  disabled={saving}
75
85
  class="h-7 w-full rounded border bg-background px-2 text-sm outline-none ring-1 ring-primary/50 focus:ring-2 focus:ring-primary transition-all"
76
86
  />
@@ -52,9 +52,13 @@
52
52
 
53
53
  async function handleLogout() {
54
54
  if (!auth) return;
55
- const result = await auth.logout();
56
- if (result.success) {
57
- navigate(result.redirectTo ?? '/login');
55
+ try {
56
+ const result = await auth.logout();
57
+ if (result.success) {
58
+ navigate(result.redirectTo ?? '/login');
59
+ }
60
+ } catch {
61
+ navigate('/login');
58
62
  }
59
63
  }
60
64
 
@@ -18,7 +18,7 @@
18
18
  const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
19
19
  const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
20
20
 
21
- function handleClose() {
21
+ function handleSuccess() {
22
22
  open = false;
23
23
  onSuccess?.();
24
24
  }
@@ -32,7 +32,7 @@
32
32
  {mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
33
33
  </Dialog.DialogTitle>
34
34
  </Dialog.DialogHeader>
35
- <AutoForm {resourceName} {mode} {id} />
35
+ <AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
36
36
  </Dialog.DialogContent>
37
37
  </Dialog.Dialog>
38
38
  {/if}
@@ -39,8 +39,7 @@
39
39
  function isActive(itemHref: string | undefined): boolean {
40
40
  if (!itemHref) return false;
41
41
  if (itemHref === '/') return currentPath === '/';
42
- // Remove hash/leading slash for comparison if necessary, but currentPath usually starts with /
43
- return currentPath.startsWith(itemHref);
42
+ return currentPath.startsWith(itemHref) && (currentPath.length === itemHref.length || currentPath[itemHref.length] === '/');
44
43
  }
45
44
 
46
45
  /** Check if any child (recursively) is active — used to auto-open parent groups */
@@ -61,6 +61,7 @@
61
61
  get action() { return mode; },
62
62
  get id() { return id; },
63
63
  get defaultValues() { return defaults; },
64
+ stepsCount: steps.length,
64
65
  redirect: 'list',
65
66
  warnWhenUnsavedChanges: true,
66
67
  get validate() { return validator; },
@@ -12,18 +12,16 @@
12
12
  onTimeout: () => void;
13
13
  }>();
14
14
 
15
- let remaining = $state(0);
15
+ let remaining = $state(duration);
16
16
  let dismissed = $state(false);
17
17
 
18
- // Initialize remaining from prop
19
- $effect.pre(() => {
20
- remaining = duration;
21
- });
22
-
23
18
  $effect(() => {
19
+ const startTime = Date.now();
20
+ const totalMs = duration;
24
21
  const interval = setInterval(() => {
25
- remaining -= 100;
22
+ remaining = totalMs - (Date.now() - startTime);
26
23
  if (remaining <= 0) {
24
+ remaining = 0;
27
25
  clearInterval(interval);
28
26
  if (!dismissed) {
29
27
  dismissed = true;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { ExternalLink } from '@lucide/svelte';
3
+ import { getRouterProvider } from '@svadmin/core';
3
4
 
4
5
  let { value, resourceName, displayValue } = $props<{
5
6
  value: string | number | null | undefined;
@@ -8,7 +9,12 @@
8
9
  }>();
9
10
 
10
11
  const display = $derived(displayValue ?? (value != null ? `#${value}` : '—'));
11
- const href = $derived(resourceName && value != null ? `#/${resourceName}/show/${value}` : undefined);
12
+ const href = $derived.by(() => {
13
+ if (!resourceName || value == null) return undefined;
14
+ const router = getRouterProvider();
15
+ const path = `/${resourceName}/show/${value}`;
16
+ return router?.formatLink?.(path) ?? path;
17
+ });
12
18
  </script>
13
19
 
14
20
  {#if href}
package/package.json CHANGED
@@ -1,31 +1,32 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.32.1",
3
+ "version": "0.32.4",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
7
7
  "./src/app.css"
8
8
  ],
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "src/app.css"
11
12
  ],
12
13
  "main": "dist/index.js",
13
14
  "types": "dist/index.d.ts",
14
15
  "exports": {
15
16
  ".": {
16
17
  "types": "./dist/index.d.ts",
17
- "default": "./dist/index.js",
18
- "svelte": "./dist/index.js"
18
+ "svelte": "./src/index.ts",
19
+ "default": "./dist/index.js"
19
20
  },
20
21
  "./app.css": {
21
- "style": "./dist/app.css",
22
- "default": "./dist/app.css",
23
- "svelte": "./dist/app.css"
22
+ "svelte": "./src/app.css",
23
+ "style": "./src/app.css",
24
+ "default": "./dist/app.css"
24
25
  },
25
26
  "./components/*": "./dist/components/*"
26
27
  },
27
28
  "peerDependencies": {
28
- "@svadmin/core": "^0.23.1",
29
+ "@svadmin/core": "^0.23.5",
29
30
  "@tanstack/svelte-query": "^6.0.0",
30
31
  "highlight.js": "^11.11.1",
31
32
  "isomorphic-dompurify": "^3.7.1",
@@ -48,6 +49,15 @@
48
49
  },
49
50
  "@svadmin/editor": {
50
51
  "optional": true
52
+ },
53
+ "@svadmin/core": {
54
+ "optional": true
55
+ },
56
+ "@tanstack/svelte-query": {
57
+ "optional": true
58
+ },
59
+ "svelte": {
60
+ "optional": true
51
61
  }
52
62
  },
53
63
  "dependencies": {
package/src/app.css ADDED
@@ -0,0 +1,674 @@
1
+ /* Typography — Inter font for clean, modern look */
2
+
3
+ /* @svadmin/ui — Design Tokens (Elygate-inspired)
4
+ * Auto-imported by AdminApp. Can be overridden by consumer's CSS.
5
+ *
6
+ * Based on Elygate dashboard style: Indigo primary, light glassmorphism,
7
+ * professional admin aesthetic with subtle depth.
8
+ */
9
+
10
+ @layer base {
11
+ /* Global border-color reset — ensures Tailwind border utilities
12
+ * (border-r, border-t, etc.) use the theme color instead of currentColor */
13
+ *,
14
+ ::after,
15
+ ::before {
16
+ border-color: var(--border);
17
+ }
18
+
19
+ :root {
20
+ --radius: 0.75rem;
21
+ --background: oklch(0.975 0.005 264);
22
+ --foreground: oklch(0.13 0.02 265);
23
+ --card: oklch(1 0 0);
24
+ --card-foreground: oklch(0.13 0.02 265);
25
+ --popover: oklch(1 0 0);
26
+ --popover-foreground: oklch(0.13 0.02 265);
27
+ --primary: oklch(0.488 0.243 264.376);
28
+ --primary-foreground: oklch(0.985 0 0);
29
+ --secondary: oklch(0.955 0.008 264);
30
+ --secondary-foreground: oklch(0.2 0.04 265);
31
+ --muted: oklch(0.955 0.005 264);
32
+ --muted-foreground: oklch(0.45 0.03 260);
33
+ --accent: oklch(0.955 0.008 264);
34
+ --accent-foreground: oklch(0.2 0.04 265);
35
+ --destructive: oklch(0.577 0.245 27.325);
36
+ --destructive-foreground: oklch(0.985 0 0);
37
+ --border: oklch(0.935 0.005 264);
38
+ --input: oklch(0.935 0.005 264);
39
+ --ring: oklch(0.488 0.243 264.376);
40
+ --chart-1: oklch(0.488 0.243 264.376);
41
+ --chart-2: oklch(0.6 0.118 184.704);
42
+ --chart-3: oklch(0.398 0.07 227.392);
43
+ --chart-4: oklch(0.828 0.189 84.429);
44
+ --chart-5: oklch(0.769 0.188 70.08);
45
+ --success: oklch(0.6 0.2 145);
46
+ --success-foreground: oklch(0.985 0 0);
47
+ --warning: oklch(0.75 0.18 85);
48
+ --warning-foreground: oklch(0.3 0.08 60);
49
+ --info: oklch(0.6 0.15 250);
50
+ --info-foreground: oklch(0.985 0 0);
51
+ --sidebar: oklch(1 0 0 / 80%);
52
+ --sidebar-foreground: oklch(0.13 0.02 265);
53
+ --sidebar-primary: oklch(0.488 0.243 264.376);
54
+ --sidebar-primary-foreground: oklch(0.985 0 0);
55
+ --sidebar-accent: oklch(0.94 0.025 264);
56
+ --sidebar-accent-foreground: oklch(0.33 0.17 264);
57
+ --sidebar-border: oklch(0.935 0.005 264);
58
+ --sidebar-ring: oklch(0.488 0.243 264.376);
59
+ }
60
+
61
+ .dark {
62
+ --background: oklch(0.13 0.015 265);
63
+ --foreground: oklch(0.985 0 0);
64
+ --card: oklch(0.16 0.015 265);
65
+ --card-foreground: oklch(0.985 0 0);
66
+ --popover: oklch(0.16 0.015 265);
67
+ --popover-foreground: oklch(0.985 0 0);
68
+ --primary: oklch(0.585 0.233 264);
69
+ --primary-foreground: oklch(0.15 0.04 265);
70
+ --secondary: oklch(0.23 0.025 265);
71
+ --secondary-foreground: oklch(0.985 0 0);
72
+ --muted: oklch(0.23 0.025 265);
73
+ --muted-foreground: oklch(0.7 0.02 260);
74
+ --accent: oklch(0.23 0.025 265);
75
+ --accent-foreground: oklch(0.985 0 0);
76
+ --destructive: oklch(0.704 0.191 22.216);
77
+ --destructive-foreground: oklch(0.985 0 0);
78
+ --border: oklch(0.27 0.02 265);
79
+ --input: oklch(0.27 0.02 265);
80
+ --ring: oklch(0.55 0.18 264);
81
+ --chart-1: oklch(0.585 0.233 264);
82
+ --chart-2: oklch(0.696 0.17 162.48);
83
+ --chart-3: oklch(0.769 0.188 70.08);
84
+ --chart-4: oklch(0.627 0.265 303.9);
85
+ --chart-5: oklch(0.645 0.246 16.439);
86
+ --success: oklch(0.65 0.2 150);
87
+ --success-foreground: oklch(0.985 0 0);
88
+ --warning: oklch(0.8 0.15 85);
89
+ --warning-foreground: oklch(0.25 0.05 60);
90
+ --info: oklch(0.65 0.15 250);
91
+ --info-foreground: oklch(0.985 0 0);
92
+ --sidebar: oklch(0.16 0.015 265 / 80%);
93
+ --sidebar-foreground: oklch(0.985 0 0);
94
+ --sidebar-primary: oklch(0.585 0.233 264);
95
+ --sidebar-primary-foreground: oklch(0.985 0 0);
96
+ --sidebar-accent: oklch(0.488 0.243 264.376 / 10%);
97
+ --sidebar-accent-foreground: oklch(0.7 0.15 264);
98
+ --sidebar-border: oklch(0.27 0.02 265);
99
+ --sidebar-ring: oklch(0.55 0.18 264);
100
+ }
101
+
102
+ /* ── Color Theme Overrides ──────────────────────────────
103
+ * Each [data-theme] selector overrides the primary-related
104
+ * CSS variables for both light and dark mode.
105
+ * Values match builtinPresets in @svadmin/core/theme.
106
+ * This CSS-level fallback prevents FOUC before JS inits.
107
+ */
108
+
109
+ /* Neutral */
110
+ [data-theme="neutral"] {
111
+ --primary: oklch(0.205 0.006 286);
112
+ --primary-foreground: oklch(0.985 0 0);
113
+ --ring: oklch(0.205 0.006 286);
114
+ --sidebar-primary: oklch(0.205 0.006 286);
115
+ --sidebar-primary-foreground: oklch(0.985 0 0);
116
+ --sidebar-ring: oklch(0.205 0.006 286);
117
+ --chart-1: oklch(0.488 0.243 264.376);
118
+ }
119
+ .dark[data-theme="neutral"],
120
+ .dark [data-theme="neutral"] {
121
+ --primary: oklch(0.922 0.004 286);
122
+ --primary-foreground: oklch(0.205 0.006 286);
123
+ --ring: oklch(0.556 0.004 286);
124
+ --sidebar-primary: oklch(0.922 0.004 286);
125
+ --sidebar-primary-foreground: oklch(0.205 0.006 286);
126
+ --sidebar-ring: oklch(0.556 0.004 286);
127
+ --chart-1: oklch(0.585 0.233 264);
128
+ }
129
+
130
+ /* Indigo (default) */
131
+ [data-theme="indigo"] {
132
+ --primary: oklch(0.488 0.243 264.376);
133
+ --primary-foreground: oklch(0.985 0 0);
134
+ --ring: oklch(0.488 0.243 264.376);
135
+ --sidebar-primary: oklch(0.488 0.243 264.376);
136
+ --sidebar-primary-foreground: oklch(0.985 0 0);
137
+ --sidebar-ring: oklch(0.488 0.243 264.376);
138
+ --chart-1: oklch(0.488 0.243 264.376);
139
+ }
140
+ .dark[data-theme="indigo"],
141
+ .dark [data-theme="indigo"] {
142
+ --primary: oklch(0.585 0.233 264);
143
+ --primary-foreground: oklch(0.15 0.04 265);
144
+ --ring: oklch(0.55 0.18 264);
145
+ --sidebar-primary: oklch(0.585 0.233 264);
146
+ --sidebar-primary-foreground: oklch(0.985 0 0);
147
+ --sidebar-ring: oklch(0.55 0.18 264);
148
+ --chart-1: oklch(0.585 0.233 264);
149
+ }
150
+
151
+ /* Blue */
152
+ [data-theme="blue"] {
153
+ --primary: oklch(0.546 0.245 262.881);
154
+ --primary-foreground: oklch(0.985 0 0);
155
+ --ring: oklch(0.546 0.245 262.881);
156
+ --sidebar-primary: oklch(0.546 0.245 262.881);
157
+ --sidebar-primary-foreground: oklch(0.985 0 0);
158
+ --sidebar-ring: oklch(0.546 0.245 262.881);
159
+ --chart-1: oklch(0.546 0.245 262.881);
160
+ }
161
+ .dark[data-theme="blue"],
162
+ .dark [data-theme="blue"] {
163
+ --primary: oklch(0.623 0.214 259.815);
164
+ --primary-foreground: oklch(0.15 0.04 265);
165
+ --ring: oklch(0.546 0.245 262.881);
166
+ --sidebar-primary: oklch(0.623 0.214 259.815);
167
+ --sidebar-primary-foreground: oklch(0.985 0 0);
168
+ --sidebar-ring: oklch(0.546 0.245 262.881);
169
+ --chart-1: oklch(0.623 0.214 259.815);
170
+ }
171
+
172
+ /* Green */
173
+ [data-theme="green"] {
174
+ --primary: oklch(0.527 0.185 150.069);
175
+ --primary-foreground: oklch(0.985 0 0);
176
+ --ring: oklch(0.527 0.185 150.069);
177
+ --sidebar-primary: oklch(0.527 0.185 150.069);
178
+ --sidebar-primary-foreground: oklch(0.985 0 0);
179
+ --sidebar-ring: oklch(0.527 0.185 150.069);
180
+ --chart-1: oklch(0.527 0.185 150.069);
181
+ }
182
+ .dark[data-theme="green"],
183
+ .dark [data-theme="green"] {
184
+ --primary: oklch(0.627 0.194 149.214);
185
+ --primary-foreground: oklch(0.15 0.05 150);
186
+ --ring: oklch(0.527 0.185 150.069);
187
+ --sidebar-primary: oklch(0.627 0.194 149.214);
188
+ --sidebar-primary-foreground: oklch(0.985 0 0);
189
+ --sidebar-ring: oklch(0.527 0.185 150.069);
190
+ --chart-1: oklch(0.627 0.194 149.214);
191
+ }
192
+
193
+ /* Rose */
194
+ [data-theme="rose"] {
195
+ --primary: oklch(0.577 0.245 27.325);
196
+ --primary-foreground: oklch(0.985 0 0);
197
+ --ring: oklch(0.577 0.245 27.325);
198
+ --sidebar-primary: oklch(0.577 0.245 27.325);
199
+ --sidebar-primary-foreground: oklch(0.985 0 0);
200
+ --sidebar-ring: oklch(0.577 0.245 27.325);
201
+ --chart-1: oklch(0.577 0.245 27.325);
202
+ }
203
+ .dark[data-theme="rose"],
204
+ .dark [data-theme="rose"] {
205
+ --primary: oklch(0.577 0.245 27.325);
206
+ --primary-foreground: oklch(0.985 0 0);
207
+ --ring: oklch(0.577 0.245 27.325);
208
+ --sidebar-primary: oklch(0.577 0.245 27.325);
209
+ --sidebar-primary-foreground: oklch(0.985 0 0);
210
+ --sidebar-ring: oklch(0.577 0.245 27.325);
211
+ --chart-1: oklch(0.577 0.245 27.325);
212
+ }
213
+
214
+ /* Orange */
215
+ [data-theme="orange"] {
216
+ --primary: oklch(0.646 0.222 41.116);
217
+ --primary-foreground: oklch(0.985 0 0);
218
+ --ring: oklch(0.646 0.222 41.116);
219
+ --sidebar-primary: oklch(0.646 0.222 41.116);
220
+ --sidebar-primary-foreground: oklch(0.985 0 0);
221
+ --sidebar-ring: oklch(0.646 0.222 41.116);
222
+ --chart-1: oklch(0.646 0.222 41.116);
223
+ }
224
+ .dark[data-theme="orange"],
225
+ .dark [data-theme="orange"] {
226
+ --primary: oklch(0.646 0.222 41.116);
227
+ --primary-foreground: oklch(0.985 0 0);
228
+ --ring: oklch(0.646 0.222 41.116);
229
+ --sidebar-primary: oklch(0.646 0.222 41.116);
230
+ --sidebar-primary-foreground: oklch(0.985 0 0);
231
+ --sidebar-ring: oklch(0.646 0.222 41.116);
232
+ --chart-1: oklch(0.646 0.222 41.116);
233
+ }
234
+
235
+ /* Violet */
236
+ [data-theme="violet"] {
237
+ --primary: oklch(0.541 0.281 293.009);
238
+ --primary-foreground: oklch(0.985 0 0);
239
+ --ring: oklch(0.541 0.281 293.009);
240
+ --sidebar-primary: oklch(0.541 0.281 293.009);
241
+ --sidebar-primary-foreground: oklch(0.985 0 0);
242
+ --sidebar-ring: oklch(0.541 0.281 293.009);
243
+ --chart-1: oklch(0.541 0.281 293.009);
244
+ }
245
+ .dark[data-theme="violet"],
246
+ .dark [data-theme="violet"] {
247
+ --primary: oklch(0.541 0.281 293.009);
248
+ --primary-foreground: oklch(0.985 0 0);
249
+ --ring: oklch(0.541 0.281 293.009);
250
+ --sidebar-primary: oklch(0.541 0.281 293.009);
251
+ --sidebar-primary-foreground: oklch(0.985 0 0);
252
+ --sidebar-ring: oklch(0.541 0.281 293.009);
253
+ --chart-1: oklch(0.541 0.281 293.009);
254
+ }
255
+
256
+ /* Stripe-inspired Soft Shadows — premium, multi-layered depth */
257
+ [data-slot="button"],
258
+ [data-slot="input"],
259
+ [data-slot="select-trigger"],
260
+ [data-slot="select"],
261
+ [data-slot="textarea"] {
262
+ /* Subtle interaction shadow */
263
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
264
+ }
265
+
266
+ /* Stripe-style: form inputs use card background instead of transparent */
267
+ [data-slot="input"],
268
+ [data-slot="textarea"],
269
+ [data-slot="select"] {
270
+ background-color: var(--card);
271
+ }
272
+
273
+ [data-slot="card"] {
274
+ /* Classic Stripe soft shadow: faint ambient + directional drop */
275
+ box-shadow: 0 2px 5px -1px rgb(0 0 0 / 0.04), 0 1px 1px -1px rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
276
+ }
277
+
278
+ [data-slot="dialog-content"],
279
+ [data-slot="sheet-content"] {
280
+ /* Deep floating layer for modals */
281
+ box-shadow: 0 10px 30px -5px rgb(0 0 0 / 0.08), 0 20px 40px -10px rgb(0 0 0 / 0.04);
282
+ }
283
+ }
284
+
285
+ body {
286
+ font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
287
+ -webkit-font-smoothing: antialiased;
288
+ -moz-osx-font-smoothing: grayscale;
289
+ }
290
+
291
+ /* Premium Typography Refinements */
292
+ h1, h2, h3 {
293
+ letter-spacing: -0.025em;
294
+ font-weight: 600;
295
+ }
296
+ h4, h5, h6 {
297
+ letter-spacing: -0.01em;
298
+ }
299
+
300
+ /* Micro-interactions: Linear/Apple ecosystem click scales */
301
+ [data-slot="button"],
302
+ button {
303
+ transition: transform 0.1s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.15s ease, box-shadow 0.15s ease;
304
+ }
305
+ [data-slot="button"]:active:not(:disabled),
306
+ button:active:not(:disabled) {
307
+ transform: scale(0.98);
308
+ }
309
+
310
+ /* Subtle glowing focus ring for inputs */
311
+ [data-slot="button"]:focus-visible,
312
+ [data-slot="input"]:focus-visible,
313
+ [data-slot="textarea"]:focus-visible,
314
+ [data-slot="select-trigger"]:focus-visible {
315
+ box-shadow: 0 0 0 1px var(--color-background), 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent) !important;
316
+ border-color: var(--color-primary);
317
+ }
318
+
319
+ /* Base scrollbar styling (Firefox + Standard) */
320
+ * {
321
+ scrollbar-width: thin;
322
+ scrollbar-color: oklch(0.8 0 0) transparent;
323
+ }
324
+ .dark * {
325
+ scrollbar-color: oklch(0.35 0 0) transparent;
326
+ }
327
+
328
+ /* Scrollbar styling (Webkit) */
329
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
330
+ ::-webkit-scrollbar-track { background: transparent; }
331
+ ::-webkit-scrollbar-thumb { background: oklch(0.8 0 0); border-radius: 3px; }
332
+ ::-webkit-scrollbar-thumb:hover { background: oklch(0.65 0 0); }
333
+ .dark ::-webkit-scrollbar-thumb { background: oklch(0.35 0 0); }
334
+ .dark ::-webkit-scrollbar-thumb:hover { background: oklch(0.45 0 0); }
335
+
336
+ /* ① Card Hover Elevation Lift — Stripe-style float on hover */
337
+ [data-slot="card"] {
338
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
339
+ }
340
+ @media (hover: hover) {
341
+ [data-slot="card"]:hover {
342
+ box-shadow: 0 6px 16px -4px rgb(0 0 0 / 0.06), 0 4px 8px -2px rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
343
+ transform: translateY(-1px);
344
+ }
345
+ }
346
+
347
+ /* ② Table Row Hover — primary color tint instead of plain gray */
348
+ [data-slot="table-row"]:hover {
349
+ background-color: color-mix(in oklch, var(--primary) 4%, var(--background)) !important;
350
+ }
351
+ [data-slot="table-row"] {
352
+ transition: background-color 0.15s ease;
353
+ }
354
+
355
+ /* ③ Sidebar Active Indicator — Linear-style left accent bar */
356
+ .sidebar-nav-active {
357
+ position: relative;
358
+ }
359
+ .sidebar-nav-active::before {
360
+ content: '';
361
+ position: absolute;
362
+ left: 0;
363
+ top: 50%;
364
+ transform: translateY(-50%);
365
+ width: 3px;
366
+ height: 60%;
367
+ border-radius: 0 3px 3px 0;
368
+ background: var(--primary);
369
+ opacity: 1;
370
+ transition: opacity 0.2s ease;
371
+ }
372
+
373
+ /* ④ Skeleton Shimmer — metallic light sweep instead of static pulse */
374
+ @keyframes svadmin-shimmer {
375
+ 0% { background-position: -200% 0; }
376
+ 100% { background-position: 200% 0; }
377
+ }
378
+ [data-slot="skeleton"] {
379
+ animation: svadmin-shimmer 1.5s ease-in-out infinite !important;
380
+ background: linear-gradient(90deg, var(--muted) 25%, color-mix(in oklch, var(--muted) 60%, white) 50%, var(--muted) 75%) !important;
381
+ background-size: 200% 100% !important;
382
+ }
383
+ .dark [data-slot="skeleton"] {
384
+ background: linear-gradient(90deg, var(--muted) 25%, color-mix(in oklch, var(--muted) 60%, var(--foreground)) 50%, var(--muted) 75%) !important;
385
+ }
386
+
387
+ /* ⑤ Route Page Transition — fade + subtle slide-up */
388
+ @keyframes svadmin-page-enter {
389
+ from {
390
+ opacity: 0;
391
+ transform: translateY(6px);
392
+ }
393
+ to {
394
+ opacity: 1;
395
+ transform: translateY(0);
396
+ }
397
+ }
398
+ .svadmin-page-enter {
399
+ animation: svadmin-page-enter 0.25s ease-out;
400
+ }
401
+
402
+ /* ⑥ Toast slide-in refinement (sonner-svelte) — tighter timing */
403
+ [data-sonner-toaster] [data-sonner-toast] {
404
+ transition-duration: 200ms !important;
405
+ transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1) !important;
406
+ }
407
+
408
+ /* ⑦ Gradient Top Border on Cards — Vercel dashboard style */
409
+ [data-slot="card"]::before {
410
+ content: '';
411
+ position: absolute;
412
+ top: 0;
413
+ left: 0;
414
+ right: 0;
415
+ height: 1px;
416
+ background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--primary) 20%, transparent), transparent);
417
+ border-radius: inherit;
418
+ pointer-events: none;
419
+ }
420
+ [data-slot="card"] {
421
+ position: relative;
422
+ overflow: hidden;
423
+ }
424
+
425
+ /* ⑧ Dark Mode Primary Glow — Linear-style neon accents */
426
+ .dark [data-slot="button"][data-variant="default"],
427
+ .dark [data-slot="badge"][data-variant="default"] {
428
+ box-shadow: 0 0 12px -3px color-mix(in oklch, var(--primary) 40%, transparent);
429
+ }
430
+
431
+ /* ⑨ Form Validation Shake Animation */
432
+ @keyframes svadmin-shake {
433
+ 0%, 100% { transform: translateX(0); }
434
+ 20% { transform: translateX(-6px); }
435
+ 40% { transform: translateX(5px); }
436
+ 60% { transform: translateX(-4px); }
437
+ 80% { transform: translateX(2px); }
438
+ }
439
+ .svadmin-shake {
440
+ animation: svadmin-shake 0.4s ease-out;
441
+ }
442
+
443
+ /* ⑩ Table Row Staggered Entrance */
444
+ @keyframes svadmin-row-enter {
445
+ from {
446
+ opacity: 0;
447
+ transform: translateY(4px);
448
+ }
449
+ to {
450
+ opacity: 1;
451
+ transform: translateY(0);
452
+ }
453
+ }
454
+ [data-slot="table-body"] [data-slot="table-row"] {
455
+ animation: svadmin-row-enter 0.3s ease-out both;
456
+ }
457
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(1) { animation-delay: 0s; }
458
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(2) { animation-delay: 0.03s; }
459
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(3) { animation-delay: 0.06s; }
460
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(4) { animation-delay: 0.09s; }
461
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(5) { animation-delay: 0.12s; }
462
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(6) { animation-delay: 0.15s; }
463
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(7) { animation-delay: 0.18s; }
464
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(8) { animation-delay: 0.21s; }
465
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(9) { animation-delay: 0.24s; }
466
+ [data-slot="table-body"] [data-slot="table-row"]:nth-child(10) { animation-delay: 0.27s; }
467
+
468
+ /* ⑪ Accessibility: Respect reduced motion preferences */
469
+ @media (prefers-reduced-motion: reduce) {
470
+ *, *::before, *::after {
471
+ animation-duration: 0.01ms !important;
472
+ animation-iteration-count: 1 !important;
473
+ transition-duration: 0.01ms !important;
474
+ scroll-behavior: auto !important;
475
+ }
476
+ }
477
+
478
+ /* ⑫b Safe Area Inset — iPhone notch / Home Bar support */
479
+ @supports (padding-bottom: env(safe-area-inset-bottom)) {
480
+ main {
481
+ padding-bottom: env(safe-area-inset-bottom);
482
+ }
483
+ }
484
+
485
+ /* ⑫ Optimistic Delete — row fade-out visual feedback */
486
+ @keyframes svadmin-row-delete {
487
+ from {
488
+ opacity: 1;
489
+ transform: translateX(0);
490
+ }
491
+ to {
492
+ opacity: 0;
493
+ transform: translateX(20px);
494
+ height: 0;
495
+ padding: 0;
496
+ margin: 0;
497
+ }
498
+ }
499
+ .svadmin-deleting {
500
+ animation: svadmin-row-delete 0.3s ease-out forwards;
501
+ pointer-events: none;
502
+ }
503
+
504
+ /* ⑬ Popover & Dropdown — frosted glass backdrop + spring entrance */
505
+ [data-slot="popover-content"],
506
+ [data-slot="dropdown-menu-content"],
507
+ [data-slot="context-menu-content"] {
508
+ backdrop-filter: blur(8px);
509
+ -webkit-backdrop-filter: blur(8px);
510
+ }
511
+
512
+ /* ⑭ Tooltip spring entrance */
513
+ [data-slot="tooltip-content"] {
514
+ animation: svadmin-tooltip-enter 0.15s cubic-bezier(0.16, 1, 0.3, 1);
515
+ }
516
+ @keyframes svadmin-tooltip-enter {
517
+ from {
518
+ opacity: 0;
519
+ transform: scale(0.96);
520
+ }
521
+ to {
522
+ opacity: 1;
523
+ transform: scale(1);
524
+ }
525
+ }
526
+
527
+ /* ⑮ Smooth scrolling for content areas */
528
+ main {
529
+ scroll-behavior: smooth;
530
+ }
531
+
532
+ /* ⑯ RTL (Right-to-Left) Language Support */
533
+ [dir="rtl"] {
534
+ direction: rtl;
535
+ }
536
+ [dir="rtl"] aside {
537
+ left: auto;
538
+ right: 0;
539
+ }
540
+ [dir="rtl"] .sidebar-nav-active::before {
541
+ left: auto;
542
+ right: 0;
543
+ border-radius: 3px 0 0 3px;
544
+ }
545
+ /* Mirror sidebar margins for RTL */
546
+ [dir="rtl"] .md\:ml-64 {
547
+ margin-left: 0;
548
+ margin-right: 16rem;
549
+ }
550
+ [dir="rtl"] .md\:ml-16 {
551
+ margin-left: 0;
552
+ margin-right: 4rem;
553
+ }
554
+
555
+ /* ⑰ Accessibility Minimum Touch Target — Apple HIG 44px minimum */
556
+ @media (max-width: 640px) and (pointer: coarse) {
557
+ [data-slot="button"] {
558
+ position: relative;
559
+ }
560
+ [data-slot="button"]::after {
561
+ content: '';
562
+ position: absolute;
563
+ top: 50%;
564
+ left: 50%;
565
+ transform: translate(-50%, -50%);
566
+ min-width: 44px;
567
+ min-height: 44px;
568
+ width: 100%;
569
+ height: 100%;
570
+ z-index: 1;
571
+ }
572
+ }
573
+
574
+ /* Tailwind CSS v4 — Map design tokens to utility classes */
575
+ @theme {
576
+ --color-background: var(--background);
577
+ --color-foreground: var(--foreground);
578
+ --color-card: var(--card);
579
+ --color-card-foreground: var(--card-foreground);
580
+ --color-popover: var(--popover);
581
+ --color-popover-foreground: var(--popover-foreground);
582
+ --color-primary: var(--primary);
583
+ --color-primary-foreground: var(--primary-foreground);
584
+ --color-secondary: var(--secondary);
585
+ --color-secondary-foreground: var(--secondary-foreground);
586
+ --color-muted: var(--muted);
587
+ --color-muted-foreground: var(--muted-foreground);
588
+ --color-accent: var(--accent);
589
+ --color-accent-foreground: var(--accent-foreground);
590
+ --color-destructive: var(--destructive);
591
+ --color-destructive-foreground: var(--destructive-foreground);
592
+ --color-border: var(--border);
593
+ --color-input: var(--input);
594
+ --color-ring: var(--ring);
595
+ --color-chart-1: var(--chart-1);
596
+ --color-chart-2: var(--chart-2);
597
+ --color-chart-3: var(--chart-3);
598
+ --color-chart-4: var(--chart-4);
599
+ --color-chart-5: var(--chart-5);
600
+ --color-sidebar: var(--sidebar);
601
+ --color-sidebar-foreground: var(--sidebar-foreground);
602
+ --color-sidebar-primary: var(--sidebar-primary);
603
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
604
+ --color-sidebar-accent: var(--sidebar-accent);
605
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
606
+ --color-sidebar-border: var(--sidebar-border);
607
+ --color-sidebar-ring: var(--sidebar-ring);
608
+ --color-success: var(--success);
609
+ --color-success-foreground: var(--success-foreground);
610
+ --color-warning: var(--warning);
611
+ --color-warning-foreground: var(--warning-foreground);
612
+ --color-info: var(--info);
613
+ --color-info-foreground: var(--info-foreground);
614
+ --radius-sm: calc(var(--radius) - 4px);
615
+ --radius-md: calc(var(--radius) - 2px);
616
+ --radius-lg: var(--radius);
617
+ --radius-xl: calc(var(--radius) + 4px);
618
+ }
619
+
620
+ /* ─── svadmin UI Components CSS ─────────────────────────────── */
621
+ /* These rules were moved from .svelte files to eliminate <style> tags */
622
+
623
+ /* MarkdownRenderer Styles */
624
+ .prose {
625
+ line-height: 1.6;
626
+ }
627
+ .prose p {
628
+ margin-top: 0.5em;
629
+ margin-bottom: 0.5em;
630
+ }
631
+
632
+ /* Streaming cursor effect */
633
+ .streaming > *:last-child::after {
634
+ content: '▋';
635
+ display: inline-block;
636
+ animation: svadmin-blink 1s step-start infinite;
637
+ margin-left: 2px;
638
+ vertical-align: baseline;
639
+ color: currentColor;
640
+ }
641
+
642
+ @keyframes svadmin-blink {
643
+ 50% { opacity: 0; }
644
+ }
645
+
646
+ /* Handle copied state for dynamically injected buttons */
647
+ .copy-btn[data-copied-id] {
648
+ color: #10b981; /* Tailwind emerald-500 */
649
+ }
650
+ .copy-btn[data-copied-id] svg {
651
+ /* Swap to Check icon */
652
+ content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="%2310b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>');
653
+ }
654
+
655
+ /* ChatDialog Styles */
656
+ .chat-markdown pre.chat-code-block {
657
+ background: var(--color-muted, hsl(var(--muted)));
658
+ border-radius: 0.5rem;
659
+ padding: 0.75rem;
660
+ margin: 0.5rem 0;
661
+ overflow-x: auto;
662
+ font-size: 0.75rem;
663
+ line-height: 1.5;
664
+ }
665
+ .chat-markdown code.chat-inline-code {
666
+ background: var(--color-muted, hsl(var(--muted)));
667
+ padding: 0.125rem 0.375rem;
668
+ border-radius: 0.25rem;
669
+ font-size: 0.8125em;
670
+ font-family: ui-monospace, monospace;
671
+ }
672
+ .chat-markdown strong {
673
+ font-weight: 600;
674
+ }