@svadmin/lite 0.25.7 → 0.27.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 +11 -5
- package/dist/components/LiteAnnouncementBanner.svelte +88 -0
- package/dist/components/LiteAnnouncementBanner.svelte.d.ts +12 -0
- package/dist/components/LiteAsyncExportModal.svelte +113 -0
- package/dist/components/LiteAsyncExportModal.svelte.d.ts +14 -0
- package/dist/components/LiteAuditLog.svelte +102 -36
- package/dist/components/LiteAuditLog.svelte.d.ts +6 -3
- package/dist/components/LiteConcurrentConflictModal.svelte +110 -0
- package/dist/components/LiteConcurrentConflictModal.svelte.d.ts +21 -0
- package/dist/components/LiteContextMenu.svelte +114 -0
- package/dist/components/LiteContextMenu.svelte.d.ts +18 -0
- package/dist/components/LiteCronExpressionBuilder.svelte +78 -0
- package/dist/components/LiteCronExpressionBuilder.svelte.d.ts +17 -0
- package/dist/components/LiteDualApprovalGate.svelte +58 -0
- package/dist/components/LiteDualApprovalGate.svelte.d.ts +11 -0
- package/dist/components/LiteJsonViewer.svelte +88 -0
- package/dist/components/LiteJsonViewer.svelte.d.ts +9 -0
- package/dist/components/LiteKeyboardShortcutsModal.svelte +56 -0
- package/dist/components/LiteKeyboardShortcutsModal.svelte.d.ts +13 -0
- package/dist/components/LiteKpiCard.svelte +169 -0
- package/dist/components/LiteKpiCard.svelte.d.ts +25 -0
- package/dist/components/LiteMetricGrid.svelte +143 -0
- package/dist/components/LiteMetricGrid.svelte.d.ts +31 -0
- package/dist/components/LiteMicroAppFrame.svelte +57 -0
- package/dist/components/LiteMicroAppFrame.svelte.d.ts +15 -0
- package/dist/components/LiteOrgChart.svelte +78 -0
- package/dist/components/LiteOrgChart.svelte.d.ts +17 -0
- package/dist/components/LitePresenceAvatarGroup.svelte +118 -15
- package/dist/components/LitePresenceAvatarGroup.svelte.d.ts +8 -3
- package/dist/components/LiteResultPage.svelte +135 -0
- package/dist/components/LiteResultPage.svelte.d.ts +17 -0
- package/dist/components/LiteSafeDeleteConfirm.svelte +86 -0
- package/dist/components/LiteSafeDeleteConfirm.svelte.d.ts +21 -0
- package/dist/components/LiteSensitiveDataMask.svelte +34 -2
- package/dist/components/LiteSensitiveDataMask.svelte.d.ts +1 -2
- package/dist/components/LiteSignaturePad.svelte +3 -1
- package/dist/components/LiteSignaturePad.svelte.d.ts +1 -0
- package/dist/components/LiteStepWizard.svelte +98 -0
- package/dist/components/LiteStepWizard.svelte.d.ts +16 -0
- package/dist/components/LiteTaskCenter.svelte +133 -0
- package/dist/components/LiteTaskCenter.svelte.d.ts +24 -0
- package/dist/components/LiteTextDiffViewer.svelte +111 -0
- package/dist/components/LiteTextDiffViewer.svelte.d.ts +10 -0
- package/dist/components/LiteTour.svelte +4 -0
- package/dist/components/LiteTour.svelte.d.ts +4 -0
- package/dist/components/LiteTrendIndicator.svelte +119 -0
- package/dist/components/LiteTrendIndicator.svelte.d.ts +19 -0
- package/dist/components/LiteUnsavedChangesGuard.svelte +53 -0
- package/dist/components/LiteUnsavedChangesGuard.svelte.d.ts +10 -0
- package/dist/components/LiteVirtualTreeSelect.svelte +92 -0
- package/dist/components/LiteVirtualTreeSelect.svelte.d.ts +20 -0
- package/dist/components/LiteWatermarkContainer.svelte +58 -0
- package/dist/components/LiteWatermarkContainer.svelte.d.ts +9 -0
- package/dist/components/LiteWorkflowStepper.svelte +56 -0
- package/dist/components/LiteWorkflowStepper.svelte.d.ts +14 -0
- package/dist/components/widgets/LiteGaugeChart.svelte +50 -3
- package/dist/components/widgets/LiteGaugeChart.svelte.d.ts +12 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +24 -0
- package/dist/rich-content.js +1 -1
- 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%
|
|
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
|
|
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
|
|
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
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type BannerTone = 'info' | 'warning' | 'error' | 'success';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
message: string;
|
|
6
|
+
tone?: BannerTone;
|
|
7
|
+
countdownText?: string;
|
|
8
|
+
actionText?: string;
|
|
9
|
+
actionHref?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
message,
|
|
15
|
+
tone = 'warning',
|
|
16
|
+
countdownText,
|
|
17
|
+
actionText,
|
|
18
|
+
actionHref,
|
|
19
|
+
class: className = '',
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
const iconChar = $derived.by(() => {
|
|
23
|
+
switch (tone) {
|
|
24
|
+
case 'info':
|
|
25
|
+
return 'ℹ';
|
|
26
|
+
case 'success':
|
|
27
|
+
return '✓';
|
|
28
|
+
case 'error':
|
|
29
|
+
return '✕';
|
|
30
|
+
case 'warning':
|
|
31
|
+
default:
|
|
32
|
+
return '⚠';
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<aside
|
|
38
|
+
class="lite-announcement-banner lite-banner-{tone} {className}"
|
|
39
|
+
data-svadmin-component="LiteAnnouncementBanner"
|
|
40
|
+
role="alert"
|
|
41
|
+
>
|
|
42
|
+
<div class="lite-banner-body">
|
|
43
|
+
<span class="lite-banner-icon" aria-hidden="true">{iconChar}</span>
|
|
44
|
+
<span class="lite-banner-message">{message}</span>
|
|
45
|
+
|
|
46
|
+
{#if countdownText}
|
|
47
|
+
<span class="lite-banner-countdown">{countdownText}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
{#if actionText}
|
|
51
|
+
{#if actionHref}
|
|
52
|
+
<a href={actionHref} class="lite-banner-action">{actionText}</a>
|
|
53
|
+
{:else}
|
|
54
|
+
<span class="lite-banner-action">{actionText}</span>
|
|
55
|
+
{/if}
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.lite-announcement-banner {
|
|
62
|
+
padding: 8px 16px;
|
|
63
|
+
border-bottom: 1px solid transparent;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
.lite-banner-info { background: #e8f3ff; color: #165dff; border-color: #bedaff; }
|
|
68
|
+
.lite-banner-warning { background: #fff7e8; color: #d46b08; border-color: #ffd591; }
|
|
69
|
+
.lite-banner-error { background: #fff1f0; color: #cf1322; border-color: #ffa39e; }
|
|
70
|
+
.lite-banner-success { background: #e8ffea; color: #00b42a; border-color: #aff0b5; }
|
|
71
|
+
.lite-banner-body { display: flex; align-items: center; flex-wrap: wrap; }
|
|
72
|
+
.lite-banner-body > * + * { margin-left: 8px; }
|
|
73
|
+
.lite-banner-icon { font-weight: 700; font-size: 14px; }
|
|
74
|
+
.lite-banner-message { font-weight: 500; flex: 1; min-width: 200px; }
|
|
75
|
+
.lite-banner-countdown {
|
|
76
|
+
font-family: monospace;
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
padding: 2px 6px;
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
background: rgba(0, 0, 0, 0.05);
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
.lite-banner-action {
|
|
84
|
+
text-decoration: underline;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
color: inherit;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type BannerTone = 'info' | 'warning' | 'error' | 'success';
|
|
2
|
+
interface Props {
|
|
3
|
+
message: string;
|
|
4
|
+
tone?: BannerTone;
|
|
5
|
+
countdownText?: string;
|
|
6
|
+
actionText?: string;
|
|
7
|
+
actionHref?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const LiteAnnouncementBanner: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type LiteAnnouncementBanner = ReturnType<typeof LiteAnnouncementBanner>;
|
|
12
|
+
export default LiteAnnouncementBanner;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface ExportCol {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
action?: string;
|
|
10
|
+
resourceName?: string;
|
|
11
|
+
columns?: ExportCol[];
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
open = true,
|
|
17
|
+
action = '/api/export',
|
|
18
|
+
resourceName = 'Records',
|
|
19
|
+
columns = [],
|
|
20
|
+
class: className = '',
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if open}
|
|
25
|
+
<section
|
|
26
|
+
class="lite-export-modal {className}"
|
|
27
|
+
data-svadmin-component="LiteAsyncExportModal"
|
|
28
|
+
aria-label="Export {resourceName}"
|
|
29
|
+
>
|
|
30
|
+
<header class="lite-export-header">
|
|
31
|
+
<h3 class="lite-export-title">Export {resourceName}</h3>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<form method="POST" action={action} class="lite-export-form">
|
|
35
|
+
<div class="lite-export-group">
|
|
36
|
+
<label for="lite-export-filename">File Name</label>
|
|
37
|
+
<input type="text" id="lite-export-filename" name="filename" value="{resourceName}-Export" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="lite-export-group">
|
|
41
|
+
<span class="lite-export-label">Format</span>
|
|
42
|
+
<div class="lite-export-radios">
|
|
43
|
+
<label><input type="radio" name="format" value="xlsx" checked /> Excel (.xlsx)</label>
|
|
44
|
+
<label><input type="radio" name="format" value="csv" /> CSV (.csv)</label>
|
|
45
|
+
<label><input type="radio" name="format" value="json" /> JSON (.json)</label>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{#if columns.length > 0}
|
|
50
|
+
<div class="lite-export-group">
|
|
51
|
+
<span class="lite-export-label">Columns</span>
|
|
52
|
+
<div class="lite-export-columns">
|
|
53
|
+
{#each columns as col (col.key)}
|
|
54
|
+
<label class="lite-export-col-item">
|
|
55
|
+
<input type="checkbox" name="columns" value={col.key} checked />
|
|
56
|
+
<span>{col.label}</span>
|
|
57
|
+
</label>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
62
|
+
|
|
63
|
+
<div class="lite-export-actions">
|
|
64
|
+
<button type="submit" class="lite-export-submit">Start Export</button>
|
|
65
|
+
</div>
|
|
66
|
+
</form>
|
|
67
|
+
</section>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.lite-export-modal {
|
|
72
|
+
border: 1px solid #e5e6eb;
|
|
73
|
+
border-radius: 8px;
|
|
74
|
+
padding: 16px;
|
|
75
|
+
background: #ffffff;
|
|
76
|
+
max-width: 480px;
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
}
|
|
79
|
+
.lite-export-header { margin-bottom: 12px; }
|
|
80
|
+
.lite-export-title { margin: 0; font-size: 15px; color: #1d2129; font-weight: 600; }
|
|
81
|
+
.lite-export-group { margin-bottom: 12px; font-size: 13px; }
|
|
82
|
+
.lite-export-group label, .lite-export-label { display: block; margin-bottom: 4px; font-weight: 500; color: #4e5969; }
|
|
83
|
+
.lite-export-group input[type="text"] {
|
|
84
|
+
width: 100%;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
padding: 6px 10px;
|
|
87
|
+
border: 1px solid #e5e6eb;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
}
|
|
90
|
+
.lite-export-radios { display: flex; margin-top: 4px; }
|
|
91
|
+
.lite-export-radios > * + * { margin-left: 12px; }
|
|
92
|
+
.lite-export-columns {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-wrap: wrap;
|
|
95
|
+
max-height: 140px;
|
|
96
|
+
overflow-y: auto;
|
|
97
|
+
padding: 6px;
|
|
98
|
+
background: #f7f8fa;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
.lite-export-col-item { display: flex; align-items: center; width: 50%; font-size: 12px; }
|
|
102
|
+
.lite-export-col-item > * + * { margin-left: 6px; }
|
|
103
|
+
.lite-export-actions { margin-top: 16px; display: flex; justify-content: flex-end; }
|
|
104
|
+
.lite-export-submit {
|
|
105
|
+
padding: 7px 16px;
|
|
106
|
+
background: #165dff;
|
|
107
|
+
color: #ffffff;
|
|
108
|
+
border: 0;
|
|
109
|
+
border-radius: 4px;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ExportCol {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
action?: string;
|
|
8
|
+
resourceName?: string;
|
|
9
|
+
columns?: ExportCol[];
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const LiteAsyncExportModal: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type LiteAsyncExportModal = ReturnType<typeof LiteAsyncExportModal>;
|
|
14
|
+
export default LiteAsyncExportModal;
|
|
@@ -1,44 +1,98 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* LiteAuditLog — SSR-compatible audit log viewer.
|
|
4
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
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
|
|
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
|
|
54
|
-
<tr>
|
|
55
|
-
<td style="font-family:monospace;font-size:12px;color:#4E5969;">{formatDate(
|
|
56
|
-
<td>{
|
|
57
|
-
<td><span class="lite-badge {actionClass(
|
|
58
|
-
<td style="color:#4E5969;">{
|
|
59
|
-
<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="
|
|
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
|
|
143
|
+
<a href={`${basePath}page=${page - 1}`} class="lite-btn lite-btn-sm">« Prev</a>
|
|
74
144
|
{/if}
|
|
75
|
-
<span style="padding:0 12px;font-size:14px;color:#4E5969;">Page {page} / {totalPages} ({
|
|
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
|
|
147
|
+
<a href={`${basePath}page=${page + 1}`} class="lite-btn lite-btn-sm">Next »</a>
|
|
78
148
|
{/if}
|
|
79
149
|
</div>
|
|
80
150
|
{/if}
|
|
81
151
|
</div>
|
|
82
152
|
|
|
83
153
|
<style>
|
|
84
|
-
.lite-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
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
|
|
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;
|