nitro-web 0.0.193 → 0.0.195

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.
@@ -7,7 +7,7 @@ import ReactSelect, {
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'
@@ -22,6 +22,7 @@ type GetSelectClassName = {
22
22
  isDisabled?: boolean
23
23
  hasError?: boolean
24
24
  usePrefixes?: boolean
25
+ classNames?: ClassNames
25
26
  }
26
27
  export type SelectOption = {
27
28
  value: unknown,
@@ -58,7 +59,9 @@ export type SelectProps = {
58
59
  /** title used to find related error messages */
59
60
  errorTitle?: string|RegExp
60
61
  /** Extend or override individual react-select part class names — merged with defaults via twMerge **/
61
- classNames?: NitroClassNamesConfig
62
+ classNames?: ClassNames
63
+ /** Show a search icon instead of the dropdown arrow **/
64
+ showSearchIcon?: boolean
62
65
  /** All other props are passed to react-select **/
63
66
  [key: string]: unknown
64
67
  }
@@ -67,8 +70,10 @@ export const Select = memo(SelectBase, (prev, next) => {
67
70
  return isFieldCached(prev, next)
68
71
  })
69
72
 
70
- function SelectBase({
71
- id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, classNames, ...props
73
+ function SelectBase({
74
+ id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, classNames: classNamesProp,
75
+ showSearchIcon, className,
76
+ ...props
72
77
  }: SelectProps) {
73
78
  let value: unknown|unknown[]
74
79
  const error = getErrorFromState(state, errorTitle || name)
@@ -85,33 +90,29 @@ function SelectBase({
85
90
  // Input is always controlled if state is passed in
86
91
  if (typeof state == 'object' && typeof value == 'undefined') value = ''
87
92
 
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])
93
+ // Merge class names (up to 1 level deep)
94
+ const classNames = useMemo(() => {
95
+ const merged = { ...selectClassNames }
96
+ for (const key in classNamesProp) {
97
+ const value = classNamesProp[key as keyof ClassNames]
98
+ if (typeof value == 'object') {
99
+ // @ts-expect-error
100
+ merged[key] = { ...merged[key] }
101
+ for (const key2 in value) {
102
+ const value2 = value[key2 as keyof typeof value]
103
+ // @ts-expect-error
104
+ merged[key][key2] = twMerge(merged[key][key2], value2)
105
+ }
106
+ } else {
107
+ // @ts-expect-error
108
+ merged[key] = twMerge(merged[key], value)
109
+ }
110
+ }
111
+ return merged
112
+ }, [classNamesProp])
112
113
 
113
114
  return (
114
- <div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${props.className || ''}`)}>
115
+ <div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${className || ''}`)}>
115
116
  <ReactSelect
116
117
  /**
117
118
  * react-select prop quick reference (https://react-select.com/props#api):
@@ -126,7 +127,7 @@ function SelectBase({
126
127
  */
127
128
  {...props}
128
129
  // @ts-expect-error
129
- _nitro={{ prefix, mode }}
130
+ _nitro={{ prefix, mode, showSearchIcon }}
130
131
  key={value as string}
131
132
  unstyled={true}
132
133
  inputId={id || name}
@@ -147,15 +148,40 @@ function SelectBase({
147
148
  }}
148
149
  options={options}
149
150
  value={value}
150
- classNames={mergedClassNames}
151
- components={{
152
- Control,
153
- SingleValue,
151
+ classNames={useMemo(() => ({
152
+ // Input container
153
+ control: (p) => getSelectClassName({ name: 'control', hasError: !!error, ...p, classNames: classNames }),
154
+ valueContainer: () => getSelectClassName({ name: 'valueContainer', classNames: classNames }),
155
+ // Input container objects
156
+ input: () => getSelectClassName({ name: 'input', hasError: !!error, classNames: classNames }),
157
+ multiValue: () => getSelectClassName({ name: 'multiValue', classNames: classNames }),
158
+ multiValueLabel: () => '',
159
+ multiValueRemove: () => getSelectClassName({ name: 'multiValueRemove', classNames: classNames }),
160
+ placeholder: () => getSelectClassName({ name: 'placeholder', classNames: classNames }),
161
+ singleValue: (p) => getSelectClassName({ name: 'singleValue', hasError: !!error, isDisabled: p.isDisabled,
162
+ classNames: classNames }),
163
+ // Indicators
164
+ clearIndicator: () => getSelectClassName({ name: 'clearIndicator', classNames: classNames }),
165
+ dropdownIndicator: () => getSelectClassName({ name: 'dropdownIndicator', classNames: classNames }),
166
+ indicatorsContainer: () => getSelectClassName({ name: 'indicatorsContainer', classNames: classNames }),
167
+ indicatorSeparator: () => getSelectClassName({ name: 'indicatorSeparator', classNames: classNames }),
168
+ // Dropmenu
169
+ menu: () => getSelectClassName({ name: 'menu', classNames: classNames }),
170
+ groupHeading: () => getSelectClassName({ name: 'groupHeading', classNames: classNames }),
171
+ noOptionsMessage: () => getSelectClassName({ name: 'noOptionsMessage', classNames: classNames }),
172
+ option: (p) => getSelectClassName({ name: 'option', ...p, classNames: classNames }),
173
+ // Nitro specific
174
+ flag: () => getSelectClassName({ name: 'flag', classNames: classNames }),
175
+ }), [!!error, classNames])}
176
+ components={{
177
+ Control,
178
+ SingleValue,
154
179
  Option,
155
- DropdownIndicator,
156
- ClearIndicator,
180
+ DropdownIndicator,
181
+ ClearIndicator,
157
182
  MultiValueRemove,
158
183
  ValueContainer,
184
+ ...props.components as object,
159
185
  }}
160
186
  styles={{
161
187
  menu: (base) => ({
@@ -245,9 +271,13 @@ function Option(props: OptionProps) {
245
271
  }
246
272
 
247
273
  const DropdownIndicator = (props: DropdownIndicatorProps) => {
274
+ const _nitro = (props.selectProps as { _nitro?: { showSearchIcon?: boolean } })?._nitro
275
+ const isSearch = _nitro?.showSearchIcon
276
+ const Icon = isSearch ? SearchIcon : ChevronsUpDownIcon
248
277
  return (
249
278
  <components.DropdownIndicator {...props}>
250
- <ChevronsUpDownIcon size={15} className="text-[#b6b8be] text-input-icon -my-0.5 -mx-[1px]" />
279
+ <Icon size={isSearch ? 13 : 15} strokeWidth={isSearch ? 2.4 : undefined}
280
+ className="text-[#b6b8be] text-input-icon -my-0.5 -mx-[1px]" />
251
281
  </components.DropdownIndicator>
252
282
  )
253
283
  }
@@ -309,12 +339,36 @@ const selectClassNames = {
309
339
  },
310
340
  // Nitro specific
311
341
  flag: 'align-middle text-[1.2em] leading-[1em] mr-1.5 flex-shrink-0',
342
+ } as const
343
+
344
+ type ClassNames = Partial<Omit<typeof selectClassNames, 'control' | 'input' | 'singleValue' | 'option'>> & {
345
+ control?: {
346
+ base?: string
347
+ focus?: string
348
+ error?: string
349
+ disabled?: string
350
+ }
351
+ input?: {
352
+ base?: string
353
+ error?: string
354
+ disabled?: string
355
+ }
356
+ singleValue?: {
357
+ base?: string
358
+ error?: string
359
+ disabled?: string
360
+ }
361
+ option?: {
362
+ base?: string
363
+ hover?: string
364
+ selected?: string
365
+ }
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
373
  if (typeof obj == 'string') return obj // no modifiers
320
374
 
@@ -332,11 +386,6 @@ export function getSelectClassName({ name, isFocused, isSelected, isDisabled, ha
332
386
  return twMerge(output)
333
387
  }
334
388
 
335
- function mergeClassNames(defaults: NitroClassNamesConfig, custom?: NitroClassNamesConfig): NitroClassNamesConfig {
336
- if (!custom) return defaults
337
- return { ...defaults, ...custom }
338
- }
339
-
340
389
  const style = css`
341
390
  /*
342
391
  todo: add these as tailwind classes
@@ -487,23 +487,23 @@ export function Styleguide({ className, elements, children, currencies, groups }
487
487
  {
488
488
  value: '1',
489
489
  label: 'Wayne Enterprises',
490
- IconLeft: <Initials initials="WE" className="inline-flex my-[-3px] mr-2 flex-shrink-0" />,
490
+ IconLeft: <Initials initials="WE" className="inline-flex my-[-2px] mr-2 flex-shrink-0" />,
491
491
  },
492
492
  {
493
493
  value: '2',
494
494
  label: 'Iceberg Lounge Limited',
495
- IconLeft: <Initials initials="IL" className="inline-flex my-[-3px] mr-2 flex-shrink-0" />,
495
+ IconLeft: <Initials initials="IL" className="inline-flex my-[-2px] mr-2 flex-shrink-0" />,
496
496
  },
497
497
  {
498
498
  value: '3',
499
499
  label: 'Ace Chemicals Company',
500
- IconLeft: <Initials initials="AC" className="inline-flex my-[-3px] mr-2 flex-shrink-0" />,
500
+ IconLeft: <Initials initials="AC" className="inline-flex my-[-2px] mr-2 flex-shrink-0" />,
501
501
  },
502
502
  ], [customerSearch])}
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.193",
3
+ "version": "0.0.195",
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 🚀",