@nucel/ui 0.1.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 (147) hide show
  1. package/README.md +235 -0
  2. package/package.json +88 -0
  3. package/src/lib/components/ui/Backdrop.svelte +19 -0
  4. package/src/lib/components/ui/CountBadge.svelte +40 -0
  5. package/src/lib/components/ui/EmptyState.svelte +68 -0
  6. package/src/lib/components/ui/KbdShortcut.svelte +20 -0
  7. package/src/lib/components/ui/MarkdownRenderer.svelte +85 -0
  8. package/src/lib/components/ui/ProgressRing.svelte +61 -0
  9. package/src/lib/components/ui/ProviderIcon.svelte +57 -0
  10. package/src/lib/components/ui/ReviewBadge.svelte +54 -0
  11. package/src/lib/components/ui/Sparkline.svelte +61 -0
  12. package/src/lib/components/ui/StatusBadge.svelte +32 -0
  13. package/src/lib/components/ui/StatusDot.svelte +65 -0
  14. package/src/lib/components/ui/TabBar.svelte +127 -0
  15. package/src/lib/components/ui/VerticalSeparator.svelte +9 -0
  16. package/src/lib/components/ui/accordion/accordion-content.svelte +22 -0
  17. package/src/lib/components/ui/accordion/accordion-item.svelte +17 -0
  18. package/src/lib/components/ui/accordion/accordion-trigger.svelte +32 -0
  19. package/src/lib/components/ui/accordion/accordion.svelte +16 -0
  20. package/src/lib/components/ui/accordion/index.ts +16 -0
  21. package/src/lib/components/ui/avatar/avatar-fallback.svelte +17 -0
  22. package/src/lib/components/ui/avatar/avatar-image.svelte +17 -0
  23. package/src/lib/components/ui/avatar/avatar.svelte +19 -0
  24. package/src/lib/components/ui/avatar/index.ts +13 -0
  25. package/src/lib/components/ui/badge/badge.svelte +49 -0
  26. package/src/lib/components/ui/badge/index.ts +2 -0
  27. package/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +23 -0
  28. package/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte +20 -0
  29. package/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte +31 -0
  30. package/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte +23 -0
  31. package/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte +23 -0
  32. package/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte +27 -0
  33. package/src/lib/components/ui/breadcrumb/breadcrumb.svelte +21 -0
  34. package/src/lib/components/ui/breadcrumb/index.ts +25 -0
  35. package/src/lib/components/ui/button/button.svelte +82 -0
  36. package/src/lib/components/ui/button/index.ts +17 -0
  37. package/src/lib/components/ui/card/card-action.svelte +20 -0
  38. package/src/lib/components/ui/card/card-content.svelte +15 -0
  39. package/src/lib/components/ui/card/card-description.svelte +20 -0
  40. package/src/lib/components/ui/card/card-footer.svelte +20 -0
  41. package/src/lib/components/ui/card/card-header.svelte +23 -0
  42. package/src/lib/components/ui/card/card-title.svelte +20 -0
  43. package/src/lib/components/ui/card/card.svelte +23 -0
  44. package/src/lib/components/ui/card/index.ts +25 -0
  45. package/src/lib/components/ui/collapsible/collapsible-content.svelte +7 -0
  46. package/src/lib/components/ui/collapsible/collapsible-trigger.svelte +7 -0
  47. package/src/lib/components/ui/collapsible/collapsible.svelte +11 -0
  48. package/src/lib/components/ui/collapsible/index.ts +13 -0
  49. package/src/lib/components/ui/dialog/dialog-close.svelte +7 -0
  50. package/src/lib/components/ui/dialog/dialog-content.svelte +45 -0
  51. package/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
  52. package/src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
  53. package/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
  54. package/src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
  55. package/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  56. package/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
  57. package/src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
  58. package/src/lib/components/ui/dialog/dialog.svelte +7 -0
  59. package/src/lib/components/ui/dialog/index.ts +34 -0
  60. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  61. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +41 -0
  62. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +29 -0
  63. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  64. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  65. package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  66. package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  67. package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  68. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
  69. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +31 -0
  70. package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  71. package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  72. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
  73. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  74. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  75. package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  76. package/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  77. package/src/lib/components/ui/dropdown-menu/index.ts +54 -0
  78. package/src/lib/components/ui/input/index.ts +7 -0
  79. package/src/lib/components/ui/input/input.svelte +52 -0
  80. package/src/lib/components/ui/label/index.ts +7 -0
  81. package/src/lib/components/ui/label/label.svelte +20 -0
  82. package/src/lib/components/ui/navigation-menu/index.ts +28 -0
  83. package/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
  84. package/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
  85. package/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
  86. package/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
  87. package/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
  88. package/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte +34 -0
  89. package/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
  90. package/src/lib/components/ui/navigation-menu/navigation-menu.svelte +32 -0
  91. package/src/lib/components/ui/progress/index.ts +1 -0
  92. package/src/lib/components/ui/progress/progress.svelte +33 -0
  93. package/src/lib/components/ui/scroll-area/index.ts +10 -0
  94. package/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +31 -0
  95. package/src/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
  96. package/src/lib/components/ui/select/index.ts +37 -0
  97. package/src/lib/components/ui/select/select-content.svelte +45 -0
  98. package/src/lib/components/ui/select/select-group-heading.svelte +21 -0
  99. package/src/lib/components/ui/select/select-group.svelte +7 -0
  100. package/src/lib/components/ui/select/select-item.svelte +38 -0
  101. package/src/lib/components/ui/select/select-label.svelte +20 -0
  102. package/src/lib/components/ui/select/select-portal.svelte +7 -0
  103. package/src/lib/components/ui/select/select-scroll-down-button.svelte +20 -0
  104. package/src/lib/components/ui/select/select-scroll-up-button.svelte +20 -0
  105. package/src/lib/components/ui/select/select-separator.svelte +18 -0
  106. package/src/lib/components/ui/select/select-trigger.svelte +29 -0
  107. package/src/lib/components/ui/select/select.svelte +11 -0
  108. package/src/lib/components/ui/separator/index.ts +7 -0
  109. package/src/lib/components/ui/separator/separator.svelte +21 -0
  110. package/src/lib/components/ui/sheet/index.ts +34 -0
  111. package/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
  112. package/src/lib/components/ui/sheet/sheet-content.svelte +62 -0
  113. package/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
  114. package/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  115. package/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
  116. package/src/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  117. package/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  118. package/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
  119. package/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  120. package/src/lib/components/ui/sheet/sheet.svelte +7 -0
  121. package/src/lib/components/ui/skeleton/index.ts +1 -0
  122. package/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
  123. package/src/lib/components/ui/sonner/index.ts +1 -0
  124. package/src/lib/components/ui/sonner/sonner.svelte +10 -0
  125. package/src/lib/components/ui/tabs/index.ts +16 -0
  126. package/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
  127. package/src/lib/components/ui/tabs/tabs-list.svelte +16 -0
  128. package/src/lib/components/ui/tabs/tabs-trigger.svelte +20 -0
  129. package/src/lib/components/ui/tabs/tabs.svelte +19 -0
  130. package/src/lib/components/ui/textarea/index.ts +7 -0
  131. package/src/lib/components/ui/textarea/textarea.svelte +23 -0
  132. package/src/lib/components/ui/toggle/index.ts +13 -0
  133. package/src/lib/components/ui/toggle/toggle.svelte +52 -0
  134. package/src/lib/components/ui/toggle-group/index.ts +10 -0
  135. package/src/lib/components/ui/toggle-group/toggle-group-item.svelte +35 -0
  136. package/src/lib/components/ui/toggle-group/toggle-group.svelte +65 -0
  137. package/src/lib/components/ui/tooltip/index.ts +19 -0
  138. package/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
  139. package/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  140. package/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  141. package/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  142. package/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
  143. package/src/lib/index.ts +244 -0
  144. package/src/lib/utils/cn.test.ts +993 -0
  145. package/src/lib/utils/cn.ts +6 -0
  146. package/src/lib/utils.ts +12 -0
  147. package/src/styles.css +127 -0
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # @nucel/ui
2
+
3
+ A comprehensive Svelte 5 UI component library for Nucel projects.
4
+
5
+ ## Features
6
+
7
+ - **36 Components**: 24 shadcn-svelte primitives + 12 custom components
8
+ - **115 Component Files**: Full comprehensive coverage
9
+ - **Svelte 5**: Built with runes and modern Svelte patterns
10
+ - **TypeScript**: Full type safety
11
+ - **Tailwind CSS v4**: Seamless styling integration
12
+ - **Accessible**: Built on top of bits-ui for accessibility
13
+ - **Tree-shakeable**: Only import what you need
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @nucel/ui
19
+ # or
20
+ bun add @nucel/ui
21
+ ```
22
+
23
+ ## Peer Dependencies
24
+
25
+ ```bash
26
+ npm install svelte@^5.0.0 tailwindcss@^4.0.0
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```svelte
32
+ <script>
33
+ import {
34
+ Button,
35
+ Card,
36
+ CardContent,
37
+ CardHeader,
38
+ CardTitle,
39
+ StatusBadge,
40
+ Sparkline,
41
+ } from '@nucel/ui';
42
+ </script>
43
+
44
+ <Card>
45
+ <CardHeader>
46
+ <CardTitle>Dashboard</CardTitle>
47
+ </CardHeader>
48
+ <CardContent>
49
+ <StatusBadge status="running" />
50
+ <Sparkline data={[10, 25, 15, 30, 20, 35, 28]} />
51
+ <Button>Action</Button>
52
+ </CardContent>
53
+ </Card>
54
+ ```
55
+
56
+ ## Components
57
+
58
+ ### shadcn-svelte Primitives (24)
59
+
60
+ - **Accordion**: Expandable/collapsible sections
61
+ - **Avatar**: User avatar with fallback
62
+ - **Badge**: Status badges with variants
63
+ - **Breadcrumb**: Navigation breadcrumbs
64
+ - **Button**: Action buttons with multiple variants
65
+ - **Card**: Content containers with header, content, footer
66
+ - **Collapsible**: Expandable/collapsible content
67
+ - **Dialog**: Modal dialogs with overlay
68
+ - **DropdownMenu**: Context menus and dropdowns
69
+ - **Input**: Text input fields
70
+ - **Label**: Form labels
71
+ - **NavigationMenu**: Top-level navigation menus
72
+ - **Progress**: Progress bars
73
+ - **ScrollArea**: Custom scrollable areas
74
+ - **Select**: Dropdown selects with search
75
+ - **Separator**: Visual dividers
76
+ - **Sheet**: Side panels and drawers
77
+ - **Skeleton**: Loading placeholders
78
+ - **Sonner**: Toast notifications
79
+ - **Tabs**: Tabbed interfaces
80
+ - **Textarea**: Multi-line text inputs
81
+ - **Toggle**: Toggle switches
82
+ - **ToggleGroup**: Grouped toggles
83
+ - **Tooltip**: Hover tooltips
84
+
85
+ ### Custom Components (12)
86
+
87
+ - **Backdrop**: Fixed overlay backdrop for modals
88
+ - **CountBadge**: Animated count indicators (99+)
89
+ - **EmptyState**: Empty state placeholders
90
+ - **KbdShortcut**: Keyboard shortcut displays
91
+ - **MarkdownRenderer**: Sanitized markdown rendering
92
+ - **ProgressRing**: Circular progress indicator
93
+ - **ReviewBadge**: PR review status badges
94
+ - **Sparkline**: Mini line charts for data visualization
95
+ - **StatusBadge**: Combined status indicator with badge
96
+ - **StatusDot**: Animated status dots
97
+ - **TabBar**: Tab navigation with variants
98
+ - **VerticalSeparator**: Inline vertical dividers
99
+
100
+ ## Usage Examples
101
+
102
+ ### Dialog
103
+
104
+ ```svelte
105
+ <script>
106
+ import {
107
+ Dialog,
108
+ DialogContent,
109
+ DialogHeader,
110
+ DialogTitle,
111
+ DialogDescription,
112
+ DialogFooter,
113
+ Button,
114
+ } from '@nucel/ui';
115
+ let open = $state(false);
116
+ </script>
117
+
118
+ <Button onclick={() => (open = true)}>Open Dialog</Button>
119
+
120
+ <Dialog bind:open>
121
+ <DialogContent>
122
+ <DialogHeader>
123
+ <DialogTitle>Confirm Action</DialogTitle>
124
+ <DialogDescription>Are you sure you want to proceed?</DialogDescription>
125
+ </DialogHeader>
126
+ <DialogFooter>
127
+ <Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
128
+ <Button onclick={() => (open = false)}>Confirm</Button>
129
+ </DialogFooter>
130
+ </DialogContent>
131
+ </Dialog>
132
+ ```
133
+
134
+ ### Status Components
135
+
136
+ ```svelte
137
+ <script>
138
+ import { StatusDot, StatusBadge } from '@nucel/ui';
139
+ </script>
140
+
141
+ <!-- Simple status dot -->
142
+ <StatusDot color="bg-green-500" animated />
143
+ <StatusDot status="running" />
144
+ <StatusDot status="failed" size="sm" />
145
+
146
+ <!-- Combined status badge -->
147
+ <StatusBadge status="open" />
148
+ <StatusBadge status="merged" label="Merged by john" />
149
+ ```
150
+
151
+ ### Data Visualization
152
+
153
+ ```svelte
154
+ <script>
155
+ import { Sparkline, ProgressRing } from '@nucel/ui';
156
+ </script>
157
+
158
+ <!-- Mini chart -->
159
+ <Sparkline data={[10, 25, 15, 30, 20]} color="stroke-blue-500" />
160
+
161
+ <!-- Circular progress -->
162
+ <ProgressRing spent={75} limit={100} size={24} />
163
+ ```
164
+
165
+ ### Navigation
166
+
167
+ ```svelte
168
+ <script>
169
+ import { TabBar } from '@nucel/ui';
170
+ import { File, GitBranch, Settings } from '@lucide/svelte';
171
+
172
+ const items = [
173
+ { id: 'files', label: 'Files', icon: File },
174
+ { id: 'branches', label: 'Branches', icon: GitBranch, count: 3 },
175
+ { id: 'settings', label: 'Settings', icon: Settings },
176
+ ];
177
+
178
+ let selected = $state('files');
179
+ </script>
180
+
181
+ <TabBar {items} {selected} onselect={(id) => (selected = id)} variant="underline" />
182
+ ```
183
+
184
+ ## Styling
185
+
186
+ Components use Tailwind CSS v4 with the following theme structure:
187
+
188
+ ```css
189
+ @theme {
190
+ --color-background: oklch(...);
191
+ --color-foreground: oklch(...);
192
+ --color-primary: oklch(...);
193
+ --color-secondary: oklch(...);
194
+ --color-destructive: oklch(...);
195
+ /* ... more theme variables */
196
+ }
197
+ ```
198
+
199
+ Import the styles in your app:
200
+
201
+ ```css
202
+ @import '@nucel/ui/src/styles.css';
203
+ ```
204
+
205
+ ## Storybook
206
+
207
+ Explore all components with interactive examples:
208
+
209
+ ```bash
210
+ cd packages/ui
211
+ bun run storybook
212
+ ```
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ # Install dependencies
218
+ bun install
219
+
220
+ # Run checks
221
+ bun run check
222
+
223
+ # Run linter
224
+ bun run lint
225
+
226
+ # Format code
227
+ bun run format
228
+
229
+ # Build Storybook
230
+ bun run build-storybook
231
+ ```
232
+
233
+ ## License
234
+
235
+ MIT © Nucel Team
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@nucel/ui",
3
+ "version": "0.1.0",
4
+ "description": "A comprehensive Svelte 5 UI component library for Nucel projects",
5
+ "type": "module",
6
+ "svelte": "./src/lib/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/lib/index.ts",
10
+ "svelte": "./src/lib/index.ts",
11
+ "default": "./src/lib/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src/lib",
16
+ "src/styles.css"
17
+ ],
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "vite build",
21
+ "check": "svelte-check --tsconfig ./tsconfig.json",
22
+ "lint": "eslint .",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check .",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "storybook": "storybook dev -p 6006",
28
+ "build-storybook": "storybook build",
29
+ "prepublishOnly": "echo 'publishing @nucel/ui'"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/nucel/ui.git"
34
+ },
35
+ "keywords": [
36
+ "svelte",
37
+ "svelte5",
38
+ "ui",
39
+ "components",
40
+ "shadcn",
41
+ "tailwindcss",
42
+ "bits-ui",
43
+ "component-library"
44
+ ],
45
+ "author": "Nucel Team",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@lucide/svelte": "^0.564.0",
49
+ "bits-ui": "^2.16.1",
50
+ "clsx": "^2.1.1",
51
+ "dompurify": "^3.3.3",
52
+ "marked": "^17.0.5",
53
+ "svelte-sonner": "^1.0.7",
54
+ "tailwind-merge": "^3.5.0",
55
+ "tailwind-variants": "^3.2.2"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^9.39.2",
59
+ "@storybook/addon-a11y": "^10.0.0",
60
+ "@storybook/addon-docs": "^10.0.0",
61
+ "@storybook/addon-svelte-csf": "^5.0.0",
62
+ "@storybook/addon-vitest": "^10.0.0",
63
+ "@storybook/svelte-vite": "^10.3.4",
64
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
65
+ "@tailwindcss/vite": "^4.1.18",
66
+ "@types/dompurify": "^3.2.0",
67
+ "eslint": "^9.39.2",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "eslint-plugin-svelte": "^3.14.0",
70
+ "globals": "^17.3.0",
71
+ "prettier": "^3.8.1",
72
+ "prettier-plugin-svelte": "^3.4.1",
73
+ "prettier-plugin-tailwindcss": "^0.7.2",
74
+ "storybook": "^10.0.0",
75
+ "svelte": "^5.49.2",
76
+ "svelte-check": "^4.3.6",
77
+ "tailwindcss": "^4.1.18",
78
+ "tw-animate-css": "^1.4.0",
79
+ "typescript": "^5.8.0",
80
+ "typescript-eslint": "^8.54.0",
81
+ "vite": "^8.0.0",
82
+ "vitest": "^4.0.18"
83
+ },
84
+ "peerDependencies": {
85
+ "svelte": "^5.0.0",
86
+ "tailwindcss": "^4.0.0"
87
+ }
88
+ }
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ let {
3
+ onclick,
4
+ zIndex = 40,
5
+ }: {
6
+ onclick?: () => void;
7
+ zIndex?: number;
8
+ } = $props();
9
+ </script>
10
+
11
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
12
+ <div
13
+ class="fixed inset-0"
14
+ style="z-index: {zIndex}"
15
+ {onclick}
16
+ onkeydown={(e) => {
17
+ if (e.key === 'Escape') onclick?.();
18
+ }}
19
+ ></div>
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ let {
3
+ count,
4
+ max = 99,
5
+ size = 'md',
6
+ variant = 'primary',
7
+ }: {
8
+ count: number;
9
+ max?: number;
10
+ size?: 'sm' | 'md';
11
+ variant?: 'primary' | 'secondary' | 'destructive';
12
+ } = $props();
13
+
14
+ const display = $derived(count > max ? `${max}+` : `${count}`);
15
+
16
+ const sizeClass = $derived(
17
+ size === 'sm'
18
+ ? 'h-3.5 min-w-[14px] text-[7px] px-0.5'
19
+ : 'h-[18px] min-w-[18px] text-[9px] px-1',
20
+ );
21
+
22
+ const variantClass = $derived.by(() => {
23
+ switch (variant) {
24
+ case 'destructive':
25
+ return 'bg-destructive text-destructive-foreground shadow-sm shadow-destructive/30';
26
+ case 'secondary':
27
+ return 'bg-secondary text-secondary-foreground';
28
+ default:
29
+ return 'bg-primary text-primary-foreground shadow-sm shadow-primary/30';
30
+ }
31
+ });
32
+ </script>
33
+
34
+ {#if count > 0}
35
+ <span
36
+ class="flex items-center justify-center rounded-full font-bold {sizeClass} {variantClass} animate-in zoom-in-50 duration-200"
37
+ >
38
+ {display}
39
+ </span>
40
+ {/if}
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import type { Snippet, Component } from 'svelte';
3
+
4
+ let {
5
+ icon,
6
+ title,
7
+ description,
8
+ action,
9
+ variant = 'default',
10
+ }: {
11
+ icon?: Component<{ class?: string }>;
12
+ title: string;
13
+ description?: string;
14
+ action?: Snippet;
15
+ variant?: 'default' | 'compact' | 'inline';
16
+ } = $props();
17
+ </script>
18
+
19
+ {#if variant === 'inline'}
20
+ <div class="text-muted-foreground flex items-center gap-2 px-3 py-2 text-xs">
21
+ {#if icon}
22
+ {@const Icon = icon}
23
+ <Icon class="h-3.5 w-3.5 shrink-0" />
24
+ {/if}
25
+ <span>{title}</span>
26
+ </div>
27
+ {:else if variant === 'compact'}
28
+ <div class="flex flex-col items-center gap-1.5 px-4 py-6 text-center">
29
+ {#if icon}
30
+ {@const Icon = icon}
31
+ <div class="bg-secondary flex h-9 w-9 items-center justify-center rounded-lg">
32
+ <Icon class="text-muted-foreground h-4 w-4" />
33
+ </div>
34
+ {/if}
35
+ <p class="text-muted-foreground text-xs font-medium">{title}</p>
36
+ {#if description}
37
+ <p class="text-muted-foreground/70 max-w-[200px] text-[10px]">{description}</p>
38
+ {/if}
39
+ {#if action}
40
+ <div class="mt-1">
41
+ {@render action()}
42
+ </div>
43
+ {/if}
44
+ </div>
45
+ {:else}
46
+ <div class="flex flex-col items-center gap-2 px-6 py-10 text-center">
47
+ {#if icon}
48
+ {@const Icon = icon}
49
+ <div class="relative mb-1">
50
+ <div class="bg-secondary flex h-12 w-12 items-center justify-center rounded-xl">
51
+ <Icon class="text-muted-foreground h-5 w-5" />
52
+ </div>
53
+ <div
54
+ class="border-background bg-muted absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full border-2"
55
+ ></div>
56
+ </div>
57
+ {/if}
58
+ <p class="text-foreground/80 text-sm font-medium">{title}</p>
59
+ {#if description}
60
+ <p class="text-muted-foreground max-w-[260px] text-xs">{description}</p>
61
+ {/if}
62
+ {#if action}
63
+ <div class="mt-2">
64
+ {@render action()}
65
+ </div>
66
+ {/if}
67
+ </div>
68
+ {/if}
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ let {
3
+ keys,
4
+ size = 'sm',
5
+ }: {
6
+ keys: string[];
7
+ size?: 'xs' | 'sm';
8
+ } = $props();
9
+
10
+ const sizeClass = $derived(size === 'xs' ? 'px-0.5 py-px text-[9px]' : 'px-1 py-0.5 text-[10px]');
11
+ </script>
12
+
13
+ <span class="inline-flex items-center gap-0.5">
14
+ {#each keys as key, i (key)}
15
+ {#if i > 0}
16
+ <span class="text-muted-foreground/30 text-[10px]">+</span>
17
+ {/if}
18
+ <kbd class="bg-muted rounded {sizeClass} text-muted-foreground font-mono">{key}</kbd>
19
+ {/each}
20
+ </span>
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import { marked } from 'marked';
3
+ import DOMPurify from 'dompurify';
4
+
5
+ let { content }: { content: string } = $props();
6
+
7
+ let html = $derived(
8
+ DOMPurify.sanitize(marked.parse(content, { gfm: true, breaks: false }) as string),
9
+ );
10
+ </script>
11
+
12
+ <div class="md-body">
13
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
14
+ {@html html}
15
+ </div>
16
+
17
+ <style>
18
+ /* :global() is required here to style injected {@html} content */
19
+ .md-body {
20
+ max-width: none;
21
+ }
22
+ .md-body :global(h1),
23
+ .md-body :global(h2),
24
+ .md-body :global(h3),
25
+ .md-body :global(h4) {
26
+ border-bottom: 1px solid var(--border);
27
+ padding-bottom: 0.5rem;
28
+ color: var(--foreground);
29
+ }
30
+ .md-body :global(code) {
31
+ background: var(--muted);
32
+ padding: 0.125rem 0.375rem;
33
+ border-radius: 0.25rem;
34
+ font-family: var(--font-mono, monospace);
35
+ font-size: 0.875em;
36
+ color: var(--foreground);
37
+ }
38
+ .md-body :global(pre) {
39
+ background: var(--background);
40
+ padding: 1rem;
41
+ border-radius: 0.375rem;
42
+ overflow-x: auto;
43
+ border: 1px solid var(--border);
44
+ }
45
+ .md-body :global(pre code) {
46
+ background: none;
47
+ padding: 0;
48
+ color: var(--muted-foreground);
49
+ }
50
+ .md-body :global(a) {
51
+ color: var(--primary);
52
+ }
53
+ .md-body :global(p) {
54
+ color: var(--muted-foreground);
55
+ line-height: 1.6;
56
+ }
57
+ .md-body :global(ul),
58
+ .md-body :global(ol) {
59
+ color: var(--muted-foreground);
60
+ }
61
+ .md-body :global(blockquote) {
62
+ border-left: 3px solid var(--border);
63
+ margin-left: 0;
64
+ padding-left: 1rem;
65
+ color: var(--muted-foreground);
66
+ }
67
+ .md-body :global(hr) {
68
+ border: none;
69
+ border-top: 1px solid var(--border);
70
+ }
71
+ .md-body :global(table) {
72
+ border-collapse: collapse;
73
+ width: 100%;
74
+ }
75
+ .md-body :global(th),
76
+ .md-body :global(td) {
77
+ border: 1px solid var(--border);
78
+ padding: 0.5rem 0.75rem;
79
+ color: var(--muted-foreground);
80
+ }
81
+ .md-body :global(th) {
82
+ background: var(--card);
83
+ color: var(--foreground);
84
+ }
85
+ </style>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { cn, type WithElementRef } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ spent = 0,
8
+ limit = 0,
9
+ size = 16,
10
+ strokeWidth = 2,
11
+ class: className,
12
+ ...restProps
13
+ }: WithElementRef<HTMLAttributes<HTMLSpanElement>> & {
14
+ spent?: number;
15
+ limit?: number;
16
+ size?: number;
17
+ strokeWidth?: number;
18
+ } = $props();
19
+
20
+ const radius = $derived((size - strokeWidth) / 2);
21
+ const circumference = $derived(2 * Math.PI * radius);
22
+ const pct = $derived(limit > 0 ? Math.min(1, spent / limit) : 0);
23
+ const dashOffset = $derived(circumference * (1 - pct));
24
+ const color = $derived(
25
+ pct >= 0.8 ? 'stroke-destructive' : pct >= 0.5 ? 'stroke-warning' : 'stroke-success',
26
+ );
27
+ const bgColor = $derived(
28
+ pct >= 0.8 ? 'stroke-destructive/20' : pct >= 0.5 ? 'stroke-warning/20' : 'stroke-success/20',
29
+ );
30
+ </script>
31
+
32
+ {#if limit > 0}
33
+ <span
34
+ bind:this={ref}
35
+ title="${spent.toFixed(2)} / ${limit.toFixed(2)} ({Math.round(pct * 100)}%)"
36
+ class={cn(className)}
37
+ {...restProps}
38
+ >
39
+ <svg width={size} height={size} class="shrink-0 -rotate-90">
40
+ <circle
41
+ cx={size / 2}
42
+ cy={size / 2}
43
+ r={radius}
44
+ fill="none"
45
+ stroke-width={strokeWidth}
46
+ class={bgColor}
47
+ />
48
+ <circle
49
+ cx={size / 2}
50
+ cy={size / 2}
51
+ r={radius}
52
+ fill="none"
53
+ stroke-width={strokeWidth}
54
+ stroke-linecap="round"
55
+ stroke-dasharray={circumference}
56
+ stroke-dashoffset={dashOffset}
57
+ class="{color} transition-[stroke-dashoffset] duration-500 ease-out"
58
+ />
59
+ </svg>
60
+ </span>
61
+ {/if}
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils.js';
3
+
4
+ let {
5
+ provider,
6
+ class: className,
7
+ ...restProps
8
+ }: {
9
+ provider: string;
10
+ class?: string;
11
+ } = $props();
12
+
13
+ const cls = $derived(cn('h-4 w-4', className));
14
+ </script>
15
+
16
+ {#if provider === 'github' || provider === 'github_issues'}
17
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="GitHub" {...restProps}>
18
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
19
+ </svg>
20
+ {:else if provider === 'gitlab'}
21
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="GitLab" {...restProps}>
22
+ <path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51 1.22 3.78a.84.84 0 0 1-.3.92z"/>
23
+ </svg>
24
+ {:else if provider === 'linear'}
25
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="Linear" {...restProps}>
26
+ <path d="M3.464 1.51 13.104 11.15A7.992 7.992 0 0 0 4.455 3.046l-.99-.99A.5.5 0 0 0 3.464 1.51zM2.1 3.697a.5.5 0 0 0 0 .707L13.596 15.9a8.027 8.027 0 0 0 1.017-1.696L3.515 3.105a.5.5 0 0 0-.707 0l-.707.592zM1.509 6.232a.5.5 0 0 0-.15.354v9.003a.5.5 0 0 0 .854.354L13.82 4.346A8.036 8.036 0 0 0 12 4a7.99 7.99 0 0 0-5.657 2.343L1.51 6.23a.5.5 0 0 0 0 0zM4 17.172l5.172 5.172A8 8 0 0 1 4 17.172zM1.359 10.24a.5.5 0 0 0 0 .707l11.697 11.697A8.03 8.03 0 0 0 15.654 21.4L2.066 10.24a.5.5 0 0 0-.707 0z"/>
27
+ </svg>
28
+ {:else if provider === 'jira'}
29
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="Jira" {...restProps}>
30
+ <path d="M11.975 0C5.962 0 .05 5.36.05 11.992c0 6.278 4.818 11.42 10.966 11.96v-3.8c-3.968-.512-7.006-3.91-7.006-7.983 0-4.44 3.602-8.04 8.04-8.04 4.438 0 8.04 3.6 8.04 8.04 0 3.96-2.862 7.276-6.65 7.93v-5.28h3.24l.93-3.84H13.44V9.73c0-1.12.55-2.2 2.3-2.2h1.78V4.21s-1.614-.276-3.158-.276c-3.22 0-5.32 1.952-5.32 5.49v2.57H5.63v3.84h3.41v5.424C4.01 20.3 0 16.46 0 11.993 0 5.36 5.36 0 12 0z"/>
31
+ <path d="M11.975 5.195C8.9 5.195 6.4 7.696 6.4 10.77h5.575v5.576c3.076 0 5.576-2.5 5.576-5.576s-2.5-5.575-5.576-5.575z"/>
32
+ </svg>
33
+ {:else if provider === 'bitbucket'}
34
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="Bitbucket" {...restProps}>
35
+ <path d="M.778 1.213a.768.768 0 0 0-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 0 0 .77-.646l3.27-20.03a.768.768 0 0 0-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z"/>
36
+ </svg>
37
+ {:else if provider === 'plane'}
38
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="Plane" {...restProps}>
39
+ <path d="M22.2 2.4 1.8 9.6l8.4 2.4 2.4 8.4 2.4-7.2 7.2-10.8z"/>
40
+ </svg>
41
+ {:else if provider === 'digitalocean' || provider === 'do'}
42
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="DigitalOcean" {...restProps}>
43
+ <path d="M12.04 0C5.408 0 .05 5.36.05 11.992c0 6.278 4.818 11.42 10.966 11.96v-3.8c-3.968-.512-7.006-3.91-7.006-7.983 0-4.44 3.602-8.04 8.04-8.04 4.438 0 8.04 3.6 8.04 8.04 0 3.96-2.862 7.276-6.65 7.93v-5.28h3.24l.93-3.84H13.44V9.73c0-1.12.55-2.2 2.3-2.2h1.78V4.21s-1.614-.276-3.158-.276c-3.22 0-5.32 1.952-5.32 5.49v2.57H5.63v3.84h3.41v5.424C4.01 20.3 0 16.46 0 11.993 0 5.36 5.36 0 12 0z"/>
44
+ </svg>
45
+ {:else if provider === 'azure' || provider === 'azuredevops'}
46
+ <svg class={cls} viewBox="0 0 24 24" fill="currentColor" aria-label="Azure DevOps" {...restProps}>
47
+ <path d="M0 5.26l2.65-.37 8.54 9.24v-8.5l-2.47-.34L0 5.26zm23.99 3.44L14.6 0l-5.4 4.82 8.12 2.65v9.75l6.67-8.52zM8.85 14.01L6.47 24l4.8-1.7 5.66-8.25-8.08-.04z"/>
48
+ </svg>
49
+ {:else}
50
+ <!-- Generic git/provider icon -->
51
+ <svg class={cls} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-label="Provider" {...restProps}>
52
+ <circle cx="18" cy="18" r="3"/>
53
+ <circle cx="6" cy="6" r="3"/>
54
+ <circle cx="6" cy="18" r="3"/>
55
+ <path d="M6 9v6M18 15v1a3 3 0 0 1-3 3H9"/>
56
+ </svg>
57
+ {/if}