nitro-web 0.2.12 → 0.2.14
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.
|
@@ -22,8 +22,14 @@ export type FieldCurrencyProps = Omit<NumericFormatProps, 'onChange' | 'value' |
|
|
|
22
22
|
name: string
|
|
23
23
|
/** name is applied if id is not provided */
|
|
24
24
|
id?: string
|
|
25
|
-
/** currency
|
|
26
|
-
|
|
25
|
+
/** render as a percent field: no currency symbol, '%' suffix */
|
|
26
|
+
percent?: boolean
|
|
27
|
+
/** minimum decimal places to show (percent only; defaults to 0) */
|
|
28
|
+
minDecimals?: number
|
|
29
|
+
/** maximum decimal places to show (percent only; defaults to 2) */
|
|
30
|
+
maxDecimals?: number
|
|
31
|
+
/** currency iso, e.g. 'nzd' (defaults to 'nzd'; ignored when percent) */
|
|
32
|
+
currency?: string
|
|
27
33
|
/** override the default currencies array used to lookup currency symbol and digits, e.g. {nzd: { symbol: '$', digits: 2 }} */
|
|
28
34
|
currencies?: Currencies,
|
|
29
35
|
/** override the default CLDR country currency format, e.g. '¤#,##0.00' */
|
|
@@ -34,10 +40,14 @@ export type FieldCurrencyProps = Omit<NumericFormatProps, 'onChange' | 'value' |
|
|
|
34
40
|
defaultValue?: Cents // defined to just fix typescript error
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
export function FieldCurrency({
|
|
43
|
+
export function FieldCurrency({
|
|
44
|
+
currency='nzd', currencies, format, percent, minDecimals, maxDecimals, onChange: onChangeProp, ...props
|
|
45
|
+
}: FieldCurrencyProps) {
|
|
38
46
|
const [dontFix, setDontFix] = useState(false)
|
|
39
47
|
const [lastBlurred, setLastBlurred] = useState(0)
|
|
40
|
-
const [settings, setSettings] = useState(() =>
|
|
48
|
+
const [settings, setSettings] = useState(() => (
|
|
49
|
+
getCurrencySettings(currency, currencies, format, props.name, percent, minDecimals, maxDecimals)
|
|
50
|
+
))
|
|
41
51
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
42
52
|
const [inputPaddingLeft, setInputPaddingLeft] = useState(12)
|
|
43
53
|
const ref = useRef({ dontFix }) // was null
|
|
@@ -64,30 +74,36 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
|
|
|
64
74
|
|
|
65
75
|
// Update the settings if the setting parameters change
|
|
66
76
|
useEffect(() => {
|
|
67
|
-
const _settings = getCurrencySettings(currency, currencies, format, props.name)
|
|
77
|
+
const _settings = getCurrencySettings(currency, currencies, format, props.name, percent, minDecimals, maxDecimals)
|
|
68
78
|
if (settings.key !== _settings.key) setSettings(_settings)
|
|
69
|
-
}, [currency, currencies, format])
|
|
79
|
+
}, [currency, currencies, format, percent, minDecimals, maxDecimals])
|
|
70
80
|
|
|
71
81
|
// Get the prefix content width
|
|
72
82
|
useEffect(() => {
|
|
73
83
|
setPrefixWidth(settings.prefix ? getPrefixWidth(settings.prefix, 1) : 0)
|
|
74
84
|
}, [settings.prefix])
|
|
75
85
|
|
|
76
|
-
function to(type: 'dollars' | 'cents', value?: Cents, settings?: { maxDecimals?: number }) {
|
|
86
|
+
function to(type: 'dollars' | 'cents', value?: Cents, settings?: { maxDecimals?: number, minDecimals?: number }) {
|
|
77
87
|
const maxDecimals = settings?.maxDecimals
|
|
88
|
+
const minDecimals = Math.min(settings?.minDecimals ?? 0, maxDecimals ?? 0)
|
|
78
89
|
const parsed = parseFloat(value + '')
|
|
79
90
|
if (!parsed && parsed !== 0) return null
|
|
80
91
|
if (!maxDecimals) return parsed
|
|
81
92
|
|
|
82
|
-
const value2 = type === 'dollars'
|
|
83
|
-
? parsed / Math.pow(10, maxDecimals)
|
|
93
|
+
const value2 = type === 'dollars'
|
|
94
|
+
? parsed / Math.pow(10, maxDecimals)
|
|
84
95
|
: parsed * Math.pow(10, maxDecimals) // e.g. 1.23 => 123
|
|
85
96
|
|
|
86
97
|
// dont fix when the user is typing.
|
|
87
98
|
if (type === 'dollars' && ref.current.dontFix) { setDontFix(false) }
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
if (type === 'cents' || ref.current.dontFix || !minDecimals) return value2
|
|
101
|
+
|
|
102
|
+
// pad to maxDecimals then trim trailing zeros back down to (at least) minDecimals
|
|
103
|
+
const [whole, decimals=''] = value2.toFixed(maxDecimals).split('.')
|
|
104
|
+
let trimmed = decimals
|
|
105
|
+
while (trimmed.length > minDecimals && trimmed.endsWith('0')) trimmed = trimmed.slice(0, -1)
|
|
106
|
+
return trimmed ? `${whole}.${trimmed}` : whole
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
function onChange(source: 'event' | 'prop', floatValue?: number) {
|
|
@@ -96,6 +112,10 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
|
|
|
96
112
|
else setInternalValue(to('cents', floatValue, settings))
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
console.log('settings', settings)
|
|
117
|
+
}, [settings])
|
|
118
|
+
|
|
99
119
|
return (
|
|
100
120
|
<div className="relative">
|
|
101
121
|
<NumericFormat
|
|
@@ -110,6 +130,7 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
|
|
|
110
130
|
onBlur={() => { setLastBlurred(Date.now())}}
|
|
111
131
|
placeholder={props.placeholder || '0.00'}
|
|
112
132
|
value={inputValue}
|
|
133
|
+
suffix={settings.suffix}
|
|
113
134
|
style={{ textIndent: `${prefixWidth}px` }}
|
|
114
135
|
type="text"
|
|
115
136
|
/>
|
|
@@ -117,15 +138,28 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
|
|
|
117
138
|
class={`absolute top-0 bottom-0 inline-flex items-center select-none text-gray-500 text-input-base ${inputValue !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
|
|
118
139
|
style={{ left: `${inputPaddingLeft}px` }}
|
|
119
140
|
>
|
|
120
|
-
{settings.prefix
|
|
141
|
+
{settings.prefix}
|
|
121
142
|
</span>
|
|
122
143
|
</div>
|
|
123
144
|
)
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
function getCurrencySettings(
|
|
147
|
+
function getCurrencySettings(
|
|
148
|
+
currency: string, currencies?: Currencies, format?: string, name?: string, percent?: boolean,
|
|
149
|
+
minDecimals?: number, maxDecimals?: number
|
|
150
|
+
) {
|
|
151
|
+
// percent reuses the currency machinery but with a '%' suffix and no currency lookup
|
|
152
|
+
if (percent) {
|
|
153
|
+
const _minDecimals = minDecimals ?? 0
|
|
154
|
+
const _maxDecimals = maxDecimals ?? 2
|
|
155
|
+
return {
|
|
156
|
+
key: `percent:${_minDecimals}:${_maxDecimals}`, currency: currency, decimalSeparator: '.', thousandSeparator: ',',
|
|
157
|
+
minDecimals: _minDecimals, maxDecimals: _maxDecimals, prefix: undefined, suffix: '%',
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
// parse CLDR currency string format, e.g. '¤#,##0.00'
|
|
128
|
-
const output: {
|
|
162
|
+
const output: {
|
|
129
163
|
key?: string
|
|
130
164
|
currency: string, // e.g. 'nzd'
|
|
131
165
|
decimalSeparator?: string, // e.g. '.'
|
|
@@ -9,7 +9,8 @@ import { Errors, type Error } from 'nitro-web/types'
|
|
|
9
9
|
import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon, ClockIcon, PaperclipIcon } from 'lucide-react'
|
|
10
10
|
import { memo, useState } from 'react'
|
|
11
11
|
|
|
12
|
-
type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea'
|
|
12
|
+
type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea'
|
|
13
|
+
| 'currency' | 'percent' | 'date' | 'color' | 'file'
|
|
13
14
|
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
14
15
|
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
15
16
|
type FieldExtraProps = {
|
|
@@ -42,6 +43,7 @@ export type FieldProps = (
|
|
|
42
43
|
| ({ type?: 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' } & InputProps & FieldExtraProps)
|
|
43
44
|
| ({ type: 'textarea' } & TextareaProps & FieldExtraProps)
|
|
44
45
|
| ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
|
|
46
|
+
| ({ type: 'percent' } & Omit<FieldCurrencyProps, 'currency'> & FieldExtraProps)
|
|
45
47
|
| ({ type: 'color' } & FieldColorProps & FieldExtraProps)
|
|
46
48
|
| ({ type: 'date' } & FieldDateProps & FieldExtraProps)
|
|
47
49
|
| ({ type: 'file' } & InputProps & FieldExtraProps)
|
|
@@ -140,7 +142,13 @@ function FieldBase({ state, icon, iconPos: ip, errorTitle, inputClassName, ...pr
|
|
|
140
142
|
{Icon}<FieldCurrency {...props} {...commonProps} />
|
|
141
143
|
</FieldContainer>
|
|
142
144
|
)
|
|
143
|
-
} else if (type == '
|
|
145
|
+
} else if (type == 'percent') {
|
|
146
|
+
return (
|
|
147
|
+
<FieldContainer error={error} className={props.className}>
|
|
148
|
+
{Icon}<FieldCurrency {...props} {...commonProps} percent />
|
|
149
|
+
</FieldContainer>
|
|
150
|
+
)
|
|
151
|
+
} else if (type == 'date') {
|
|
144
152
|
return (
|
|
145
153
|
<FieldContainer error={error} className={props.className}>
|
|
146
154
|
<FieldDate {...props} {...commonProps} Icon={Icon} />
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { css } from 'twin.macro'
|
|
3
|
-
import { memo, useMemo, Fragment } from 'react'
|
|
3
|
+
import { memo, useMemo, useState, Fragment, FocusEvent } from 'react'
|
|
4
4
|
import ReactSelect, {
|
|
5
5
|
components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
|
|
6
6
|
DropdownIndicatorProps, MultiValueRemoveProps, // ClassNamesConfig,
|
|
7
7
|
ValueContainerProps,
|
|
8
8
|
MenuProps,
|
|
9
|
+
InputProps,
|
|
9
10
|
} from 'react-select'
|
|
10
11
|
import { CheckCircleIcon } from '@heroicons/react/20/solid'
|
|
11
12
|
import { ChevronsUpDownIcon, SearchIcon, XIcon } from 'lucide-react'
|
|
@@ -59,7 +60,7 @@ export type SelectProps<IsMulti extends boolean = false> = {
|
|
|
59
60
|
/** The state object to get the value and check errors from **/
|
|
60
61
|
state?: { errors?: Errors, [key: string]: any } // was unknown|unknown[]
|
|
61
62
|
/** Select variations **/
|
|
62
|
-
mode?:
|
|
63
|
+
mode?: 'combobox'
|
|
63
64
|
/** Pass dependencies to break memoization, handy for onChange/onInputChange **/
|
|
64
65
|
deps?: unknown[]
|
|
65
66
|
/** title used to find related error messages */
|
|
@@ -68,6 +69,12 @@ export type SelectProps<IsMulti extends boolean = false> = {
|
|
|
68
69
|
classNames?: ClassNames
|
|
69
70
|
/** Show a search icon instead of the dropdown arrow **/
|
|
70
71
|
showSearchIcon?: boolean
|
|
72
|
+
/** Combobox only: min input length before the menu opens (0 = disabled) **/
|
|
73
|
+
minLenForSearch?: number
|
|
74
|
+
/** Combobox only: hide the menu when nothing matches instead of showing "No options" (default true) **/
|
|
75
|
+
hideEmptyMenu?: boolean
|
|
76
|
+
/** Hide the right dropdown icon + its padding (text-field look) **/
|
|
77
|
+
hideDropdownIcon?: boolean
|
|
71
78
|
/** All other props are passed to react-select **/
|
|
72
79
|
[key: string]: unknown
|
|
73
80
|
}
|
|
@@ -77,18 +84,24 @@ export const Select = memo(SelectBase, (prev, next) => {
|
|
|
77
84
|
}) as <IsMulti extends boolean = false>(props: SelectProps<IsMulti>) => React.ReactElement | null
|
|
78
85
|
|
|
79
86
|
function SelectBase<IsMulti extends boolean = false>({
|
|
80
|
-
id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode
|
|
81
|
-
showSearchIcon, className,
|
|
87
|
+
id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode, errorTitle, classNames: classNamesProp,
|
|
88
|
+
showSearchIcon, className, minLenForSearch = 0, hideEmptyMenu = true, hideDropdownIcon,
|
|
82
89
|
...props
|
|
83
90
|
}: SelectProps<IsMulti>) {
|
|
84
91
|
let value: unknown|unknown[]
|
|
85
92
|
const error = getErrorFromState(state, errorTitle || name)
|
|
86
93
|
if (!name) throw new Error('Select component requires a `name` and `options` prop')
|
|
87
94
|
|
|
95
|
+
// Combobox: free text with a suggestions dropdown, the typed text is the value (full-width text-input feel)
|
|
96
|
+
const isCombobox = mode === 'combobox'
|
|
97
|
+
|
|
88
98
|
// Get value from value or state
|
|
89
99
|
if (typeof props.value !== 'undefined') value = props.value
|
|
90
100
|
else if (typeof state == 'object') value = deepFind(state, name)
|
|
91
101
|
|
|
102
|
+
// Raw (unconverted) value, used as the combobox input text
|
|
103
|
+
const rawValue = value
|
|
104
|
+
|
|
92
105
|
// If multi-select, filter options by value
|
|
93
106
|
if (Array.isArray(value)) value = options.filter(o => (value as unknown[]).includes(o.value))
|
|
94
107
|
else value = options.find(o => value === o.value)
|
|
@@ -97,9 +110,28 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
97
110
|
if (typeof state == 'object' && typeof value == 'undefined') value = ''
|
|
98
111
|
else if (typeof value == 'undefined') value = null // new
|
|
99
112
|
|
|
113
|
+
// Combobox: input text (option label when matched, else raw typed text) + matches, to gate the menu
|
|
114
|
+
const [focused, setFocused] = useState(false)
|
|
115
|
+
const [pickedFromMenu, setPickedFromMenu] = useState(false) // keeps the menu closed after a selection until typing
|
|
116
|
+
const comboInput = typeof value == 'object' && value && typeof (value as SelectOption).label == 'string'
|
|
117
|
+
? (value as SelectOption).label as string
|
|
118
|
+
: String(rawValue ?? '')
|
|
119
|
+
const comboMatches = !isCombobox ? 0 : options.filter((o) => {
|
|
120
|
+
const label = typeof o.label == 'string' ? o.label : (o.labelSearch || '')
|
|
121
|
+
return filterFn({ label: label, value: String(o.value), data: o }, comboInput)
|
|
122
|
+
}).length
|
|
123
|
+
|
|
100
124
|
// Merge class names (up to 1 level deep)
|
|
101
125
|
const classNames = useMemo(() => {
|
|
102
126
|
const merged = { ...selectClassNames }
|
|
127
|
+
// Combobox: input spans full width + stretches to full control height (cancels the valueContainer's vertical
|
|
128
|
+
// padding) so the whole field is an easy text target, and the text cursor sits on the input, not the control
|
|
129
|
+
if (isCombobox) {
|
|
130
|
+
const m = merged as ClassNames
|
|
131
|
+
m.input = { ...m.input, base: twMerge(m.input?.base,
|
|
132
|
+
'w-full ![grid-template-columns:0_1fr] cursor-text hover:cursor-text '
|
|
133
|
+
+ '(-my-input-y -my-[9px]) ([&>input]:!py-input-y [&>input]:!py-[9px])') }
|
|
134
|
+
}
|
|
103
135
|
for (const key in classNamesProp) {
|
|
104
136
|
const value = classNamesProp[key as keyof ClassNames]
|
|
105
137
|
if (typeof value == 'object') {
|
|
@@ -116,10 +148,12 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
116
148
|
}
|
|
117
149
|
}
|
|
118
150
|
return merged
|
|
119
|
-
}, [classNamesProp])
|
|
151
|
+
}, [classNamesProp, isCombobox])
|
|
120
152
|
|
|
121
153
|
return (
|
|
122
|
-
<div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${className || ''}`)}
|
|
154
|
+
<div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${className || ''}`)}
|
|
155
|
+
// Combobox: clicking the (already focused) control reopens the menu after a selection
|
|
156
|
+
onMouseDown={isCombobox ? () => setPickedFromMenu(false) : undefined}>
|
|
123
157
|
<ReactSelect
|
|
124
158
|
/**
|
|
125
159
|
* react-select prop quick reference (https://react-select.com/props#api):
|
|
@@ -133,8 +167,8 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
133
167
|
* menuIsOpen={false}
|
|
134
168
|
*/
|
|
135
169
|
{...props}
|
|
136
|
-
_nitro={{ prefix, mode, showSearchIcon }}
|
|
137
|
-
key={value as string}
|
|
170
|
+
_nitro={{ prefix, mode, showSearchIcon: showSearchIcon ?? isCombobox }}
|
|
171
|
+
key={isCombobox ? name : value as string}
|
|
138
172
|
unstyled={true}
|
|
139
173
|
inputId={id || name}
|
|
140
174
|
id={containerId}
|
|
@@ -146,6 +180,7 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
146
180
|
menuPlacement="auto"
|
|
147
181
|
minMenuHeight={250}
|
|
148
182
|
onChange={!onChange ? undefined : (o) => {
|
|
183
|
+
if (isCombobox) setPickedFromMenu(true) // close the menu after picking an option
|
|
149
184
|
// An array is returned for multi-select
|
|
150
185
|
type OptionType = IsMulti extends true ? SelectOption[] : SelectedOption
|
|
151
186
|
let value: unknown | unknown[] = []
|
|
@@ -169,7 +204,7 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
169
204
|
)
|
|
170
205
|
}}
|
|
171
206
|
options={options}
|
|
172
|
-
value={value}
|
|
207
|
+
value={isCombobox ? (typeof value == 'object' ? value : null) : value}
|
|
173
208
|
// @ts-expect-error
|
|
174
209
|
classNames={useMemo(() => ({
|
|
175
210
|
// Input container
|
|
@@ -205,6 +240,8 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
205
240
|
MultiValueRemove,
|
|
206
241
|
ValueContainer,
|
|
207
242
|
Menu,
|
|
243
|
+
...(isCombobox ? { Input: ComboboxInput } : {}),
|
|
244
|
+
...(hideDropdownIcon ? { IndicatorsContainer: () => null } : {}),
|
|
208
245
|
...props.components as object,
|
|
209
246
|
}}
|
|
210
247
|
// menuIsOpen={!search ? false : undefined}
|
|
@@ -231,6 +268,29 @@ function SelectBase<IsMulti extends boolean = false>({
|
|
|
231
268
|
// isMulti={true}
|
|
232
269
|
// isDisabled={true}
|
|
233
270
|
// maxMenuHeight={200}
|
|
271
|
+
// Combobox: bind the input text to the value (typed text is the value), options are just suggestions
|
|
272
|
+
{...(isCombobox ? {
|
|
273
|
+
inputValue: comboInput,
|
|
274
|
+
controlShouldRenderValue: false,
|
|
275
|
+
// Gate the menu: needs focus, min input length, and (optionally) a match to avoid the empty "No options" menu.
|
|
276
|
+
// A menuIsOpen prop hard-overrides this.
|
|
277
|
+
menuIsOpen: 'menuIsOpen' in props
|
|
278
|
+
? props.menuIsOpen as boolean | undefined
|
|
279
|
+
: focused && !pickedFromMenu && comboInput.length >= minLenForSearch && (!hideEmptyMenu || comboMatches >= 1),
|
|
280
|
+
onFocus: (e: FocusEvent<HTMLInputElement>) => {
|
|
281
|
+
setFocused(true); setPickedFromMenu(false);
|
|
282
|
+
(props.onFocus as ((e: FocusEvent<HTMLInputElement>) => void) | undefined)?.(e)
|
|
283
|
+
},
|
|
284
|
+
onBlur: (e: FocusEvent<HTMLInputElement>) => {
|
|
285
|
+
setFocused(false);
|
|
286
|
+
(props.onBlur as ((e: FocusEvent<HTMLInputElement>) => void) | undefined)?.(e)
|
|
287
|
+
},
|
|
288
|
+
onInputChange: (v: string, meta: { action: string }) => {
|
|
289
|
+
if (meta.action !== 'input-change') return
|
|
290
|
+
setPickedFromMenu(false) // typing re-opens the menu
|
|
291
|
+
onChange?.({ target: { name: name, value: v } }, null as never)
|
|
292
|
+
},
|
|
293
|
+
} : {})}
|
|
234
294
|
/>
|
|
235
295
|
{error && <div class="mt-1.5 text-xs text-danger-foreground">{error.detail}</div>}
|
|
236
296
|
</div>
|
|
@@ -241,9 +301,14 @@ function Menu(props: MenuProps) {
|
|
|
241
301
|
return props.options.length === 0 ? null : <components.Menu {...props} />
|
|
242
302
|
}
|
|
243
303
|
|
|
304
|
+
// Combobox: keep the input visible after selecting (react-select hides it to show a value chip we don't render)
|
|
305
|
+
function ComboboxInput(props: InputProps) {
|
|
306
|
+
return <components.Input {...props} isHidden={false} />
|
|
307
|
+
}
|
|
308
|
+
|
|
244
309
|
function Control({ children, ...props }: ControlProps) {
|
|
245
310
|
// const selectedOption = props.getValue()[0]
|
|
246
|
-
const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?:
|
|
311
|
+
const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?: 'combobox' } })?._nitro
|
|
247
312
|
return (
|
|
248
313
|
<components.Control {...props}>
|
|
249
314
|
{_nitro?.prefix
|
|
@@ -287,7 +352,7 @@ function SingleValue({ children, ...props }: SingleValueProps) {
|
|
|
287
352
|
|
|
288
353
|
function Option(props: OptionProps) {
|
|
289
354
|
const data = props.data as SelectOption
|
|
290
|
-
// const _nitro = (props.selectProps as { _nitro?: { mode?:
|
|
355
|
+
// const _nitro = (props.selectProps as { _nitro?: { mode?: 'combobox' } })?._nitro
|
|
291
356
|
// @ts-expect-error
|
|
292
357
|
const flagClassName = props.getClassNames('flag')
|
|
293
358
|
return (
|
|
@@ -10,20 +10,20 @@ import React from 'react'
|
|
|
10
10
|
|
|
11
11
|
const perPage = 10
|
|
12
12
|
const allGroups = [
|
|
13
|
-
'Links',
|
|
14
|
-
'Dropdowns',
|
|
15
|
-
'Filters',
|
|
16
|
-
'Button Colors & Sizes',
|
|
17
|
-
'Button Icons',
|
|
18
|
-
'Loading Elements',
|
|
19
|
-
'Varients',
|
|
13
|
+
// 'Links',
|
|
14
|
+
// 'Dropdowns',
|
|
15
|
+
// 'Filters',
|
|
16
|
+
// 'Button Colors & Sizes',
|
|
17
|
+
// 'Button Icons',
|
|
18
|
+
// 'Loading Elements',
|
|
19
|
+
// 'Varients',
|
|
20
20
|
'Selects',
|
|
21
|
-
'Inputs',
|
|
22
|
-
'Date Inputs',
|
|
23
|
-
'File Inputs & Calendar & Time',
|
|
24
|
-
'Tables',
|
|
25
|
-
'Modals & Notifications',
|
|
26
|
-
'Custom Components',
|
|
21
|
+
// 'Inputs',
|
|
22
|
+
// 'Date Inputs',
|
|
23
|
+
// 'File Inputs & Calendar & Time',
|
|
24
|
+
// 'Tables',
|
|
25
|
+
// 'Modals & Notifications',
|
|
26
|
+
// 'Custom Components',
|
|
27
27
|
] as const
|
|
28
28
|
|
|
29
29
|
const statusColors = function(status: string) {
|
|
@@ -69,6 +69,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
69
69
|
colorsMulti: ['blue', 'green'],
|
|
70
70
|
country: 'cd',
|
|
71
71
|
currency: 'nzd',
|
|
72
|
+
percent: 1250,
|
|
72
73
|
customer: '1',
|
|
73
74
|
date: Date.now(),
|
|
74
75
|
dateRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 33],
|
|
@@ -79,6 +80,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
79
80
|
calendarSingle: Date.now(),
|
|
80
81
|
calendarRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 8],
|
|
81
82
|
firstName: 'Bruce',
|
|
83
|
+
combobox: 'blue',
|
|
82
84
|
tableFilter: '',
|
|
83
85
|
errors: [
|
|
84
86
|
{ title: 'address', detail: 'Address is required' },
|
|
@@ -89,7 +91,8 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
89
91
|
brandColor: '#8656ED',
|
|
90
92
|
colorsMulti: ['blue'],
|
|
91
93
|
country: 'au',
|
|
92
|
-
currency: 'btc',
|
|
94
|
+
currency: 'btc',
|
|
95
|
+
percent: 875,
|
|
93
96
|
date: Date.now() + 1000 * 60 * 60 * 24 * 1.2,
|
|
94
97
|
dateRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 5.2],
|
|
95
98
|
dateMultiple: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 3.2],
|
|
@@ -100,6 +103,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
100
103
|
calendarRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 3.2],
|
|
101
104
|
firstName: 'John',
|
|
102
105
|
tableFilter: '',
|
|
106
|
+
combobox: 'green',
|
|
103
107
|
errors: [
|
|
104
108
|
{ title: 'address', detail: 'Address is required' },
|
|
105
109
|
],
|
|
@@ -223,7 +227,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
223
227
|
options={[
|
|
224
228
|
{ label: 'Set Status', onClick: () => { console.log('set', row._id) } },
|
|
225
229
|
{ label: 'Delete', onClick: () => { console.log('remove', row._id) } },
|
|
226
|
-
]}
|
|
230
|
+
]}
|
|
227
231
|
dir={rows.slice(0, perPage).length - 3 < i ? 'top-right' : 'bottom-right'}
|
|
228
232
|
minWidth={100}
|
|
229
233
|
>
|
|
@@ -470,8 +474,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
470
474
|
<Select
|
|
471
475
|
// menuIsOpen={true}
|
|
472
476
|
placeholder="Select or add customer..."
|
|
473
|
-
name="customer"
|
|
474
|
-
mode="customer"
|
|
477
|
+
name="customer"
|
|
475
478
|
state={state}
|
|
476
479
|
onChange={onCustomerInputChange}
|
|
477
480
|
onInputChange={onCustomerSearch}
|
|
@@ -533,6 +536,30 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
533
536
|
], [])}
|
|
534
537
|
/>
|
|
535
538
|
</div>
|
|
539
|
+
<div>
|
|
540
|
+
<label for="combobox">Combobox (Autocomplete /w or free text)</label>
|
|
541
|
+
<Select
|
|
542
|
+
name="combobox"
|
|
543
|
+
mode="combobox"
|
|
544
|
+
minLenForSearch={1}
|
|
545
|
+
state={state}
|
|
546
|
+
onChange={(e) => onChange(e, setState)}
|
|
547
|
+
placeholder="Start typing to search for a color..."
|
|
548
|
+
// hideDropdownIcon={true}
|
|
549
|
+
options={useMemo(() => [
|
|
550
|
+
{ value: 'blue', label: 'Blue' },
|
|
551
|
+
{ value: 'green', label: 'Green' },
|
|
552
|
+
{ value: 'yellow', label: 'Yellow' },
|
|
553
|
+
{ value: 'red', label: 'Red' },
|
|
554
|
+
{ value: 'orange', label: 'Orange' },
|
|
555
|
+
{ value: 'purple', label: 'Purple' },
|
|
556
|
+
{ value: 'pink', label: 'Pink' },
|
|
557
|
+
{ value: 'gray', label: 'Gray' },
|
|
558
|
+
{ value: 'black', label: 'Black' },
|
|
559
|
+
{ value: 'white', label: 'White' },
|
|
560
|
+
], [])}
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
536
563
|
</div>
|
|
537
564
|
</div>
|
|
538
565
|
)}
|
|
@@ -583,13 +610,20 @@ export function Styleguide({ className, elements, children, currencies, groups }
|
|
|
583
610
|
<div>
|
|
584
611
|
<label for="amount">Amount ({state.amount})</label>
|
|
585
612
|
<Field
|
|
586
|
-
name="amount"
|
|
613
|
+
name="amount"
|
|
614
|
+
type="currency"
|
|
615
|
+
state={state}
|
|
616
|
+
currency={state.currency || 'nzd'}
|
|
587
617
|
// Example of using a custom format and currencies, e.g.
|
|
588
618
|
format={'¤#,##0.00'}
|
|
589
619
|
currencies={currencies}
|
|
590
620
|
onChange={(e) => onChange(e, setState)}
|
|
591
621
|
/>
|
|
592
622
|
</div>
|
|
623
|
+
<div>
|
|
624
|
+
<label for="percent">Percent ({state.percent})</label>
|
|
625
|
+
<Field name="percent" type="percent" state={state} onChange={(e) => onChange(e, setState)} minDecimals={1} maxDecimals={4} />
|
|
626
|
+
</div>
|
|
593
627
|
<div>
|
|
594
628
|
<label for="firstName">First Name (disabled)</label>
|
|
595
629
|
<Field name="firstName" state={state} onChange={(e) => onChange(e, setState)} disabled />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
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 🚀",
|