@svadmin/lite 0.25.7 → 0.26.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.
Files changed (33) hide show
  1. package/README.md +11 -5
  2. package/dist/components/LiteAuditLog.svelte +102 -36
  3. package/dist/components/LiteAuditLog.svelte.d.ts +6 -3
  4. package/dist/components/LiteConcurrentConflictModal.svelte +110 -0
  5. package/dist/components/LiteConcurrentConflictModal.svelte.d.ts +21 -0
  6. package/dist/components/LiteCronExpressionBuilder.svelte +78 -0
  7. package/dist/components/LiteCronExpressionBuilder.svelte.d.ts +17 -0
  8. package/dist/components/LiteMetricGrid.svelte +143 -0
  9. package/dist/components/LiteMetricGrid.svelte.d.ts +31 -0
  10. package/dist/components/LiteMicroAppFrame.svelte +57 -0
  11. package/dist/components/LiteMicroAppFrame.svelte.d.ts +15 -0
  12. package/dist/components/LiteOrgChart.svelte +78 -0
  13. package/dist/components/LiteOrgChart.svelte.d.ts +17 -0
  14. package/dist/components/LitePresenceAvatarGroup.svelte +118 -15
  15. package/dist/components/LitePresenceAvatarGroup.svelte.d.ts +8 -3
  16. package/dist/components/LiteSafeDeleteConfirm.svelte +86 -0
  17. package/dist/components/LiteSafeDeleteConfirm.svelte.d.ts +21 -0
  18. package/dist/components/LiteSignaturePad.svelte +3 -1
  19. package/dist/components/LiteSignaturePad.svelte.d.ts +1 -0
  20. package/dist/components/LiteTaskCenter.svelte +133 -0
  21. package/dist/components/LiteTaskCenter.svelte.d.ts +24 -0
  22. package/dist/components/LiteTour.svelte +4 -0
  23. package/dist/components/LiteTour.svelte.d.ts +4 -0
  24. package/dist/components/LiteTrendIndicator.svelte +119 -0
  25. package/dist/components/LiteTrendIndicator.svelte.d.ts +19 -0
  26. package/dist/components/LiteVirtualTreeSelect.svelte +92 -0
  27. package/dist/components/LiteVirtualTreeSelect.svelte.d.ts +20 -0
  28. package/dist/components/widgets/LiteGaugeChart.svelte +50 -3
  29. package/dist/components/widgets/LiteGaugeChart.svelte.d.ts +12 -0
  30. package/dist/index.d.ts +14 -0
  31. package/dist/index.js +10 -0
  32. package/dist/rich-content.js +1 -1
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -262,7 +262,7 @@ it, so the rest of Lite has no extra runtime dependency. Install the peer with
262
262
  import LiteQRCode from '@svadmin/lite/components/LiteQRCode.svelte';
263
263
  ```
264
264
 
265
- `@svadmin/lite` provides **100% component parity** (179/179 components) corresponding to `@svadmin/ui`. See [PARITY.md](./PARITY.md) for the generated complete matrix and status details.
265
+ `@svadmin/lite` provides **100% mapping coverage** (193/193 components) corresponding to `@svadmin/ui`: 134 exact 1:1 renderings, 54 semantic fallbacks, and 5 SPA-only components that intentionally have no server rendering. See [PARITY.md](./PARITY.md) for the generated complete matrix and status details.
266
266
 
267
267
  ### Core Pages & Layout
268
268
 
@@ -275,8 +275,11 @@ import LiteQRCode from '@svadmin/lite/components/LiteQRCode.svelte';
275
275
  | `LiteImage` / `LiteImagePreview` / `LiteVideoPlayer` / `LiteQRCode` | Media rendering with native controls, links and server-rendered QR |
276
276
  | `LiteCalendar` / `LiteCarousel` / `LiteTimeline` / `LiteStatistic` / `LiteCountdown` | Read-only dashboards and chronological displays |
277
277
  | `LiteCommentThread` / `LiteTour` / `LiteNotificationCenter` / `LiteDetailDrawer` / `LitePopconfirm` | Server-rendered collaboration and disclosure surfaces |
278
+ | `LiteTaskCenter` / `LiteAuditLog` / `LiteAuditTimeline` | Server-rendered task queues, audit trails and field-level change diffs |
279
+ | `LiteSafeDeleteConfirm` / `LiteConcurrentConflictModal` | Native POST confirmation and field-level conflict resolution forms |
280
+ | `LiteMicroAppFrame` | Sandboxed iframe embedding with a direct fallback link |
278
281
  | `LiteSteps` / `LiteProgress` / `LiteSpin` / `LiteSkeleton` / `LiteResult` | Progress, loading and outcome surfaces |
279
- | `LiteTree` / `LiteItemList` / `LiteRowActions` / `LiteOtpInput` / `LiteTwoFactorStepper` | Hierarchical data, row actions and verification flows |
282
+ | `LiteTree` / `LiteOrgChart` / `LiteItemList` / `LiteRowActions` / `LiteOtpInput` / `LiteTwoFactorStepper` | Hierarchical data, organization charts, row actions and verification flows |
280
283
  | `LiteAutoComplete` | Native `datalist` suggestions without async client popovers |
281
284
  | `LiteSidebar` / `LiteHeader` | Discrete layout primitives |
282
285
  | `LiteListPage` | Full-featured list view with search, table, bulk actions, and pagination |
@@ -302,7 +305,8 @@ import LiteQRCode from '@svadmin/lite/components/LiteQRCode.svelte';
302
305
  | Category | Components |
303
306
  |----------|------------|
304
307
  | **Text & Numeric** | `LiteTextField`, `LiteNumberField`, `LiteCurrencyField`, `LitePercentField`, `LitePhoneField`, `LiteEmailField`, `LiteUrlField`, `LiteRatingField`, `LiteCopyField` |
305
- | **Date & Choice** | `LiteDateField`, `LiteDateRangeField`, `LiteBooleanField`, `LiteSelectField`, `LiteMultiSelectField`, `LiteTreeSelect`, `LiteCascader`, `LiteTagField`, `LiteRelationField` |
308
+ | **Date & Choice** | `LiteDateField`, `LiteDateRangeField`, `LiteBooleanField`, `LiteSelectField`, `LiteMultiSelectField`, `LiteTreeSelect`, `LiteVirtualTreeSelect`, `LiteCascader`, `LiteTagField`, `LiteRelationField` |
309
+ | **Scheduling & Encoding** | `LiteCronExpressionBuilder` |
306
310
  | **Media & Rich & Array** | `LiteAvatarField`, `LiteImageField`, `LiteFileField`, `LiteCodeField`, `LiteMarkdownField`, `LiteRichTextField`, `LiteJsonField`, `LiteArrayField`, `LiteDynamicFormList`, `LiteTransfer`, `LiteImageCropper`, `LiteJsonSchemaForm`, `LiteMentionsInput`, `LiteSignaturePad` |
307
311
 
308
312
  ### Action Buttons (10 Buttons)
@@ -317,7 +321,8 @@ import LiteQRCode from '@svadmin/lite/components/LiteQRCode.svelte';
317
321
 
318
322
  | Component | Description |
319
323
  |-----------|-------------|
320
- | `LiteStatsCard`, `LiteMetricStrip`, `LiteInsightCard`, `LiteAnomalyBadge`, `LiteBadge` | Metric KPI cards/strips and semantic status badges |
324
+ | `LiteStatsCard`, `LiteMetricStrip`, `LiteMetricGrid`, `LiteInsightCard`, `LiteAnomalyBadge`, `LiteBadge` | Metric KPI cards, metric collections and semantic status badges |
325
+ | `LiteTrendIndicator` | Server-computed trend direction, rate and semantic color |
321
326
  | `LiteBarChart`, `LiteLineChart`, `LitePieChart` | Server-rendered SVG/HTML charts with fallback data tables |
322
327
  | `LiteAreaChart`, `LiteScatterChart`, `LiteRadarChart`, `LiteGaugeChart` | Bounded chart data tables where a client chart engine is unavailable |
323
328
  | `LiteFunnelChart`, `LiteHeatmapChart`, `LiteWaterfallChart`, `LiteRoseChart`, `LiteDonutChart` | Bounded chart data tables with the same fallback contract |
@@ -346,6 +351,7 @@ import LiteQRCode from '@svadmin/lite/components/LiteQRCode.svelte';
346
351
  | `LiteStepForm`, `LiteTableSummary`, `LiteVersionDiffViewer`, `LiteEditableTable`, `LiteDraggableRowTable` | Server-driven enterprise data interaction components |
347
352
  | `LiteMediaLibraryModal`, `LiteActivityFeed`, `LiteKanbanBoard`, `LitePivotTable` | Media, activity, workflow, and analysis views |
348
353
  | `LiteCanvasAnnotation`, `LiteSpreadsheetView`, `LiteDecisionTable` | SSR-compatible annotation, spreadsheet, and rules views |
354
+ | `SliderCaptcha`, `OfflineGuard`, `useNetworkStatus` | SPA-only components: CAPTCHA interaction, live offline trapping and reactive network state require client JavaScript |
349
355
  | `DevTools` | SPA-only component with no Lite counterpart |
350
356
 
351
357
  ## Parity Tracking & Visualization
@@ -355,7 +361,7 @@ To monitor and maintain 100% component parity between `@svadmin/ui` and `@svadmi
355
361
  - **CLI Dashboard**: Run `bun run check:parity` to inspect the live coverage matrix in terminal.
356
362
  - **Parity Matrix Document**: [PARITY.md](./PARITY.md) is auto-generated with exhaustive module-by-module tables.
357
363
  - **Machine-Readable JSON**: `packages/lite/parity.json` provides structured data for automation.
358
- - **Interactive Showroom**: Visit `/lite/parity` in the example app to interactively preview all 179 components and live metrics.
364
+ - **Interactive Showroom**: Visit `/lite/parity` in the example app to interactively preview all 193 components and live metrics.
359
365
  - **CI Contract Test**: `scripts/parity-contract.test.ts` runs in CI to prevent regression and ensure every new UI component has a documented Lite counterpart.
360
366
 
361
367
  ## Server Utilities
@@ -1,44 +1,98 @@
1
1
  <script lang="ts">
2
2
  /**
3
3
  * LiteAuditLog — SSR-compatible audit log viewer.
4
- * Pure HTML table with pagination links. No client-side JS required.
4
+ * Accepts both the legacy AuthProvider AuditLog shape and the current
5
+ * core AuditEntry shape, including field-level diffs.
5
6
  */
6
- import type { AuditLog } from '@svadmin/core';
7
+ import type { AuditLog, AuditEntry } from '@svadmin/core';
7
8
 
8
9
  interface Props {
9
- logs: AuditLog[];
10
+ logs?: AuditLog[];
11
+ entries?: AuditEntry[];
10
12
  total?: number;
11
13
  page?: number;
12
14
  pageSize?: number;
13
15
  basePath?: string;
16
+ title?: string;
14
17
  }
15
18
 
16
19
  let {
17
- logs,
20
+ logs = [],
21
+ entries = [],
18
22
  total = 0,
19
23
  page = 1,
20
24
  pageSize = 20,
21
25
  basePath = '?',
26
+ title = 'Audit Logs',
22
27
  }: Props = $props();
23
28
 
24
- const totalPages = $derived(Math.ceil(total / pageSize) || 1);
29
+ interface DisplayEntry {
30
+ id: string;
31
+ timestamp: string | Date;
32
+ user: string;
33
+ action: string;
34
+ resource: string;
35
+ ip: string;
36
+ changes: Array<{ field: string; oldValue: unknown; newValue: unknown }>;
37
+ }
38
+
39
+ function fieldChanges(entry: AuditEntry): Array<{ field: string; oldValue: unknown; newValue: unknown }> {
40
+ const previous = entry.previousData ?? {};
41
+ const current = entry.data ?? {};
42
+ const keys = new Set([...Object.keys(previous), ...Object.keys(current)]);
43
+ return [...keys]
44
+ .filter((key) => key !== 'id' && key !== 'updatedAt' && key !== 'updated_at')
45
+ .filter((key) => JSON.stringify(previous[key]) !== JSON.stringify(current[key]))
46
+ .map((key) => ({ field: key, oldValue: previous[key], newValue: current[key] }));
47
+ }
48
+
49
+ function displayValue(value: unknown): string {
50
+ if (value === null || value === undefined) return '—';
51
+ if (typeof value === 'object') return JSON.stringify(value);
52
+ return String(value);
53
+ }
25
54
 
26
- function formatDate(auditTimestamp: string | Date) {
27
- return new Date(auditTimestamp).toLocaleString();
55
+ const rows = $derived<DisplayEntry[]>(
56
+ entries.length > 0
57
+ ? entries.map((entry, index) => ({
58
+ id: String(entry.id ?? index),
59
+ timestamp: entry.timestamp,
60
+ user: entry.userId ?? '—',
61
+ action: entry.action,
62
+ resource: `${entry.resource ?? '—'}${entry.recordId != null ? ` #${entry.recordId}` : ''}`,
63
+ ip: entry.ipAddress ?? String(entry.meta?.['ip'] ?? '—'),
64
+ changes: fieldChanges(entry),
65
+ }))
66
+ : logs.map((log) => ({
67
+ id: String(log.id),
68
+ timestamp: log.createdAt,
69
+ user: log.userName ?? String(log.userId ?? '—'),
70
+ action: log.action,
71
+ resource: log.resource ?? '—',
72
+ ip: log.ipAddress ?? '—',
73
+ changes: [],
74
+ })),
75
+ );
76
+
77
+ const effectiveTotal = $derived(total || rows.length);
78
+ const totalPages = $derived(Math.max(1, Math.ceil(effectiveTotal / Math.max(1, pageSize))));
79
+
80
+ function formatDate(value: string | Date) {
81
+ return new Date(value).toLocaleString();
28
82
  }
29
83
 
30
84
  function actionClass(action: string): string {
31
- const normalizedAction = action.toLowerCase();
32
- if (normalizedAction.includes('delete') || normalizedAction.includes('remove')) return 'lite-badge-danger';
33
- if (normalizedAction.includes('create') || normalizedAction.includes('add')) return 'lite-badge-success';
34
- if (normalizedAction.includes('update') || normalizedAction.includes('edit')) return 'lite-badge-warning';
85
+ const normalized = action.toLowerCase();
86
+ if (normalized.includes('delete') || normalized.includes('remove')) return 'lite-badge-danger';
87
+ if (normalized.includes('create') || normalized.includes('add')) return 'lite-badge-success';
88
+ if (normalized.includes('update') || normalized.includes('edit')) return 'lite-badge-warning';
35
89
  return 'lite-badge-default';
36
90
  }
37
91
  </script>
38
92
 
39
93
  <div class="lite-audit-log">
40
- <h2 style="margin-bottom:16px;">Audit Logs</h2>
41
-
94
+ <h2>{title}</h2>
95
+
42
96
  <table class="lite-table">
43
97
  <thead>
44
98
  <tr>
@@ -46,50 +100,62 @@
46
100
  <th>User</th>
47
101
  <th>Action</th>
48
102
  <th>Resource</th>
103
+ <th>Changes</th>
49
104
  <th style="width:120px;">IP Address</th>
50
105
  </tr>
51
106
  </thead>
52
107
  <tbody>
53
- {#each logs as log, _i (_i)}
54
- <tr>
55
- <td style="font-family:monospace;font-size:12px;color:#4E5969;">{formatDate(log.createdAt)}</td>
56
- <td>{log.userName ?? '—'}</td>
57
- <td><span class="lite-badge {actionClass(log.action)}">{log.action}</span></td>
58
- <td style="color:#4E5969;">{log.resource ?? '—'}</td>
59
- <td style="font-family:monospace;font-size:12px;color:#4E5969;">{log.ipAddress ?? '—'}</td>
108
+ {#each rows as row (row.id)}
109
+ <tr data-audit-entry={row.id}>
110
+ <td style="font-family:monospace;font-size:12px;color:#4E5969;">{formatDate(row.timestamp)}</td>
111
+ <td>{row.user}</td>
112
+ <td><span class="lite-badge {actionClass(row.action)}">{row.action}</span></td>
113
+ <td style="color:#4E5969;">{row.resource}</td>
114
+ <td>
115
+ {#if row.changes.length > 0}
116
+ <ul class="lite-audit-diff">
117
+ {#each row.changes as change (change.field)}
118
+ <li>
119
+ <strong>{change.field}</strong>
120
+ <span class="lite-audit-old">{displayValue(change.oldValue)}</span>
121
+ <span aria-hidden="true">→</span>
122
+ <span class="lite-audit-new">{displayValue(change.newValue)}</span>
123
+ </li>
124
+ {/each}
125
+ </ul>
126
+ {:else}
127
+ <span style="color:#86909C;">—</span>
128
+ {/if}
129
+ </td>
130
+ <td style="font-family:monospace;font-size:12px;color:#4E5969;">{row.ip}</td>
60
131
  </tr>
61
132
  {:else}
62
133
  <tr>
63
- <td colspan="5" style="text-align:center;padding:24px;color:#86909C;">No audit logs found.</td>
134
+ <td colspan="6" style="text-align:center;padding:24px;color:#86909C;">No audit logs found.</td>
64
135
  </tr>
65
136
  {/each}
66
137
  </tbody>
67
138
  </table>
68
139
 
69
- <!-- Pagination -->
70
140
  {#if totalPages > 1}
71
141
  <div class="lite-pagination" style="margin-top:12px;">
72
142
  {#if page > 1}
73
- <a href={`${basePath}page={page - 1}`} class="lite-btn lite-btn-sm">&laquo; Prev</a>
143
+ <a href={`${basePath}page=${page - 1}`} class="lite-btn lite-btn-sm">&laquo; Prev</a>
74
144
  {/if}
75
- <span style="padding:0 12px;font-size:14px;color:#4E5969;">Page {page} / {totalPages} ({total} total)</span>
145
+ <span style="padding:0 12px;font-size:14px;color:#4E5969;">Page {page} / {totalPages} ({effectiveTotal} total)</span>
76
146
  {#if page < totalPages}
77
- <a href={`${basePath}page={page + 1}`} class="lite-btn lite-btn-sm">Next &raquo;</a>
147
+ <a href={`${basePath}page=${page + 1}`} class="lite-btn lite-btn-sm">Next &raquo;</a>
78
148
  {/if}
79
149
  </div>
80
150
  {/if}
81
151
  </div>
82
152
 
83
153
  <style>
84
- .lite-badge {
85
- display: inline-block;
86
- padding: 2px 8px;
87
- border-radius: 4px;
88
- font-size: 12px;
89
- font-weight: 600;
90
- }
91
- .lite-badge-danger { background: #FFECE8; color: #dc2626; border: 1px solid #FDCDC5; }
92
- .lite-badge-success { background: #f0fdf4; color: #009A29; border: 1px solid #bbf7d0; }
93
- .lite-badge-warning { background: #FFF7E8; color: #D25F00; border: 1px solid #FFE4BA; }
94
- .lite-badge-default { background: #F2F3F5; color: #4E5969; border: 1px solid #E5E6EB; }
154
+ .lite-audit-log h2 { margin: 0 0 16px; color: #1d2129; font-size: 16px; }
155
+ .lite-audit-diff { margin: 0; padding: 0; list-style: none; font-size: 11px; }
156
+ .lite-audit-diff li { display: flex; flex-wrap: wrap; align-items: baseline; }
157
+ .lite-audit-diff li + li { margin-top: 3px; }
158
+ .lite-audit-diff strong { margin-right: 4px; color: #1d2129; }
159
+ .lite-audit-old { margin-right: 4px; color: #f53f3f; text-decoration: line-through; }
160
+ .lite-audit-new { color: #00b42a; }
95
161
  </style>
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * LiteAuditLog — SSR-compatible audit log viewer.
3
- * Pure HTML table with pagination links. No client-side JS required.
3
+ * Accepts both the legacy AuthProvider AuditLog shape and the current
4
+ * core AuditEntry shape, including field-level diffs.
4
5
  */
5
- import type { AuditLog } from '@svadmin/core';
6
+ import type { AuditLog, AuditEntry } from '@svadmin/core';
6
7
  interface Props {
7
- logs: AuditLog[];
8
+ logs?: AuditLog[];
9
+ entries?: AuditEntry[];
8
10
  total?: number;
9
11
  page?: number;
10
12
  pageSize?: number;
11
13
  basePath?: string;
14
+ title?: string;
12
15
  }
13
16
  declare const LiteAuditLog: import("svelte").Component<Props, {}, "">;
14
17
  type LiteAuditLog = ReturnType<typeof LiteAuditLog>;
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ export interface LiteConflictField {
3
+ field: string;
4
+ label?: string;
5
+ localValue?: unknown;
6
+ remoteValue?: unknown;
7
+ }
8
+
9
+ interface Props {
10
+ open?: boolean;
11
+ title?: string;
12
+ description?: string;
13
+ fields?: LiteConflictField[];
14
+ formAction?: string;
15
+ namePrefix?: string;
16
+ modifiedBy?: string;
17
+ cancelHref?: string;
18
+ submitLabel?: string;
19
+ class?: string;
20
+ }
21
+
22
+ let {
23
+ open = true,
24
+ title = 'Resolve edit conflict',
25
+ description = 'The record changed while you were editing. Choose the value to keep for each field.',
26
+ fields = [],
27
+ formAction = '',
28
+ namePrefix = 'resolve',
29
+ modifiedBy,
30
+ cancelHref,
31
+ submitLabel = 'Save resolution',
32
+ class: className = '',
33
+ }: Props = $props();
34
+
35
+ function formatValue(value: unknown): string {
36
+ if (value === null || value === undefined) return '—';
37
+ if (typeof value === 'object') return JSON.stringify(value);
38
+ return String(value);
39
+ }
40
+ </script>
41
+
42
+ {#if open}
43
+ <div
44
+ class="lite-conflict-modal lite-card {className}"
45
+ role="dialog"
46
+ aria-modal="true"
47
+ tabindex="-1"
48
+ aria-labelledby="lite-conflict-title"
49
+ >
50
+ <header>
51
+ <h2 id="lite-conflict-title">{title}</h2>
52
+ {#if modifiedBy}<p class="lite-conflict-meta">Last modified by <strong>{modifiedBy}</strong></p>{/if}
53
+ <p>{description}</p>
54
+ </header>
55
+
56
+ {#if fields.length === 0}
57
+ <p class="lite-conflict-empty" role="status">No conflicting fields were provided.</p>
58
+ {:else}
59
+ <form method="POST" action={formAction}>
60
+ <table class="lite-table">
61
+ <thead>
62
+ <tr>
63
+ <th>Field</th>
64
+ <th>My value</th>
65
+ <th>Server value</th>
66
+ <th>Keep</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {#each fields as field (field.field)}
71
+ <tr data-conflict-field={field.field}>
72
+ <th scope="row">{field.label ?? field.field}</th>
73
+ <td><code>{formatValue(field.localValue)}</code></td>
74
+ <td><code>{formatValue(field.remoteValue)}</code></td>
75
+ <td>
76
+ <label>
77
+ <input type="radio" name={`${namePrefix}[${field.field}]`} value="local" checked />
78
+ Mine
79
+ </label>
80
+ <label>
81
+ <input type="radio" name={`${namePrefix}[${field.field}]`} value="remote" />
82
+ Theirs
83
+ </label>
84
+ </td>
85
+ </tr>
86
+ {/each}
87
+ </tbody>
88
+ </table>
89
+
90
+ <div class="lite-conflict-actions">
91
+ <button type="submit">{submitLabel}</button>
92
+ {#if cancelHref}<a href={cancelHref}>Discard and go back</a>{/if}
93
+ </div>
94
+ </form>
95
+ {/if}
96
+ </div>
97
+ {/if}
98
+
99
+ <style>
100
+ .lite-conflict-modal { padding: 16px; }
101
+ .lite-conflict-modal header h2 { margin: 0; color: #1d2129; font-size: 16px; }
102
+ .lite-conflict-modal header p { margin: 6px 0 12px; color: #4e5969; font-size: 12px; line-height: 1.5; }
103
+ .lite-conflict-meta { margin-bottom: 0 !important; }
104
+ .lite-conflict-empty { color: #86909c; }
105
+ .lite-conflict-modal code { display: inline-block; max-width: 220px; overflow-wrap: anywhere; padding: 2px 5px; border-radius: 3px; background: #f2f3f5; font-size: 11px; }
106
+ .lite-conflict-actions { display: flex; align-items: center; margin-top: 12px; }
107
+ .lite-conflict-actions > * + * { margin-left: 12px; }
108
+ .lite-conflict-actions button { padding: 7px 12px; border: 0; border-radius: 4px; background: #165dff; color: #fff; font-size: 12px; cursor: pointer; }
109
+ .lite-conflict-actions a { color: #4e5969; font-size: 12px; }
110
+ </style>
@@ -0,0 +1,21 @@
1
+ export interface LiteConflictField {
2
+ field: string;
3
+ label?: string;
4
+ localValue?: unknown;
5
+ remoteValue?: unknown;
6
+ }
7
+ interface Props {
8
+ open?: boolean;
9
+ title?: string;
10
+ description?: string;
11
+ fields?: LiteConflictField[];
12
+ formAction?: string;
13
+ namePrefix?: string;
14
+ modifiedBy?: string;
15
+ cancelHref?: string;
16
+ submitLabel?: string;
17
+ class?: string;
18
+ }
19
+ declare const LiteConcurrentConflictModal: import("svelte").Component<Props, {}, "">;
20
+ type LiteConcurrentConflictModal = ReturnType<typeof LiteConcurrentConflictModal>;
21
+ export default LiteConcurrentConflictModal;
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ interface CronPreset {
3
+ label: string;
4
+ value: string;
5
+ }
6
+
7
+ interface Props {
8
+ value?: string;
9
+ name?: string;
10
+ label?: string;
11
+ description?: string;
12
+ presets?: CronPreset[];
13
+ formAction?: string;
14
+ disabled?: boolean;
15
+ class?: string;
16
+ }
17
+
18
+ let {
19
+ value = '',
20
+ name = 'cron',
21
+ label = 'Cron expression',
22
+ description = 'Use a preset value or edit the expression directly; the visual builder is a client enhancement.',
23
+ presets = [
24
+ { label: 'Every minute', value: '* * * * *' },
25
+ { label: 'Every hour', value: '0 * * * *' },
26
+ { label: 'Every day at midnight', value: '0 0 * * *' },
27
+ { label: 'Every Monday at 09:00', value: '0 9 * * 1' },
28
+ ],
29
+ formAction,
30
+ disabled = false,
31
+ class: className = '',
32
+ }: Props = $props();
33
+
34
+ let expression = $state(value);
35
+ </script>
36
+
37
+ <section class="lite-cron-builder lite-card {className}" aria-label={label}>
38
+ <header>
39
+ <h2>{label}</h2>
40
+ {#if description}<p>{description}</p>{/if}
41
+ </header>
42
+
43
+ <div class="lite-cron-preset">
44
+ <span class="lite-cron-preset-label">Presets</span>
45
+ <ul>
46
+ {#each presets as preset (preset.value)}
47
+ <li><strong>{preset.label}</strong> <code>{preset.value}</code></li>
48
+ {/each}
49
+ </ul>
50
+ </div>
51
+
52
+ {#if formAction}
53
+ <form method="POST" action={formAction}>
54
+ <label for="lite-cron-expression">Expression</label>
55
+ <input id="lite-cron-expression" {name} bind:value={expression} {disabled} autocomplete="off" spellcheck="false" />
56
+ <button type="submit" {disabled}>Save schedule</button>
57
+ </form>
58
+ {:else}
59
+ <label for="lite-cron-expression">Expression</label>
60
+ <input id="lite-cron-expression" {name} bind:value={expression} {disabled} autocomplete="off" spellcheck="false" />
61
+ {/if}
62
+
63
+ <code class="lite-cron-preview">{expression || '—'}</code>
64
+ </section>
65
+
66
+ <style>
67
+ .lite-cron-builder { padding: 16px; }
68
+ .lite-cron-builder header h2 { margin: 0; color: #1d2129; font-size: 16px; }
69
+ .lite-cron-builder header p { margin: 4px 0 12px; color: #4e5969; font-size: 12px; line-height: 1.5; }
70
+ .lite-cron-builder label { display: block; margin: 10px 0 4px; color: #4e5969; font-size: 12px; font-weight: 600; }
71
+ .lite-cron-preset-label { display: block; margin: 10px 0 4px; color: #4e5969; font-size: 12px; font-weight: 600; }
72
+ .lite-cron-preset ul { margin: 0; padding: 0; list-style: none; }
73
+ .lite-cron-preset li { padding: 5px 0; color: #4e5969; font-size: 12px; }
74
+ .lite-cron-preset li strong { margin-right: 6px; color: #1d2129; }
75
+ .lite-cron-builder input { width: 100%; box-sizing: border-box; padding: 7px 9px; border: 1px solid #e5e6eb; border-radius: 4px; background: #fff; color: #1d2129; font: inherit; font-size: 13px; }
76
+ .lite-cron-builder button { margin-top: 10px; padding: 7px 12px; border: 0; border-radius: 4px; background: #165dff; color: #fff; font-size: 12px; cursor: pointer; }
77
+ .lite-cron-preview { display: block; margin-top: 10px; padding: 8px 10px; border-radius: 4px; background: #f7f8fa; color: #1d2129; font-size: 12px; }
78
+ </style>
@@ -0,0 +1,17 @@
1
+ interface CronPreset {
2
+ label: string;
3
+ value: string;
4
+ }
5
+ interface Props {
6
+ value?: string;
7
+ name?: string;
8
+ label?: string;
9
+ description?: string;
10
+ presets?: CronPreset[];
11
+ formAction?: string;
12
+ disabled?: boolean;
13
+ class?: string;
14
+ }
15
+ declare const LiteCronExpressionBuilder: import("svelte").Component<Props, {}, "">;
16
+ type LiteCronExpressionBuilder = ReturnType<typeof LiteCronExpressionBuilder>;
17
+ export default LiteCronExpressionBuilder;
@@ -0,0 +1,143 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import LiteStatsCard from './LiteStatsCard.svelte';
4
+
5
+ type MetricColumns = 1 | 2 | 3 | 4 | 5 | 6 | 'auto';
6
+ type MetricGap = 'sm' | 'md' | 'lg';
7
+
8
+ interface LiteMetricGridItem {
9
+ id?: string;
10
+ label: string;
11
+ value: string | number;
12
+ description?: string;
13
+ trend?: 'up' | 'down' | 'neutral';
14
+ trendValue?: string;
15
+ icon?: string;
16
+ href?: string;
17
+ }
18
+
19
+ interface Props {
20
+ items?: LiteMetricGridItem[];
21
+ cols?: MetricColumns;
22
+ gap?: MetricGap;
23
+ title?: string;
24
+ description?: string;
25
+ loading?: boolean;
26
+ skeletonCount?: number;
27
+ empty?: boolean;
28
+ emptyMessage?: string;
29
+ class?: string;
30
+ actions?: Snippet;
31
+ children?: Snippet;
32
+ emptyContent?: Snippet;
33
+ }
34
+
35
+ let {
36
+ items = [],
37
+ cols = 'auto',
38
+ gap = 'md',
39
+ title,
40
+ description,
41
+ loading = false,
42
+ skeletonCount = 4,
43
+ empty = false,
44
+ emptyMessage = 'No metrics available',
45
+ class: className = '',
46
+ actions,
47
+ children,
48
+ emptyContent,
49
+ }: Props = $props();
50
+
51
+ const skeletonItems = $derived(Array.from({ length: Math.max(1, skeletonCount) }, (_, index) => index));
52
+ </script>
53
+
54
+ <section
55
+ class="lite-metric-grid lite-metric-grid-{cols} lite-metric-grid-gap-{gap} {className}"
56
+ aria-label={title ?? 'Dashboard metrics'}
57
+ data-columns={cols}
58
+ >
59
+ {#if title || description || actions}
60
+ <header class="lite-metric-grid-header">
61
+ <div>
62
+ {#if title}<h2>{title}</h2>{/if}
63
+ {#if description}<p>{description}</p>{/if}
64
+ </div>
65
+ {#if actions}<div class="lite-metric-grid-actions">{@render actions()}</div>{/if}
66
+ </header>
67
+ {/if}
68
+
69
+ {#if loading}
70
+ <div class="lite-metric-grid-items">
71
+ {#each skeletonItems as item (item)}
72
+ <div class="lite-metric-grid-cell">
73
+ <div class="lite-metric-grid-skeleton" aria-hidden="true">
74
+ <span></span><span></span><span></span>
75
+ </div>
76
+ </div>
77
+ {/each}
78
+ </div>
79
+ {:else if empty}
80
+ <div class="lite-metric-grid-empty" role="status">
81
+ {#if emptyContent}{@render emptyContent()}{:else}<p>{emptyMessage}</p>{/if}
82
+ </div>
83
+ {:else if children}
84
+ {@render children()}
85
+ {:else if items.length === 0}
86
+ <div class="lite-metric-grid-empty" role="status"><p>{emptyMessage}</p></div>
87
+ {:else}
88
+ <div class="lite-metric-grid-items">
89
+ {#each items as item (item.id ?? item.label)}
90
+ <div class="lite-metric-grid-cell">
91
+ <LiteStatsCard
92
+ label={item.label}
93
+ value={item.value}
94
+ description={item.description}
95
+ trend={item.trend}
96
+ trendValue={item.trendValue}
97
+ icon={item.icon}
98
+ href={item.href}
99
+ />
100
+ </div>
101
+ {/each}
102
+ </div>
103
+ {/if}
104
+ </section>
105
+
106
+ <style>
107
+ .lite-metric-grid { display: block; }
108
+ .lite-metric-grid-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 12px; }
109
+ .lite-metric-grid-header > div + div { margin-left: 12px; }
110
+ .lite-metric-grid-header h2 { margin: 0; color: #1d2129; font-size: 16px; }
111
+ .lite-metric-grid-header p { margin: 4px 0 0; color: #4e5969; font-size: 12px; }
112
+ .lite-metric-grid-actions { display: flex; }
113
+ .lite-metric-grid-actions > * + * { margin-left: 8px; }
114
+ .lite-metric-grid-items { display: flex; flex-wrap: wrap; margin: 0 -8px; }
115
+ .lite-metric-grid-cell { box-sizing: border-box; width: 100%; padding: 0 8px; margin-bottom: 16px; }
116
+ .lite-metric-grid-gap-sm .lite-metric-grid-items { margin: 0 -6px; }
117
+ .lite-metric-grid-gap-sm .lite-metric-grid-cell { padding: 0 6px; margin-bottom: 12px; }
118
+ .lite-metric-grid-gap-lg .lite-metric-grid-items { margin: 0 -12px; }
119
+ .lite-metric-grid-gap-lg .lite-metric-grid-cell { padding: 0 12px; margin-bottom: 24px; }
120
+ .lite-metric-grid-auto .lite-metric-grid-cell { width: 25%; }
121
+ .lite-metric-grid-1 .lite-metric-grid-cell { width: 100%; }
122
+ .lite-metric-grid-2 .lite-metric-grid-cell { width: 50%; }
123
+ .lite-metric-grid-3 .lite-metric-grid-cell { width: 33.3333%; }
124
+ .lite-metric-grid-4 .lite-metric-grid-cell { width: 25%; }
125
+ .lite-metric-grid-5 .lite-metric-grid-cell { width: 20%; }
126
+ .lite-metric-grid-6 .lite-metric-grid-cell { width: 16.6666%; }
127
+ .lite-metric-grid-skeleton { min-height: 96px; border: 1px solid #e5e6eb; border-radius: 6px; padding: 16px; background: #f7f8fa; }
128
+ .lite-metric-grid-skeleton span { display: block; height: 10px; margin-bottom: 10px; border-radius: 3px; background: #e5e6eb; }
129
+ .lite-metric-grid-skeleton span:first-child { width: 45%; }
130
+ .lite-metric-grid-skeleton span:nth-child(2) { width: 70%; height: 20px; }
131
+ .lite-metric-grid-skeleton span:last-child { width: 35%; margin-bottom: 0; }
132
+ .lite-metric-grid-empty { border: 1px dashed #e5e6eb; border-radius: 6px; padding: 24px; color: #86909c; text-align: center; background: #f7f8fa; }
133
+ @media (max-width: 900px) {
134
+ .lite-metric-grid-auto .lite-metric-grid-cell,
135
+ .lite-metric-grid-3 .lite-metric-grid-cell,
136
+ .lite-metric-grid-4 .lite-metric-grid-cell,
137
+ .lite-metric-grid-5 .lite-metric-grid-cell,
138
+ .lite-metric-grid-6 .lite-metric-grid-cell { width: 50%; }
139
+ }
140
+ @media (max-width: 560px) {
141
+ .lite-metric-grid-cell { width: 100% !important; }
142
+ }
143
+ </style>