@svadmin/lite 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +49 -6
  2. package/package.json +5 -1
  3. package/src/components/LiteArrayField.svelte +112 -0
  4. package/src/components/LiteAuditLog.svelte +104 -0
  5. package/src/components/LiteBreadcrumbs.svelte +39 -0
  6. package/src/components/LiteChatDialog.svelte +101 -0
  7. package/src/components/LiteConfirmDialog.svelte +60 -0
  8. package/src/components/LiteEmptyState.svelte +39 -0
  9. package/src/components/LiteLayout.svelte +58 -10
  10. package/src/components/LitePermissionMatrix.svelte +147 -0
  11. package/src/components/LiteShow.svelte +3 -38
  12. package/src/components/LiteShowField.svelte +51 -0
  13. package/src/components/LiteStatsCard.svelte +70 -0
  14. package/src/components/LiteTabs.svelte +57 -0
  15. package/src/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
  16. package/src/components/advanced/LiteDraggableHeader.svelte +33 -0
  17. package/src/components/advanced/LiteDrawerForm.svelte +42 -0
  18. package/src/components/advanced/LiteInlineEdit.svelte +32 -0
  19. package/src/components/advanced/LiteModalForm.svelte +44 -0
  20. package/src/components/advanced/LiteToast.svelte +34 -0
  21. package/src/components/advanced/LiteUndoableNotification.svelte +25 -0
  22. package/src/components/advanced/LiteVirtualTable.svelte +44 -0
  23. package/src/components/advanced/index.ts +8 -0
  24. package/src/components/buttons/LiteCloneButton.svelte +33 -0
  25. package/src/components/buttons/LiteCreateButton.svelte +31 -0
  26. package/src/components/buttons/LiteDeleteButton.svelte +57 -0
  27. package/src/components/buttons/LiteEditButton.svelte +33 -0
  28. package/src/components/buttons/LiteExportButton.svelte +31 -0
  29. package/src/components/buttons/LiteImportButton.svelte +50 -0
  30. package/src/components/buttons/LiteListButton.svelte +31 -0
  31. package/src/components/buttons/LiteRefreshButton.svelte +27 -0
  32. package/src/components/buttons/LiteSaveButton.svelte +27 -0
  33. package/src/components/buttons/LiteShowButton.svelte +33 -0
  34. package/src/components/buttons/index.ts +10 -0
  35. package/src/components/fields/LiteBooleanField.svelte +41 -0
  36. package/src/components/fields/LiteDateField.svelte +51 -0
  37. package/src/components/fields/LiteEmailField.svelte +40 -0
  38. package/src/components/fields/LiteFileField.svelte +53 -0
  39. package/src/components/fields/LiteImageField.svelte +57 -0
  40. package/src/components/fields/LiteJsonField.svelte +43 -0
  41. package/src/components/fields/LiteMarkdownField.svelte +33 -0
  42. package/src/components/fields/LiteMultiSelectField.svelte +57 -0
  43. package/src/components/fields/LiteNumberField.svelte +34 -0
  44. package/src/components/fields/LiteRelationField.svelte +66 -0
  45. package/src/components/fields/LiteRichTextField.svelte +45 -0
  46. package/src/components/fields/LiteSelectField.svelte +47 -0
  47. package/src/components/fields/LiteTagField.svelte +44 -0
  48. package/src/components/fields/LiteTextField.svelte +34 -0
  49. package/src/components/fields/LiteUrlField.svelte +40 -0
  50. package/src/components/fields/index.ts +15 -0
  51. package/src/components/layout/LiteAuthenticated.svelte +23 -0
  52. package/src/components/layout/LiteCanAccess.svelte +27 -0
  53. package/src/components/layout/LiteCatchAllNavigate.svelte +20 -0
  54. package/src/components/layout/LiteErrorBoundary.svelte +20 -0
  55. package/src/components/layout/LiteErrorComponent.svelte +37 -0
  56. package/src/components/layout/LiteHeader.svelte +19 -0
  57. package/src/components/layout/LiteNavigateToResource.svelte +25 -0
  58. package/src/components/layout/LiteSidebar.svelte +93 -0
  59. package/src/components/layout/index.ts +8 -0
  60. package/src/components/pages/LiteCreatePage.svelte +39 -0
  61. package/src/components/pages/LiteEditPage.svelte +54 -0
  62. package/src/components/pages/LiteForgotPasswordPage.svelte +60 -0
  63. package/src/components/pages/LiteListPage.svelte +77 -0
  64. package/src/components/pages/LiteProfilePage.svelte +61 -0
  65. package/src/components/pages/LiteRegisterPage.svelte +64 -0
  66. package/src/components/pages/LiteShowPage.svelte +61 -0
  67. package/src/components/pages/LiteUpdatePasswordPage.svelte +51 -0
  68. package/src/components/pages/index.ts +8 -0
  69. package/src/components/widgets/LiteAnomalyBadge.svelte +40 -0
  70. package/src/components/widgets/LiteBarChart.svelte +45 -0
  71. package/src/components/widgets/LiteInsightCard.svelte +28 -0
  72. package/src/components/widgets/LiteLineChart.svelte +48 -0
  73. package/src/components/widgets/LitePieChart.svelte +44 -0
  74. package/src/components/widgets/index.ts +5 -0
  75. package/src/index.ts +28 -0
  76. package/src/lite.css +372 -124
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Lightweight, SSR-compatible admin UI for [@svadmin](https://github.com/zuohuadong/svadmin).**
4
4
 
5
- Zero client-side JavaScript required. Works in IE11 and legacy browsers.
5
+ Zero client-side JavaScript required. Works in IE11 and all modern browsers.
6
6
 
7
7
  ## Why?
8
8
 
@@ -25,6 +25,7 @@ However, some enterprise/government environments require IE11 compatibility.
25
25
  | **Auth Guard** | Server hook redirects unauthenticated users |
26
26
  | **UA Detection** | Auto-redirect IE11 users to `/lite/` routes |
27
27
  | **i18n** | Uses `@svadmin/core` `t()` translations |
28
+ | **Multi-level Menu** | Config-driven 2/3 level menus via `MenuItem[]` |
28
29
  | **Print** | `@media print` optimized styles |
29
30
 
30
31
  ## Quick Start
@@ -81,6 +82,45 @@ export const actions = createCrudActions(dataProvider, postsResource);
81
82
  </LiteLayout>
82
83
  ```
83
84
 
85
+ ### 2b. Multi-level menu (optional)
86
+
87
+ Pass a `menu` prop to `LiteLayout` to replace the auto-generated flat resource list with a multi-level sidebar:
88
+
89
+ ```svelte
90
+ <script lang="ts">
91
+ import type { MenuItem } from '@svadmin/core';
92
+
93
+ const menu: MenuItem[] = [
94
+ { name: 'home', label: 'Dashboard', href: '/lite' },
95
+ {
96
+ name: 'content', label: 'Content',
97
+ children: [
98
+ { name: 'posts', label: 'Posts', href: '/lite/posts' },
99
+ { name: 'categories', label: 'Categories', href: '/lite/categories' },
100
+ ],
101
+ },
102
+ {
103
+ name: 'system', label: 'System',
104
+ children: [
105
+ { name: 'users', label: 'Users', href: '/lite/users' },
106
+ {
107
+ name: 'settings', label: 'Settings',
108
+ children: [
109
+ { name: 'general', label: 'General', href: '/lite/settings/general' },
110
+ { name: 'security', label: 'Security', href: '/lite/settings/security' },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ { name: 'docs', label: 'Documentation', href: 'https://docs.example.com', target: '_blank' },
116
+ ];
117
+ </script>
118
+
119
+ <LiteLayout {resources} {menu} currentResource="posts" brandName="My Admin">
120
+ <!-- ... -->
121
+ </LiteLayout>
122
+ ```
123
+
84
124
  ### 3. Auto-redirect legacy browsers
85
125
 
86
126
  ```typescript
@@ -105,7 +145,7 @@ export const handle = createLegacyRedirectHook('/lite');
105
145
 
106
146
  | Component | Description |
107
147
  |-----------|-------------|
108
- | `LiteLayout` | Sidebar + main area layout |
148
+ | `LiteLayout` | Sidebar + main area layout (supports `menu` prop for multi-level menus) |
109
149
  | `LiteTable` | HTML table with sort links and delete confirmation |
110
150
  | `LiteSearch` | GET-based search form |
111
151
  | `LitePagination` | Page number links |
@@ -129,11 +169,14 @@ export const handle = createLegacyRedirectHook('/lite');
129
169
  ## CSS
130
170
 
131
171
  Import `@svadmin/lite/src/lite.css` in your layout. It's fully self-contained:
132
- - No CSS variables
133
- - No modern CSS features
134
- - Vendor-prefixed flexbox for IE11
172
+ - IE11+ baseline (standard flexbox, no CSS variables)
173
+ - Custom-styled checkboxes, radios, and selects (no `appearance: none` needed)
174
+ - Indigo/Slate color system aligned with `@svadmin/ui`
175
+ - Modern focus rings (`box-shadow` based)
176
+ - Smooth transitions on all interactive elements
177
+ - Multi-layer translucent shadows
135
178
  - Print-optimized styles
136
- - 350 lines, ~10KB unminified
179
+ - ~500 lines, ~14KB unminified
137
180
 
138
181
  ## Optional: Progressive Enhancement
139
182
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/lite",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "SSR-compatible lightweight admin UI for @svadmin — zero client-side JS, works in IE11",
5
5
  "type": "module",
6
6
  "files": [
@@ -29,6 +29,10 @@
29
29
  "zod": "^3.24.0"
30
30
  },
31
31
  "license": "MIT",
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
32
36
  "author": "zuohuadong",
33
37
  "repository": {
34
38
  "type": "git",
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteArrayField — SSR-compatible dynamic array sub-form.
4
+ * Uses progressive enhancement: hidden inputs + JS optional add/remove.
5
+ * Falls back to server-side form POST for non-JS browsers.
6
+ */
7
+ import type { FieldDefinition } from '@svadmin/core';
8
+ import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
9
+
10
+ interface Props {
11
+ field: FieldDefinition;
12
+ /** Current array values */
13
+ values: Record<string, unknown>[];
14
+ /** Form field name prefix (for nested form encoding) */
15
+ namePrefix?: string;
16
+ }
17
+
18
+ let {
19
+ field,
20
+ values = [],
21
+ namePrefix = field.key,
22
+ }: Props = $props();
23
+
24
+ const subFields = $derived(field.subFields || []);
25
+ </script>
26
+
27
+ <fieldset class="lite-array-field">
28
+ <legend style="font-weight:600;font-size:14px;margin-bottom:8px;">
29
+ {field.label}
30
+ {#if field.required}<span style="color:#dc2626;">*</span>{/if}
31
+ </legend>
32
+
33
+ {#each values as item, i}
34
+ <div class="lite-array-item" style="border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin-bottom:8px;background:#f8fafc;">
35
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
36
+ <span style="font-size:12px;font-weight:600;color:#94a3b8;">#{i + 1}</span>
37
+ <button type="button" class="lite-btn lite-btn-sm" style="color:#dc2626;border-color:#fecaca;" onclick="this.closest('.lite-array-item').remove()">
38
+ Remove
39
+ </button>
40
+ </div>
41
+ {#each subFields as sub}
42
+ <div style="margin-bottom:8px;">
43
+ <label for="{namePrefix}_{i}_{sub.key}" style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:4px;">
44
+ {sub.label}
45
+ {#if sub.required}<span style="color:#dc2626;">*</span>{/if}
46
+ </label>
47
+ {#if sub.type === 'textarea'}
48
+ <textarea
49
+ id="{namePrefix}_{i}_{sub.key}"
50
+ name="{namePrefix}[{i}][{sub.key}]"
51
+ class="lite-input"
52
+ rows="3"
53
+ placeholder={fieldToPlaceholder(sub)}
54
+ >{item[sub.key] ?? ''}</textarea>
55
+ {:else if sub.type === 'boolean'}
56
+ <input
57
+ id="{namePrefix}_{i}_{sub.key}"
58
+ name="{namePrefix}[{i}][{sub.key}]"
59
+ type="checkbox"
60
+ checked={!!item[sub.key]}
61
+ style="width:18px;height:18px;"
62
+ />
63
+ {:else if sub.type === 'select' && sub.options}
64
+ <select
65
+ id="{namePrefix}_{i}_{sub.key}"
66
+ name="{namePrefix}[{i}][{sub.key}]"
67
+ class="lite-input"
68
+ >
69
+ {#each sub.options as opt}
70
+ <option value={typeof opt === 'string' ? opt : opt.value} selected={item[sub.key] === (typeof opt === 'string' ? opt : opt.value)}>
71
+ {typeof opt === 'string' ? opt : opt.label}
72
+ </option>
73
+ {/each}
74
+ </select>
75
+ {:else}
76
+ <input
77
+ id="{namePrefix}_{i}_{sub.key}"
78
+ name="{namePrefix}[{i}][{sub.key}]"
79
+ type={fieldToInputType(sub)}
80
+ value={item[sub.key] ?? ''}
81
+ class="lite-input"
82
+ placeholder={fieldToPlaceholder(sub)}
83
+ />
84
+ {/if}
85
+ </div>
86
+ {/each}
87
+ </div>
88
+ {/each}
89
+
90
+ {#if values.length === 0}
91
+ <p style="text-align:center;padding:16px;color:#94a3b8;font-size:14px;">No items added yet.</p>
92
+ {/if}
93
+
94
+ <button type="button" class="lite-btn" style="margin-top:4px;" onclick="
95
+ const template = this.previousElementSibling?.previousElementSibling?.cloneNode(true);
96
+ if (template) this.parentElement.insertBefore(template, this.previousElementSibling);
97
+ ">
98
+ + Add Item
99
+ </button>
100
+ </fieldset>
101
+
102
+ <style>
103
+ .lite-array-field {
104
+ border: 2px dashed #e2e8f0;
105
+ border-radius: 8px;
106
+ padding: 16px;
107
+ margin-bottom: 16px;
108
+ }
109
+ .lite-array-field legend {
110
+ padding: 0 8px;
111
+ }
112
+ </style>
@@ -0,0 +1,104 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteAuditLog — SSR-compatible audit log viewer.
4
+ * Pure HTML table with pagination links. No client-side JS required.
5
+ */
6
+
7
+ interface AuditEntry {
8
+ id: string | number;
9
+ userName?: string;
10
+ action: string;
11
+ resource?: string;
12
+ createdAt: string;
13
+ ipAddress?: string;
14
+ details?: string;
15
+ }
16
+
17
+ interface Props {
18
+ logs: AuditEntry[];
19
+ total?: number;
20
+ page?: number;
21
+ pageSize?: number;
22
+ basePath?: string;
23
+ }
24
+
25
+ let {
26
+ logs,
27
+ total = 0,
28
+ page = 1,
29
+ pageSize = 20,
30
+ basePath = '?',
31
+ }: Props = $props();
32
+
33
+ const totalPages = $derived(Math.ceil(total / pageSize) || 1);
34
+
35
+ function formatDate(d: string) {
36
+ return new Date(d).toLocaleString();
37
+ }
38
+
39
+ function actionClass(action: string): string {
40
+ const a = action.toLowerCase();
41
+ if (a.includes('delete') || a.includes('remove')) return 'lite-badge-danger';
42
+ if (a.includes('create') || a.includes('add')) return 'lite-badge-success';
43
+ if (a.includes('update') || a.includes('edit')) return 'lite-badge-warning';
44
+ return 'lite-badge-default';
45
+ }
46
+ </script>
47
+
48
+ <div class="lite-audit-log">
49
+ <h2 style="margin-bottom:16px;">Audit Logs</h2>
50
+
51
+ <table class="lite-table">
52
+ <thead>
53
+ <tr>
54
+ <th style="width:160px;">Time</th>
55
+ <th>User</th>
56
+ <th>Action</th>
57
+ <th>Resource</th>
58
+ <th style="width:120px;">IP Address</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody>
62
+ {#each logs as log}
63
+ <tr>
64
+ <td style="font-family:monospace;font-size:12px;color:#64748b;">{formatDate(log.createdAt)}</td>
65
+ <td>{log.userName ?? '—'}</td>
66
+ <td><span class="lite-badge {actionClass(log.action)}">{log.action}</span></td>
67
+ <td style="color:#64748b;">{log.resource ?? '—'}</td>
68
+ <td style="font-family:monospace;font-size:12px;color:#64748b;">{log.ipAddress ?? '—'}</td>
69
+ </tr>
70
+ {:else}
71
+ <tr>
72
+ <td colspan="5" style="text-align:center;padding:24px;color:#94a3b8;">No audit logs found.</td>
73
+ </tr>
74
+ {/each}
75
+ </tbody>
76
+ </table>
77
+
78
+ <!-- Pagination -->
79
+ {#if totalPages > 1}
80
+ <div class="lite-pagination" style="margin-top:12px;">
81
+ {#if page > 1}
82
+ <a href="{basePath}page={page - 1}" class="lite-btn lite-btn-sm">&laquo; Prev</a>
83
+ {/if}
84
+ <span style="padding:0 12px;font-size:14px;color:#64748b;">Page {page} / {totalPages} ({total} total)</span>
85
+ {#if page < totalPages}
86
+ <a href="{basePath}page={page + 1}" class="lite-btn lite-btn-sm">Next &raquo;</a>
87
+ {/if}
88
+ </div>
89
+ {/if}
90
+ </div>
91
+
92
+ <style>
93
+ .lite-badge {
94
+ display: inline-block;
95
+ padding: 2px 8px;
96
+ border-radius: 4px;
97
+ font-size: 12px;
98
+ font-weight: 600;
99
+ }
100
+ .lite-badge-danger { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
101
+ .lite-badge-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
102
+ .lite-badge-warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
103
+ .lite-badge-default { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
104
+ </style>
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteBreadcrumbs — SSR-compatible breadcrumb navigation.
4
+ * Pure HTML <nav> with <a> links. No client-side JS required.
5
+ */
6
+
7
+ interface BreadcrumbItem {
8
+ label: string;
9
+ href?: string;
10
+ }
11
+
12
+ interface Props {
13
+ items: BreadcrumbItem[];
14
+ /** Separator character between items */
15
+ separator?: string;
16
+ }
17
+
18
+ let {
19
+ items,
20
+ separator = '/',
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <nav class="lite-breadcrumbs" aria-label="Breadcrumb">
25
+ <ol>
26
+ {#each items as item, i}
27
+ <li>
28
+ {#if item.href && i < items.length - 1}
29
+ <a href={item.href}>{item.label}</a>
30
+ {:else}
31
+ <span class="current">{item.label}</span>
32
+ {/if}
33
+ {#if i < items.length - 1}
34
+ <span class="lite-breadcrumb-sep" aria-hidden="true">{separator}</span>
35
+ {/if}
36
+ </li>
37
+ {/each}
38
+ </ol>
39
+ </nav>
@@ -0,0 +1,101 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteChatDialog — SSR-compatible AI Chat Dialog fallback.
4
+ * Relies on standard <form> POST without polling or WebSockets.
5
+ * No client-side JS required.
6
+ */
7
+ import type { ChatMessage } from '@svadmin/core';
8
+
9
+ interface Props {
10
+ /** History of messages */
11
+ messages: ChatMessage[];
12
+ /** URL to handle the POST request for new messages */
13
+ actionRoute: string;
14
+ /** If true, shows a "Thinking..." indicator */
15
+ isLoading?: boolean;
16
+ title?: string;
17
+ placeholder?: string;
18
+ submitLabel?: string;
19
+ }
20
+
21
+ let {
22
+ messages = [],
23
+ actionRoute,
24
+ isLoading = false,
25
+ title = 'AI Assistant',
26
+ placeholder = 'Type your message...',
27
+ submitLabel = 'Send'
28
+ }: Props = $props();
29
+
30
+ function formatDate(ts: number) {
31
+ if (!ts) return '';
32
+ return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
33
+ }
34
+
35
+ // Auto-scroll anchor logic: pure HTML way to scroll to bottom after page load
36
+ // User should include "#latest-msg" in the form action if possible
37
+ </script>
38
+
39
+ <div class="lite-chat-dialog lite-card" style="display:flex;flex-direction:column;height:560px;padding:0;overflow:hidden;max-width:400px;margin:0 auto;">
40
+ <!-- Header -->
41
+ <div style="background:#0f172a;color:#fff;padding:14px 16px;font-weight:600;flex-shrink:0;display:flex;align-items:center;border-top-left-radius:8px;border-top-right-radius:8px;">
42
+ {title}
43
+ </div>
44
+
45
+ <!-- Message History -->
46
+ <div class="lite-chat-history" style="flex:1;overflow-y:auto;padding:16px;background:#f8fafc;">
47
+ {#if messages.length === 0}
48
+ <div style="text-align:center;color:#64748b;margin-top:60px;font-size:14px;">
49
+ How can I help you today?
50
+ </div>
51
+ {/if}
52
+
53
+ {#each messages as msg, i}
54
+ {#if msg.role === 'user' || msg.role === 'assistant'}
55
+ <div
56
+ id={i === messages.length - 1 ? 'latest-msg' : ''}
57
+ style="margin-bottom:16px;display:flex;flex-direction:column;align-items:{msg.role === 'user' ? 'flex-end' : 'flex-start'};"
58
+ >
59
+ <div style="
60
+ max-width:85%;
61
+ padding:10px 14px;
62
+ border-radius:12px;
63
+ font-size:14px;
64
+ line-height:1.5;
65
+ {msg.role === 'user'
66
+ ? 'background:#4f46e5;color:#fff;border-bottom-right-radius:2px;'
67
+ : 'background:#e2e8f0;color:#0f172a;border-bottom-left-radius:2px;'}"
68
+ >
69
+ <div style="white-space:pre-wrap;word-break:break-word;">{msg.content}</div>
70
+ </div>
71
+ <div style="font-size:11px;color:#94a3b8;margin-top:4px;">
72
+ {msg.role === 'user' ? 'You' : 'Assistant'} • {formatDate(msg.timestamp)}
73
+ </div>
74
+ </div>
75
+ {/if}
76
+ {/each}
77
+
78
+ {#if isLoading}
79
+ <div id="latest-msg" style="display:flex;flex-direction:column;align-items:flex-start;margin-bottom:16px;">
80
+ <div style="background:#e2e8f0;color:#64748b;padding:10px 14px;border-radius:12px;border-bottom-left-radius:2px;font-size:13px;font-style:italic;">
81
+ Thinking...
82
+ </div>
83
+ </div>
84
+ {/if}
85
+ </div>
86
+
87
+ <!-- Input Form -->
88
+ <form method="POST" action={actionRoute} style="display:flex;padding:12px;background:#fff;border-top:1px solid #e2e8f0;align-items:stretch;margin:0;flex-shrink:0;">
89
+ <textarea
90
+ name="message"
91
+ class="lite-input"
92
+ placeholder={placeholder}
93
+ required
94
+ rows="1"
95
+ style="min-height:40px;resize:none;margin-right:8px;"
96
+ ></textarea>
97
+ <button type="submit" class="lite-btn lite-btn-primary" disabled={isLoading} style="display:flex;align-items:center;padding:0 16px;">
98
+ {submitLabel}
99
+ </button>
100
+ </form>
101
+ </div>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteConfirmDialog — SSR-compatible confirmation dialog.
4
+ * Utilizes pure HTML <details> and <summary> to show a popup.
5
+ * No client-side JS required.
6
+ */
7
+
8
+ interface Props {
9
+ title?: string;
10
+ description?: string;
11
+ confirmLabel?: string;
12
+ cancelLabel?: string;
13
+ /** The summary element (button) that triggers the dropdown */
14
+ triggerLabel: string;
15
+ triggerClass?: string;
16
+ /** Form action to submit on confirm */
17
+ action: string;
18
+ /** Any hidden inputs to include in the form */
19
+ hiddenInputs?: Record<string, string>;
20
+ align?: 'left' | 'right';
21
+ }
22
+
23
+ let {
24
+ title = 'Are you sure?',
25
+ description = 'This action cannot be undone.',
26
+ confirmLabel = 'Confirm',
27
+ cancelLabel = 'Cancel',
28
+ triggerLabel,
29
+ triggerClass = 'lite-btn lite-btn-danger lite-btn-sm',
30
+ action,
31
+ hiddenInputs = {},
32
+ align = 'right'
33
+ }: Props = $props();
34
+ </script>
35
+
36
+ <details class="lite-confirm-details">
37
+ <summary class={triggerClass}>{triggerLabel}</summary>
38
+ <div class="lite-confirm-panel" style="text-align:left; {align === 'right' ? 'right:0;' : 'left:0;'}">
39
+ <div style="font-weight:600;margin-bottom:8px;color:#0f172a;">{title}</div>
40
+ {#if description}
41
+ <div style="font-size:12px;color:#64748b;margin-bottom:16px;">{description}</div>
42
+ {/if}
43
+ <form method="POST" {action} style="display:flex;margin:0;justify-content:flex-end;">
44
+ {#each Object.entries(hiddenInputs) as [key, val]}
45
+ <input type="hidden" name={key} value={val} />
46
+ {/each}
47
+ <button
48
+ type="button"
49
+ class="lite-btn lite-btn-sm"
50
+ style="margin-right:8px;"
51
+ onclick="this.closest('details').removeAttribute('open')"
52
+ >
53
+ {cancelLabel}
54
+ </button>
55
+ <button type="submit" class="lite-btn lite-btn-primary lite-btn-sm lite-btn-danger">
56
+ {confirmLabel}
57
+ </button>
58
+ </form>
59
+ </div>
60
+ </details>
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteEmptyState — SSR-compatible empty state indicator.
4
+ * Renders a centered box with an icon, title, description, and link.
5
+ * No client-side JS required.
6
+ */
7
+
8
+ interface Props {
9
+ title: string;
10
+ description?: string;
11
+ /** Optional HTML entity or text icon */
12
+ icon?: string;
13
+ actionLabel?: string;
14
+ actionUrl?: string;
15
+ }
16
+
17
+ let {
18
+ title,
19
+ description,
20
+ icon,
21
+ actionLabel,
22
+ actionUrl,
23
+ }: Props = $props();
24
+ </script>
25
+
26
+ <div class="lite-empty-state lite-card" style="text-align:center;padding:48px 24px;">
27
+ {#if icon}
28
+ <div style="font-size:32px;color:#94a3b8;margin-bottom:16px;">{icon}</div>
29
+ {/if}
30
+ <h3 style="font-size:16px;font-weight:600;color:#0f172a;margin:0 0 8px 0;">{title}</h3>
31
+ {#if description}
32
+ <p style="font-size:14px;color:#64748b;margin:0 0 24px 0;">{description}</p>
33
+ {/if}
34
+ {#if actionLabel && actionUrl}
35
+ <a href={actionUrl} class="lite-btn lite-btn-primary">
36
+ {actionLabel}
37
+ </a>
38
+ {/if}
39
+ </div>
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * LiteLayout — Minimal server-rendered sidebar + main layout.
4
4
  * No client-side JS required. Uses only IE11-safe CSS classes.
5
+ * Supports multi-level menus via <details>/<summary> (pure HTML).
5
6
  */
6
- import type { ResourceDefinition } from '@svadmin/core';
7
+ import type { ResourceDefinition, MenuItem } from '@svadmin/core';
7
8
  import { t } from '@svadmin/core/i18n';
8
9
 
9
10
  interface Props {
@@ -12,6 +13,8 @@
12
13
  brandName?: string;
13
14
  userName?: string;
14
15
  basePath?: string;
16
+ /** Optional multi-level menu configuration */
17
+ menu?: MenuItem[];
15
18
  children: any;
16
19
  }
17
20
 
@@ -21,26 +24,70 @@
21
24
  brandName = 'Admin',
22
25
  userName = '',
23
26
  basePath = '/lite',
27
+ menu,
24
28
  children,
25
29
  }: Props = $props();
26
30
 
27
31
  const menuResources = $derived(
28
- resources.filter(r => r.showInMenu !== false)
32
+ resources.filter((r: ResourceDefinition) => r.showInMenu !== false)
29
33
  );
34
+
35
+ function isActive(href: string | undefined): boolean {
36
+ if (!href) return false;
37
+ return currentResource === href.replace(basePath + '/', '');
38
+ }
30
39
  </script>
31
40
 
32
41
  <div class="lite-wrapper">
33
42
  <!-- Sidebar -->
34
43
  <nav class="lite-sidebar">
35
44
  <div class="lite-sidebar-brand">{brandName}</div>
36
- {#each menuResources as res}
37
- <a
38
- href="{basePath}/{res.name}"
39
- class={res.name === currentResource ? 'active' : ''}
40
- >
41
- {res.label}
42
- </a>
43
- {/each}
45
+ {#if menu && menu.length > 0}
46
+ {#each menu as item}
47
+ {#if item.children && item.children.length > 0}
48
+ <details class="lite-menu-group">
49
+ <summary class="lite-menu-parent">{item.label ?? item.name}</summary>
50
+ {#each item.children as child}
51
+ {#if child.children && child.children.length > 0}
52
+ <details class="lite-menu-group" style="margin-left:12px">
53
+ <summary class="lite-menu-parent">{child.label ?? child.name}</summary>
54
+ {#each child.children as grandchild}
55
+ <a
56
+ href={grandchild.href ?? `${basePath}/${grandchild.name}`}
57
+ class={isActive(grandchild.href ?? `${basePath}/${grandchild.name}`) ? 'active' : ''}
58
+ target={grandchild.target === '_blank' ? '_blank' : undefined}
59
+ style="padding-left:40px"
60
+ >{grandchild.label ?? grandchild.name}</a>
61
+ {/each}
62
+ </details>
63
+ {:else}
64
+ <a
65
+ href={child.href ?? `${basePath}/${child.name}`}
66
+ class={isActive(child.href ?? `${basePath}/${child.name}`) ? 'active' : ''}
67
+ target={child.target === '_blank' ? '_blank' : undefined}
68
+ style="padding-left:28px"
69
+ >{child.label ?? child.name}</a>
70
+ {/if}
71
+ {/each}
72
+ </details>
73
+ {:else}
74
+ <a
75
+ href={item.href ?? `${basePath}/${item.name}`}
76
+ class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
77
+ target={item.target === '_blank' ? '_blank' : undefined}
78
+ >{item.label ?? item.name}</a>
79
+ {/if}
80
+ {/each}
81
+ {:else}
82
+ {#each menuResources as res}
83
+ <a
84
+ href="{basePath}/{res.name}"
85
+ class={res.name === currentResource ? 'active' : ''}
86
+ >
87
+ {res.label}
88
+ </a>
89
+ {/each}
90
+ {/if}
44
91
  {#if userName}
45
92
  <div style="position:absolute;bottom:0;left:0;right:0;padding:12px 16px;border-top:1px solid #334155;font-size:12px;color:#94a3b8;">
46
93
  {userName}
@@ -56,3 +103,4 @@
56
103
  {@render children()}
57
104
  </main>
58
105
  </div>
106
+