uibee 2.6.0 → 2.7.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.
Files changed (74) hide show
  1. package/dist/src/components/buttons/button.js +1 -1
  2. package/dist/src/components/index.d.ts +5 -3
  3. package/dist/src/components/index.js +5 -3
  4. package/dist/src/components/inputs/checkbox.d.ts +13 -0
  5. package/dist/src/components/inputs/checkbox.js +17 -0
  6. package/dist/src/components/inputs/input.d.ts +11 -9
  7. package/dist/src/components/inputs/input.js +76 -9
  8. package/dist/src/components/inputs/radio.d.ts +14 -0
  9. package/dist/src/components/inputs/radio.js +17 -0
  10. package/dist/src/components/inputs/range.d.ts +17 -0
  11. package/dist/src/components/inputs/range.js +20 -0
  12. package/dist/src/components/inputs/select.d.ts +11 -11
  13. package/dist/src/components/inputs/select.js +54 -50
  14. package/dist/src/components/inputs/shared/dateTimePickerPopup.d.ts +8 -0
  15. package/dist/src/components/inputs/shared/dateTimePickerPopup.js +117 -0
  16. package/dist/src/components/inputs/shared/fieldWrapper.d.ts +12 -0
  17. package/dist/src/components/inputs/shared/fieldWrapper.js +7 -0
  18. package/dist/src/components/inputs/shared/index.d.ts +5 -0
  19. package/dist/src/components/inputs/shared/index.js +5 -0
  20. package/dist/src/components/inputs/shared/inputError.d.ts +6 -0
  21. package/dist/src/components/inputs/shared/inputError.js +6 -0
  22. package/dist/src/components/inputs/shared/inputInfo.d.ts +5 -0
  23. package/dist/src/components/inputs/shared/inputInfo.js +5 -0
  24. package/dist/src/components/inputs/shared/inputLabel.d.ts +9 -0
  25. package/dist/src/components/inputs/shared/inputLabel.js +4 -0
  26. package/dist/src/components/inputs/shared/selectionWrapper.d.ts +13 -0
  27. package/dist/src/components/inputs/shared/selectionWrapper.js +7 -0
  28. package/dist/src/components/inputs/switch.d.ts +10 -7
  29. package/dist/src/components/inputs/switch.js +13 -5
  30. package/dist/src/components/inputs/textarea.d.ts +15 -0
  31. package/dist/src/components/inputs/textarea.js +14 -0
  32. package/dist/src/components/logo/logo.js +1 -1
  33. package/dist/src/globals.css +386 -161
  34. package/dist/src/hooks/index.d.ts +1 -0
  35. package/dist/src/hooks/index.js +1 -0
  36. package/dist/src/hooks/useClickOutside.d.ts +1 -0
  37. package/dist/src/hooks/useClickOutside.js +20 -0
  38. package/eslint.config.js +1 -0
  39. package/package.json +3 -2
  40. package/src/components/buttons/button.tsx +1 -1
  41. package/src/components/index.ts +5 -3
  42. package/src/components/inputs/checkbox.tsx +66 -0
  43. package/src/components/inputs/input.tsx +137 -35
  44. package/src/components/inputs/radio.tsx +67 -0
  45. package/src/components/inputs/range.tsx +84 -0
  46. package/src/components/inputs/select.tsx +137 -172
  47. package/src/components/inputs/shared/dateTimePickerPopup.tsx +219 -0
  48. package/src/components/inputs/shared/fieldWrapper.tsx +44 -0
  49. package/src/components/inputs/shared/index.ts +5 -0
  50. package/src/components/inputs/shared/inputError.tsx +21 -0
  51. package/src/components/inputs/shared/inputInfo.tsx +17 -0
  52. package/src/components/inputs/shared/inputLabel.tsx +19 -0
  53. package/src/components/inputs/shared/selectionWrapper.tsx +47 -0
  54. package/src/components/inputs/switch.tsx +48 -25
  55. package/src/components/inputs/textarea.tsx +65 -0
  56. package/src/components/logo/logo.tsx +1 -1
  57. package/src/globals.css +36 -0
  58. package/src/hooks/index.ts +1 -0
  59. package/src/hooks/useClickOutside.ts +27 -0
  60. package/dist/src/components/inputs/erase.d.ts +0 -3
  61. package/dist/src/components/inputs/erase.js +0 -5
  62. package/dist/src/components/inputs/label.d.ts +0 -10
  63. package/dist/src/components/inputs/label.js +0 -13
  64. package/dist/src/components/inputs/markdown.d.ts +0 -15
  65. package/dist/src/components/inputs/markdown.js +0 -32
  66. package/dist/src/components/inputs/tag.d.ts +0 -11
  67. package/dist/src/components/inputs/tag.js +0 -44
  68. package/dist/src/components/inputs/tooltip.d.ts +0 -4
  69. package/dist/src/components/inputs/tooltip.js +0 -4
  70. package/src/components/inputs/erase.tsx +0 -13
  71. package/src/components/inputs/label.tsx +0 -31
  72. package/src/components/inputs/markdown.tsx +0 -129
  73. package/src/components/inputs/tag.tsx +0 -137
  74. package/src/components/inputs/tooltip.tsx +0 -12
@@ -1,202 +1,167 @@
1
1
  'use client'
2
2
 
3
- import { useRef, useState } from 'react'
4
- import ToolTip from './tooltip'
5
- import Label from './label'
6
- import EraseButton from './erase'
3
+ import { useState, useEffect } from 'react'
4
+ import { useClickOutside } from '../../hooks'
5
+ import { ChevronDown, X } from 'lucide-react'
6
+ import { FieldWrapper } from './shared'
7
7
 
8
- type Option = {
9
- value: string | number;
10
- label: string;
11
- image?: string
8
+ export type Option = {
9
+ value: string | number
10
+ label: string
12
11
  }
13
12
 
14
-
15
- type SelectProps = {
13
+ export type SelectProps = {
14
+ label?: string
16
15
  name: string
17
- label: string
18
- value: string | number
19
- setValue: (_: string | number) => void
16
+ value?: string | number | null
17
+ onChange?: (value: string | number | null) => void
20
18
  options: Option[]
19
+ error?: string
21
20
  className?: string
22
- tooltip?: string
21
+ disabled?: boolean
23
22
  required?: boolean
24
- children?: React.ReactNode
25
- color?: string
26
- }
27
-
28
- type SelectedOptionProps = {
29
- value: string | number
30
- selectedOption: Option | undefined
23
+ placeholder?: string
24
+ info?: string
25
+ clearable?: boolean
31
26
  }
32
27
 
33
28
  export default function Select({
34
- name,
35
29
  label,
30
+ name,
36
31
  value,
32
+ onChange,
37
33
  options,
34
+ error,
38
35
  className,
39
- tooltip,
36
+ disabled,
40
37
  required,
41
- children,
42
- setValue,
43
- color
38
+ placeholder = 'Select an option',
39
+ info,
40
+ clearable = true,
44
41
  }: SelectProps) {
45
- const [hasBlured, setHasBlured] = useState(false)
46
- const selectRef = useRef<HTMLSelectElement | null>(null)
47
- const selectedOption = options.find((o) => o.value === value)
42
+ const [isOpen, setIsOpen] = useState(false)
43
+ const [selectedOption, setSelectedOption] = useState<Option | undefined>(
44
+ options.find(opt => opt.value === value)
45
+ )
48
46
 
49
- function handleChoose(value: string | number) {
50
- setValue(value)
51
- setHasBlured(true)
52
- if (selectRef.current) {
53
- selectRef.current.value = String(value)
54
- selectRef.current.blur()
55
- }
56
- }
47
+ useEffect(() => {
48
+ setSelectedOption(options.find(opt => opt.value === value))
49
+ }, [value, options])
57
50
 
58
- return (
59
- <div className={`w-full ${className}`}>
60
- <div className='relative flex items-center'>
61
- <select
62
- ref={selectRef}
63
- name={name}
64
- className={
65
- 'peer cursor-pointer block px-2.5 pb-2.5 pt-4 ' +
66
- 'w-full text-sm rounded-lg border-[0.10rem] ' +
67
- 'appearance-none border-login-200 focus:ring-0 ' +
68
- 'focus:outline-none focus:border-login-50 ' +
69
- `${color ? color : 'bg-login-800'}`
70
- }
71
- value={value}
72
- onChange={(e) => {
73
- setValue(e.target.value)
74
- setHasBlured(true)
75
- }}
76
- onBlur={() => setHasBlured(true)}
77
- onMouseDown={(e) => {
78
- e.preventDefault()
79
- selectRef.current?.focus()
80
- }}
81
- required={required}
82
- >
83
- <option value='' hidden />
84
- {options.map((option) => (
85
- <option key={option.value} value={option.value}>
86
- {option.label}
87
- </option>
88
- ))}
89
- </select>
51
+ const containerRef = useClickOutside<HTMLDivElement>(() => setIsOpen(false))
90
52
 
91
- <Label
92
- label={label}
93
- value={value}
94
- required={required}
95
- color={color}
96
- showRequired={required && !value && hasBlured}
97
- />
98
-
99
- {value && (
100
- <EraseButton
101
- setData={(v: string) => {
102
- setValue(v)
103
- setHasBlured(true)
104
- }}
105
- />
106
- )}
107
- {!value && tooltip && <ToolTip info={tooltip} />}
53
+ const handleSelect = (option: Option) => {
54
+ if (disabled) return
55
+ setSelectedOption(option)
56
+ setIsOpen(false)
57
+ if (onChange) {
58
+ onChange(option.value)
59
+ }
60
+ }
108
61
 
109
- <SelectContent
110
- options={options}
111
- value={value}
112
- selectedOption={selectedOption}
113
- handleChoose={handleChoose}
114
- color={color}
115
- />
116
- </div>
117
- {children}
118
- </div>
119
- )
120
- }
62
+ const handleClear = (e: React.MouseEvent) => {
63
+ e.stopPropagation()
64
+ if (disabled) return
65
+ setSelectedOption(undefined)
66
+ if (onChange) {
67
+ onChange(null)
68
+ }
69
+ }
121
70
 
122
- function SelectContent({
123
- options,
124
- value,
125
- selectedOption,
126
- handleChoose,
127
- color
128
- }: {
129
- options: Option[]
130
- value: string | number
131
- selectedOption: Option | undefined
132
- handleChoose: (value: string | number) => void
133
- color?: string
134
- }) {
135
71
  return (
136
- <div
137
- className={
138
- 'hidden peer-focus:block absolute left-0 ' +
139
- 'right-0 top-full mt-1 z-50'
140
- }
72
+ <FieldWrapper
73
+ label={label}
74
+ name={name}
75
+ required={required}
76
+ info={info}
77
+ error={error}
78
+ className={className}
141
79
  >
142
- <div
143
- className={
144
- `${color ? color : 'bg-login-800'}` + ' border-[0.10rem] border-login-200 ' +
145
- 'rounded-lg shadow-lg p-0 max-h-72 overflow-hidden'
146
- }
147
- >
148
- <div className='max-h-72 overflow-auto'>
149
- <SelectedOption
150
- value={value}
151
- selectedOption={selectedOption}
152
- />
153
- <div className='p-2'>
154
- {options
155
- .filter((o) => o.value !== value)
156
- .map((opt) => (
157
- <button
158
- key={opt.value}
159
- type='button'
160
- className={
161
- 'cursor-pointer w-full flex ' +
162
- 'items-center gap-3 px-2 py-2 ' +
163
- 'text-sm hover:bg-surface ' +
164
- 'rounded hover:bg-login-600'
165
- }
166
- onMouseDown={(e) => {
167
- e.preventDefault()
168
- handleChoose(opt.value)
169
- }}
170
- >
171
- <span className='text-left'>
172
- {opt.label}
173
- </span>
174
- </button>
175
- ))}
80
+ <div className='relative' ref={containerRef}>
81
+ <button
82
+ type='button'
83
+ onClick={() => !disabled && setIsOpen(!isOpen)}
84
+ disabled={disabled}
85
+ aria-haspopup='listbox'
86
+ aria-expanded={isOpen}
87
+ aria-labelledby={label ? undefined : name}
88
+ className={`
89
+ w-full rounded-md bg-login-500/50 border border-login-500
90
+ text-login-text text-left
91
+ focus:outline-none focus:border-login focus:ring-1 focus:ring-login
92
+ disabled:opacity-50 disabled:cursor-not-allowed
93
+ py-2 pl-3 pr-10
94
+ transition-all duration-200
95
+ flex items-center justify-between
96
+ ${error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
97
+ ${!selectedOption ? 'text-login-200' : ''}
98
+ `}
99
+ title={label}
100
+ >
101
+ <span className='truncate'>
102
+ {selectedOption ? selectedOption.label : placeholder}
103
+ </span>
104
+ <div className='absolute inset-y-0 right-0 flex items-center px-2 gap-1'>
105
+ {clearable && selectedOption && !disabled && (
106
+ <div
107
+ role='button'
108
+ onClick={handleClear}
109
+ className={`
110
+ p-1 hover:bg-login-500 rounded-full text-login-200
111
+ hover:text-red-400 transition-colors cursor-pointer
112
+ `}
113
+ title='Clear selection'
114
+ >
115
+ <X className='w-3 h-3' />
116
+ </div>
117
+ )}
118
+ <div className={`
119
+ text-login-200 pointer-events-none
120
+ transition-transform duration-200
121
+ ${isOpen ? 'rotate-180' : ''}
122
+ `}>
123
+ <ChevronDown className='w-4 h-4' />
124
+ </div>
176
125
  </div>
177
- </div>
178
- </div>
179
- </div>
180
- )
181
- }
182
-
183
- function SelectedOption({ value, selectedOption }: SelectedOptionProps) {
184
- if (!value) {
185
- return <></>
186
- }
126
+ </button>
187
127
 
188
- return (
189
- <div
190
- className={
191
- 'sticky top-0 bg-surface px-2 py-2 z-10 border-b ' +
192
- 'border-login-200 bg-login-600'
193
- }
194
- >
195
- <div className='flex items-center gap-3'>
196
- <span className='font-medium text-left'>
197
- {selectedOption?.label}
198
- </span>
128
+ {isOpen && (
129
+ <div className={`
130
+ absolute z-50 w-full mt-1 bg-login-600 border border-login-500
131
+ rounded-md shadow-lg max-h-60 overflow-auto noscroll
132
+ `}>
133
+ {options.length > 0 ? (
134
+ <ul className='py-1' role='listbox'>
135
+ {options.map((option) => (
136
+ <li key={option.value} role='option' aria-selected={selectedOption?.value === option.value}>
137
+ <button
138
+ type='button'
139
+ onClick={() => handleSelect(option)}
140
+ className={`
141
+ w-full text-left px-3 py-2 text-sm
142
+ hover:bg-login-500 transition-colors duration-150
143
+ ${selectedOption?.value === option.value ? 'bg-login-500 text-login' : 'text-login-text'}
144
+ `}
145
+ >
146
+ {option.label}
147
+ </button>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ ) : (
152
+ <div className='px-3 py-2 text-sm text-login-200'>
153
+ No options available
154
+ </div>
155
+ )}
156
+ </div>
157
+ )}
199
158
  </div>
200
- </div>
159
+ <input
160
+ type='hidden'
161
+ name={name}
162
+ value={selectedOption?.value || ''}
163
+ required={required}
164
+ />
165
+ </FieldWrapper>
201
166
  )
202
167
  }
@@ -0,0 +1,219 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+
4
+ type DateTimePickerPopupProps = {
5
+ value: Date | null
6
+ onChange: (date: Date) => void
7
+ type: 'date' | 'time' | 'datetime-local'
8
+ onClose?: () => void
9
+ }
10
+
11
+ const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
12
+ const MONTHS = [
13
+ 'January', 'February', 'March', 'April', 'May', 'June',
14
+ 'July', 'August', 'September', 'October', 'November', 'December'
15
+ ]
16
+
17
+ export default function DateTimePickerPopup({
18
+ value,
19
+ onChange,
20
+ type,
21
+ onClose,
22
+ }: DateTimePickerPopupProps) {
23
+ const [currentDate, setCurrentDate] = useState(new Date())
24
+ const [timeInput, setTimeInput] = useState({
25
+ hours: value ? value.getHours().toString() : '0',
26
+ minutes: value ? value.getMinutes().toString() : '0',
27
+ })
28
+
29
+ useEffect(() => {
30
+ if (value) {
31
+ setCurrentDate(value)
32
+ setTimeInput(prev => ({
33
+ hours: prev.hours === '' && value.getHours() === 0 ? '' : value.getHours().toString(),
34
+ minutes: prev.minutes === '' && value.getMinutes() === 0 ? '' : value.getMinutes().toString(),
35
+ }))
36
+ }
37
+ }, [value])
38
+
39
+ const handleDateSelect = (day: number) => {
40
+ const newDate = new Date(currentDate)
41
+ newDate.setDate(day)
42
+
43
+ if (value) {
44
+ newDate.setHours(value.getHours())
45
+ newDate.setMinutes(value.getMinutes())
46
+ } else {
47
+ newDate.setHours(0, 0, 0, 0)
48
+ }
49
+
50
+ onChange(newDate)
51
+
52
+ if (type === 'date' && onClose) {
53
+ onClose()
54
+ }
55
+ }
56
+
57
+ const handleTimeChange = (timeUnit: 'hours' | 'minutes', val: number) => {
58
+ const newDate = value ? new Date(value) : new Date()
59
+ if (!value) {
60
+ newDate.setHours(0, 0, 0, 0)
61
+ }
62
+
63
+ if (timeUnit === 'hours') {
64
+ if (val < 0 || val > 23) return
65
+ newDate.setHours(val)
66
+ }
67
+ if (timeUnit === 'minutes') {
68
+ if (val < 0 || val > 59) return
69
+ newDate.setMinutes(val)
70
+ }
71
+
72
+ onChange(newDate)
73
+ }
74
+
75
+ const onTimeInputChange = (unit: 'hours' | 'minutes', val: string) => {
76
+ if (val === '') {
77
+ setTimeInput(prev => ({ ...prev, [unit]: '' }))
78
+ handleTimeChange(unit, 0)
79
+ return
80
+ }
81
+
82
+ if (!/^\d+$/.test(val)) return
83
+
84
+ const num = parseInt(val)
85
+
86
+ if (unit === 'hours' && num > 23) return
87
+ if (unit === 'minutes' && num > 59) return
88
+
89
+ setTimeInput(prev => ({ ...prev, [unit]: val }))
90
+ handleTimeChange(unit, num)
91
+ }
92
+
93
+ const onTimeInputBlur = (unit: 'hours' | 'minutes') => {
94
+ if (timeInput[unit] === '') {
95
+ const num = unit === 'hours' ? (value?.getHours() ?? 0) : (value?.getMinutes() ?? 0)
96
+ setTimeInput(prev => ({ ...prev, [unit]: num.toString() }))
97
+ }
98
+ }
99
+
100
+ const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate()
101
+ const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay()
102
+
103
+ const renderCalendar = () => {
104
+ const year = currentDate.getFullYear()
105
+ const month = currentDate.getMonth()
106
+ const daysInMonth = getDaysInMonth(year, month)
107
+ const firstDay = getFirstDayOfMonth(year, month)
108
+ const days = []
109
+
110
+ for (let i = 0; i < firstDay; i++) {
111
+ days.push(<div key={`empty-${i}`} className='w-8 h-8' />)
112
+ }
113
+
114
+ for (let i = 1; i <= daysInMonth; i++) {
115
+ const isSelected = value &&
116
+ value.getDate() === i &&
117
+ value.getMonth() === month &&
118
+ value.getFullYear() === year
119
+
120
+ const isToday = new Date().getDate() === i &&
121
+ new Date().getMonth() === month &&
122
+ new Date().getFullYear() === year
123
+
124
+ days.push(
125
+ <button
126
+ key={i}
127
+ type='button'
128
+ onClick={() => handleDateSelect(i)}
129
+ className={`
130
+ w-8 h-8 flex items-center justify-center rounded-full text-sm
131
+ hover:bg-login-500 transition-colors
132
+ ${isSelected ? 'bg-login! text-white! hover:bg-login!' : ''}
133
+ ${!isSelected && isToday ? 'text-login! font-bold' : ''}
134
+ ${!isSelected && !isToday ? 'text-login-text!' : ''}
135
+ `}
136
+ >
137
+ {i}
138
+ </button>
139
+ )
140
+ }
141
+
142
+ return (
143
+ <div className='p-2'>
144
+ <div className='flex items-center justify-between mb-2'>
145
+ <button
146
+ type='button'
147
+ onClick={() => setCurrentDate(new Date(year, month - 1))}
148
+ className='p-1 hover:bg-login-500 rounded-full text-login-text'
149
+ >
150
+ <ChevronLeft className='w-4 h-4' />
151
+ </button>
152
+ <span className='font-medium text-login-text'>
153
+ {MONTHS[month]} {year}
154
+ </span>
155
+ <button
156
+ type='button'
157
+ onClick={() => setCurrentDate(new Date(year, month + 1))}
158
+ className='p-1 hover:bg-login-500 rounded-full text-login-text'
159
+ >
160
+ <ChevronRight className='w-4 h-4' />
161
+ </button>
162
+ </div>
163
+ <div className='grid grid-cols-7 gap-1 mb-1'>
164
+ {DAYS.map(d => (
165
+ <div key={d} className='w-8 text-center text-xs text-login-200 font-medium'>
166
+ {d}
167
+ </div>
168
+ ))}
169
+ </div>
170
+ <div className='grid grid-cols-7 gap-1'>
171
+ {days}
172
+ </div>
173
+ </div>
174
+ )
175
+ }
176
+
177
+ const renderTimePicker = () => {
178
+ return (
179
+ <div className='p-2 border-t border-login-500 flex justify-center gap-2'>
180
+ <div className='flex flex-col items-center'>
181
+ <label className='text-xs text-login-200 mb-1'>Hour</label>
182
+ <input
183
+ type='text'
184
+ inputMode='numeric'
185
+ value={timeInput.hours}
186
+ onChange={(e) => onTimeInputChange('hours', e.target.value)}
187
+ onBlur={() => onTimeInputBlur('hours')}
188
+ className={`
189
+ w-16 p-1 bg-login-500 rounded text-center text-login-text
190
+ border border-login-500 focus:border-login outline-none
191
+ `}
192
+ />
193
+ </div>
194
+ <div className='flex items-end pb-2 text-login-text'>:</div>
195
+ <div className='flex flex-col items-center'>
196
+ <label className='text-xs text-login-200 mb-1'>Minute</label>
197
+ <input
198
+ type='text'
199
+ inputMode='numeric'
200
+ value={timeInput.minutes}
201
+ onChange={(e) => onTimeInputChange('minutes', e.target.value)}
202
+ onBlur={() => onTimeInputBlur('minutes')}
203
+ className={`
204
+ w-16 p-1 bg-login-500 rounded text-center text-login-text
205
+ border border-login-500 focus:border-login outline-none
206
+ `}
207
+ />
208
+ </div>
209
+ </div>
210
+ )
211
+ }
212
+
213
+ return (
214
+ <div className='absolute top-full left-0 z-50 mt-1 bg-login-600 border border-login-500 rounded-md shadow-lg p-1 min-w-70'>
215
+ {type !== 'time' && renderCalendar()}
216
+ {(type === 'time' || type === 'datetime-local') && renderTimePicker()}
217
+ </div>
218
+ )
219
+ }
@@ -0,0 +1,44 @@
1
+ import { ReactNode } from 'react'
2
+ import InputLabel from './inputLabel'
3
+ import InputInfo from './inputInfo'
4
+ import InputError from './inputError'
5
+
6
+ interface FieldWrapperProps {
7
+ label?: string
8
+ name: string
9
+ required?: boolean
10
+ info?: string
11
+ error?: string
12
+ children: ReactNode
13
+ className?: string
14
+ }
15
+
16
+ export default function FieldWrapper({
17
+ label,
18
+ name,
19
+ required,
20
+ info,
21
+ error,
22
+ children,
23
+ className,
24
+ }: FieldWrapperProps) {
25
+ return (
26
+ <div className={`flex flex-col gap-1 w-full relative ${className || ''}`}>
27
+ {(label || info) && (
28
+ <div className='flex items-center justify-between mb-1'>
29
+ {label && (
30
+ <InputLabel
31
+ label={label}
32
+ name={name}
33
+ required={required}
34
+ className='ml-1'
35
+ />
36
+ )}
37
+ {info && <InputInfo info={info} />}
38
+ </div>
39
+ )}
40
+ {children}
41
+ <InputError error={error} id={`${name}-error`} />
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,5 @@
1
+ export { default as FieldWrapper } from './fieldWrapper'
2
+ export { default as SelectionWrapper } from './selectionWrapper'
3
+ export { default as InputLabel } from './inputLabel'
4
+ export { default as InputInfo } from './inputInfo'
5
+ export { default as InputError } from './inputError'
@@ -0,0 +1,21 @@
1
+ interface InputErrorProps {
2
+ error?: string
3
+ id?: string
4
+ }
5
+
6
+ export default function InputError({ error, id }: InputErrorProps) {
7
+ if (!error) return <div className='h-4' />
8
+
9
+ return (
10
+ <div className='h-4'>
11
+ <span
12
+ id={id}
13
+ className='text-xs text-red-500 ml-1 truncate block'
14
+ role='alert'
15
+ title={error}
16
+ >
17
+ {error}
18
+ </span>
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,17 @@
1
+ import { CircleHelp } from 'lucide-react'
2
+
3
+ interface InputInfoProps {
4
+ info: string
5
+ }
6
+
7
+ export default function InputInfo({ info }: InputInfoProps) {
8
+ return (
9
+ <div
10
+ className='text-login-200 hover:text-login-text transition-colors'
11
+ aria-label={info}
12
+ title={info}
13
+ >
14
+ <CircleHelp className='w-4 h-4' />
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,19 @@
1
+ interface InputLabelProps {
2
+ label: string
3
+ name: string
4
+ required?: boolean
5
+ disabled?: boolean
6
+ className?: string
7
+ }
8
+
9
+ export default function InputLabel({ label, name, required, disabled, className }: InputLabelProps) {
10
+ return (
11
+ <label
12
+ htmlFor={name}
13
+ className={`text-sm font-medium text-login-text ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className || ''}`}
14
+ title={label}
15
+ >
16
+ {label} {required && <span className='text-red-500'>*</span>}
17
+ </label>
18
+ )
19
+ }