@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 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, and delete confirmation |
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 (33 Fields)
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`, `CopilotPanel` | SPA-only components with no Lite counterpart |
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 (with `fieldsToZodSchema` alias) |
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.content}</div>
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'} {formatDate(msg.timestamp)}
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">&nbsp;</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="actions">
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, fieldsToZodSchema, resourceToZodSchema, fieldToInputType, fieldToPlaceholder, } from './schema-generator';
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, fieldsToZodSchema, resourceToZodSchema, fieldToInputType, fieldToPlaceholder, } from './schema-generator';
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 type SchemaValidationResult<T = Record<string, unknown>> = {
14
- success: true;
15
- data: T;
16
- error?: never;
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
- "~standard": {
29
- version: 1;
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
  */
@@ -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 safeParse = (values) => {
364
+ const validateAndNormalize = (values) => {
365
365
  if (typeof values !== "object" || values === null) {
366
366
  return {
367
- success: false,
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 parse = (values) => {
394
- const res = safeParse(values);
395
- if (!res.success) {
396
- const err = new Error(res.error?.issues[0]?.message || "Validation failed");
397
- err.issues = res.error?.issues ?? [];
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 res.data ?? {};
393
+ return validation.data ?? {};
401
394
  };
402
395
  return Object.assign(baseSchema, {
403
- parse,
404
- safeParse,
405
- Check: (val) => safeParse(val).success,
406
- "~standard": {
407
- version: 1,
408
- vendor: "svadmin",
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
  */
@@ -473,16 +473,19 @@ function formatValidationErrors(issues) {
473
473
  return errors;
474
474
  }
475
475
  function validateFormVariables(resource, mode, values) {
476
- const result = resourceToTypeBoxSchema(resource, mode).safeParse(values);
477
- if (result.success)
478
- return { success: true, data: result.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(result.error.issues),
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.8.1",
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.48.0",
52
+ "@svadmin/core": ">=0.34.2 <0.49.0",
53
53
  "@sveltejs/kit": "^2.70.3"
54
54
  },
55
55
  "dependencies": {