@svadmin/lite 0.3.3 → 0.3.5
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 +18 -4
- package/src/components/LiteAlert.svelte +0 -19
- package/src/components/LiteArrayField.svelte +0 -113
- package/src/components/LiteAuditLog.svelte +0 -104
- package/src/components/LiteBreadcrumbs.svelte +0 -39
- package/src/components/LiteChatDialog.svelte +0 -101
- package/src/components/LiteConfirmDialog.svelte +0 -60
- package/src/components/LiteEmptyState.svelte +0 -39
- package/src/components/LiteForm.svelte +0 -138
- package/src/components/LiteLayout.svelte +0 -106
- package/src/components/LiteLogin.svelte +0 -41
- package/src/components/LitePagination.svelte +0 -73
- package/src/components/LitePermissionMatrix.svelte +0 -147
- package/src/components/LiteSearch.svelte +0 -41
- package/src/components/LiteShow.svelte +0 -61
- package/src/components/LiteShowField.svelte +0 -51
- package/src/components/LiteStatsCard.svelte +0 -70
- package/src/components/LiteTable.svelte +0 -132
- package/src/components/LiteTabs.svelte +0 -57
- package/src/components/advanced/LiteAutoSaveIndicator.svelte +0 -26
- package/src/components/advanced/LiteDraggableHeader.svelte +0 -33
- package/src/components/advanced/LiteDrawerForm.svelte +0 -42
- package/src/components/advanced/LiteInlineEdit.svelte +0 -32
- package/src/components/advanced/LiteModalForm.svelte +0 -44
- package/src/components/advanced/LiteToast.svelte +0 -34
- package/src/components/advanced/LiteUndoableNotification.svelte +0 -25
- package/src/components/advanced/LiteVirtualTable.svelte +0 -44
- package/src/components/buttons/LiteCloneButton.svelte +0 -33
- package/src/components/buttons/LiteCreateButton.svelte +0 -31
- package/src/components/buttons/LiteDeleteButton.svelte +0 -57
- package/src/components/buttons/LiteEditButton.svelte +0 -33
- package/src/components/buttons/LiteExportButton.svelte +0 -31
- package/src/components/buttons/LiteImportButton.svelte +0 -50
- package/src/components/buttons/LiteListButton.svelte +0 -31
- package/src/components/buttons/LiteRefreshButton.svelte +0 -27
- package/src/components/buttons/LiteSaveButton.svelte +0 -27
- package/src/components/buttons/LiteShowButton.svelte +0 -33
- package/src/components/fields/LiteBooleanField.svelte +0 -41
- package/src/components/fields/LiteDateField.svelte +0 -51
- package/src/components/fields/LiteEmailField.svelte +0 -40
- package/src/components/fields/LiteFileField.svelte +0 -53
- package/src/components/fields/LiteImageField.svelte +0 -57
- package/src/components/fields/LiteJsonField.svelte +0 -43
- package/src/components/fields/LiteMarkdownField.svelte +0 -33
- package/src/components/fields/LiteMultiSelectField.svelte +0 -57
- package/src/components/fields/LiteNumberField.svelte +0 -34
- package/src/components/fields/LiteRelationField.svelte +0 -66
- package/src/components/fields/LiteRichTextField.svelte +0 -45
- package/src/components/fields/LiteSelectField.svelte +0 -47
- package/src/components/fields/LiteTagField.svelte +0 -44
- package/src/components/fields/LiteTextField.svelte +0 -34
- package/src/components/fields/LiteUrlField.svelte +0 -40
- package/src/components/layout/LiteAuthenticated.svelte +0 -23
- package/src/components/layout/LiteCanAccess.svelte +0 -27
- package/src/components/layout/LiteCatchAllNavigate.svelte +0 -20
- package/src/components/layout/LiteErrorBoundary.svelte +0 -20
- package/src/components/layout/LiteErrorComponent.svelte +0 -37
- package/src/components/layout/LiteHeader.svelte +0 -19
- package/src/components/layout/LiteNavigateToResource.svelte +0 -25
- package/src/components/layout/LiteSidebar.svelte +0 -93
- package/src/components/pages/LiteCreatePage.svelte +0 -39
- package/src/components/pages/LiteEditPage.svelte +0 -54
- package/src/components/pages/LiteForgotPasswordPage.svelte +0 -60
- package/src/components/pages/LiteListPage.svelte +0 -81
- package/src/components/pages/LiteProfilePage.svelte +0 -61
- package/src/components/pages/LiteRegisterPage.svelte +0 -64
- package/src/components/pages/LiteShowPage.svelte +0 -61
- package/src/components/pages/LiteUpdatePasswordPage.svelte +0 -51
- package/src/components/widgets/LiteAnomalyBadge.svelte +0 -40
- package/src/components/widgets/LiteBarChart.svelte +0 -45
- package/src/components/widgets/LiteInsightCard.svelte +0 -28
- package/src/components/widgets/LiteLineChart.svelte +0 -48
- package/src/components/widgets/LitePieChart.svelte +0 -44
- package/src/lite.css +0 -708
- package/static/enhance.js +0 -56
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "SSR-compatible lightweight admin UI for @svadmin — zero client-side JS, works in IE11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
-
"src
|
|
8
|
-
"
|
|
7
|
+
"src/**/*.ts",
|
|
8
|
+
"!src/**/*.test.*",
|
|
9
|
+
"!src/**/*.spec.*",
|
|
10
|
+
"!src/**/vitest.config.*",
|
|
11
|
+
"!src/**/setupTest.*"
|
|
9
12
|
],
|
|
10
13
|
"main": "src/index.ts",
|
|
11
14
|
"types": "src/index.ts",
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
},
|
|
22
25
|
"peerDependencies": {
|
|
23
26
|
"svelte": "^5.0.0",
|
|
24
|
-
"@svadmin/core": "^0.
|
|
27
|
+
"@svadmin/core": "^0.25.3",
|
|
25
28
|
"@sveltejs/kit": "^2.0.0"
|
|
26
29
|
},
|
|
27
30
|
"dependencies": {
|
|
@@ -39,5 +42,16 @@
|
|
|
39
42
|
"type": "git",
|
|
40
43
|
"url": "https://github.com/zuohuadong/svadmin.git",
|
|
41
44
|
"directory": "packages/lite"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"svelte": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@svadmin/core": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@sveltejs/kit": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
57
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteAlert — Success/error feedback banner for form actions.
|
|
4
|
-
* Renders from SvelteKit form action results.
|
|
5
|
-
*/
|
|
6
|
-
interface Props {
|
|
7
|
-
type: 'success' | 'error' | 'info';
|
|
8
|
-
message: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
let { type, message }: Props = $props();
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
{#if message}
|
|
15
|
-
<div class="lite-alert {type === 'success' ? 'lite-alert-success' : type === 'error' ? 'lite-alert-error' : ''}">
|
|
16
|
-
{#if type === 'success'}✓{:else if type === 'error'}✗{:else}ℹ{/if}
|
|
17
|
-
{message}
|
|
18
|
-
</div>
|
|
19
|
-
{/if}
|
|
@@ -1,113 +0,0 @@
|
|
|
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={(e) => (e.currentTarget as HTMLElement).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={(e) => {
|
|
95
|
-
const target = e.currentTarget as HTMLElement;
|
|
96
|
-
const template = target.previousElementSibling?.previousElementSibling?.cloneNode(true);
|
|
97
|
-
if (template && target.parentElement) target.parentElement.insertBefore(template, target.previousElementSibling);
|
|
98
|
-
}}>
|
|
99
|
-
+ Add Item
|
|
100
|
-
</button>
|
|
101
|
-
</fieldset>
|
|
102
|
-
|
|
103
|
-
<style>
|
|
104
|
-
.lite-array-field {
|
|
105
|
-
border: 2px dashed #e2e8f0;
|
|
106
|
-
border-radius: 8px;
|
|
107
|
-
padding: 16px;
|
|
108
|
-
margin-bottom: 16px;
|
|
109
|
-
}
|
|
110
|
-
.lite-array-field legend {
|
|
111
|
-
padding: 0 8px;
|
|
112
|
-
}
|
|
113
|
-
</style>
|
|
@@ -1,104 +0,0 @@
|
|
|
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>
|
|
@@ -1,39 +0,0 @@
|
|
|
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>
|
|
@@ -1,101 +0,0 @@
|
|
|
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>
|
|
@@ -1,60 +0,0 @@
|
|
|
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={(e) => (e.currentTarget as HTMLElement).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>
|
|
@@ -1,39 +0,0 @@
|
|
|
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>
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LiteForm — Server-rendered form driven by FieldDefinitions.
|
|
4
|
-
* Uses native <form> with method="POST" for IE11 compatibility.
|
|
5
|
-
* Integrates with sveltekit-superforms for server-side validation.
|
|
6
|
-
*/
|
|
7
|
-
import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';
|
|
8
|
-
import { t } from '@svadmin/core/i18n';
|
|
9
|
-
import { fieldToInputType, fieldToPlaceholder } from '../schema-generator';
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
fields: FieldDefinition[];
|
|
13
|
-
/** Resource definition (used for primaryKey) */
|
|
14
|
-
resource?: ResourceDefinition;
|
|
15
|
-
/** Existing values (for edit mode) */
|
|
16
|
-
values?: Record<string, unknown>;
|
|
17
|
-
/** Validation errors from server */
|
|
18
|
-
errors?: Record<string, string[]>;
|
|
19
|
-
/** 'create' or 'edit' */
|
|
20
|
-
mode?: 'create' | 'edit';
|
|
21
|
-
/** Form action URL */
|
|
22
|
-
action?: string;
|
|
23
|
-
/** Submit button label */
|
|
24
|
-
submitLabel?: string;
|
|
25
|
-
/** Cancel URL */
|
|
26
|
-
cancelUrl?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let {
|
|
30
|
-
fields,
|
|
31
|
-
resource,
|
|
32
|
-
values = {},
|
|
33
|
-
errors = {},
|
|
34
|
-
mode = 'create',
|
|
35
|
-
action = '',
|
|
36
|
-
submitLabel,
|
|
37
|
-
cancelUrl,
|
|
38
|
-
}: Props = $props();
|
|
39
|
-
|
|
40
|
-
const formFields = $derived(
|
|
41
|
-
fields.filter(f => {
|
|
42
|
-
if (f.showInForm === false) return false;
|
|
43
|
-
if (mode === 'create' && f.showInCreate === false) return false;
|
|
44
|
-
if (mode === 'edit' && f.showInEdit === false) return false;
|
|
45
|
-
return true;
|
|
46
|
-
})
|
|
47
|
-
);
|
|
48
|
-
</script>
|
|
49
|
-
|
|
50
|
-
<form method="POST" action={action || undefined} class="lite-card" enctype="multipart/form-data">
|
|
51
|
-
{#if mode === 'edit'}
|
|
52
|
-
{@const pk = resource?.primaryKey ?? 'id'}
|
|
53
|
-
{#if values[pk] != null}
|
|
54
|
-
<input type="hidden" name="_id" value={String(values[pk])} />
|
|
55
|
-
{/if}
|
|
56
|
-
{/if}
|
|
57
|
-
|
|
58
|
-
{#each formFields as field}
|
|
59
|
-
{@const inputType = fieldToInputType(field)}
|
|
60
|
-
{@const placeholder = fieldToPlaceholder(field)}
|
|
61
|
-
{@const value = values[field.key]}
|
|
62
|
-
{@const fieldErrors = errors[field.key]}
|
|
63
|
-
{@const hasError = fieldErrors && fieldErrors.length > 0}
|
|
64
|
-
|
|
65
|
-
<div class="lite-form-group">
|
|
66
|
-
{#if inputType === 'checkbox'}
|
|
67
|
-
<div class="lite-checkbox-group">
|
|
68
|
-
<input
|
|
69
|
-
type="checkbox"
|
|
70
|
-
name={field.key}
|
|
71
|
-
id={field.key}
|
|
72
|
-
checked={!!value}
|
|
73
|
-
/>
|
|
74
|
-
<label for={field.key}>{field.label}</label>
|
|
75
|
-
</div>
|
|
76
|
-
{:else}
|
|
77
|
-
<label for={field.key}>
|
|
78
|
-
{field.label}
|
|
79
|
-
{#if field.required}<span class="required">*</span>{/if}
|
|
80
|
-
</label>
|
|
81
|
-
|
|
82
|
-
{#if inputType === 'textarea'}
|
|
83
|
-
<textarea
|
|
84
|
-
name={field.key}
|
|
85
|
-
id={field.key}
|
|
86
|
-
class="lite-input {hasError ? 'lite-input-error' : ''}"
|
|
87
|
-
placeholder={placeholder}
|
|
88
|
-
>{value ?? ''}</textarea>
|
|
89
|
-
{:else if inputType === 'select'}
|
|
90
|
-
<select
|
|
91
|
-
name={field.key}
|
|
92
|
-
id={field.key}
|
|
93
|
-
class="lite-select {hasError ? 'lite-input-error' : ''}"
|
|
94
|
-
{...field.type === 'multiselect' ? { multiple: true } : {}}
|
|
95
|
-
>
|
|
96
|
-
{#if field.type !== 'multiselect'}
|
|
97
|
-
<option value="">-- {t('common.select') || 'Select'} --</option>
|
|
98
|
-
{/if}
|
|
99
|
-
{#if field.options}
|
|
100
|
-
{#each field.options as opt}
|
|
101
|
-
<option
|
|
102
|
-
value={String(opt.value)}
|
|
103
|
-
selected={field.type === 'multiselect' ? (Array.isArray(value) && value.includes(opt.value)) : value === opt.value}
|
|
104
|
-
>
|
|
105
|
-
{opt.label}
|
|
106
|
-
</option>
|
|
107
|
-
{/each}
|
|
108
|
-
{/if}
|
|
109
|
-
</select>
|
|
110
|
-
{:else}
|
|
111
|
-
<input
|
|
112
|
-
type={inputType}
|
|
113
|
-
name={field.key}
|
|
114
|
-
id={field.key}
|
|
115
|
-
value={String(value ?? '')}
|
|
116
|
-
class="lite-input {hasError ? 'lite-input-error' : ''}"
|
|
117
|
-
placeholder={placeholder}
|
|
118
|
-
{... field.required ? { required: true } : {}}
|
|
119
|
-
{... field.type === 'images' ? { multiple: true } : {}}
|
|
120
|
-
/>
|
|
121
|
-
{/if}
|
|
122
|
-
|
|
123
|
-
{#if hasError}
|
|
124
|
-
{#each fieldErrors as err}
|
|
125
|
-
<div class="lite-error-text">{err}</div>
|
|
126
|
-
{/each}
|
|
127
|
-
{/if}
|
|
128
|
-
{/if}
|
|
129
|
-
</div>
|
|
130
|
-
{/each}
|
|
131
|
-
|
|
132
|
-
<div style="display:flex;align-items:center;margin-top:20px;">
|
|
133
|
-
<button type="submit" class="lite-btn lite-btn-primary">{submitLabel || t('common.save') || 'Save'}</button>
|
|
134
|
-
{#if cancelUrl}
|
|
135
|
-
<a href={cancelUrl} class="lite-btn" style="margin-left:8px;">{t('common.cancel') || 'Cancel'}</a>
|
|
136
|
-
{/if}
|
|
137
|
-
</div>
|
|
138
|
-
</form>
|