nitro-web 0.0.85 → 0.0.87
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/globals.ts +10 -6
- package/package.json +14 -6
- package/types/{required-globals.d.ts → globals.d.ts} +3 -1
- package/.editorconfig +0 -9
- package/components/auth/auth.api.js +0 -410
- package/components/auth/reset.tsx +0 -86
- package/components/auth/signin.tsx +0 -76
- package/components/auth/signup.tsx +0 -62
- package/components/billing/stripe.api.js +0 -268
- package/components/dashboard/dashboard.tsx +0 -32
- package/components/partials/element/accordion.tsx +0 -102
- package/components/partials/element/avatar.tsx +0 -40
- package/components/partials/element/button.tsx +0 -98
- package/components/partials/element/calendar.tsx +0 -125
- package/components/partials/element/dropdown.tsx +0 -248
- package/components/partials/element/filters.tsx +0 -194
- package/components/partials/element/github-link.tsx +0 -16
- package/components/partials/element/initials.tsx +0 -66
- package/components/partials/element/message.tsx +0 -141
- package/components/partials/element/modal.tsx +0 -90
- package/components/partials/element/sidebar.tsx +0 -195
- package/components/partials/element/tooltip.tsx +0 -154
- package/components/partials/element/topbar.tsx +0 -15
- package/components/partials/form/checkbox.tsx +0 -150
- package/components/partials/form/drop-handler.tsx +0 -68
- package/components/partials/form/drop.tsx +0 -141
- package/components/partials/form/field-color.tsx +0 -86
- package/components/partials/form/field-currency.tsx +0 -158
- package/components/partials/form/field-date.tsx +0 -252
- package/components/partials/form/field.tsx +0 -231
- package/components/partials/form/form-error.tsx +0 -27
- package/components/partials/form/location.tsx +0 -225
- package/components/partials/form/select.tsx +0 -360
- package/components/partials/is-first-render.ts +0 -14
- package/components/partials/not-found.tsx +0 -7
- package/components/partials/styleguide.tsx +0 -407
- package/semver-updater.cjs +0 -13
- package/tsconfig.json +0 -38
- package/tsconfig.types.json +0 -15
- package/types/core-only-globals.d.ts +0 -9
- package/types.ts +0 -60
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { format, isValid, parse } from 'date-fns'
|
|
3
|
-
import { getPrefixWidth } from 'nitro-web/util'
|
|
4
|
-
import { Calendar, Dropdown } from 'nitro-web'
|
|
5
|
-
import { dayButtonClassName } from '../element/calendar'
|
|
6
|
-
|
|
7
|
-
type Mode = 'single' | 'multiple' | 'range'
|
|
8
|
-
type DropdownRef = {
|
|
9
|
-
setIsActive: (value: boolean) => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
|
13
|
-
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
|
|
14
|
-
name: string
|
|
15
|
-
/** mode of the date picker */
|
|
16
|
-
mode: Mode
|
|
17
|
-
/** name is used as the id if not provided */
|
|
18
|
-
id?: string
|
|
19
|
-
/** show the time picker */
|
|
20
|
-
showTime?: boolean
|
|
21
|
-
/** prefix to add to the input */
|
|
22
|
-
prefix?: string
|
|
23
|
-
/** number of months to show in the dropdown */
|
|
24
|
-
numberOfMonths?: number
|
|
25
|
-
/** icon to show in the input */
|
|
26
|
-
Icon?: React.ReactNode
|
|
27
|
-
/** direction of the dropdown */
|
|
28
|
-
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// An array is returned for mode = 'multiple' or 'range'
|
|
32
|
-
export type FieldDateProps = (
|
|
33
|
-
| ({ mode: 'single' } & PreFieldDateProps & {
|
|
34
|
-
onChange?: (e: { target: { name: string, value: null|number } }) => void
|
|
35
|
-
value?: null|number|string
|
|
36
|
-
})
|
|
37
|
-
| ({ mode: 'multiple' | 'range' } & PreFieldDateProps & {
|
|
38
|
-
onChange?: (e: { target: { name: string, value: (null|number)[] } }) => void
|
|
39
|
-
value?: null|number|string|(null|number|string)[]
|
|
40
|
-
})
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
type TimePickerProps = {
|
|
44
|
-
date: Date|null
|
|
45
|
-
onChange: (mode: Mode, value: number|null) => void
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function FieldDate({
|
|
49
|
-
dir = 'bottom-left',
|
|
50
|
-
Icon,
|
|
51
|
-
mode,
|
|
52
|
-
numberOfMonths,
|
|
53
|
-
onChange: onChangeProp,
|
|
54
|
-
prefix = '',
|
|
55
|
-
showTime,
|
|
56
|
-
value: valueProp,
|
|
57
|
-
...props
|
|
58
|
-
}: FieldDateProps) {
|
|
59
|
-
const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
|
|
60
|
-
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
61
|
-
const dropdownRef = useRef<DropdownRef>(null)
|
|
62
|
-
const [month, setMonth] = useState<number|undefined>()
|
|
63
|
-
const [lastUpdated, setLastUpdated] = useState(0)
|
|
64
|
-
const id = props.id || props.name
|
|
65
|
-
|
|
66
|
-
// Since value and onChange are optional, we need to hold the value in state if not provided
|
|
67
|
-
const [internalValue, setInternalValue] = useState<typeof valueProp>(valueProp)
|
|
68
|
-
const value = valueProp ?? internalValue
|
|
69
|
-
const onChange = onChangeProp ?? ((e: { target: { name: string, value: any } }) => setInternalValue(e.target.value))
|
|
70
|
-
|
|
71
|
-
// Convert the value to an array of valid* dates
|
|
72
|
-
const dates = useMemo(() => {
|
|
73
|
-
const arrOfNumbers = typeof value === 'string'
|
|
74
|
-
? value.split(/\s*,\s*/g).map(o => parseFloat(o))
|
|
75
|
-
: Array.isArray(value) ? value : [value]
|
|
76
|
-
const out = arrOfNumbers.map(date => isValid(date) ? new Date(date as number) : null) /// changed to null
|
|
77
|
-
return out
|
|
78
|
-
}, [value])
|
|
79
|
-
|
|
80
|
-
// Hold the input value in state
|
|
81
|
-
const [inputValue, setInputValue] = useState(() => getInputValue(dates))
|
|
82
|
-
|
|
83
|
-
// Update the date's inputValue (text) when the value changes outside of the component
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(dates))
|
|
86
|
-
}, [dates])
|
|
87
|
-
|
|
88
|
-
// Get the prefix content width
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
setPrefixWidth(getPrefixWidth(prefix, 4))
|
|
91
|
-
}, [prefix])
|
|
92
|
-
|
|
93
|
-
function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
|
|
94
|
-
if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
95
|
-
setInputValue(getInputValue(value))
|
|
96
|
-
// Update the value
|
|
97
|
-
onChange({ target: { name: props.name, value: getOutputValue(value) } })
|
|
98
|
-
setLastUpdated(new Date().getTime())
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
102
|
-
setInputValue(e.target.value) // keep the input value in sync
|
|
103
|
-
|
|
104
|
-
let split = e.target.value.split(/-|,/).map(o => {
|
|
105
|
-
const date = parse(o.trim(), localePattern, new Date())
|
|
106
|
-
return isValid(date) ? date : null
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
// For single/range we need limit the array
|
|
110
|
-
if (mode == 'range' && split.length > 1) split.length = 2
|
|
111
|
-
else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
|
|
112
|
-
|
|
113
|
-
// Swap dates if needed
|
|
114
|
-
if (mode == 'range' && (split[0] || 0) > (split[1] || 0)) split = [split[0], split[0]]
|
|
115
|
-
|
|
116
|
-
// Set month
|
|
117
|
-
for (let i=split.length; i--;) {
|
|
118
|
-
if (split[i]) setMonth((split[i] as Date).getTime())
|
|
119
|
-
break
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Update the value
|
|
123
|
-
const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
|
|
124
|
-
onChange({ target: { name: props.name, value: getOutputValue(value) }})
|
|
125
|
-
setLastUpdated(new Date().getTime())
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function getInputValue(value: Date|number|null|(Date|number|null)[]) {
|
|
129
|
-
const _dates = Array.isArray(value) ? value : [value]
|
|
130
|
-
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function getOutputValue(value: Date|number|null|(Date|number|null)[]): any {
|
|
134
|
-
// console.log(value)
|
|
135
|
-
return value
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<Dropdown
|
|
140
|
-
ref={dropdownRef}
|
|
141
|
-
menuToggles={false}
|
|
142
|
-
// animate={false}
|
|
143
|
-
// menuIsOpen={true}
|
|
144
|
-
minWidth={0}
|
|
145
|
-
menuContent={
|
|
146
|
-
<div className="flex">
|
|
147
|
-
<Calendar
|
|
148
|
-
// Calendar actually accepts an array of dates, but the type is not typed correctly
|
|
149
|
-
{...{ mode: mode, value: dates as any, numberOfMonths: numberOfMonths, month: month }}
|
|
150
|
-
preserveTime={!!showTime}
|
|
151
|
-
onChange={onCalendarChange}
|
|
152
|
-
className="pt-1 pb-2 px-3"
|
|
153
|
-
/>
|
|
154
|
-
{!!showTime && mode == 'single' && <TimePicker date={dates?.[0]} onChange={onCalendarChange} />}
|
|
155
|
-
</div>
|
|
156
|
-
}
|
|
157
|
-
dir={dir}
|
|
158
|
-
>
|
|
159
|
-
<div className="grid grid-cols-1">
|
|
160
|
-
{Icon}
|
|
161
|
-
{
|
|
162
|
-
prefix &&
|
|
163
|
-
// Similar classNames to the input.tsx:IconWrapper()
|
|
164
|
-
<span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-[12px] ml-input-x">
|
|
165
|
-
{prefix}
|
|
166
|
-
</span>
|
|
167
|
-
}
|
|
168
|
-
<input
|
|
169
|
-
{...props}
|
|
170
|
-
key={'k' + prefixWidth}
|
|
171
|
-
id={id}
|
|
172
|
-
autoComplete="off"
|
|
173
|
-
className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
174
|
-
onBlur={() => setInputValue(getInputValue(dates))}
|
|
175
|
-
onChange={onInputChange}
|
|
176
|
-
style={{ textIndent: prefixWidth + 'px' }}
|
|
177
|
-
type="text"
|
|
178
|
-
value={inputValue}
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
</Dropdown>
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function TimePicker({ date, onChange }: TimePickerProps) {
|
|
186
|
-
const lists = [
|
|
187
|
-
[12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // hours
|
|
188
|
-
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55], // minutes
|
|
189
|
-
['AM', 'PM'], // AM/PM
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
// Get current values from date or use defaults
|
|
193
|
-
const hour = date ? parseInt(format(date, 'h')) : undefined
|
|
194
|
-
const minute = date ? parseInt(format(date, 'm')) : undefined
|
|
195
|
-
const period = date ? format(date, 'a') : undefined
|
|
196
|
-
|
|
197
|
-
const handleTimeChange = (type: 'hour' | 'minute' | 'period', value: string | number) => {
|
|
198
|
-
// Create a new date object from the current date or current time
|
|
199
|
-
const newDate = new Date(date || new Date())
|
|
200
|
-
|
|
201
|
-
if (type === 'hour') {
|
|
202
|
-
// Parse the time with the new hour value
|
|
203
|
-
const timeString = `${value}:${format(newDate, 'mm')} ${format(newDate, 'a')}`
|
|
204
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
205
|
-
newDate.setHours(updatedDate.getHours(), updatedDate.getMinutes())
|
|
206
|
-
} else if (type === 'minute') {
|
|
207
|
-
// Parse the time with the new minute value
|
|
208
|
-
const timeString = `${format(newDate, 'h')}:${value} ${format(newDate, 'a')}`
|
|
209
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
210
|
-
newDate.setMinutes(updatedDate.getMinutes())
|
|
211
|
-
} else if (type === 'period') {
|
|
212
|
-
// Parse the time with the new period value
|
|
213
|
-
const timeString = `${format(newDate, 'h')}:${format(newDate, 'mm')} ${value}`
|
|
214
|
-
const updatedDate = parse(timeString, 'h:mm a', newDate)
|
|
215
|
-
newDate.setHours(updatedDate.getHours())
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
onChange('single', newDate.getTime())
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return (
|
|
222
|
-
lists.map((list, i) => {
|
|
223
|
-
const type = i === 0 ? 'hour' : i === 1 ? 'minute' : 'period'
|
|
224
|
-
const currentValue = i === 0 ? hour : i === 1 ? minute : period
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
<div key={i} className="w-[60px] py-1 relative overflow-hidden hover:overflow-y-auto border-l border-gray-100">
|
|
228
|
-
<div className="w-[60px] absolute flex flex-col items-center">
|
|
229
|
-
{list.map(item => (
|
|
230
|
-
<div
|
|
231
|
-
className="py-1 flex group cursor-pointer"
|
|
232
|
-
key={item}
|
|
233
|
-
onClick={() => handleTimeChange(type, item)}
|
|
234
|
-
>
|
|
235
|
-
<button
|
|
236
|
-
key={item}
|
|
237
|
-
className={
|
|
238
|
-
`${dayButtonClassName} rounded-full flex justify-center items-center group-hover:bg-gray-100 `
|
|
239
|
-
+ (item === currentValue ? '!bg-input-border-focus text-white' : '')
|
|
240
|
-
}
|
|
241
|
-
onClick={() => handleTimeChange(type, item)}
|
|
242
|
-
>
|
|
243
|
-
{item.toString().padStart(2, '0').toLowerCase()}
|
|
244
|
-
</button>
|
|
245
|
-
</div>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
)
|
|
250
|
-
})
|
|
251
|
-
)
|
|
252
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
// fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
3
|
-
import { css } from 'twin.macro'
|
|
4
|
-
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
|
|
5
|
-
import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
|
|
6
|
-
import { Errors, type Error } from 'nitro-web/types'
|
|
7
|
-
import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon } from 'lucide-react'
|
|
8
|
-
import { memo } from 'react'
|
|
9
|
-
|
|
10
|
-
type FieldType = 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
11
|
-
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
12
|
-
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
13
|
-
type FieldExtraProps = {
|
|
14
|
-
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
|
|
15
|
-
name: string
|
|
16
|
-
/** name is applied if id is not provided */
|
|
17
|
-
id?: string
|
|
18
|
-
/** state object to get the value, and check errors against */
|
|
19
|
-
state?: { errors?: Errors, [key: string]: any }
|
|
20
|
-
/** type of the field */
|
|
21
|
-
type?: FieldType
|
|
22
|
-
/** icon to show in the input */
|
|
23
|
-
icon?: React.ReactNode
|
|
24
|
-
iconPos?: 'left' | 'right'
|
|
25
|
-
/** Dependencies to break the implicit memoization of onChange/onInputChange */
|
|
26
|
-
deps?: unknown[]
|
|
27
|
-
placeholder?: string
|
|
28
|
-
/** title used to find related error messages */
|
|
29
|
-
errorTitle?: string|RegExp
|
|
30
|
-
}
|
|
31
|
-
type IconWrapperProps = {
|
|
32
|
-
iconPos: 'left' | 'right'
|
|
33
|
-
icon?: React.ReactNode
|
|
34
|
-
[key: string]: unknown
|
|
35
|
-
}
|
|
36
|
-
// Discriminated union (https://stackoverflow.com/a/77351290/1900648)
|
|
37
|
-
export type FieldProps = (
|
|
38
|
-
| ({ type?: 'text' | 'password' | 'email' | 'filter' | 'search' } & InputProps & FieldExtraProps)
|
|
39
|
-
| ({ type: 'textarea' } & TextareaProps & FieldExtraProps)
|
|
40
|
-
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
41
|
-
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
42
|
-
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
43
|
-
)
|
|
44
|
-
type IsFieldCachedProps = {
|
|
45
|
-
name: string
|
|
46
|
-
state?: FieldProps['state']
|
|
47
|
-
deps?: FieldProps['deps']
|
|
48
|
-
errorTitle?: FieldProps['errorTitle']
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const Field = memo(FieldBase, (prev, next) => {
|
|
52
|
-
return isFieldCached(prev, next)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProps) {
|
|
56
|
-
// `type` must be kept as props.type for TS to be happy and follow the conditions below
|
|
57
|
-
let value!: string
|
|
58
|
-
let Icon!: React.ReactNode
|
|
59
|
-
const error = getErrorFromState(state, errorTitle || props.name)
|
|
60
|
-
const type = props.type
|
|
61
|
-
const iconPos = ip == 'left' || (type == 'color' && !ip) ? 'left' : 'right'
|
|
62
|
-
const id = props.id || props.name
|
|
63
|
-
|
|
64
|
-
if (!props.name) {
|
|
65
|
-
throw new Error('Field component requires a `name` prop')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Input type
|
|
69
|
-
const [inputType, setInputType] = useState(() => { // eslint-disable-line
|
|
70
|
-
return type == 'password' ? 'password' : (type == 'textarea' ? 'textarea' : 'text')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Value: Input is always controlled if state is passed in
|
|
74
|
-
if (typeof props.value !== 'undefined') value = props.value as string
|
|
75
|
-
else if (typeof state == 'object') {
|
|
76
|
-
const v = deepFind(state, props.name) as string | undefined
|
|
77
|
-
value = v ?? ''
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Icon
|
|
81
|
-
if (type == 'password') {
|
|
82
|
-
Icon = <IconWrapper
|
|
83
|
-
iconPos={iconPos}
|
|
84
|
-
icon={icon || inputType == 'password' ? <EyeOffIcon /> : <EyeIcon />}
|
|
85
|
-
onClick={() => setInputType(o => o == 'password' ? 'text' : 'password')}
|
|
86
|
-
className="size-[15px] size-input-icon pointer-events-auto"
|
|
87
|
-
/>
|
|
88
|
-
} else if (type == 'email') {
|
|
89
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon || <MailIcon />} className="size-[14px] size-input-icon" />
|
|
90
|
-
} else if (type == 'filter') {
|
|
91
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon || <FunnelIcon />} className="size-[14px] size-input-icon" />
|
|
92
|
-
} else if (type == 'search') {
|
|
93
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon || <SearchIcon />} className="size-[14px] size-input-icon" />
|
|
94
|
-
} else if (type == 'color') {
|
|
95
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ColorSvg hex={value}/>} className="size-[17px]" />
|
|
96
|
-
} else if (type == 'date') {
|
|
97
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon || <CalendarIcon />} className="size-[14px] size-input-icon" />
|
|
98
|
-
} else if (icon) {
|
|
99
|
-
Icon = <IconWrapper iconPos={iconPos} icon={icon} className="size-[14px] size-input-icon" />
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Classname
|
|
103
|
-
const inputClassName = getInputClasses({ error, Icon, iconPos, type })
|
|
104
|
-
const commonProps = { id: id, value: value, className: inputClassName }
|
|
105
|
-
|
|
106
|
-
// Type has to be referenced as props.type for TS to be happy
|
|
107
|
-
if (!type || type == 'text' || type == 'password' || type == 'email' || type == 'filter' || type == 'search') {
|
|
108
|
-
return (
|
|
109
|
-
<FieldContainer error={error} className={props.className}>
|
|
110
|
-
{Icon}<input {...props} {...commonProps} type={inputType} />
|
|
111
|
-
</FieldContainer>
|
|
112
|
-
)
|
|
113
|
-
} else if (type == 'textarea') {
|
|
114
|
-
return (
|
|
115
|
-
<FieldContainer error={error} className={props.className}>
|
|
116
|
-
{Icon}<textarea {...props} {...commonProps} />
|
|
117
|
-
</FieldContainer>
|
|
118
|
-
)
|
|
119
|
-
} else if (type == 'currency') {
|
|
120
|
-
return (
|
|
121
|
-
<FieldContainer error={error} className={props.className}>
|
|
122
|
-
{Icon}<FieldCurrency {...props} {...commonProps} />
|
|
123
|
-
</FieldContainer>
|
|
124
|
-
)
|
|
125
|
-
} else if (type == 'color') {
|
|
126
|
-
return (
|
|
127
|
-
<FieldContainer error={error} className={props.className}>
|
|
128
|
-
<FieldColor {...props} {...commonProps} Icon={Icon} />
|
|
129
|
-
</FieldContainer>
|
|
130
|
-
)
|
|
131
|
-
} else if (type == 'date') {
|
|
132
|
-
return (
|
|
133
|
-
<FieldContainer error={error} className={props.className}>
|
|
134
|
-
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
135
|
-
</FieldContainer>
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function FieldContainer({ children, className, error }: { children: React.ReactNode, className?: string, error?: Error }) {
|
|
141
|
-
return (
|
|
142
|
-
<div css={style} className={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after grid grid-cols-1 nitro-field ${className || ''}`)}>
|
|
143
|
-
{children}
|
|
144
|
-
{error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?: React.ReactNode, iconPos: string, type?: string }) {
|
|
150
|
-
// not twMerge
|
|
151
|
-
const px = 'px-[12px]'
|
|
152
|
-
const py = 'py-[9px] py-input-y'
|
|
153
|
-
return (
|
|
154
|
-
'block col-start-1 row-start-1 w-full rounded-md bg-white text-input-base outline outline-1 -outline-offset-1 ' +
|
|
155
|
-
'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' + `${py} ${px} ` +
|
|
156
|
-
(iconPos == 'right' && Icon ? 'pr-[32px] pr-input-x-icon pl-input-x ' : '') +
|
|
157
|
-
(iconPos == 'left' && Icon ? 'pl-[32px] pl-input-x-icon pr-input-x ' : 'px-input-x ') +
|
|
158
|
-
(iconPos == 'left' && Icon && type == 'color' ? 'indent-[5px] ' : '') +
|
|
159
|
-
(error
|
|
160
|
-
? 'text-danger-foreground outline-danger focus:outline-danger '
|
|
161
|
-
: 'text-input outline-input-border focus:outline-input-border-focus ') +
|
|
162
|
-
(iconPos == 'right' ? 'justify-self-start ' : 'justify-self-end ') +
|
|
163
|
-
'nitro-input'
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function IconWrapper({ icon, iconPos, ...props }: IconWrapperProps) {
|
|
168
|
-
return (
|
|
169
|
-
!!icon &&
|
|
170
|
-
<div
|
|
171
|
-
{...props}
|
|
172
|
-
className={
|
|
173
|
-
'z-[0] col-start-1 row-start-1 self-center text-[#c6c8ce] text-input-icon select-none [&>svg]:size-full ' +
|
|
174
|
-
(iconPos == 'right' ? 'justify-self-end mr-[12px] mr-input-x ' : 'justify-self-start ml-[12px] ml-input-x ') +
|
|
175
|
-
props.className || ''
|
|
176
|
-
}
|
|
177
|
-
>{icon}</div>
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function ColorSvg({ hex }: { hex?: string }) {
|
|
182
|
-
return (
|
|
183
|
-
<span class="block size-full rounded-md" style={{ backgroundColor: hex ? hex : '#f1f1f1' }}></span>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function isFieldCached(prev: IsFieldCachedProps, next: IsFieldCachedProps) {
|
|
188
|
-
// Check if the field is cached, onChange/onInputChange doesn't affect the cache
|
|
189
|
-
const path = prev.name
|
|
190
|
-
const prevState = prev.state || {}
|
|
191
|
-
const nextState = next.state || {}
|
|
192
|
-
const errorTitle = next.errorTitle || path
|
|
193
|
-
|
|
194
|
-
// Check if any prop has changed, except `onChange`/`onInputChange`
|
|
195
|
-
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)])
|
|
196
|
-
for (const k of allKeys) {
|
|
197
|
-
if (k === 'state' || k === 'onChange' || k === 'onInputChange') continue
|
|
198
|
-
if (prev[k as keyof typeof prev] !== next[k as keyof typeof next]) {
|
|
199
|
-
// console.log(4, 'changed', path, k)
|
|
200
|
-
return false
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// If `deps` have changed, handy for onChange/onInputChange, re-render!
|
|
205
|
-
if ((next.deps?.length !== prev.deps?.length) || next.deps?.some((v, i) => v !== prev.deps?.[i])) return false
|
|
206
|
-
|
|
207
|
-
// If the state value has changed, re-render!
|
|
208
|
-
if (deepFind(prevState, path) !== deepFind(nextState, path)) return false
|
|
209
|
-
|
|
210
|
-
// If the state error has changed, re-render!
|
|
211
|
-
if (getErrorFromState(prevState, errorTitle) !== getErrorFromState(nextState, errorTitle)) return false
|
|
212
|
-
|
|
213
|
-
// All good, use cached version
|
|
214
|
-
return true
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const style = css`
|
|
218
|
-
input {
|
|
219
|
-
appearance: textfield;
|
|
220
|
-
-moz-appearance: textfield;
|
|
221
|
-
}
|
|
222
|
-
input::-webkit-outer-spin-button,
|
|
223
|
-
input::-webkit-inner-spin-button {
|
|
224
|
-
-webkit-appearance: none;
|
|
225
|
-
margin: 0;
|
|
226
|
-
}
|
|
227
|
-
/* tw4 we can use calc to determine the padding-left with css variables...
|
|
228
|
-
.inputt {
|
|
229
|
-
padding-left: calc(var(--input-x) * 2);
|
|
230
|
-
} */
|
|
231
|
-
`
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Errors } from 'nitro-web/types'
|
|
2
|
-
|
|
3
|
-
type FormError = {
|
|
4
|
-
state: { errors?: Errors },
|
|
5
|
-
// display all errors except these field titles, e.g. ['name', 'address']
|
|
6
|
-
fields?: Array<string>,
|
|
7
|
-
className?: string,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function FormError({ state, fields, className }: FormError) {
|
|
11
|
-
// A catch all error element that should be placed next to the submit button
|
|
12
|
-
let error: { title: string, detail: string } | undefined
|
|
13
|
-
for (const item of state.errors || []) {
|
|
14
|
-
if (!item.title || item.title.match(/^(error|invalid)$/i) || (fields && !fields.includes(item.title))) {
|
|
15
|
-
error = item
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return (
|
|
19
|
-
<>
|
|
20
|
-
{error ? (
|
|
21
|
-
<div class={`text-danger-foreground mt-1 text-sm nitro-error ${className||''}`}>
|
|
22
|
-
{error.detail}
|
|
23
|
-
</div>
|
|
24
|
-
) : null}
|
|
25
|
-
</>
|
|
26
|
-
)
|
|
27
|
-
}
|