@nucel/ui 0.3.0 → 0.10.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 (78) hide show
  1. package/package.json +8 -36
  2. package/src/lib/components/BottomSheet.svelte +96 -0
  3. package/src/lib/components/Breadcrumbs.svelte +57 -0
  4. package/src/lib/components/Checkbox.svelte +64 -0
  5. package/src/lib/components/CodeBlock.svelte +264 -0
  6. package/src/lib/components/CodeEditor.svelte +175 -0
  7. package/src/lib/components/ColorInput.svelte +41 -0
  8. package/src/lib/components/ColorInput.test.ts +126 -0
  9. package/src/lib/components/Combobox.svelte +103 -0
  10. package/src/lib/components/CommandPalette.svelte +135 -0
  11. package/src/lib/components/CopyButton.svelte +95 -0
  12. package/src/lib/components/CopyButton.test.ts +213 -0
  13. package/src/lib/components/DataTable.svelte +202 -0
  14. package/src/lib/components/DateRangePicker.svelte +185 -0
  15. package/src/lib/components/DiffEditor.svelte +174 -0
  16. package/src/lib/components/Drawer.svelte +69 -0
  17. package/src/lib/components/Fab.svelte +59 -0
  18. package/src/lib/components/Form.svelte +38 -0
  19. package/src/lib/components/FormField.svelte +51 -0
  20. package/src/lib/components/IconButton.svelte +86 -0
  21. package/src/lib/components/IconButton.test.ts +139 -0
  22. package/src/lib/components/InlineCode.svelte +28 -0
  23. package/src/lib/components/Pagination.svelte +65 -0
  24. package/src/lib/components/Radio.svelte +60 -0
  25. package/src/lib/components/RadioGroup.svelte +26 -0
  26. package/src/lib/components/SearchInput.svelte +77 -0
  27. package/src/lib/components/Skeleton.svelte +76 -0
  28. package/src/lib/components/StatCard.svelte +97 -0
  29. package/src/lib/components/ThemeProvider.svelte +157 -0
  30. package/src/lib/components/ThemeToggle.svelte +68 -0
  31. package/src/lib/components/ThreeWayMerge.svelte +185 -0
  32. package/src/lib/components/ui/MarkdownRenderer.svelte +126 -8
  33. package/src/lib/components/ui/Sparkline.svelte +1 -1
  34. package/src/lib/components/ui/StatusBadge.svelte +6 -3
  35. package/src/lib/components/ui/StatusDot.svelte +3 -3
  36. package/src/lib/index.ts +113 -63
  37. package/src/lib/utils/cn.test.ts +993 -0
  38. package/src/lib/utils/detectLanguage.ts +187 -0
  39. package/src/lib/utils/monaco-workers.d.ts +32 -0
  40. package/src/lib/utils/monacoLoader.ts +167 -0
  41. package/src/lib/utils/shikiHighlighter.ts +78 -0
  42. package/src/styles.css +100 -32
  43. package/src/lib/components/ui/Alert.svelte +0 -47
  44. package/src/lib/components/ui/AppCard.svelte +0 -76
  45. package/src/lib/components/ui/AppShell.svelte +0 -14
  46. package/src/lib/components/ui/AppSidebar.svelte +0 -45
  47. package/src/lib/components/ui/BranchPill.svelte +0 -19
  48. package/src/lib/components/ui/CodeBlock.svelte +0 -92
  49. package/src/lib/components/ui/CommentPill.svelte +0 -12
  50. package/src/lib/components/ui/CopyButton.svelte +0 -43
  51. package/src/lib/components/ui/CostDisplay.svelte +0 -26
  52. package/src/lib/components/ui/FilterBar.svelte +0 -63
  53. package/src/lib/components/ui/FormField.svelte +0 -34
  54. package/src/lib/components/ui/KanbanBoard.svelte +0 -27
  55. package/src/lib/components/ui/KanbanCard.svelte +0 -43
  56. package/src/lib/components/ui/KanbanColumn.svelte +0 -52
  57. package/src/lib/components/ui/ListCard.svelte +0 -9
  58. package/src/lib/components/ui/MetricCard.svelte +0 -79
  59. package/src/lib/components/ui/NavItem.svelte +0 -42
  60. package/src/lib/components/ui/NavSection.svelte +0 -17
  61. package/src/lib/components/ui/PageHeader.svelte +0 -25
  62. package/src/lib/components/ui/Pagination.svelte +0 -85
  63. package/src/lib/components/ui/PermissionChips.svelte +0 -49
  64. package/src/lib/components/ui/Section.svelte +0 -21
  65. package/src/lib/components/ui/SectionTitle.svelte +0 -16
  66. package/src/lib/components/ui/StatCard.svelte +0 -19
  67. package/src/lib/components/ui/StatusPill.svelte +0 -54
  68. package/src/lib/components/ui/Timeline.svelte +0 -85
  69. package/src/lib/components/ui/editor/RichEditor.svelte +0 -580
  70. package/src/lib/components/ui/editor/mention-suggestion.ts +0 -144
  71. package/src/lib/components/ui/table/Table.svelte +0 -12
  72. package/src/lib/components/ui/table/TableBody.svelte +0 -10
  73. package/src/lib/components/ui/table/TableCaption.svelte +0 -10
  74. package/src/lib/components/ui/table/TableCell.svelte +0 -10
  75. package/src/lib/components/ui/table/TableHead.svelte +0 -10
  76. package/src/lib/components/ui/table/TableHeader.svelte +0 -10
  77. package/src/lib/components/ui/table/TableRow.svelte +0 -10
  78. package/src/lib/components/ui/table/index.ts +0 -7
@@ -1,47 +0,0 @@
1
- <script lang="ts">
2
- import type { Component, Snippet } from 'svelte';
3
- import { cn } from '$lib/utils/cn.js';
4
-
5
- let {
6
- variant = 'default',
7
- title,
8
- children,
9
- icon: Icon,
10
- class: className,
11
- }: {
12
- variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info';
13
- title?: string;
14
- children?: Snippet;
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- icon?: any;
17
- class?: string;
18
- } = $props();
19
-
20
- const variantClasses: Record<string, string> = {
21
- default: 'border-border/60 bg-muted/40 text-foreground',
22
- destructive: 'border-destructive/30 bg-destructive/10 text-destructive',
23
- success: 'border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-400',
24
- warning: 'border-yellow-500/30 bg-yellow-500/10 text-yellow-500',
25
- info: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
26
- };
27
-
28
- const containerClass = $derived(
29
- cn(
30
- 'flex items-start gap-3 rounded-md border px-4 py-3 text-sm',
31
- variantClasses[variant],
32
- className,
33
- ),
34
- );
35
- </script>
36
-
37
- <div class={containerClass} role="alert">
38
- {#if Icon}
39
- <Icon class="mt-0.5 h-4 w-4 shrink-0" />
40
- {/if}
41
- <div>
42
- {#if title}
43
- <p class="font-semibold">{title}</p>
44
- {/if}
45
- {@render children?.()}
46
- </div>
47
- </div>
@@ -1,76 +0,0 @@
1
- <script lang="ts">
2
- // Reusable card for a Nucel-App row. Used by:
3
- // - Settings/Developer/Apps/Index (developer's owned apps)
4
- // - Marketplace/Index (public apps)
5
- // - Org/InstalledApps (apps installed on an org)
6
- //
7
- // The data model is intentionally flat — callers pass exactly what
8
- // they want rendered and slot any actions (Install, Suspend, Uninstall,
9
- // ChevronRight, etc.) into the trailing `actions` snippet.
10
- //
11
- // `meta` and `subtitle` are optional secondary lines so each call site
12
- // can attach its own context (e.g. "by @owner-org · published 5d ago"
13
- // for marketplace; "installed as deploy-bot[bot] · 3h ago" for org).
14
-
15
- import { Bot } from '@lucide/svelte';
16
- import PermissionChips from './PermissionChips.svelte';
17
- import type { Snippet } from 'svelte';
18
-
19
- let {
20
- name,
21
- href,
22
- icon_url = null,
23
- subtitle = null,
24
- meta = null,
25
- permissions = null,
26
- actions,
27
- badges,
28
- }: {
29
- name: string;
30
- href?: string;
31
- icon_url?: string | null;
32
- subtitle?: string | null;
33
- meta?: Snippet | null;
34
- permissions?: Record<string, string> | null;
35
- actions?: Snippet;
36
- badges?: Snippet;
37
- } = $props();
38
- </script>
39
-
40
- <div class="flex items-start gap-3 px-4 py-4">
41
- <div class="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary/10 text-primary">
42
- {#if icon_url}
43
- <img src={icon_url} alt="" class="h-full w-full object-cover" />
44
- {:else}
45
- <Bot class="h-4 w-4" />
46
- {/if}
47
- </div>
48
-
49
- <div class="min-w-0 flex-1">
50
- <div class="flex items-center gap-2 flex-wrap">
51
- {#if href}
52
- <a class="text-sm font-medium text-foreground hover:text-primary hover:underline" {href}>{name}</a>
53
- {:else}
54
- <span class="text-sm font-medium text-foreground">{name}</span>
55
- {/if}
56
- {#if badges}{@render badges()}{/if}
57
- </div>
58
- {#if subtitle}
59
- <p class="mt-0.5 text-xs text-muted-foreground">{subtitle}</p>
60
- {/if}
61
- {#if meta}
62
- <div class="mt-1 text-[11px] text-muted-foreground">{@render meta()}</div>
63
- {/if}
64
- {#if permissions}
65
- <div class="mt-2">
66
- <PermissionChips {permissions} compact />
67
- </div>
68
- {/if}
69
- </div>
70
-
71
- {#if actions}
72
- <div class="flex shrink-0 flex-col items-end gap-2">
73
- {@render actions()}
74
- </div>
75
- {/if}
76
- </div>
@@ -1,14 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- let { sidebar, header, children }: { sidebar?: Snippet; header?: Snippet; children?: Snippet } = $props();
4
- </script>
5
-
6
- <div class="flex h-screen overflow-hidden bg-background">
7
- {#if sidebar}{@render sidebar()}{/if}
8
- <div class="flex flex-1 flex-col overflow-hidden">
9
- {#if header}<header class="flex h-12 shrink-0 items-center border-b border-border bg-background px-4">{@render header()}</header>{/if}
10
- <main class="flex-1 overflow-y-auto p-6">
11
- {@render children?.()}
12
- </main>
13
- </div>
14
- </div>
@@ -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,19 +0,0 @@
1
- <script lang="ts">
2
- import { GitBranch } from '@lucide/svelte';
3
-
4
- let {
5
- name,
6
- size = 'sm',
7
- }: {
8
- name: string;
9
- size?: 'xs' | 'sm';
10
- } = $props();
11
-
12
- let sizeCls = $derived(size === 'xs' ? 'text-[10px] px-1.5 py-0.5' : 'text-[11px] px-2 py-0.5');
13
- let iconSize = $derived(size === 'xs' ? 'h-2.5 w-2.5' : 'h-3 w-3');
14
- </script>
15
-
16
- <span class="inline-flex items-center gap-1 rounded-md border border-border/60 bg-background font-mono text-foreground/80 {sizeCls}">
17
- <GitBranch class={iconSize} />
18
- {name}
19
- </span>
@@ -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,12 +0,0 @@
1
- <script lang="ts">
2
- import { MessageCircle } from '@lucide/svelte';
3
-
4
- let { count }: { count: number } = $props();
5
- </script>
6
-
7
- {#if count > 0}
8
- <div class="flex shrink-0 items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground">
9
- <MessageCircle class="h-3 w-3" />
10
- <span>{count}</span>
11
- </div>
12
- {/if}
@@ -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,27 +0,0 @@
1
- <script lang="ts">
2
- // Outer kanban container: a horizontally-scrollable row of columns plus
3
- // an optional trailing slot for an "Add column" affordance.
4
- //
5
- // Composition: caller puts <KanbanColumn> elements (or anything else)
6
- // into the default slot. DnD wiring stays out of this primitive — the
7
- // consumer attaches dndzone to whichever container they want sortable.
8
-
9
- import type { Snippet } from 'svelte';
10
-
11
- let {
12
- children,
13
- trailing,
14
- }: {
15
- children: Snippet;
16
- trailing?: Snippet;
17
- } = $props();
18
- </script>
19
-
20
- <div class="flex items-start gap-3 overflow-x-auto pb-4">
21
- {@render children()}
22
- {#if trailing}
23
- <div class="w-72 flex-shrink-0">
24
- {@render trailing()}
25
- </div>
26
- {/if}
27
- </div>
@@ -1,43 +0,0 @@
1
- <script lang="ts">
2
- // Minimal card for a kanban column. Title is required; body is optional
3
- // single-paragraph supplementary text. `onDelete` adds a hover-revealed
4
- // X button. `extra` is a snippet for arbitrary chrome (badges, avatars,
5
- // etc.) under the body.
6
-
7
- import { X } from '@lucide/svelte';
8
- import type { Snippet } from 'svelte';
9
-
10
- let {
11
- title,
12
- body = null,
13
- onDelete,
14
- extra,
15
- }: {
16
- title: string;
17
- body?: string | null;
18
- onDelete?: () => void;
19
- extra?: Snippet;
20
- } = $props();
21
- </script>
22
-
23
- <div class="group rounded-md border border-border bg-background p-2.5 text-sm shadow-sm">
24
- <div class="flex items-start justify-between gap-1">
25
- <span class="leading-snug">{title}</span>
26
- {#if onDelete}
27
- <button
28
- type="button"
29
- aria-label="Delete card"
30
- class="opacity-0 transition-opacity group-hover:opacity-100"
31
- onclick={onDelete}
32
- >
33
- <X class="h-3 w-3 text-muted-foreground hover:text-destructive" />
34
- </button>
35
- {/if}
36
- </div>
37
- {#if body}
38
- <p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{body}</p>
39
- {/if}
40
- {#if extra}
41
- <div class="mt-1.5">{@render extra()}</div>
42
- {/if}
43
- </div>
@@ -1,52 +0,0 @@
1
- <script lang="ts">
2
- // A single kanban column: header (title + count + optional delete),
3
- // a body snippet for cards (where the caller attaches a dndzone), and
4
- // a footer snippet for the per-column "Add card" affordance.
5
-
6
- import { X } from '@lucide/svelte';
7
- import type { Snippet } from 'svelte';
8
-
9
- let {
10
- name,
11
- count,
12
- onDelete,
13
- body,
14
- footer,
15
- }: {
16
- name: string;
17
- count: number;
18
- /** When provided, renders a small delete-X in the header. */
19
- onDelete?: () => void;
20
- /** Sortable body — caller drops their dndzone-decorated container here. */
21
- body: Snippet;
22
- /** Optional footer (e.g. "+ Add card"). */
23
- footer?: Snippet;
24
- } = $props();
25
- </script>
26
-
27
- <div class="w-72 flex-shrink-0 rounded-lg bg-muted/50 p-3">
28
- <div class="mb-2 flex items-center justify-between">
29
- <h3 class="text-sm font-semibold">{name}</h3>
30
- <div class="flex items-center gap-1">
31
- <span class="rounded-full bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
32
- {count}
33
- </span>
34
- {#if onDelete}
35
- <button
36
- type="button"
37
- aria-label={`Delete column ${name}`}
38
- class="rounded p-0.5 text-muted-foreground hover:bg-background hover:text-destructive"
39
- onclick={onDelete}
40
- >
41
- <X class="h-3 w-3" />
42
- </button>
43
- {/if}
44
- </div>
45
- </div>
46
-
47
- {@render body()}
48
-
49
- {#if footer}
50
- {@render footer()}
51
- {/if}
52
- </div>
@@ -1,9 +0,0 @@
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>