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.
@@ -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
- checked?: boolean
8
- onChange?: (e: ChangeEvent<HTMLInputElement>) => void
9
- className?: string
10
- disabled?: boolean
5
+ label?: string
11
6
  error?: string
12
7
  info?: string
13
- required?: boolean
8
+ className?: string
14
9
  switchOnly?: boolean
15
10
  }
16
11
 
17
- export default function Switch({
18
- label,
19
- name,
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 { type ChangeEvent, useState } from 'react'
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
- placeholder?: string
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
- name,
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}