@inferencesh/ui 0.0.0-stage → 0.1.1
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.
- package/LICENSE +22 -0
- package/README.md +68 -2
- package/components.json +22 -0
- package/package.json +231 -4
- package/src/components/audio-player.tsx +549 -0
- package/src/components/button.tsx +13 -0
- package/src/components/chart.tsx +403 -0
- package/src/components/circuit.tsx +276 -0
- package/src/components/code-block/code-block.tsx +260 -0
- package/src/components/code-block/index.ts +18 -0
- package/src/components/code-block/languages.ts +311 -0
- package/src/components/code-block/styles.ts +39 -0
- package/src/components/code-block/tokenizer.ts +290 -0
- package/src/components/code-block/types.ts +38 -0
- package/src/components/code-block/utils.ts +39 -0
- package/src/components/code.tsx +48 -0
- package/src/components/cookie-consent.tsx +137 -0
- package/src/components/copy-button.tsx +44 -0
- package/src/components/country-dropdown.tsx +188 -0
- package/src/components/glow-loader-fast.tsx +32 -0
- package/src/components/glow-loader-mono.tsx +33 -0
- package/src/components/glow-loader-sm-fast.tsx +32 -0
- package/src/components/glow-loader-sm-mono.tsx +34 -0
- package/src/components/glow-loader-sm.tsx +32 -0
- package/src/components/glow-loader.tsx +32 -0
- package/src/components/glow-logo.tsx +32 -0
- package/src/components/gradient.tsx +80 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-sidebar.tsx +351 -0
- package/src/components/input-color.tsx +349 -0
- package/src/components/logo-base.tsx +213 -0
- package/src/components/logo-color.tsx +23 -0
- package/src/components/logo-full.tsx +71 -0
- package/src/components/logo-svg.tsx +23 -0
- package/src/components/menu-navigation.tsx +359 -0
- package/src/components/multi-select.tsx +721 -0
- package/src/components/pastel-gradient.tsx +25 -0
- package/src/components/pixel-symmetry.tsx +151 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/scroll-more.tsx +131 -0
- package/src/components/select.tsx +85 -0
- package/src/components/shimmering-text.tsx +108 -0
- package/src/components/sidebar-light.tsx +102 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/theme-switcher-sidebar.tsx +56 -0
- package/src/components/theme-switcher-simple.tsx +110 -0
- package/src/components/theme-switcher.tsx +47 -0
- package/src/components/toggle-group.tsx +55 -0
- package/src/components/tooltip-button.tsx +91 -0
- package/src/components/tooltip.tsx +71 -0
- package/src/components/tree-view.tsx +499 -0
- package/src/components/unglow-loader.tsx +33 -0
- package/src/components/waveform.tsx +1632 -0
- package/src/components/zoomable-image.tsx +76 -0
- package/src/docs/api-docs.tsx +763 -0
- package/src/docs/callout.tsx +160 -0
- package/src/docs/code-group.tsx +215 -0
- package/src/docs/copy-menu.tsx +143 -0
- package/src/docs/doc-tabs.tsx +84 -0
- package/src/docs/index.ts +16 -0
- package/src/docs/steps.tsx +105 -0
- package/src/docs/table-of-contents.tsx +174 -0
- package/src/examples/guide-markdown.ts +129 -0
- package/src/examples/index.ts +1153 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/use-copy-to-clipboard.ts +36 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/display-name.ts +15 -0
- package/src/lib/example-input.ts +130 -0
- package/src/lib/plans.ts +44 -0
- package/src/lib/safe-content.ts +137 -0
- package/src/lib/schema.ts +320 -0
- package/src/lib/storage.ts +108 -0
- package/src/markdown/docs-markdown-renderer.tsx +518 -0
- package/src/markdown/helpers.ts +95 -0
- package/src/markdown/index.ts +24 -0
- package/src/markdown/markdown-renderer.tsx +232 -0
- package/src/markdown/youtube-embed.tsx +43 -0
- package/src/og/image.ts +312 -0
- package/src/og/index.tsx +430 -0
- package/src/og/query.ts +26 -0
- package/src/shadcn/accordion.tsx +71 -0
- package/src/shadcn/alert-dialog.tsx +187 -0
- package/src/shadcn/alert.tsx +75 -0
- package/src/shadcn/aspect-ratio.tsx +22 -0
- package/src/shadcn/attachment.tsx +207 -0
- package/src/shadcn/avatar.tsx +106 -0
- package/src/shadcn/badge.tsx +51 -0
- package/src/shadcn/breadcrumb.tsx +124 -0
- package/src/shadcn/bubble.tsx +127 -0
- package/src/shadcn/button-group.tsx +87 -0
- package/src/shadcn/button.tsx +57 -0
- package/src/shadcn/calendar.tsx +219 -0
- package/src/shadcn/card.tsx +102 -0
- package/src/shadcn/carousel.tsx +242 -0
- package/src/shadcn/chart.tsx +372 -0
- package/src/shadcn/checkbox.tsx +28 -0
- package/src/shadcn/collapsible.tsx +19 -0
- package/src/shadcn/combobox.tsx +297 -0
- package/src/shadcn/command.tsx +194 -0
- package/src/shadcn/context-menu.tsx +270 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/direction.tsx +4 -0
- package/src/shadcn/drawer.tsx +227 -0
- package/src/shadcn/dropdown-menu.tsx +265 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +238 -0
- package/src/shadcn/form.tsx +173 -0
- package/src/shadcn/hover-card.tsx +50 -0
- package/src/shadcn/index.ts +53 -0
- package/src/shadcn/input-group.tsx +156 -0
- package/src/shadcn/input-otp.tsx +84 -0
- package/src/shadcn/input.tsx +19 -0
- package/src/shadcn/item.tsx +201 -0
- package/src/shadcn/kbd.tsx +26 -0
- package/src/shadcn/label.tsx +19 -0
- package/src/shadcn/marker.tsx +70 -0
- package/src/shadcn/menubar.tsx +278 -0
- package/src/shadcn/message-scroller.tsx +131 -0
- package/src/shadcn/message.tsx +91 -0
- package/src/shadcn/native-select.tsx +60 -0
- package/src/shadcn/navigation-menu.tsx +167 -0
- package/src/shadcn/pagination.tsx +130 -0
- package/src/shadcn/popover.tsx +87 -0
- package/src/shadcn/progress.tsx +82 -0
- package/src/shadcn/radio-group.tsx +35 -0
- package/src/shadcn/resizable.tsx +49 -0
- package/src/shadcn/scroll-area.tsx +52 -0
- package/src/shadcn/select.tsx +200 -0
- package/src/shadcn/separator.tsx +22 -0
- package/src/shadcn/sheet.tsx +136 -0
- package/src/shadcn/sidebar.tsx +721 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/slider.tsx +51 -0
- package/src/shadcn/sonner.tsx +49 -0
- package/src/shadcn/spinner.tsx +10 -0
- package/src/shadcn/switch.tsx +29 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +79 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +89 -0
- package/src/shadcn/toggle.tsx +44 -0
- package/src/shadcn/tooltip.tsx +63 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/css/base.css +92 -0
- package/src/theme/css/bitboy.css +75 -0
- package/src/theme/css/dark.css +47 -0
- package/src/theme/css/designer.css +122 -0
- package/src/theme/css/fonts.css +33 -0
- package/src/theme/css/globals.css +14 -0
- package/src/theme/css/hackintosh.css +57 -0
- package/src/theme/css/llama.css +103 -0
- package/src/theme/css/safelist.css +39 -0
- package/src/theme/css/shadcn.css +77 -0
- package/src/theme/css/theme.css +216 -0
- package/src/theme/embed-theme.ts +146 -0
- package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
- package/src/theme/fonts/Hack-Regular.woff2 +0 -0
- package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
- package/src/theme/fonts/LICENSE-Hack.txt +45 -0
- package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
- package/src/theme/fonts/Mona-Sans.woff2 +0 -0
- package/src/theme/index.ts +15 -0
- package/src/utils/cn.ts +3 -0
- 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
|
+
}
|