parthenon-ui 1.0.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 (36) hide show
  1. package/package.json +74 -0
  2. package/src/components/.gitkeep +0 -0
  3. package/src/components/avatar.tsx +109 -0
  4. package/src/components/badge.tsx +52 -0
  5. package/src/components/button.tsx +122 -0
  6. package/src/components/card.tsx +108 -0
  7. package/src/components/checkbox.tsx +37 -0
  8. package/src/components/collapsible.tsx +21 -0
  9. package/src/components/color-picker.tsx +270 -0
  10. package/src/components/command.tsx +195 -0
  11. package/src/components/context-menu.tsx +270 -0
  12. package/src/components/dialog.tsx +169 -0
  13. package/src/components/dropdown-menu.tsx +279 -0
  14. package/src/components/empty.tsx +104 -0
  15. package/src/components/index.ts +27 -0
  16. package/src/components/input-group.tsx +155 -0
  17. package/src/components/input.tsx +27 -0
  18. package/src/components/label.tsx +18 -0
  19. package/src/components/popover.tsx +88 -0
  20. package/src/components/scroll-area.tsx +55 -0
  21. package/src/components/select.tsx +201 -0
  22. package/src/components/separator.tsx +23 -0
  23. package/src/components/sheet.tsx +138 -0
  24. package/src/components/sidebar.tsx +729 -0
  25. package/src/components/skeleton.tsx +13 -0
  26. package/src/components/sonner.tsx +59 -0
  27. package/src/components/switch.tsx +51 -0
  28. package/src/components/table.tsx +375 -0
  29. package/src/components/tabs.tsx +80 -0
  30. package/src/components/textarea.tsx +18 -0
  31. package/src/components/tooltip.tsx +64 -0
  32. package/src/hooks/.gitkeep +0 -0
  33. package/src/hooks/use-mobile.ts +19 -0
  34. package/src/lib/.gitkeep +0 -0
  35. package/src/lib/utils.ts +6 -0
  36. package/src/styles/globals.css +654 -0
@@ -0,0 +1,270 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@workspace/ui/lib/utils"
5
+
6
+ interface ColorPickerProps {
7
+ value: string
8
+ onChange: (hex: string) => void
9
+ className?: string
10
+ }
11
+
12
+ function hexToHsl(hex: string): { h: number; s: number; l: number } {
13
+ hex = hex.replace("#", "")
14
+ const r = parseInt(hex.substring(0, 2), 16) / 255
15
+ const g = parseInt(hex.substring(2, 4), 16) / 255
16
+ const b = parseInt(hex.substring(4, 6), 16) / 255
17
+ const max = Math.max(r, g, b)
18
+ const min = Math.min(r, g, b)
19
+ const l = (max + min) / 2
20
+ if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) }
21
+ const d = max - min
22
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
23
+ let h = 0
24
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6
25
+ else if (max === g) h = ((b - r) / d + 2) / 6
26
+ else h = ((r - g) / d + 4) / 6
27
+ return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }
28
+ }
29
+
30
+ function hslToHex(h: number, s: number, l: number): string {
31
+ s /= 100; l /= 100
32
+ const c = (1 - Math.abs(2 * l - 1)) * s
33
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
34
+ const m = l - c / 2
35
+ let r = 0, g = 0, b = 0
36
+ if (h < 60) { r = c; g = x }
37
+ else if (h < 120) { r = x; g = c }
38
+ else if (h < 180) { g = c; b = x }
39
+ else if (h < 240) { g = x; b = c }
40
+ else if (h < 300) { r = x; b = c }
41
+ else { r = c; b = x }
42
+ const toHex = (v: number) => Math.round((v + m) * 255).toString(16).padStart(2, "0")
43
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
44
+ }
45
+
46
+ function ColorPicker({ value, onChange, className }: ColorPickerProps) {
47
+ const [isOpen, setIsOpen] = React.useState(false)
48
+ const [inputValue, setInputValue] = React.useState(value)
49
+ const [hue, setHue] = React.useState(0)
50
+ const [sat, setSat] = React.useState(100)
51
+ const [lit, setLit] = React.useState(50)
52
+ const [isDraggingCanvas, setIsDraggingCanvas] = React.useState(false)
53
+ const [isDraggingHue, setIsDraggingHue] = React.useState(false)
54
+ const containerRef = React.useRef<HTMLDivElement>(null)
55
+ const canvasRef = React.useRef<HTMLCanvasElement>(null)
56
+ const hueRef = React.useRef<HTMLDivElement>(null)
57
+
58
+ React.useEffect(() => {
59
+ setInputValue(value)
60
+ if (value && /^#[0-9a-fA-F]{6}$/.test(value)) {
61
+ const hsl = hexToHsl(value)
62
+ setHue(hsl.h)
63
+ setSat(hsl.s)
64
+ setLit(hsl.l)
65
+ }
66
+ }, [value])
67
+
68
+ // Draw canvas: saturation (x) × lightness (y)
69
+ React.useEffect(() => {
70
+ const canvas = canvasRef.current
71
+ if (!canvas) return
72
+ const ctx = canvas.getContext("2d")
73
+ if (!ctx) return
74
+ const w = canvas.width
75
+ const h = canvas.height
76
+ const hueColor = hslToHex(hue, 100, 50)
77
+ // Horizontal: white to hue (saturation)
78
+ const gradH = ctx.createLinearGradient(0, 0, w, 0)
79
+ gradH.addColorStop(0, "#ffffff")
80
+ gradH.addColorStop(1, hueColor)
81
+ ctx.fillStyle = gradH
82
+ ctx.fillRect(0, 0, w, h)
83
+ // Vertical: transparent to black (lightness)
84
+ const gradV = ctx.createLinearGradient(0, 0, 0, h)
85
+ gradV.addColorStop(0, "rgba(0,0,0,0)")
86
+ gradV.addColorStop(1, "#000000")
87
+ ctx.fillStyle = gradV
88
+ ctx.fillRect(0, 0, w, h)
89
+ }, [hue])
90
+
91
+ React.useEffect(() => {
92
+ if (!isOpen) return
93
+ const handler = (e: MouseEvent) => {
94
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
95
+ setIsOpen(false)
96
+ }
97
+ }
98
+ document.addEventListener("mousedown", handler)
99
+ return () => document.removeEventListener("mousedown", handler)
100
+ }, [isOpen])
101
+
102
+ // Canvas controls both saturation (x) and lightness (y)
103
+ const updateCanvas = React.useCallback((clientX: number, clientY: number) => {
104
+ const canvas = canvasRef.current
105
+ if (!canvas) return
106
+ const rect = canvas.getBoundingClientRect()
107
+ const x = Math.max(0, Math.min(clientX - rect.left, rect.width))
108
+ const y = Math.max(0, Math.min(clientY - rect.top, rect.height))
109
+ const newSat = Math.round((x / rect.width) * 100)
110
+ const newLit = Math.round((1 - y / rect.height) * 100)
111
+ setSat(newSat)
112
+ setLit(newLit)
113
+ const hex = hslToHex(hue, newSat, newLit)
114
+ setInputValue(hex)
115
+ onChange(hex)
116
+ }, [hue, onChange])
117
+
118
+ const updateHue = React.useCallback((clientX: number) => {
119
+ const el = hueRef.current
120
+ if (!el) return
121
+ const rect = el.getBoundingClientRect()
122
+ const x = Math.max(0, Math.min(clientX - rect.left, rect.width))
123
+ const newHue = Math.round((x / rect.width) * 360)
124
+ setHue(newHue)
125
+ const hex = hslToHex(newHue, sat, lit)
126
+ setInputValue(hex)
127
+ onChange(hex)
128
+ }, [sat, lit, onChange])
129
+
130
+ React.useEffect(() => {
131
+ if (!isDraggingCanvas && !isDraggingHue) return
132
+ const handleMove = (e: MouseEvent) => {
133
+ if (isDraggingCanvas) updateCanvas(e.clientX, e.clientY)
134
+ if (isDraggingHue) updateHue(e.clientX)
135
+ }
136
+ const handleUp = () => {
137
+ setIsDraggingCanvas(false)
138
+ setIsDraggingHue(false)
139
+ }
140
+ document.addEventListener("mousemove", handleMove)
141
+ document.addEventListener("mouseup", handleUp)
142
+ return () => {
143
+ document.removeEventListener("mousemove", handleMove)
144
+ document.removeEventListener("mouseup", handleUp)
145
+ }
146
+ }, [isDraggingCanvas, isDraggingHue, updateCanvas, updateHue])
147
+
148
+ const handleCanvasDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
149
+ setIsDraggingCanvas(true)
150
+ updateCanvas(e.clientX, e.clientY)
151
+ }
152
+ const handleHueDown = (e: React.MouseEvent<HTMLDivElement>) => {
153
+ setIsDraggingHue(true)
154
+ updateHue(e.clientX)
155
+ }
156
+
157
+ const handleHexInput = (v: string) => {
158
+ setInputValue(v)
159
+ if (/^#[0-9a-fA-F]{6}$/.test(v)) onChange(v)
160
+ }
161
+
162
+ const handleClear = (e: React.MouseEvent) => {
163
+ e.stopPropagation()
164
+ onChange("")
165
+ setInputValue("")
166
+ }
167
+
168
+ const currentColor = value || "#808080"
169
+
170
+ return (
171
+ <div ref={containerRef} className={cn("relative", className)}>
172
+ <button
173
+ type="button"
174
+ onClick={() => setIsOpen(!isOpen)}
175
+ className="flex items-center gap-3 rounded-xl border border-border/50 px-3 py-2 hover:bg-muted/50 transition-colors"
176
+ >
177
+ <div
178
+ className="size-10 rounded-xl border-2 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.15),0_2px_4px_rgba(0,0,0,0.15)]"
179
+ style={{ backgroundColor: currentColor, borderColor: currentColor }}
180
+ />
181
+ <div className="flex flex-col items-start">
182
+ <span className="text-xs font-medium text-foreground">{value ? "Custom" : "Default"}</span>
183
+ <span className="text-[11px] font-mono text-muted-foreground">{value || "—"}</span>
184
+ </div>
185
+ {value && (
186
+ <span
187
+ role="button"
188
+ tabIndex={0}
189
+ onClick={handleClear}
190
+ onKeyDown={(e) => e.key === "Enter" && handleClear(e)}
191
+ className="ml-2 text-muted-foreground/50 hover:text-muted-foreground text-xs cursor-pointer"
192
+ >
193
+ ×
194
+ </span>
195
+ )}
196
+ </button>
197
+
198
+ {isOpen && (
199
+ <div className="absolute top-full left-0 mt-2 z-50 w-72 rounded-2xl border border-border bg-popover p-4 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.1),inset_0_-2px_0_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(0,0,0,0.05),0_2px_0_0_rgba(0,0,0,0.08),0_4px_0_0_rgba(0,0,0,0.06),0_6px_0_0_rgba(0,0,0,0.04),0_8px_0_0_rgba(0,0,0,0.02),0_12px_24px_-4px_rgba(0,0,0,0.15)]">
200
+ {/* Current color preview */}
201
+ <div className="flex items-center gap-3 mb-3">
202
+ <div
203
+ className="size-12 rounded-xl border-2 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.15),0_2px_4px_rgba(0,0,0,0.15)]"
204
+ style={{ backgroundColor: currentColor, borderColor: currentColor }}
205
+ />
206
+ <div>
207
+ <p className="text-xs font-medium text-foreground">Current color</p>
208
+ <p className="text-[11px] font-mono text-muted-foreground">{value || "Default (theme)"}</p>
209
+ </div>
210
+ </div>
211
+
212
+ {/* Canvas with pointer */}
213
+ <div className="relative h-[160px] rounded-xl border border-border overflow-hidden cursor-crosshair select-none">
214
+ <canvas
215
+ ref={canvasRef}
216
+ width={248}
217
+ height={160}
218
+ className="absolute inset-0 w-full h-full"
219
+ onMouseDown={handleCanvasDown}
220
+ />
221
+ {/* 3D pointer */}
222
+ <div
223
+ className="absolute w-4 h-4 -translate-x-1/2 -translate-y-1/2 pointer-events-none rounded-full"
224
+ style={{
225
+ left: `${sat}%`,
226
+ top: `${(1 - lit / 100) * 100}%`,
227
+ boxShadow: "0 1px 3px rgba(0,0,0,0.4), 0 0 0 1.5px rgba(255,255,255,0.9), inset 0 1px 0 rgba(255,255,255,0.5)",
228
+ }}
229
+ />
230
+ </div>
231
+
232
+ {/* Hue slider */}
233
+ <div className="mt-3">
234
+ <p className="text-[10px] font-medium text-muted-foreground mb-1.5">Hue</p>
235
+ <div
236
+ ref={hueRef}
237
+ className="relative h-5 rounded-full cursor-pointer border border-border select-none"
238
+ style={{
239
+ background: "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
240
+ }}
241
+ onMouseDown={handleHueDown}
242
+ >
243
+ <div
244
+ className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 size-5 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.2),0_1px_3px_rgba(0,0,0,0.3)] pointer-events-none"
245
+ style={{ left: `${(hue / 360) * 100}%`, backgroundColor: hslToHex(hue, 100, 50) }}
246
+ />
247
+ </div>
248
+ </div>
249
+
250
+ {/* Hex input */}
251
+ <div className="mt-3 flex items-center gap-2">
252
+ <div
253
+ className="size-10 rounded-xl border-2 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.15),0_2px_4px_rgba(0,0,0,0.15)]"
254
+ style={{ backgroundColor: currentColor, borderColor: currentColor }}
255
+ />
256
+ <input
257
+ type="text"
258
+ value={inputValue}
259
+ onChange={(e) => handleHexInput(e.target.value)}
260
+ placeholder="#000000"
261
+ className="flex-1 text-xs font-mono bg-muted/50 border border-border rounded-lg px-2.5 py-2 focus:outline-none focus:ring-1 focus:ring-ring"
262
+ />
263
+ </div>
264
+ </div>
265
+ )}
266
+ </div>
267
+ )
268
+ }
269
+
270
+ export { ColorPicker }
@@ -0,0 +1,195 @@
1
+ import * as React from "react"
2
+ import { Command as CommandPrimitive } from "cmdk"
3
+
4
+ import { cn } from "@workspace/ui/lib/utils"
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@workspace/ui/components/dialog"
12
+ import {
13
+ InputGroup,
14
+ InputGroupAddon,
15
+ } from "@workspace/ui/components/input-group"
16
+ import { HugeiconsIcon } from "@hugeicons/react"
17
+ import { SearchIcon, Tick02Icon } from "@hugeicons/core-free-icons"
18
+
19
+ function Command({
20
+ className,
21
+ ...props
22
+ }: React.ComponentProps<typeof CommandPrimitive>) {
23
+ return (
24
+ <CommandPrimitive
25
+ data-slot="command"
26
+ className={cn(
27
+ "flex size-full flex-col overflow-hidden rounded-4xl bg-popover p-1 text-popover-foreground",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function CommandDialog({
36
+ title = "Command Palette",
37
+ description = "Search for a command to run...",
38
+ children,
39
+ className,
40
+ showCloseButton = false,
41
+ ...props
42
+ }: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
43
+ title?: string
44
+ description?: string
45
+ className?: string
46
+ showCloseButton?: boolean
47
+ children: React.ReactNode
48
+ }) {
49
+ return (
50
+ <Dialog {...props}>
51
+ <DialogHeader className="sr-only">
52
+ <DialogTitle>{title}</DialogTitle>
53
+ <DialogDescription>{description}</DialogDescription>
54
+ </DialogHeader>
55
+ <DialogContent
56
+ className={cn(
57
+ "top-1/3 translate-y-0 overflow-hidden rounded-4xl! p-0",
58
+ className
59
+ )}
60
+ showCloseButton={showCloseButton}
61
+ >
62
+ {children}
63
+ </DialogContent>
64
+ </Dialog>
65
+ )
66
+ }
67
+
68
+ function CommandInput({
69
+ className,
70
+ ...props
71
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
72
+ return (
73
+ <div data-slot="command-input-wrapper" className="p-1 pb-0">
74
+ <InputGroup className="h-9 bg-input/30">
75
+ <CommandPrimitive.Input
76
+ data-slot="command-input"
77
+ className={cn(
78
+ "w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ <InputGroupAddon>
84
+ <HugeiconsIcon icon={SearchIcon} strokeWidth={2} className="size-4 shrink-0 opacity-50" />
85
+ </InputGroupAddon>
86
+ </InputGroup>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ function CommandList({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
95
+ return (
96
+ <CommandPrimitive.List
97
+ data-slot="command-list"
98
+ className={cn(
99
+ "no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ function CommandEmpty({
108
+ className,
109
+ ...props
110
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
111
+ return (
112
+ <CommandPrimitive.Empty
113
+ data-slot="command-empty"
114
+ className={cn("py-6 text-center text-sm", className)}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ function CommandGroup({
121
+ className,
122
+ ...props
123
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
124
+ return (
125
+ <CommandPrimitive.Group
126
+ data-slot="command-group"
127
+ className={cn(
128
+ "overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
129
+ className
130
+ )}
131
+ {...props}
132
+ />
133
+ )
134
+ }
135
+
136
+ function CommandSeparator({
137
+ className,
138
+ ...props
139
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
140
+ return (
141
+ <CommandPrimitive.Separator
142
+ data-slot="command-separator"
143
+ className={cn("my-1 h-px bg-border/50", className)}
144
+ {...props}
145
+ />
146
+ )
147
+ }
148
+
149
+ function CommandItem({
150
+ className,
151
+ children,
152
+ ...props
153
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
154
+ return (
155
+ <CommandPrimitive.Item
156
+ data-slot="command-item"
157
+ className={cn(
158
+ "group/command-item relative flex cursor-default items-center gap-2 rounded-lg px-3 py-2 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-2xl data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ {children}
164
+ <HugeiconsIcon icon={Tick02Icon} strokeWidth={2} className="ms-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
165
+ </CommandPrimitive.Item>
166
+ )
167
+ }
168
+
169
+ function CommandShortcut({
170
+ className,
171
+ ...props
172
+ }: React.ComponentProps<"span">) {
173
+ return (
174
+ <span
175
+ data-slot="command-shortcut"
176
+ className={cn(
177
+ "ms-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
178
+ className
179
+ )}
180
+ {...props}
181
+ />
182
+ )
183
+ }
184
+
185
+ export {
186
+ Command,
187
+ CommandDialog,
188
+ CommandInput,
189
+ CommandList,
190
+ CommandEmpty,
191
+ CommandGroup,
192
+ CommandItem,
193
+ CommandShortcut,
194
+ CommandSeparator,
195
+ }