@nucel/ui 0.2.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 -32
  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 +120 -45
  37. package/src/lib/utils/detectLanguage.ts +187 -0
  38. package/src/lib/utils/monaco-workers.d.ts +32 -0
  39. package/src/lib/utils/monacoLoader.ts +167 -0
  40. package/src/lib/utils/shikiHighlighter.ts +78 -0
  41. package/src/styles.css +100 -32
  42. package/src/lib/components/ui/Alert.svelte +0 -47
  43. package/src/lib/components/ui/Alert.test.ts +0 -206
  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/BranchPill.test.ts +0 -121
  49. package/src/lib/components/ui/CommentPill.svelte +0 -12
  50. package/src/lib/components/ui/CostDisplay.svelte +0 -26
  51. package/src/lib/components/ui/CostDisplay.test.ts +0 -1115
  52. package/src/lib/components/ui/FormField.svelte +0 -34
  53. package/src/lib/components/ui/FormField.test.ts +0 -41
  54. package/src/lib/components/ui/ListCard.svelte +0 -9
  55. package/src/lib/components/ui/NavItem.svelte +0 -42
  56. package/src/lib/components/ui/NavSection.svelte +0 -17
  57. package/src/lib/components/ui/PageHeader.svelte +0 -25
  58. package/src/lib/components/ui/PageHeader.test.ts +0 -72
  59. package/src/lib/components/ui/PermissionChips.svelte +0 -49
  60. package/src/lib/components/ui/ProgressRing.test.ts +0 -239
  61. package/src/lib/components/ui/Section.svelte +0 -21
  62. package/src/lib/components/ui/Section.test.ts +0 -44
  63. package/src/lib/components/ui/SectionTitle.svelte +0 -16
  64. package/src/lib/components/ui/StatCard.svelte +0 -19
  65. package/src/lib/components/ui/StatusBadge.test.ts +0 -150
  66. package/src/lib/components/ui/StatusPill.svelte +0 -54
  67. package/src/lib/components/ui/StatusPill.test.ts +0 -125
  68. package/src/lib/components/ui/editor/RichEditor.svelte +0 -580
  69. package/src/lib/components/ui/editor/mention-suggestion.ts +0 -144
  70. package/src/lib/components/ui/table/Table.svelte +0 -12
  71. package/src/lib/components/ui/table/Table.test.ts +0 -317
  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
@@ -0,0 +1,185 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+ import CodeEditor from './CodeEditor.svelte';
4
+ import type { CodeEditorTheme } from './CodeEditor.svelte';
5
+
6
+ type Props = {
7
+ /** Common ancestor / merge base content. */
8
+ base?: string;
9
+ /** "Ours" side (current branch). */
10
+ ours?: string;
11
+ /** "Theirs" side (incoming branch). */
12
+ theirs?: string;
13
+ /** Resolved/merged content. Bindable — consumer reads this back. */
14
+ merged?: string;
15
+ /** Optional labels for each pane. */
16
+ baseLabel?: string;
17
+ oursLabel?: string;
18
+ theirsLabel?: string;
19
+ mergedLabel?: string;
20
+ /** Monaco language id. */
21
+ language?: string;
22
+ /** Theme mode. */
23
+ theme?: CodeEditorTheme;
24
+ /** Height per editor pane. */
25
+ paneHeight?: string;
26
+ /** Container className. */
27
+ class?: string;
28
+ /** Called when merged content changes. */
29
+ onchange?: (merged: string) => void;
30
+ };
31
+
32
+ let {
33
+ base = '',
34
+ ours = '',
35
+ theirs = '',
36
+ merged = $bindable(''),
37
+ baseLabel = 'Base',
38
+ oursLabel = 'Ours',
39
+ theirsLabel = 'Theirs',
40
+ mergedLabel = 'Merged result',
41
+ language = 'plaintext',
42
+ theme = 'auto',
43
+ paneHeight = '320px',
44
+ class: className,
45
+ onchange,
46
+ }: Props = $props();
47
+
48
+ function acceptOurs() {
49
+ merged = ours;
50
+ onchange?.(merged);
51
+ }
52
+
53
+ function acceptTheirs() {
54
+ merged = theirs;
55
+ onchange?.(merged);
56
+ }
57
+
58
+ function acceptBase() {
59
+ merged = base;
60
+ onchange?.(merged);
61
+ }
62
+
63
+ function acceptCombined() {
64
+ // Crude but useful starting point: concatenate ours then theirs with
65
+ // a marker between. Consumers wanting a smarter merge should run a
66
+ // proper 3-way text-merge on the backend and feed `merged` directly.
67
+ merged = `${ours}\n\n// ===== incoming =====\n${theirs}`;
68
+ onchange?.(merged);
69
+ }
70
+
71
+ function handleMergedChange(next: string) {
72
+ onchange?.(next);
73
+ }
74
+ </script>
75
+
76
+ <div
77
+ data-slot="three-way-merge"
78
+ class={cn('flex w-full flex-col gap-3', className)}
79
+ role="region"
80
+ aria-label="Three-way merge"
81
+ >
82
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-3">
83
+ <div class="flex flex-col gap-1.5">
84
+ <div class="text-muted-foreground flex items-center justify-between text-xs font-medium">
85
+ <span>{baseLabel}</span>
86
+ <button
87
+ type="button"
88
+ class="hover:text-foreground rounded px-1.5 py-0.5 text-[11px] outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
89
+ onclick={acceptBase}
90
+ >
91
+ Accept base
92
+ </button>
93
+ </div>
94
+ <CodeEditor
95
+ value={base}
96
+ {language}
97
+ {theme}
98
+ readOnly
99
+ height={paneHeight}
100
+ ariaLabel={`${baseLabel} (read-only)`}
101
+ />
102
+ </div>
103
+
104
+ <div class="flex flex-col gap-1.5">
105
+ <div class="text-muted-foreground flex items-center justify-between text-xs font-medium">
106
+ <span class="text-success">{oursLabel}</span>
107
+ <button
108
+ type="button"
109
+ class="hover:text-foreground rounded px-1.5 py-0.5 text-[11px] outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
110
+ onclick={acceptOurs}
111
+ >
112
+ Accept ours
113
+ </button>
114
+ </div>
115
+ <CodeEditor
116
+ value={ours}
117
+ {language}
118
+ {theme}
119
+ readOnly
120
+ height={paneHeight}
121
+ ariaLabel={`${oursLabel} (read-only)`}
122
+ />
123
+ </div>
124
+
125
+ <div class="flex flex-col gap-1.5">
126
+ <div class="text-muted-foreground flex items-center justify-between text-xs font-medium">
127
+ <span class="text-warning">{theirsLabel}</span>
128
+ <button
129
+ type="button"
130
+ class="hover:text-foreground rounded px-1.5 py-0.5 text-[11px] outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
131
+ onclick={acceptTheirs}
132
+ >
133
+ Accept theirs
134
+ </button>
135
+ </div>
136
+ <CodeEditor
137
+ value={theirs}
138
+ {language}
139
+ {theme}
140
+ readOnly
141
+ height={paneHeight}
142
+ ariaLabel={`${theirsLabel} (read-only)`}
143
+ />
144
+ </div>
145
+ </div>
146
+
147
+ <div class="flex flex-col gap-1.5">
148
+ <div class="flex items-center justify-between gap-2">
149
+ <span class="text-foreground text-sm font-medium">{mergedLabel}</span>
150
+ <div class="flex items-center gap-2">
151
+ <button
152
+ type="button"
153
+ class="hover:bg-accent rounded-md border border-border px-2 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
154
+ onclick={acceptCombined}
155
+ title="Concatenate ours + theirs as a starting point for manual edit"
156
+ >
157
+ Combine
158
+ </button>
159
+ <button
160
+ type="button"
161
+ class="hover:bg-accent rounded-md border border-border px-2 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
162
+ onclick={acceptOurs}
163
+ >
164
+ Use ours
165
+ </button>
166
+ <button
167
+ type="button"
168
+ class="hover:bg-accent rounded-md border border-border px-2 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
169
+ onclick={acceptTheirs}
170
+ >
171
+ Use theirs
172
+ </button>
173
+ </div>
174
+ </div>
175
+ <CodeEditor
176
+ bind:value={merged}
177
+ {language}
178
+ {theme}
179
+ height={paneHeight}
180
+ placeholder="Edit the merged result here…"
181
+ ariaLabel={mergedLabel}
182
+ onchange={handleMergedChange}
183
+ />
184
+ </div>
185
+ </div>
@@ -1,16 +1,113 @@
1
1
  <script lang="ts">
2
2
  import { marked } from 'marked';
3
3
  import DOMPurify from 'dompurify';
4
+ import { onMount, tick } from 'svelte';
5
+ import {
6
+ getHighlighter,
7
+ loadLanguage,
8
+ resolveLang,
9
+ SHIKI_LIGHT_THEME,
10
+ SHIKI_DARK_THEME,
11
+ } from '../../utils/shikiHighlighter.js';
4
12
 
5
- let { content = '' }: { content?: string } = $props();
13
+ let {
14
+ content,
15
+ highlight = true,
16
+ }: {
17
+ content: string;
18
+ /**
19
+ * When true (default), fenced code blocks are run through Shiki after
20
+ * the markdown is rendered. Set to false to fall back to plain `<pre>`.
21
+ */
22
+ highlight?: boolean;
23
+ } = $props();
24
+
25
+ let host = $state<HTMLDivElement | undefined>(undefined);
26
+ let isDark = $state(false);
6
27
 
7
28
  let html = $derived(
8
- content ? DOMPurify.sanitize(marked.parse(content, { gfm: true, breaks: false }) as string) : '',
29
+ DOMPurify.sanitize(marked.parse(content, { gfm: true, breaks: false }) as string, {
30
+ // Allow Shiki's inline style attributes on spans (colour tokens).
31
+ ADD_ATTR: ['style', 'data-line', 'data-language'],
32
+ }),
9
33
  );
34
+
35
+ function readDarkMode(): boolean {
36
+ if (typeof document === 'undefined') return false;
37
+ return document.documentElement.classList.contains('dark');
38
+ }
39
+
40
+ onMount(() => {
41
+ isDark = readDarkMode();
42
+ const mo = new MutationObserver(() => {
43
+ isDark = readDarkMode();
44
+ });
45
+ mo.observe(document.documentElement, {
46
+ attributes: true,
47
+ attributeFilter: ['class'],
48
+ });
49
+ return () => mo.disconnect();
50
+ });
51
+
52
+ // Re-highlight whenever the markdown re-renders or the theme flips.
53
+ $effect(() => {
54
+ void html;
55
+ void isDark;
56
+ if (!highlight || typeof window === 'undefined' || !host) return;
57
+
58
+ let cancelled = false;
59
+ (async () => {
60
+ // Wait one tick so the @html DOM is in place.
61
+ await tick();
62
+ if (cancelled || !host) return;
63
+
64
+ const blocks = host.querySelectorAll('pre > code[class*="language-"]');
65
+ if (blocks.length === 0) return;
66
+
67
+ const theme = isDark ? SHIKI_DARK_THEME : SHIKI_LIGHT_THEME;
68
+ const hl = await getHighlighter();
69
+
70
+ for (const block of Array.from(blocks)) {
71
+ if (cancelled) return;
72
+ const pre = block.parentElement;
73
+ if (!pre || pre.dataset.shikiDone === 'true') continue;
74
+
75
+ const langClass = Array.from(block.classList).find((c) =>
76
+ c.startsWith('language-'),
77
+ );
78
+ const requested = langClass ? langClass.slice('language-'.length) : 'plaintext';
79
+
80
+ await loadLanguage(requested);
81
+ const lang = resolveLang(requested);
82
+ const code = block.textContent ?? '';
83
+
84
+ try {
85
+ const shikiHtml = hl.codeToHtml(code, { lang, theme });
86
+ // Shiki only emits <pre><code><span> with style="color: #xxx"
87
+ // attributes — fully trusted output, but parse it via the DOM
88
+ // parser (rather than innerHTML) to keep our defensive defaults.
89
+ const doc = new DOMParser().parseFromString(shikiHtml, 'text/html');
90
+ const next = doc.body.firstElementChild;
91
+ if (next) {
92
+ (next as HTMLElement).dataset.shikiDone = 'true';
93
+ (next as HTMLElement).dataset.lang = requested;
94
+ pre.replaceWith(next);
95
+ }
96
+ } catch (err) {
97
+ console.warn('[@nucel/ui MarkdownRenderer] Shiki failed:', err);
98
+ pre.dataset.shikiDone = 'true';
99
+ }
100
+ }
101
+ })();
102
+
103
+ return () => {
104
+ cancelled = true;
105
+ };
106
+ });
10
107
  </script>
11
108
 
12
- <div class="md-body">
13
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
109
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
110
+ <div bind:this={host} class="md-body" data-theme={isDark ? 'dark' : 'light'}>
14
111
  {@html html}
15
112
  </div>
16
113
 
@@ -27,7 +124,8 @@
27
124
  padding-bottom: 0.5rem;
28
125
  color: var(--foreground);
29
126
  }
30
- .md-body :global(code) {
127
+ /* Inline code (not inside a <pre>) — keep simple. */
128
+ .md-body :global(code:not(pre code)) {
31
129
  background: var(--muted);
32
130
  padding: 0.125rem 0.375rem;
33
131
  border-radius: 0.25rem;
@@ -35,17 +133,37 @@
35
133
  font-size: 0.875em;
36
134
  color: var(--foreground);
37
135
  }
136
+ /* Fallback <pre> (before Shiki replaces it, or when highlight=false). */
38
137
  .md-body :global(pre) {
39
- background: var(--background);
138
+ background: var(--muted);
40
139
  padding: 1rem;
41
- border-radius: 0.375rem;
140
+ border-radius: 0.5rem;
42
141
  overflow-x: auto;
43
142
  border: 1px solid var(--border);
44
143
  }
45
144
  .md-body :global(pre code) {
46
145
  background: none;
47
146
  padding: 0;
48
- color: var(--muted-foreground);
147
+ color: var(--foreground);
148
+ }
149
+ /* Shiki-rendered <pre class="shiki ...">.
150
+ The colour tokens come from Shiki; we own the chrome. */
151
+ .md-body :global(pre.shiki) {
152
+ margin: 1rem 0;
153
+ padding: 1rem;
154
+ border-radius: 0.5rem;
155
+ border: 1px solid var(--border);
156
+ overflow-x: auto;
157
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
158
+ font-size: 0.85rem;
159
+ line-height: 1.55;
160
+ tab-size: 2;
161
+ }
162
+ .md-body :global(pre.shiki code) {
163
+ display: block;
164
+ min-width: max-content;
165
+ background: transparent;
166
+ padding: 0;
49
167
  }
50
168
  .md-body :global(a) {
51
169
  color: var(--primary);
@@ -3,7 +3,7 @@
3
3
 
4
4
  let {
5
5
  ref = $bindable(null),
6
- data = [] as number[],
6
+ data,
7
7
  color = 'stroke-primary',
8
8
  fillColor = 'fill-primary/10',
9
9
  width = 80,
@@ -9,9 +9,12 @@
9
9
  const classes: Record<string, string> = {
10
10
  open: 'border-success/30 bg-success/10 text-success',
11
11
  closed: '',
12
- merged: 'border-purple-500/30 bg-purple-500/10 text-purple-400',
13
- running: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
14
- active: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
12
+ merged:
13
+ 'border-purple-500/30 bg-purple-500/10 text-purple-700 dark:border-purple-400/30 dark:bg-purple-400/10 dark:text-purple-300',
14
+ running:
15
+ 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:border-blue-400/30 dark:bg-blue-400/10 dark:text-blue-300',
16
+ active:
17
+ 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:border-blue-400/30 dark:bg-blue-400/10 dark:text-blue-300',
15
18
  passed: 'border-success/30 bg-success/10 text-success',
16
19
  succeeded: 'border-success/30 bg-success/10 text-success',
17
20
  failed: 'border-destructive/30 bg-destructive/10 text-destructive',
@@ -41,11 +41,11 @@
41
41
  closed: 'bg-muted-foreground',
42
42
  paused: 'bg-muted-foreground',
43
43
  terminated: 'bg-muted-foreground',
44
- running: 'bg-blue-500',
45
- merged: 'bg-purple-500',
44
+ running: 'bg-blue-500 dark:bg-blue-400',
45
+ merged: 'bg-purple-500 dark:bg-purple-400',
46
46
  failed: 'bg-destructive',
47
47
  error: 'bg-destructive',
48
- warning: 'bg-yellow-500',
48
+ warning: 'bg-warning',
49
49
  };
50
50
 
51
51
  const resolvedColor = $derived(
package/src/lib/index.ts CHANGED
@@ -163,8 +163,8 @@ export {
163
163
  // Separator
164
164
  export { Separator } from './components/ui/separator/index.js';
165
165
 
166
- // Skeleton
167
- export { Skeleton } from './components/ui/skeleton/index.js';
166
+ // Skeleton (primitive — kept for backwards-compat)
167
+ export { Skeleton as SkeletonPrimitive } from './components/ui/skeleton/index.js';
168
168
 
169
169
  // Sonner (Toaster)
170
170
  export { Toaster } from './components/ui/sonner/index.js';
@@ -243,61 +243,136 @@ export { default as Sparkline } from './components/ui/Sparkline.svelte';
243
243
  // ProviderIcon
244
244
  export { default as ProviderIcon } from './components/ui/ProviderIcon.svelte';
245
245
 
246
- // RichEditor — full-featured TipTap editor (wiki, issues, comments)
247
- export { default as RichEditor } from './components/ui/editor/RichEditor.svelte';
246
+ // Checkbox (shadcn-styled wrapper over bits-ui)
247
+ export { default as Checkbox } from './components/Checkbox.svelte';
248
248
 
249
- // CostDisplay animated cost/currency display component
250
- export { default as CostDisplay } from './components/ui/CostDisplay.svelte';
249
+ // RadioGroup + Radio (shadcn-styled wrappers over bits-ui)
250
+ export { default as RadioGroup } from './components/RadioGroup.svelte';
251
+ export { default as Radio } from './components/Radio.svelte';
251
252
 
252
- // StatusPill
253
- export { default as StatusPill } from './components/ui/StatusPill.svelte';
253
+ // Form + FormField (InertiaJS-friendly defaults)
254
+ export { default as Form } from './components/Form.svelte';
255
+ export { default as FormField } from './components/FormField.svelte';
254
256
 
255
- // PageHeader
256
- export { default as PageHeader } from './components/ui/PageHeader.svelte';
257
+ // Pagination (composite, bits-ui driven)
258
+ export { default as Pagination } from './components/Pagination.svelte';
257
259
 
258
- // Table
259
- export {
260
- Table,
261
- TableHeader,
262
- TableBody,
263
- TableRow,
264
- TableHead,
265
- TableCell,
266
- TableCaption,
267
- } from './components/ui/table/index.js';
260
+ // StatCard (dashboard metric)
261
+ export { default as StatCard } from './components/StatCard.svelte';
262
+
263
+ // Breadcrumbs (array-driven composite; for low-level pieces use Breadcrumb*)
264
+ export { default as Breadcrumbs } from './components/Breadcrumbs.svelte';
265
+
266
+ // ---- 0.5.0 additions ----
267
+
268
+ // DataTable
269
+ export { default as DataTable } from './components/DataTable.svelte';
270
+ export type { ColumnDef, SortDirection } from './components/DataTable.svelte';
271
+
272
+ // SearchInput
273
+ export { default as SearchInput } from './components/SearchInput.svelte';
274
+
275
+ // DateRangePicker
276
+ export { default as DateRangePicker } from './components/DateRangePicker.svelte';
277
+ export type { DateRange, DateRangePreset } from './components/DateRangePicker.svelte';
278
+ export { DEFAULT_PRESETS as DateRangePickerDefaultPresets } from './components/DateRangePicker.svelte';
279
+
280
+ // CodeBlock
281
+ export { default as CodeBlock } from './components/CodeBlock.svelte';
268
282
 
269
- // Alert
270
- export { default as Alert } from './components/ui/Alert.svelte';
283
+ // Skeleton (composite — shimmer + width/height; supersedes SkeletonPrimitive)
284
+ export { default as Skeleton } from './components/Skeleton.svelte';
271
285
 
272
- // ListCard
273
- export { default as ListCard } from './components/ui/ListCard.svelte';
286
+ // Drawer (side panel, built on Sheet primitive)
287
+ export { default as Drawer } from './components/Drawer.svelte';
274
288
 
275
- // StatCard
276
- export { default as StatCard } from './components/ui/StatCard.svelte';
289
+ // Combobox (searchable Select built on bits-ui Combobox)
290
+ export { default as Combobox } from './components/Combobox.svelte';
291
+ export type { ComboboxOption } from './components/Combobox.svelte';
277
292
 
278
- // BranchPill
279
- export { default as BranchPill } from './components/ui/BranchPill.svelte';
293
+ // CommandPalette (cmdk-style palette built on bits-ui Command + Dialog)
294
+ export { default as CommandPalette } from './components/CommandPalette.svelte';
295
+ export type { CommandPaletteItem } from './components/CommandPalette.svelte';
280
296
 
281
- // CommentPill
282
- export { default as CommentPill } from './components/ui/CommentPill.svelte';
297
+ // ---- 0.6.0 additions ----
283
298
 
284
- // PermissionChipsNucel-App permission badges
285
- export { default as PermissionChips } from './components/ui/PermissionChips.svelte';
299
+ // ThemeProviderexposes a `theme` ('light' | 'dark' | 'system') context,
300
+ // persists the user's preference in localStorage, and reacts to
301
+ // `prefers-color-scheme` when set to 'system'.
302
+ export { default as ThemeProvider } from './components/ThemeProvider.svelte';
303
+ export {
304
+ getThemeContext,
305
+ type Theme,
306
+ type ResolvedTheme,
307
+ type ThemeContext,
308
+ } from './components/ThemeProvider.svelte';
309
+
310
+ // ThemeToggle — icon button cycling system -> light -> dark -> system.
311
+ export { default as ThemeToggle } from './components/ThemeToggle.svelte';
312
+
313
+ // ---- 0.7.0 additions ----
314
+
315
+ // CodeEditor — Monaco-powered editable code editor.
316
+ // Lazy-loaded on first mount; SSR-safe; matches @nucel/ui design tokens.
317
+ export { default as CodeEditor } from './components/CodeEditor.svelte';
318
+ export type { CodeEditorTheme } from './components/CodeEditor.svelte';
286
319
 
287
- // AppCardNucel-App row card (marketplace, installed apps, owned apps)
288
- export { default as AppCard } from './components/ui/AppCard.svelte';
320
+ // DiffEditorMonaco-powered side-by-side or inline diff view.
321
+ export { default as DiffEditor } from './components/DiffEditor.svelte';
289
322
 
290
- // Section
291
- export { default as Section } from './components/ui/Section.svelte';
323
+ // ThreeWayMerge — base/ours/theirs/merged conflict-resolution UI on top of Monaco.
324
+ export { default as ThreeWayMerge } from './components/ThreeWayMerge.svelte';
292
325
 
293
- // SectionTitle
294
- export { default as SectionTitle } from './components/ui/SectionTitle.svelte';
326
+ // Monaco loader (advanced — use the components above when possible).
327
+ export { loadMonaco, resolveMonacoTheme } from './utils/monacoLoader.js';
295
328
 
296
- // FormField
297
- export { default as FormField } from './components/ui/FormField.svelte';
329
+ // ---- 0.8.0 additions ----
298
330
 
299
- // Layout
300
- export { default as AppShell } from './components/ui/AppShell.svelte';
301
- export { default as AppSidebar } from './components/ui/AppSidebar.svelte';
302
- export { default as NavItem } from './components/ui/NavItem.svelte';
303
- export { default as NavSection } from './components/ui/NavSection.svelte';
331
+ // InlineCode — single-line <code> with subtle background, no highlighting.
332
+ export { default as InlineCode } from './components/InlineCode.svelte';
333
+
334
+ // Shiki helpers (re-exported so consumers can warm the highlighter early or
335
+ // load extra languages outside CodeBlock e.g. for a route prefetch hook).
336
+ export {
337
+ getHighlighter as getShikiHighlighter,
338
+ loadLanguage as loadShikiLanguage,
339
+ SHIKI_LIGHT_THEME,
340
+ SHIKI_DARK_THEME,
341
+ } from './utils/shikiHighlighter.js';
342
+
343
+ // File-path → Shiki language id helper, useful for repo file viewers.
344
+ export { detectLanguageFromPath } from './utils/detectLanguage.js';
345
+
346
+ // ---- 0.9.0 additions (mobile primitives) ----
347
+
348
+ // BottomSheet — mobile bottom-anchored sheet (Sheet primitive with grabber +
349
+ // safe-area). Pair with `md:hidden` on triggers to keep desktop unchanged.
350
+ export { default as BottomSheet } from './components/BottomSheet.svelte';
351
+
352
+ // Fab — floating action button for mobile primary actions.
353
+ // Defaults to `md:hidden`; pass `alwaysVisible` to show on desktop too.
354
+ export { default as Fab } from './components/Fab.svelte';
355
+
356
+ // ---- 0.10.0 additions (raw-primitive gap closers) ----
357
+
358
+ // CopyButton — copy-to-clipboard button with built-in "Copied" feedback and
359
+ // timeout reset. Replaces the duplicated clipboard.writeText + copied-state
360
+ // pattern in repo clone boxes, token reveal screens, session viewers, etc.
361
+ export { default as CopyButton } from './components/CopyButton.svelte';
362
+ export type { CopyButtonVariant, CopyButtonSize } from './components/CopyButton.svelte';
363
+
364
+ // IconButton — icon-only button OR anchor (pass `href`). Defaults to the
365
+ // muted-foreground "toolbar glyph" look (hover:bg-accent) used in TopBar,
366
+ // file trees, and dismiss buttons. `size="tap"` gives a 44×44 mobile target.
367
+ // `aria-label` is required.
368
+ export { default as IconButton } from './components/IconButton.svelte';
369
+ export {
370
+ iconButtonVariants,
371
+ type IconButtonVariant,
372
+ type IconButtonSize,
373
+ type IconButtonProps,
374
+ } from './components/IconButton.svelte';
375
+
376
+ // ColorInput — styled native <input type="color"> wrapper matching the
377
+ // form-control border/ring/focus tokens. Optional `showValue` hex readout.
378
+ export { default as ColorInput } from './components/ColorInput.svelte';