@nucel/ui 0.3.0 → 0.11.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 +27 -34
- package/src/lib/components/BottomSheet.svelte +96 -0
- package/src/lib/components/Breadcrumbs.svelte +57 -0
- package/src/lib/components/Checkbox.svelte +64 -0
- package/src/lib/components/CodeBlock.svelte +264 -0
- package/src/lib/components/CodeEditor.svelte +175 -0
- package/src/lib/components/ColorInput.svelte +41 -0
- package/src/lib/components/ColorInput.test.ts +126 -0
- package/src/lib/components/Combobox.svelte +103 -0
- package/src/lib/components/CommandPalette.svelte +135 -0
- package/src/lib/components/CopyButton.svelte +95 -0
- package/src/lib/components/CopyButton.test.ts +213 -0
- package/src/lib/components/DataTable.svelte +202 -0
- package/src/lib/components/DateRangePicker.svelte +185 -0
- package/src/lib/components/DiffEditor.svelte +174 -0
- package/src/lib/components/Drawer.svelte +69 -0
- package/src/lib/components/Fab.svelte +59 -0
- package/src/lib/components/Form.svelte +38 -0
- package/src/lib/components/FormField.svelte +51 -0
- package/src/lib/components/IconButton.svelte +86 -0
- package/src/lib/components/IconButton.test.ts +139 -0
- package/src/lib/components/InlineCode.svelte +28 -0
- package/src/lib/components/Pagination.svelte +65 -0
- package/src/lib/components/Radio.svelte +60 -0
- package/src/lib/components/RadioGroup.svelte +26 -0
- package/src/lib/components/SearchInput.svelte +77 -0
- package/src/lib/components/Skeleton.svelte +76 -0
- package/src/lib/components/StatCard.svelte +97 -0
- package/src/lib/components/ThemeProvider.svelte +157 -0
- package/src/lib/components/ThemeToggle.svelte +68 -0
- package/src/lib/components/ThreeWayMerge.svelte +185 -0
- package/src/lib/components/ui/Alert.svelte +1 -1
- package/src/lib/components/ui/MarkdownRenderer.svelte +126 -8
- package/src/lib/components/ui/Sparkline.svelte +1 -1
- package/src/lib/components/ui/StatusBadge.svelte +6 -3
- package/src/lib/components/ui/StatusDot.svelte +3 -3
- package/src/lib/components/ui/table/Table.svelte +1 -1
- package/src/lib/components/ui/table/TableBody.svelte +1 -1
- package/src/lib/components/ui/table/TableCaption.svelte +1 -1
- package/src/lib/components/ui/table/TableCell.svelte +1 -1
- package/src/lib/components/ui/table/TableHead.svelte +1 -1
- package/src/lib/components/ui/table/TableHeader.svelte +1 -1
- package/src/lib/components/ui/table/TableRow.svelte +1 -1
- package/src/lib/index.ts +161 -61
- package/src/lib/utils/cn.test.ts +993 -0
- package/src/lib/utils/detectLanguage.ts +187 -0
- package/src/lib/utils/monaco-workers.d.ts +32 -0
- package/src/lib/utils/monacoLoader.ts +167 -0
- package/src/lib/utils/shikiHighlighter.ts +78 -0
- package/src/styles.css +100 -32
- package/src/lib/components/ui/AppShell.svelte +0 -14
- package/src/lib/components/ui/AppSidebar.svelte +0 -45
- package/src/lib/components/ui/CodeBlock.svelte +0 -92
- package/src/lib/components/ui/CopyButton.svelte +0 -43
- package/src/lib/components/ui/CostDisplay.svelte +0 -26
- package/src/lib/components/ui/FilterBar.svelte +0 -63
- package/src/lib/components/ui/FormField.svelte +0 -34
- package/src/lib/components/ui/MetricCard.svelte +0 -79
- package/src/lib/components/ui/NavItem.svelte +0 -42
- package/src/lib/components/ui/NavSection.svelte +0 -17
- package/src/lib/components/ui/Pagination.svelte +0 -85
- package/src/lib/components/ui/StatCard.svelte +0 -19
- package/src/lib/components/ui/Timeline.svelte +0 -85
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
collapsed = $bindable(false),
|
|
7
|
-
brand,
|
|
8
|
-
nav,
|
|
9
|
-
footer,
|
|
10
|
-
}: {
|
|
11
|
-
collapsed?: boolean;
|
|
12
|
-
brand?: Snippet<[{ collapsed: boolean }]>;
|
|
13
|
-
nav?: Snippet<[{ collapsed: boolean }]>;
|
|
14
|
-
footer?: Snippet<[{ collapsed: boolean }]>;
|
|
15
|
-
} = $props();
|
|
16
|
-
</script>
|
|
17
|
-
|
|
18
|
-
<aside
|
|
19
|
-
class="flex flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200"
|
|
20
|
-
style:width={collapsed ? '56px' : '224px'}
|
|
21
|
-
style:min-width={collapsed ? '56px' : '224px'}
|
|
22
|
-
>
|
|
23
|
-
<div class="flex h-12 shrink-0 items-center border-b border-sidebar-border px-3">
|
|
24
|
-
<div class="flex-1 overflow-hidden">
|
|
25
|
-
{#if brand}{@render brand({ collapsed })}{/if}
|
|
26
|
-
</div>
|
|
27
|
-
<button
|
|
28
|
-
class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
|
|
29
|
-
onclick={() => (collapsed = !collapsed)}
|
|
30
|
-
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
31
|
-
>
|
|
32
|
-
{#if collapsed}<ChevronRight size={14} />{:else}<ChevronLeft size={14} />{/if}
|
|
33
|
-
</button>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<nav class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
|
|
37
|
-
{#if nav}{@render nav({ collapsed })}{/if}
|
|
38
|
-
</nav>
|
|
39
|
-
|
|
40
|
-
{#if footer}
|
|
41
|
-
<div class="border-t border-sidebar-border p-2">
|
|
42
|
-
{@render footer({ collapsed })}
|
|
43
|
-
</div>
|
|
44
|
-
{/if}
|
|
45
|
-
</aside>
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { createHighlighter, type Highlighter } from 'shiki';
|
|
3
|
-
import CopyButton from './CopyButton.svelte';
|
|
4
|
-
import { cn } from '$lib/utils/cn.js';
|
|
5
|
-
|
|
6
|
-
let {
|
|
7
|
-
code,
|
|
8
|
-
language = 'plaintext',
|
|
9
|
-
showLanguage = true,
|
|
10
|
-
class: className,
|
|
11
|
-
}: {
|
|
12
|
-
code: string;
|
|
13
|
-
language?: string;
|
|
14
|
-
showLanguage?: boolean;
|
|
15
|
-
class?: string;
|
|
16
|
-
} = $props();
|
|
17
|
-
|
|
18
|
-
let html = $state('');
|
|
19
|
-
let highlighter: Highlighter | null = null;
|
|
20
|
-
|
|
21
|
-
const SUPPORTED_LANGS = [
|
|
22
|
-
'typescript',
|
|
23
|
-
'javascript',
|
|
24
|
-
'svelte',
|
|
25
|
-
'html',
|
|
26
|
-
'css',
|
|
27
|
-
'json',
|
|
28
|
-
'bash',
|
|
29
|
-
'shell',
|
|
30
|
-
'python',
|
|
31
|
-
'rust',
|
|
32
|
-
'go',
|
|
33
|
-
'plaintext',
|
|
34
|
-
'sql',
|
|
35
|
-
'yaml',
|
|
36
|
-
'toml',
|
|
37
|
-
'markdown',
|
|
38
|
-
] as const;
|
|
39
|
-
|
|
40
|
-
async function highlight(src: string, lang: string) {
|
|
41
|
-
if (!highlighter) {
|
|
42
|
-
highlighter = await createHighlighter({
|
|
43
|
-
themes: ['github-dark'],
|
|
44
|
-
langs: SUPPORTED_LANGS as unknown as string[],
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
const safeLang = SUPPORTED_LANGS.includes(lang as (typeof SUPPORTED_LANGS)[number])
|
|
48
|
-
? lang
|
|
49
|
-
: 'plaintext';
|
|
50
|
-
return highlighter.codeToHtml(src, { lang: safeLang, theme: 'github-dark' });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
$effect(() => {
|
|
54
|
-
highlight(code, language)
|
|
55
|
-
.then((result) => {
|
|
56
|
-
html = result;
|
|
57
|
-
})
|
|
58
|
-
.catch(() => {
|
|
59
|
-
// Highlighting failed (e.g. in a test environment without WASM support).
|
|
60
|
-
// The component falls back to the plain <pre> display.
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
</script>
|
|
64
|
-
|
|
65
|
-
<div
|
|
66
|
-
class={cn(
|
|
67
|
-
'relative overflow-hidden rounded-xl border border-border bg-[#0d1117] font-mono text-sm',
|
|
68
|
-
className,
|
|
69
|
-
)}
|
|
70
|
-
>
|
|
71
|
-
{#if showLanguage || true}
|
|
72
|
-
<div
|
|
73
|
-
class="flex items-center justify-between border-b border-white/8 px-4 py-2"
|
|
74
|
-
>
|
|
75
|
-
{#if showLanguage}
|
|
76
|
-
<span class="text-[11px] font-medium uppercase tracking-wide text-white/40">
|
|
77
|
-
{language}
|
|
78
|
-
</span>
|
|
79
|
-
{:else}
|
|
80
|
-
<span></span>
|
|
81
|
-
{/if}
|
|
82
|
-
<CopyButton text={code} size={14} class="text-white/40 hover:bg-white/8 hover:text-white/80" />
|
|
83
|
-
</div>
|
|
84
|
-
{/if}
|
|
85
|
-
|
|
86
|
-
{#if html}
|
|
87
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
88
|
-
<div class="overflow-x-auto [&>pre]:p-4 [&>pre]:text-sm">{@html html}</div>
|
|
89
|
-
{:else}
|
|
90
|
-
<pre class="overflow-x-auto p-4 text-white/70">{code}</pre>
|
|
91
|
-
{/if}
|
|
92
|
-
</div>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Copy, Check } from '@lucide/svelte';
|
|
3
|
-
import { cn } from '$lib/utils/cn.js';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
text,
|
|
7
|
-
size = 16,
|
|
8
|
-
class: className,
|
|
9
|
-
}: {
|
|
10
|
-
text: string;
|
|
11
|
-
size?: number;
|
|
12
|
-
class?: string;
|
|
13
|
-
} = $props();
|
|
14
|
-
|
|
15
|
-
let copied = $state(false);
|
|
16
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
-
|
|
18
|
-
async function copy() {
|
|
19
|
-
await navigator.clipboard.writeText(text);
|
|
20
|
-
copied = true;
|
|
21
|
-
if (timer) clearTimeout(timer);
|
|
22
|
-
timer = setTimeout(() => {
|
|
23
|
-
copied = false;
|
|
24
|
-
timer = null;
|
|
25
|
-
}, 2000);
|
|
26
|
-
}
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<button
|
|
30
|
-
type="button"
|
|
31
|
-
onclick={copy}
|
|
32
|
-
aria-label={copied ? 'Copied' : 'Copy to clipboard'}
|
|
33
|
-
class={cn(
|
|
34
|
-
'inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
35
|
-
className,
|
|
36
|
-
)}
|
|
37
|
-
>
|
|
38
|
-
{#if copied}
|
|
39
|
-
<Check size={size} class="text-success" />
|
|
40
|
-
{:else}
|
|
41
|
-
<Copy size={size} />
|
|
42
|
-
{/if}
|
|
43
|
-
</button>
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import NumberFlow from "@number-flow/svelte";
|
|
3
|
-
|
|
4
|
-
let {
|
|
5
|
-
amount,
|
|
6
|
-
precision = 2,
|
|
7
|
-
size = "sm",
|
|
8
|
-
class: className = "",
|
|
9
|
-
}: {
|
|
10
|
-
amount: number;
|
|
11
|
-
precision?: 2 | 3 | 4;
|
|
12
|
-
size?: "xs" | "sm";
|
|
13
|
-
class?: string;
|
|
14
|
-
} = $props();
|
|
15
|
-
|
|
16
|
-
const sizeClass = $derived(
|
|
17
|
-
size === "xs" ? "text-[10px]" : "text-xs",
|
|
18
|
-
);
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<span class="tabular-nums text-muted-foreground {sizeClass} {className}">
|
|
22
|
-
<NumberFlow
|
|
23
|
-
value={amount}
|
|
24
|
-
format={{ style: "currency", currency: "USD", minimumFractionDigits: precision, maximumFractionDigits: precision }}
|
|
25
|
-
/>
|
|
26
|
-
</span>
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Search, X } from '@lucide/svelte';
|
|
3
|
-
import { cn } from '$lib/utils/cn.js';
|
|
4
|
-
|
|
5
|
-
type Filter = {
|
|
6
|
-
key: string;
|
|
7
|
-
label: string;
|
|
8
|
-
value: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
let {
|
|
12
|
-
value = '',
|
|
13
|
-
filters = [],
|
|
14
|
-
placeholder = 'Search…',
|
|
15
|
-
onchange,
|
|
16
|
-
onremove,
|
|
17
|
-
class: className,
|
|
18
|
-
}: {
|
|
19
|
-
value?: string;
|
|
20
|
-
filters?: Filter[];
|
|
21
|
-
placeholder?: string;
|
|
22
|
-
onchange?: (value: string) => void;
|
|
23
|
-
onremove?: (filter: Filter) => void;
|
|
24
|
-
class?: string;
|
|
25
|
-
} = $props();
|
|
26
|
-
|
|
27
|
-
function handleInput(e: Event) {
|
|
28
|
-
const target = e.currentTarget as HTMLInputElement;
|
|
29
|
-
onchange?.(target.value);
|
|
30
|
-
}
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
<div class={cn('flex flex-wrap items-center gap-2', className)}>
|
|
34
|
-
<!-- search input -->
|
|
35
|
-
<div class="relative flex min-w-[200px] items-center">
|
|
36
|
-
<Search size={14} class="absolute left-2.5 text-muted-foreground" />
|
|
37
|
-
<input
|
|
38
|
-
type="search"
|
|
39
|
-
{placeholder}
|
|
40
|
-
{value}
|
|
41
|
-
oninput={handleInput}
|
|
42
|
-
class="h-8 w-full rounded-md border border-border bg-background pl-8 pr-3 text-[13px] text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<!-- filter chips -->
|
|
47
|
-
{#each filters as filter}
|
|
48
|
-
<span
|
|
49
|
-
class="inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2.5 py-0.5 text-[12px] font-medium text-foreground"
|
|
50
|
-
>
|
|
51
|
-
<span class="text-muted-foreground">{filter.label}:</span>
|
|
52
|
-
<span>{filter.value}</span>
|
|
53
|
-
<button
|
|
54
|
-
type="button"
|
|
55
|
-
onclick={() => onremove?.(filter)}
|
|
56
|
-
aria-label="Remove {filter.label} filter"
|
|
57
|
-
class="ml-0.5 rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
58
|
-
>
|
|
59
|
-
<X size={11} />
|
|
60
|
-
</button>
|
|
61
|
-
</span>
|
|
62
|
-
{/each}
|
|
63
|
-
</div>
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { cn } from '../../utils/cn.js';
|
|
4
|
-
import { Label } from './label/index.js';
|
|
5
|
-
|
|
6
|
-
let {
|
|
7
|
-
label,
|
|
8
|
-
for: forId,
|
|
9
|
-
hint,
|
|
10
|
-
error,
|
|
11
|
-
children,
|
|
12
|
-
class: className,
|
|
13
|
-
}: {
|
|
14
|
-
label?: string;
|
|
15
|
-
for?: string;
|
|
16
|
-
hint?: string;
|
|
17
|
-
error?: string;
|
|
18
|
-
children?: Snippet;
|
|
19
|
-
class?: string;
|
|
20
|
-
} = $props();
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<div class={cn('space-y-1.5', className)}>
|
|
24
|
-
{#if label}
|
|
25
|
-
<Label for={forId}>{label}</Label>
|
|
26
|
-
{/if}
|
|
27
|
-
{@render children?.()}
|
|
28
|
-
{#if hint && !error}
|
|
29
|
-
<p class="text-[11px] text-muted-foreground">{hint}</p>
|
|
30
|
-
{/if}
|
|
31
|
-
{#if error}
|
|
32
|
-
<p class="text-sm text-destructive">{error}</p>
|
|
33
|
-
{/if}
|
|
34
|
-
</div>
|
|
@@ -1,79 +0,0 @@
|
|
|
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>
|
|
@@ -1,42 +0,0 @@
|
|
|
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>
|
|
@@ -1,17 +0,0 @@
|
|
|
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}
|
|
@@ -1,85 +0,0 @@
|
|
|
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>
|
|
@@ -1,19 +0,0 @@
|
|
|
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>
|
|
@@ -1,85 +0,0 @@
|
|
|
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>
|