@svadmin/lite 0.1.0 → 0.2.2
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/README.md +49 -6
- package/package.json +6 -2
- package/src/components/LiteArrayField.svelte +112 -0
- package/src/components/LiteAuditLog.svelte +104 -0
- package/src/components/LiteBreadcrumbs.svelte +39 -0
- package/src/components/LiteChatDialog.svelte +101 -0
- package/src/components/LiteConfirmDialog.svelte +60 -0
- package/src/components/LiteEmptyState.svelte +39 -0
- package/src/components/LiteLayout.svelte +58 -10
- package/src/components/LitePermissionMatrix.svelte +147 -0
- package/src/components/LiteShow.svelte +3 -38
- package/src/components/LiteShowField.svelte +51 -0
- package/src/components/LiteStatsCard.svelte +70 -0
- package/src/components/LiteTabs.svelte +57 -0
- package/src/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
- package/src/components/advanced/LiteDraggableHeader.svelte +33 -0
- package/src/components/advanced/LiteDrawerForm.svelte +42 -0
- package/src/components/advanced/LiteInlineEdit.svelte +32 -0
- package/src/components/advanced/LiteModalForm.svelte +44 -0
- package/src/components/advanced/LiteToast.svelte +34 -0
- package/src/components/advanced/LiteUndoableNotification.svelte +25 -0
- package/src/components/advanced/LiteVirtualTable.svelte +44 -0
- package/src/components/advanced/index.ts +8 -0
- package/src/components/buttons/LiteCloneButton.svelte +33 -0
- package/src/components/buttons/LiteCreateButton.svelte +31 -0
- package/src/components/buttons/LiteDeleteButton.svelte +57 -0
- package/src/components/buttons/LiteEditButton.svelte +33 -0
- package/src/components/buttons/LiteExportButton.svelte +31 -0
- package/src/components/buttons/LiteImportButton.svelte +50 -0
- package/src/components/buttons/LiteListButton.svelte +31 -0
- package/src/components/buttons/LiteRefreshButton.svelte +27 -0
- package/src/components/buttons/LiteSaveButton.svelte +27 -0
- package/src/components/buttons/LiteShowButton.svelte +33 -0
- package/src/components/buttons/index.ts +10 -0
- package/src/components/fields/LiteBooleanField.svelte +41 -0
- package/src/components/fields/LiteDateField.svelte +51 -0
- package/src/components/fields/LiteEmailField.svelte +40 -0
- package/src/components/fields/LiteFileField.svelte +53 -0
- package/src/components/fields/LiteImageField.svelte +57 -0
- package/src/components/fields/LiteJsonField.svelte +43 -0
- package/src/components/fields/LiteMarkdownField.svelte +33 -0
- package/src/components/fields/LiteMultiSelectField.svelte +57 -0
- package/src/components/fields/LiteNumberField.svelte +34 -0
- package/src/components/fields/LiteRelationField.svelte +66 -0
- package/src/components/fields/LiteRichTextField.svelte +45 -0
- package/src/components/fields/LiteSelectField.svelte +47 -0
- package/src/components/fields/LiteTagField.svelte +44 -0
- package/src/components/fields/LiteTextField.svelte +34 -0
- package/src/components/fields/LiteUrlField.svelte +40 -0
- package/src/components/fields/index.ts +15 -0
- package/src/components/layout/LiteAuthenticated.svelte +23 -0
- package/src/components/layout/LiteCanAccess.svelte +27 -0
- package/src/components/layout/LiteCatchAllNavigate.svelte +20 -0
- package/src/components/layout/LiteErrorBoundary.svelte +20 -0
- package/src/components/layout/LiteErrorComponent.svelte +37 -0
- package/src/components/layout/LiteHeader.svelte +19 -0
- package/src/components/layout/LiteNavigateToResource.svelte +25 -0
- package/src/components/layout/LiteSidebar.svelte +93 -0
- package/src/components/layout/index.ts +8 -0
- package/src/components/pages/LiteCreatePage.svelte +39 -0
- package/src/components/pages/LiteEditPage.svelte +54 -0
- package/src/components/pages/LiteForgotPasswordPage.svelte +60 -0
- package/src/components/pages/LiteListPage.svelte +77 -0
- package/src/components/pages/LiteProfilePage.svelte +61 -0
- package/src/components/pages/LiteRegisterPage.svelte +64 -0
- package/src/components/pages/LiteShowPage.svelte +61 -0
- package/src/components/pages/LiteUpdatePasswordPage.svelte +51 -0
- package/src/components/pages/index.ts +8 -0
- package/src/components/widgets/LiteAnomalyBadge.svelte +40 -0
- package/src/components/widgets/LiteBarChart.svelte +45 -0
- package/src/components/widgets/LiteInsightCard.svelte +28 -0
- package/src/components/widgets/LiteLineChart.svelte +48 -0
- package/src/components/widgets/LitePieChart.svelte +44 -0
- package/src/components/widgets/index.ts +5 -0
- package/src/index.ts +28 -0
- 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
|
|
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
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "SSR-compatible lightweight admin UI for @svadmin — zero client-side JS, works in IE11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"svelte": "^5.0.0",
|
|
24
|
-
"@svadmin/core": "^0.
|
|
24
|
+
"@svadmin/core": "^0.19.3",
|
|
25
25
|
"@sveltejs/kit": "^2.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
@@ -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">« 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 »</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
|
-
{#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
|