@svadmin/lite 0.8.1 → 0.9.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/README.md +7 -6
- package/dist/components/LiteBadge.svelte +43 -0
- package/dist/components/LiteBadge.svelte.d.ts +12 -0
- package/dist/components/LiteChatDialog.svelte +9 -2
- package/dist/components/LiteMetricStrip.svelte +96 -0
- package/dist/components/LiteMetricStrip.svelte.d.ts +33 -0
- package/dist/components/LiteTable.svelte +12 -4
- package/dist/components/LiteTable.svelte.d.ts +7 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/lite.css +91 -0
- package/dist/schema-generator.d.ts +6 -33
- package/dist/schema-generator.js +16 -44
- package/dist/server-adapter.js +7 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ IE11 before the decision is made.
|
|
|
263
263
|
| `LiteProfilePage` | User profile management page with avatar and info |
|
|
264
264
|
| `LiteRegisterPage` | User registration page with server-side validation |
|
|
265
265
|
| `LiteForgotPasswordPage` / `LiteUpdatePasswordPage` | Password recovery and credential rotation flows |
|
|
266
|
-
| `LiteTable` | HTML table with sort links, selectable rows,
|
|
266
|
+
| `LiteTable` | HTML table with sort links, selectable rows, delete confirmation, and optional sticky edge/action columns |
|
|
267
267
|
| `LiteForm` | Schema-driven form renderer supporting all field definitions |
|
|
268
268
|
| `LiteShow` | Detailed field-by-field record viewer |
|
|
269
269
|
| `LitePagination` / `LiteSearch` | URL-driven pagination and GET search controls |
|
|
@@ -272,14 +272,13 @@ IE11 before the decision is made.
|
|
|
272
272
|
| `LiteCanAccess` / `LiteErrorBoundary` | Server-side access gate and constrained-environment error boundary |
|
|
273
273
|
| `LiteSplitPaneLayout` / `LiteMultiTabKeepAlive` | Dense two-pane layouts and multi-workspace navigation |
|
|
274
274
|
|
|
275
|
-
### Field Components (
|
|
275
|
+
### Field Components (32 Fields)
|
|
276
276
|
|
|
277
277
|
| Category | Components |
|
|
278
278
|
|----------|------------|
|
|
279
279
|
| **Text & Numeric** | `LiteTextField`, `LiteNumberField`, `LiteCurrencyField`, `LitePercentField`, `LitePhoneField`, `LiteEmailField`, `LiteUrlField`, `LiteRatingField`, `LiteCopyField` |
|
|
280
280
|
| **Date & Choice** | `LiteDateField`, `LiteDateRangeField`, `LiteBooleanField`, `LiteSelectField`, `LiteMultiSelectField`, `LiteTreeSelect`, `LiteCascader`, `LiteTagField`, `LiteRelationField` |
|
|
281
281
|
| **Media & Rich & Array** | `LiteAvatarField`, `LiteImageField`, `LiteFileField`, `LiteCodeField`, `LiteMarkdownField`, `LiteRichTextField`, `LiteJsonField`, `LiteArrayField`, `LiteDynamicFormList`, `LiteTransfer`, `LiteImageCropper`, `LiteJsonSchemaForm`, `LiteMentionsInput`, `LiteSignaturePad` |
|
|
282
|
-
| **SPA-only** | `VoiceInput` (Web Speech API; no SSR counterpart) |
|
|
283
282
|
|
|
284
283
|
### Action Buttons (10 Buttons)
|
|
285
284
|
|
|
@@ -293,7 +292,7 @@ IE11 before the decision is made.
|
|
|
293
292
|
|
|
294
293
|
| Component | Description |
|
|
295
294
|
|-----------|-------------|
|
|
296
|
-
| `LiteStatsCard`, `LiteInsightCard`, `LiteAnomalyBadge` | Metric KPI cards and status badges |
|
|
295
|
+
| `LiteStatsCard`, `LiteMetricStrip`, `LiteInsightCard`, `LiteAnomalyBadge`, `LiteBadge` | Metric KPI cards/strips and semantic status badges |
|
|
297
296
|
| `LiteBarChart`, `LiteLineChart`, `LitePieChart` | Server-rendered SVG/HTML charts with fallback data tables |
|
|
298
297
|
| `LitePresenceAvatarGroup`, `LiteGanttChart`, `LiteOfflineSyncBanner` | Presence, schedule, and offline mutation status rendered without client hydration |
|
|
299
298
|
|
|
@@ -320,7 +319,7 @@ IE11 before the decision is made.
|
|
|
320
319
|
| `LiteStepForm`, `LiteTableSummary`, `LiteVersionDiffViewer`, `LiteEditableTable`, `LiteDraggableRowTable` | Server-driven enterprise data interaction components |
|
|
321
320
|
| `LiteMediaLibraryModal`, `LiteActivityFeed`, `LiteKanbanBoard`, `LitePivotTable` | Media, activity, workflow, and analysis views |
|
|
322
321
|
| `LiteCanvasAnnotation`, `LiteSpreadsheetView`, `LiteDecisionTable` | SSR-compatible annotation, spreadsheet, and rules views |
|
|
323
|
-
| `DevTools
|
|
322
|
+
| `DevTools` | SPA-only component with no Lite counterpart |
|
|
324
323
|
|
|
325
324
|
## Parity Tracking & Visualization
|
|
326
325
|
|
|
@@ -342,7 +341,9 @@ To monitor and maintain 100% component parity between `@svadmin/ui` and `@svadmi
|
|
|
342
341
|
| `createAuthGuard(authProvider)` | Server hook for authentication |
|
|
343
342
|
| `createAuthActions(authProvider)` | Login/logout actions plus optional provider-delegating account actions |
|
|
344
343
|
| `createLegacyRedirectHook()` | Auto-redirect IE11 to `/lite/` |
|
|
345
|
-
| `fieldsToTypeBoxSchema(fields)` | Generate the TypeBox schema used by Lite actions or other consumers
|
|
344
|
+
| `fieldsToTypeBoxSchema(fields)` | Generate the TypeBox schema used by Lite actions or other consumers |
|
|
345
|
+
|
|
346
|
+
Schema generation is TypeBox-only. The former Zod-named exports and parser-compatible methods were removed; use `Check`, `Errors`, and `Decode` on the returned schema.
|
|
346
347
|
|
|
347
348
|
## CSS
|
|
348
349
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
export type LiteBadgeVariant =
|
|
5
|
+
| 'default'
|
|
6
|
+
| 'secondary'
|
|
7
|
+
| 'destructive'
|
|
8
|
+
| 'subtle'
|
|
9
|
+
| 'subtle-success'
|
|
10
|
+
| 'subtle-warning'
|
|
11
|
+
| 'subtle-destructive'
|
|
12
|
+
| 'subtle-pill'
|
|
13
|
+
| 'outline'
|
|
14
|
+
| 'ghost'
|
|
15
|
+
| 'link'
|
|
16
|
+
| 'info'
|
|
17
|
+
| 'success'
|
|
18
|
+
| 'warning';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
variant?: LiteBadgeVariant;
|
|
22
|
+
href?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
children?: import('svelte').Snippet;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
variant = 'default',
|
|
29
|
+
href,
|
|
30
|
+
class: className = '',
|
|
31
|
+
children,
|
|
32
|
+
...restProps
|
|
33
|
+
}: Props & HTMLAnchorAttributes = $props();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<svelte:element
|
|
37
|
+
this={href ? 'a' : 'span'}
|
|
38
|
+
{href}
|
|
39
|
+
class={'lite-badge lite-badge-' + variant + (className ? ' ' + className : '')}
|
|
40
|
+
{...restProps}
|
|
41
|
+
>
|
|
42
|
+
{@render children?.()}
|
|
43
|
+
</svelte:element>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
2
|
+
export type LiteBadgeVariant = 'default' | 'secondary' | 'destructive' | 'subtle' | 'subtle-success' | 'subtle-warning' | 'subtle-destructive' | 'subtle-pill' | 'outline' | 'ghost' | 'link' | 'info' | 'success' | 'warning';
|
|
3
|
+
interface Props {
|
|
4
|
+
variant?: LiteBadgeVariant;
|
|
5
|
+
href?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
children?: import('svelte').Snippet;
|
|
8
|
+
}
|
|
9
|
+
type $$ComponentProps = Props & HTMLAnchorAttributes;
|
|
10
|
+
declare const LiteBadge: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type LiteBadge = ReturnType<typeof LiteBadge>;
|
|
12
|
+
export default LiteBadge;
|
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function messageText(message: ChatMessage): string {
|
|
36
|
+
return message.parts
|
|
37
|
+
.filter((part): part is Extract<ChatMessage['parts'][number], { type: 'text' }> => part.type === 'text')
|
|
38
|
+
.map((part) => part.text)
|
|
39
|
+
.join('');
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
// Auto-scroll anchor logic: pure HTML way to scroll to bottom after page load
|
|
36
43
|
// User should include "#latest-msg" in the form action if possible
|
|
37
44
|
</script>
|
|
@@ -66,10 +73,10 @@
|
|
|
66
73
|
? 'background:#4f46e5;color:#fff;border-bottom-right-radius:2px;'
|
|
67
74
|
: 'background:#e2e8f0;color:#0f172a;border-bottom-left-radius:2px;'}"
|
|
68
75
|
>
|
|
69
|
-
<div style="white-space:pre-wrap;word-break:break-word;">{msg
|
|
76
|
+
<div style="white-space:pre-wrap;word-break:break-word;">{messageText(msg)}</div>
|
|
70
77
|
</div>
|
|
71
78
|
<div style="font-size:11px;color:#94a3b8;margin-top:4px;">
|
|
72
|
-
{msg.role === 'user' ? 'You' : 'Assistant'}
|
|
79
|
+
{msg.role === 'user' ? 'You' : 'Assistant'} · {formatDate(msg.createdAt)}
|
|
73
80
|
</div>
|
|
74
81
|
</div>
|
|
75
82
|
{/if}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
import LiteBadge from './LiteBadge.svelte';
|
|
4
|
+
|
|
5
|
+
export type LiteMetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
6
|
+
export type LiteMetricBadgeTone = 'default' | 'success' | 'warning' | 'danger' | 'info';
|
|
7
|
+
|
|
8
|
+
export interface LiteMetricStripItem {
|
|
9
|
+
id?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
value: string | number;
|
|
12
|
+
tone?: LiteMetricTone;
|
|
13
|
+
badge?: { text: string; tone?: LiteMetricBadgeTone };
|
|
14
|
+
icon?: Component<{ class?: string }>;
|
|
15
|
+
href?: string;
|
|
16
|
+
trend?: { value: number; label?: string };
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
items?: LiteMetricStripItem[];
|
|
23
|
+
columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
items = [],
|
|
31
|
+
columns = 'auto',
|
|
32
|
+
ariaLabel = 'Metrics overview',
|
|
33
|
+
class: className = '',
|
|
34
|
+
children,
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
const toneClass: Record<LiteMetricTone, string> = {
|
|
38
|
+
default: '',
|
|
39
|
+
primary: 'lite-metric-value-primary',
|
|
40
|
+
success: 'lite-metric-value-success',
|
|
41
|
+
warning: 'lite-metric-value-warning',
|
|
42
|
+
danger: 'lite-metric-value-danger',
|
|
43
|
+
info: 'lite-metric-value-info',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const badgeVariant: Record<LiteMetricBadgeTone, 'default' | 'subtle-success' | 'subtle-warning' | 'subtle-destructive' | 'info'> = {
|
|
47
|
+
default: 'default',
|
|
48
|
+
success: 'subtle-success',
|
|
49
|
+
warning: 'subtle-warning',
|
|
50
|
+
danger: 'subtle-destructive',
|
|
51
|
+
info: 'info',
|
|
52
|
+
};
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
role="region"
|
|
57
|
+
aria-label={ariaLabel}
|
|
58
|
+
class={'lite-metric-strip lite-metric-strip-' + columns + (className ? ' ' + className : '')}
|
|
59
|
+
>
|
|
60
|
+
{#if children}
|
|
61
|
+
{@render children()}
|
|
62
|
+
{:else}
|
|
63
|
+
{#each items as item, index (item.id || index)}
|
|
64
|
+
{@const interactive = Boolean(item.href)}
|
|
65
|
+
<svelte:element
|
|
66
|
+
this={interactive ? 'a' : 'div'}
|
|
67
|
+
href={item.href}
|
|
68
|
+
class={'lite-metric-item' + (interactive ? ' lite-metric-item-link' : '') + (item.class ? ' ' + item.class : '')}
|
|
69
|
+
>
|
|
70
|
+
<div class="lite-metric-label">
|
|
71
|
+
<span>{item.label}</span>
|
|
72
|
+
{#if item.icon}<span class="lite-metric-icon" aria-hidden="true"><item.icon class="lite-metric-icon-svg" /></span>{/if}
|
|
73
|
+
</div>
|
|
74
|
+
{#if item.loading}
|
|
75
|
+
<span class="lite-metric-loading" aria-label="Loading"> </span>
|
|
76
|
+
{:else}
|
|
77
|
+
<div class="lite-metric-value-row">
|
|
78
|
+
<strong class={'lite-metric-value ' + (toneClass[item.tone || 'default'] || '')}>{item.value}</strong>
|
|
79
|
+
{#if item.badge}
|
|
80
|
+
<LiteBadge variant={badgeVariant[item.badge.tone || 'default']}>{item.badge.text}</LiteBadge>
|
|
81
|
+
{:else if item.trend}
|
|
82
|
+
<span
|
|
83
|
+
class={'lite-metric-trend ' + (item.trend.value >= 0 ? 'lite-metric-trend-up' : 'lite-metric-trend-down')}
|
|
84
|
+
aria-label={(item.trend.value >= 0 ? 'Up ' : 'Down ') + Math.abs(item.trend.value) + ' percent'}
|
|
85
|
+
>
|
|
86
|
+
<span aria-hidden="true">{item.trend.value >= 0 ? '↑' : '↓'}</span>
|
|
87
|
+
{Math.abs(item.trend.value)}%
|
|
88
|
+
{#if item.trend.label}<span class="lite-metric-trend-label">{item.trend.label}</span>{/if}
|
|
89
|
+
</span>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
</svelte:element>
|
|
94
|
+
{/each}
|
|
95
|
+
{/if}
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
export type LiteMetricTone = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
3
|
+
export type LiteMetricBadgeTone = 'default' | 'success' | 'warning' | 'danger' | 'info';
|
|
4
|
+
export interface LiteMetricStripItem {
|
|
5
|
+
id?: string;
|
|
6
|
+
label: string;
|
|
7
|
+
value: string | number;
|
|
8
|
+
tone?: LiteMetricTone;
|
|
9
|
+
badge?: {
|
|
10
|
+
text: string;
|
|
11
|
+
tone?: LiteMetricBadgeTone;
|
|
12
|
+
};
|
|
13
|
+
icon?: Component<{
|
|
14
|
+
class?: string;
|
|
15
|
+
}>;
|
|
16
|
+
href?: string;
|
|
17
|
+
trend?: {
|
|
18
|
+
value: number;
|
|
19
|
+
label?: string;
|
|
20
|
+
};
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
interface Props {
|
|
25
|
+
items?: LiteMetricStripItem[];
|
|
26
|
+
columns?: 2 | 3 | 4 | 5 | 6 | 'auto';
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
children?: Snippet;
|
|
30
|
+
}
|
|
31
|
+
declare const LiteMetricStrip: Component<Props, {}, "">;
|
|
32
|
+
type LiteMetricStrip = ReturnType<typeof LiteMetricStrip>;
|
|
33
|
+
export default LiteMetricStrip;
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
canEdit?: boolean;
|
|
21
21
|
canDelete?: boolean;
|
|
22
22
|
enableBatch?: boolean;
|
|
23
|
+
/** One field key per edge; left is ignored while batch selection is active. */
|
|
24
|
+
stickyColumns?: { left?: string; right?: string };
|
|
25
|
+
/** Pin actions on the right; this takes precedence over stickyColumns.right. */
|
|
26
|
+
stickyActions?: boolean;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
let {
|
|
@@ -33,6 +37,8 @@
|
|
|
33
37
|
canEdit,
|
|
34
38
|
canDelete,
|
|
35
39
|
enableBatch = false,
|
|
40
|
+
stickyColumns = {},
|
|
41
|
+
stickyActions = false,
|
|
36
42
|
}: Props = $props();
|
|
37
43
|
|
|
38
44
|
const tableId = $props.id();
|
|
@@ -45,6 +51,8 @@
|
|
|
45
51
|
const listFields = $derived(
|
|
46
52
|
resource.fields.filter(f => f.showInList !== false)
|
|
47
53
|
);
|
|
54
|
+
const stickyLeft = $derived(showBatch ? undefined : stickyColumns.left);
|
|
55
|
+
const stickyRight = $derived(stickyActions ? undefined : stickyColumns.right);
|
|
48
56
|
|
|
49
57
|
function sortUrl(field: FieldDefinition): string {
|
|
50
58
|
const newOrder = currentSort === field.key && currentOrder === "asc" ? "desc" : "asc";
|
|
@@ -103,7 +111,7 @@
|
|
|
103
111
|
</th>
|
|
104
112
|
{/if}
|
|
105
113
|
{#each listFields as field, _i (_i)}
|
|
106
|
-
<th>
|
|
114
|
+
<th class={stickyLeft === field.key ? 'lite-table-sticky-left' : stickyRight === field.key ? 'lite-table-sticky-right' : undefined} data-sticky={stickyLeft === field.key ? 'left' : stickyRight === field.key ? 'right' : undefined}>
|
|
107
115
|
{#if field.sortable !== false}
|
|
108
116
|
<a href={sortUrl(field)}>
|
|
109
117
|
{field.label}
|
|
@@ -115,7 +123,7 @@
|
|
|
115
123
|
</th>
|
|
116
124
|
{/each}
|
|
117
125
|
{#if showView || showEdit || showDelete}
|
|
118
|
-
<th style="text-align:right;">{t("common.actions") || "Actions"}</th>
|
|
126
|
+
<th class={stickyActions ? 'lite-table-sticky-right' : undefined} data-sticky={stickyActions ? 'right' : undefined} style="text-align:right;">{t("common.actions") || "Actions"}</th>
|
|
119
127
|
{/if}
|
|
120
128
|
</tr>
|
|
121
129
|
</thead>
|
|
@@ -129,7 +137,7 @@
|
|
|
129
137
|
</td>
|
|
130
138
|
{/if}
|
|
131
139
|
{#each listFields as field, _i (_i)}
|
|
132
|
-
<td>
|
|
140
|
+
<td class={stickyLeft === field.key ? 'lite-table-sticky-left' : stickyRight === field.key ? 'lite-table-sticky-right' : undefined} data-sticky={stickyLeft === field.key ? 'left' : stickyRight === field.key ? 'right' : undefined}>
|
|
133
141
|
{#if field.type === "boolean"}
|
|
134
142
|
<span class="lite-bool {isExplicitBooleanTrue(record[field.key]) ? "lite-bool-true" : ""}"></span>
|
|
135
143
|
{:else if field.type === "tags" && Array.isArray(record[field.key])}
|
|
@@ -148,7 +156,7 @@
|
|
|
148
156
|
</td>
|
|
149
157
|
{/each}
|
|
150
158
|
{#if showView || showEdit || showDelete}
|
|
151
|
-
<td class=
|
|
159
|
+
<td class={'actions' + (stickyActions ? ' lite-table-sticky-right' : '')} data-sticky={stickyActions ? 'right' : undefined}>
|
|
152
160
|
{#if showView}
|
|
153
161
|
<a href={basePath + "/" + resource.name + "/show/" + id} class="lite-btn lite-btn-sm">{t("common.show") || "Show"}</a>
|
|
154
162
|
{/if}
|
|
@@ -15,6 +15,13 @@ interface Props {
|
|
|
15
15
|
canEdit?: boolean;
|
|
16
16
|
canDelete?: boolean;
|
|
17
17
|
enableBatch?: boolean;
|
|
18
|
+
/** One field key per edge; left is ignored while batch selection is active. */
|
|
19
|
+
stickyColumns?: {
|
|
20
|
+
left?: string;
|
|
21
|
+
right?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Pin actions on the right; this takes precedence over stickyColumns.right. */
|
|
24
|
+
stickyActions?: boolean;
|
|
18
25
|
}
|
|
19
26
|
declare const LiteTable: import("svelte").Component<Props, {}, "">;
|
|
20
27
|
type LiteTable = ReturnType<typeof LiteTable>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { createListLoader, createDetailLoader, createCrudActions, createAuthGuar
|
|
|
2
2
|
export type { LegacyRedirectOptions, ListLoaderResult } from './server-adapter';
|
|
3
3
|
export { LITE_COMPATIBILITY_CATALOG, detectLiteCapabilities, resolveLiteCompatibility, } from './compatibility';
|
|
4
4
|
export type { LiteCapability, LiteCapabilitySupport, LiteCompatibilityDescriptor, LiteCompatibilityResolution, LiteFallbackKind, } from './compatibility';
|
|
5
|
-
export { fieldsToTypeBoxSchema, resourceToTypeBoxSchema,
|
|
5
|
+
export { fieldsToTypeBoxSchema, resourceToTypeBoxSchema, fieldToInputType, fieldToPlaceholder, } from './schema-generator';
|
|
6
6
|
export { getStatusBadgeClass, parseExplicitBoolean, isExplicitBooleanTrue } from './value-normalization';
|
|
7
7
|
export { default as LiteLayout } from './components/LiteLayout.svelte';
|
|
8
8
|
export { default as LiteTable } from './components/LiteTable.svelte';
|
|
@@ -22,6 +22,10 @@ export { default as LiteFilterBuilder } from './components/LiteFilterBuilder.sve
|
|
|
22
22
|
export type { FilterRuleItem } from './components/LiteFilterBuilder.svelte';
|
|
23
23
|
export { default as LiteBreadcrumbs } from './components/LiteBreadcrumbs.svelte';
|
|
24
24
|
export { default as LiteStatsCard } from './components/LiteStatsCard.svelte';
|
|
25
|
+
export { default as LiteMetricStrip } from './components/LiteMetricStrip.svelte';
|
|
26
|
+
export type { LiteMetricStripItem, LiteMetricTone, LiteMetricBadgeTone, } from './components/LiteMetricStrip.svelte';
|
|
27
|
+
export { default as LiteBadge } from './components/LiteBadge.svelte';
|
|
28
|
+
export type { LiteBadgeVariant } from './components/LiteBadge.svelte';
|
|
25
29
|
export { default as LiteConfirmDialog } from './components/LiteConfirmDialog.svelte';
|
|
26
30
|
export { default as LiteEmptyState } from './components/LiteEmptyState.svelte';
|
|
27
31
|
export { default as LiteTabs } from './components/LiteTabs.svelte';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { createListLoader, createDetailLoader, createCrudActions, createAuthGuar
|
|
|
5
5
|
// Optional browser capabilities. The SSR baseline does not import browser globals.
|
|
6
6
|
export { LITE_COMPATIBILITY_CATALOG, detectLiteCapabilities, resolveLiteCompatibility, } from './compatibility';
|
|
7
7
|
// Schema generator (TypeBox schemas used by Lite actions and client forms)
|
|
8
|
-
export { fieldsToTypeBoxSchema, resourceToTypeBoxSchema,
|
|
8
|
+
export { fieldsToTypeBoxSchema, resourceToTypeBoxSchema, fieldToInputType, fieldToPlaceholder, } from './schema-generator';
|
|
9
9
|
// UI Components (use in +page.svelte with csr = false)
|
|
10
10
|
export { getStatusBadgeClass, parseExplicitBoolean, isExplicitBooleanTrue } from './value-normalization';
|
|
11
11
|
export { default as LiteLayout } from './components/LiteLayout.svelte';
|
|
@@ -24,6 +24,8 @@ export { default as LiteTransfer } from './components/LiteTransfer.svelte';
|
|
|
24
24
|
export { default as LiteFilterBuilder } from './components/LiteFilterBuilder.svelte';
|
|
25
25
|
export { default as LiteBreadcrumbs } from './components/LiteBreadcrumbs.svelte';
|
|
26
26
|
export { default as LiteStatsCard } from './components/LiteStatsCard.svelte';
|
|
27
|
+
export { default as LiteMetricStrip } from './components/LiteMetricStrip.svelte';
|
|
28
|
+
export { default as LiteBadge } from './components/LiteBadge.svelte';
|
|
27
29
|
export { default as LiteConfirmDialog } from './components/LiteConfirmDialog.svelte';
|
|
28
30
|
export { default as LiteEmptyState } from './components/LiteEmptyState.svelte';
|
|
29
31
|
export { default as LiteTabs } from './components/LiteTabs.svelte';
|
package/dist/lite.css
CHANGED
|
@@ -862,6 +862,22 @@ textarea.lite-input {
|
|
|
862
862
|
overflow-x: auto;
|
|
863
863
|
}
|
|
864
864
|
|
|
865
|
+
.lite-table .lite-table-sticky-left,
|
|
866
|
+
.lite-table .lite-table-sticky-right {
|
|
867
|
+
position: sticky;
|
|
868
|
+
z-index: 2;
|
|
869
|
+
background: #fff;
|
|
870
|
+
}
|
|
871
|
+
.lite-table th.lite-table-sticky-left,
|
|
872
|
+
.lite-table th.lite-table-sticky-right {
|
|
873
|
+
z-index: 3;
|
|
874
|
+
background: #f8fafc;
|
|
875
|
+
}
|
|
876
|
+
.lite-table .lite-table-sticky-left { left: 0; box-shadow: 1px 0 0 #e2e8f0; }
|
|
877
|
+
.lite-table .lite-table-sticky-right { right: 0; box-shadow: -1px 0 0 #e2e8f0; }
|
|
878
|
+
.lite-table tr:hover .lite-table-sticky-left,
|
|
879
|
+
.lite-table tr:hover .lite-table-sticky-right { background: #f8f7fd; }
|
|
880
|
+
|
|
865
881
|
.lite-muted {
|
|
866
882
|
color: #64748b;
|
|
867
883
|
font-size: 12px;
|
|
@@ -1044,6 +1060,70 @@ textarea.lite-input {
|
|
|
1044
1060
|
border-color: #fde68a;
|
|
1045
1061
|
}
|
|
1046
1062
|
|
|
1063
|
+
.lite-badge-subtle {
|
|
1064
|
+
background: #eef2ff;
|
|
1065
|
+
color: #4338ca;
|
|
1066
|
+
border-color: #c7d2fe;
|
|
1067
|
+
}
|
|
1068
|
+
.lite-badge-default { background: #4f46e5; color: #fff; border-color: #4f46e5; }
|
|
1069
|
+
.lite-badge-secondary { background: #f1f5f9; color: #334155; border-color: #e2e8f0; }
|
|
1070
|
+
.lite-badge-destructive,
|
|
1071
|
+
.lite-badge-subtle-destructive { background: #fef2f2; color: #dc2626; border-color: #fecaca; }
|
|
1072
|
+
.lite-badge-subtle-success { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
|
|
1073
|
+
.lite-badge-subtle-warning { background: #fffbeb; color: #92400e; border-color: #fde68a; }
|
|
1074
|
+
.lite-badge-subtle-pill {
|
|
1075
|
+
border-radius: 999px;
|
|
1076
|
+
background: #f8fafc;
|
|
1077
|
+
color: #475569;
|
|
1078
|
+
font-weight: 500;
|
|
1079
|
+
}
|
|
1080
|
+
.lite-badge-outline { background: transparent; color: #0f172a; }
|
|
1081
|
+
.lite-badge-ghost { background: transparent; border-color: transparent; color: #475569; }
|
|
1082
|
+
.lite-badge-link { padding-left: 0; padding-right: 0; background: transparent; border-color: transparent; color: #4f46e5; text-decoration: underline; }
|
|
1083
|
+
|
|
1084
|
+
/* ─── Metric strip ────────────────────────────────────────── */
|
|
1085
|
+
.lite-metric-strip {
|
|
1086
|
+
display: flex;
|
|
1087
|
+
flex-wrap: wrap;
|
|
1088
|
+
overflow: hidden;
|
|
1089
|
+
border: 1px solid #e2e8f0;
|
|
1090
|
+
border-radius: 8px;
|
|
1091
|
+
background: #e2e8f0;
|
|
1092
|
+
}
|
|
1093
|
+
.lite-metric-item {
|
|
1094
|
+
flex: 1 1 160px;
|
|
1095
|
+
min-width: 0;
|
|
1096
|
+
padding: 14px 16px;
|
|
1097
|
+
background: #fff;
|
|
1098
|
+
color: #0f172a;
|
|
1099
|
+
text-decoration: none;
|
|
1100
|
+
border-right: 1px solid #e2e8f0;
|
|
1101
|
+
border-bottom: 1px solid #e2e8f0;
|
|
1102
|
+
}
|
|
1103
|
+
.lite-metric-item-link:hover { background: #f8fafc; text-decoration: none; }
|
|
1104
|
+
.lite-metric-label,
|
|
1105
|
+
.lite-metric-value-row { display: flex; align-items: center; justify-content: space-between; }
|
|
1106
|
+
.lite-metric-label { color: #64748b; font-size: 12px; font-weight: 600; }
|
|
1107
|
+
.lite-metric-icon { margin-left: 8px; color: #94a3b8; }
|
|
1108
|
+
.lite-metric-icon-svg { width: 14px; height: 14px; }
|
|
1109
|
+
.lite-metric-value { margin-top: 4px; font-size: 23px; line-height: 1.2; color: #0f172a; }
|
|
1110
|
+
.lite-metric-value-primary { color: #4f46e5; }
|
|
1111
|
+
.lite-metric-value-success { color: #16a34a; }
|
|
1112
|
+
.lite-metric-value-warning { color: #d97706; }
|
|
1113
|
+
.lite-metric-value-danger { color: #dc2626; }
|
|
1114
|
+
.lite-metric-value-info { color: #2563eb; }
|
|
1115
|
+
.lite-metric-loading { display: block; width: 64px; height: 24px; margin-top: 6px; background: #e2e8f0; border-radius: 4px; }
|
|
1116
|
+
.lite-metric-trend { margin-left: 8px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
1117
|
+
.lite-metric-trend-up { color: #16a34a; }
|
|
1118
|
+
.lite-metric-trend-down { color: #dc2626; }
|
|
1119
|
+
.lite-metric-trend-label { margin-left: 4px; color: #64748b; font-weight: 400; }
|
|
1120
|
+
.lite-metric-strip-2 .lite-metric-item { flex-basis: 45%; }
|
|
1121
|
+
.lite-metric-strip-3 .lite-metric-item { flex-basis: 30%; }
|
|
1122
|
+
.lite-metric-strip-4 .lite-metric-item { flex-basis: 22%; }
|
|
1123
|
+
.lite-metric-strip-5 .lite-metric-item { flex-basis: 18%; }
|
|
1124
|
+
.lite-metric-strip-6 .lite-metric-item,
|
|
1125
|
+
.lite-metric-strip-auto .lite-metric-item { flex-basis: 15%; }
|
|
1126
|
+
|
|
1047
1127
|
/* ─── Boolean indicator ────────────────────────────────────── */
|
|
1048
1128
|
.lite-bool {
|
|
1049
1129
|
display: inline-block;
|
|
@@ -1317,6 +1397,17 @@ textarea.lite-input {
|
|
|
1317
1397
|
.lite-realtime-status > .lite-btn {
|
|
1318
1398
|
margin-top: 10px;
|
|
1319
1399
|
}
|
|
1400
|
+
.lite-metric-strip-3 .lite-metric-item,
|
|
1401
|
+
.lite-metric-strip-4 .lite-metric-item,
|
|
1402
|
+
.lite-metric-strip-5 .lite-metric-item,
|
|
1403
|
+
.lite-metric-strip-6 .lite-metric-item,
|
|
1404
|
+
.lite-metric-strip-auto .lite-metric-item {
|
|
1405
|
+
flex-basis: 45%;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
@media (max-width: 420px) {
|
|
1410
|
+
.lite-metric-strip .lite-metric-item { flex-basis: 100%; }
|
|
1320
1411
|
}
|
|
1321
1412
|
|
|
1322
1413
|
/* ─── Print ────────────────────────────────────────────────── */
|
|
@@ -10,33 +10,14 @@ export interface SchemaValidationIssue {
|
|
|
10
10
|
path: (string | number)[];
|
|
11
11
|
message: string;
|
|
12
12
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} | {
|
|
18
|
-
success: false;
|
|
19
|
-
error: {
|
|
20
|
-
issues: SchemaValidationIssue[];
|
|
21
|
-
};
|
|
22
|
-
data?: never;
|
|
23
|
-
};
|
|
13
|
+
export interface TypeBoxValidationError {
|
|
14
|
+
path: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
24
17
|
export type TypeBoxEnhancedSchema<T = Record<string, unknown>> = TObject & {
|
|
25
|
-
parse: (values: unknown) => T;
|
|
26
|
-
safeParse: (values: unknown) => SchemaValidationResult<T>;
|
|
27
18
|
Check: (values: unknown) => boolean;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
vendor: "svadmin";
|
|
31
|
-
validate: (values: unknown) => {
|
|
32
|
-
value: T;
|
|
33
|
-
} | {
|
|
34
|
-
issues: Array<{
|
|
35
|
-
message: string;
|
|
36
|
-
path?: (string | number)[];
|
|
37
|
-
}>;
|
|
38
|
-
};
|
|
39
|
-
};
|
|
19
|
+
Errors: (values: unknown) => Iterable<TypeBoxValidationError>;
|
|
20
|
+
Decode: (values: unknown) => T;
|
|
40
21
|
};
|
|
41
22
|
/**
|
|
42
23
|
* Generate a TypeBox object schema from a list of FieldDefinitions.
|
|
@@ -47,14 +28,6 @@ export declare function fieldsToTypeBoxSchema(fields: FieldDefinition[], mode?:
|
|
|
47
28
|
* Convenience wrapper around fieldsToTypeBoxSchema.
|
|
48
29
|
*/
|
|
49
30
|
export declare function resourceToTypeBoxSchema(resource: ResourceDefinition, mode?: "create" | "edit"): TypeBoxEnhancedSchema;
|
|
50
|
-
/**
|
|
51
|
-
* Backward compatibility alias for fieldsToTypeBoxSchema
|
|
52
|
-
*/
|
|
53
|
-
export declare const fieldsToZodSchema: typeof fieldsToTypeBoxSchema;
|
|
54
|
-
/**
|
|
55
|
-
* Backward compatibility alias for resourceToTypeBoxSchema
|
|
56
|
-
*/
|
|
57
|
-
export declare const resourceToZodSchema: typeof resourceToTypeBoxSchema;
|
|
58
31
|
/**
|
|
59
32
|
* Determine a conservative HTML input type for server-rendered forms.
|
|
60
33
|
*/
|
package/dist/schema-generator.js
CHANGED
|
@@ -361,11 +361,10 @@ export function fieldsToTypeBoxSchema(fields, mode = "create") {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
const baseSchema = Type.Object(shape, { additionalProperties: true });
|
|
364
|
-
const
|
|
364
|
+
const validateAndNormalize = (values) => {
|
|
365
365
|
if (typeof values !== "object" || values === null) {
|
|
366
366
|
return {
|
|
367
|
-
|
|
368
|
-
error: { issues: [{ path: ["_root"], message: "Values must be an object" }] },
|
|
367
|
+
issues: [{ path: ["_root"], message: "Values must be an object" }],
|
|
369
368
|
};
|
|
370
369
|
}
|
|
371
370
|
const input = values;
|
|
@@ -380,45 +379,26 @@ export function fieldsToTypeBoxSchema(fields, mode = "create") {
|
|
|
380
379
|
}
|
|
381
380
|
}
|
|
382
381
|
if (allIssues.length > 0) {
|
|
383
|
-
return {
|
|
384
|
-
success: false,
|
|
385
|
-
error: { issues: allIssues },
|
|
386
|
-
};
|
|
382
|
+
return { issues: allIssues };
|
|
387
383
|
}
|
|
388
|
-
return {
|
|
389
|
-
success: true,
|
|
390
|
-
data: resultData,
|
|
391
|
-
};
|
|
384
|
+
return { data: resultData, issues: [] };
|
|
392
385
|
};
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
if (
|
|
396
|
-
const err = new Error(
|
|
397
|
-
err.
|
|
386
|
+
const decode = (values) => {
|
|
387
|
+
const validation = validateAndNormalize(values);
|
|
388
|
+
if (validation.issues.length > 0) {
|
|
389
|
+
const err = new Error(validation.issues[0]?.message || "Validation failed");
|
|
390
|
+
err.errors = validation.issues;
|
|
398
391
|
throw err;
|
|
399
392
|
}
|
|
400
|
-
return
|
|
393
|
+
return validation.data ?? {};
|
|
401
394
|
};
|
|
402
395
|
return Object.assign(baseSchema, {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
validate: (val) => {
|
|
410
|
-
const res = safeParse(val);
|
|
411
|
-
if (res.success) {
|
|
412
|
-
return { value: res.data ?? {} };
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
issues: (res.error?.issues ?? []).map((iss) => ({
|
|
416
|
-
message: iss.message,
|
|
417
|
-
path: iss.path,
|
|
418
|
-
})),
|
|
419
|
-
};
|
|
420
|
-
},
|
|
421
|
-
},
|
|
396
|
+
Check: (val) => validateAndNormalize(val).issues.length === 0,
|
|
397
|
+
Errors: (val) => validateAndNormalize(val).issues.map((issue) => ({
|
|
398
|
+
path: issue.path.length > 0 ? `/${issue.path.map(String).join("/")}` : "",
|
|
399
|
+
message: issue.message,
|
|
400
|
+
})),
|
|
401
|
+
Decode: decode,
|
|
422
402
|
});
|
|
423
403
|
}
|
|
424
404
|
/**
|
|
@@ -429,14 +409,6 @@ export function resourceToTypeBoxSchema(resource, mode = "create") {
|
|
|
429
409
|
const primaryKey = resource.primaryKey ?? "id";
|
|
430
410
|
return fieldsToTypeBoxSchema(resource.fields.filter((field) => field.key !== primaryKey), mode);
|
|
431
411
|
}
|
|
432
|
-
/**
|
|
433
|
-
* Backward compatibility alias for fieldsToTypeBoxSchema
|
|
434
|
-
*/
|
|
435
|
-
export const fieldsToZodSchema = fieldsToTypeBoxSchema;
|
|
436
|
-
/**
|
|
437
|
-
* Backward compatibility alias for resourceToTypeBoxSchema
|
|
438
|
-
*/
|
|
439
|
-
export const resourceToZodSchema = resourceToTypeBoxSchema;
|
|
440
412
|
/**
|
|
441
413
|
* Determine a conservative HTML input type for server-rendered forms.
|
|
442
414
|
*/
|
package/dist/server-adapter.js
CHANGED
|
@@ -473,16 +473,19 @@ function formatValidationErrors(issues) {
|
|
|
473
473
|
return errors;
|
|
474
474
|
}
|
|
475
475
|
function validateFormVariables(resource, mode, values) {
|
|
476
|
-
const
|
|
477
|
-
if (
|
|
478
|
-
return { success: true, data:
|
|
476
|
+
const schema = resourceToTypeBoxSchema(resource, mode);
|
|
477
|
+
if (schema.Check(values))
|
|
478
|
+
return { success: true, data: schema.Decode(values) };
|
|
479
479
|
return {
|
|
480
480
|
success: false,
|
|
481
481
|
failure: {
|
|
482
482
|
success: false,
|
|
483
483
|
error: 'Validation failed',
|
|
484
484
|
values: formValuesForResponse(resource.fields, values),
|
|
485
|
-
errors: formatValidationErrors(
|
|
485
|
+
errors: formatValidationErrors([...schema.Errors(values)].map((issue) => ({
|
|
486
|
+
path: issue.path.split('/').filter(Boolean),
|
|
487
|
+
message: issue.message,
|
|
488
|
+
}))),
|
|
486
489
|
},
|
|
487
490
|
};
|
|
488
491
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"svelte": "^5.56.10",
|
|
52
|
-
"@svadmin/core": ">=0.34.2 <0.
|
|
52
|
+
"@svadmin/core": ">=0.34.2 <0.49.0",
|
|
53
53
|
"@sveltejs/kit": "^2.70.3"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|