@svadmin/ui 0.20.3 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.20.3",
3
+ "version": "0.22.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -46,8 +46,10 @@
46
46
  selectable?: boolean;
47
47
  /** Custom header actions (right side) */
48
48
  headerActions?: Snippet;
49
- /** Custom cell renderer per field */
50
- cellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
49
+ /** Custom cell renderers by field key */
50
+ columns?: Record<string, Snippet<[{ value: unknown; record: Record<string, unknown> }]>>;
51
+ /** Global fallback cell renderer */
52
+ defaultCellRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; record: Record<string, unknown> }]>;
51
53
  /** Custom row actions (edit/delete column) */
52
54
  rowActions?: Snippet<[{ record: Record<string, unknown>; id: string | number }]>;
53
55
  /** Custom empty state */
@@ -64,7 +66,8 @@
64
66
  resourceName,
65
67
  selectable = true,
66
68
  headerActions,
67
- cellRenderer,
69
+ columns: customColumns,
70
+ defaultCellRenderer,
68
71
  rowActions,
69
72
  emptyState,
70
73
  expandedRowRender,
@@ -560,8 +563,10 @@
560
563
  </div>
561
564
  {:else}
562
565
  {@const field = visibleFields.find(f => f.key === cell.column.id)}
563
- {#if cellRenderer && field}
564
- {@render cellRenderer({ field, value: cell.getValue(), record })}
566
+ {#if customColumns && field && customColumns[field.key]}
567
+ {@render customColumns[field.key]({ value: cell.getValue(), record })}
568
+ {:else if defaultCellRenderer && field}
569
+ {@render defaultCellRenderer({ field, value: cell.getValue(), record })}
565
570
  {:else if field?.type === 'boolean'}
566
571
  <span class="inline-block h-2 w-2 rounded-full {cell.getValue() ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
567
572
  {:else if field?.type === 'date' && cell.getValue()}
@@ -694,8 +699,10 @@
694
699
  <div class="flex items-start justify-between gap-4">
695
700
  <span class="text-xs font-medium text-muted-foreground shrink-0">{field.label}</span>
696
701
  <span class="text-sm text-right truncate max-w-[60%]">
697
- {#if cellRenderer}
698
- {@render cellRenderer({ field, value, record })}
702
+ {#if customColumns && field && customColumns[field.key]}
703
+ {@render customColumns[field.key]({ value, record })}
704
+ {:else if defaultCellRenderer && field}
705
+ {@render defaultCellRenderer({ field, value, record })}
699
706
  {:else if field.type === 'boolean'}
700
707
  <span class="inline-block h-2 w-2 rounded-full {value ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
701
708
  {:else if field.type === 'date' && value}
@@ -18,6 +18,9 @@
18
18
  placeholder?: string;
19
19
  searchable?: boolean;
20
20
  onSearch?: (value: string) => Filter[];
21
+ id?: string;
22
+ class?: string;
23
+ [key: string]: any;
21
24
  }
22
25
 
23
26
  let {
@@ -29,6 +32,7 @@
29
32
  placeholder = 'Select...',
30
33
  searchable = true,
31
34
  onSearch,
35
+ ...restProps
32
36
  }: Props = $props();
33
37
 
34
38
  const { options, query, onSearchChange } = useSelect({
@@ -71,6 +75,7 @@
71
75
  type="button"
72
76
  class="w-full justify-between font-normal"
73
77
  onclick={() => { open = !open; }}
78
+ {...restProps}
74
79
  >
75
80
  {#if selectedLabel}
76
81
  <span class="truncate">{selectedLabel}</span>
@@ -1,12 +1,6 @@
1
- <script context="module">
2
- export interface GridModule {
3
- id: string;
4
- [key: string]: unknown;
5
- }
6
- </script>
7
-
8
1
  <script lang="ts">
9
2
  import type { Snippet } from 'svelte';
3
+ import type { GridModule } from '../types.js';
10
4
 
11
5
  let {
12
6
  modules = $bindable<GridModule[]>([]),
@@ -18,6 +18,7 @@
18
18
  import ArrayField from './fields/ArrayField.svelte';
19
19
  import { Plus, X } from 'lucide-svelte';
20
20
  import type { Snippet } from 'svelte';
21
+ import { getRichTextEditor } from '../editor-config.svelte.js';
21
22
 
22
23
  let { field, value, onchange, children } = $props<{
23
24
  field: FieldDefinition;
@@ -164,24 +165,16 @@
164
165
  />
165
166
 
166
167
  {:else if field.type === 'richtext'}
167
- {#await import('@svadmin/editor')}
168
- <Textarea
168
+ {@const EditorComp = getRichTextEditor()}
169
+ {#if EditorComp}
170
+ <EditorComp
169
171
  id={field.key}
170
- value={strVal}
171
- oninput={(e) => onchange((e.target as HTMLTextAreaElement).value)}
172
- required={field.required}
173
- rows={10}
174
- placeholder={t('field.enterValue', { label: field.label })}
175
- class="resize-y"
176
- />
177
- {:then editorMod}
178
- <editorMod.Editor
179
172
  value={strVal}
180
173
  placeholder={t('field.enterValue', { label: field.label })}
181
174
  preset="full"
182
- onchange={(html) => onchange(html)}
175
+ onchange={(html: string) => onchange(html)}
183
176
  />
184
- {:catch}
177
+ {:else}
185
178
  <Textarea
186
179
  id={field.key}
187
180
  value={strVal}
@@ -191,7 +184,7 @@
191
184
  placeholder={t('field.enterValue', { label: field.label })}
192
185
  class="resize-y"
193
186
  />
194
- {/await}
187
+ {/if}
195
188
 
196
189
  {:else if field.type === 'textarea'}
197
190
  <Textarea
@@ -206,6 +199,7 @@
206
199
 
207
200
  {:else if field.type === 'relation' && field.resource}
208
201
  <ComboboxField
202
+ id={field.key}
209
203
  resource={field.resource}
210
204
  value={value as string | number | null}
211
205
  onchange={(v) => onchange(v)}
@@ -218,6 +212,7 @@
218
212
  {#if (field.options?.length ?? 0) > 8}
219
213
  <!-- Many options → use ComboboxField-style search -->
220
214
  <ComboboxField
215
+ id={field.key}
221
216
  resource=""
222
217
  value={value as string | number | null}
223
218
  onchange={(v) => onchange(v)}
@@ -242,6 +237,7 @@
242
237
  {#each field.options ?? [] as opt}
243
238
  <label class="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5 transition-colors">
244
239
  <Checkbox
240
+ id={`${field.key}-${opt.value}`}
245
241
  checked={multiVal.includes(opt.value)}
246
242
  onCheckedChange={() => toggleMulti(opt.value)}
247
243
  />
@@ -267,6 +263,7 @@
267
263
  {:else if field.type === 'boolean'}
268
264
  <div class="flex items-center gap-2 pt-1">
269
265
  <Switch
266
+ id={field.key}
270
267
  checked={boolVal}
271
268
  onCheckedChange={(v) => onchange(v)}
272
269
  />
@@ -289,6 +286,7 @@
289
286
  {/each}
290
287
  </div>
291
288
  <Input
289
+ id={field.key}
292
290
  type="text"
293
291
  placeholder={t('field.tagsPlaceholder')}
294
292
  onkeydown={handleTagKeydown}
@@ -309,6 +307,7 @@
309
307
  {#each imagesVal as url, i}
310
308
  <div class="flex items-center gap-2">
311
309
  <Input
310
+ id={i === 0 ? field.key : undefined}
312
311
  type="text"
313
312
  value={url}
314
313
  oninput={(e) => updateImage(i, (e.target as HTMLInputElement).value)}
@@ -1,12 +1,7 @@
1
- <script context="module">
2
- export type RoleInfo = { code: string; name: string; [key: string]: any };
3
- export type ResourceInfo = { code: string; name: string; section?: string; [key: string]: any };
4
- export type ActionInfo = { code: string; name: string; [key: string]: any };
5
- </script>
6
-
7
1
  <script lang="ts">
8
2
  import { Shield, AlertCircle, Check } from 'lucide-svelte';
9
3
  import type { Snippet } from 'svelte';
4
+ import type { RoleInfo, ResourceInfo, ActionInfo } from '../types.js';
10
5
 
11
6
  let {
12
7
  // Basic metadata lists
@@ -17,13 +17,14 @@
17
17
  ChevronLeft, ChevronDown, LogOut, Sun, Moon, Palette
18
18
  } from 'lucide-svelte';
19
19
 
20
- let { collapsed, identity, title, onToggle, onLogout, menu }: {
20
+ let { collapsed, identity, title, onToggle, onLogout, menu, legacyHashRouting = false }: {
21
21
  collapsed: boolean;
22
22
  identity: Identity | null;
23
23
  title: string;
24
24
  onToggle: () => void;
25
25
  onLogout: () => void;
26
26
  menu?: MenuItem[];
27
+ legacyHashRouting?: boolean;
27
28
  } = $props();
28
29
 
29
30
  const resources = getResources();
@@ -205,7 +206,7 @@
205
206
  {#each group.items as item}
206
207
  {@const active = isActive(item.path)}
207
208
  <a
208
- href={`#${item.path}`}
209
+ href={legacyHashRouting ? `#${item.path}` : item.path}
209
210
  class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
210
211
  {active
211
212
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -228,7 +229,7 @@
228
229
  {#snippet child({ props }: { props: Record<string, unknown> })}
229
230
  <a
230
231
  {...props}
231
- href={`#${item.path}`}
232
+ href={legacyHashRouting ? `#${item.path}` : item.path}
232
233
  class="flex items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
233
234
  {active
234
235
  ? 'bg-sidebar-accent text-sidebar-accent-foreground'
@@ -244,7 +245,7 @@
244
245
  </Tooltip.Root>
245
246
  {:else}
246
247
  <a
247
- href={`#${item.path}`}
248
+ href={legacyHashRouting ? `#${item.path}` : item.path}
248
249
  class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
249
250
  {active
250
251
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -284,7 +285,7 @@
284
285
  {#each colorThemes as ct}
285
286
  <button
286
287
  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
287
- onclick={() => { setColorTheme(ct.id); colorPickerOpen = false; }}
288
+ onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
288
289
  >
289
290
  <span
290
291
  class="h-3.5 w-3.5 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
@@ -1,23 +1,9 @@
1
- <script context="module">
2
- export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
3
-
4
- export interface BackgroundTask {
5
- id: string;
6
- title: string;
7
- status: TaskStatus;
8
- progress?: number; // 0-100
9
- message?: string;
10
- createdAt: string | Date;
11
- downloadUrl?: string;
12
- [key: string]: unknown;
13
- }
14
- </script>
15
-
16
1
  <script lang="ts">
17
2
  import type { Snippet } from 'svelte';
18
3
  import { Loader2, CheckCircle2, XCircle, Clock, Download, X, ListTodo } from 'lucide-svelte';
19
4
  import { Button } from './ui/button/index.js';
20
5
  import { Badge } from './ui/badge/index.js';
6
+ import type { BackgroundTask, TaskStatus } from '../types.js';
21
7
 
22
8
  let {
23
9
  open = $bindable(false),
@@ -1,17 +1,9 @@
1
- <script context="module">
2
- export interface Tenant {
3
- id: string;
4
- name: string;
5
- logo?: string;
6
- [key: string]: unknown;
7
- }
8
- </script>
9
-
10
1
  <script lang="ts">
11
2
  import type { Snippet } from 'svelte';
12
3
  import { ChevronsUpDown, Building2, Check } from 'lucide-svelte';
13
4
  import { Button } from './ui/button/index.js';
14
5
  import * as DropdownMenu from './ui/dropdown-menu/index.js';
6
+ import type { Tenant } from '../types.js';
15
7
 
16
8
  let {
17
9
  tenants = [],
@@ -0,0 +1,23 @@
1
+ import type { Component } from 'svelte';
2
+
3
+ let registeredEditor: Component<any> | undefined = $state(undefined);
4
+
5
+ /**
6
+ * Register a global Rich Text Editor component.
7
+ * This decouples @svadmin/ui from @svadmin/editor, making the editor completely optional.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { Editor } from '@svadmin/editor';
12
+ * import { setRichTextEditor } from '@svadmin/ui';
13
+ *
14
+ * setRichTextEditor(Editor);
15
+ * ```
16
+ */
17
+ export function setRichTextEditor(editorComponent: Component<any>): void {
18
+ registeredEditor = editorComponent;
19
+ }
20
+
21
+ export function getRichTextEditor(): Component<any> | undefined {
22
+ return registeredEditor;
23
+ }
package/src/index.ts CHANGED
@@ -61,18 +61,23 @@ export { default as InlineEdit } from "./components/InlineEdit.svelte";
61
61
  export { default as VirtualTable } from "./components/VirtualTable.svelte";
62
62
  export { default as DraggableHeader } from "./components/DraggableHeader.svelte";
63
63
  export { default as SettingsPage } from "./components/SettingsPage.svelte";
64
+
65
+ // Editor Registry
66
+ export { setRichTextEditor, getRichTextEditor } from "./editor-config.svelte.js";
67
+
68
+ // Settings & Preferences
64
69
  export { default as AppearanceSettings } from "./components/AppearanceSettings.svelte";
65
70
  export { default as AboutSettings } from "./components/AboutSettings.svelte";
66
71
  export { default as PermissionMatrix } from "./components/PermissionMatrix.svelte";
67
- export type { RoleInfo, ResourceInfo, ActionInfo } from "./components/PermissionMatrix.svelte";
72
+ export type { RoleInfo, ResourceInfo, ActionInfo } from "./types.js";
68
73
  export { default as RolesSettings } from "./components/RolesSettings.svelte";
69
74
  export { default as AuditLogViewer } from "./components/AuditLogViewer.svelte";
70
75
  export { default as TenantSwitcher } from "./components/TenantSwitcher.svelte";
71
- export type { Tenant } from "./components/TenantSwitcher.svelte";
76
+ export type { Tenant } from "./types.js";
72
77
  export { default as TaskQueueDrawer } from "./components/TaskQueueDrawer.svelte";
73
- export type { BackgroundTask, TaskStatus } from "./components/TaskQueueDrawer.svelte";
78
+ export type { BackgroundTask, TaskStatus } from "./types.js";
74
79
  export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
75
- export type { GridModule } from "./components/DraggableGrid.svelte";
80
+ export type { GridModule } from "./types.js";
76
81
 
77
82
  // Dashboard Charts (zero-dependency SVG)
78
83
  export { BarChart, LineChart, PieChart } from './components/charts/index.js';
package/src/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ export type RoleInfo = { code: string; name: string; [key: string]: any };
2
+ export type ResourceInfo = { code: string; name: string; section?: string; [key: string]: any };
3
+ export type ActionInfo = { code: string; name: string; [key: string]: any };
4
+
5
+ export interface Tenant {
6
+ id: string;
7
+ name: string;
8
+ logo?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
13
+
14
+ export interface BackgroundTask {
15
+ id: string;
16
+ title: string;
17
+ status: TaskStatus;
18
+ progress?: number; // 0-100
19
+ message?: string;
20
+ createdAt: string | Date;
21
+ downloadUrl?: string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface GridModule {
26
+ id: string;
27
+ w: number;
28
+ h: number;
29
+ x: number;
30
+ y: number;
31
+ title?: string;
32
+ componentProps?: any; // Component specific props
33
+ }