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 iso, e.g. 'nzd' */
26
- currency: string
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({ currency='nzd', currencies, format, onChange: onChangeProp, ...props }: FieldCurrencyProps) {
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(() => getCurrencySettings(currency, currencies, format, props.name))
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
- // console.log('to', type, value, value2)
90
- return type === 'cents' || ref.current.dontFix ? value2 : value2.toFixed(maxDecimals)
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 || settings.suffix}
141
+ {settings.prefix}
121
142
  </span>
122
143
  </div>
123
144
  )
124
145
  }
125
146
 
126
- function getCurrencySettings(currency: string, currencies?: Currencies, format?: string, name?: string) {
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' | 'currency' | 'date' | 'color' | 'file'
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 == 'date') {
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?: string
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='', errorTitle, classNames: classNamesProp,
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?: string } })?._nitro
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?: string } })?._nitro
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" type="currency" state={state} currency={state.currency || 'nzd'}
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.12",
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 🚀",