nitro-web 0.0.116 → 0.0.118
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/client/css/components.css +12 -0
- package/client/index.ts +1 -0
- package/components/partials/element/calendar.tsx +4 -4
- package/components/partials/form/field-date.tsx +14 -84
- package/components/partials/form/field-time.tsx +197 -0
- package/components/partials/form/field.tsx +22 -4
- package/components/partials/form/select.tsx +1 -1
- package/components/partials/styleguide.tsx +6 -1
- package/package.json +1 -1
|
@@ -95,4 +95,16 @@
|
|
|
95
95
|
.loading-dots::after {
|
|
96
96
|
content: "";
|
|
97
97
|
animation: dots 2s steps(1, end) infinite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---- Scrollbar -------------------- */
|
|
101
|
+
|
|
102
|
+
.sm-scrollbar::-webkit-scrollbar {
|
|
103
|
+
width: 4px;
|
|
104
|
+
}
|
|
105
|
+
.sm-scrollbar::-webkit-scrollbar-thumb {
|
|
106
|
+
background-color: rgba(0,0,0,0.3);
|
|
107
|
+
}
|
|
108
|
+
.sm-scrollbar::-webkit-scrollbar-track {
|
|
109
|
+
background: rgba(0,0,0,0.05);
|
|
98
110
|
}
|
package/client/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ export { Field, isFieldCached, type FieldProps } from '../components/partials/fo
|
|
|
45
45
|
export { FieldColor, type FieldColorProps } from '../components/partials/form/field-color'
|
|
46
46
|
export { FieldCurrency, type FieldCurrencyProps } from '../components/partials/form/field-currency'
|
|
47
47
|
export { FieldDate, type FieldDateProps } from '../components/partials/form/field-date'
|
|
48
|
+
export { FieldTime, type FieldTimeProps } from '../components/partials/form/field-time'
|
|
48
49
|
export { Location } from '../components/partials/form/location'
|
|
49
50
|
export { Select, getSelectStyle, type SelectProps } from '../components/partials/form/select'
|
|
50
51
|
|
|
@@ -16,7 +16,7 @@ export type DayPickerProps = Omit<DayPickerPropsBase,
|
|
|
16
16
|
|
|
17
17
|
export type CalendarProps = DayPickerProps & {
|
|
18
18
|
mode?: Mode
|
|
19
|
-
onChange?: (
|
|
19
|
+
onChange?: (value: null|number|(null|number)[]) => void
|
|
20
20
|
value?: null|number|string|(null|number|string)[]
|
|
21
21
|
numberOfMonths?: number
|
|
22
22
|
month?: number // the value may be updated from an outside source, thus the month may have changed
|
|
@@ -48,17 +48,17 @@ export function Calendar({ mode='single', onChange, value, numberOfMonths, month
|
|
|
48
48
|
case 'single': {
|
|
49
49
|
const date = newDate as ModeSelection<'single'>
|
|
50
50
|
preserveTimeFn(date)
|
|
51
|
-
onChange?.(
|
|
51
|
+
onChange?.(date?.getTime() ?? null)
|
|
52
52
|
break
|
|
53
53
|
}
|
|
54
54
|
case 'range': {
|
|
55
55
|
const { from, to } = (newDate ?? {}) as ModeSelection<'range'>
|
|
56
|
-
onChange?.(
|
|
56
|
+
onChange?.(from ? [from.getTime() || null, to?.getTime() || null] : null)
|
|
57
57
|
break
|
|
58
58
|
}
|
|
59
59
|
case 'multiple': {
|
|
60
60
|
const dates = (newDate as ModeSelection<'multiple'>)?.filter(Boolean) ?? []
|
|
61
|
-
onChange?.(
|
|
61
|
+
onChange?.(dates.map((d) => d.getTime()))
|
|
62
62
|
break
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { format, isValid, parse } from 'date-fns'
|
|
3
3
|
import { getPrefixWidth } from 'nitro-web/util'
|
|
4
4
|
import { Calendar, Dropdown } from 'nitro-web'
|
|
5
|
-
import {
|
|
5
|
+
import { DayPickerProps } from '../element/calendar'
|
|
6
|
+
import { TimePicker } from './field-time'
|
|
6
7
|
|
|
7
8
|
type Mode = 'single' | 'multiple' | 'range'
|
|
8
9
|
type DropdownRef = {
|
|
@@ -42,11 +43,6 @@ export type FieldDateProps = (
|
|
|
42
43
|
})
|
|
43
44
|
)
|
|
44
45
|
|
|
45
|
-
type TimePickerProps = {
|
|
46
|
-
date: Date|null
|
|
47
|
-
onChange: (mode: Mode, value: number|null) => void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
export function FieldDate({
|
|
51
47
|
dir = 'bottom-left',
|
|
52
48
|
Icon,
|
|
@@ -59,6 +55,8 @@ export function FieldDate({
|
|
|
59
55
|
DayPickerProps,
|
|
60
56
|
...props
|
|
61
57
|
}: FieldDateProps) {
|
|
58
|
+
// Currently this displays the dates in local timezone and saves in utc. We should allow the user to display the dates in a
|
|
59
|
+
// different timezone.
|
|
62
60
|
const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
|
|
63
61
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
64
62
|
const dropdownRef = useRef<DropdownRef>(null)
|
|
@@ -72,7 +70,7 @@ export function FieldDate({
|
|
|
72
70
|
const onChange = onChangeProp ?? ((e: { target: { name: string, value: any } }) => setInternalValue(e.target.value))
|
|
73
71
|
|
|
74
72
|
// Convert the value to an array of valid* dates
|
|
75
|
-
const
|
|
73
|
+
const validDates = useMemo(() => {
|
|
76
74
|
const arrOfNumbers = typeof value === 'string'
|
|
77
75
|
? value.split(/\s*,\s*/g).map(o => parseFloat(o))
|
|
78
76
|
: Array.isArray(value) ? value : [value]
|
|
@@ -81,19 +79,19 @@ export function FieldDate({
|
|
|
81
79
|
}, [value])
|
|
82
80
|
|
|
83
81
|
// Hold the input value in state
|
|
84
|
-
const [inputValue, setInputValue] = useState(() => getInputValue(
|
|
82
|
+
const [inputValue, setInputValue] = useState(() => getInputValue(validDates))
|
|
85
83
|
|
|
86
84
|
// Update the date's inputValue (text) when the value changes outside of the component
|
|
87
85
|
useEffect(() => {
|
|
88
|
-
if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(
|
|
89
|
-
}, [
|
|
86
|
+
if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(validDates))
|
|
87
|
+
}, [validDates])
|
|
90
88
|
|
|
91
89
|
// Get the prefix content width
|
|
92
90
|
useEffect(() => {
|
|
93
91
|
setPrefixWidth(getPrefixWidth(prefix, 4))
|
|
94
92
|
}, [prefix])
|
|
95
93
|
|
|
96
|
-
function onCalendarChange(
|
|
94
|
+
function onCalendarChange(value: null|number|(null|number)[]) {
|
|
97
95
|
if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
98
96
|
setInputValue(getInputValue(value))
|
|
99
97
|
// Update the value
|
|
@@ -102,6 +100,7 @@ export function FieldDate({
|
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
103
|
+
// Calls onChange (should update state, thus updating the value) with "raw" values
|
|
105
104
|
setInputValue(e.target.value) // keep the input value in sync
|
|
106
105
|
|
|
107
106
|
let split = e.target.value.split(/-|,/).map(o => {
|
|
@@ -132,7 +131,7 @@ export function FieldDate({
|
|
|
132
131
|
const _dates = Array.isArray(value) ? value : [value]
|
|
133
132
|
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
134
133
|
}
|
|
135
|
-
|
|
134
|
+
|
|
136
135
|
function getOutputValue(value: Date|number|null|(Date|number|null)[]): any {
|
|
137
136
|
// console.log(value)
|
|
138
137
|
return value
|
|
@@ -149,13 +148,13 @@ export function FieldDate({
|
|
|
149
148
|
<div className="flex">
|
|
150
149
|
<Calendar
|
|
151
150
|
// Calendar actually accepts an array of dates, but the type is not typed correctly
|
|
152
|
-
{...{ mode: mode, value:
|
|
151
|
+
{...{ mode: mode, value: validDates as any, numberOfMonths: numberOfMonths, month: month }}
|
|
153
152
|
{...DayPickerProps}
|
|
154
153
|
preserveTime={!!showTime}
|
|
155
154
|
onChange={onCalendarChange}
|
|
156
155
|
className="pt-1 pb-2 px-3"
|
|
157
156
|
/>
|
|
158
|
-
{!!showTime && mode == 'single' && <TimePicker date={
|
|
157
|
+
{!!showTime && mode == 'single' && <TimePicker date={validDates?.[0] ?? undefined} onChange={onCalendarChange} />}
|
|
159
158
|
</div>
|
|
160
159
|
}
|
|
161
160
|
dir={dir}
|
|
@@ -175,7 +174,7 @@ export function FieldDate({
|
|
|
175
174
|
id={id}
|
|
176
175
|
autoComplete="off"
|
|
177
176
|
className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
178
|
-
onBlur={() => setInputValue(getInputValue(
|
|
177
|
+
onBlur={() => setInputValue(getInputValue(validDates))} // onChange should of updated the value -> validValue by this point
|
|
179
178
|
onChange={onInputChange}
|
|
180
179
|
style={{ textIndent: prefixWidth + 'px' }}
|
|
181
180
|
type="text"
|
|
@@ -185,72 +184,3 @@ export function FieldDate({
|
|
|
185
184
|
</Dropdown>
|
|
186
185
|
)
|
|
187
186
|
}
|
|
188
|
-
|
|
189
|
-
function TimePicker({ date, onChange }: TimePickerProps) {
|
|
190
|
-
const lists = [
|
|
191
|
-
[12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // hours
|
|
192
|
-
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55], // minutes
|
|
193
|
-
['AM', 'PM'], // AM/PM
|
|
194
|
-
]
|
|
195
|
-
|
|
196
|
-
// Get current values from date or use defaults
|
|
197
|
-
const hour = date ? parseInt(format(date, 'h')) : undefined
|
|
198
|
-
const minute = date ? parseInt(format(date, 'm')) : undefined
|
|
199
|
-
const period = date ? format(date, 'a') : undefined
|
|
200
|
-
|
|
201
|
-
const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
|
|
202
|
-
// Create a new date object from the current date or current time
|
|
203
|
-
const newDate = new Date(date || new Date())
|
|
204
|
-
|
|
205
|
-
if (type === 'hour') {
|
|
206
|
-
// Parse the time with the new hour value
|
|
207
|
-
const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
|
|
208
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
209
|
-
newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
|
|
210
|
-
} else if (type === 'minute') {
|
|
211
|
-
// Parse the time with the new minute value
|
|
212
|
-
const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
|
|
213
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
214
|
-
newDate.setMinutes(updatedDate.getMinutes())
|
|
215
|
-
} else if (type === 'period') {
|
|
216
|
-
// Parse the time with the new period value
|
|
217
|
-
const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
|
|
218
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
219
|
-
newDate.setHours(updatedDate.getHours())
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
onChange('single', newDate.getTime())
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
lists.map((list, i) => {
|
|
227
|
-
const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
|
|
228
|
-
const currentValue = i === 0 ? hour : i === 1 ? minute : period
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<div key={i} className="w-[60px] py-1 relative overflow-hidden hover:overflow-y-auto border-l border-gray-100">
|
|
232
|
-
<div className="w-[60px] absolute flex flex-col items-center">
|
|
233
|
-
{list.map(item => (
|
|
234
|
-
<div
|
|
235
|
-
className="py-1 flex group cursor-pointer"
|
|
236
|
-
key={item}
|
|
237
|
-
onClick={() => handleTimeChange(type, item)}
|
|
238
|
-
>
|
|
239
|
-
<button
|
|
240
|
-
key={item}
|
|
241
|
-
className={
|
|
242
|
-
`${dayButtonClassName} rounded-full flex justify-center items-center group-hover:bg-gray-100 `
|
|
243
|
-
+ (item === currentValue ? '!bg-input-border-focus text-white' : '')
|
|
244
|
-
}
|
|
245
|
-
onClick={() => handleTimeChange(type, item)}
|
|
246
|
-
>
|
|
247
|
-
{item.toString().padStart(2, '0').toLowerCase()}
|
|
248
|
-
</button>
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
)
|
|
254
|
-
})
|
|
255
|
-
)
|
|
256
|
-
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { format, parse } from 'date-fns'
|
|
2
|
+
import { Button, Dropdown } from 'nitro-web'
|
|
3
|
+
import { dayButtonClassName } from '../element/calendar'
|
|
4
|
+
|
|
5
|
+
type Timestamp = number // timestamp on epoch day
|
|
6
|
+
type DropdownRef = {
|
|
7
|
+
setIsActive: (value: boolean) => void
|
|
8
|
+
}
|
|
9
|
+
export type FieldTimeProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
10
|
+
name: string
|
|
11
|
+
id?: string
|
|
12
|
+
onChange?: (e: { target: { name: string, value: null|number } }) => void
|
|
13
|
+
value?: string | Timestamp;
|
|
14
|
+
Icon?: React.ReactNode
|
|
15
|
+
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
16
|
+
// tz?: string
|
|
17
|
+
}
|
|
18
|
+
type TimePickerProps = {
|
|
19
|
+
date?: Date
|
|
20
|
+
onChange: (value: Timestamp) => void
|
|
21
|
+
// tz?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function FieldTime({ onChange, value, Icon, dir = 'bottom-left', ...props }: FieldTimeProps) {
|
|
25
|
+
// time is viewed and set in local timezone, and saved as timestamp on epoch day.
|
|
26
|
+
// Note: timestamp is better than saving seconds so we can easily view this in a particular timezone
|
|
27
|
+
const localePattern = 'hh:mmaa'
|
|
28
|
+
const dropdownRef = useRef<DropdownRef>(null)
|
|
29
|
+
const id = props.id || props.name
|
|
30
|
+
|
|
31
|
+
// Convert the value to a valid time value
|
|
32
|
+
const validValue = useMemo(() => {
|
|
33
|
+
const num = typeof value === 'string' ? parseInt(value) : value
|
|
34
|
+
console.log(11, num)
|
|
35
|
+
return typeof num === 'number' && !isNaN(num) ? num : new Date(0).getTime()
|
|
36
|
+
}, [value])
|
|
37
|
+
|
|
38
|
+
// Hold the input value in state
|
|
39
|
+
const [inputValue, setInputValue] = useState(() => getInputValue(validValue))
|
|
40
|
+
|
|
41
|
+
function onTimePickerChange(value: Timestamp) {
|
|
42
|
+
setInputValue(getInputValue(value))
|
|
43
|
+
if (onChange) onChange({ target: { name: props.name, value: value }})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getInputValue(timestamp: Timestamp) {
|
|
47
|
+
// Get the input-value in local timezone
|
|
48
|
+
return typeof timestamp === 'number' ? format(new Date(timestamp), localePattern) : ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
52
|
+
// Assume the string is in local timezone, and calls onChange with "raw" values (should update state, thus updating the value).
|
|
53
|
+
setInputValue(e.target.value) // keep the input value in sync
|
|
54
|
+
const [, _hour, _minute, _second, _period] = e.target.value.match(/(\d{1,2}):(\d{2})(:\d{2})?\s*(am|pm)/i) || []
|
|
55
|
+
if (!_hour || !_minute) return
|
|
56
|
+
const hour24 = parseInt(_hour) < 12 && _period.match(/pm/i) ? parseInt(_hour) + 12 : parseInt(_hour)
|
|
57
|
+
const minute = parseInt(_minute)
|
|
58
|
+
|
|
59
|
+
// Assume the time string is in the local timezone, and convert to UTC date from epoch
|
|
60
|
+
const localDate = new Date(0)
|
|
61
|
+
localDate.setHours(hour24, minute, _second ? parseInt(_second) : 0, 0)
|
|
62
|
+
const value = localDate.getTime()
|
|
63
|
+
if (onChange) onChange({ target: { name: props.name, value: value }})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onNowClick() {
|
|
67
|
+
const epochDay = new Date(0)
|
|
68
|
+
const now = new Date()
|
|
69
|
+
// now set hours, minutes, seconds to now but on epoch day
|
|
70
|
+
epochDay.setHours(now.getHours(), now.getMinutes(), now.getSeconds(), 0)
|
|
71
|
+
onTimePickerChange(epochDay.getTime())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dropdown
|
|
76
|
+
ref={dropdownRef}
|
|
77
|
+
menuToggles={false}
|
|
78
|
+
// animate={false}
|
|
79
|
+
// menuIsOpen={true}
|
|
80
|
+
minWidth={0}
|
|
81
|
+
dir={dir}
|
|
82
|
+
menuContent={
|
|
83
|
+
<div>
|
|
84
|
+
<div className="flex justify-center h-[250px]">
|
|
85
|
+
<TimePicker date={new Date(validValue)} onChange={onTimePickerChange} />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex justify-between p-2 border-t border-gray-100">
|
|
88
|
+
<Button color="secondary" size="xs" onClick={() => onNowClick()}>Now</Button>
|
|
89
|
+
<Button color="primary" size="xs" onClick={() => dropdownRef.current?.setIsActive(false)}>Done</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
<div className="grid grid-cols-1">
|
|
95
|
+
{Icon}
|
|
96
|
+
<input
|
|
97
|
+
{...props}
|
|
98
|
+
id={id}
|
|
99
|
+
autoComplete="off"
|
|
100
|
+
className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
101
|
+
value={inputValue}
|
|
102
|
+
onChange={onInputChange}
|
|
103
|
+
onBlur={() => setInputValue(getInputValue(validValue))} // onChange should of updated the value -> validValue by this point
|
|
104
|
+
type="text"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</Dropdown>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function TimePicker({ date, onChange }: TimePickerProps) {
|
|
112
|
+
const lists = [
|
|
113
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // hours
|
|
114
|
+
[
|
|
115
|
+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
116
|
+
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
|
117
|
+
51, 52, 53, 54, 55, 56, 57, 58, 59,
|
|
118
|
+
], // minutes
|
|
119
|
+
['AM', 'PM'], // AM/PM
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
// Get current values from date or use defaults
|
|
123
|
+
const hour = date ? parseInt(format(date, 'h')) : undefined
|
|
124
|
+
const minute = date ? parseInt(format(date, 'm')) : undefined
|
|
125
|
+
const period = date ? format(date, 'a') : undefined
|
|
126
|
+
|
|
127
|
+
const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
|
|
128
|
+
// Creates a new date object in the local timezone, and calls onChange with the timestamp
|
|
129
|
+
const newDate = new Date(date || new Date())
|
|
130
|
+
|
|
131
|
+
if (type === 'hour') {
|
|
132
|
+
// Parse the time with the new hour value
|
|
133
|
+
const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
|
|
134
|
+
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
135
|
+
newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
|
|
136
|
+
} else if (type === 'minute') {
|
|
137
|
+
// Parse the time with the new minute value
|
|
138
|
+
const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
|
|
139
|
+
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
140
|
+
newDate.setMinutes(updatedDate.getMinutes())
|
|
141
|
+
} else if (type === 'period') {
|
|
142
|
+
// Parse the time with the new period value
|
|
143
|
+
const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
|
|
144
|
+
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
145
|
+
newDate.setHours(updatedDate.getHours())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onChange(newDate.getTime())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scrollIntoView(type: 'hour' | 'minute' | 'period', value: string | number, element: HTMLElement) {
|
|
152
|
+
const container = element?.parentElement?.parentElement
|
|
153
|
+
if (element && container) {
|
|
154
|
+
const topContainerPadding = 0
|
|
155
|
+
const scrollTop = element.offsetTop - container.offsetTop - topContainerPadding
|
|
156
|
+
container.scrollTo({ top: scrollTop, behavior: 'smooth' })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
lists.map((list, i) => {
|
|
162
|
+
const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
|
|
163
|
+
const currentValue = i === 0 ? hour : i === 1 ? minute : period
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
key={i}
|
|
168
|
+
className="w-[60px] py-2 relative overflow-hidden hover:overflow-y-auto border-l border-gray-100 sm-scrollbar first:border-l-0"
|
|
169
|
+
>
|
|
170
|
+
<div className="w-[60px] absolute flex flex-col items-center">
|
|
171
|
+
{/* using absolute since the scrollbar takes up space */}
|
|
172
|
+
{list.map(item => (
|
|
173
|
+
<div
|
|
174
|
+
className="py-[1px] flex group cursor-pointer"
|
|
175
|
+
key={item}
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
handleTimeChange(type, item)
|
|
178
|
+
scrollIntoView(type, item, e.currentTarget)
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<button
|
|
182
|
+
key={item}
|
|
183
|
+
className={
|
|
184
|
+
`${dayButtonClassName} rounded-full flex justify-center items-center group-hover:bg-gray-100 `
|
|
185
|
+
+ (item === currentValue ? '!bg-input-border-focus text-white' : '')
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
{item.toString().padStart(2, '0').toLowerCase()}
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
// fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
3
3
|
import { css } from 'twin.macro'
|
|
4
|
-
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps
|
|
4
|
+
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps, FieldTime,
|
|
5
|
+
FieldTimeProps } from 'nitro-web'
|
|
5
6
|
import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
|
|
6
7
|
import { Errors, type Error } from 'nitro-web/types'
|
|
7
|
-
import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon } from 'lucide-react'
|
|
8
|
+
import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon, ClockIcon } from 'lucide-react'
|
|
8
9
|
import { memo } from 'react'
|
|
9
10
|
|
|
10
|
-
type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
11
|
+
type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color' | 'time'
|
|
11
12
|
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
12
13
|
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
13
14
|
type FieldExtraProps = {
|
|
@@ -40,6 +41,8 @@ export type FieldProps = (
|
|
|
40
41
|
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
41
42
|
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
42
43
|
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
44
|
+
| ({ type: 'time' } & FieldTimeProps & FieldExtraProps)
|
|
45
|
+
// | ({ type: 'time2' } & FieldTimeProps2 & FieldExtraProps)
|
|
43
46
|
)
|
|
44
47
|
type IsFieldCachedProps = {
|
|
45
48
|
name: string
|
|
@@ -95,6 +98,8 @@ function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProp
|
|
|
95
98
|
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ColorSvg hex={value}/>} className="size-[17px]" />
|
|
96
99
|
} else if (type == 'date') {
|
|
97
100
|
Icon = <IconWrapper iconPos={iconPos} icon={icon || <CalendarIcon />} className="size-[14px] size-input-icon" />
|
|
101
|
+
} else if (type == 'time') {
|
|
102
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ClockIcon />} className="size-[14px] size-input-icon" />
|
|
98
103
|
} else if (icon) {
|
|
99
104
|
Icon = <IconWrapper iconPos={iconPos} icon={icon} className="size-[14px] size-input-icon" />
|
|
100
105
|
}
|
|
@@ -134,7 +139,20 @@ function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProp
|
|
|
134
139
|
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
135
140
|
</FieldContainer>
|
|
136
141
|
)
|
|
137
|
-
}
|
|
142
|
+
} else if (type == 'time') {
|
|
143
|
+
return (
|
|
144
|
+
<FieldContainer error={error} className={props.className}>
|
|
145
|
+
<FieldTime {...props} {...commonProps} Icon={Icon} />
|
|
146
|
+
</FieldContainer>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
// else if (type == 'time2') {
|
|
150
|
+
// return (
|
|
151
|
+
// <FieldContainer error={error} className={props.className}>
|
|
152
|
+
// <FieldTime2 {...props} {...commonProps} Icon={Icon} />
|
|
153
|
+
// </FieldContainer>
|
|
154
|
+
// )
|
|
155
|
+
// }
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
function FieldContainer({ children, className, error }: { children: React.ReactNode, className?: string, error?: Error }) {
|
|
@@ -224,7 +224,7 @@ function Option(props: OptionProps) {
|
|
|
224
224
|
const DropdownIndicator = (props: DropdownIndicatorProps) => {
|
|
225
225
|
return (
|
|
226
226
|
<components.DropdownIndicator {...props}>
|
|
227
|
-
<ChevronsUpDownIcon size={15} className="text-
|
|
227
|
+
<ChevronsUpDownIcon size={15} className="text-[#b6b8be] text-input-icon -my-0.5 -mx-[1px]" />
|
|
228
228
|
</components.DropdownIndicator>
|
|
229
229
|
)
|
|
230
230
|
}
|
|
@@ -45,6 +45,7 @@ export function Styleguide({ className, elements, children, currencies }: Styleg
|
|
|
45
45
|
date: Date.now(),
|
|
46
46
|
'date-range': [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 33],
|
|
47
47
|
'date-time': Date.now(),
|
|
48
|
+
time: null,
|
|
48
49
|
calendar: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 8],
|
|
49
50
|
firstName: 'Bruce',
|
|
50
51
|
tableFilter: '',
|
|
@@ -471,6 +472,10 @@ export function Styleguide({ className, elements, children, currencies }: Styleg
|
|
|
471
472
|
<label for="date">Date multi-select (right aligned)</label>
|
|
472
473
|
<Field name="date" type="date" mode="multiple" state={state} onChange={(e) => onChange(setState, e)} dir="bottom-right" />
|
|
473
474
|
</div>
|
|
475
|
+
<div>
|
|
476
|
+
<label for="time">Time</label>
|
|
477
|
+
<Field name="time" type="time" state={state} onChange={(e) => onChange(setState, e)} />
|
|
478
|
+
</div>
|
|
474
479
|
</div>
|
|
475
480
|
|
|
476
481
|
<h2 class="h3">File Inputs & Calendar</h2>
|
|
@@ -482,7 +487,7 @@ export function Styleguide({ className, elements, children, currencies }: Styleg
|
|
|
482
487
|
<div>
|
|
483
488
|
<label for="calendar">Calendar</label>
|
|
484
489
|
<Calendar mode="range" value={state.calendar} numberOfMonths={1}
|
|
485
|
-
onChange={(
|
|
490
|
+
onChange={(value) => {
|
|
486
491
|
onChange(setState, { target: { name: 'calendar', value: value } })
|
|
487
492
|
}}
|
|
488
493
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.118",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|