uibee 2.7.17 → 2.8.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.
- package/dist/src/components/inputs/checkbox.d.ts +4 -9
- package/dist/src/components/inputs/checkbox.js +3 -2
- package/dist/src/components/inputs/input.d.ts +4 -11
- package/dist/src/components/inputs/input.js +30 -8
- package/dist/src/components/inputs/radio.d.ts +4 -10
- package/dist/src/components/inputs/radio.js +4 -2
- package/dist/src/components/inputs/range.d.ts +3 -11
- package/dist/src/components/inputs/range.js +4 -2
- package/dist/src/components/inputs/shared/colorPickerPopup.d.ts +7 -0
- package/dist/src/components/inputs/shared/colorPickerPopup.js +185 -0
- package/dist/src/components/inputs/shared/index.d.ts +1 -0
- package/dist/src/components/inputs/shared/index.js +1 -0
- package/dist/src/components/inputs/switch.d.ts +4 -9
- package/dist/src/components/inputs/switch.js +4 -3
- package/dist/src/components/inputs/textarea.d.ts +4 -11
- package/dist/src/components/inputs/textarea.js +5 -3
- package/dist/src/globals.css +148 -0
- package/package.json +1 -1
- package/src/components/inputs/checkbox.tsx +9 -25
- package/src/components/inputs/input.tsx +47 -38
- package/src/components/inputs/radio.tsx +10 -28
- package/src/components/inputs/range.tsx +8 -29
- package/src/components/inputs/shared/colorPickerPopup.tsx +240 -0
- package/src/components/inputs/shared/index.ts +1 -0
- package/src/components/inputs/switch.tsx +10 -27
- package/src/components/inputs/textarea.tsx +11 -32
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { type JSX, useState, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export type ColorPickerPopupProps = {
|
|
4
|
+
value: string
|
|
5
|
+
onChange: (color: string) => void
|
|
6
|
+
onClose: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hexToHsv(hex: string): { h: number; s: number; v: number } {
|
|
10
|
+
hex = hex.replace('#', '')
|
|
11
|
+
let r = 0, g = 0, b = 0
|
|
12
|
+
if (hex.length === 3) {
|
|
13
|
+
r = parseInt(hex[0] + hex[0], 16)
|
|
14
|
+
g = parseInt(hex[1] + hex[1], 16)
|
|
15
|
+
b = parseInt(hex[2] + hex[2], 16)
|
|
16
|
+
} else if (hex.length === 6) {
|
|
17
|
+
r = parseInt(hex.substring(0, 2), 16)
|
|
18
|
+
g = parseInt(hex.substring(2, 4), 16)
|
|
19
|
+
b = parseInt(hex.substring(4, 6), 16)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
r /= 255
|
|
23
|
+
g /= 255
|
|
24
|
+
b /= 255
|
|
25
|
+
|
|
26
|
+
const max = Math.max(r, g, b)
|
|
27
|
+
const min = Math.min(r, g, b)
|
|
28
|
+
const d = max - min
|
|
29
|
+
let h = 0
|
|
30
|
+
const s = max === 0 ? 0 : d / max
|
|
31
|
+
const v = max
|
|
32
|
+
|
|
33
|
+
if (max !== min) {
|
|
34
|
+
switch (max) {
|
|
35
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
|
36
|
+
case g: h = (b - r) / d + 2; break
|
|
37
|
+
case b: h = (r - g) / d + 4; break
|
|
38
|
+
}
|
|
39
|
+
h /= 6
|
|
40
|
+
}
|
|
41
|
+
return { h: h * 360, s: s * 100, v: v * 100 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hsvToRgb(h: number, s: number, v: number): { r: number; g: number; b: number } {
|
|
45
|
+
let r = 0, g = 0, b = 0
|
|
46
|
+
const i = Math.floor(h * 6)
|
|
47
|
+
const f = h * 6 - i
|
|
48
|
+
const p = v * (1 - s)
|
|
49
|
+
const q = v * (1 - f * s)
|
|
50
|
+
const t = v * (1 - (1 - f) * s)
|
|
51
|
+
|
|
52
|
+
switch (i % 6) {
|
|
53
|
+
case 0: r = v; g = t; b = p; break
|
|
54
|
+
case 1: r = q; g = v; b = p; break
|
|
55
|
+
case 2: r = p; g = v; b = t; break
|
|
56
|
+
case 3: r = p; g = q; b = v; break
|
|
57
|
+
case 4: r = t; g = p; b = v; break
|
|
58
|
+
case 5: r = v; g = p; b = q; break
|
|
59
|
+
}
|
|
60
|
+
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hsvToHex(h: number, s: number, v: number): string {
|
|
64
|
+
const { r, g, b } = hsvToRgb(h / 360, s / 100, v / 100)
|
|
65
|
+
function toHex(x: number) {
|
|
66
|
+
const hex = x.toString(16)
|
|
67
|
+
return hex.length === 1 ? '0' + hex : hex
|
|
68
|
+
}
|
|
69
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PRESET_COLORS: string[] = [
|
|
73
|
+
'#f87171', '#fb923c', '#fbbf24', '#facc15', '#a3e635', '#4ade80', '#34d399', '#2dd4bf',
|
|
74
|
+
'#38bdf8', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9', '#f472b6', '#fb7185'
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
function SaturationPicker({ hsv, onChange }: { hsv: { h: number, s: number, v: number }, onChange: (s: number, v: number) => void }) {
|
|
78
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
79
|
+
|
|
80
|
+
function handleMove(e: MouseEvent | React.MouseEvent) {
|
|
81
|
+
if (!containerRef.current) return
|
|
82
|
+
const { left, top, width, height } = containerRef.current.getBoundingClientRect()
|
|
83
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1)
|
|
84
|
+
const y = Math.min(Math.max((e.clientY - top) / height, 0), 1)
|
|
85
|
+
|
|
86
|
+
onChange(x * 100, (1 - y) * 100)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleMouseDown(e: React.MouseEvent) {
|
|
90
|
+
handleMove(e)
|
|
91
|
+
function moveHandler(e: MouseEvent) { handleMove(e) }
|
|
92
|
+
function upHandler() {
|
|
93
|
+
window.removeEventListener('mousemove', moveHandler)
|
|
94
|
+
window.removeEventListener('mouseup', upHandler)
|
|
95
|
+
}
|
|
96
|
+
window.addEventListener('mousemove', moveHandler)
|
|
97
|
+
window.addEventListener('mouseup', upHandler)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const bgColor = hsvToHex(hsv.h, 100, 100)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={containerRef}
|
|
105
|
+
className='w-full h-32 relative rounded-md overflow-hidden cursor-crosshair mb-3 select-none'
|
|
106
|
+
style={{ backgroundColor: bgColor }}
|
|
107
|
+
onMouseDown={handleMouseDown}
|
|
108
|
+
>
|
|
109
|
+
<div className='absolute inset-0 bg-linear-to-r from-white to-transparent' />
|
|
110
|
+
<div className='absolute inset-0 bg-linear-to-t from-black to-transparent' />
|
|
111
|
+
<div
|
|
112
|
+
className={`
|
|
113
|
+
absolute w-3 h-3 border-2 border-white rounded-full
|
|
114
|
+
shadow-md -translate-x-1/2 -translate-y-1/2 pointer-events-none
|
|
115
|
+
`}
|
|
116
|
+
style={{ left: `${hsv.s}%`, top: `${100 - hsv.v}%` }}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function HuePicker({ hue, onChange }: { hue: number, onChange: (h: number) => void }) {
|
|
123
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
124
|
+
|
|
125
|
+
function handleMove(e: MouseEvent | React.MouseEvent) {
|
|
126
|
+
if (!containerRef.current) return
|
|
127
|
+
const { left, width } = containerRef.current.getBoundingClientRect()
|
|
128
|
+
const x = Math.min(Math.max((e.clientX - left) / width, 0), 1)
|
|
129
|
+
onChange(x * 360)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleMouseDown(e: React.MouseEvent) {
|
|
133
|
+
handleMove(e)
|
|
134
|
+
function moveHandler(e: MouseEvent) { handleMove(e) }
|
|
135
|
+
function upHandler() {
|
|
136
|
+
window.removeEventListener('mousemove', moveHandler)
|
|
137
|
+
window.removeEventListener('mouseup', upHandler)
|
|
138
|
+
}
|
|
139
|
+
window.addEventListener('mousemove', moveHandler)
|
|
140
|
+
window.addEventListener('mouseup', upHandler)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
ref={containerRef}
|
|
146
|
+
className='w-full h-3 relative rounded-full cursor-pointer mb-4 select-none'
|
|
147
|
+
style={{ background: 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)' }}
|
|
148
|
+
onMouseDown={handleMouseDown}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
className={`
|
|
152
|
+
absolute w-4 h-4 bg-white border border-gray-200
|
|
153
|
+
rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2
|
|
154
|
+
top-1/2 pointer-events-none
|
|
155
|
+
`}
|
|
156
|
+
style={{ left: `${(hue / 360) * 100}%` }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default function ColorPickerPopup({ value, onChange, onClose }: ColorPickerPopupProps): JSX.Element {
|
|
163
|
+
const [hsv, setHsv] = useState(() => hexToHsv(value || '#000000'))
|
|
164
|
+
const [hexInput, setHexInput] = useState(value || '#000000')
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (value && value !== hexInput) {
|
|
168
|
+
setHsv(hexToHsv(value))
|
|
169
|
+
setHexInput(value)
|
|
170
|
+
}
|
|
171
|
+
}, [value])
|
|
172
|
+
|
|
173
|
+
function handleColorChange(newHsv: { h: number, s: number, v: number }) {
|
|
174
|
+
setHsv(newHsv)
|
|
175
|
+
const hex = hsvToHex(newHsv.h, newHsv.s, newHsv.v)
|
|
176
|
+
setHexInput(hex)
|
|
177
|
+
onChange(hex)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleSaturationChange(s: number, v: number) { handleColorChange({ ...hsv, s, v }) }
|
|
181
|
+
function handleHueChange(h: number) { handleColorChange({ ...hsv, h }) }
|
|
182
|
+
|
|
183
|
+
function manualHexChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
184
|
+
const val = e.target.value
|
|
185
|
+
setHexInput(val)
|
|
186
|
+
if (/^#[0-9A-F]{6}$/i.test(val)) {
|
|
187
|
+
const newHsv = hexToHsv(val)
|
|
188
|
+
setHsv(newHsv)
|
|
189
|
+
onChange(val)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className='absolute top-full left-0 mt-1 z-50 bg-login-600 border border-login-500 rounded-md shadow-lg p-3 w-64 select-none'>
|
|
195
|
+
<SaturationPicker hsv={hsv} onChange={handleSaturationChange} />
|
|
196
|
+
<HuePicker hue={hsv.h} onChange={handleHueChange} />
|
|
197
|
+
|
|
198
|
+
<div className='flex items-center gap-2 mb-3'>
|
|
199
|
+
<div className='text-xs text-login-200 font-mono'>HEX</div>
|
|
200
|
+
<input
|
|
201
|
+
type='text'
|
|
202
|
+
value={hexInput}
|
|
203
|
+
onChange={manualHexChange}
|
|
204
|
+
className={`
|
|
205
|
+
flex-1 min-w-0 bg-login-500 border border-login-500 rounded
|
|
206
|
+
px-2 py-1 text-sm text-login-text focus:outline-none
|
|
207
|
+
focus:border-login focus:ring-1 focus:ring-login
|
|
208
|
+
`}
|
|
209
|
+
spellCheck={false}
|
|
210
|
+
/>
|
|
211
|
+
<div
|
|
212
|
+
className='w-8 h-8 rounded border border-login-500 shrink-0'
|
|
213
|
+
style={{ backgroundColor: hexInput }}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className='grid grid-cols-8 gap-1.5 pt-3 border-t border-login-500'>
|
|
218
|
+
{PRESET_COLORS.map(color => (
|
|
219
|
+
<button
|
|
220
|
+
key={color}
|
|
221
|
+
type='button'
|
|
222
|
+
className={`
|
|
223
|
+
w-6 h-6 rounded-sm cursor-pointer hover:scale-110
|
|
224
|
+
hover:zIndex-10 transition-transform ring-1 ring-inset ring-black/10
|
|
225
|
+
`}
|
|
226
|
+
style={{ backgroundColor: color }}
|
|
227
|
+
onClick={() => {
|
|
228
|
+
const newHsv = hexToHsv(color)
|
|
229
|
+
setHsv(newHsv)
|
|
230
|
+
setHexInput(color)
|
|
231
|
+
onChange(color)
|
|
232
|
+
onClose()
|
|
233
|
+
}}
|
|
234
|
+
title={color}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
@@ -3,3 +3,4 @@ export { default as SelectionWrapper } from './selectionWrapper'
|
|
|
3
3
|
export { default as InputLabel } from './inputLabel'
|
|
4
4
|
export { default as InputInfo } from './inputInfo'
|
|
5
5
|
export { default as InputError } from './inputError'
|
|
6
|
+
export { default as ColorPickerPopup } from './colorPickerPopup'
|
|
@@ -1,51 +1,34 @@
|
|
|
1
|
-
import { type ChangeEvent } from 'react'
|
|
2
1
|
import { SelectionWrapper } from './shared'
|
|
3
2
|
|
|
4
|
-
export type SwitchProps = {
|
|
5
|
-
label?: string
|
|
3
|
+
export type SwitchProps = Omit<React.ComponentProps<'input'>, 'name'> & {
|
|
6
4
|
name: string
|
|
7
|
-
|
|
8
|
-
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
|
9
|
-
className?: string
|
|
10
|
-
disabled?: boolean
|
|
5
|
+
label?: string
|
|
11
6
|
error?: string
|
|
12
7
|
info?: string
|
|
13
|
-
|
|
8
|
+
className?: string
|
|
14
9
|
switchOnly?: boolean
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
export default function Switch({
|
|
18
|
-
label,
|
|
19
|
-
|
|
20
|
-
checked,
|
|
21
|
-
onChange,
|
|
22
|
-
className,
|
|
23
|
-
disabled,
|
|
24
|
-
error,
|
|
25
|
-
info,
|
|
26
|
-
required,
|
|
27
|
-
switchOnly,
|
|
28
|
-
}: SwitchProps) {
|
|
12
|
+
export default function Switch(props: SwitchProps) {
|
|
13
|
+
const { name, label, error, info, className, switchOnly, ...inputProps } = props
|
|
14
|
+
|
|
29
15
|
return (
|
|
30
16
|
<SelectionWrapper
|
|
31
17
|
label={label}
|
|
32
18
|
name={name}
|
|
33
|
-
required={required}
|
|
19
|
+
required={inputProps.required}
|
|
34
20
|
info={info}
|
|
35
21
|
error={error}
|
|
36
22
|
hideError={switchOnly}
|
|
37
23
|
className={className}
|
|
38
|
-
disabled={disabled}
|
|
24
|
+
disabled={inputProps.disabled}
|
|
39
25
|
>
|
|
40
26
|
<label className={`relative inline-flex items-center cursor-pointer ${switchOnly ? 'h-fit' : 'h-10.5'}`}>
|
|
41
27
|
<input
|
|
28
|
+
{...inputProps}
|
|
42
29
|
type='checkbox'
|
|
43
30
|
id={name}
|
|
44
31
|
name={name}
|
|
45
|
-
checked={checked}
|
|
46
|
-
onChange={onChange}
|
|
47
|
-
disabled={disabled}
|
|
48
|
-
required={required}
|
|
49
32
|
className='sr-only peer'
|
|
50
33
|
/>
|
|
51
34
|
<div className={`
|
|
@@ -54,7 +37,7 @@ export default function Switch({
|
|
|
54
37
|
after:content-[''] after:absolute ${switchOnly ? 'after:top-0.5' : 'after:top-2.75'} after:left-0.5
|
|
55
38
|
after:bg-white after:border-gray-300 after:border after:rounded-full
|
|
56
39
|
after:h-5 after:w-5 after:transition-all peer-checked:bg-login
|
|
57
|
-
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
40
|
+
${inputProps.disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
58
41
|
${error ? 'ring-1 ring-red-500' : ''}
|
|
59
42
|
`}></div>
|
|
60
43
|
</label>
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react'
|
|
2
2
|
import ReactMarkdown from 'react-markdown'
|
|
3
3
|
import { Eye, Pencil } from 'lucide-react'
|
|
4
4
|
import { FieldWrapper } from './shared'
|
|
5
5
|
|
|
6
|
-
export type TextareaProps = {
|
|
7
|
-
label?: string
|
|
6
|
+
export type TextareaProps = Omit<React.ComponentProps<'textarea'>, 'name'> & {
|
|
8
7
|
name: string
|
|
9
|
-
|
|
10
|
-
type?: 'markdown' | 'json' | 'text'
|
|
11
|
-
value?: string
|
|
12
|
-
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
|
8
|
+
label?: string
|
|
13
9
|
error?: string
|
|
14
10
|
className?: string
|
|
15
|
-
disabled?: boolean
|
|
16
|
-
required?: boolean
|
|
17
|
-
rows?: number
|
|
18
11
|
info?: string
|
|
12
|
+
type?: 'markdown' | 'json' | 'text'
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
function isValidJson(str: string): string | null {
|
|
@@ -27,30 +21,19 @@ function isValidJson(str: string): string | null {
|
|
|
27
21
|
}
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
export default function Textarea({
|
|
31
|
-
label,
|
|
32
|
-
|
|
33
|
-
placeholder,
|
|
34
|
-
value,
|
|
35
|
-
onChange,
|
|
36
|
-
error,
|
|
37
|
-
className,
|
|
38
|
-
disabled,
|
|
39
|
-
required,
|
|
40
|
-
rows = 4,
|
|
41
|
-
info,
|
|
42
|
-
type = 'text',
|
|
43
|
-
}: TextareaProps) {
|
|
24
|
+
export default function Textarea(props: TextareaProps) {
|
|
25
|
+
const { name, label, error, className, info, type = 'text', rows = 4, ...textareaProps } = props
|
|
26
|
+
const { value } = textareaProps
|
|
44
27
|
const [preview, setPreview] = useState(false)
|
|
45
28
|
|
|
46
|
-
const jsonError = type === 'json' && value ? isValidJson(value) : undefined
|
|
29
|
+
const jsonError = type === 'json' && value ? isValidJson(value as string) : undefined
|
|
47
30
|
const displayError = jsonError || error
|
|
48
31
|
|
|
49
32
|
return (
|
|
50
33
|
<FieldWrapper
|
|
51
34
|
label={label}
|
|
52
35
|
name={name}
|
|
53
|
-
required={required}
|
|
36
|
+
required={textareaProps.required}
|
|
54
37
|
info={info}
|
|
55
38
|
error={displayError}
|
|
56
39
|
className={className}
|
|
@@ -80,17 +63,13 @@ export default function Textarea({
|
|
|
80
63
|
`}
|
|
81
64
|
style={{ minHeight: `${rows * 1.5}rem` }}
|
|
82
65
|
>
|
|
83
|
-
<ReactMarkdown>{value || ''}</ReactMarkdown>
|
|
66
|
+
<ReactMarkdown>{String(value || '')}</ReactMarkdown>
|
|
84
67
|
</div>
|
|
85
68
|
) : (
|
|
86
69
|
<textarea
|
|
70
|
+
{...textareaProps}
|
|
87
71
|
id={name}
|
|
88
72
|
name={name}
|
|
89
|
-
placeholder={placeholder}
|
|
90
|
-
value={value}
|
|
91
|
-
onChange={onChange}
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
required={required}
|
|
94
73
|
rows={rows}
|
|
95
74
|
title={label}
|
|
96
75
|
aria-invalid={!!error}
|