nitro-web 0.0.65 → 0.0.66

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/client/index.ts CHANGED
@@ -37,19 +37,20 @@ export { Modal } from '../components/partials/element/modal'
37
37
  export { Sidebar, type SidebarProps } from '../components/partials/element/sidebar'
38
38
  export { Tooltip } from '../components/partials/element/tooltip'
39
39
  export { Topbar } from '../components/partials/element/topbar'
40
- // Component Form
40
+
41
+ // Component Form Elements
41
42
  export { Checkbox } from '../components/partials/form/checkbox'
42
43
  export { Drop } from '../components/partials/form/drop'
43
44
  export { DropHandler } from '../components/partials/form/drop-handler'
44
45
  export { FormError } from '../components/partials/form/form-error'
45
- export { Field, isFieldCached } from '../components/partials/form/field'
46
+ export { Field, isFieldCached, type FieldProps } from '../components/partials/form/field'
46
47
  export { FieldColor, type FieldColorProps } from '../components/partials/form/field-color'
47
48
  export { FieldCurrency, type FieldCurrencyProps } from '../components/partials/form/field-currency'
48
49
  export { FieldDate, type FieldDateProps } from '../components/partials/form/field-date'
49
50
  export { Location } from '../components/partials/form/location'
50
- export { Select, getSelectStyle } from '../components/partials/form/select'
51
+ export { Select, getSelectStyle, type SelectProps } from '../components/partials/form/select'
51
52
 
52
- // Component Other
53
+ // Component Other Components
53
54
  export { IsFirstRender } from '../components/partials/is-first-render'
54
55
 
55
56
  // Expose the injected config
@@ -11,6 +11,7 @@ type Button = React.ButtonHTMLAttributes<HTMLButtonElement> & {
11
11
  IconLeftEnd?: React.ReactNode|'v'
12
12
  IconRight?: React.ReactNode|'v'
13
13
  IconRightEnd?: React.ReactNode|'v'
14
+ IconCenter?: React.ReactNode|'v'
14
15
  children?: React.ReactNode|'v'
15
16
  }
16
17
 
@@ -23,16 +24,18 @@ export function Button({
23
24
  IconLeft,
24
25
  IconLeftEnd,
25
26
  IconRight,
26
- IconRightEnd,
27
+ IconRightEnd,
28
+ IconCenter,
27
29
  children,
28
30
  type='button',
29
31
  ...props
30
32
  }: Button) {
31
33
  // const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
32
- const iconPosition = IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : 'none'
34
+ const iconPosition =
35
+ IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : IconCenter ? 'center' : 'none'
33
36
  const base =
34
- 'relative inline-block text-center font-medium shadow-sm focus-visible:outline focus-visible:outline-2 ' +
35
- 'focus-visible:outline-offset-2 ring-inset ring-1'
37
+ 'relative inline-flex items-center justify-center text-center font-medium shadow-sm focus-visible:outline ' +
38
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 ring-inset ring-1' + (children ? '' : ' aspect-square')
36
39
 
37
40
  // Button colors, you can use custom colors by using className instead
38
41
  const colors = {
@@ -46,14 +49,14 @@ export function Button({
46
49
 
47
50
  // Button sizes (px is better for height consistency)
48
51
  const sizes = {
49
- xs: 'px-[6px] py-[3px] px-button-x-xs py-button-y-xs text-xs rounded',
50
- sm: 'px-[10px] py-[6px] px-button-x-sm py-button-y-sm text-button-size rounded-md',
51
- md: 'px-[12px] py-[9px] px-button-x-md py-button-y-md text-button-size rounded-md', // default
52
- lg: 'px-[18px] py-[11px] px-button-x-lg py-button-y-lg text-button-size rounded-md',
52
+ xs: 'px-[6px] h-[25px] px-button-x-xs h-button-h-xs text-xs !text-button-xs rounded',
53
+ sm: 'px-[10px] h-[32px] px-button-x-sm h-button-h-sm text-button-md text-button-sm rounded-md',
54
+ md: 'px-[12px] h-[38px] px-button-x-md h-button-h-md text-button-md rounded-md', // default
55
+ lg: 'px-[18px] h-[42px] px-button-x-lg h-button-h-lg text-button-md !text-button-lg rounded-md',
53
56
  }
54
57
 
55
58
  const appliedColor = color === 'custom' ? customColor : colors[color]
56
- const contentLayout = `gap-x-1.5 ${iconPosition == 'none' ? '' : 'inline-flex items-center justify-center'}`
59
+ const contentLayout = `gap-x-1.5 ${iconPosition == 'none' ? '' : ''}`
57
60
  const loading = isLoading ? '[&>*]:opacity-0 text-opacity-0' : ''
58
61
 
59
62
  function getIcon(Icon: React.ReactNode | 'v') {
@@ -68,11 +71,18 @@ export function Button({
68
71
  class={twMerge(`${base} ${sizes[size]} ${appliedColor} ${contentLayout} ${loading} nitro-button ${className||''}`)}
69
72
  {...props}
70
73
  >
71
- {IconLeft && getIcon(IconLeft)}
72
- {IconLeftEnd && getIcon(IconLeftEnd)}
73
- <span class={`${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1' : ''}`}>{children}</span>
74
- {IconRight && getIcon(IconRight)}
75
- {IconRightEnd && getIcon(IconRightEnd)}
74
+ {
75
+ IconCenter &&
76
+ <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
77
+ {getIcon(IconCenter)}
78
+ </span>
79
+ }
80
+ {(IconLeft || IconLeftEnd) && getIcon(IconLeft || IconLeftEnd)}
81
+ <span class={`flex items-center ${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1 justify-center' : ''}`}>
82
+ <span className="w-0">&nbsp;</span> {/* for min-height */}
83
+ {children}
84
+ </span>
85
+ {(IconRight || IconRightEnd) && getIcon(IconRight || IconRightEnd)}
76
86
  {
77
87
  isLoading &&
78
88
  <span className={
@@ -1,24 +1,25 @@
1
1
  import { forwardRef, Dispatch, SetStateAction, useRef, useEffect, useImperativeHandle } from 'react'
2
- import { Button, Dropdown, Field, Select, twMerge } from 'nitro-web'
3
- import { camelCaseToTitle, debounce, omit, queryString, queryObject } from 'nitro-web/util'
2
+ import { Button, Dropdown, Field, Select, type FieldProps, type SelectProps } from 'nitro-web'
3
+ import { camelCaseToTitle, debounce, omit, queryString, queryObject, twMerge } from 'nitro-web/util'
4
4
  import { ListFilterIcon } from 'lucide-react'
5
5
 
6
- export type FilterType = {
7
- name: string
8
- type: 'text'|'date'|'search'|'select'
6
+ type CommonProps = {
9
7
  label?: string
10
- enums?: { label: string, value: string }[]
11
- placeholder?: string
8
+ rowClassName?: string
12
9
  }
10
+ export type FilterType = (
11
+ | FieldProps & CommonProps
12
+ | ({ type: 'select' } & SelectProps & CommonProps)
13
+ )
13
14
 
14
15
  type FilterState = {
15
16
  [key: string]: string | true
16
17
  }
17
18
 
18
19
  type FiltersProps = {
20
+ state?: FilterState
21
+ setState?: Dispatch<SetStateAction<FilterState>>
19
22
  filters?: FilterType[]
20
- state: FilterState
21
- setState: Dispatch<SetStateAction<FilterState>>
22
23
  elements?: {
23
24
  Button?: typeof Button
24
25
  Dropdown?: typeof Dropdown
@@ -31,6 +32,7 @@ type FiltersProps = {
31
32
  buttonClassName?: string
32
33
  buttonText?: string
33
34
  buttonCounterClassName?: string
35
+ filtersContainerClassName?: string
34
36
  }
35
37
 
36
38
  export type FiltersHandleType = {
@@ -40,14 +42,26 @@ export type FiltersHandleType = {
40
42
  const debounceTime = 250
41
43
 
42
44
  export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
43
- filters, state, setState, elements, dropdownProps, buttonProps, buttonClassName, buttonText, buttonCounterClassName,
45
+ filters,
46
+ setState: setState2,
47
+ state: state2,
48
+ buttonClassName,
49
+ buttonCounterClassName,
50
+ buttonProps,
51
+ buttonText,
52
+ dropdownProps,
53
+ elements,
54
+ filtersContainerClassName,
44
55
  }, ref) => {
45
56
  const location = useLocation()
46
57
  const navigate = useNavigate()
47
- const stateRef = useRef(state)
48
58
  const [lastUpdated, setLastUpdated] = useState(0)
49
59
  const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
50
- const count = Object.keys(state).length - (Object.keys(state).includes('page') ? 1 : 0)
60
+ const [state3, setState3] = useState(() => ({ ...queryObject(location.search) }))
61
+ const [state, setState] = [state2 || state3, setState2 || setState3]
62
+ const stateRef = useRef(state)
63
+ const count = useMemo(() => Object.keys(state).filter((k) => state[k] && filters?.some((f) => f.name === k)).length, [state, filters])
64
+
51
65
  const Elements = {
52
66
  Button: elements?.Button || Button,
53
67
  Dropdown: elements?.Dropdown || Dropdown,
@@ -85,9 +99,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
85
99
 
86
100
  function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
87
101
  e.preventDefault()
88
- setState((s) => ({
89
- ...(s.page ? { page: s.page } : {}), // keep pagination
90
- } as FilterState))
102
+ setState((s) => omit(s, filters?.map((f) => f.name) || []) as FilterState)
91
103
  onAfterChange()
92
104
  }
93
105
 
@@ -106,8 +118,8 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
106
118
  const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
107
119
  navigate(location.pathname + queryStr, { replace: true })
108
120
  }
109
-
110
- if (!filters) return null
121
+
122
+ // if (!filters) return null
111
123
  return (
112
124
  <Elements.Dropdown
113
125
  dir="bottom-right"
@@ -119,47 +131,31 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
119
131
  <div class="text-lg font-semibold">Filters</div>
120
132
  <Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
121
133
  </div>
122
- <div class="flex flex-col px-4 py-4 mb-[-6px]">
134
+ <div class={twMerge(`flex flex-wrap gap-4 px-4 py-4 pb-6 ${filtersContainerClassName || ''}`)}>
123
135
  {
124
- filters.map((filter) => (
125
- <div key={filter.name}>
136
+ filters?.map(({label, rowClassName, ...filter}, i) => (
137
+ <div key={i} class={twMerge(`w-full ${rowClassName||''}`)}>
126
138
  <div class="flex justify-between">
127
- <label for={filter.name}>{filter.label || camelCaseToTitle(filter.name)}</label>
139
+ <label for={filter.id || filter.name}>{label || camelCaseToTitle(filter.name)}</label>
128
140
  <a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
129
141
  </div>
130
142
  {
131
- (filter.type === 'text' || filter.type === 'search') &&
132
- <Elements.Field
133
- class="mb-4"
134
- name={filter.name}
135
- type={filter.type}
136
- placeholder={filter.placeholder}
137
- state={state}
138
- onChange={onInputChange}
139
- />
140
- }
141
- {
142
- filter.type === 'date' &&
143
- <Elements.Field
144
- class="mb-4"
145
- name={filter.name}
146
- type="date"
147
- mode="range"
143
+ filter.type === 'select' &&
144
+ <Elements.Select
145
+ {...filter}
146
+ class="mb-0"
148
147
  state={state}
149
148
  onChange={onInputChange}
150
- placeholder={filter.placeholder || 'Select range...'}
149
+ type={undefined}
151
150
  />
152
151
  }
153
152
  {
154
- filter.type === 'select' &&
155
- <Elements.Select
156
- class="mb-4"
157
- name={filter.name}
158
- type="country"
153
+ filter.type !== 'select' &&
154
+ <Elements.Field
155
+ {...filter}
156
+ class="mb-0"
159
157
  state={state}
160
- options={filter.enums || []}
161
158
  onChange={onInputChange}
162
- placeholder={filter.placeholder}
163
159
  />
164
160
  }
165
161
  </div>
@@ -3,24 +3,29 @@ import Saturation from '@uiw/react-color-saturation'
3
3
  import Hue from '@uiw/react-color-hue'
4
4
  import { Dropdown, util } from 'nitro-web'
5
5
 
6
- export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
6
+ export type FieldColorProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'|'value'> & {
7
7
  name: string
8
8
  /** name is applied if id is not provided */
9
9
  id?: string
10
10
  defaultColor?: string
11
11
  Icon?: React.ReactNode
12
- onChange?: (event: { target: { name: string, value: string|null } }) => void
13
- value?: string|null
12
+ onChange?: (event: { target: { name: string, value: string } }) => void
13
+ value?: string
14
14
  }
15
15
 
16
- export function FieldColor({ defaultColor='#333', Icon, onChange, value, ...props }: FieldColorProps) {
16
+ export function FieldColor({ defaultColor='#333', Icon, onChange: onChangeProp, value: valueProp, ...props }: FieldColorProps) {
17
17
  const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
18
18
  const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
19
19
  const id = props.id || props.name
20
20
 
21
- function onInputChange(e: { target: { name: string, value: string|null } }) {
21
+ // Since value and onChange are optional, we need to hold the value in state if not provided
22
+ const [internalValue, setInternalValue] = useState(valueProp ?? defaultColor)
23
+ const value = valueProp ?? internalValue
24
+ const onChange = onChangeProp ?? ((e: { target: { name: string, value: string } }) => setInternalValue(e.target.value))
25
+
26
+ function onInputChange(e: { target: { name: string, value: string } }) {
22
27
  setLastChanged(`ic-${Date.now()}`)
23
- if (onChange) onChange(e)
28
+ onChange(e)
24
29
  }
25
30
 
26
31
  return (
@@ -149,7 +149,7 @@ export function FieldCurrency({ config, currency='nzd', onChange, value, default
149
149
  defaultValue={defaultValue}
150
150
  />
151
151
  <span
152
- class={`absolute top-0 bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-input-size ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
152
+ class={`absolute top-0 bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-input-base ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
153
153
  >
154
154
  {settings.prefix || settings.suffix}
155
155
  </span>
@@ -9,14 +9,21 @@ type DropdownRef = {
9
9
  }
10
10
 
11
11
  type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
12
+ /** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
12
13
  name: string
14
+ /** mode of the date picker */
13
15
  mode: Mode
14
- // name is applied if id is not provided
16
+ /** name is used as the id if not provided */
15
17
  id?: string
18
+ /** show the time picker */
16
19
  showTime?: boolean
20
+ /** prefix to add to the input */
17
21
  prefix?: string
22
+ /** number of months to show in the dropdown */
18
23
  numberOfMonths?: number
24
+ /** icon to show in the input */
19
25
  Icon?: React.ReactNode
26
+ /** direction of the dropdown */
20
27
  dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
21
28
  }
22
29
 
@@ -27,7 +34,7 @@ export type FieldDateProps = (
27
34
  value?: null|number|string
28
35
  })
29
36
  | ({ mode: 'multiple' | 'range' } & PreFieldDateProps & {
30
- onChange: (e: { target: { name: string, value: (null|number)[] } }) => void
37
+ onChange?: (e: { target: { name: string, value: (null|number)[] } }) => void
31
38
  value?: null|number|string|(null|number|string)[]
32
39
  })
33
40
  )
@@ -37,8 +44,16 @@ type TimePickerProps = {
37
44
  onChange: (mode: Mode, value: number|null) => void
38
45
  }
39
46
 
40
- export function FieldDate({
41
- mode, onChange, prefix='', value, numberOfMonths, Icon, showTime, dir = 'bottom-left', ...props
47
+ export function FieldDate({
48
+ dir = 'bottom-left',
49
+ Icon,
50
+ mode,
51
+ numberOfMonths,
52
+ onChange: onChangeProp,
53
+ prefix = '',
54
+ showTime,
55
+ value: valueProp,
56
+ ...props
42
57
  }: FieldDateProps) {
43
58
  const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
44
59
  const [prefixWidth, setPrefixWidth] = useState(0)
@@ -46,7 +61,12 @@ export function FieldDate({
46
61
  const [month, setMonth] = useState<number|undefined>()
47
62
  const [lastUpdated, setLastUpdated] = useState(0)
48
63
  const id = props.id || props.name
49
-
64
+
65
+ // Since value and onChange are optional, we need to hold the value in state if not provided
66
+ const [internalValue, setInternalValue] = useState<typeof valueProp>(valueProp)
67
+ const value = valueProp ?? internalValue
68
+ const onChange = onChangeProp ?? ((e: { target: { name: string, value: any } }) => setInternalValue(e.target.value))
69
+
50
70
  // Convert the value to an array of valid* dates
51
71
  const dates = useMemo(() => {
52
72
  const _dates = Array.isArray(value) ? value : [value]
@@ -70,10 +90,8 @@ export function FieldDate({
70
90
  if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
71
91
  setInputValue(getInputValue(value))
72
92
  // Update the value
73
- if (onChange) {
74
- onChange({ target: { name: props.name, value: value as any } })
75
- setLastUpdated(new Date().getTime())
76
- }
93
+ onChange({ target: { name: props.name, value: value as any } })
94
+ setLastUpdated(new Date().getTime())
77
95
  }
78
96
 
79
97
  function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
@@ -104,10 +122,8 @@ export function FieldDate({
104
122
 
105
123
  // Update the value
106
124
  const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
107
- if (onChange) {
108
- onChange({ target: { name: props.name, value: value as any }})
109
- setLastUpdated(new Date().getTime())
110
- }
125
+ onChange({ target: { name: props.name, value: value as any }})
126
+ setLastUpdated(new Date().getTime())
111
127
  }
112
128
 
113
129
  return (
@@ -135,7 +151,7 @@ export function FieldDate({
135
151
  {
136
152
  prefix &&
137
153
  // Similar classNames to the input.tsx:IconWrapper()
138
- <span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-size ml-3">
154
+ <span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-3">
139
155
  {prefix}
140
156
  </span>
141
157
  }
@@ -1,26 +1,30 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // Maybe use fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
2
3
  import { css } from 'twin.macro'
3
4
  import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
4
5
  import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
5
6
  import { Errors, type Error } from 'nitro-web/types'
6
7
  import { EnvelopeIcon, CalendarIcon, FunnelIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/20/solid'
7
8
  import { memo } from 'react'
8
- // Maybe use fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
9
9
 
10
+ type FieldType = 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
10
11
  type InputProps = React.InputHTMLAttributes<HTMLInputElement>
11
12
  type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
12
13
  type FieldExtraProps = {
13
- // field name or path on state (used to match errors), e.g. 'date', 'company.email'
14
+ /** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
14
15
  name: string
15
- // name is applied if id is not provided
16
+ /** name is applied if id is not provided */
16
17
  id?: string
17
- // state object to get the value, and check errors against
18
+ /** state object to get the value, and check errors against */
18
19
  state?: { errors?: Errors, [key: string]: any }
19
- type?: 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
20
+ /** type of the field */
21
+ type?: FieldType
22
+ /** icon to show in the input */
20
23
  icon?: React.ReactNode
21
24
  iconPos?: 'left' | 'right'
22
- /** Pass dependencies to break memoization, handy for onChange/onInputChange **/
25
+ /** Pass dependencies to break memoization, handy for onChange/onInputChange */
23
26
  deps?: unknown[]
27
+ placeholder?: string
24
28
  }
25
29
  type IconWrapperProps = {
26
30
  iconPos: string
@@ -46,7 +50,7 @@ export const Field = memo(FieldBase, (prev, next) => {
46
50
  })
47
51
 
48
52
  function FieldBase({ state, icon, iconPos: ip, ...props }: FieldProps) {
49
- // type must be kept as props.type for TS to be happy and follow the conditions below
53
+ // `type` must be kept as props.type for TS to be happy and follow the conditions below
50
54
  let value!: string
51
55
  let Icon!: React.ReactNode
52
56
  const error = getErrorFromState(state, props.name)
@@ -145,7 +149,7 @@ function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?:
145
149
  const plWithIcon = type == 'color' ? 'pl-9' : 'pl-8' // was sm:pl-8 pl-8, etc
146
150
  const prWithIcon = type == 'color' ? 'pr-9' : 'pr-8'
147
151
  return (
148
- `block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-input-size outline outline-1 -outline-offset-1 ` +
152
+ `block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-input-base outline outline-1 -outline-offset-1 ` +
149
153
  'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
150
154
  (iconPos == 'right' && Icon ? `${pl} ${prWithIcon} ` : (Icon ? `${plWithIcon} ${pr} ` : `${pl} ${pr} `)) +
151
155
  (error
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { css } from 'twin.macro'
3
3
  import { memo } from 'react'
4
- import ReactSelect, { components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
5
- DropdownIndicatorProps, MultiValueRemoveProps } from 'react-select'
4
+ import ReactSelect, {
5
+ components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
6
+ DropdownIndicatorProps, MultiValueRemoveProps,
7
+ } from 'react-select'
6
8
  import { ChevronUpDownIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
7
9
  import { isFieldCached } from 'nitro-web'
8
10
  import { getErrorFromState, deepFind, twMerge } from 'nitro-web/util'
@@ -19,11 +21,13 @@ type GetSelectStyle = {
19
21
  }
20
22
 
21
23
  /** Select (all other props are passed to react-select) **/
22
- type SelectProps = {
24
+ export type SelectProps = {
23
25
  /** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
24
26
  name: string
25
- /** name used if not provided **/
26
- inputId?: string
27
+ /** inputId, the name is used if not provided **/
28
+ id?: string
29
+ /** 'container' id to pass to react-select **/
30
+ containerId?: string
27
31
  /** The minimum width of the dropdown menu **/
28
32
  minMenuWidth?: number
29
33
  /** The prefix to add to the input **/
@@ -35,7 +39,7 @@ type SelectProps = {
35
39
  /** The state object to get the value and check errors from **/
36
40
  state?: { errors?: Errors, [key: string]: any } // was unknown|unknown[]
37
41
  /** Select variations **/
38
- type?: 'country'|'customer'|''
42
+ mode?: 'country'|'customer'|''
39
43
  /** Pass dependencies to break memoization, handy for onChange/onInputChange **/
40
44
  deps?: unknown[]
41
45
  /** All other props are passed to react-select **/
@@ -46,7 +50,7 @@ export const Select = memo(SelectBase, (prev, next) => {
46
50
  return isFieldCached(prev, next)
47
51
  })
48
52
 
49
- function SelectBase({ inputId, minMenuWidth, name, prefix='', onChange, options, state, type='', ...props }: SelectProps) {
53
+ function SelectBase({ id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', ...props }: SelectProps) {
50
54
  let value: unknown|unknown[]
51
55
  const error = getErrorFromState(state, name)
52
56
  if (!name) throw new Error('Select component requires a `name` and `options` prop')
@@ -78,10 +82,11 @@ function SelectBase({ inputId, minMenuWidth, name, prefix='', onChange, options,
78
82
  */
79
83
  {...props}
80
84
  // @ts-expect-error
81
- _nitro={{ prefix, type }}
85
+ _nitro={{ prefix, mode }}
82
86
  key={value as string}
83
87
  unstyled={true}
84
- inputId={inputId || name}
88
+ inputId={id || name}
89
+ id={containerId}
85
90
  filterOption={(option, searchText) => {
86
91
  if ((option.data as {fixed?: boolean}).fixed) return true
87
92
  return filterFn(option, searchText)
@@ -161,7 +166,7 @@ function Control({ children, ...props }: ControlProps) {
161
166
  // todo: check that the flag/prefix looks okay
162
167
  const selectedOption = props.getValue()[0]
163
168
  const optionFlag = (selectedOption as { flag?: string })?.flag
164
- const _nitro = (props.selectProps as { _nitro?: { prefix?: string, type?: string } })?._nitro
169
+ const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?: string } })?._nitro
165
170
  return (
166
171
  <components.Control {...props}>
167
172
  {
@@ -173,7 +178,7 @@ function Control({ children, ...props }: ControlProps) {
173
178
  {children}
174
179
  </>
175
180
  )
176
- } else if (_nitro?.type == 'country') {
181
+ } else if (_nitro?.mode == 'country') {
177
182
  return (
178
183
  <>
179
184
  { optionFlag && <Flag flag={optionFlag} /> }
@@ -201,10 +206,10 @@ function SingleValue(props: SingleValueProps) {
201
206
  function Option(props: OptionProps) {
202
207
  // todo: check that the flag looks okay
203
208
  const data = props.data as { className?: string, flag?: string }
204
- const _nitro = (props.selectProps as { _nitro?: { type?: string } })?._nitro
209
+ const _nitro = (props.selectProps as { _nitro?: { mode?: string } })?._nitro
205
210
  return (
206
211
  <components.Option className={data.className} {...props}>
207
- { _nitro?.type == 'country' && <Flag flag={data.flag} /> }
212
+ { _nitro?.mode == 'country' && <Flag flag={data.flag} /> }
208
213
  <span class="flex-auto">{props.label}</span>
209
214
  {props.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-1" />}
210
215
  </components.Option>
@@ -248,7 +253,7 @@ const selectStyles = {
248
253
  // Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
249
254
  // Input container
250
255
  control: {
251
- base: 'rounded-md bg-white hover:cursor-pointer text-input-size outline outline-1 -outline-offset-1 '
256
+ base: 'rounded-md bg-white hover:cursor-pointer text-input-base outline outline-1 -outline-offset-1 '
252
257
  + '!min-h-0 outline-input-border',
253
258
  focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
254
259
  error: 'outline-danger',
@@ -273,8 +278,8 @@ const selectStyles = {
273
278
  indicatorsContainer: 'p-1 px-2 gap-1',
274
279
  indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
275
280
  // Dropdown menu
276
- menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-size overflow-hidden shadow-dropdown-ul',
277
- groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-size',
281
+ menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-base overflow-hidden shadow-dropdown-ul',
282
+ groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-base',
278
283
  noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
279
284
  option: {
280
285
  base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
@@ -3,7 +3,7 @@ import {
3
3
  Filters, FiltersHandleType, FilterType,
4
4
  } from 'nitro-web'
5
5
  import { getCountryOptions, getCurrencyOptions, ucFirst } from 'nitro-web/util'
6
- import { Check } from 'lucide-react'
6
+ import { Check, FileEditIcon } from 'lucide-react'
7
7
 
8
8
  type StyleguideProps = {
9
9
  className?: string
@@ -34,27 +34,40 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
34
34
  })
35
35
  const [filterState, setFilterState] = useState({})
36
36
  const filtersRef = useRef<FiltersHandleType>(null)
37
- const filters: FilterType[] = useMemo(() => [
38
- {
39
- name: 'dateRange',
40
- type: 'date',
41
- },
42
- {
43
- name: 'search',
44
- type: 'search',
45
- label: 'Keyword Search',
46
- placeholder: 'Job, employee name...',
47
- },
48
- {
49
- name: 'status',
50
- type: 'select',
51
- enums: [
52
- { label: 'Pending', value: 'pending' },
53
- { label: 'Approved', value: 'approved' },
54
- { label: 'Rejected', value: 'rejected' },
55
- ],
56
- },
57
- ], [])
37
+ const filters = useMemo(() => {
38
+ const filters: FilterType[] = [
39
+ {
40
+ type: 'date',
41
+ name: 'dateRange',
42
+ mode: 'range',
43
+ placeholder: 'Select a range...',
44
+ },
45
+ {
46
+ type: 'search',
47
+ name: 'search',
48
+ label: 'Keyword Search',
49
+ placeholder: 'Job, employee name...',
50
+ },
51
+ {
52
+ type: 'select',
53
+ name: 'status',
54
+ rowClassName: 'flex-1',
55
+ options: [
56
+ { label: 'Pending', value: 'pending' },
57
+ { label: 'Approved', value: 'approved' },
58
+ { label: 'Rejected', value: 'rejected' },
59
+ ],
60
+ },
61
+ {
62
+ type: 'color',
63
+ name: 'color',
64
+ label: 'Half column',
65
+ placeholder: 'Select color...',
66
+ rowClassName: 'flex-1',
67
+ },
68
+ ]
69
+ return filters
70
+ }, [])
58
71
 
59
72
  const options = useMemo(() => [
60
73
  { label: 'Open customer preview' },
@@ -182,6 +195,9 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
182
195
  <div><Button IconRight="v">IconRight</Button></div>
183
196
  <div><Button IconRightEnd="v" className="w-[190px]">IconRightEnd 190px</Button></div>
184
197
  <div><Button color="primary" IconRight="v" isLoading>primary isLoading</Button></div>
198
+ <div><Button IconCenter={<FileEditIcon size={18}/>}></Button></div>
199
+ <div><Button size="sm" IconCenter={<FileEditIcon size={16}/>}></Button></div>
200
+ <div><Button size="xs" IconCenter={<FileEditIcon size={14}/>}></Button></div>
185
201
  </div>
186
202
 
187
203
  <h2 class="h3">Checkboxes</h2>
@@ -243,7 +259,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
243
259
  <Select
244
260
  // https://github.com/lipis/flag-icons
245
261
  name="country"
246
- type="country"
262
+ mode="country"
247
263
  state={state}
248
264
  options={useMemo(() => getCountryOptions(injectedConfig.countries), [])}
249
265
  onChange={(e) => onChange(setState, e)}
@@ -255,7 +271,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
255
271
  // menuIsOpen={true}
256
272
  placeholder="Select or add customer..."
257
273
  name="customer"
258
- type="customer"
274
+ mode="customer"
259
275
  state={state}
260
276
  onChange={onCustomerInputChange}
261
277
  onInputChange={onCustomerSearch}
@@ -323,7 +339,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
323
339
  </div>
324
340
  <div>
325
341
  <label for="brandColor">Brand Color</label>
326
- <Field name="brandColor" type="color" state={state} iconPos="left" onChange={(e) => onChange(setState, e)} />
342
+ <Field name="brandColor" type="color" iconPos="left" state={state} onChange={(e) => onChange(setState, e)} />
327
343
  </div>
328
344
  <div>
329
345
  <label for="amount">Amount ({state.amount})</label>
@@ -343,8 +359,8 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
343
359
  <Field name="date-range" type="date" mode="range" prefix="Date:" state={state} onChange={(e) => onChange(setState, e)} />
344
360
  </div>
345
361
  <div>
346
- <label for="date">Date (right aligned)</label>
347
- <Field name="date" type="date" mode="single" state={state} onChange={(e) => onChange(setState, e)} dir="bottom-right" />
362
+ <label for="date">Date multi-select (right aligned)</label>
363
+ <Field name="date" type="date" mode="multiple" state={state} onChange={(e) => onChange(setState, e)} dir="bottom-right" />
348
364
  </div>
349
365
  </div>
350
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.65",
3
+ "version": "0.0.66",
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 🚀",
package/types/util.d.ts CHANGED
@@ -503,6 +503,73 @@ export function onChange<T>(setState: React.Dispatch<React.SetStateAction<T>>, e
503
503
  * @returns {string}
504
504
  */
505
505
  export function pad(num?: number, padLeft?: number, fixedRight?: number): string;
506
+ /**
507
+ * Validates req.query "filters" against a config object, and returns a MongoDB-compatible query object.
508
+ * @param {{ [key: string]: string }} query - req.query
509
+ * E.g. {
510
+ * dateRange: '1749038400000,1749729600000',
511
+ * location: '10-RS',
512
+ * status: 'incomplete',
513
+ * search: 'John'
514
+ * }
515
+ * @param {{ [key: string]: 'string'|'number'|'search'|'dateRange'|string[] }} config - allowed filters and their rules
516
+ * E.g. {
517
+ * dateRange: 'dateRange',
518
+ * location: 'string',
519
+ * status: ['incomplete', 'complete'],
520
+ * search: 'string',
521
+ * }
522
+ * @example returned object (using the examples above):
523
+ * E.g. {
524
+ * date: { $gte: 1749038400000, $lte: 1749729600000 },
525
+ * location: '10-RS',
526
+ * status: 'incomplete',
527
+ * search: 'John'
528
+ * }
529
+ */
530
+ export function parseFilters(query: {
531
+ [key: string]: string;
532
+ }, config: {
533
+ [key: string]: "string" | "number" | "search" | "dateRange" | string[];
534
+ }): {
535
+ [key: string]: string | number | string[] | {
536
+ $gte: number;
537
+ $lte?: number;
538
+ } | {
539
+ $search: string;
540
+ };
541
+ };
542
+ /**
543
+ * Parses req.query "pagination" and "sorting" fields and returns a monastery-compatible options object.
544
+ * @param {{ fieldsFlattened: object, name: string }} model - The Monastery model
545
+ * @param {{ page?: string, sort?: '1'|'-1', sortBy?: string }} query - req.query
546
+ * E.g. {
547
+ * page: '1',
548
+ * sort: '1',
549
+ * sortBy: 'createdAt'
550
+ * }
551
+ * @param {number} [limit=10]
552
+ * @example returned object (using the examples above):
553
+ * E.g. {
554
+ * limit: 10,
555
+ * skip: undefined,
556
+ * sort: { createdAt: 1 },
557
+ * }
558
+ */
559
+ export function parseSortOptions(model: {
560
+ fieldsFlattened: object;
561
+ name: string;
562
+ }, query: {
563
+ page?: string;
564
+ sort?: "1" | "-1";
565
+ sortBy?: string;
566
+ }, limit?: number): {
567
+ limit: number;
568
+ skip: number;
569
+ sort: {
570
+ [x: string]: number;
571
+ };
572
+ };
506
573
  /**
507
574
  * Picks fields from an object
508
575
  * @param {{ [key: string]: any }} obj
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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"}
package/util.js CHANGED
@@ -1199,6 +1199,112 @@ export function pad (num=0, padLeft=0, fixedRight) {
1199
1199
  }
1200
1200
  }
1201
1201
 
1202
+ /**
1203
+ * Validates req.query "filters" against a config object, and returns a MongoDB-compatible query object.
1204
+ * @param {{ [key: string]: string }} query - req.query
1205
+ * E.g. {
1206
+ * dateRange: '1749038400000,1749729600000',
1207
+ * location: '10-RS',
1208
+ * status: 'incomplete',
1209
+ * search: 'John'
1210
+ * }
1211
+ * @param {{ [key: string]: 'string'|'number'|'search'|'dateRange'|string[] }} config - allowed filters and their rules
1212
+ * E.g. {
1213
+ * dateRange: 'dateRange',
1214
+ * location: 'string',
1215
+ * status: ['incomplete', 'complete'],
1216
+ * search: 'string',
1217
+ * }
1218
+ * @example returned object (using the examples above):
1219
+ * E.g. {
1220
+ * date: { $gte: 1749038400000, $lte: 1749729600000 },
1221
+ * location: '10-RS',
1222
+ * status: 'incomplete',
1223
+ * search: 'John'
1224
+ * }
1225
+ */
1226
+ export function parseFilters(query, config) {
1227
+ /** @type {{ [key: string]: string|number|{ $gte: number; $lte?: number; }|{ $search: string }|string[] }} */
1228
+ const mongoQuery = {}
1229
+
1230
+ for (const key in query) {
1231
+ const val = query[key]
1232
+ const rule = config[key]
1233
+
1234
+ if (!rule) {
1235
+ continue
1236
+
1237
+ } else if (rule === 'string') {
1238
+ if (typeof val !== 'string') throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a string.`)
1239
+ mongoQuery[key] = val
1240
+
1241
+ } else if (rule === 'number') {
1242
+ if (typeof val !== 'number' || isNaN(val)) throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a number.`)
1243
+ mongoQuery[key] = parseFloat(val)
1244
+
1245
+ } else if (rule === 'search') {
1246
+ if (typeof val !== 'string') throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a string.`)
1247
+ mongoQuery['$text'] = { $search: val }
1248
+
1249
+ } else if (Array.isArray(rule)) {
1250
+ if (!rule.includes(val)) {
1251
+ throw new Error(`The "${key}" filter has an invalid value "${val}". Allowed values: "${rule.join('", "')}"`)
1252
+ }
1253
+ mongoQuery[key] = val
1254
+
1255
+ } else if (rule === 'dateRange') {
1256
+ const [start, end] = val.split(',').map(Number)
1257
+ if (isNaN(start) && isNaN(end)) throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a date range.`)
1258
+ else if (isNaN(start)) mongoQuery.date = { $gte: 0, $lte: end }
1259
+ else if (isNaN(end)) mongoQuery.date = { $gte: start }
1260
+ else mongoQuery.date = { $gte: start, $lte: end }
1261
+
1262
+ } else {
1263
+ throw new Error(`Unknown filter type "${rule}" in the config.`)
1264
+ }
1265
+ }
1266
+
1267
+ return mongoQuery
1268
+ }
1269
+
1270
+ /**
1271
+ * Parses req.query "pagination" and "sorting" fields and returns a monastery-compatible options object.
1272
+ * @param {{ fieldsFlattened: object, name: string }} model - The Monastery model
1273
+ * @param {{ page?: string, sort?: '1'|'-1', sortBy?: string }} query - req.query
1274
+ * E.g. {
1275
+ * page: '1',
1276
+ * sort: '1',
1277
+ * sortBy: 'createdAt'
1278
+ * }
1279
+ * @param {number} [limit=10]
1280
+ * @example returned object (using the examples above):
1281
+ * E.g. {
1282
+ * limit: 10,
1283
+ * skip: undefined,
1284
+ * sort: { createdAt: 1 },
1285
+ * }
1286
+ */
1287
+ export function parseSortOptions(model, query, limit = 10) {
1288
+ const page = parseInt(query.page || '') || 1
1289
+
1290
+ // Validate sortBy value
1291
+ const sortBy = query.sortBy || 'createdAt'
1292
+ const fields = Object.keys(model.fieldsFlattened)
1293
+ if (!fields.includes(sortBy)) {
1294
+ throw new Error(`"${sortBy}" is an invalid sortBy value for the "${model.name}" model.`)
1295
+ }
1296
+
1297
+ const sort = sortBy === 'createdAt' && !query.sort ? -1 : (parseInt(query.sort || '') || 1)
1298
+
1299
+ return {
1300
+ limit: limit + 1, // get an extra row to signal there are more pages
1301
+ skip: page > 1 ? (page - 1) * limit : undefined,
1302
+ sort: {
1303
+ [sortBy]: sort,
1304
+ },
1305
+ }
1306
+ }
1307
+
1202
1308
  /**
1203
1309
  * Picks fields from an object
1204
1310
  * @param {{ [key: string]: any }} obj
@@ -1570,7 +1676,13 @@ export function trim (string) {
1570
1676
  */
1571
1677
  export function twMerge(...args) {
1572
1678
  const ignoredClasses = /** @type {string[]} */([])
1573
- const ignoreClasses = ['text-button-size', 'text-input-size']
1679
+ const ignoreClasses = [
1680
+ 'text-button-xs',
1681
+ 'text-button-sm',
1682
+ 'text-button-md',
1683
+ 'text-button-lg',
1684
+ 'text-input-base',
1685
+ ]
1574
1686
  const classes = args.filter(Boolean).join(' ').split(' ')
1575
1687
 
1576
1688
  const filteredClasses = classes.filter(c => {