@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 +1 -1
- package/src/components/AutoTable.svelte +14 -7
- package/src/components/ComboboxField.svelte +5 -0
- package/src/components/DraggableGrid.svelte +1 -7
- package/src/components/FieldRenderer.svelte +13 -14
- package/src/components/PermissionMatrix.svelte +1 -6
- package/src/components/Sidebar.svelte +6 -5
- package/src/components/TaskQueueDrawer.svelte +1 -15
- package/src/components/TenantSwitcher.svelte +1 -9
- package/src/editor-config.svelte.ts +23 -0
- package/src/index.ts +9 -4
- package/src/types.ts +33 -0
package/package.json
CHANGED
|
@@ -46,8 +46,10 @@
|
|
|
46
46
|
selectable?: boolean;
|
|
47
47
|
/** Custom header actions (right side) */
|
|
48
48
|
headerActions?: Snippet;
|
|
49
|
-
/** Custom cell
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
564
|
-
{@render
|
|
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
|
|
698
|
-
{@render
|
|
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
|
-
{
|
|
168
|
-
|
|
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
|
-
{:
|
|
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
|
-
{/
|
|
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 "./
|
|
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 "./
|
|
76
|
+
export type { Tenant } from "./types.js";
|
|
72
77
|
export { default as TaskQueueDrawer } from "./components/TaskQueueDrawer.svelte";
|
|
73
|
-
export type { BackgroundTask, TaskStatus } from "./
|
|
78
|
+
export type { BackgroundTask, TaskStatus } from "./types.js";
|
|
74
79
|
export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
|
|
75
|
-
export type { GridModule } from "./
|
|
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
|
+
}
|