@inferencesh/ui 0.0.0-stage → 0.1.2

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 (165) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +68 -2
  3. package/components.json +22 -0
  4. package/package.json +231 -4
  5. package/src/components/audio-player.tsx +549 -0
  6. package/src/components/button.tsx +13 -0
  7. package/src/components/chart.tsx +403 -0
  8. package/src/components/circuit.tsx +276 -0
  9. package/src/components/code-block/code-block.tsx +260 -0
  10. package/src/components/code-block/index.ts +18 -0
  11. package/src/components/code-block/languages.ts +311 -0
  12. package/src/components/code-block/styles.ts +39 -0
  13. package/src/components/code-block/tokenizer.ts +290 -0
  14. package/src/components/code-block/types.ts +38 -0
  15. package/src/components/code-block/utils.ts +39 -0
  16. package/src/components/code.tsx +48 -0
  17. package/src/components/cookie-consent.tsx +137 -0
  18. package/src/components/copy-button.tsx +44 -0
  19. package/src/components/country-dropdown.tsx +188 -0
  20. package/src/components/glow-loader-fast.tsx +32 -0
  21. package/src/components/glow-loader-mono.tsx +33 -0
  22. package/src/components/glow-loader-sm-fast.tsx +32 -0
  23. package/src/components/glow-loader-sm-mono.tsx +34 -0
  24. package/src/components/glow-loader-sm.tsx +32 -0
  25. package/src/components/glow-loader.tsx +32 -0
  26. package/src/components/glow-logo.tsx +32 -0
  27. package/src/components/gradient.tsx +80 -0
  28. package/src/components/index.ts +45 -0
  29. package/src/components/inline-sidebar.tsx +351 -0
  30. package/src/components/input-color.tsx +349 -0
  31. package/src/components/logo-base.tsx +213 -0
  32. package/src/components/logo-color.tsx +23 -0
  33. package/src/components/logo-full.tsx +71 -0
  34. package/src/components/logo-svg.tsx +23 -0
  35. package/src/components/menu-navigation.tsx +359 -0
  36. package/src/components/multi-select.tsx +721 -0
  37. package/src/components/pastel-gradient.tsx +25 -0
  38. package/src/components/pixel-symmetry.tsx +151 -0
  39. package/src/components/progress.tsx +28 -0
  40. package/src/components/scroll-more.tsx +131 -0
  41. package/src/components/select.tsx +85 -0
  42. package/src/components/shimmering-text.tsx +108 -0
  43. package/src/components/sidebar-light.tsx +102 -0
  44. package/src/components/sonner.tsx +38 -0
  45. package/src/components/theme-switcher-sidebar.tsx +56 -0
  46. package/src/components/theme-switcher-simple.tsx +110 -0
  47. package/src/components/theme-switcher.tsx +47 -0
  48. package/src/components/toggle-group.tsx +55 -0
  49. package/src/components/tooltip-button.tsx +91 -0
  50. package/src/components/tooltip.tsx +71 -0
  51. package/src/components/tree-view.tsx +499 -0
  52. package/src/components/unglow-loader.tsx +33 -0
  53. package/src/components/waveform.tsx +1632 -0
  54. package/src/components/zoomable-image.tsx +76 -0
  55. package/src/docs/api-docs.tsx +763 -0
  56. package/src/docs/callout.tsx +160 -0
  57. package/src/docs/code-group.tsx +215 -0
  58. package/src/docs/copy-menu.tsx +143 -0
  59. package/src/docs/doc-tabs.tsx +84 -0
  60. package/src/docs/index.ts +16 -0
  61. package/src/docs/steps.tsx +105 -0
  62. package/src/docs/table-of-contents.tsx +174 -0
  63. package/src/examples/guide-markdown.ts +129 -0
  64. package/src/examples/index.ts +1153 -0
  65. package/src/global.d.ts +1 -0
  66. package/src/hooks/use-copy-to-clipboard.ts +36 -0
  67. package/src/hooks/use-mobile.ts +19 -0
  68. package/src/lib/display-name.ts +15 -0
  69. package/src/lib/example-input.ts +130 -0
  70. package/src/lib/plans.ts +44 -0
  71. package/src/lib/safe-content.ts +137 -0
  72. package/src/lib/schema.ts +320 -0
  73. package/src/lib/storage.ts +108 -0
  74. package/src/markdown/docs-markdown-renderer.tsx +518 -0
  75. package/src/markdown/helpers.ts +95 -0
  76. package/src/markdown/index.ts +24 -0
  77. package/src/markdown/markdown-renderer.tsx +232 -0
  78. package/src/markdown/youtube-embed.tsx +43 -0
  79. package/src/og/image.ts +312 -0
  80. package/src/og/index.tsx +430 -0
  81. package/src/og/query.ts +26 -0
  82. package/src/shadcn/accordion.tsx +71 -0
  83. package/src/shadcn/alert-dialog.tsx +187 -0
  84. package/src/shadcn/alert.tsx +75 -0
  85. package/src/shadcn/aspect-ratio.tsx +22 -0
  86. package/src/shadcn/attachment.tsx +207 -0
  87. package/src/shadcn/avatar.tsx +106 -0
  88. package/src/shadcn/badge.tsx +51 -0
  89. package/src/shadcn/breadcrumb.tsx +124 -0
  90. package/src/shadcn/bubble.tsx +127 -0
  91. package/src/shadcn/button-group.tsx +87 -0
  92. package/src/shadcn/button.tsx +57 -0
  93. package/src/shadcn/calendar.tsx +219 -0
  94. package/src/shadcn/card.tsx +102 -0
  95. package/src/shadcn/carousel.tsx +242 -0
  96. package/src/shadcn/chart.tsx +372 -0
  97. package/src/shadcn/checkbox.tsx +28 -0
  98. package/src/shadcn/collapsible.tsx +19 -0
  99. package/src/shadcn/combobox.tsx +297 -0
  100. package/src/shadcn/command.tsx +194 -0
  101. package/src/shadcn/context-menu.tsx +270 -0
  102. package/src/shadcn/dialog.tsx +158 -0
  103. package/src/shadcn/direction.tsx +4 -0
  104. package/src/shadcn/drawer.tsx +227 -0
  105. package/src/shadcn/dropdown-menu.tsx +265 -0
  106. package/src/shadcn/empty.tsx +103 -0
  107. package/src/shadcn/field.tsx +238 -0
  108. package/src/shadcn/form.tsx +173 -0
  109. package/src/shadcn/hover-card.tsx +50 -0
  110. package/src/shadcn/index.ts +53 -0
  111. package/src/shadcn/input-group.tsx +156 -0
  112. package/src/shadcn/input-otp.tsx +84 -0
  113. package/src/shadcn/input.tsx +19 -0
  114. package/src/shadcn/item.tsx +201 -0
  115. package/src/shadcn/kbd.tsx +26 -0
  116. package/src/shadcn/label.tsx +19 -0
  117. package/src/shadcn/marker.tsx +70 -0
  118. package/src/shadcn/menubar.tsx +278 -0
  119. package/src/shadcn/message-scroller.tsx +131 -0
  120. package/src/shadcn/message.tsx +91 -0
  121. package/src/shadcn/native-select.tsx +60 -0
  122. package/src/shadcn/navigation-menu.tsx +167 -0
  123. package/src/shadcn/pagination.tsx +130 -0
  124. package/src/shadcn/popover.tsx +87 -0
  125. package/src/shadcn/progress.tsx +82 -0
  126. package/src/shadcn/radio-group.tsx +35 -0
  127. package/src/shadcn/resizable.tsx +49 -0
  128. package/src/shadcn/scroll-area.tsx +52 -0
  129. package/src/shadcn/select.tsx +200 -0
  130. package/src/shadcn/separator.tsx +22 -0
  131. package/src/shadcn/sheet.tsx +136 -0
  132. package/src/shadcn/sidebar.tsx +721 -0
  133. package/src/shadcn/skeleton.tsx +13 -0
  134. package/src/shadcn/slider.tsx +51 -0
  135. package/src/shadcn/sonner.tsx +49 -0
  136. package/src/shadcn/spinner.tsx +10 -0
  137. package/src/shadcn/switch.tsx +29 -0
  138. package/src/shadcn/table.tsx +115 -0
  139. package/src/shadcn/tabs.tsx +79 -0
  140. package/src/shadcn/textarea.tsx +17 -0
  141. package/src/shadcn/toggle-group.tsx +89 -0
  142. package/src/shadcn/toggle.tsx +44 -0
  143. package/src/shadcn/tooltip.tsx +63 -0
  144. package/src/theme/config.ts +48 -0
  145. package/src/theme/css/base.css +92 -0
  146. package/src/theme/css/bitboy.css +75 -0
  147. package/src/theme/css/dark.css +47 -0
  148. package/src/theme/css/designer.css +122 -0
  149. package/src/theme/css/fonts.css +33 -0
  150. package/src/theme/css/globals.css +14 -0
  151. package/src/theme/css/hackintosh.css +57 -0
  152. package/src/theme/css/llama.css +103 -0
  153. package/src/theme/css/safelist.css +39 -0
  154. package/src/theme/css/shadcn.css +77 -0
  155. package/src/theme/css/theme.css +216 -0
  156. package/src/theme/embed-theme.ts +146 -0
  157. package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
  158. package/src/theme/fonts/Hack-Regular.woff2 +0 -0
  159. package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
  160. package/src/theme/fonts/LICENSE-Hack.txt +45 -0
  161. package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
  162. package/src/theme/fonts/Mona-Sans.woff2 +0 -0
  163. package/src/theme/index.ts +15 -0
  164. package/src/utils/cn.ts +3 -0
  165. package/src/utils/index.ts +2 -0
@@ -0,0 +1,351 @@
1
+ "use client"
2
+
3
+ import React, { useState } from 'react'
4
+ import { cn } from "cn"
5
+ import { ChevronDown, ChevronRight, Menu } from 'lucide-react'
6
+
7
+ export interface InlineSidebarItem<T extends string = string> {
8
+ id: T
9
+ label: string
10
+ icon?: React.ComponentType<{ className?: string }>
11
+ children?: InlineSidebarItem<T>[]
12
+ onClick?: () => void
13
+ }
14
+
15
+ interface InlineSidebarProps<T extends string = string> {
16
+ items: InlineSidebarItem<T>[]
17
+ activeItem?: T
18
+ onItemChange?: (item: T) => void
19
+ className?: string
20
+ /** Show items as a tree with collapsible groups */
21
+ tree?: boolean
22
+ /** Show items with section headers (non-collapsible, always visible) */
23
+ sections?: boolean
24
+ /** Default expanded item IDs (for tree mode) */
25
+ defaultExpanded?: T[]
26
+ }
27
+
28
+ export function InlineSidebar<T extends string = string>({
29
+ items,
30
+ activeItem,
31
+ onItemChange,
32
+ className,
33
+ tree = false,
34
+ sections = false,
35
+ defaultExpanded = [],
36
+ }: InlineSidebarProps<T>) {
37
+ const [expanded, setExpanded] = useState<Set<T>>(new Set(defaultExpanded))
38
+
39
+ const toggleExpanded = (id: T) => {
40
+ setExpanded(prev => {
41
+ const next = new Set(prev)
42
+ if (next.has(id)) {
43
+ next.delete(id)
44
+ } else {
45
+ next.add(id)
46
+ }
47
+ return next
48
+ })
49
+ }
50
+
51
+ const renderItem = (item: InlineSidebarItem<T>, depth: number = 0) => {
52
+ const Icon = item.icon
53
+ const isActive = activeItem === item.id
54
+ const hasChildren = item.children && item.children.length > 0
55
+ const isExpanded = expanded.has(item.id)
56
+
57
+ const handleClick = () => {
58
+ if (hasChildren && tree) {
59
+ toggleExpanded(item.id)
60
+ }
61
+ if (item.onClick) {
62
+ item.onClick()
63
+ } else if (onItemChange && !hasChildren) {
64
+ onItemChange(item.id)
65
+ }
66
+ }
67
+
68
+ return (
69
+ <div key={item.id}>
70
+ <button
71
+ type="button"
72
+ onClick={handleClick}
73
+ className={cn(
74
+ "w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors whitespace-nowrap cursor-pointer",
75
+ isActive && !hasChildren
76
+ ? "bg-primary text-primary-foreground"
77
+ : "text-muted-foreground hover:text-foreground hover:bg-muted",
78
+ depth > 0,
79
+ )}
80
+ style={{ paddingLeft: `${12 + depth * 12}px` }}
81
+ >
82
+
83
+ {Icon && <Icon className="h-4 w-4 shrink-0" />}
84
+ <span className="truncate">{item.label}</span>
85
+ {hasChildren && tree && (
86
+ <ChevronRight className={cn(
87
+ "h-3.5 w-3.5 shrink-0 transition-transform",
88
+ isExpanded && "rotate-90"
89
+ )} />
90
+ )}
91
+ </button>
92
+
93
+ {hasChildren && tree && isExpanded && (
94
+ <div className="mt-0.5">
95
+ {item.children!.map(child => renderItem(child, depth + 1))}
96
+ </div>
97
+ )}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ // Sections mode - always visible section headers with children
103
+ if (sections) {
104
+ const renderSectionItem = (item: InlineSidebarItem<T>) => {
105
+ const Icon = item.icon
106
+ const isActive = activeItem === item.id
107
+ return (
108
+ <button
109
+ type="button"
110
+ key={item.id}
111
+ onClick={() => {
112
+ if (item.onClick) {
113
+ item.onClick()
114
+ } else if (onItemChange) {
115
+ onItemChange(item.id)
116
+ }
117
+ }}
118
+ className={cn(
119
+ "w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors whitespace-nowrap cursor-pointer",
120
+ isActive
121
+ ? "bg-primary text-primary-foreground"
122
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
123
+ )}
124
+ >
125
+ {Icon && <Icon className="h-4 w-4 shrink-0" />}
126
+ <span className="truncate">{item.label}</span>
127
+ </button>
128
+ )
129
+ }
130
+
131
+ return (
132
+ <nav className={cn("flex flex-col gap-4", className)}>
133
+ {items.map((item) => {
134
+ const hasChildren = item.children && item.children.length > 0
135
+ if (hasChildren) {
136
+ return (
137
+ <div key={item.id} className="flex flex-col gap-1">
138
+ <div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
139
+ {item.label}
140
+ </div>
141
+ <div className="flex flex-col gap-0.5">
142
+ {item.children!.map(child => renderSectionItem(child))}
143
+ </div>
144
+ </div>
145
+ )
146
+ }
147
+ return renderSectionItem(item)
148
+ })}
149
+ </nav>
150
+ )
151
+ }
152
+
153
+ // Flat mode (original behavior)
154
+ if (!tree) {
155
+ return (
156
+ <nav className={cn("flex flex-col gap-2 overflow-x-auto pb-2 lg:pb-0", className)}>
157
+ {items.map((item) => {
158
+ const Icon = item.icon
159
+ const isActive = activeItem === item.id
160
+ return (
161
+ <button
162
+ type="button"
163
+ key={item.id}
164
+ onClick={() => {
165
+ if (item.onClick) {
166
+ item.onClick()
167
+ } else if (onItemChange) {
168
+ onItemChange(item.id)
169
+ }
170
+ }}
171
+ className={cn(
172
+ "flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors whitespace-nowrap cursor-pointer",
173
+ isActive
174
+ ? "bg-primary text-primary-foreground"
175
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
176
+ )}
177
+ >
178
+ {Icon && <Icon className="h-4 w-4" />}
179
+ {item.label}
180
+ </button>
181
+ )
182
+ })}
183
+ </nav>
184
+ )
185
+ }
186
+
187
+ // Tree mode
188
+ return (
189
+ <nav className={cn("flex flex-col gap-0.5", className)}>
190
+ {items.map(item => renderItem(item, 0))}
191
+ </nav>
192
+ )
193
+ }
194
+
195
+ // ============================================================================
196
+ // Compound Components (shadcn Card style - each renders independently)
197
+ // ============================================================================
198
+
199
+ interface InlineSidebarLayoutProps {
200
+ children: React.ReactNode
201
+ className?: string
202
+ }
203
+
204
+ function InlineSidebarLayout({ children, className }: InlineSidebarLayoutProps) {
205
+ return (
206
+ <div
207
+ data-slot="inline-sidebar-layout"
208
+ className={cn("relative lg:grid lg:grid-cols-[auto_1fr_auto]", className)}
209
+ >
210
+ {children}
211
+ </div>
212
+ )
213
+ }
214
+
215
+ interface InlineSidebarLayoutLeftProps {
216
+ children?: React.ReactNode
217
+ className?: string
218
+ /** Width class for the sidebar (e.g. 'lg:w-52') */
219
+ width?: string
220
+ /** Show border separator */
221
+ bordered?: boolean
222
+ /**
223
+ * Class name for the sticky container.
224
+ */
225
+ stickyClassName?: string
226
+ }
227
+
228
+ function InlineSidebarLayoutLeft({
229
+ children,
230
+ className,
231
+ width = 'lg:w-52',
232
+ bordered,
233
+ stickyClassName
234
+ }: InlineSidebarLayoutLeftProps) {
235
+ const [mobileOpen, setMobileOpen] = useState(false)
236
+
237
+ return (
238
+ <aside
239
+ data-slot="inline-sidebar-left"
240
+ className={cn(
241
+ "shrink-0",
242
+ width,
243
+ )}
244
+ >
245
+ {/* Mobile: accordion toggle + collapsible content */}
246
+ <div className="lg:hidden border-b border-border">
247
+ <button
248
+ onClick={() => setMobileOpen(!mobileOpen)}
249
+ className="cursor-pointer w-full flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground hover:text-foreground transition-colors"
250
+ aria-expanded={mobileOpen}
251
+ >
252
+ <Menu className="h-4 w-4" />
253
+ <ChevronDown className={cn(
254
+ "h-3.5 w-3.5 ml-auto transition-transform duration-200",
255
+ mobileOpen && "rotate-180"
256
+ )} />
257
+ </button>
258
+ <div className={cn(
259
+ "overflow-hidden transition-all duration-200 ease-out",
260
+ mobileOpen ? "max-h-[70vh] overflow-y-auto" : "max-h-0",
261
+ )}>
262
+ <div className="px-4 pb-4">
263
+ {children}
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ {/* Desktop: sticky sidebar */}
269
+ <div className={cn(
270
+ "hidden lg:block lg:sticky lg:top-0 overflow-y-auto p-4",
271
+ bordered && "border-r border-border",
272
+ stickyClassName || "lg:h-screen",
273
+ className,
274
+ )}>
275
+ {children}
276
+ </div>
277
+ </aside>
278
+ )
279
+ }
280
+
281
+ interface InlineSidebarLayoutContentProps {
282
+ children: React.ReactNode
283
+ className?: string
284
+ }
285
+
286
+ function InlineSidebarLayoutContent({ children, className }: InlineSidebarLayoutContentProps) {
287
+ return (
288
+ <main
289
+ data-slot="inline-sidebar-content"
290
+ className={cn("min-w-0 px-0 lg:px-4 py-4 lg:px-8 select-text", className)}
291
+ >
292
+ {children}
293
+ </main>
294
+ )
295
+ }
296
+
297
+ interface InlineSidebarLayoutRightProps {
298
+ children?: React.ReactNode
299
+ className?: string
300
+ /** Width class for the sidebar (e.g. 'lg:w-52') */
301
+ width?: string
302
+ /** Show border separator */
303
+ bordered?: boolean
304
+ /**
305
+ * Class name for the sticky container.
306
+ */
307
+ stickyClassName?: string
308
+ }
309
+
310
+ function InlineSidebarLayoutRight({
311
+ children,
312
+ className,
313
+ width = 'lg:w-52',
314
+ bordered,
315
+ stickyClassName
316
+ }: InlineSidebarLayoutRightProps) {
317
+ if (!children) {
318
+ return null
319
+ }
320
+
321
+ return (
322
+ <aside
323
+ data-slot="inline-sidebar-right"
324
+ className={cn(
325
+ // No select-none here: this is a generic content slot, not chrome. It
326
+ // cannot know what it holds — a table of contents is fine to unselect,
327
+ // a chat panel is not, and the slot has no way to tell them apart.
328
+ // Children that want unselectable text should say so themselves.
329
+ "hidden lg:block shrink-0",
330
+ width,
331
+ className
332
+ )}
333
+ >
334
+ <div className={cn(
335
+ "lg:sticky lg:top-0 overflow-y-auto bg-background p-4",
336
+ bordered && "border-l border-border",
337
+ stickyClassName || "lg:h-screen"
338
+ )}>
339
+ {children}
340
+ </div>
341
+ </aside>
342
+ )
343
+ }
344
+
345
+ export {
346
+ InlineSidebarLayout,
347
+ InlineSidebarLayoutLeft,
348
+ InlineSidebarLayoutContent,
349
+ InlineSidebarLayoutRight,
350
+ }
351
+
@@ -0,0 +1,349 @@
1
+ "use client";
2
+
3
+ import { Button } from "../shadcn/button";
4
+ import { Input } from "../shadcn/input";
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverTrigger
9
+ } from "../shadcn/popover";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue
16
+ } from "../shadcn/select";
17
+ import { PipetteIcon } from "lucide-react";
18
+ import { useEffect, useState } from "react";
19
+ import { HexColorPicker } from "react-colorful";
20
+
21
+ interface ColorValues {
22
+ hex: string;
23
+ rgb: { r: number; g: number; b: number };
24
+ hsl: { h: number; s: number; l: number };
25
+ }
26
+
27
+ interface InputColorProps {
28
+ onColorChange?: (color: string) => void;
29
+ }
30
+
31
+ export default function InputColor({ onColorChange }: InputColorProps) {
32
+ const [colorFormat, setColorFormat] = useState("HEX");
33
+ const [color, setColor] = useState("#FF0000");
34
+ const [colorValues, setColorValues] = useState<ColorValues>({
35
+ hex: "#FF0000",
36
+ rgb: { r: 255, g: 0, b: 0 },
37
+ hsl: { h: 0, s: 100, l: 50 },
38
+ });
39
+
40
+ // Conversion utilities
41
+ const hexToRgb = (hex: string) => {
42
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
43
+ return result
44
+ ? {
45
+ r: Number.parseInt(result[1], 16),
46
+ g: Number.parseInt(result[2], 16),
47
+ b: Number.parseInt(result[3], 16),
48
+ }
49
+ : { r: 0, g: 0, b: 0 };
50
+ };
51
+
52
+ const rgbToHex = (r: number, g: number, b: number) => {
53
+ return (
54
+ "#" +
55
+ [r, g, b]
56
+ .map((x) => {
57
+ const hex = x.toString(16);
58
+ return hex.length === 1 ? "0" + hex : hex;
59
+ })
60
+ .join("")
61
+ .toUpperCase()
62
+ );
63
+ };
64
+
65
+ const rgbToHsl = (r: number, g: number, b: number) => {
66
+ r /= 255;
67
+ g /= 255;
68
+ b /= 255;
69
+ const max = Math.max(r, g, b);
70
+ const min = Math.min(r, g, b);
71
+ let h = 0,
72
+ s = 0;
73
+ const l = (max + min) / 2;
74
+
75
+ if (max === min) {
76
+ h = s = 0;
77
+ } else {
78
+ const d = max - min;
79
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
80
+ switch (max) {
81
+ case r:
82
+ h = (g - b) / d + (g < b ? 6 : 0);
83
+ break;
84
+ case g:
85
+ h = (b - r) / d + 2;
86
+ break;
87
+ case b:
88
+ h = (r - g) / d + 4;
89
+ break;
90
+ }
91
+ h /= 6;
92
+ }
93
+
94
+ return {
95
+ h: Math.round(h * 360),
96
+ s: Math.round(s * 100),
97
+ l: Math.round(l * 100),
98
+ };
99
+ };
100
+
101
+ const hslToRgb = (h: number, s: number, l: number) => {
102
+ h /= 360;
103
+ s /= 100;
104
+ l /= 100;
105
+ const hue2rgb = (p: number, q: number, t: number) => {
106
+ if (t < 0) t += 1;
107
+ if (t > 1) t -= 1;
108
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
109
+ if (t < 1 / 2) return q;
110
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
111
+ return p;
112
+ };
113
+
114
+ let r, g, b;
115
+ if (s === 0) {
116
+ r = g = b = l;
117
+ } else {
118
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
119
+ const p = 2 * l - q;
120
+ r = hue2rgb(p, q, h + 1 / 3);
121
+ g = hue2rgb(p, q, h);
122
+ b = hue2rgb(p, q, h - 1 / 3);
123
+ }
124
+
125
+ return {
126
+ r: Math.round(r * 255),
127
+ g: Math.round(g * 255),
128
+ b: Math.round(b * 255),
129
+ };
130
+ };
131
+
132
+ // Update all color formats when color changes
133
+ const updateColorValues = (newColor: string) => {
134
+ const rgb = hexToRgb(newColor);
135
+ const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
136
+
137
+ setColorValues({
138
+ hex: newColor.toUpperCase(),
139
+ rgb,
140
+ hsl,
141
+ });
142
+ };
143
+
144
+ // Handle color picker change
145
+ const handleColorChange = (newColor: string) => {
146
+ setColor(newColor);
147
+ updateColorValues(newColor);
148
+ onColorChange?.(newColor);
149
+ };
150
+
151
+ // Handle HEX input change
152
+ const handleHexChange = (value: string) => {
153
+ // Ensure it starts with # and convert to uppercase
154
+ let formattedValue = value.toUpperCase();
155
+ if (!formattedValue.startsWith("#")) {
156
+ formattedValue = "#" + formattedValue;
157
+ }
158
+
159
+ // Limit to 7 characters (#XXXXXX)
160
+ if (formattedValue.length > 7) {
161
+ formattedValue = formattedValue.slice(0, 7);
162
+ }
163
+
164
+ // Validate hex format
165
+ const hexRegex = /^#[0-9A-F]{6}$/;
166
+ if (hexRegex.test(formattedValue)) {
167
+ setColor(formattedValue);
168
+ updateColorValues(formattedValue);
169
+ onColorChange?.(formattedValue);
170
+ } else if (formattedValue.length <= 7) {
171
+ // Update display but don't convert until valid
172
+ setColorValues((prev) => ({ ...prev, hex: formattedValue }));
173
+ }
174
+ };
175
+
176
+ // Handle RGB input change
177
+ const handleRgbChange = (component: "r" | "g" | "b", value: string) => {
178
+ const numValue = Number.parseInt(value) || 0;
179
+ const clampedValue = Math.max(0, Math.min(255, numValue));
180
+
181
+ const newRgb = { ...colorValues.rgb, [component]: clampedValue };
182
+ const hex = rgbToHex(newRgb.r, newRgb.g, newRgb.b);
183
+ const hsl = rgbToHsl(newRgb.r, newRgb.g, newRgb.b);
184
+
185
+ setColor(hex);
186
+ setColorValues({ hex, rgb: newRgb, hsl });
187
+ onColorChange?.(hex);
188
+ };
189
+
190
+ // Handle HSL input change
191
+ const handleHslChange = (component: "h" | "s" | "l", value: string) => {
192
+ const numValue = Number.parseInt(value) || 0;
193
+ let clampedValue;
194
+
195
+ if (component === "h") {
196
+ clampedValue = Math.max(0, Math.min(360, numValue));
197
+ } else {
198
+ clampedValue = Math.max(0, Math.min(100, numValue));
199
+ }
200
+
201
+ const newHsl = { ...colorValues.hsl, [component]: clampedValue };
202
+ const rgb = hslToRgb(newHsl.h, newHsl.s, newHsl.l);
203
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
204
+
205
+ setColor(hex);
206
+ setColorValues({ hex, rgb, hsl: newHsl });
207
+ onColorChange?.(hex);
208
+ };
209
+
210
+ // Handle popover close
211
+ const handlePopoverChange = (open: boolean) => {
212
+ if (!open) {
213
+ setColorFormat("HEX");
214
+ }
215
+ };
216
+
217
+ // Check if EyeDropper API is available
218
+ const isEyeDropperAvailable = () => {
219
+ return typeof window !== "undefined" && "EyeDropper" in window;
220
+ };
221
+
222
+ // Handle eyedropper click
223
+ const handleEyeDropper = async () => {
224
+ if (!isEyeDropperAvailable()) {
225
+ alert("Eyedropper is not supported in your browser");
226
+ return;
227
+ }
228
+
229
+ try {
230
+ // @ts-expect-error - TypeScript doesn't have types for EyeDropper yet
231
+ const eyeDropper = new window.EyeDropper();
232
+ const result = await eyeDropper.open();
233
+ const pickedColor = result.sRGBHex;
234
+ setColor(pickedColor);
235
+ updateColorValues(pickedColor);
236
+ } catch (error) {
237
+ // console.error(error);
238
+ }
239
+ };
240
+
241
+ // Initialize color values on mount
242
+ useEffect(() => {
243
+ updateColorValues(color);
244
+ // eslint-disable-next-line react-hooks/exhaustive-deps
245
+ }, []);
246
+
247
+ return (
248
+ <Popover onOpenChange={handlePopoverChange}>
249
+ <PopoverTrigger render={<Button className="h-full w-full color-picker-button" size="icon" style={{ backgroundColor: color }} />} />
250
+
251
+ <PopoverContent className="w-auto p-3" align="start">
252
+ <div className="color-picker space-y-3">
253
+ <div className="relative">
254
+ <Button
255
+ variant="ghost"
256
+ size="icon"
257
+ className="absolute -top-1.5 -left-1 z-10 flex items-center gap-1 h-7 w-7 bg-transparent hover:bg-transparent"
258
+ onClick={handleEyeDropper}
259
+ disabled={!isEyeDropperAvailable()}
260
+ >
261
+ <PipetteIcon className="h-3 w-3" />
262
+ </Button>
263
+ <HexColorPicker
264
+ className="!w-[236px] !aspect-square !h-[236px]"
265
+ color={color}
266
+ onChange={handleColorChange}
267
+ />
268
+ </div>
269
+ <div className="flex gap-2">
270
+ <Select value={colorFormat} onValueChange={(v) => v && setColorFormat(v)}>
271
+ <SelectTrigger className="!w-[4.5rem] !text-sm !h-7 px-2 py-1">
272
+ <SelectValue placeholder="Color" />
273
+ </SelectTrigger>
274
+ <SelectContent className="min-w-20">
275
+ <SelectItem value="HEX" className="text-sm">
276
+ hex
277
+ </SelectItem>
278
+ <SelectItem value="RGB" className="text-sm">
279
+ rgb
280
+ </SelectItem>
281
+ <SelectItem value="HSL" className="text-sm">
282
+ hsl
283
+ </SelectItem>
284
+ </SelectContent>
285
+ </Select>
286
+
287
+ {colorFormat === "HEX" ? (
288
+ <Input
289
+ className="h-7 text-sm w-[156px]"
290
+ value={colorValues.hex}
291
+ onChange={(e) => handleHexChange(e.target.value)}
292
+ placeholder="#FF0000"
293
+ maxLength={7}
294
+ />
295
+ ) : colorFormat === "RGB" ? (
296
+ <div className="flex items-center">
297
+ <Input
298
+ className="rounded-r-none h-7 w-[52px] text-center text-sm"
299
+ value={colorValues.rgb.r}
300
+ onChange={(e) => handleRgbChange("r", e.target.value)}
301
+ placeholder="255"
302
+ maxLength={3}
303
+ />
304
+ <Input
305
+ className="rounded-none h-7 w-[52px] text-center border-x-0 text-sm"
306
+ value={colorValues.rgb.g}
307
+ onChange={(e) => handleRgbChange("g", e.target.value)}
308
+ placeholder="255"
309
+ maxLength={3}
310
+ />
311
+ <Input
312
+ className="rounded-l-none h-7 w-[52px] text-center text-sm"
313
+ value={colorValues.rgb.b}
314
+ onChange={(e) => handleRgbChange("b", e.target.value)}
315
+ placeholder="255"
316
+ maxLength={3}
317
+ />
318
+ </div>
319
+ ) : (
320
+ <div className="flex items-center">
321
+ <Input
322
+ className="rounded-r-none h-7 w-13 text-center text-sm"
323
+ value={colorValues.hsl.h}
324
+ onChange={(e) => handleHslChange("h", e.target.value)}
325
+ placeholder="360"
326
+ maxLength={3}
327
+ />
328
+ <Input
329
+ className="rounded-none h-7 w-13 text-center border-x-0 text-sm"
330
+ value={colorValues.hsl.s}
331
+ onChange={(e) => handleHslChange("s", e.target.value)}
332
+ placeholder="100"
333
+ maxLength={3}
334
+ />
335
+ <Input
336
+ className="rounded-l-none h-7 w-13 text-center text-sm"
337
+ value={colorValues.hsl.l}
338
+ onChange={(e) => handleHslChange("l", e.target.value)}
339
+ placeholder="100"
340
+ maxLength={3}
341
+ />
342
+ </div>
343
+ )}
344
+ </div>
345
+ </div>
346
+ </PopoverContent>
347
+ </Popover>
348
+ );
349
+ }