@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.
Files changed (42) hide show
  1. package/package.json +35 -3
  2. package/src/lib/components/ui/Alert.svelte +47 -0
  3. package/src/lib/components/ui/AppCard.svelte +76 -0
  4. package/src/lib/components/ui/AppShell.svelte +14 -0
  5. package/src/lib/components/ui/AppSidebar.svelte +45 -0
  6. package/src/lib/components/ui/BranchPill.svelte +19 -0
  7. package/src/lib/components/ui/CodeBlock.svelte +92 -0
  8. package/src/lib/components/ui/CommentPill.svelte +12 -0
  9. package/src/lib/components/ui/CopyButton.svelte +43 -0
  10. package/src/lib/components/ui/CostDisplay.svelte +26 -0
  11. package/src/lib/components/ui/FilterBar.svelte +63 -0
  12. package/src/lib/components/ui/FormField.svelte +34 -0
  13. package/src/lib/components/ui/KanbanBoard.svelte +27 -0
  14. package/src/lib/components/ui/KanbanCard.svelte +43 -0
  15. package/src/lib/components/ui/KanbanColumn.svelte +52 -0
  16. package/src/lib/components/ui/ListCard.svelte +9 -0
  17. package/src/lib/components/ui/MarkdownRenderer.svelte +2 -2
  18. package/src/lib/components/ui/MetricCard.svelte +79 -0
  19. package/src/lib/components/ui/NavItem.svelte +42 -0
  20. package/src/lib/components/ui/NavSection.svelte +17 -0
  21. package/src/lib/components/ui/PageHeader.svelte +25 -0
  22. package/src/lib/components/ui/Pagination.svelte +85 -0
  23. package/src/lib/components/ui/PermissionChips.svelte +49 -0
  24. package/src/lib/components/ui/Section.svelte +21 -0
  25. package/src/lib/components/ui/SectionTitle.svelte +16 -0
  26. package/src/lib/components/ui/Sparkline.svelte +1 -1
  27. package/src/lib/components/ui/StatCard.svelte +19 -0
  28. package/src/lib/components/ui/StatusPill.svelte +54 -0
  29. package/src/lib/components/ui/Timeline.svelte +85 -0
  30. package/src/lib/components/ui/editor/RichEditor.svelte +580 -0
  31. package/src/lib/components/ui/editor/mention-suggestion.ts +144 -0
  32. package/src/lib/components/ui/table/Table.svelte +12 -0
  33. package/src/lib/components/ui/table/TableBody.svelte +10 -0
  34. package/src/lib/components/ui/table/TableCaption.svelte +10 -0
  35. package/src/lib/components/ui/table/TableCell.svelte +10 -0
  36. package/src/lib/components/ui/table/TableHead.svelte +10 -0
  37. package/src/lib/components/ui/table/TableHeader.svelte +10 -0
  38. package/src/lib/components/ui/table/TableRow.svelte +10 -0
  39. package/src/lib/components/ui/table/index.ts +7 -0
  40. package/src/lib/index.ts +84 -0
  41. package/src/styles.css +6 -0
  42. package/src/lib/utils/cn.test.ts +0 -993
@@ -0,0 +1,9 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ let { children }: { children: Snippet } = $props();
5
+ </script>
6
+
7
+ <div class="overflow-hidden rounded-xl border border-border bg-card divide-y divide-border">
8
+ {@render children()}
9
+ </div>
@@ -2,10 +2,10 @@
2
2
  import { marked } from 'marked';
3
3
  import DOMPurify from 'dompurify';
4
4
 
5
- let { content }: { content: string } = $props();
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>
@@ -3,7 +3,7 @@
3
3
 
4
4
  let {
5
5
  ref = $bindable(null),
6
- data,
6
+ data = [] as number[],
7
7
  color = 'stroke-primary',
8
8
  fillColor = 'fill-primary/10',
9
9
  width = 80,
@@ -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>