@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,144 @@
1
+ import tippy, { type Instance, type Props as TippyProps } from 'tippy.js';
2
+
3
+ export type MentionItem = {
4
+ id: string;
5
+ label: string;
6
+ type?: 'user' | 'team';
7
+ slug?: string;
8
+ avatar_url?: string;
9
+ sublabel?: string;
10
+ };
11
+
12
+ function escapeHtml(text: string): string {
13
+ const div = document.createElement('div');
14
+ div.textContent = text;
15
+ return div.innerHTML;
16
+ }
17
+
18
+ function getInitials(name: string): string {
19
+ return name.split(' ').map(p => p.charAt(0)).join('').toUpperCase().slice(0, 2);
20
+ }
21
+
22
+ function createDropdown(props: { items: MentionItem[]; command: (item: MentionItem) => void }) {
23
+ const el = document.createElement('div');
24
+ el.className = 'mention-list';
25
+ let selectedIndex = 0;
26
+ let items = props.items;
27
+ let command = props.command;
28
+
29
+ function render() {
30
+ if (items.length === 0) {
31
+ el.innerHTML = `<div class="mention-list-empty">No results</div>`;
32
+ return;
33
+ }
34
+ if (items.length === 1 && (items[0] as any).type === 'hint') {
35
+ el.innerHTML = `<div class="mention-list-empty">${escapeHtml(items[0].label)}</div>`;
36
+ return;
37
+ }
38
+ el.innerHTML = items.map((item, i) => `
39
+ <button type="button" class="mention-list-item ${i === selectedIndex ? 'is-selected' : ''}" data-index="${i}">
40
+ ${item.avatar_url
41
+ ? `<img src="${escapeHtml(item.avatar_url)}" alt="" class="mention-avatar" />`
42
+ : `<span class="mention-avatar-fallback">${getInitials(item.label)}</span>`
43
+ }
44
+ <span class="mention-info">
45
+ <span class="mention-label">${escapeHtml(item.label)}</span>
46
+ ${item.sublabel ? `<span class="mention-sublabel">${escapeHtml(item.sublabel)}</span>` : ''}
47
+ </span>
48
+ ${item.type === 'team' ? `<span class="mention-badge">Team</span>` : ''}
49
+ </button>
50
+ `).join('');
51
+
52
+ el.querySelectorAll('.mention-list-item').forEach(btn => {
53
+ btn.addEventListener('click', () => {
54
+ const idx = parseInt((btn as HTMLElement).dataset.index ?? '0');
55
+ select(idx);
56
+ });
57
+ });
58
+ }
59
+
60
+ function select(idx: number) {
61
+ const item = items[idx];
62
+ if (item) command(item);
63
+ }
64
+
65
+ function updateSelection(idx: number) {
66
+ selectedIndex = ((idx % items.length) + items.length) % items.length;
67
+ render();
68
+ }
69
+
70
+ render();
71
+
72
+ return {
73
+ el,
74
+ update(newItems: MentionItem[], newCommand: (item: MentionItem) => void) {
75
+ items = newItems;
76
+ command = newCommand;
77
+ selectedIndex = 0;
78
+ render();
79
+ },
80
+ onKeyDown(key: string): boolean {
81
+ if (key === 'ArrowUp') { updateSelection(selectedIndex - 1); return true; }
82
+ if (key === 'ArrowDown') { updateSelection(selectedIndex + 1); return true; }
83
+ if (key === 'Enter') { select(selectedIndex); return true; }
84
+ return false;
85
+ },
86
+ };
87
+ }
88
+
89
+ export function createMentionSuggestion(mentionsUrl: string) {
90
+ return {
91
+ char: '@',
92
+ allowSpaces: false,
93
+ items: async ({ query }: { query: string }): Promise<MentionItem[]> => {
94
+ if (!query || query.length < 1) {
95
+ return [{ id: 'hint', label: 'Type to search users…', type: 'user' } as any];
96
+ }
97
+ try {
98
+ const sep = mentionsUrl.includes('?') ? '&' : '?';
99
+ const res = await fetch(`${mentionsUrl}${sep}query=${encodeURIComponent(query)}`, {
100
+ headers: { Accept: 'application/json' },
101
+ });
102
+ if (!res.ok) return [];
103
+ const data = await res.json();
104
+ return Array.isArray(data.suggestions) ? data.suggestions : [];
105
+ } catch {
106
+ return [];
107
+ }
108
+ },
109
+ render: () => {
110
+ let dropdown: ReturnType<typeof createDropdown>;
111
+ let popup: Instance<TippyProps>[];
112
+
113
+ return {
114
+ onStart(props: any) {
115
+ dropdown = createDropdown({ items: props.items, command: (item) => props.command({ id: item.id, label: item.label, type: item.type, slug: item.slug }) });
116
+ if (!props.clientRect) return;
117
+ popup = tippy('body', {
118
+ getReferenceClientRect: props.clientRect,
119
+ appendTo: () => document.body,
120
+ content: dropdown.el,
121
+ showOnCreate: true,
122
+ interactive: true,
123
+ trigger: 'manual',
124
+ placement: 'bottom-start',
125
+ }) as Instance<TippyProps>[];
126
+ },
127
+ onUpdate(props: any) {
128
+ dropdown.update(props.items, (item) => props.command({ id: item.id, label: item.label, type: item.type, slug: item.slug }));
129
+ if (props.clientRect && popup?.[0]) {
130
+ popup[0].setProps({ getReferenceClientRect: props.clientRect });
131
+ }
132
+ },
133
+ onKeyDown(props: any) {
134
+ if (props.event.key === 'Escape') { popup?.[0]?.hide(); return true; }
135
+ return dropdown.onKeyDown(props.event.key);
136
+ },
137
+ onExit() {
138
+ popup?.[0]?.destroy();
139
+ dropdown.el.remove();
140
+ },
141
+ };
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className }: { children: Snippet; class?: string } = $props();
6
+ </script>
7
+
8
+ <div class={cn("w-full overflow-auto", className)}>
9
+ <table class="w-full caption-bottom text-sm">
10
+ {@render children()}
11
+ </table>
12
+ </div>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className }: { children: Snippet; class?: string } = $props();
6
+ </script>
7
+
8
+ <tbody class={cn("[&_tr:last-child]:border-0", className)}>
9
+ {@render children()}
10
+ </tbody>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className }: { children: Snippet; class?: string } = $props();
6
+ </script>
7
+
8
+ <caption class={cn("mt-4 text-xs text-muted-foreground", className)}>
9
+ {@render children()}
10
+ </caption>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className, ...rest }: { children?: Snippet; class?: string; [key: string]: unknown } = $props();
6
+ </script>
7
+
8
+ <td class={cn("px-4 py-2.5 align-middle [&:has([role=checkbox])]:pr-0", className)} {...rest}>
9
+ {@render children?.()}
10
+ </td>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className, ...rest }: { children?: Snippet; class?: string; [key: string]: unknown } = $props();
6
+ </script>
7
+
8
+ <th class={cn("h-9 px-4 text-left align-middle text-[10px] font-semibold uppercase tracking-wide text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} {...rest}>
9
+ {@render children?.()}
10
+ </th>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className }: { children: Snippet; class?: string } = $props();
6
+ </script>
7
+
8
+ <thead class={cn("[&_tr]:border-b [&_tr]:border-border", className)}>
9
+ {@render children()}
10
+ </thead>
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '$lib/utils/cn.js';
4
+
5
+ let { children, class: className }: { children: Snippet; class?: string } = $props();
6
+ </script>
7
+
8
+ <tr class={cn("border-b border-border transition-colors hover:bg-accent/50 data-[state=selected]:bg-accent", className)}>
9
+ {@render children()}
10
+ </tr>
@@ -0,0 +1,7 @@
1
+ export { default as Table } from './Table.svelte';
2
+ export { default as TableHeader } from './TableHeader.svelte';
3
+ export { default as TableBody } from './TableBody.svelte';
4
+ export { default as TableRow } from './TableRow.svelte';
5
+ export { default as TableHead } from './TableHead.svelte';
6
+ export { default as TableCell } from './TableCell.svelte';
7
+ export { default as TableCaption } from './TableCaption.svelte';
package/src/lib/index.ts CHANGED
@@ -242,3 +242,87 @@ export { default as Sparkline } from './components/ui/Sparkline.svelte';
242
242
 
243
243
  // ProviderIcon
244
244
  export { default as ProviderIcon } from './components/ui/ProviderIcon.svelte';
245
+
246
+ // RichEditor — full-featured TipTap editor (wiki, issues, comments)
247
+ export { default as RichEditor } from './components/ui/editor/RichEditor.svelte';
248
+
249
+ // CostDisplay — animated cost/currency display component
250
+ export { default as CostDisplay } from './components/ui/CostDisplay.svelte';
251
+
252
+ // StatusPill
253
+ export { default as StatusPill } from './components/ui/StatusPill.svelte';
254
+
255
+ // PageHeader
256
+ export { default as PageHeader } from './components/ui/PageHeader.svelte';
257
+
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';
268
+
269
+ // Alert
270
+ export { default as Alert } from './components/ui/Alert.svelte';
271
+
272
+ // ListCard
273
+ export { default as ListCard } from './components/ui/ListCard.svelte';
274
+
275
+ // StatCard
276
+ export { default as StatCard } from './components/ui/StatCard.svelte';
277
+
278
+ // BranchPill
279
+ export { default as BranchPill } from './components/ui/BranchPill.svelte';
280
+
281
+ // CommentPill
282
+ export { default as CommentPill } from './components/ui/CommentPill.svelte';
283
+
284
+ // PermissionChips — Nucel-App permission badges
285
+ export { default as PermissionChips } from './components/ui/PermissionChips.svelte';
286
+
287
+ // AppCard — Nucel-App row card (marketplace, installed apps, owned apps)
288
+ export { default as AppCard } from './components/ui/AppCard.svelte';
289
+
290
+ // Kanban primitives — generic Trello-style column/card chrome.
291
+ // DnD wiring stays out: the consumer attaches svelte-dnd-action's
292
+ // `dndzone` to whichever container they want sortable.
293
+ export { default as KanbanBoard } from './components/ui/KanbanBoard.svelte';
294
+ export { default as KanbanColumn } from './components/ui/KanbanColumn.svelte';
295
+ export { default as KanbanCard } from './components/ui/KanbanCard.svelte';
296
+
297
+ // Section
298
+ export { default as Section } from './components/ui/Section.svelte';
299
+
300
+ // SectionTitle
301
+ export { default as SectionTitle } from './components/ui/SectionTitle.svelte';
302
+
303
+ // FormField
304
+ export { default as FormField } from './components/ui/FormField.svelte';
305
+
306
+ // Layout
307
+ export { default as AppShell } from './components/ui/AppShell.svelte';
308
+ export { default as AppSidebar } from './components/ui/AppSidebar.svelte';
309
+ export { default as NavItem } from './components/ui/NavItem.svelte';
310
+ export { default as NavSection } from './components/ui/NavSection.svelte';
311
+
312
+ // CopyButton
313
+ export { default as CopyButton } from './components/ui/CopyButton.svelte';
314
+
315
+ // Pagination
316
+ export { default as Pagination } from './components/ui/Pagination.svelte';
317
+
318
+ // CodeBlock
319
+ export { default as CodeBlock } from './components/ui/CodeBlock.svelte';
320
+
321
+ // Timeline
322
+ export { default as Timeline } from './components/ui/Timeline.svelte';
323
+
324
+ // FilterBar
325
+ export { default as FilterBar } from './components/ui/FilterBar.svelte';
326
+
327
+ // MetricCard
328
+ export { default as MetricCard } from './components/ui/MetricCard.svelte';
package/src/styles.css CHANGED
@@ -24,6 +24,8 @@
24
24
  --accent: oklch(0.967 0.001 286.375);
25
25
  --accent-foreground: oklch(0.21 0.006 285.885);
26
26
  --destructive: oklch(0.577 0.245 27.325);
27
+ --success: oklch(0.527 0.154 150.069);
28
+ --warning: oklch(0.769 0.188 70.08);
27
29
  --border: oklch(0.92 0.004 286.32);
28
30
  --input: oklch(0.92 0.004 286.32);
29
31
  --ring: oklch(0.705 0.015 286.067);
@@ -58,6 +60,8 @@
58
60
  --accent: oklch(0.22 0.006 286.033);
59
61
  --accent-foreground: oklch(0.97 0 0);
60
62
  --destructive: oklch(0.704 0.191 22.216);
63
+ --success: oklch(0.627 0.168 150.069);
64
+ --warning: oklch(0.828 0.189 84.429);
61
65
  --border: oklch(1 0 0 / 8%);
62
66
  --input: oklch(1 0 0 / 12%);
63
67
  --ring: oklch(0.552 0.016 285.938);
@@ -97,6 +101,8 @@
97
101
  --color-accent: var(--accent);
98
102
  --color-accent-foreground: var(--accent-foreground);
99
103
  --color-destructive: var(--destructive);
104
+ --color-success: var(--success);
105
+ --color-warning: var(--warning);
100
106
  --color-border: var(--border);
101
107
  --color-input: var(--input);
102
108
  --color-ring: var(--ring);