nitro-web 0.0.15 → 0.0.17
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/_example/client/css/index.css +1 -1
- package/_example/client/index.ts +1 -3
- package/_example/components/index.tsx +10 -4
- package/_example/tailwind.config.js +17 -17
- package/client/index.ts +9 -5
- package/components/auth/reset.tsx +4 -4
- package/components/auth/signin.tsx +3 -3
- package/components/auth/signup.tsx +5 -5
- package/components/dashboard/dashboard.tsx +3 -3
- package/components/partials/element/button.tsx +4 -3
- package/components/partials/element/calendar.tsx +108 -0
- package/components/partials/element/modal.tsx +54 -196
- package/components/partials/element/sidebar.tsx +5 -4
- package/components/partials/form/checkbox.tsx +1 -1
- package/components/partials/form/input-color.tsx +21 -20
- package/components/partials/form/input-currency.tsx +51 -35
- package/components/partials/form/input-date.tsx +55 -167
- package/components/partials/form/input.tsx +123 -92
- package/components/partials/form/select.tsx +4 -4
- package/components/partials/styleguide.tsx +85 -43
- package/components/settings/settings-account.tsx +6 -6
- package/components/settings/settings-business.tsx +12 -12
- package/components/settings/settings-team--member.tsx +8 -8
- package/package.json +10 -6
- package/readme.md +11 -7
- package/types/required-globals.d.ts +1 -0
- package/types/util.d.ts +19 -10
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -3
- package/util.js +22 -14
- package/webpack.config.js +1 -5
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// todo: finish tailwind conversion
|
|
3
|
-
import { css } from 'twin.macro'
|
|
4
|
-
import { DayPicker } from 'react-day-picker'
|
|
5
1
|
import { format, isValid, parse } from 'date-fns'
|
|
6
|
-
import {
|
|
7
|
-
import { Dropdown } from 'nitro-web'
|
|
8
|
-
import 'react-day-picker/dist/style.css'
|
|
2
|
+
import { getPrefixWidth } from 'nitro-web/util'
|
|
3
|
+
import { Calendar, Dropdown } from 'nitro-web'
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
type Mode = 'single' | 'multiple' | 'range'
|
|
6
|
+
type DropdownRef = {
|
|
7
|
+
setIsActive: (value: boolean) => void
|
|
8
|
+
}
|
|
9
|
+
export type FieldDateProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
10
|
+
name: string
|
|
11
|
+
id?: string
|
|
12
|
+
mode?: Mode
|
|
13
|
+
// an array is returned for non-single modes
|
|
14
|
+
onChange?: (e: { target: { id: string, value: null|number|(null|number)[] } }) => void
|
|
15
|
+
prefix?: string
|
|
16
|
+
value?: null|number|string|(null|number|string)[]
|
|
17
|
+
numberOfMonths?: number
|
|
18
|
+
Icon?: React.ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FieldDate({ mode='single', onChange, prefix='', value, numberOfMonths, Icon, ...props }: FieldDateProps) {
|
|
14
22
|
const localePattern = 'd MMM yyyy'
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
23
|
+
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
24
|
+
const dropdownRef = useRef<DropdownRef>(null)
|
|
25
|
+
const id = props.id || props.name
|
|
26
|
+
const [month, setMonth] = useState<number|undefined>()
|
|
18
27
|
|
|
28
|
+
// Convert the value to an array of valid* dates
|
|
19
29
|
const dates = useMemo(() => {
|
|
20
|
-
// Convert the value to an array of valid* dates
|
|
21
30
|
const _dates = Array.isArray(value) ? value : [value]
|
|
22
|
-
return _dates.map(date => isValid(date) ? new Date(date) :
|
|
31
|
+
return _dates.map(date => isValid(date) ? new Date(date as number) : null) /// change to null
|
|
23
32
|
}, [value])
|
|
24
33
|
|
|
25
|
-
// Hold the month in state to control the calendar when the input changes
|
|
26
|
-
const [month, setMonth] = useState(dates[0])
|
|
27
|
-
|
|
28
34
|
// Hold the input value in state
|
|
29
35
|
const [inputValue, setInputValue] = useState(() => getInputValue(dates))
|
|
30
36
|
|
|
37
|
+
// Get the prefix content width
|
|
31
38
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
setPrefixWidth(getCurrencyPrefixWidth(prefix, 4))
|
|
39
|
+
setPrefixWidth(getPrefixWidth(prefix, 4))
|
|
34
40
|
}, [prefix])
|
|
35
41
|
|
|
36
|
-
function
|
|
37
|
-
if (mode == 'single')
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else if (mode == 'range') {
|
|
43
|
-
const {from, to} = newDate || {} // may not exist
|
|
44
|
-
callOnChange(from ? [from?.getTime() || null, to?.getTime() || null] : null)
|
|
45
|
-
setInputValue(getInputValue(from ? [from, to] : []))
|
|
42
|
+
function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
|
|
43
|
+
if (mode == 'single') dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
44
|
+
setInputValue(getInputValue(value))
|
|
45
|
+
if (onChange) onChange({ target: { id: id, value: value }})
|
|
46
|
+
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
48
|
+
function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
|
|
49
|
+
const _dates = Array.isArray(dates) ? dates : [dates]
|
|
50
|
+
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
53
|
+
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
54
54
|
setInputValue(e.target.value) // keep the input value in sync
|
|
55
55
|
|
|
56
56
|
let split = e.target.value.split(/-|,/).map(o => {
|
|
@@ -63,162 +63,50 @@ export function InputDate({ className, prefix, id, onChange, mode='single', valu
|
|
|
63
63
|
else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
|
|
64
64
|
|
|
65
65
|
// Swap dates if needed
|
|
66
|
-
if (mode == 'range' && split[0] > split[1]) split = [split[0], split[0]]
|
|
66
|
+
if (mode == 'range' && (split[0] || 0) > (split[1] || 0)) split = [split[0], split[0]]
|
|
67
67
|
|
|
68
68
|
// Set month
|
|
69
69
|
for (let i=split.length; i--;) {
|
|
70
|
-
if (split[i]) setMonth(split[i])
|
|
70
|
+
if (split[i]) setMonth((split[i] as Date).getTime())
|
|
71
71
|
break
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
function getInputValue(dates) {
|
|
79
|
-
return dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function callOnChange(value) {
|
|
83
|
-
if (onChange) onChange({ target: { id: id, value: value }}) // timestamp|[timestamp]
|
|
74
|
+
// Update
|
|
75
|
+
const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
|
|
76
|
+
if (onChange) onChange({ target: { id, value }})
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
return (
|
|
87
80
|
<Dropdown
|
|
88
|
-
ref={
|
|
89
|
-
css={style}
|
|
81
|
+
ref={dropdownRef}
|
|
90
82
|
menuToggles={false}
|
|
91
83
|
animate={false}
|
|
92
84
|
// menuIsOpen={true}
|
|
85
|
+
minWidth={0}
|
|
93
86
|
menuChildren={
|
|
94
|
-
<
|
|
95
|
-
mode={mode}
|
|
96
|
-
month={month}
|
|
97
|
-
onMonthChange={setMonth}
|
|
98
|
-
numberOfMonths={mode == 'range' ? 2 : 1}
|
|
99
|
-
selected={mode === 'single' ? dates[0] : mode == 'range' ? { from: dates[0], to: dates[1] } : dates}
|
|
100
|
-
onSelect={handleDayPickerSelect}
|
|
101
|
-
/>
|
|
87
|
+
<Calendar {...{ mode, value, numberOfMonths, month }} onChange={onCalendarChange} className="px-3 pt-1 pb-2" />
|
|
102
88
|
}
|
|
103
89
|
>
|
|
104
|
-
<div>
|
|
105
|
-
{
|
|
90
|
+
<div className="grid grid-cols-1">
|
|
91
|
+
{Icon}
|
|
92
|
+
{
|
|
93
|
+
prefix &&
|
|
94
|
+
// Similar classNames to the input.tsx:IconWrapper()
|
|
95
|
+
<span className="relative col-start-1 row-start-1 self-center select-none z-[1] justify-self-start text-sm ml-3">{prefix}</span>
|
|
96
|
+
}
|
|
106
97
|
<input
|
|
107
98
|
{...props}
|
|
108
|
-
key={'k'+prefixWidth}
|
|
99
|
+
key={'k' + prefixWidth}
|
|
109
100
|
id={id}
|
|
110
101
|
autoComplete="off"
|
|
111
|
-
className={
|
|
112
|
-
className + ' ' + isInvalid
|
|
113
|
-
}
|
|
102
|
+
className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
|
|
114
103
|
value={inputValue}
|
|
115
|
-
onChange={
|
|
104
|
+
onChange={onInputChange}
|
|
116
105
|
onBlur={() => setInputValue(getInputValue(dates))}
|
|
117
106
|
style={{ textIndent: prefixWidth + 'px' }}
|
|
107
|
+
type="text"
|
|
118
108
|
/>
|
|
119
109
|
</div>
|
|
120
110
|
</Dropdown>
|
|
121
111
|
)
|
|
122
112
|
}
|
|
123
|
-
|
|
124
|
-
const style = css`
|
|
125
|
-
.rdp {
|
|
126
|
-
--rdp-cell-size: 34px;
|
|
127
|
-
--rdp-caption-font-size: 12px;
|
|
128
|
-
--rdp-accent-color: blue; /* theme('colors.primary') */
|
|
129
|
-
font-size: 13px;
|
|
130
|
-
margin: 0 12px 11px;
|
|
131
|
-
svg {
|
|
132
|
-
width: 13px;
|
|
133
|
-
height: 13px;
|
|
134
|
-
}
|
|
135
|
-
.rdp-caption_label {
|
|
136
|
-
height: var(--rdp-cell-size);
|
|
137
|
-
}
|
|
138
|
-
.rdp-head_cell {
|
|
139
|
-
text-align: center !important;
|
|
140
|
-
}
|
|
141
|
-
tr {
|
|
142
|
-
display: flex;
|
|
143
|
-
justify-content: space-around;
|
|
144
|
-
align-items: center;
|
|
145
|
-
th,
|
|
146
|
-
td {
|
|
147
|
-
display: flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
margin-left: -1px;
|
|
150
|
-
margin-top: -1px;
|
|
151
|
-
.rdp-day {
|
|
152
|
-
border: 0 !important;
|
|
153
|
-
position: relative;
|
|
154
|
-
border-radius: 0 !important;
|
|
155
|
-
color: inherit;
|
|
156
|
-
background-color: transparent !important;
|
|
157
|
-
&:before {
|
|
158
|
-
content: '';
|
|
159
|
-
position: absolute;
|
|
160
|
-
display: block;
|
|
161
|
-
left: 0px;
|
|
162
|
-
top: 0px;
|
|
163
|
-
bottom: 0px;
|
|
164
|
-
right: 0px;
|
|
165
|
-
z-index: -1;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
.rdp-day:focus,
|
|
169
|
-
.rdp-day:hover,
|
|
170
|
-
.rdp-day:active {
|
|
171
|
-
&:not([disabled]):not(.rdp-day_selected) {
|
|
172
|
-
&:before {
|
|
173
|
-
left: 1px;
|
|
174
|
-
top: 1px;
|
|
175
|
-
bottom: 1px;
|
|
176
|
-
right: 1px;
|
|
177
|
-
border-radius: 50%;
|
|
178
|
-
background-color: #e7edff;
|
|
179
|
-
}
|
|
180
|
-
&:active {
|
|
181
|
-
color: white;
|
|
182
|
-
&:before {
|
|
183
|
-
background-color: blue; /* theme('colors.primary') */
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
.rdp-day_selected {
|
|
189
|
-
color: white;
|
|
190
|
-
:before {
|
|
191
|
-
border-radius: 50%;
|
|
192
|
-
background-color: blue; /* theme('colors.primary') */
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
.rdp-day_range_middle {
|
|
196
|
-
color: black; /* theme('colors.dark') */
|
|
197
|
-
:before {
|
|
198
|
-
border-radius: 0;
|
|
199
|
-
border: 1px solid rgb(151 133 185);
|
|
200
|
-
background-color: blue; /* theme('colors.primary-light') */
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
.rdp-day_range_start,
|
|
204
|
-
.rdp-day_range_end {
|
|
205
|
-
position: relative;
|
|
206
|
-
z-index: 1;
|
|
207
|
-
&.rdp-day_range_start:before {
|
|
208
|
-
border-top-right-radius: 0px;
|
|
209
|
-
border-bottom-right-radius: 0px;
|
|
210
|
-
}
|
|
211
|
-
&.rdp-day_range_end:before {
|
|
212
|
-
border-top-left-radius: 0px;
|
|
213
|
-
border-bottom-left-radius: 0px;
|
|
214
|
-
}
|
|
215
|
-
&.rdp-day_range_start.rdp-day_range_end:before {
|
|
216
|
-
border-radius: 50%;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
@@ -1,134 +1,165 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { css } from 'twin.macro'
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
import { util, FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
|
|
4
|
+
import { Errors, type Error } from 'types'
|
|
5
5
|
import {
|
|
6
6
|
EnvelopeIcon,
|
|
7
|
-
|
|
7
|
+
CalendarIcon,
|
|
8
8
|
FunnelIcon,
|
|
9
9
|
MagnifyingGlassIcon,
|
|
10
10
|
EyeIcon,
|
|
11
11
|
EyeSlashIcon,
|
|
12
12
|
} from '@heroicons/react/20/solid'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
15
|
+
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
16
|
+
type FieldExtraProps = {
|
|
17
|
+
// field name or path on state (used to match errors), e.g. 'date', 'company.email'
|
|
15
18
|
name: string
|
|
16
|
-
state?: any
|
|
17
19
|
id?: string
|
|
18
|
-
|
|
19
|
-
[key: string]:
|
|
20
|
+
// state object to get the value, and check errors against
|
|
21
|
+
state?: { errors: Errors, [key: string]: unknown }
|
|
22
|
+
type?: 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
23
|
+
icon?: React.ReactNode
|
|
24
|
+
iconPos?: 'left' | 'right'
|
|
25
|
+
}
|
|
26
|
+
type IconWrapperProps = {
|
|
27
|
+
iconPos: string
|
|
28
|
+
icon?: React.ReactNode
|
|
29
|
+
[key: string]: unknown
|
|
20
30
|
}
|
|
31
|
+
// Discriminated union (https://stackoverflow.com/a/77351290/1900648)
|
|
32
|
+
export type FieldProps = (
|
|
33
|
+
| ({ type?: 'text' | 'password' | 'email' | 'filter' | 'search' } & InputProps & FieldExtraProps)
|
|
34
|
+
| ({ type: 'textarea' } & TextareaProps & FieldExtraProps)
|
|
35
|
+
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
36
|
+
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
37
|
+
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
41
|
+
// type must be kept as props.type for TS to be happy and follow the conditions below
|
|
42
|
+
let error!: Error
|
|
43
|
+
let value!: string
|
|
44
|
+
let Icon!: React.ReactNode
|
|
45
|
+
const type = props.type
|
|
46
|
+
const iconPos = ip == 'left' || (type == 'color' && !ip) ? 'left' : 'right'
|
|
21
47
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* @param {string} [id] - not required, name used if not provided
|
|
28
|
-
* @param {('password'|'email'|'text'|'date'|'filter'|'search'|'color'|'textarea'|'currency')} [type='text']
|
|
29
|
-
*/
|
|
30
|
-
let IconSvg: React.ReactNode
|
|
31
|
-
let onClick: () => void
|
|
32
|
-
let iconDir = 'right'
|
|
33
|
-
let InputEl = 'input'
|
|
48
|
+
if (!props.name) {
|
|
49
|
+
throw new Error('Input component requires a `name` prop')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Input type
|
|
34
53
|
const [inputType, setInputType] = useState(() => { // eslint-disable-line
|
|
35
|
-
return type == 'password' ? 'password' : (type == 'textarea' ?
|
|
54
|
+
return type == 'password' ? 'password' : (type == 'textarea' ? 'textarea' : 'text')
|
|
36
55
|
})
|
|
37
|
-
|
|
38
|
-
if (!name) throw new Error('Input component requires a `name` prop')
|
|
39
56
|
|
|
40
|
-
// Input is always controlled if state is passed in
|
|
41
|
-
if (props.value)
|
|
42
|
-
|
|
43
|
-
} else if (typeof state == 'object') {
|
|
44
|
-
value = util.deepFind(state, name)
|
|
45
|
-
if (typeof value == 'undefined') value = ''
|
|
46
|
-
}
|
|
57
|
+
// Value: Input is always controlled if state is passed in
|
|
58
|
+
if (props.value) value = props.value as string
|
|
59
|
+
else if (typeof state == 'object') value = util.deepFind(state, props.name) ?? ''
|
|
47
60
|
|
|
48
|
-
//
|
|
61
|
+
// Errors: find any that match this input path
|
|
49
62
|
for (const item of (state?.errors || [])) {
|
|
50
|
-
if (util.isRegex(name) && (item.title||'').match(name))
|
|
51
|
-
else if (item.title == name) error = item
|
|
63
|
+
if (util.isRegex(props.name) && (item.title || '').match(props.name)) error = item
|
|
64
|
+
else if (item.title == props.name) error = item
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
//
|
|
67
|
+
// Icon
|
|
55
68
|
if (type == 'password') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
Icon = <IconWrapper
|
|
70
|
+
iconPos={iconPos}
|
|
71
|
+
icon={icon || inputType == 'password' ? <EyeSlashIcon /> : <EyeIcon />}
|
|
72
|
+
onClick={() => setInputType(o => o == 'password' ? 'text' : 'password')}
|
|
73
|
+
className="pointer-events-auto"
|
|
74
|
+
/>
|
|
75
|
+
} else if (type == 'email') {
|
|
76
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <EnvelopeIcon />} />
|
|
77
|
+
} else if (type == 'filter') {
|
|
78
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <FunnelIcon />} className="size-3" />
|
|
79
|
+
} else if (type == 'search') {
|
|
80
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <MagnifyingGlassIcon />} className="size-4" />
|
|
67
81
|
} else if (type == 'color') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else
|
|
72
|
-
|
|
73
|
-
} else if (type == 'currency') {
|
|
74
|
-
if (!props.config) throw new Error('Input: `config` is required when type=currency')
|
|
75
|
-
InputEl = InputCurrency
|
|
82
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <ColorSvg hex={value}/>} className="size-[17px]" />
|
|
83
|
+
} else if (type == 'date') {
|
|
84
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon || <CalendarIcon />} className="size-4" />
|
|
85
|
+
} else {
|
|
86
|
+
Icon = <IconWrapper iconPos={iconPos} icon={icon} />
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
//
|
|
79
|
-
const
|
|
89
|
+
// Classname
|
|
90
|
+
const inputClassName = getInputClasses({ error, Icon, iconPos, type })
|
|
91
|
+
const commonProps = { id: props.name || props.id, value: value, className: inputClassName }
|
|
80
92
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// Type has to be referenced as props.type for TS to be happy
|
|
94
|
+
if (!type || type == 'text' || type == 'password' || type == 'email' || type == 'filter' || type == 'search') {
|
|
95
|
+
return (
|
|
96
|
+
<FieldContainer error={error} className={props.className}>
|
|
97
|
+
{Icon}<input {...props} {...commonProps} type={inputType} />
|
|
98
|
+
</FieldContainer>
|
|
99
|
+
)
|
|
100
|
+
} else if (type == 'textarea') {
|
|
101
|
+
return (
|
|
102
|
+
<FieldContainer error={error} className={props.className}>
|
|
103
|
+
{Icon}<textarea {...props} {...commonProps} />
|
|
104
|
+
</FieldContainer>
|
|
105
|
+
)
|
|
106
|
+
} else if (type == 'currency') {
|
|
107
|
+
return (
|
|
108
|
+
<FieldContainer error={error} className={props.className}>
|
|
109
|
+
{Icon}<FieldCurrency {...props} {...commonProps} />
|
|
110
|
+
</FieldContainer>
|
|
111
|
+
)
|
|
112
|
+
} else if (type == 'color') {
|
|
113
|
+
return (
|
|
114
|
+
<FieldContainer error={error} className={props.className}>
|
|
115
|
+
<FieldColor {...props} {...commonProps} Icon={Icon} />
|
|
116
|
+
</FieldContainer>
|
|
117
|
+
)
|
|
118
|
+
} else if (type == 'date') {
|
|
119
|
+
return (
|
|
120
|
+
<FieldContainer error={error} className={props.className}>
|
|
121
|
+
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
122
|
+
</FieldContainer>
|
|
123
|
+
)
|
|
95
124
|
}
|
|
125
|
+
}
|
|
96
126
|
|
|
97
|
-
|
|
98
|
-
if (!['color', 'date'].includes(type)) delete inputProps.iconEl
|
|
99
|
-
|
|
127
|
+
function FieldContainer({ children, className, error }: { children: React.ReactNode, className?: string, error?: Error }) {
|
|
100
128
|
return (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{ !inputProps.iconEl && iconEl }
|
|
104
|
-
<InputEl {...inputProps} />
|
|
129
|
+
<div css={style} className={`mt-input-before mb-input-after grid grid-cols-1 ${className || ''}`}>
|
|
130
|
+
{children}
|
|
105
131
|
{error && <div class="mt-1.5 text-xs text-danger">{error.detail}</div>}
|
|
106
132
|
</div>
|
|
107
133
|
)
|
|
108
134
|
}
|
|
109
135
|
|
|
110
|
-
type
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
function getInputClasses({ error, Icon, iconPos, type }: { error: Error, Icon?: React.ReactNode, iconPos: string, type?: string }) {
|
|
137
|
+
const paddingLeft = type == 'color' ? 'sm:pl-9 pl-9' : 'sm:pl-8 pl-8'
|
|
138
|
+
const paddingRight = type == 'color' ? 'sm:pr-9 pr-9' : 'sm:pr-8 pr-8'
|
|
139
|
+
return (
|
|
140
|
+
'col-start-1 row-start-1 block w-full rounded-md bg-white py-2 text-sm leading-[1.65] outline outline-1 -outline-offset-1 ' +
|
|
141
|
+
'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
|
|
142
|
+
(iconPos == 'right' && Icon ? `${paddingRight} pl-3 ` : (Icon ? `${paddingLeft} pr-3 ` : 'px-3 ')) +
|
|
143
|
+
(error ? 'text-red-900 outline-danger focus:outline-danger ' : 'text-input outline-input-border focus:outline-primary ') +
|
|
144
|
+
(iconPos == 'right' ? 'justify-self-start ' : 'justify-self-end ')
|
|
145
|
+
)
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
function
|
|
118
|
-
const iconSize = type == 'color' ? 'size-[18px]' : 'size-4'
|
|
148
|
+
function IconWrapper({ icon, iconPos, ...props }: IconWrapperProps) {
|
|
119
149
|
return (
|
|
120
|
-
!!
|
|
121
|
-
<div
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
!!icon &&
|
|
151
|
+
<div
|
|
152
|
+
{...props}
|
|
153
|
+
className={twMerge(
|
|
154
|
+
'relative size-[14px] col-start-1 row-start-1 self-center text-[#c6c8ce] select-none z-[1] ' +
|
|
155
|
+
(iconPos == 'right' ? 'justify-self-end mr-3 ' : 'justify-self-start ml-3 ') +
|
|
156
|
+
props.className || ''
|
|
157
|
+
)}
|
|
158
|
+
>{icon}</div>
|
|
128
159
|
)
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
function
|
|
162
|
+
function ColorSvg({ hex }: { hex?: string }) {
|
|
132
163
|
return (
|
|
133
164
|
<span class="block size-full rounded-md" style={{ backgroundColor: hex ? hex : '#f1f1f1' }}></span>
|
|
134
165
|
)
|
|
@@ -2,7 +2,7 @@ import { css } from 'twin.macro'
|
|
|
2
2
|
import { twMerge } from 'tailwind-merge'
|
|
3
3
|
import ReactSelect, { components, ControlProps, createFilter, OptionProps, SingleValueProps } from 'react-select'
|
|
4
4
|
import { ClearIndicatorProps, DropdownIndicatorProps, MultiValueRemoveProps } from 'react-select'
|
|
5
|
-
import {
|
|
5
|
+
import { ChevronUpDownIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
|
6
6
|
import { util } from 'nitro-web'
|
|
7
7
|
import { Errors } from 'types'
|
|
8
8
|
|
|
@@ -203,7 +203,7 @@ function Option(props: OptionProps) {
|
|
|
203
203
|
const DropdownIndicator = (props: DropdownIndicatorProps) => {
|
|
204
204
|
return (
|
|
205
205
|
<components.DropdownIndicator {...props}>
|
|
206
|
-
<
|
|
206
|
+
<ChevronUpDownIcon className="text-gray-400 size-[17px] -my-0.5 -mx-0.5" />
|
|
207
207
|
</components.DropdownIndicator>
|
|
208
208
|
)
|
|
209
209
|
}
|
|
@@ -237,7 +237,7 @@ const selectStyles = {
|
|
|
237
237
|
// Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
|
|
238
238
|
// Input container
|
|
239
239
|
control: {
|
|
240
|
-
base: 'rounded-md bg-white hover:cursor-pointer text-sm
|
|
240
|
+
base: 'rounded-md bg-white hover:cursor-pointer text-sm leading-[1.65] outline outline-1 -outline-offset-1 outline-input-border',
|
|
241
241
|
focus: 'outline-2 -outline-offset-2 outline-primary',
|
|
242
242
|
error: 'outline-danger',
|
|
243
243
|
},
|
|
@@ -249,7 +249,7 @@ const selectStyles = {
|
|
|
249
249
|
},
|
|
250
250
|
multiValue: 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
|
|
251
251
|
multiValueLabel: '',
|
|
252
|
-
multiValueRemove: 'border border-
|
|
252
|
+
multiValueRemove: 'border border-black/10 bg-clip-content bg-white rounded-md text-dark hover:bg-red-50',
|
|
253
253
|
placeholder: 'text-input-placeholder',
|
|
254
254
|
singleValue: {
|
|
255
255
|
base: 'text-input',
|