@proyecto-viviana/ui 0.3.1 → 0.3.3
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/dist/components.css +1077 -1077
- package/dist/index.js +236 -249
- package/dist/index.js.map +3 -3
- package/dist/index.ssr.js +78 -81
- package/dist/index.ssr.js.map +3 -3
- package/dist/radio/index.d.ts +12 -27
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/test-utils/index.d.ts +2 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/autocomplete/index.tsx +313 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/calendar/DateField.tsx +200 -0
- package/src/calendar/DatePicker.tsx +298 -0
- package/src/calendar/RangeCalendar.tsx +236 -0
- package/src/calendar/TimeField.tsx +196 -0
- package/src/calendar/index.tsx +223 -0
- package/src/checkbox/index.tsx +257 -0
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +1077 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +46 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +132 -0
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +58 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +29 -0
- package/src/custom/profile-card/index.tsx +64 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +105 -0
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -0
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +322 -0
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +130 -0
- package/src/listbox/index.tsx +231 -0
- package/src/menu/index.tsx +297 -0
- package/src/meter/index.tsx +163 -0
- package/src/numberfield/index.tsx +482 -0
- package/src/popover/index.tsx +260 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +173 -0
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/separator/index.tsx +141 -0
- package/src/slider/index.tsx +382 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +112 -0
- package/src/switch/index.tsx +90 -0
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/test-utils/index.ts +40 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +197 -0
- package/src/tree/index.tsx +494 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color components for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled color picker components built on top of solidaria-components.
|
|
5
|
+
* Inspired by Spectrum 2's color picker patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, splitProps, createContext, useContext, Show } from 'solid-js'
|
|
9
|
+
import {
|
|
10
|
+
ColorSlider as HeadlessColorSlider,
|
|
11
|
+
ColorSliderTrack as HeadlessColorSliderTrack,
|
|
12
|
+
ColorSliderThumb as HeadlessColorSliderThumb,
|
|
13
|
+
ColorArea as HeadlessColorArea,
|
|
14
|
+
ColorAreaGradient as HeadlessColorAreaGradient,
|
|
15
|
+
ColorAreaThumb as HeadlessColorAreaThumb,
|
|
16
|
+
ColorWheel as HeadlessColorWheel,
|
|
17
|
+
ColorWheelTrack as HeadlessColorWheelTrack,
|
|
18
|
+
ColorWheelThumb as HeadlessColorWheelThumb,
|
|
19
|
+
ColorField as HeadlessColorField,
|
|
20
|
+
ColorFieldInput as HeadlessColorFieldInput,
|
|
21
|
+
ColorSwatch as HeadlessColorSwatch,
|
|
22
|
+
type ColorSliderProps as HeadlessColorSliderProps,
|
|
23
|
+
type ColorAreaProps as HeadlessColorAreaProps,
|
|
24
|
+
type ColorWheelProps as HeadlessColorWheelProps,
|
|
25
|
+
type ColorFieldProps as HeadlessColorFieldProps,
|
|
26
|
+
type ColorSwatchProps as HeadlessColorSwatchProps,
|
|
27
|
+
type ColorSliderRenderProps,
|
|
28
|
+
type ColorSliderTrackRenderProps,
|
|
29
|
+
type ColorSliderThumbRenderProps,
|
|
30
|
+
type ColorAreaRenderProps,
|
|
31
|
+
type ColorAreaThumbRenderProps,
|
|
32
|
+
type ColorWheelRenderProps,
|
|
33
|
+
type ColorWheelThumbRenderProps,
|
|
34
|
+
type ColorFieldRenderProps,
|
|
35
|
+
type ColorSwatchRenderProps,
|
|
36
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
37
|
+
import type { Color, ColorChannel, ColorFormat } from '@proyecto-viviana/solid-stately'
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// SIZE CONTEXT
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export type ColorSize = 'sm' | 'md' | 'lg'
|
|
44
|
+
|
|
45
|
+
interface ColorContextValue {
|
|
46
|
+
size: ColorSize
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ColorSizeContext = createContext<ColorContextValue>({ size: 'md' })
|
|
50
|
+
|
|
51
|
+
// ============================================
|
|
52
|
+
// STYLES
|
|
53
|
+
// ============================================
|
|
54
|
+
|
|
55
|
+
const sizeStyles = {
|
|
56
|
+
sm: {
|
|
57
|
+
slider: {
|
|
58
|
+
track: 'h-4 rounded',
|
|
59
|
+
thumb: 'w-4 h-4',
|
|
60
|
+
label: 'text-sm',
|
|
61
|
+
},
|
|
62
|
+
area: {
|
|
63
|
+
container: 'w-48 h-48',
|
|
64
|
+
thumb: 'w-4 h-4',
|
|
65
|
+
},
|
|
66
|
+
wheel: {
|
|
67
|
+
container: 'w-48 h-48',
|
|
68
|
+
track: 'stroke-[16px]',
|
|
69
|
+
thumb: 'w-4 h-4',
|
|
70
|
+
},
|
|
71
|
+
field: {
|
|
72
|
+
input: 'h-8 text-sm px-2',
|
|
73
|
+
label: 'text-sm',
|
|
74
|
+
},
|
|
75
|
+
swatch: 'w-8 h-8',
|
|
76
|
+
},
|
|
77
|
+
md: {
|
|
78
|
+
slider: {
|
|
79
|
+
track: 'h-6 rounded-md',
|
|
80
|
+
thumb: 'w-5 h-5',
|
|
81
|
+
label: 'text-base',
|
|
82
|
+
},
|
|
83
|
+
area: {
|
|
84
|
+
container: 'w-64 h-64',
|
|
85
|
+
thumb: 'w-5 h-5',
|
|
86
|
+
},
|
|
87
|
+
wheel: {
|
|
88
|
+
container: 'w-64 h-64',
|
|
89
|
+
track: 'stroke-[20px]',
|
|
90
|
+
thumb: 'w-5 h-5',
|
|
91
|
+
},
|
|
92
|
+
field: {
|
|
93
|
+
input: 'h-10 text-base px-3',
|
|
94
|
+
label: 'text-base',
|
|
95
|
+
},
|
|
96
|
+
swatch: 'w-10 h-10',
|
|
97
|
+
},
|
|
98
|
+
lg: {
|
|
99
|
+
slider: {
|
|
100
|
+
track: 'h-8 rounded-lg',
|
|
101
|
+
thumb: 'w-6 h-6',
|
|
102
|
+
label: 'text-lg',
|
|
103
|
+
},
|
|
104
|
+
area: {
|
|
105
|
+
container: 'w-80 h-80',
|
|
106
|
+
thumb: 'w-6 h-6',
|
|
107
|
+
},
|
|
108
|
+
wheel: {
|
|
109
|
+
container: 'w-80 h-80',
|
|
110
|
+
track: 'stroke-[24px]',
|
|
111
|
+
thumb: 'w-6 h-6',
|
|
112
|
+
},
|
|
113
|
+
field: {
|
|
114
|
+
input: 'h-12 text-lg px-4',
|
|
115
|
+
label: 'text-lg',
|
|
116
|
+
},
|
|
117
|
+
swatch: 'w-12 h-12',
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================
|
|
122
|
+
// COLOR SLIDER
|
|
123
|
+
// ============================================
|
|
124
|
+
|
|
125
|
+
export interface ColorSliderProps extends Omit<HeadlessColorSliderProps, 'class' | 'style' | 'children'> {
|
|
126
|
+
/** The size of the color slider. */
|
|
127
|
+
size?: ColorSize
|
|
128
|
+
/** Additional CSS class name. */
|
|
129
|
+
class?: string
|
|
130
|
+
/** Show the current value. */
|
|
131
|
+
showValue?: boolean
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A color slider allows users to adjust a single color channel.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```tsx
|
|
139
|
+
* const [color, setColor] = createSignal(parseColor('hsl(0, 100%, 50%)'))
|
|
140
|
+
*
|
|
141
|
+
* <ColorSlider
|
|
142
|
+
* channel="hue"
|
|
143
|
+
* value={color()}
|
|
144
|
+
* onChange={setColor}
|
|
145
|
+
* label="Hue"
|
|
146
|
+
* />
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function ColorSlider(props: ColorSliderProps): JSX.Element {
|
|
150
|
+
const [local, headlessProps] = splitProps(props, ['size', 'class', 'showValue'])
|
|
151
|
+
|
|
152
|
+
const size = () => local.size ?? 'md'
|
|
153
|
+
const styles = () => sizeStyles[size()]
|
|
154
|
+
const customClass = local.class ?? ''
|
|
155
|
+
|
|
156
|
+
const getClassName = (renderProps: ColorSliderRenderProps): string => {
|
|
157
|
+
const base = 'flex flex-col gap-1.5'
|
|
158
|
+
let stateClass = ''
|
|
159
|
+
if (renderProps.isDisabled) {
|
|
160
|
+
stateClass = 'opacity-50'
|
|
161
|
+
}
|
|
162
|
+
return [base, stateClass, customClass].filter(Boolean).join(' ')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const contextValue = () => ({ size: size() })
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<ColorSizeContext.Provider value={contextValue()}>
|
|
169
|
+
<HeadlessColorSlider {...headlessProps} class={getClassName}>
|
|
170
|
+
{(renderProps: ColorSliderRenderProps) => (
|
|
171
|
+
<>
|
|
172
|
+
<div class="flex items-center justify-between">
|
|
173
|
+
<Show when={headlessProps.label}>
|
|
174
|
+
<span class={`text-primary-200 font-medium ${styles().slider.label}`}>
|
|
175
|
+
{headlessProps.label}
|
|
176
|
+
</span>
|
|
177
|
+
</Show>
|
|
178
|
+
<Show when={local.showValue}>
|
|
179
|
+
<span class={`text-primary-400 ${styles().slider.label}`}>
|
|
180
|
+
{Math.round(renderProps.value)}
|
|
181
|
+
</span>
|
|
182
|
+
</Show>
|
|
183
|
+
</div>
|
|
184
|
+
<ColorSliderTrack>
|
|
185
|
+
{() => <ColorSliderThumb />}
|
|
186
|
+
</ColorSliderTrack>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
</HeadlessColorSlider>
|
|
190
|
+
</ColorSizeContext.Provider>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The track component for a color slider.
|
|
196
|
+
*/
|
|
197
|
+
export function ColorSliderTrack(props: { children?: JSX.Element | (() => JSX.Element); class?: string }): JSX.Element {
|
|
198
|
+
const context = useContext(ColorSizeContext)
|
|
199
|
+
const styles = sizeStyles[context.size]
|
|
200
|
+
const customClass = props.class ?? ''
|
|
201
|
+
|
|
202
|
+
const getClassName = (renderProps: ColorSliderTrackRenderProps): string => {
|
|
203
|
+
const base = `relative ${styles.slider.track} shadow-inner border border-bg-300`
|
|
204
|
+
const dragClass = renderProps.isDragging ? 'cursor-grabbing' : 'cursor-pointer'
|
|
205
|
+
return [base, dragClass, customClass].filter(Boolean).join(' ')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<HeadlessColorSliderTrack class={getClassName}>
|
|
210
|
+
{props.children}
|
|
211
|
+
</HeadlessColorSliderTrack>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The thumb component for a color slider.
|
|
217
|
+
*/
|
|
218
|
+
export function ColorSliderThumb(props: { class?: string }): JSX.Element {
|
|
219
|
+
const context = useContext(ColorSizeContext)
|
|
220
|
+
const styles = sizeStyles[context.size]
|
|
221
|
+
const customClass = props.class ?? ''
|
|
222
|
+
|
|
223
|
+
const getClassName = (renderProps: ColorSliderThumbRenderProps): string => {
|
|
224
|
+
const base = `${styles.slider.thumb} rounded-full border-2 border-white shadow-md cursor-grab`
|
|
225
|
+
const dragClass = renderProps.isDragging ? 'cursor-grabbing scale-110' : ''
|
|
226
|
+
const focusClass = renderProps.isFocusVisible ? 'ring-2 ring-accent-300 ring-offset-2' : ''
|
|
227
|
+
const disabledClass = renderProps.isDisabled ? 'cursor-not-allowed' : ''
|
|
228
|
+
return [base, dragClass, focusClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return <HeadlessColorSliderThumb class={getClassName} />
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================
|
|
235
|
+
// COLOR AREA
|
|
236
|
+
// ============================================
|
|
237
|
+
|
|
238
|
+
export interface ColorAreaProps extends Omit<HeadlessColorAreaProps, 'class' | 'style' | 'children'> {
|
|
239
|
+
/** The size of the color area. */
|
|
240
|
+
size?: ColorSize
|
|
241
|
+
/** Additional CSS class name. */
|
|
242
|
+
class?: string
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* A color area allows users to select a color by dragging in a 2D gradient.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```tsx
|
|
250
|
+
* const [color, setColor] = createSignal(parseColor('hsl(0, 100%, 50%)'))
|
|
251
|
+
*
|
|
252
|
+
* <ColorArea
|
|
253
|
+
* value={color()}
|
|
254
|
+
* onChange={setColor}
|
|
255
|
+
* xChannel="saturation"
|
|
256
|
+
* yChannel="lightness"
|
|
257
|
+
* />
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
export function ColorArea(props: ColorAreaProps): JSX.Element {
|
|
261
|
+
const [local, headlessProps] = splitProps(props, ['size', 'class'])
|
|
262
|
+
|
|
263
|
+
const size = () => local.size ?? 'md'
|
|
264
|
+
const styles = () => sizeStyles[size()]
|
|
265
|
+
const customClass = local.class ?? ''
|
|
266
|
+
|
|
267
|
+
const getClassName = (renderProps: ColorAreaRenderProps): string => {
|
|
268
|
+
const base = `relative ${styles().area.container} rounded-lg overflow-hidden border border-bg-300 shadow-inner`
|
|
269
|
+
let stateClass = ''
|
|
270
|
+
if (renderProps.isDisabled) {
|
|
271
|
+
stateClass = 'opacity-50 cursor-not-allowed'
|
|
272
|
+
}
|
|
273
|
+
return [base, stateClass, customClass].filter(Boolean).join(' ')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const contextValue = () => ({ size: size() })
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<ColorSizeContext.Provider value={contextValue()}>
|
|
280
|
+
<HeadlessColorArea {...headlessProps} class={getClassName}>
|
|
281
|
+
{() => (
|
|
282
|
+
<>
|
|
283
|
+
<ColorAreaGradient />
|
|
284
|
+
<ColorAreaThumb />
|
|
285
|
+
</>
|
|
286
|
+
)}
|
|
287
|
+
</HeadlessColorArea>
|
|
288
|
+
</ColorSizeContext.Provider>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The gradient background for a color area.
|
|
294
|
+
*/
|
|
295
|
+
export function ColorAreaGradient(props: { class?: string }): JSX.Element {
|
|
296
|
+
const customClass = props.class ?? ''
|
|
297
|
+
const className = `absolute inset-0 ${customClass}`
|
|
298
|
+
|
|
299
|
+
return <HeadlessColorAreaGradient class={className} />
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The thumb component for a color area.
|
|
304
|
+
*/
|
|
305
|
+
export function ColorAreaThumb(props: { class?: string }): JSX.Element {
|
|
306
|
+
const context = useContext(ColorSizeContext)
|
|
307
|
+
const styles = sizeStyles[context.size]
|
|
308
|
+
const customClass = props.class ?? ''
|
|
309
|
+
|
|
310
|
+
const getClassName = (renderProps: ColorAreaThumbRenderProps): string => {
|
|
311
|
+
const base = `${styles.area.thumb} rounded-full border-2 border-white shadow-md cursor-grab`
|
|
312
|
+
const dragClass = renderProps.isDragging ? 'cursor-grabbing scale-110' : ''
|
|
313
|
+
const focusClass = renderProps.isFocusVisible ? 'ring-2 ring-accent-300 ring-offset-2' : ''
|
|
314
|
+
const disabledClass = renderProps.isDisabled ? 'cursor-not-allowed' : ''
|
|
315
|
+
return [base, dragClass, focusClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return <HeadlessColorAreaThumb class={getClassName} />
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================
|
|
322
|
+
// COLOR WHEEL
|
|
323
|
+
// ============================================
|
|
324
|
+
|
|
325
|
+
export interface ColorWheelProps extends Omit<HeadlessColorWheelProps, 'class' | 'style' | 'children'> {
|
|
326
|
+
/** The size of the color wheel. */
|
|
327
|
+
size?: ColorSize
|
|
328
|
+
/** Additional CSS class name. */
|
|
329
|
+
class?: string
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A color wheel allows users to select a hue by dragging around a circular track.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```tsx
|
|
337
|
+
* const [color, setColor] = createSignal(parseColor('hsl(0, 100%, 50%)'))
|
|
338
|
+
*
|
|
339
|
+
* <ColorWheel
|
|
340
|
+
* value={color()}
|
|
341
|
+
* onChange={setColor}
|
|
342
|
+
* />
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
export function ColorWheel(props: ColorWheelProps): JSX.Element {
|
|
346
|
+
const [local, headlessProps] = splitProps(props, ['size', 'class'])
|
|
347
|
+
|
|
348
|
+
const size = () => local.size ?? 'md'
|
|
349
|
+
const styles = () => sizeStyles[size()]
|
|
350
|
+
const customClass = local.class ?? ''
|
|
351
|
+
|
|
352
|
+
const getClassName = (renderProps: ColorWheelRenderProps): string => {
|
|
353
|
+
const base = `relative ${styles().wheel.container}`
|
|
354
|
+
let stateClass = ''
|
|
355
|
+
if (renderProps.isDisabled) {
|
|
356
|
+
stateClass = 'opacity-50 cursor-not-allowed'
|
|
357
|
+
}
|
|
358
|
+
return [base, stateClass, customClass].filter(Boolean).join(' ')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const contextValue = () => ({ size: size() })
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<ColorSizeContext.Provider value={contextValue()}>
|
|
365
|
+
<HeadlessColorWheel {...headlessProps} class={getClassName}>
|
|
366
|
+
{() => (
|
|
367
|
+
<>
|
|
368
|
+
<ColorWheelTrack />
|
|
369
|
+
<ColorWheelThumb />
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
</HeadlessColorWheel>
|
|
373
|
+
</ColorSizeContext.Provider>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The circular track for a color wheel.
|
|
379
|
+
*/
|
|
380
|
+
export function ColorWheelTrack(props: { class?: string }): JSX.Element {
|
|
381
|
+
const context = useContext(ColorSizeContext)
|
|
382
|
+
const styles = sizeStyles[context.size]
|
|
383
|
+
const customClass = props.class ?? ''
|
|
384
|
+
|
|
385
|
+
const className = `${styles.wheel.track} ${customClass}`
|
|
386
|
+
|
|
387
|
+
return <HeadlessColorWheelTrack class={className} />
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The thumb component for a color wheel.
|
|
392
|
+
*/
|
|
393
|
+
export function ColorWheelThumb(props: { class?: string }): JSX.Element {
|
|
394
|
+
const context = useContext(ColorSizeContext)
|
|
395
|
+
const styles = sizeStyles[context.size]
|
|
396
|
+
const customClass = props.class ?? ''
|
|
397
|
+
|
|
398
|
+
const getClassName = (renderProps: ColorWheelThumbRenderProps): string => {
|
|
399
|
+
const base = `${styles.wheel.thumb} rounded-full border-2 border-white shadow-md cursor-grab`
|
|
400
|
+
const dragClass = renderProps.isDragging ? 'cursor-grabbing scale-110' : ''
|
|
401
|
+
const focusClass = renderProps.isFocusVisible ? 'ring-2 ring-accent-300 ring-offset-2' : ''
|
|
402
|
+
const disabledClass = renderProps.isDisabled ? 'cursor-not-allowed' : ''
|
|
403
|
+
return [base, dragClass, focusClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return <HeadlessColorWheelThumb class={getClassName} />
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================
|
|
410
|
+
// COLOR FIELD
|
|
411
|
+
// ============================================
|
|
412
|
+
|
|
413
|
+
export interface ColorFieldProps extends Omit<HeadlessColorFieldProps, 'class' | 'style' | 'children'> {
|
|
414
|
+
/** The size of the color field. */
|
|
415
|
+
size?: ColorSize
|
|
416
|
+
/** Additional CSS class name. */
|
|
417
|
+
class?: string
|
|
418
|
+
/** Description text below the input. */
|
|
419
|
+
description?: string
|
|
420
|
+
/** Error message to display. */
|
|
421
|
+
errorMessage?: string
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A color field allows users to enter a color value as text.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```tsx
|
|
429
|
+
* const [color, setColor] = createSignal(parseColor('#ff0000'))
|
|
430
|
+
*
|
|
431
|
+
* <ColorField
|
|
432
|
+
* value={color()}
|
|
433
|
+
* onChange={setColor}
|
|
434
|
+
* label="Color"
|
|
435
|
+
* />
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
export function ColorField(props: ColorFieldProps): JSX.Element {
|
|
439
|
+
const [local, headlessProps] = splitProps(props, [
|
|
440
|
+
'size',
|
|
441
|
+
'class',
|
|
442
|
+
'description',
|
|
443
|
+
'errorMessage',
|
|
444
|
+
])
|
|
445
|
+
|
|
446
|
+
const size = () => local.size ?? 'md'
|
|
447
|
+
const styles = () => sizeStyles[size()]
|
|
448
|
+
const customClass = local.class ?? ''
|
|
449
|
+
|
|
450
|
+
const getClassName = (renderProps: ColorFieldRenderProps): string => {
|
|
451
|
+
const base = 'flex flex-col gap-1.5'
|
|
452
|
+
let stateClass = ''
|
|
453
|
+
if (renderProps.isDisabled) {
|
|
454
|
+
stateClass = 'opacity-50'
|
|
455
|
+
}
|
|
456
|
+
return [base, stateClass, customClass].filter(Boolean).join(' ')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const contextValue = () => ({ size: size() })
|
|
460
|
+
|
|
461
|
+
return (
|
|
462
|
+
<ColorSizeContext.Provider value={contextValue()}>
|
|
463
|
+
<HeadlessColorField {...headlessProps} class={getClassName}>
|
|
464
|
+
{() => (
|
|
465
|
+
<>
|
|
466
|
+
<Show when={headlessProps.label}>
|
|
467
|
+
<span class={`text-primary-200 font-medium ${styles().field.label}`}>
|
|
468
|
+
{headlessProps.label}
|
|
469
|
+
</span>
|
|
470
|
+
</Show>
|
|
471
|
+
<ColorFieldInput isInvalid={!!local.errorMessage} />
|
|
472
|
+
<Show when={local.description && !local.errorMessage}>
|
|
473
|
+
<span class="text-primary-400 text-sm">{local.description}</span>
|
|
474
|
+
</Show>
|
|
475
|
+
<Show when={local.errorMessage}>
|
|
476
|
+
<span class="text-danger-400 text-sm">{local.errorMessage}</span>
|
|
477
|
+
</Show>
|
|
478
|
+
</>
|
|
479
|
+
)}
|
|
480
|
+
</HeadlessColorField>
|
|
481
|
+
</ColorSizeContext.Provider>
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The input component for a color field.
|
|
487
|
+
*/
|
|
488
|
+
export function ColorFieldInput(props: { class?: string; isInvalid?: boolean }): JSX.Element {
|
|
489
|
+
const context = useContext(ColorSizeContext)
|
|
490
|
+
const styles = sizeStyles[context.size]
|
|
491
|
+
const customClass = props.class ?? ''
|
|
492
|
+
|
|
493
|
+
const base = `${styles.field.input} w-full rounded-md border bg-bg-400 text-primary-200 placeholder:text-primary-500 focus:outline-none focus:ring-2 focus:ring-accent-300`
|
|
494
|
+
const borderClass = props.isInvalid
|
|
495
|
+
? 'border-danger-400'
|
|
496
|
+
: 'border-bg-300 focus:border-accent-300'
|
|
497
|
+
const className = [base, borderClass, customClass].filter(Boolean).join(' ')
|
|
498
|
+
|
|
499
|
+
return <HeadlessColorFieldInput class={className} />
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ============================================
|
|
503
|
+
// COLOR SWATCH
|
|
504
|
+
// ============================================
|
|
505
|
+
|
|
506
|
+
export interface ColorSwatchProps extends Omit<HeadlessColorSwatchProps, 'class' | 'style'> {
|
|
507
|
+
/** The size of the color swatch. */
|
|
508
|
+
size?: ColorSize
|
|
509
|
+
/** Additional CSS class name. */
|
|
510
|
+
class?: string
|
|
511
|
+
/** Whether the swatch is selectable. */
|
|
512
|
+
isSelectable?: boolean
|
|
513
|
+
/** Whether the swatch is selected. */
|
|
514
|
+
isSelected?: boolean
|
|
515
|
+
/** Handler called when the swatch is clicked. */
|
|
516
|
+
onClick?: () => void
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* A color swatch displays a color sample.
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```tsx
|
|
524
|
+
* <ColorSwatch color={parseColor('#ff0000')} />
|
|
525
|
+
*
|
|
526
|
+
* // Selectable swatch
|
|
527
|
+
* <ColorSwatch
|
|
528
|
+
* color={parseColor('#00ff00')}
|
|
529
|
+
* isSelectable
|
|
530
|
+
* isSelected={selectedColor === '#00ff00'}
|
|
531
|
+
* onClick={() => setSelectedColor('#00ff00')}
|
|
532
|
+
* />
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
export function ColorSwatch(props: ColorSwatchProps): JSX.Element {
|
|
536
|
+
const [local, headlessProps] = splitProps(props, [
|
|
537
|
+
'size',
|
|
538
|
+
'class',
|
|
539
|
+
'isSelectable',
|
|
540
|
+
'isSelected',
|
|
541
|
+
'onClick',
|
|
542
|
+
])
|
|
543
|
+
|
|
544
|
+
const size = () => local.size ?? 'md'
|
|
545
|
+
const styles = () => sizeStyles[size()]
|
|
546
|
+
const customClass = local.class ?? ''
|
|
547
|
+
|
|
548
|
+
const getClassName = (_renderProps: ColorSwatchRenderProps): string => {
|
|
549
|
+
const base = `${styles().swatch} rounded-md border border-bg-300 shadow-sm`
|
|
550
|
+
const selectableClass = local.isSelectable
|
|
551
|
+
? 'cursor-pointer hover:scale-105 transition-transform'
|
|
552
|
+
: ''
|
|
553
|
+
const selectedClass = local.isSelected
|
|
554
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
555
|
+
: ''
|
|
556
|
+
return [base, selectableClass, selectedClass, customClass].filter(Boolean).join(' ')
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const handleClick = () => {
|
|
560
|
+
if (local.isSelectable && local.onClick) {
|
|
561
|
+
local.onClick()
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div onClick={handleClick}>
|
|
567
|
+
<HeadlessColorSwatch {...headlessProps} class={getClassName} />
|
|
568
|
+
</div>
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================
|
|
573
|
+
// COLOR PICKER (Composite Component)
|
|
574
|
+
// ============================================
|
|
575
|
+
|
|
576
|
+
export interface ColorPickerProps {
|
|
577
|
+
/** The current color value (controlled). */
|
|
578
|
+
value?: Color | string
|
|
579
|
+
/** The default color value (uncontrolled). */
|
|
580
|
+
defaultValue?: Color | string
|
|
581
|
+
/** Handler called when the color changes. */
|
|
582
|
+
onChange?: (color: Color) => void
|
|
583
|
+
/** The size of the picker. */
|
|
584
|
+
size?: ColorSize
|
|
585
|
+
/** Additional CSS class name. */
|
|
586
|
+
class?: string
|
|
587
|
+
/** Whether the picker is disabled. */
|
|
588
|
+
isDisabled?: boolean
|
|
589
|
+
/** A label for the picker. */
|
|
590
|
+
label?: string
|
|
591
|
+
/** Whether to show the hex input field. */
|
|
592
|
+
showInput?: boolean
|
|
593
|
+
/** Whether to show channel sliders. */
|
|
594
|
+
showSliders?: boolean
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* A complete color picker component with area, sliders, and input.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```tsx
|
|
602
|
+
* const [color, setColor] = createSignal(parseColor('hsl(0, 100%, 50%)'))
|
|
603
|
+
*
|
|
604
|
+
* <ColorPicker
|
|
605
|
+
* value={color()}
|
|
606
|
+
* onChange={setColor}
|
|
607
|
+
* label="Pick a color"
|
|
608
|
+
* showInput
|
|
609
|
+
* showSliders
|
|
610
|
+
* />
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
export function ColorPicker(props: ColorPickerProps): JSX.Element {
|
|
614
|
+
const size = () => props.size ?? 'md'
|
|
615
|
+
const styles = () => sizeStyles[size()]
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<div class={`flex flex-col gap-4 ${props.class ?? ''}`}>
|
|
619
|
+
<Show when={props.label}>
|
|
620
|
+
<span class={`text-primary-200 font-medium ${styles().field.label}`}>
|
|
621
|
+
{props.label}
|
|
622
|
+
</span>
|
|
623
|
+
</Show>
|
|
624
|
+
|
|
625
|
+
<ColorArea
|
|
626
|
+
value={props.value}
|
|
627
|
+
defaultValue={props.defaultValue}
|
|
628
|
+
onChange={props.onChange}
|
|
629
|
+
xChannel="saturation"
|
|
630
|
+
yChannel="lightness"
|
|
631
|
+
size={size()}
|
|
632
|
+
isDisabled={props.isDisabled}
|
|
633
|
+
/>
|
|
634
|
+
|
|
635
|
+
<Show when={props.showSliders !== false}>
|
|
636
|
+
<ColorSlider
|
|
637
|
+
value={props.value}
|
|
638
|
+
defaultValue={props.defaultValue}
|
|
639
|
+
onChange={props.onChange}
|
|
640
|
+
channel="hue"
|
|
641
|
+
label="Hue"
|
|
642
|
+
size={size()}
|
|
643
|
+
showValue
|
|
644
|
+
isDisabled={props.isDisabled}
|
|
645
|
+
/>
|
|
646
|
+
|
|
647
|
+
<ColorSlider
|
|
648
|
+
value={props.value}
|
|
649
|
+
defaultValue={props.defaultValue}
|
|
650
|
+
onChange={props.onChange}
|
|
651
|
+
channel="alpha"
|
|
652
|
+
label="Alpha"
|
|
653
|
+
size={size()}
|
|
654
|
+
showValue
|
|
655
|
+
isDisabled={props.isDisabled}
|
|
656
|
+
/>
|
|
657
|
+
</Show>
|
|
658
|
+
|
|
659
|
+
<Show when={props.showInput}>
|
|
660
|
+
<ColorField
|
|
661
|
+
value={props.value}
|
|
662
|
+
defaultValue={props.defaultValue}
|
|
663
|
+
onChange={(color) => {
|
|
664
|
+
if (color && props.onChange) {
|
|
665
|
+
props.onChange(color)
|
|
666
|
+
}
|
|
667
|
+
}}
|
|
668
|
+
label="Hex"
|
|
669
|
+
size={size()}
|
|
670
|
+
isDisabled={props.isDisabled}
|
|
671
|
+
/>
|
|
672
|
+
</Show>
|
|
673
|
+
</div>
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Attach sub-components for convenience
|
|
678
|
+
ColorSlider.Track = ColorSliderTrack
|
|
679
|
+
ColorSlider.Thumb = ColorSliderThumb
|
|
680
|
+
ColorArea.Gradient = ColorAreaGradient
|
|
681
|
+
ColorArea.Thumb = ColorAreaThumb
|
|
682
|
+
ColorWheel.Track = ColorWheelTrack
|
|
683
|
+
ColorWheel.Thumb = ColorWheelThumb
|
|
684
|
+
ColorField.Input = ColorFieldInput
|
|
685
|
+
|
|
686
|
+
// Re-export types for convenience
|
|
687
|
+
export type { Color, ColorChannel, ColorFormat }
|