@svadmin/ui 0.51.0 → 0.52.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/dist/components/content/AuditTimeline.svelte +86 -0
- package/dist/components/content/AuditTimeline.svelte.d.ts +20 -0
- package/dist/components/content/index.d.ts +2 -0
- package/dist/components/content/index.js +1 -0
- package/dist/components/fields/BooleanField.svelte +90 -8
- package/dist/components/fields/BooleanField.svelte.d.ts +8 -0
- package/dist/components/fields/DateField.svelte +74 -4
- package/dist/components/fields/DateField.svelte.d.ts +4 -0
- package/dist/components/fields/NumberField.svelte +72 -4
- package/dist/components/fields/NumberField.svelte.d.ts +9 -0
- package/dist/components/fields/TagField.svelte +90 -6
- package/dist/components/fields/TagField.svelte.d.ts +9 -0
- package/dist/components/fields/UrlField.svelte +43 -7
- package/dist/components/fields/UrlField.svelte.d.ts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Status } from './StatusBadge.svelte';
|
|
3
|
+
|
|
4
|
+
export interface TimelineItem {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
actor?: string;
|
|
10
|
+
status?: Status;
|
|
11
|
+
tag?: string;
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { cn } from '../../utils.js';
|
|
18
|
+
import StatusBadge from './StatusBadge.svelte';
|
|
19
|
+
import DataState from './DataState.svelte';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
items?: TimelineItem[];
|
|
23
|
+
emptyTitle?: string;
|
|
24
|
+
emptyDescription?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
items = [],
|
|
30
|
+
emptyTitle = 'No timeline events',
|
|
31
|
+
emptyDescription = 'Activity events and status transitions will appear here.',
|
|
32
|
+
class: className = '',
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const dotClass: Record<Status, string> = {
|
|
36
|
+
success: 'bg-success border-success/30 text-success',
|
|
37
|
+
warning: 'bg-warning border-warning/30 text-warning-foreground',
|
|
38
|
+
danger: 'bg-destructive border-destructive/30 text-destructive',
|
|
39
|
+
info: 'bg-primary border-primary/30 text-primary',
|
|
40
|
+
neutral: 'bg-muted-foreground border-border text-muted-foreground',
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<div class={cn('space-y-4', className)}>
|
|
45
|
+
{#if items.length === 0}
|
|
46
|
+
<DataState state="empty" title={emptyTitle} description={emptyDescription} />
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="relative pl-6 space-y-6 before:absolute before:left-2 before:top-2 before:bottom-2 before:w-0.5 before:bg-border">
|
|
49
|
+
{#each items as item (item.id)}
|
|
50
|
+
{@const statusTone = item.status ?? 'info'}
|
|
51
|
+
<div class="relative flex flex-col gap-1 text-sm group">
|
|
52
|
+
<span
|
|
53
|
+
class={cn(
|
|
54
|
+
'absolute -left-6 top-1 flex h-4 w-4 items-center justify-center rounded-full border-2 bg-background transition-transform group-hover:scale-110',
|
|
55
|
+
item.status ? dotClass[statusTone] : 'border-border bg-muted'
|
|
56
|
+
)}
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
></span>
|
|
59
|
+
|
|
60
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
61
|
+
<div class="flex items-center gap-2">
|
|
62
|
+
<span class="font-medium text-foreground">{item.title}</span>
|
|
63
|
+
{#if item.actor}
|
|
64
|
+
<span class="text-xs text-muted-foreground">by {item.actor}</span>
|
|
65
|
+
{/if}
|
|
66
|
+
{#if item.status}
|
|
67
|
+
<StatusBadge status={item.status} label={item.tag ?? item.status} class="text-[11px] py-0 h-4" />
|
|
68
|
+
{:else if item.tag}
|
|
69
|
+
<span class="inline-flex items-center rounded border border-border px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground bg-muted">
|
|
70
|
+
{item.tag}
|
|
71
|
+
</span>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
{#if item.timestamp}
|
|
75
|
+
<time class="text-xs tabular-nums text-muted-foreground font-mono">{item.timestamp}</time>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{#if item.description}
|
|
80
|
+
<p class="text-xs text-muted-foreground leading-relaxed whitespace-pre-wrap">{item.description}</p>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Status } from './StatusBadge.svelte';
|
|
2
|
+
export interface TimelineItem {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
timestamp?: string;
|
|
7
|
+
actor?: string;
|
|
8
|
+
status?: Status;
|
|
9
|
+
tag?: string;
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface Props {
|
|
13
|
+
items?: TimelineItem[];
|
|
14
|
+
emptyTitle?: string;
|
|
15
|
+
emptyDescription?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const AuditTimeline: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type AuditTimeline = ReturnType<typeof AuditTimeline>;
|
|
20
|
+
export default AuditTimeline;
|
|
@@ -41,3 +41,5 @@ export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
|
41
41
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
42
42
|
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
43
43
|
export type { FeedbackNoticeTone, FeedbackNoticePriority } from './FeedbackNotice.svelte';
|
|
44
|
+
export { default as AuditTimeline } from './AuditTimeline.svelte';
|
|
45
|
+
export type { TimelineItem } from './AuditTimeline.svelte';
|
|
@@ -29,3 +29,4 @@ export { default as OtpInput } from './OtpInput.svelte';
|
|
|
29
29
|
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
30
30
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
31
31
|
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
32
|
+
export { default as AuditTimeline } from './AuditTimeline.svelte';
|
|
@@ -1,13 +1,95 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type BooleanTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
import { Badge } from '../ui/badge/index.js';
|
|
7
|
+
import { cn } from '../../utils.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value: boolean | null | undefined;
|
|
11
|
+
mode?: 'icon' | 'badge' | 'tag' | 'text';
|
|
12
|
+
trueIcon?: string;
|
|
13
|
+
falseIcon?: string;
|
|
14
|
+
trueLabel?: string;
|
|
15
|
+
falseLabel?: string;
|
|
16
|
+
nullLabel?: string;
|
|
17
|
+
trueTone?: BooleanTone;
|
|
18
|
+
falseTone?: BooleanTone;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
value,
|
|
24
|
+
mode = 'icon',
|
|
25
|
+
trueIcon = '✓',
|
|
26
|
+
falseIcon = '✗',
|
|
27
|
+
trueLabel = 'Yes',
|
|
28
|
+
falseLabel = 'No',
|
|
29
|
+
nullLabel = '—',
|
|
30
|
+
trueTone = 'success',
|
|
31
|
+
falseTone = 'danger',
|
|
32
|
+
class: className = '',
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const toneClass: Record<BooleanTone, string> = {
|
|
36
|
+
success: 'border-success/30 bg-success/10 text-success',
|
|
37
|
+
warning: 'border-warning/30 bg-warning/10 text-warning-foreground',
|
|
38
|
+
danger: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
39
|
+
info: 'border-primary/30 bg-primary/10 text-primary',
|
|
40
|
+
neutral: 'border-border bg-muted text-muted-foreground',
|
|
41
|
+
};
|
|
4
42
|
</script>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
43
|
+
|
|
44
|
+
{#if value == null}
|
|
45
|
+
<span class={cn('text-muted-foreground', className)}>{nullLabel}</span>
|
|
46
|
+
{:else if mode === 'badge' || mode === 'tag'}
|
|
47
|
+
<Badge
|
|
48
|
+
variant="outline"
|
|
49
|
+
class={cn(
|
|
50
|
+
'font-medium text-xs',
|
|
51
|
+
value ? toneClass[trueTone] : toneClass[falseTone],
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{value ? trueLabel : falseLabel}
|
|
56
|
+
</Badge>
|
|
57
|
+
{:else if mode === 'text'}
|
|
58
|
+
<span
|
|
59
|
+
class={cn(
|
|
60
|
+
'font-medium text-sm',
|
|
61
|
+
value
|
|
62
|
+
? trueTone === 'success'
|
|
63
|
+
? 'text-success'
|
|
64
|
+
: 'text-foreground'
|
|
65
|
+
: falseTone === 'danger'
|
|
66
|
+
? 'text-destructive'
|
|
67
|
+
: 'text-muted-foreground',
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{value ? trueLabel : falseLabel}
|
|
8
72
|
</span>
|
|
9
73
|
{:else}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
74
|
+
{#if value}
|
|
75
|
+
<span
|
|
76
|
+
class={cn(
|
|
77
|
+
'inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-semibold',
|
|
78
|
+
toneClass[trueTone],
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{trueIcon}
|
|
83
|
+
</span>
|
|
84
|
+
{:else}
|
|
85
|
+
<span
|
|
86
|
+
class={cn(
|
|
87
|
+
'inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-semibold',
|
|
88
|
+
toneClass[falseTone],
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
{falseIcon}
|
|
93
|
+
</span>
|
|
94
|
+
{/if}
|
|
13
95
|
{/if}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export type BooleanTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
1
2
|
interface Props {
|
|
2
3
|
value: boolean | null | undefined;
|
|
4
|
+
mode?: 'icon' | 'badge' | 'tag' | 'text';
|
|
3
5
|
trueIcon?: string;
|
|
4
6
|
falseIcon?: string;
|
|
7
|
+
trueLabel?: string;
|
|
8
|
+
falseLabel?: string;
|
|
9
|
+
nullLabel?: string;
|
|
10
|
+
trueTone?: BooleanTone;
|
|
11
|
+
falseTone?: BooleanTone;
|
|
12
|
+
class?: string;
|
|
5
13
|
}
|
|
6
14
|
declare const BooleanField: import("svelte").Component<Props, {}, "">;
|
|
7
15
|
type BooleanField = ReturnType<typeof BooleanField>;
|
|
@@ -1,6 +1,76 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type DateFieldFormat = 'date' | 'time' | 'datetime' | 'relative' | 'iso';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
import { cn } from '../../utils.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
value: string | number | Date | null | undefined;
|
|
10
|
+
locale?: string;
|
|
11
|
+
format?: DateFieldFormat;
|
|
12
|
+
options?: Intl.DateTimeFormatOptions;
|
|
13
|
+
nullLabel?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
value,
|
|
19
|
+
locale = 'en-US',
|
|
20
|
+
format = 'date',
|
|
21
|
+
options,
|
|
22
|
+
nullLabel = '—',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
function formatRelative(date: Date, loc: string): string {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const diffMs = date.getTime() - now;
|
|
29
|
+
const diffSec = Math.round(diffMs / 1000);
|
|
30
|
+
const diffMin = Math.round(diffSec / 60);
|
|
31
|
+
const diffHour = Math.round(diffMin / 60);
|
|
32
|
+
const diffDay = Math.round(diffHour / 24);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const rtf = new Intl.RelativeTimeFormat(loc, { numeric: 'auto' });
|
|
36
|
+
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
|
|
37
|
+
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
|
|
38
|
+
if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
|
|
39
|
+
if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback
|
|
42
|
+
}
|
|
43
|
+
return new Intl.DateTimeFormat(loc, { dateStyle: 'medium' }).format(date);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatted = $derived.by(() => {
|
|
47
|
+
if (value == null || value === '') return nullLabel;
|
|
48
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
49
|
+
if (isNaN(d.getTime())) return nullLabel;
|
|
50
|
+
|
|
51
|
+
if (format === 'iso') return d.toISOString();
|
|
52
|
+
if (format === 'relative') return formatRelative(d, locale);
|
|
53
|
+
|
|
54
|
+
const defaultOptions: Intl.DateTimeFormatOptions =
|
|
55
|
+
options ??
|
|
56
|
+
(format === 'datetime'
|
|
57
|
+
? { dateStyle: 'medium', timeStyle: 'short' }
|
|
58
|
+
: format === 'time'
|
|
59
|
+
? { timeStyle: 'medium' }
|
|
60
|
+
: { dateStyle: 'medium' });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return new Intl.DateTimeFormat(locale, defaultOptions).format(d);
|
|
64
|
+
} catch {
|
|
65
|
+
return d.toLocaleDateString();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const fullIsoTitle = $derived.by(() => {
|
|
70
|
+
if (value == null || value === '') return undefined;
|
|
71
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
72
|
+
return isNaN(d.getTime()) ? undefined : d.toISOString();
|
|
73
|
+
});
|
|
5
74
|
</script>
|
|
6
|
-
|
|
75
|
+
|
|
76
|
+
<span class={cn('field-date text-sm', className)} title={fullIsoTitle}>{formatted}</span>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export type DateFieldFormat = 'date' | 'time' | 'datetime' | 'relative' | 'iso';
|
|
1
2
|
interface Props {
|
|
2
3
|
value: string | number | Date | null | undefined;
|
|
3
4
|
locale?: string;
|
|
5
|
+
format?: DateFieldFormat;
|
|
4
6
|
options?: Intl.DateTimeFormatOptions;
|
|
7
|
+
nullLabel?: string;
|
|
8
|
+
class?: string;
|
|
5
9
|
}
|
|
6
10
|
declare const DateField: import("svelte").Component<Props, {}, "">;
|
|
7
11
|
type DateField = ReturnType<typeof DateField>;
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: number | string | null | undefined;
|
|
6
|
+
locale?: string;
|
|
7
|
+
currency?: string;
|
|
8
|
+
unit?: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
precision?: number;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
signed?: boolean;
|
|
13
|
+
tabular?: boolean;
|
|
14
|
+
options?: Intl.NumberFormatOptions;
|
|
15
|
+
nullLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
value,
|
|
21
|
+
locale = 'en-US',
|
|
22
|
+
currency,
|
|
23
|
+
unit,
|
|
24
|
+
prefix,
|
|
25
|
+
precision,
|
|
26
|
+
compact = false,
|
|
27
|
+
signed = false,
|
|
28
|
+
tabular = true,
|
|
29
|
+
options = {},
|
|
30
|
+
nullLabel = '—',
|
|
31
|
+
class: className = '',
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
const formatted = $derived.by(() => {
|
|
35
|
+
if (value == null || value === '') return nullLabel;
|
|
36
|
+
const num = Number(value);
|
|
37
|
+
if (isNaN(num)) return nullLabel;
|
|
38
|
+
|
|
39
|
+
const opts: Intl.NumberFormatOptions = { ...options };
|
|
40
|
+
|
|
41
|
+
if (currency) {
|
|
42
|
+
opts.style = 'currency';
|
|
43
|
+
opts.currency = currency;
|
|
44
|
+
}
|
|
45
|
+
if (compact) {
|
|
46
|
+
opts.notation = 'compact';
|
|
47
|
+
opts.compactDisplay = 'short';
|
|
48
|
+
}
|
|
49
|
+
if (precision != null) {
|
|
50
|
+
opts.minimumFractionDigits = precision;
|
|
51
|
+
opts.maximumFractionDigits = precision;
|
|
52
|
+
}
|
|
53
|
+
if (signed && num > 0 && !currency) {
|
|
54
|
+
opts.signDisplay = 'always';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
let result = new Intl.NumberFormat(locale, opts).format(num);
|
|
59
|
+
if (prefix && !currency) {
|
|
60
|
+
result = `${prefix}${result}`;
|
|
61
|
+
}
|
|
62
|
+
if (unit) {
|
|
63
|
+
result = `${result} ${unit}`;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
} catch {
|
|
67
|
+
return String(num);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
5
70
|
</script>
|
|
6
|
-
|
|
71
|
+
|
|
72
|
+
<span class={cn('field-number text-sm', tabular && 'font-mono tabular-nums', className)}>
|
|
73
|
+
{formatted}
|
|
74
|
+
</span>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: number | string | null | undefined;
|
|
3
3
|
locale?: string;
|
|
4
|
+
currency?: string;
|
|
5
|
+
unit?: string;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
precision?: number;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
signed?: boolean;
|
|
10
|
+
tabular?: boolean;
|
|
4
11
|
options?: Intl.NumberFormatOptions;
|
|
12
|
+
nullLabel?: string;
|
|
13
|
+
class?: string;
|
|
5
14
|
}
|
|
6
15
|
declare const NumberField: import("svelte").Component<Props, {}, "">;
|
|
7
16
|
type NumberField = ReturnType<typeof NumberField>;
|
|
@@ -1,12 +1,96 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type TagTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
6
|
import { Badge } from '../ui/badge/index.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
import { cn } from '../../utils.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value: string | string[] | null | undefined;
|
|
11
|
+
colorMap?: Record<string, TagTone | string>;
|
|
12
|
+
tone?: TagTone;
|
|
13
|
+
variant?: 'secondary' | 'outline' | 'default' | 'destructive';
|
|
14
|
+
size?: 'sm' | 'default';
|
|
15
|
+
clickable?: boolean;
|
|
16
|
+
onclick?: (tag: string) => void;
|
|
17
|
+
nullLabel?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
value,
|
|
23
|
+
colorMap,
|
|
24
|
+
tone,
|
|
25
|
+
variant = 'secondary',
|
|
26
|
+
size = 'default',
|
|
27
|
+
clickable = false,
|
|
28
|
+
onclick,
|
|
29
|
+
nullLabel = '—',
|
|
30
|
+
class: className = '',
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
const toneClass: Record<TagTone, string> = {
|
|
34
|
+
success: 'border-success/30 bg-success/10 text-success',
|
|
35
|
+
warning: 'border-warning/30 bg-warning/10 text-warning-foreground',
|
|
36
|
+
danger: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
37
|
+
info: 'border-primary/30 bg-primary/10 text-primary',
|
|
38
|
+
neutral: 'border-border bg-muted text-muted-foreground',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const tags = $derived(
|
|
42
|
+
Array.isArray(value)
|
|
43
|
+
? value.filter((t): t is string => t != null && t !== '')
|
|
44
|
+
: value != null && value !== ''
|
|
45
|
+
? [String(value)]
|
|
46
|
+
: []
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function resolveTagClass(tag: string): string {
|
|
50
|
+
const mapped = colorMap?.[tag] ?? tone;
|
|
51
|
+
if (mapped && mapped in toneClass) {
|
|
52
|
+
return toneClass[mapped as TagTone];
|
|
53
|
+
}
|
|
54
|
+
if (typeof mapped === 'string') {
|
|
55
|
+
return mapped;
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
6
59
|
</script>
|
|
7
|
-
|
|
60
|
+
|
|
61
|
+
<span class={cn('inline-flex flex-wrap gap-1 items-center', className)}>
|
|
8
62
|
{#each tags as tag, _i (_i)}
|
|
9
|
-
|
|
63
|
+
{@const customClass = resolveTagClass(tag)}
|
|
64
|
+
{#if clickable || onclick}
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onclick={() => onclick?.(tag)}
|
|
68
|
+
class="focus:outline-none focus:ring-1 focus:ring-ring rounded"
|
|
69
|
+
>
|
|
70
|
+
<Badge
|
|
71
|
+
variant={customClass ? 'outline' : variant}
|
|
72
|
+
class={cn(
|
|
73
|
+
'transition-colors cursor-pointer hover:opacity-80',
|
|
74
|
+
size === 'sm' ? 'px-1.5 py-0 text-[11px] h-5' : '',
|
|
75
|
+
customClass
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{tag}
|
|
79
|
+
</Badge>
|
|
80
|
+
</button>
|
|
81
|
+
{:else}
|
|
82
|
+
<Badge
|
|
83
|
+
variant={customClass ? 'outline' : variant}
|
|
84
|
+
class={cn(
|
|
85
|
+
size === 'sm' ? 'px-1.5 py-0 text-[11px] h-5' : '',
|
|
86
|
+
customClass
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{tag}
|
|
90
|
+
</Badge>
|
|
91
|
+
{/if}
|
|
10
92
|
{/each}
|
|
11
|
-
{#if tags.length === 0}
|
|
93
|
+
{#if tags.length === 0}
|
|
94
|
+
<span class="text-muted-foreground">{nullLabel}</span>
|
|
95
|
+
{/if}
|
|
12
96
|
</span>
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
export type TagTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
1
2
|
interface Props {
|
|
2
3
|
value: string | string[] | null | undefined;
|
|
4
|
+
colorMap?: Record<string, TagTone | string>;
|
|
5
|
+
tone?: TagTone;
|
|
6
|
+
variant?: 'secondary' | 'outline' | 'default' | 'destructive';
|
|
7
|
+
size?: 'sm' | 'default';
|
|
8
|
+
clickable?: boolean;
|
|
9
|
+
onclick?: (tag: string) => void;
|
|
10
|
+
nullLabel?: string;
|
|
11
|
+
class?: string;
|
|
3
12
|
}
|
|
4
13
|
declare const TagField: import("svelte").Component<Props, {}, "">;
|
|
5
14
|
type TagField = ReturnType<typeof TagField>;
|
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ExternalLink } from '@lucide/svelte';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
value: string | null | undefined;
|
|
7
|
+
label?: string;
|
|
8
|
+
target?: string;
|
|
9
|
+
maxDisplayLength?: number;
|
|
10
|
+
showIcon?: boolean;
|
|
11
|
+
nullLabel?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
value,
|
|
17
|
+
label,
|
|
18
|
+
target = '_blank',
|
|
19
|
+
maxDisplayLength = 40,
|
|
20
|
+
showIcon = true,
|
|
21
|
+
nullLabel = '—',
|
|
22
|
+
class: className = '',
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const display = $derived.by(() => {
|
|
26
|
+
if (label) return label;
|
|
27
|
+
if (!value) return '';
|
|
28
|
+
if (maxDisplayLength && value.length > maxDisplayLength) {
|
|
29
|
+
return value.slice(0, maxDisplayLength) + '…';
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
});
|
|
6
33
|
</script>
|
|
34
|
+
|
|
7
35
|
{#if value}
|
|
8
|
-
<a
|
|
9
|
-
{
|
|
10
|
-
|
|
36
|
+
<a
|
|
37
|
+
href={value}
|
|
38
|
+
{target}
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
class={cn('text-primary hover:underline inline-flex items-center gap-1 text-sm font-medium', className)}
|
|
41
|
+
title={value}
|
|
42
|
+
>
|
|
43
|
+
<span>{display}</span>
|
|
44
|
+
{#if showIcon}
|
|
45
|
+
<ExternalLink class="h-3 w-3 shrink-0 opacity-60" />
|
|
46
|
+
{/if}
|
|
11
47
|
</a>
|
|
12
48
|
{:else}
|
|
13
|
-
<span class="text-muted-foreground"
|
|
49
|
+
<span class="text-muted-foreground">{nullLabel}</span>
|
|
14
50
|
{/if}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: string | null | undefined;
|
|
3
3
|
label?: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
maxDisplayLength?: number;
|
|
6
|
+
showIcon?: boolean;
|
|
7
|
+
nullLabel?: string;
|
|
8
|
+
class?: string;
|
|
4
9
|
}
|
|
5
10
|
declare const UrlField: import("svelte").Component<Props, {}, "">;
|
|
6
11
|
type UrlField = ReturnType<typeof UrlField>;
|
package/dist/index.d.ts
CHANGED
|
@@ -204,3 +204,8 @@ export type { DemoMember, DemoProject, DemoTeam, DemoIntegration, DemoApiKey, De
|
|
|
204
204
|
export { default as RowActions } from './components/RowActions.svelte';
|
|
205
205
|
export type { RowActionItem } from './components/RowActions.svelte';
|
|
206
206
|
export { default as DetailDrawer } from './components/DetailDrawer.svelte';
|
|
207
|
+
export { default as AuditTimeline } from './components/content/AuditTimeline.svelte';
|
|
208
|
+
export type { TimelineItem } from './components/content/AuditTimeline.svelte';
|
|
209
|
+
export type { TagTone } from './components/fields/TagField.svelte';
|
|
210
|
+
export type { BooleanTone } from './components/fields/BooleanField.svelte';
|
|
211
|
+
export type { DateFieldFormat } from './components/fields/DateField.svelte';
|
package/dist/index.js
CHANGED
|
@@ -202,3 +202,4 @@ export { default as SystemErrorState } from './components/content/SystemErrorSta
|
|
|
202
202
|
export { referenceDemoData } from './reference-data.js';
|
|
203
203
|
export { default as RowActions } from './components/RowActions.svelte';
|
|
204
204
|
export { default as DetailDrawer } from './components/DetailDrawer.svelte';
|
|
205
|
+
export { default as AuditTimeline } from './components/content/AuditTimeline.svelte';
|