@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
|
@@ -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>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type MetricColumns = 1 | 2 | 3 | 4 | 5 | 6 | 'auto';
|
|
3
|
+
type MetricGap = 'sm' | 'md' | 'lg';
|
|
4
|
+
interface LiteMetricGridItem {
|
|
5
|
+
id?: string;
|
|
6
|
+
label: string;
|
|
7
|
+
value: string | number;
|
|
8
|
+
description?: string;
|
|
9
|
+
trend?: 'up' | 'down' | 'neutral';
|
|
10
|
+
trendValue?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
items?: LiteMetricGridItem[];
|
|
16
|
+
cols?: MetricColumns;
|
|
17
|
+
gap?: MetricGap;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
skeletonCount?: number;
|
|
22
|
+
empty?: boolean;
|
|
23
|
+
emptyMessage?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
actions?: Snippet;
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
emptyContent?: Snippet;
|
|
28
|
+
}
|
|
29
|
+
declare const LiteMetricGrid: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type LiteMetricGrid = ReturnType<typeof LiteMetricGrid>;
|
|
31
|
+
export default LiteMetricGrid;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLIframeAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
src: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
height?: string | number;
|
|
8
|
+
sandbox?: string;
|
|
9
|
+
allow?: string;
|
|
10
|
+
referrerpolicy?: HTMLIframeAttributes['referrerpolicy'];
|
|
11
|
+
fallbackHref?: string;
|
|
12
|
+
fallbackLabel?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
src,
|
|
18
|
+
title = 'Embedded application',
|
|
19
|
+
height = '650px',
|
|
20
|
+
sandbox = 'allow-scripts allow-forms allow-same-origin',
|
|
21
|
+
allow,
|
|
22
|
+
referrerpolicy = 'strict-origin-when-cross-origin',
|
|
23
|
+
fallbackHref,
|
|
24
|
+
fallbackLabel = 'Open in a new tab',
|
|
25
|
+
class: className = '',
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const frameHeight = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<section class="lite-micro-app-frame lite-card {className}" aria-label={title}>
|
|
32
|
+
<header>
|
|
33
|
+
<h2>{title}</h2>
|
|
34
|
+
{#if fallbackHref}<a href={fallbackHref} target="_blank" rel="noopener noreferrer">{fallbackLabel}</a>{/if}
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<iframe
|
|
38
|
+
{src}
|
|
39
|
+
{title}
|
|
40
|
+
{sandbox}
|
|
41
|
+
{allow}
|
|
42
|
+
{referrerpolicy}
|
|
43
|
+
style={`height:${frameHeight};`}
|
|
44
|
+
loading="lazy"
|
|
45
|
+
></iframe>
|
|
46
|
+
<p class="lite-micro-app-help">Embedded messaging, theme sync and automatic height are client-side enhancements.</p>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.lite-micro-app-frame { padding: 12px; }
|
|
51
|
+
.lite-micro-app-frame header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }
|
|
52
|
+
.lite-micro-app-frame header a { margin-left: 12px; }
|
|
53
|
+
.lite-micro-app-frame h2 { margin: 0; color: #1d2129; font-size: 15px; }
|
|
54
|
+
.lite-micro-app-frame header a { color: #165dff; font-size: 12px; }
|
|
55
|
+
.lite-micro-app-frame iframe { display: block; width: 100%; border: 1px solid #e5e6eb; border-radius: 4px; background: #fff; }
|
|
56
|
+
.lite-micro-app-help { margin: 6px 0 0; color: #86909c; font-size: 11px; }
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HTMLIframeAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
src: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
height?: string | number;
|
|
6
|
+
sandbox?: string;
|
|
7
|
+
allow?: string;
|
|
8
|
+
referrerpolicy?: HTMLIframeAttributes['referrerpolicy'];
|
|
9
|
+
fallbackHref?: string;
|
|
10
|
+
fallbackLabel?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const LiteMicroAppFrame: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type LiteMicroAppFrame = ReturnType<typeof LiteMicroAppFrame>;
|
|
15
|
+
export default LiteMicroAppFrame;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface LiteOrgNode {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
department?: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
children?: LiteOrgNode[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
data?: LiteOrgNode | LiteOrgNode[];
|
|
13
|
+
title?: string;
|
|
14
|
+
emptyText?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
data = [],
|
|
20
|
+
title = 'Organization chart',
|
|
21
|
+
emptyText = 'No organization data',
|
|
22
|
+
class: className = '',
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
interface FlatNode {
|
|
26
|
+
node: LiteOrgNode;
|
|
27
|
+
depth: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function flatten(nodes: LiteOrgNode[], depth = 0, out: FlatNode[] = []): FlatNode[] {
|
|
31
|
+
for (const node of nodes) {
|
|
32
|
+
out.push({ node, depth });
|
|
33
|
+
if (node.children?.length) flatten(node.children, depth + 1, out);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rows = $derived(flatten(Array.isArray(data) ? data : [data]));
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<section class="lite-org-chart lite-card {className}" aria-label={title}>
|
|
42
|
+
<h2>{title}</h2>
|
|
43
|
+
{#if rows.length === 0}
|
|
44
|
+
<p class="lite-org-empty">{emptyText}</p>
|
|
45
|
+
{:else}
|
|
46
|
+
<table class="lite-table">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th>Name</th>
|
|
50
|
+
<th>Title</th>
|
|
51
|
+
<th>Department</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
{#each rows as row (row.node.id)}
|
|
56
|
+
<tr data-depth={row.depth}>
|
|
57
|
+
<td>
|
|
58
|
+
<span class="lite-org-indent" style={`padding-left:${row.depth * 16}px`}>
|
|
59
|
+
{#if row.depth > 0}<span class="lite-org-branch" aria-hidden="true">└</span>{/if}
|
|
60
|
+
{#if row.node.href}<a href={row.node.href}>{row.node.name}</a>{:else}{row.node.name}{/if}
|
|
61
|
+
</span>
|
|
62
|
+
</td>
|
|
63
|
+
<td>{row.node.title ?? '—'}</td>
|
|
64
|
+
<td>{row.node.department ?? '—'}</td>
|
|
65
|
+
</tr>
|
|
66
|
+
{/each}
|
|
67
|
+
</tbody>
|
|
68
|
+
</table>
|
|
69
|
+
{/if}
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
.lite-org-chart { padding: 16px; }
|
|
74
|
+
.lite-org-chart h2 { margin: 0 0 12px; color: #1d2129; font-size: 16px; }
|
|
75
|
+
.lite-org-empty { margin: 0; padding: 20px; color: #86909c; text-align: center; }
|
|
76
|
+
.lite-org-indent { display: inline-block; }
|
|
77
|
+
.lite-org-branch { display: inline-block; margin-right: 4px; color: #86909c; }
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface LiteOrgNode {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
department?: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
children?: LiteOrgNode[];
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
data?: LiteOrgNode | LiteOrgNode[];
|
|
11
|
+
title?: string;
|
|
12
|
+
emptyText?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const LiteOrgChart: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type LiteOrgChart = ReturnType<typeof LiteOrgChart>;
|
|
17
|
+
export default LiteOrgChart;
|
|
@@ -1,55 +1,158 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
interface LitePresenceUser {
|
|
2
|
+
export interface LitePresenceUser {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
|
-
|
|
5
|
+
avatar?: string;
|
|
6
|
+
role?: string;
|
|
7
|
+
status?: 'online' | 'viewing' | 'idle' | 'editing';
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
users?: LitePresenceUser[];
|
|
10
12
|
label?: string;
|
|
11
|
-
|
|
13
|
+
maxVisible?: number;
|
|
14
|
+
maxCount?: number;
|
|
15
|
+
size?: 'sm' | 'md' | 'lg';
|
|
16
|
+
showStatusDot?: boolean;
|
|
12
17
|
class?: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
let {
|
|
16
21
|
users = [],
|
|
17
22
|
label = 'Viewing now',
|
|
18
|
-
|
|
23
|
+
maxVisible,
|
|
24
|
+
maxCount,
|
|
25
|
+
size = 'md',
|
|
26
|
+
showStatusDot = true,
|
|
27
|
+
class: className = '',
|
|
19
28
|
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const limit = $derived(Math.max(0, maxVisible ?? maxCount ?? 4));
|
|
31
|
+
const visible = $derived(users.slice(0, limit));
|
|
32
|
+
const hidden = $derived(users.slice(limit));
|
|
33
|
+
|
|
34
|
+
function initials(name: string): string {
|
|
35
|
+
return name
|
|
36
|
+
.trim()
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.map((part) => part[0] ?? '')
|
|
39
|
+
.join('')
|
|
40
|
+
.slice(0, 2)
|
|
41
|
+
.toUpperCase() || 'U';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function statusLabel(status: LitePresenceUser['status']): string {
|
|
45
|
+
return status ?? 'online';
|
|
46
|
+
}
|
|
20
47
|
</script>
|
|
21
48
|
|
|
22
|
-
<div class="lite-presence-container {className}">
|
|
49
|
+
<div class="lite-presence-container lite-presence-{size} {className}">
|
|
23
50
|
<span class="lite-presence-label">{label}:</span>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
|
|
52
|
+
{#each visible as user (user.id)}
|
|
53
|
+
<span
|
|
54
|
+
class="lite-presence-badge"
|
|
55
|
+
title={`${user.name}${user.role ? ` (${user.role})` : ''} · ${statusLabel(user.status)}`}
|
|
56
|
+
data-presence-user={user.id}
|
|
57
|
+
data-status={statusLabel(user.status)}
|
|
58
|
+
>
|
|
59
|
+
{#if user.avatar}
|
|
60
|
+
<img src={user.avatar} alt="" class="lite-presence-avatar" />
|
|
61
|
+
{:else}
|
|
62
|
+
<span class="lite-presence-initials" aria-hidden="true">{initials(user.name)}</span>
|
|
63
|
+
{/if}
|
|
64
|
+
<span class="lite-presence-name">{user.name}</span>
|
|
65
|
+
{#if showStatusDot && user.status}
|
|
66
|
+
<span class="lite-presence-dot" aria-hidden="true"></span>
|
|
67
|
+
{/if}
|
|
27
68
|
</span>
|
|
28
69
|
{/each}
|
|
70
|
+
|
|
71
|
+
{#if hidden.length > 0}
|
|
72
|
+
<span
|
|
73
|
+
class="lite-presence-overflow"
|
|
74
|
+
title={hidden.map((user) => user.name).join(', ')}
|
|
75
|
+
aria-label={`+${hidden.length} more users`}
|
|
76
|
+
>
|
|
77
|
+
+{hidden.length}
|
|
78
|
+
</span>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
29
81
|
{#if users.length === 0}
|
|
30
|
-
<span
|
|
82
|
+
<span class="lite-presence-empty">No active viewers</span>
|
|
31
83
|
{/if}
|
|
32
84
|
</div>
|
|
33
85
|
|
|
34
86
|
<style>
|
|
35
87
|
.lite-presence-container {
|
|
36
88
|
display: inline-flex;
|
|
89
|
+
flex-wrap: wrap;
|
|
37
90
|
align-items: center;
|
|
38
91
|
font-size: 12px;
|
|
39
92
|
margin-bottom: 8px;
|
|
40
93
|
}
|
|
41
94
|
.lite-presence-label {
|
|
42
|
-
color: #
|
|
43
|
-
margin-right:
|
|
95
|
+
color: #4e5969;
|
|
96
|
+
margin-right: 2px;
|
|
44
97
|
font-weight: 500;
|
|
45
98
|
}
|
|
46
99
|
.lite-presence-badge {
|
|
100
|
+
position: relative;
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
padding: 2px 6px 2px 3px;
|
|
104
|
+
background: #f2f3f5;
|
|
105
|
+
border: 1px solid #e5e6eb;
|
|
106
|
+
border-radius: 999px;
|
|
107
|
+
margin-right: 2px;
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
line-height: 1.4;
|
|
110
|
+
}
|
|
111
|
+
.lite-presence-avatar,
|
|
112
|
+
.lite-presence-initials {
|
|
47
113
|
display: inline-block;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
border-radius: 4px;
|
|
114
|
+
width: 20px;
|
|
115
|
+
height: 20px;
|
|
116
|
+
flex: 0 0 20px;
|
|
52
117
|
margin-right: 4px;
|
|
118
|
+
border-radius: 999px;
|
|
119
|
+
text-align: center;
|
|
120
|
+
line-height: 20px;
|
|
121
|
+
object-fit: cover;
|
|
122
|
+
background: #e8f3ff;
|
|
123
|
+
color: #165dff;
|
|
124
|
+
font-size: 9px;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
}
|
|
127
|
+
.lite-presence-name { max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
128
|
+
.lite-presence-dot {
|
|
129
|
+
margin-left: 4px;
|
|
130
|
+
width: 6px;
|
|
131
|
+
height: 6px;
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
background: #00b42a;
|
|
134
|
+
box-shadow: 0 0 0 1px #fff;
|
|
135
|
+
}
|
|
136
|
+
.lite-presence-badge[data-status='idle'] .lite-presence-dot { background: #86909c; }
|
|
137
|
+
.lite-presence-badge[data-status='editing'] .lite-presence-dot { background: #ff7d00; }
|
|
138
|
+
.lite-presence-badge[data-status='viewing'] .lite-presence-dot { background: #00b42a; }
|
|
139
|
+
.lite-presence-overflow {
|
|
140
|
+
display: inline-block;
|
|
141
|
+
min-width: 24px;
|
|
142
|
+
line-height: 24px;
|
|
143
|
+
text-align: center;
|
|
144
|
+
height: 24px;
|
|
145
|
+
padding: 0 5px;
|
|
146
|
+
border: 1px solid #e5e6eb;
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
background: #fff;
|
|
149
|
+
color: #4e5969;
|
|
53
150
|
font-size: 11px;
|
|
151
|
+
font-weight: 600;
|
|
54
152
|
}
|
|
153
|
+
.lite-presence-empty { color: #86909c; font-size: 11px; }
|
|
154
|
+
.lite-presence-sm .lite-presence-avatar,
|
|
155
|
+
.lite-presence-sm .lite-presence-initials { width: 16px; height: 16px; flex-basis: 16px; font-size: 8px; }
|
|
156
|
+
.lite-presence-lg .lite-presence-avatar,
|
|
157
|
+
.lite-presence-lg .lite-presence-initials { width: 26px; height: 26px; flex-basis: 26px; font-size: 10px; }
|
|
55
158
|
</style>
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
interface LitePresenceUser {
|
|
1
|
+
export interface LitePresenceUser {
|
|
2
2
|
id: string;
|
|
3
3
|
name: string;
|
|
4
|
-
|
|
4
|
+
avatar?: string;
|
|
5
|
+
role?: string;
|
|
6
|
+
status?: 'online' | 'viewing' | 'idle' | 'editing';
|
|
5
7
|
}
|
|
6
8
|
interface Props {
|
|
7
9
|
users?: LitePresenceUser[];
|
|
8
10
|
label?: string;
|
|
9
|
-
|
|
11
|
+
maxVisible?: number;
|
|
12
|
+
maxCount?: number;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
showStatusDot?: boolean;
|
|
10
15
|
class?: string;
|
|
11
16
|
}
|
|
12
17
|
declare const LitePresenceAvatarGroup: import("svelte").Component<Props, {}, "">;
|
|
@@ -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;
|