@nucel/ui 0.1.0 → 0.3.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/package.json +35 -3
- package/src/lib/components/ui/Alert.svelte +47 -0
- package/src/lib/components/ui/AppCard.svelte +76 -0
- package/src/lib/components/ui/AppShell.svelte +14 -0
- package/src/lib/components/ui/AppSidebar.svelte +45 -0
- package/src/lib/components/ui/BranchPill.svelte +19 -0
- package/src/lib/components/ui/CodeBlock.svelte +92 -0
- package/src/lib/components/ui/CommentPill.svelte +12 -0
- package/src/lib/components/ui/CopyButton.svelte +43 -0
- package/src/lib/components/ui/CostDisplay.svelte +26 -0
- package/src/lib/components/ui/FilterBar.svelte +63 -0
- package/src/lib/components/ui/FormField.svelte +34 -0
- package/src/lib/components/ui/KanbanBoard.svelte +27 -0
- package/src/lib/components/ui/KanbanCard.svelte +43 -0
- package/src/lib/components/ui/KanbanColumn.svelte +52 -0
- package/src/lib/components/ui/ListCard.svelte +9 -0
- package/src/lib/components/ui/MarkdownRenderer.svelte +2 -2
- package/src/lib/components/ui/MetricCard.svelte +79 -0
- package/src/lib/components/ui/NavItem.svelte +42 -0
- package/src/lib/components/ui/NavSection.svelte +17 -0
- package/src/lib/components/ui/PageHeader.svelte +25 -0
- package/src/lib/components/ui/Pagination.svelte +85 -0
- package/src/lib/components/ui/PermissionChips.svelte +49 -0
- package/src/lib/components/ui/Section.svelte +21 -0
- package/src/lib/components/ui/SectionTitle.svelte +16 -0
- package/src/lib/components/ui/Sparkline.svelte +1 -1
- package/src/lib/components/ui/StatCard.svelte +19 -0
- package/src/lib/components/ui/StatusPill.svelte +54 -0
- package/src/lib/components/ui/Timeline.svelte +85 -0
- package/src/lib/components/ui/editor/RichEditor.svelte +580 -0
- package/src/lib/components/ui/editor/mention-suggestion.ts +144 -0
- package/src/lib/components/ui/table/Table.svelte +12 -0
- package/src/lib/components/ui/table/TableBody.svelte +10 -0
- package/src/lib/components/ui/table/TableCaption.svelte +10 -0
- package/src/lib/components/ui/table/TableCell.svelte +10 -0
- package/src/lib/components/ui/table/TableHead.svelte +10 -0
- package/src/lib/components/ui/table/TableHeader.svelte +10 -0
- package/src/lib/components/ui/table/TableRow.svelte +10 -0
- package/src/lib/components/ui/table/index.ts +7 -0
- package/src/lib/index.ts +84 -0
- package/src/styles.css +6 -0
- package/src/lib/utils/cn.test.ts +0 -993
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { marked } from 'marked';
|
|
3
3
|
import DOMPurify from 'dompurify';
|
|
4
4
|
|
|
5
|
-
let { content }: { content
|
|
5
|
+
let { content = '' }: { content?: string } = $props();
|
|
6
6
|
|
|
7
7
|
let html = $derived(
|
|
8
|
-
DOMPurify.sanitize(marked.parse(content, { gfm: true, breaks: false }) as string),
|
|
8
|
+
content ? DOMPurify.sanitize(marked.parse(content, { gfm: true, breaks: false }) as string) : '',
|
|
9
9
|
);
|
|
10
10
|
</script>
|
|
11
11
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TrendingUp, TrendingDown, Minus } from '@lucide/svelte';
|
|
3
|
+
import Sparkline from './Sparkline.svelte';
|
|
4
|
+
import { cn } from '$lib/utils/cn.js';
|
|
5
|
+
|
|
6
|
+
type Trend = 'up' | 'down' | 'neutral';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
value,
|
|
10
|
+
label,
|
|
11
|
+
hint,
|
|
12
|
+
data = [],
|
|
13
|
+
trend,
|
|
14
|
+
class: className,
|
|
15
|
+
}: {
|
|
16
|
+
value: string | number;
|
|
17
|
+
label: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
data?: number[];
|
|
20
|
+
trend?: Trend;
|
|
21
|
+
class?: string;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
const trendStyles: Record<Trend, string> = {
|
|
25
|
+
up: 'text-success',
|
|
26
|
+
down: 'text-destructive',
|
|
27
|
+
neutral: 'text-muted-foreground',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const trendSparkColor: Record<Trend, string> = {
|
|
31
|
+
up: 'stroke-success',
|
|
32
|
+
down: 'stroke-destructive',
|
|
33
|
+
neutral: 'stroke-muted-foreground',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const trendFillColor: Record<Trend, string> = {
|
|
37
|
+
up: 'fill-success/10',
|
|
38
|
+
down: 'fill-destructive/10',
|
|
39
|
+
neutral: 'fill-muted/30',
|
|
40
|
+
};
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class={cn('rounded-xl border border-border bg-card p-4', className)}>
|
|
44
|
+
<div class="flex items-start justify-between gap-2">
|
|
45
|
+
<div class="min-w-0 flex-1">
|
|
46
|
+
<div class="text-xl font-semibold tracking-tight tabular-nums text-foreground">{value}</div>
|
|
47
|
+
<div class="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
48
|
+
{label}
|
|
49
|
+
</div>
|
|
50
|
+
{#if hint}
|
|
51
|
+
<div
|
|
52
|
+
class={cn(
|
|
53
|
+
'mt-1 flex items-center gap-1 text-[11px]',
|
|
54
|
+
trend ? trendStyles[trend] : 'text-muted-foreground/70',
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{#if trend === 'up'}
|
|
58
|
+
<TrendingUp size={11} />
|
|
59
|
+
{:else if trend === 'down'}
|
|
60
|
+
<TrendingDown size={11} />
|
|
61
|
+
{:else if trend === 'neutral'}
|
|
62
|
+
<Minus size={11} />
|
|
63
|
+
{/if}
|
|
64
|
+
{hint}
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{#if data.length > 0}
|
|
70
|
+
<Sparkline
|
|
71
|
+
{data}
|
|
72
|
+
color={trend ? trendSparkColor[trend] : 'stroke-primary'}
|
|
73
|
+
fillColor={trend ? trendFillColor[trend] : 'fill-primary/10'}
|
|
74
|
+
width={72}
|
|
75
|
+
height={28}
|
|
76
|
+
/>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '$lib/utils/cn.js';
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAnchorAttributes & {
|
|
6
|
+
href: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: any;
|
|
9
|
+
active?: boolean;
|
|
10
|
+
collapsed?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
href,
|
|
15
|
+
label,
|
|
16
|
+
icon: Icon,
|
|
17
|
+
active = false,
|
|
18
|
+
collapsed = false,
|
|
19
|
+
class: className,
|
|
20
|
+
...rest
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<a
|
|
25
|
+
{href}
|
|
26
|
+
class={cn(
|
|
27
|
+
'flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
28
|
+
active && 'bg-sidebar-accent text-sidebar-primary',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
aria-label={collapsed ? label : undefined}
|
|
32
|
+
{...rest}
|
|
33
|
+
>
|
|
34
|
+
{#if Icon}
|
|
35
|
+
<span class="flex size-5 shrink-0 items-center justify-center">
|
|
36
|
+
<Icon size={16} />
|
|
37
|
+
</span>
|
|
38
|
+
{/if}
|
|
39
|
+
{#if !collapsed}
|
|
40
|
+
<span class="truncate">{label}</span>
|
|
41
|
+
{/if}
|
|
42
|
+
</a>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
label,
|
|
4
|
+
collapsed = false,
|
|
5
|
+
}: {
|
|
6
|
+
label: string;
|
|
7
|
+
collapsed?: boolean;
|
|
8
|
+
} = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
{#if !collapsed}
|
|
12
|
+
<p class="px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80">
|
|
13
|
+
{label}
|
|
14
|
+
</p>
|
|
15
|
+
{:else}
|
|
16
|
+
<div class="pt-3"></div>
|
|
17
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
title,
|
|
6
|
+
subtitle,
|
|
7
|
+
actions,
|
|
8
|
+
}: {
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
actions?: Snippet;
|
|
12
|
+
} = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="mb-5 flex items-start justify-between gap-4">
|
|
16
|
+
<div class="min-w-0">
|
|
17
|
+
<h1 class="text-lg font-semibold tracking-tight text-foreground">{title}</h1>
|
|
18
|
+
{#if subtitle}
|
|
19
|
+
<p class="mt-0.5 text-xs text-muted-foreground">{subtitle}</p>
|
|
20
|
+
{/if}
|
|
21
|
+
</div>
|
|
22
|
+
{#if actions}
|
|
23
|
+
<div class="flex shrink-0 items-center gap-2">{@render actions()}</div>
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
|
3
|
+
import { cn } from '$lib/utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
page,
|
|
7
|
+
totalPages,
|
|
8
|
+
onchange,
|
|
9
|
+
class: className,
|
|
10
|
+
}: {
|
|
11
|
+
page: number;
|
|
12
|
+
totalPages: number;
|
|
13
|
+
onchange?: (page: number) => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
function go(p: number) {
|
|
18
|
+
if (p < 1 || p > totalPages) return;
|
|
19
|
+
onchange?.(p);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const visiblePages = $derived.by(() => {
|
|
23
|
+
const pages: (number | '...')[] = [];
|
|
24
|
+
if (totalPages <= 7) {
|
|
25
|
+
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
|
26
|
+
} else {
|
|
27
|
+
pages.push(1);
|
|
28
|
+
if (page > 3) pages.push('...');
|
|
29
|
+
for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) {
|
|
30
|
+
pages.push(i);
|
|
31
|
+
}
|
|
32
|
+
if (page < totalPages - 2) pages.push('...');
|
|
33
|
+
pages.push(totalPages);
|
|
34
|
+
}
|
|
35
|
+
return pages;
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<nav
|
|
40
|
+
aria-label="Pagination"
|
|
41
|
+
class={cn('flex items-center gap-1', className)}
|
|
42
|
+
>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onclick={() => go(page - 1)}
|
|
46
|
+
disabled={page <= 1}
|
|
47
|
+
aria-label="Previous page"
|
|
48
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
|
|
49
|
+
>
|
|
50
|
+
<ChevronLeft size={15} />
|
|
51
|
+
</button>
|
|
52
|
+
|
|
53
|
+
{#each visiblePages as p}
|
|
54
|
+
{#if p === '...'}
|
|
55
|
+
<span class="flex h-8 w-8 items-center justify-center text-[13px] text-muted-foreground">
|
|
56
|
+
…
|
|
57
|
+
</span>
|
|
58
|
+
{:else}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onclick={() => go(p)}
|
|
62
|
+
aria-label="Page {p}"
|
|
63
|
+
aria-current={p === page ? 'page' : undefined}
|
|
64
|
+
class={cn(
|
|
65
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-md border text-[13px] font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
66
|
+
p === page
|
|
67
|
+
? 'border-border bg-primary text-primary-foreground hover:bg-primary/90'
|
|
68
|
+
: 'border-transparent text-foreground',
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{p}
|
|
72
|
+
</button>
|
|
73
|
+
{/if}
|
|
74
|
+
{/each}
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onclick={() => go(page + 1)}
|
|
79
|
+
disabled={page >= totalPages}
|
|
80
|
+
aria-label="Next page"
|
|
81
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
|
|
82
|
+
>
|
|
83
|
+
<ChevronRight size={15} />
|
|
84
|
+
</button>
|
|
85
|
+
</nav>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Compact permission chips for Nucel-App permission sets.
|
|
3
|
+
//
|
|
4
|
+
// Used by the marketplace card, installed-apps card, and the
|
|
5
|
+
// developer's per-app Show page. Filters out `none` automatically and
|
|
6
|
+
// sorts alphabetically so the visual order is stable across calls.
|
|
7
|
+
//
|
|
8
|
+
// `compact=true` (default) renders single-line `key:level` chips
|
|
9
|
+
// (used inside cards). `compact=false` renders a per-row list with
|
|
10
|
+
// primary-tinted level badges (used on the Show page).
|
|
11
|
+
|
|
12
|
+
let { permissions, compact = true }: {
|
|
13
|
+
permissions: Record<string, string> | null | undefined;
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const entries = $derived(
|
|
18
|
+
Object.entries(permissions ?? {})
|
|
19
|
+
.filter(([_, v]) => typeof v === 'string' && v !== 'none')
|
|
20
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
21
|
+
);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if entries.length === 0}
|
|
25
|
+
<span class="text-[11px] italic text-muted-foreground">No write access requested.</span>
|
|
26
|
+
{:else if compact}
|
|
27
|
+
<div class="flex flex-wrap gap-1">
|
|
28
|
+
{#each entries as [key, level]}
|
|
29
|
+
<span class="rounded-md border border-border/60 bg-muted/40 px-1.5 py-0.5 text-[11px]">
|
|
30
|
+
<code class="font-mono">{key}</code>:{level}
|
|
31
|
+
</span>
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
{:else}
|
|
35
|
+
<ul class="space-y-1.5">
|
|
36
|
+
{#each entries as [key, level]}
|
|
37
|
+
<li class="flex items-center justify-between gap-3 rounded-md border border-border/60 bg-muted/30 px-3 py-1.5 text-xs">
|
|
38
|
+
<code class="font-mono">{key}</code>
|
|
39
|
+
<span
|
|
40
|
+
class="rounded-md border px-1.5 py-0.5 text-[11px] {level === 'write' || level === 'admin'
|
|
41
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
42
|
+
: 'border-border/60 bg-muted text-muted-foreground'}"
|
|
43
|
+
>
|
|
44
|
+
{level}
|
|
45
|
+
</span>
|
|
46
|
+
</li>
|
|
47
|
+
{/each}
|
|
48
|
+
</ul>
|
|
49
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title,
|
|
7
|
+
children,
|
|
8
|
+
class: className,
|
|
9
|
+
}: {
|
|
10
|
+
title?: string;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
class?: string;
|
|
13
|
+
} = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class={cn('rounded-xl border border-border bg-card p-5', className)}>
|
|
17
|
+
{#if title}
|
|
18
|
+
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{title}</h3>
|
|
19
|
+
{/if}
|
|
20
|
+
{@render children?.()}
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
children,
|
|
7
|
+
class: className,
|
|
8
|
+
}: {
|
|
9
|
+
children: Snippet;
|
|
10
|
+
class?: string;
|
|
11
|
+
} = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<h3 class={cn('mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground', className)}>
|
|
15
|
+
{@render children()}
|
|
16
|
+
</h3>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
label,
|
|
4
|
+
value,
|
|
5
|
+
hint,
|
|
6
|
+
}: {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string | number;
|
|
9
|
+
hint?: string;
|
|
10
|
+
} = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="rounded-xl border border-border bg-card p-4">
|
|
14
|
+
<div class="text-xl font-semibold tracking-tight tabular-nums text-foreground">{value}</div>
|
|
15
|
+
<div class="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">{label}</div>
|
|
16
|
+
{#if hint}
|
|
17
|
+
<div class="mt-1 text-[11px] text-muted-foreground/70">{hint}</div>
|
|
18
|
+
{/if}
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Check, X, Loader2, CircleSlash, Clock, AlertTriangle } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
type Status = 'success' | 'failure' | 'running' | 'pending' | 'cancelled' | 'warning';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
status,
|
|
8
|
+
label,
|
|
9
|
+
size = 'sm',
|
|
10
|
+
}: {
|
|
11
|
+
status: Status;
|
|
12
|
+
label?: string;
|
|
13
|
+
size?: 'xs' | 'sm';
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
const defaults: Record<Status, string> = {
|
|
17
|
+
success: 'passed',
|
|
18
|
+
failure: 'failed',
|
|
19
|
+
running: 'running',
|
|
20
|
+
pending: 'pending',
|
|
21
|
+
cancelled: 'cancelled',
|
|
22
|
+
warning: 'warning',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const styles: Record<Status, string> = {
|
|
26
|
+
success: 'border-green-500/30 bg-green-500/10 text-green-500',
|
|
27
|
+
failure: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
28
|
+
running: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
|
|
29
|
+
pending: 'border-yellow-500/30 bg-yellow-500/10 text-yellow-500',
|
|
30
|
+
cancelled: 'border-border/60 bg-muted text-muted-foreground',
|
|
31
|
+
warning: 'border-amber-500/30 bg-amber-500/10 text-amber-500',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let text = $derived(label ?? defaults[status]);
|
|
35
|
+
let sizeCls = $derived(size === 'xs' ? 'text-[10px] px-1.5 py-0.5' : 'text-[11px] px-2 py-0.5');
|
|
36
|
+
let iconSize = $derived(size === 'xs' ? 'h-2.5 w-2.5' : 'h-3 w-3');
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<span class="inline-flex items-center gap-1 rounded-full border font-medium {sizeCls} {styles[status]}">
|
|
40
|
+
{#if status === 'success'}
|
|
41
|
+
<Check class={iconSize} />
|
|
42
|
+
{:else if status === 'failure'}
|
|
43
|
+
<X class={iconSize} />
|
|
44
|
+
{:else if status === 'running'}
|
|
45
|
+
<Loader2 class="{iconSize} animate-spin" />
|
|
46
|
+
{:else if status === 'cancelled'}
|
|
47
|
+
<CircleSlash class={iconSize} />
|
|
48
|
+
{:else if status === 'warning'}
|
|
49
|
+
<AlertTriangle class={iconSize} />
|
|
50
|
+
{:else}
|
|
51
|
+
<Clock class={iconSize} />
|
|
52
|
+
{/if}
|
|
53
|
+
{text}
|
|
54
|
+
</span>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
X,
|
|
5
|
+
AlertTriangle,
|
|
6
|
+
Clock,
|
|
7
|
+
Loader2,
|
|
8
|
+
} from '@lucide/svelte';
|
|
9
|
+
import { cn } from '$lib/utils/cn.js';
|
|
10
|
+
|
|
11
|
+
type Status = 'success' | 'error' | 'warning' | 'pending' | 'running';
|
|
12
|
+
|
|
13
|
+
type TimelineItem = {
|
|
14
|
+
time?: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
icon?: any;
|
|
18
|
+
status?: Status;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
items,
|
|
23
|
+
class: className,
|
|
24
|
+
}: {
|
|
25
|
+
items: TimelineItem[];
|
|
26
|
+
class?: string;
|
|
27
|
+
} = $props();
|
|
28
|
+
|
|
29
|
+
const statusStyles: Record<Status, string> = {
|
|
30
|
+
success: 'border-green-500/40 bg-green-500/10 text-green-500',
|
|
31
|
+
error: 'border-destructive/40 bg-destructive/10 text-destructive',
|
|
32
|
+
warning: 'border-amber-500/40 bg-amber-500/10 text-amber-500',
|
|
33
|
+
pending: 'border-border bg-muted text-muted-foreground',
|
|
34
|
+
running: 'border-blue-500/40 bg-blue-500/10 text-blue-400',
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<ol class={cn('flex flex-col', className)}>
|
|
39
|
+
{#each items as item, i}
|
|
40
|
+
{@const isLast = i === items.length - 1}
|
|
41
|
+
{@const status = item.status}
|
|
42
|
+
<li class="relative flex gap-4">
|
|
43
|
+
<!-- connector line -->
|
|
44
|
+
{#if !isLast}
|
|
45
|
+
<div class="absolute left-[15px] top-8 h-full w-px bg-border"></div>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
<!-- dot / icon -->
|
|
49
|
+
<div
|
|
50
|
+
class={cn(
|
|
51
|
+
'relative z-10 mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border',
|
|
52
|
+
status ? statusStyles[status] : 'border-border bg-muted text-muted-foreground',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{#if item.icon}
|
|
56
|
+
{@const Icon = item.icon}
|
|
57
|
+
<Icon size={14} />
|
|
58
|
+
{:else if status === 'success'}
|
|
59
|
+
<Check size={14} />
|
|
60
|
+
{:else if status === 'error'}
|
|
61
|
+
<X size={14} />
|
|
62
|
+
{:else if status === 'warning'}
|
|
63
|
+
<AlertTriangle size={14} />
|
|
64
|
+
{:else if status === 'running'}
|
|
65
|
+
<Loader2 size={14} class="animate-spin" />
|
|
66
|
+
{:else}
|
|
67
|
+
<Clock size={14} />
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- content -->
|
|
72
|
+
<div class={cn('min-w-0 flex-1 pb-6', isLast && 'pb-0')}>
|
|
73
|
+
<div class="flex items-baseline gap-2">
|
|
74
|
+
<span class="text-[13px] font-medium text-foreground">{item.title}</span>
|
|
75
|
+
{#if item.time}
|
|
76
|
+
<span class="shrink-0 text-[11px] text-muted-foreground">{item.time}</span>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
{#if item.description}
|
|
80
|
+
<p class="mt-0.5 text-[13px] text-muted-foreground">{item.description}</p>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
</li>
|
|
84
|
+
{/each}
|
|
85
|
+
</ol>
|