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.
- package/dist/src/components/buttons/button.js +1 -1
- package/dist/src/components/index.d.ts +5 -3
- package/dist/src/components/index.js +5 -3
- package/dist/src/components/inputs/checkbox.d.ts +13 -0
- package/dist/src/components/inputs/checkbox.js +17 -0
- package/dist/src/components/inputs/input.d.ts +11 -9
- package/dist/src/components/inputs/input.js +76 -9
- package/dist/src/components/inputs/radio.d.ts +14 -0
- package/dist/src/components/inputs/radio.js +17 -0
- package/dist/src/components/inputs/range.d.ts +17 -0
- package/dist/src/components/inputs/range.js +20 -0
- package/dist/src/components/inputs/select.d.ts +11 -11
- package/dist/src/components/inputs/select.js +54 -50
- package/dist/src/components/inputs/shared/dateTimePickerPopup.d.ts +8 -0
- package/dist/src/components/inputs/shared/dateTimePickerPopup.js +117 -0
- package/dist/src/components/inputs/shared/fieldWrapper.d.ts +12 -0
- package/dist/src/components/inputs/shared/fieldWrapper.js +7 -0
- package/dist/src/components/inputs/shared/index.d.ts +5 -0
- package/dist/src/components/inputs/shared/index.js +5 -0
- package/dist/src/components/inputs/shared/inputError.d.ts +6 -0
- package/dist/src/components/inputs/shared/inputError.js +6 -0
- package/dist/src/components/inputs/shared/inputInfo.d.ts +5 -0
- package/dist/src/components/inputs/shared/inputInfo.js +5 -0
- package/dist/src/components/inputs/shared/inputLabel.d.ts +9 -0
- package/dist/src/components/inputs/shared/inputLabel.js +4 -0
- package/dist/src/components/inputs/shared/selectionWrapper.d.ts +13 -0
- package/dist/src/components/inputs/shared/selectionWrapper.js +7 -0
- package/dist/src/components/inputs/switch.d.ts +10 -7
- package/dist/src/components/inputs/switch.js +13 -5
- package/dist/src/components/inputs/textarea.d.ts +15 -0
- package/dist/src/components/inputs/textarea.js +14 -0
- package/dist/src/components/logo/logo.js +1 -1
- package/dist/src/globals.css +386 -161
- package/dist/src/hooks/index.d.ts +1 -0
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/useClickOutside.d.ts +1 -0
- package/dist/src/hooks/useClickOutside.js +20 -0
- package/eslint.config.js +1 -0
- package/package.json +3 -2
- package/src/components/buttons/button.tsx +1 -1
- package/src/components/index.ts +5 -3
- package/src/components/inputs/checkbox.tsx +66 -0
- package/src/components/inputs/input.tsx +137 -35
- package/src/components/inputs/radio.tsx +67 -0
- package/src/components/inputs/range.tsx +84 -0
- package/src/components/inputs/select.tsx +137 -172
- package/src/components/inputs/shared/dateTimePickerPopup.tsx +219 -0
- package/src/components/inputs/shared/fieldWrapper.tsx +44 -0
- package/src/components/inputs/shared/index.ts +5 -0
- package/src/components/inputs/shared/inputError.tsx +21 -0
- package/src/components/inputs/shared/inputInfo.tsx +17 -0
- package/src/components/inputs/shared/inputLabel.tsx +19 -0
- package/src/components/inputs/shared/selectionWrapper.tsx +47 -0
- package/src/components/inputs/switch.tsx +48 -25
- package/src/components/inputs/textarea.tsx +65 -0
- package/src/components/logo/logo.tsx +1 -1
- package/src/globals.css +36 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useClickOutside.ts +27 -0
- package/dist/src/components/inputs/erase.d.ts +0 -3
- package/dist/src/components/inputs/erase.js +0 -5
- package/dist/src/components/inputs/label.d.ts +0 -10
- package/dist/src/components/inputs/label.js +0 -13
- package/dist/src/components/inputs/markdown.d.ts +0 -15
- package/dist/src/components/inputs/markdown.js +0 -32
- package/dist/src/components/inputs/tag.d.ts +0 -11
- package/dist/src/components/inputs/tag.js +0 -44
- package/dist/src/components/inputs/tooltip.d.ts +0 -4
- package/dist/src/components/inputs/tooltip.js +0 -4
- package/src/components/inputs/erase.tsx +0 -13
- package/src/components/inputs/label.tsx +0 -31
- package/src/components/inputs/markdown.tsx +0 -129
- package/src/components/inputs/tag.tsx +0 -137
- package/src/components/inputs/tooltip.tsx +0 -12
|
@@ -1,202 +1,167 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
-
|
|
13
|
+
export type SelectProps = {
|
|
14
|
+
label?: string
|
|
16
15
|
name: string
|
|
17
|
-
|
|
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
|
-
|
|
21
|
+
disabled?: boolean
|
|
23
22
|
required?: boolean
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
36
|
+
disabled,
|
|
40
37
|
required,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
placeholder = 'Select an option',
|
|
39
|
+
info,
|
|
40
|
+
clearable = true,
|
|
44
41
|
}: SelectProps) {
|
|
45
|
-
const [
|
|
46
|
-
const
|
|
47
|
-
|
|
42
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
43
|
+
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
|
|
44
|
+
options.find(opt => opt.value === value)
|
|
45
|
+
)
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
</
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function SelectedOption({ value, selectedOption }: SelectedOptionProps) {
|
|
184
|
-
if (!value) {
|
|
185
|
-
return <></>
|
|
186
|
-
}
|
|
126
|
+
</button>
|
|
187
127
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
+
}
|