@svadmin/lite 0.26.0 → 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.
Files changed (31) hide show
  1. package/dist/components/LiteAnnouncementBanner.svelte +88 -0
  2. package/dist/components/LiteAnnouncementBanner.svelte.d.ts +12 -0
  3. package/dist/components/LiteAsyncExportModal.svelte +113 -0
  4. package/dist/components/LiteAsyncExportModal.svelte.d.ts +14 -0
  5. package/dist/components/LiteContextMenu.svelte +114 -0
  6. package/dist/components/LiteContextMenu.svelte.d.ts +18 -0
  7. package/dist/components/LiteDualApprovalGate.svelte +58 -0
  8. package/dist/components/LiteDualApprovalGate.svelte.d.ts +11 -0
  9. package/dist/components/LiteJsonViewer.svelte +88 -0
  10. package/dist/components/LiteJsonViewer.svelte.d.ts +9 -0
  11. package/dist/components/LiteKeyboardShortcutsModal.svelte +56 -0
  12. package/dist/components/LiteKeyboardShortcutsModal.svelte.d.ts +13 -0
  13. package/dist/components/LiteKpiCard.svelte +169 -0
  14. package/dist/components/LiteKpiCard.svelte.d.ts +25 -0
  15. package/dist/components/LiteResultPage.svelte +135 -0
  16. package/dist/components/LiteResultPage.svelte.d.ts +17 -0
  17. package/dist/components/LiteSensitiveDataMask.svelte +34 -2
  18. package/dist/components/LiteSensitiveDataMask.svelte.d.ts +1 -2
  19. package/dist/components/LiteStepWizard.svelte +98 -0
  20. package/dist/components/LiteStepWizard.svelte.d.ts +16 -0
  21. package/dist/components/LiteTextDiffViewer.svelte +111 -0
  22. package/dist/components/LiteTextDiffViewer.svelte.d.ts +10 -0
  23. package/dist/components/LiteUnsavedChangesGuard.svelte +53 -0
  24. package/dist/components/LiteUnsavedChangesGuard.svelte.d.ts +10 -0
  25. package/dist/components/LiteWatermarkContainer.svelte +58 -0
  26. package/dist/components/LiteWatermarkContainer.svelte.d.ts +9 -0
  27. package/dist/components/LiteWorkflowStepper.svelte +56 -0
  28. package/dist/components/LiteWorkflowStepper.svelte.d.ts +14 -0
  29. package/dist/index.d.ts +17 -0
  30. package/dist/index.js +14 -0
  31. package/package.json +1 -1
@@ -0,0 +1,169 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface TargetProgress {
5
+ current: number;
6
+ target: number;
7
+ label?: string;
8
+ }
9
+
10
+ interface Props {
11
+ title: string;
12
+ value: number | string;
13
+ prefix?: string;
14
+ suffix?: string;
15
+ subtitle?: string;
16
+ trendText?: string;
17
+ trendDirection?: 'up' | 'down' | 'neutral';
18
+ targetProgress?: TargetProgress;
19
+ sparklineData?: number[];
20
+ alertStatus?: 'normal' | 'warning' | 'danger';
21
+ alertMessage?: string;
22
+ class?: string;
23
+ actions?: Snippet;
24
+ children?: Snippet;
25
+ }
26
+
27
+ let {
28
+ title,
29
+ value,
30
+ prefix = '',
31
+ suffix = '',
32
+ subtitle,
33
+ trendText,
34
+ trendDirection = 'neutral',
35
+ targetProgress,
36
+ sparklineData,
37
+ alertStatus = 'normal',
38
+ alertMessage,
39
+ class: className = '',
40
+ actions,
41
+ children,
42
+ }: Props = $props();
43
+
44
+ const formattedValue = $derived(
45
+ typeof value === 'number' ? value.toLocaleString('en-US') : String(value)
46
+ );
47
+
48
+ const progressPercent = $derived.by(() => {
49
+ if (!targetProgress || targetProgress.target <= 0) return 0;
50
+ const pct = (targetProgress.current / targetProgress.target) * 100;
51
+ return Math.max(0, Math.min(100, Math.round(pct)));
52
+ });
53
+
54
+ const sparklinePoints = $derived.by(() => {
55
+ if (!sparklineData || sparklineData.length < 2) return '';
56
+ const min = Math.min(...sparklineData);
57
+ const max = Math.max(...sparklineData);
58
+ const range = max - min || 1;
59
+ const width = 100;
60
+ const height = 30;
61
+ const padding = 2;
62
+
63
+ return sparklineData
64
+ .map((val, idx) => {
65
+ const x = padding + (idx / (sparklineData.length - 1)) * (width - padding * 2);
66
+ const y = height - padding - ((val - min) / range) * (height - padding * 2);
67
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
68
+ })
69
+ .join(' ');
70
+ });
71
+ </script>
72
+
73
+ <div
74
+ class="lite-kpi-card lite-kpi-card-{alertStatus} {className}"
75
+ data-svadmin-component="LiteKpiCard"
76
+ role="region"
77
+ aria-label={title}
78
+ >
79
+ <header class="lite-kpi-card-header">
80
+ <div class="lite-kpi-card-title-group">
81
+ <h4 class="lite-kpi-card-title">{title}</h4>
82
+ {#if subtitle}
83
+ <span class="lite-kpi-card-subtitle">{subtitle}</span>
84
+ {/if}
85
+ </div>
86
+ {#if actions}
87
+ <div class="lite-kpi-card-actions">{@render actions()}</div>
88
+ {/if}
89
+ </header>
90
+
91
+ <div class="lite-kpi-card-body">
92
+ <div class="lite-kpi-card-metric">
93
+ {#if prefix}<span class="lite-kpi-card-prefix">{prefix}</span>{/if}
94
+ <span class="lite-kpi-card-value">{formattedValue}</span>
95
+ {#if suffix}<span class="lite-kpi-card-suffix">{suffix}</span>{/if}
96
+ </div>
97
+
98
+ {#if trendText}
99
+ <div class="lite-kpi-card-trend lite-kpi-card-trend-{trendDirection}">
100
+ {#if trendDirection === 'up'}↑{:else if trendDirection === 'down'}↓{/if}
101
+ <span>{trendText}</span>
102
+ </div>
103
+ {/if}
104
+
105
+ {#if sparklinePoints}
106
+ <div class="lite-kpi-card-sparkline">
107
+ <svg viewBox="0 0 100 30" class="lite-kpi-sparkline-svg" aria-hidden="true">
108
+ <polyline points={sparklinePoints} fill="none" stroke="currentColor" stroke-width="2" />
109
+ </svg>
110
+ </div>
111
+ {/if}
112
+ </div>
113
+
114
+ {#if targetProgress}
115
+ <div class="lite-kpi-card-progress">
116
+ <div class="lite-kpi-progress-label">
117
+ <span>{targetProgress.label ?? 'Target progress'}</span>
118
+ <span class="lite-kpi-progress-pct">{progressPercent}%</span>
119
+ </div>
120
+ <div class="lite-kpi-progress-track">
121
+ <div class="lite-kpi-progress-bar" style="width: {progressPercent}%;"></div>
122
+ </div>
123
+ </div>
124
+ {/if}
125
+
126
+ {#if alertMessage && alertStatus !== 'normal'}
127
+ <div class="lite-kpi-card-alert lite-kpi-alert-{alertStatus}" role="alert">
128
+ <span>{alertMessage}</span>
129
+ </div>
130
+ {/if}
131
+
132
+ {#if children}
133
+ <div class="lite-kpi-card-extra">{@render children()}</div>
134
+ {/if}
135
+ </div>
136
+
137
+ <style>
138
+ .lite-kpi-card {
139
+ border: 1px solid #e5e6eb;
140
+ border-radius: 8px;
141
+ padding: 16px;
142
+ background: #ffffff;
143
+ box-sizing: border-box;
144
+ }
145
+ .lite-kpi-card-warning { border-color: #ffc766; background: #fffdf5; }
146
+ .lite-kpi-card-danger { border-color: #ffd8d2; background: #fff7f6; }
147
+ .lite-kpi-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
148
+ .lite-kpi-card-title { margin: 0; font-size: 13px; font-weight: 600; color: #4e5969; }
149
+ .lite-kpi-card-subtitle { font-size: 11px; color: #86909c; display: block; margin-top: 2px; }
150
+ .lite-kpi-card-body { display: flex; align-items: baseline; flex-wrap: wrap; margin-bottom: 8px; }
151
+ .lite-kpi-card-body > * + * { margin-left: 8px; }
152
+ .lite-kpi-card-metric { font-family: monospace; font-size: 24px; font-weight: 700; color: #1d2129; }
153
+ .lite-kpi-card-prefix, .lite-kpi-card-suffix { font-size: 13px; font-weight: 400; color: #86909c; margin: 0 2px; }
154
+ .lite-kpi-card-trend { font-size: 11px; font-weight: 600; padding: 2px 6px; border-radius: 4px; display: inline-flex; align-items: center; }
155
+ .lite-kpi-card-trend > * + * { margin-left: 2px; }
156
+ .lite-kpi-card-trend-up { color: #00b42a; background: #e8ffea; }
157
+ .lite-kpi-card-trend-down { color: #f53f3f; background: #ffece8; }
158
+ .lite-kpi-card-trend-neutral { color: #4e5969; background: #f2f3f5; }
159
+ .lite-kpi-card-sparkline { margin-left: auto; width: 80px; height: 24px; color: #165dff; }
160
+ .lite-kpi-sparkline-svg { width: 100%; height: 100%; display: block; }
161
+ .lite-kpi-card-progress { margin-top: 8px; }
162
+ .lite-kpi-progress-label { display: flex; justify-content: space-between; font-size: 11px; color: #86909c; margin-bottom: 4px; }
163
+ .lite-kpi-progress-track { height: 4px; background: #f2f3f5; border-radius: 2px; overflow: hidden; }
164
+ .lite-kpi-progress-bar { height: 100%; background: #165dff; border-radius: 2px; }
165
+ .lite-kpi-card-alert { margin-top: 10px; padding: 6px 10px; border-radius: 4px; font-size: 12px; }
166
+ .lite-kpi-alert-warning { background: #fff7e8; color: #d46b08; border: 1px solid #ffd591; }
167
+ .lite-kpi-alert-danger { background: #fff1f0; color: #cf1322; border: 1px solid #ffa39e; }
168
+ .lite-kpi-card-extra { margin-top: 10px; border-top: 1px solid #f2f3f5; padding-top: 8px; }
169
+ </style>
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface TargetProgress {
3
+ current: number;
4
+ target: number;
5
+ label?: string;
6
+ }
7
+ interface Props {
8
+ title: string;
9
+ value: number | string;
10
+ prefix?: string;
11
+ suffix?: string;
12
+ subtitle?: string;
13
+ trendText?: string;
14
+ trendDirection?: 'up' | 'down' | 'neutral';
15
+ targetProgress?: TargetProgress;
16
+ sparklineData?: number[];
17
+ alertStatus?: 'normal' | 'warning' | 'danger';
18
+ alertMessage?: string;
19
+ class?: string;
20
+ actions?: Snippet;
21
+ children?: Snippet;
22
+ }
23
+ declare const LiteKpiCard: import("svelte").Component<Props, {}, "">;
24
+ type LiteKpiCard = ReturnType<typeof LiteKpiCard>;
25
+ export default LiteKpiCard;
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type ResultStatus = 'success' | 'processing' | 'warning' | 'error' | '403' | '404' | '500';
5
+
6
+ interface Props {
7
+ status?: ResultStatus;
8
+ title: string;
9
+ description?: string;
10
+ primaryActionHref?: string;
11
+ primaryActionLabel?: string;
12
+ secondaryActionHref?: string;
13
+ secondaryActionLabel?: string;
14
+ class?: string;
15
+ actions?: Snippet;
16
+ children?: Snippet;
17
+ }
18
+
19
+ let {
20
+ status = 'success',
21
+ title,
22
+ description,
23
+ primaryActionHref,
24
+ primaryActionLabel = 'Back to Home',
25
+ secondaryActionHref,
26
+ secondaryActionLabel,
27
+ class: className = '',
28
+ actions,
29
+ children,
30
+ }: Props = $props();
31
+
32
+ const iconSymbol = $derived.by(() => {
33
+ switch (status) {
34
+ case 'success':
35
+ return '✓';
36
+ case 'processing':
37
+ return '⏳';
38
+ case 'warning':
39
+ case '403':
40
+ return '!';
41
+ case 'error':
42
+ case '500':
43
+ return '✕';
44
+ case '404':
45
+ default:
46
+ return '?';
47
+ }
48
+ });
49
+ </script>
50
+
51
+ <div
52
+ class="lite-result-page lite-result-{status} {className}"
53
+ data-svadmin-component="LiteResultPage"
54
+ role="status"
55
+ >
56
+ <div class="lite-result-icon-wrapper" aria-hidden="true">
57
+ <span class="lite-result-icon">{iconSymbol}</span>
58
+ </div>
59
+
60
+ <h2 class="lite-result-title">{title}</h2>
61
+
62
+ {#if description}
63
+ <p class="lite-result-description">{description}</p>
64
+ {/if}
65
+
66
+ {#if actions}
67
+ <div class="lite-result-actions">{@render actions()}</div>
68
+ {:else if primaryActionHref || secondaryActionHref}
69
+ <div class="lite-result-actions">
70
+ {#if primaryActionHref}
71
+ <a href={primaryActionHref} class="lite-result-btn-primary">{primaryActionLabel}</a>
72
+ {/if}
73
+ {#if secondaryActionHref && secondaryActionLabel}
74
+ <a href={secondaryActionHref} class="lite-result-btn-secondary">{secondaryActionLabel}</a>
75
+ {/if}
76
+ </div>
77
+ {/if}
78
+
79
+ {#if children}
80
+ <div class="lite-result-extra">{@render children()}</div>
81
+ {/if}
82
+ </div>
83
+
84
+ <style>
85
+ .lite-result-page {
86
+ text-align: center;
87
+ padding: 48px 24px;
88
+ background: #ffffff;
89
+ border-radius: 8px;
90
+ border: 1px solid #e5e6eb;
91
+ box-sizing: border-box;
92
+ }
93
+ .lite-result-icon-wrapper {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ width: 64px;
98
+ height: 64px;
99
+ border-radius: 50%;
100
+ margin-bottom: 16px;
101
+ font-size: 28px;
102
+ font-weight: 700;
103
+ }
104
+ .lite-result-success .lite-result-icon-wrapper { background: #e8ffea; color: #00b42a; }
105
+ .lite-result-processing .lite-result-icon-wrapper { background: #e8f3ff; color: #165dff; }
106
+ .lite-result-warning .lite-result-icon-wrapper { background: #fff7e8; color: #ff7d00; }
107
+ .lite-result-error .lite-result-icon-wrapper { background: #ffece8; color: #f53f3f; }
108
+ .lite-result-403 .lite-result-icon-wrapper { background: #fff7e8; color: #ff7d00; }
109
+ .lite-result-404 .lite-result-icon-wrapper { background: #f2f3f5; color: #86909c; }
110
+ .lite-result-500 .lite-result-icon-wrapper { background: #ffece8; color: #f53f3f; }
111
+ .lite-result-title { margin: 0 0 8px; font-size: 20px; font-weight: 600; color: #1d2129; }
112
+ .lite-result-description { margin: 0 0 24px; font-size: 14px; color: #4e5969; max-width: 480px; margin-left: auto; margin-right: auto; }
113
+ .lite-result-actions { display: flex; justify-content: center; }
114
+ .lite-result-actions > * + * { margin-left: 12px; }
115
+ .lite-result-btn-primary {
116
+ display: inline-block;
117
+ padding: 8px 18px;
118
+ background: #165dff;
119
+ color: #ffffff;
120
+ border-radius: 4px;
121
+ text-decoration: none;
122
+ font-size: 13px;
123
+ font-weight: 500;
124
+ }
125
+ .lite-result-btn-secondary {
126
+ display: inline-block;
127
+ padding: 8px 18px;
128
+ background: #f2f3f5;
129
+ color: #4e5969;
130
+ border-radius: 4px;
131
+ text-decoration: none;
132
+ font-size: 13px;
133
+ }
134
+ .lite-result-extra { margin-top: 24px; padding-top: 16px; border-top: 1px solid #f2f3f5; }
135
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ type ResultStatus = 'success' | 'processing' | 'warning' | 'error' | '403' | '404' | '500';
3
+ interface Props {
4
+ status?: ResultStatus;
5
+ title: string;
6
+ description?: string;
7
+ primaryActionHref?: string;
8
+ primaryActionLabel?: string;
9
+ secondaryActionHref?: string;
10
+ secondaryActionLabel?: string;
11
+ class?: string;
12
+ actions?: Snippet;
13
+ children?: Snippet;
14
+ }
15
+ declare const LiteResultPage: import("svelte").Component<Props, {}, "">;
16
+ type LiteResultPage = ReturnType<typeof LiteResultPage>;
17
+ export default LiteResultPage;
@@ -1,8 +1,17 @@
1
1
  <script lang="ts">
2
2
  interface Props {
3
3
  value?: string | number | null;
4
- type?: 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
5
- /** Caller class merged with the built-in class list. */
4
+ type?:
5
+ | 'phone'
6
+ | 'mobile'
7
+ | 'id-card'
8
+ | 'idCard'
9
+ | 'email'
10
+ | 'bank-card'
11
+ | 'bankCard'
12
+ | 'name'
13
+ | 'secret'
14
+ | 'custom';
6
15
  class?: string;
7
16
  }
8
17
 
@@ -14,13 +23,36 @@
14
23
 
15
24
  const rawString = $derived(String(value ?? ''));
16
25
 
26
+ function maskName(val: string): string {
27
+ const trimmed = val.trim();
28
+ if (!trimmed) return '—';
29
+ if (trimmed.includes(' ')) {
30
+ return trimmed
31
+ .split(/\s+/)
32
+ .map((part) => (part.length <= 1 ? '*' : `${part[0]}${'*'.repeat(Math.max(1, part.length - 2))}${part.length > 2 ? part.slice(-1) : '*'}`))
33
+ .join(' ');
34
+ }
35
+ const len = trimmed.length;
36
+ if (len <= 1) return '*';
37
+ if (len === 2) return `${trimmed[0]}*`;
38
+ if (len === 3) return `${trimmed[0]}*${trimmed[2]}`;
39
+ return `${trimmed[0]}${'*'.repeat(len - 2)}${trimmed.slice(-1)}`;
40
+ }
41
+
17
42
  function mask(val: string, maskType: string): string {
18
43
  if (!val) return '—';
19
44
  switch (maskType) {
20
45
  case 'phone':
46
+ case 'mobile':
21
47
  return val.length >= 7 ? `${val.slice(0, 3)}****${val.slice(-4)}` : '****';
22
48
  case 'id-card':
49
+ case 'idCard':
23
50
  return val.length >= 10 ? `${val.slice(0, 6)}********${val.slice(-4)}` : '********';
51
+ case 'bank-card':
52
+ case 'bankCard':
53
+ return val.length >= 8 ? `${val.slice(0, 4)} **** **** ${val.slice(-4)}` : '**** ****';
54
+ case 'name':
55
+ return maskName(val);
24
56
  case 'email': {
25
57
  const parts = val.split('@');
26
58
  const [name, domain] = parts;
@@ -1,7 +1,6 @@
1
1
  interface Props {
2
2
  value?: string | number | null;
3
- type?: 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
4
- /** Caller class merged with the built-in class list. */
3
+ type?: 'phone' | 'mobile' | 'id-card' | 'idCard' | 'email' | 'bank-card' | 'bankCard' | 'name' | 'secret' | 'custom';
5
4
  class?: string;
6
5
  }
7
6
  declare const LiteSensitiveDataMask: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export interface LiteStepItem {
5
+ id: string;
6
+ title: string;
7
+ description?: string;
8
+ }
9
+
10
+ interface Props {
11
+ steps: LiteStepItem[];
12
+ currentStep?: number;
13
+ action?: string;
14
+ class?: string;
15
+ children?: Snippet;
16
+ }
17
+
18
+ let {
19
+ steps = [],
20
+ currentStep = 0,
21
+ action,
22
+ class: className = '',
23
+ children,
24
+ }: Props = $props();
25
+ </script>
26
+
27
+ <div
28
+ class="lite-step-wizard {className}"
29
+ data-svadmin-component="LiteStepWizard"
30
+ data-current-step={currentStep}
31
+ >
32
+ <nav class="lite-steps-nav" aria-label="Wizard Steps">
33
+ <ol class="lite-steps-list">
34
+ {#each steps as step, idx (step.id)}
35
+ {@const isDone = idx < currentStep}
36
+ {@const isCurrent = idx === currentStep}
37
+ <li
38
+ class="lite-step-item"
39
+ class:lite-step-done={isDone}
40
+ class:lite-step-active={isCurrent}
41
+ >
42
+ <span class="lite-step-badge">
43
+ {#if isDone}✓{:else}{idx + 1}{/if}
44
+ </span>
45
+ <span class="lite-step-info">
46
+ <span class="lite-step-title">{step.title}</span>
47
+ {#if step.description}
48
+ <span class="lite-step-desc">{step.description}</span>
49
+ {/if}
50
+ </span>
51
+ {#if idx < steps.length - 1}
52
+ <span class="lite-step-divider" aria-hidden="true"></span>
53
+ {/if}
54
+ </li>
55
+ {/each}
56
+ </ol>
57
+ </nav>
58
+
59
+ <div class="lite-step-body">
60
+ {#if children}{@render children()}{/if}
61
+ </div>
62
+
63
+ {#if action}
64
+ <form {action} method="POST" class="lite-step-actions">
65
+ <input type="hidden" name="currentStep" value={currentStep} />
66
+ {#if currentStep > 0}
67
+ <button type="submit" name="direction" value="prev" class="lite-btn-secondary">上一步</button>
68
+ {/if}
69
+ <button type="submit" name="direction" value="next" class="lite-btn-primary">
70
+ {currentStep >= steps.length - 1 ? '提交完成' : '下一步'}
71
+ </button>
72
+ </form>
73
+ {/if}
74
+ </div>
75
+
76
+ <style>
77
+ .lite-step-wizard { border: 1px solid #e5e6eb; border-radius: 8px; background: #ffffff; padding: 20px; }
78
+ .lite-steps-nav { margin-bottom: 24px; border-bottom: 1px solid #f2f3f5; padding-bottom: 16px; }
79
+ .lite-steps-list { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; }
80
+ .lite-step-item { display: flex; align-items: center; flex: 1; }
81
+ .lite-step-item > * + * { margin-left: 8px; }
82
+ .lite-step-badge {
83
+ width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center;
84
+ justify-content: center; font-size: 12px; font-weight: 700; background: #f2f3f5; color: #4e5969;
85
+ }
86
+ .lite-step-done .lite-step-badge { background: #00b42a; color: #fff; }
87
+ .lite-step-active .lite-step-badge { background: #165dff; color: #fff; }
88
+ .lite-step-info { display: flex; flex-direction: column; }
89
+ .lite-step-title { font-size: 13px; font-weight: 600; color: #1d2129; }
90
+ .lite-step-desc { font-size: 11px; color: #86909c; }
91
+ .lite-step-divider { flex: 1; height: 1px; background: #e5e6eb; margin: 0 12px; }
92
+ .lite-step-done .lite-step-divider { background: #00b42a; }
93
+ .lite-step-body { min-height: 120px; }
94
+ .lite-step-actions { display: flex; justify-content: flex-end; margin-top: 20px; border-top: 1px solid #f2f3f5; padding-top: 16px; }
95
+ .lite-step-actions > * + * { margin-left: 12px; }
96
+ .lite-btn-primary { padding: 6px 16px; background: #165dff; color: #fff; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; }
97
+ .lite-btn-secondary { padding: 6px 16px; background: #f2f3f5; color: #4e5969; border: 1px solid #e5e6eb; border-radius: 4px; font-size: 12px; cursor: pointer; }
98
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface LiteStepItem {
3
+ id: string;
4
+ title: string;
5
+ description?: string;
6
+ }
7
+ interface Props {
8
+ steps: LiteStepItem[];
9
+ currentStep?: number;
10
+ action?: string;
11
+ class?: string;
12
+ children?: Snippet;
13
+ }
14
+ declare const LiteStepWizard: import("svelte").Component<Props, {}, "">;
15
+ type LiteStepWizard = ReturnType<typeof LiteStepWizard>;
16
+ export default LiteStepWizard;
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ oldText?: string;
4
+ newText?: string;
5
+ oldTitle?: string;
6
+ newTitle?: string;
7
+ class?: string;
8
+ }
9
+
10
+ let {
11
+ oldText = '',
12
+ newText = '',
13
+ oldTitle = 'Baseline',
14
+ newTitle = 'Current',
15
+ class: className = '',
16
+ }: Props = $props();
17
+
18
+ interface DiffEntry {
19
+ type: 'same' | 'add' | 'del';
20
+ lineNo: number;
21
+ text: string;
22
+ }
23
+
24
+ const diffEntries = $derived.by<DiffEntry[]>(() => {
25
+ const oldLines = oldText.split(/\r?\n/u);
26
+ const newLines = newText.split(/\r?\n/u);
27
+ const result: DiffEntry[] = [];
28
+
29
+ let lineCounter = 1;
30
+ let i = 0;
31
+ let j = 0;
32
+
33
+ while (i < oldLines.length && j < newLines.length) {
34
+ if (oldLines[i] === newLines[j]) {
35
+ result.push({ type: 'same', lineNo: lineCounter++, text: oldLines[i] ?? '' });
36
+ i++;
37
+ j++;
38
+ } else {
39
+ result.push({ type: 'del', lineNo: lineCounter++, text: oldLines[i] ?? '' });
40
+ result.push({ type: 'add', lineNo: lineCounter++, text: newLines[j] ?? '' });
41
+ i++;
42
+ j++;
43
+ }
44
+ }
45
+ while (i < oldLines.length) {
46
+ result.push({ type: 'del', lineNo: lineCounter++, text: oldLines[i] ?? '' });
47
+ i++;
48
+ }
49
+ while (j < newLines.length) {
50
+ result.push({ type: 'add', lineNo: lineCounter++, text: newLines[j] ?? '' });
51
+ j++;
52
+ }
53
+
54
+ return result;
55
+ });
56
+ </script>
57
+
58
+ <div
59
+ class="lite-text-diff {className}"
60
+ data-svadmin-component="LiteTextDiffViewer"
61
+ role="region"
62
+ aria-label="Text Diff"
63
+ >
64
+ <header class="lite-diff-header">
65
+ <span class="lite-diff-title">{oldTitle} ↔ {newTitle}</span>
66
+ </header>
67
+
68
+ <div class="lite-diff-body">
69
+ <table class="lite-diff-table">
70
+ <tbody>
71
+ {#each diffEntries as entry (entry.lineNo)}
72
+ <tr class="lite-diff-row lite-diff-{entry.type}">
73
+ <td class="lite-diff-line-no">{entry.lineNo}</td>
74
+ <td class="lite-diff-sign">
75
+ {#if entry.type === 'add'}+{/if}
76
+ {#if entry.type === 'del'}-{/if}
77
+ </td>
78
+ <td class="lite-diff-content">{entry.text}</td>
79
+ </tr>
80
+ {/each}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ </div>
85
+
86
+ <style>
87
+ .lite-text-diff {
88
+ border: 1px solid #e5e6eb;
89
+ border-radius: 6px;
90
+ background: #ffffff;
91
+ font-family: monospace;
92
+ font-size: 12px;
93
+ overflow: hidden;
94
+ }
95
+ .lite-diff-header {
96
+ padding: 8px 12px;
97
+ background: #f7f8fa;
98
+ border-bottom: 1px solid #e5e6eb;
99
+ font-weight: 600;
100
+ color: #4e5969;
101
+ }
102
+ .lite-diff-body { max-height: 400px; overflow: auto; }
103
+ .lite-diff-table { width: 100%; border-collapse: collapse; }
104
+ .lite-diff-row td { padding: 2px 6px; }
105
+ .lite-diff-line-no { width: 40px; text-align: right; color: #86909c; user-select: none; border-right: 1px solid #f2f3f5; }
106
+ .lite-diff-sign { width: 20px; text-align: center; font-weight: 700; user-select: none; }
107
+ .lite-diff-content { white-space: pre-wrap; word-break: break-all; }
108
+ .lite-diff-add { background: #e8ffea; color: #00b42a; }
109
+ .lite-diff-del { background: #ffece8; color: #f53f3f; }
110
+ .lite-diff-same { color: #1d2129; }
111
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ oldText?: string;
3
+ newText?: string;
4
+ oldTitle?: string;
5
+ newTitle?: string;
6
+ class?: string;
7
+ }
8
+ declare const LiteTextDiffViewer: import("svelte").Component<Props, {}, "">;
9
+ type LiteTextDiffViewer = ReturnType<typeof LiteTextDiffViewer>;
10
+ export default LiteTextDiffViewer;