nitro-web 0.0.194 → 0.0.196

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.
@@ -29,6 +29,8 @@ type FieldExtraProps = {
29
29
  placeholder?: string
30
30
  /** title used to find related error messages */
31
31
  errorTitle?: string|RegExp
32
+ /** class names to override the default class names */
33
+ inputClassName?: string
32
34
  }
33
35
  type IconWrapperProps = {
34
36
  iconPos: 'left' | 'right'
@@ -54,7 +56,7 @@ export const Field = memo(FieldBase, (prev, next) => {
54
56
  return isFieldCached(prev, next)
55
57
  })
56
58
 
57
- function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProps) {
59
+ function FieldBase({ state, icon, iconPos: ip, errorTitle, inputClassName, ...props }: FieldProps) {
58
60
  // `type` must be kept as props.type for TS to be happy and follow the conditions below
59
61
  let value!: any
60
62
  let Icon!: React.ReactNode
@@ -104,8 +106,9 @@ function FieldBase({ state, icon, iconPos: ip, errorTitle, ...props }: FieldProp
104
106
  }
105
107
 
106
108
  // Classname
107
- const inputClassName = getInputClasses({ error, Icon, iconPos, type })
108
- const commonProps = { id: id, value: value, className: inputClassName }
109
+ const inputClasses = getInputClasses({ error, Icon, iconPos, type })
110
+ const inputClassName2 = inputClassName ? twMerge(inputClasses, inputClassName) : inputClasses.replaceAll(/\(|\)/g, '')
111
+ const commonProps = { id: id, value: value, className: inputClassName2 }
109
112
 
110
113
  // Type has to be referenced as props.type for TS to be happy
111
114
  if (!type || type == 'text' || type == 'number' || type == 'password' || type == 'email' || type == 'filter' || type == 'search') {
@@ -160,9 +163,11 @@ function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?:
160
163
  const py = 'py-[9px] py-input-y'
161
164
  return (
162
165
  'block col-start-1 row-start-1 w-full rounded-md bg-white disabled:bg-input-disabled-bg text-input-base outline outline-1 -outline-offset-1 ' +
163
- 'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' + `${py} ${px} ` +
164
- (iconPos == 'right' && Icon ? 'pr-[32px] pr-input-x-icon pl-input-x ' : '') +
165
- (iconPos == 'left' && Icon ? 'pl-[32px] pl-input-x-icon pr-input-x ' : 'px-input-x ') +
166
+ 'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 (' +
167
+ `${py} ${px} ` +
168
+ (iconPos == 'right' && Icon ? 'pr-[32px] pr-input-x-icon pl-input-x ' : '') +
169
+ (iconPos == 'left' && Icon ? 'pl-[32px] pl-input-x-icon pr-input-x ' : 'px-input-x ') +
170
+ ')' +
166
171
  (iconPos == 'left' && Icon && type == 'color' ? 'indent-[5px] ' : '') +
167
172
  (error
168
173
  ? 'text-danger-foreground outline-danger focus:outline-danger '
@@ -3,18 +3,17 @@ import { css } from 'twin.macro'
3
3
  import { memo, useMemo, Fragment } from 'react'
4
4
  import ReactSelect, {
5
5
  components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
6
- DropdownIndicatorProps, MultiValueRemoveProps, ClassNamesConfig,
6
+ DropdownIndicatorProps, MultiValueRemoveProps, // ClassNamesConfig,
7
7
  ValueContainerProps,
8
8
  } from 'react-select'
9
9
  import { CheckCircleIcon } from '@heroicons/react/20/solid'
10
- import { ChevronsUpDownIcon, XIcon } from 'lucide-react'
10
+ import { ChevronsUpDownIcon, SearchIcon, XIcon } from 'lucide-react'
11
11
  import { isFieldCached } from 'nitro-web'
12
12
  import { getErrorFromState, deepFind, twMerge } from 'nitro-web/util'
13
13
  import { Errors } from 'nitro-web/types'
14
14
 
15
15
  const filterFn = createFilter()
16
16
 
17
- type NitroClassNamesConfig = ClassNamesConfig & { flag?: () => string }
18
17
  type GetSelectClassName = {
19
18
  name: string
20
19
  isFocused?: boolean
@@ -22,6 +21,7 @@ type GetSelectClassName = {
22
21
  isDisabled?: boolean
23
22
  hasError?: boolean
24
23
  usePrefixes?: boolean
24
+ classNames?: ClassNames
25
25
  }
26
26
  export type SelectOption = {
27
27
  value: unknown,
@@ -58,7 +58,9 @@ export type SelectProps = {
58
58
  /** title used to find related error messages */
59
59
  errorTitle?: string|RegExp
60
60
  /** Extend or override individual react-select part class names — merged with defaults via twMerge **/
61
- classNames?: NitroClassNamesConfig
61
+ classNames?: ClassNames
62
+ /** Show a search icon instead of the dropdown arrow **/
63
+ showSearchIcon?: boolean
62
64
  /** All other props are passed to react-select **/
63
65
  [key: string]: unknown
64
66
  }
@@ -67,8 +69,10 @@ export const Select = memo(SelectBase, (prev, next) => {
67
69
  return isFieldCached(prev, next)
68
70
  })
69
71
 
70
- function SelectBase({
71
- id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, classNames, ...props
72
+ function SelectBase({
73
+ id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, classNames: classNamesProp,
74
+ showSearchIcon, className,
75
+ ...props
72
76
  }: SelectProps) {
73
77
  let value: unknown|unknown[]
74
78
  const error = getErrorFromState(state, errorTitle || name)
@@ -85,33 +89,29 @@ function SelectBase({
85
89
  // Input is always controlled if state is passed in
86
90
  if (typeof state == 'object' && typeof value == 'undefined') value = ''
87
91
 
88
- const mergedClassNames = useMemo(() => mergeClassNames({
89
- // Input container
90
- control: (p) => getSelectClassName({ name: 'control', hasError: !!error, ...p }),
91
- valueContainer: () => getSelectClassName({ name: 'valueContainer' }),
92
- // Input container objects
93
- input: () => getSelectClassName({ name: 'input', hasError: !!error }),
94
- multiValue: () => getSelectClassName({ name: 'multiValue' }),
95
- multiValueLabel: () => '',
96
- multiValueRemove: () => getSelectClassName({ name: 'multiValueRemove' }),
97
- placeholder: () => getSelectClassName({ name: 'placeholder' }),
98
- singleValue: (p) => getSelectClassName({ name: 'singleValue', hasError: !!error, isDisabled: p.isDisabled }),
99
- // Indicators
100
- clearIndicator: () => getSelectClassName({ name: 'clearIndicator' }),
101
- dropdownIndicator: () => getSelectClassName({ name: 'dropdownIndicator' }),
102
- indicatorsContainer: () => getSelectClassName({ name: 'indicatorsContainer' }),
103
- indicatorSeparator: () => getSelectClassName({ name: 'indicatorSeparator' }),
104
- // Dropmenu
105
- menu: () => getSelectClassName({ name: 'menu' }),
106
- groupHeading: () => getSelectClassName({ name: 'groupHeading' }),
107
- noOptionsMessage: () => getSelectClassName({ name: 'noOptionsMessage' }),
108
- option: (p) => getSelectClassName({ name: 'option', ...p }),
109
- // Nitro specific
110
- flag: () => getSelectClassName({ name: 'flag' }),
111
- }, classNames), [!!error, classNames])
92
+ // Merge class names (up to 1 level deep)
93
+ const classNames = useMemo(() => {
94
+ const merged = { ...selectClassNames }
95
+ for (const key in classNamesProp) {
96
+ const value = classNamesProp[key as keyof ClassNames]
97
+ if (typeof value == 'object') {
98
+ // @ts-expect-error
99
+ merged[key] = { ...merged[key] }
100
+ for (const key2 in value) {
101
+ const value2 = value[key2 as keyof typeof value]
102
+ // @ts-expect-error
103
+ merged[key][key2] = twMerge(merged[key][key2], value2)
104
+ }
105
+ } else {
106
+ // @ts-expect-error
107
+ merged[key] = twMerge(merged[key], value)
108
+ }
109
+ }
110
+ return merged
111
+ }, [classNamesProp])
112
112
 
113
113
  return (
114
- <div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${props.className || ''}`)}>
114
+ <div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${className || ''}`)}>
115
115
  <ReactSelect
116
116
  /**
117
117
  * react-select prop quick reference (https://react-select.com/props#api):
@@ -126,7 +126,7 @@ function SelectBase({
126
126
  */
127
127
  {...props}
128
128
  // @ts-expect-error
129
- _nitro={{ prefix, mode }}
129
+ _nitro={{ prefix, mode, showSearchIcon }}
130
130
  key={value as string}
131
131
  unstyled={true}
132
132
  inputId={id || name}
@@ -147,15 +147,40 @@ function SelectBase({
147
147
  }}
148
148
  options={options}
149
149
  value={value}
150
- classNames={mergedClassNames}
151
- components={{
152
- Control,
153
- SingleValue,
150
+ classNames={useMemo(() => ({
151
+ // Input container
152
+ control: (p) => getSelectClassName({ name: 'control', hasError: !!error, ...p, classNames: classNames }),
153
+ valueContainer: () => getSelectClassName({ name: 'valueContainer', classNames: classNames }),
154
+ // Input container objects
155
+ input: () => getSelectClassName({ name: 'input', hasError: !!error, classNames: classNames }),
156
+ multiValue: () => getSelectClassName({ name: 'multiValue', classNames: classNames }),
157
+ multiValueLabel: () => '',
158
+ multiValueRemove: () => getSelectClassName({ name: 'multiValueRemove', classNames: classNames }),
159
+ placeholder: () => getSelectClassName({ name: 'placeholder', classNames: classNames }),
160
+ singleValue: (p) => getSelectClassName({ name: 'singleValue', hasError: !!error, isDisabled: p.isDisabled,
161
+ classNames: classNames }),
162
+ // Indicators
163
+ clearIndicator: () => getSelectClassName({ name: 'clearIndicator', classNames: classNames }),
164
+ dropdownIndicator: () => getSelectClassName({ name: 'dropdownIndicator', classNames: classNames }),
165
+ indicatorsContainer: () => getSelectClassName({ name: 'indicatorsContainer', classNames: classNames }),
166
+ indicatorSeparator: () => getSelectClassName({ name: 'indicatorSeparator', classNames: classNames }),
167
+ // Dropmenu
168
+ menu: () => getSelectClassName({ name: 'menu', classNames: classNames }),
169
+ groupHeading: () => getSelectClassName({ name: 'groupHeading', classNames: classNames }),
170
+ noOptionsMessage: () => getSelectClassName({ name: 'noOptionsMessage', classNames: classNames }),
171
+ option: (p) => getSelectClassName({ name: 'option', ...p, classNames: classNames }),
172
+ // Nitro specific
173
+ flag: () => getSelectClassName({ name: 'flag', classNames: classNames }),
174
+ }), [!!error, classNames])}
175
+ components={{
176
+ Control,
177
+ SingleValue,
154
178
  Option,
155
- DropdownIndicator,
156
- ClearIndicator,
179
+ DropdownIndicator,
180
+ ClearIndicator,
157
181
  MultiValueRemove,
158
182
  ValueContainer,
183
+ ...props.components as object,
159
184
  }}
160
185
  styles={{
161
186
  menu: (base) => ({
@@ -245,9 +270,13 @@ function Option(props: OptionProps) {
245
270
  }
246
271
 
247
272
  const DropdownIndicator = (props: DropdownIndicatorProps) => {
273
+ const _nitro = (props.selectProps as { _nitro?: { showSearchIcon?: boolean } })?._nitro
274
+ const isSearch = _nitro?.showSearchIcon
275
+ const Icon = isSearch ? SearchIcon : ChevronsUpDownIcon
248
276
  return (
249
277
  <components.DropdownIndicator {...props}>
250
- <ChevronsUpDownIcon size={15} className="text-[#b6b8be] text-input-icon -my-0.5 -mx-[1px]" />
278
+ <Icon size={isSearch ? 13 : 15} strokeWidth={isSearch ? 2.4 : undefined}
279
+ className="text-[#b6b8be] text-input-icon -my-0.5 -mx-[1px]" />
251
280
  </components.DropdownIndicator>
252
281
  )
253
282
  }
@@ -278,7 +307,7 @@ const selectClassNames = {
278
307
  error: 'outline-danger',
279
308
  disabled: 'cursor-not-allowed bg-input-disabled-bg',
280
309
  },
281
- valueContainer: 'gap-1 py-[9px] px-[12px] py-input-y px-input-x', // dont twMerge (input-x is optional)
310
+ valueContainer: 'gap-1 (py-[9px] px-[12px] py-input-y px-input-x)', // dont twMerge (input-x is optional)
282
311
  // Input container objects
283
312
  input: {
284
313
  base: 'text-input',
@@ -309,14 +338,41 @@ const selectClassNames = {
309
338
  },
310
339
  // Nitro specific
311
340
  flag: 'align-middle text-[1.2em] leading-[1em] mr-1.5 flex-shrink-0',
341
+ } as const
342
+
343
+ type ClassNames = {
344
+ // Input container
345
+ control?: { base?: string, focus?: string, error?: string, disabled?: string }
346
+ valueContainer?: string
347
+ // Input container objects
348
+ input?: { base?: string, error?: string, disabled?: string }
349
+ multiValue?: string
350
+ multiValueLabel?: string
351
+ multiValueRemove?: string
352
+ placeholder?: string
353
+ singleValue?: { base?: string, error?: string, disabled?: string }
354
+ // Icon indicators
355
+ clearIndicator?: string
356
+ dropdownIndicator?: string
357
+ indicatorsContainer?: string
358
+ indicatorSeparator?: string
359
+ // Dropdown menu
360
+ menu?: string
361
+ groupHeading?: string
362
+ noOptionsMessage?: string
363
+ option?: { base?: string, hover?: string, selected?: string }
364
+ // Nitro specific
365
+ flag?: string
312
366
  }
313
367
 
314
- export function getSelectClassName({ name, isFocused, isSelected, isDisabled, hasError, usePrefixes }: GetSelectClassName) {
368
+ export function getSelectClassName({ name, isFocused, isSelected, isDisabled, hasError, usePrefixes, classNames }: GetSelectClassName) {
315
369
  // Returns a class list that conditionally includes hover/focus modifier classes, or uses CSS modifiers, e.g. hover:, focus:
316
370
  // @ts-expect-error
317
- const obj = selectClassNames[name]
371
+ const obj = classNames?.[name] || selectClassNames[name]
318
372
  let output = obj?.base
319
- if (typeof obj == 'string') return obj // no modifiers
373
+
374
+ if (typeof obj == 'string' && obj.includes('(')) return obj.replaceAll(/\(|\)/g, '') // no modifiers & still has group wrappers
375
+ else if (typeof obj == 'string') return obj // no modifiers
320
376
 
321
377
  if (usePrefixes) {
322
378
  if (obj.focus) output += ' ' + obj.focus.split(' ').map((part: string) => `focus:${part}`).join(' ')
@@ -332,11 +388,6 @@ export function getSelectClassName({ name, isFocused, isSelected, isDisabled, ha
332
388
  return twMerge(output)
333
389
  }
334
390
 
335
- function mergeClassNames(defaults: NitroClassNamesConfig, custom?: NitroClassNamesConfig): NitroClassNamesConfig {
336
- if (!custom) return defaults
337
- return { ...defaults, ...custom }
338
- }
339
-
340
391
  const style = css`
341
392
  /*
342
393
  todo: add these as tailwind classes
@@ -503,7 +503,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
503
503
  />
504
504
  </div>
505
505
  <div>
506
- <label for="currency">Currencies</label>
506
+ <label for="currency">Currencies (and classNames prop)</label>
507
507
  <Select
508
508
  name="currency"
509
509
  state={state}
@@ -512,6 +512,11 @@ export function Styleguide({ className, elements, children, currencies, groups }
512
512
  { value: 'aud', label: 'Australian Dollar' },
513
513
  ]), [])}
514
514
  onChange={(e) => onChange(e, setState)}
515
+ classNames={{
516
+ control: {
517
+ focus: 'outline-secondary',
518
+ },
519
+ }}
515
520
  />
516
521
  </div>
517
522
  <div>
@@ -522,7 +527,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
522
527
  options={useMemo(() => [
523
528
  { value: 'edit', label: 'Edit' },
524
529
  { value: 'delete', label: 'Delete' },
525
- ], [])}
530
+ ], [])}
526
531
  />
527
532
  </div>
528
533
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.194",
3
+ "version": "0.0.196",
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 🚀",