nitro-web 0.0.65 → 0.0.67

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,9 +1,10 @@
1
1
  import { css } from 'twin.macro'
2
2
  import { forwardRef, cloneElement } from 'react'
3
- import { getSelectStyle } from 'nitro-web'
3
+ import { getSelectStyle, twMerge } from 'nitro-web'
4
4
  import { CheckCircleIcon } from '@heroicons/react/24/solid'
5
5
 
6
6
  type DropdownProps = {
7
+ allowOverflow?: boolean
7
8
  animate?: boolean
8
9
  children?: React.ReactNode
9
10
  className?: string
@@ -18,20 +19,23 @@ type DropdownProps = {
18
19
  /** The content to render inside the top of the dropdown **/
19
20
  menuContent?: React.ReactNode
20
21
  menuClassName?: string
22
+ menuOptionClassName?: string
21
23
  menuIsOpen?: boolean
22
24
  menuToggles?: boolean
23
25
  toggleCallback?: (isActive: boolean) => void
24
26
  }
25
27
 
26
28
  export const Dropdown = forwardRef(function Dropdown({
29
+ allowOverflow=false,
27
30
  animate=true,
28
31
  children,
29
32
  className,
30
33
  dir,
31
34
  options,
32
35
  isHoverable,
33
- minWidth,
36
+ minWidth, // remove in favour of menuClassName
34
37
  menuClassName,
38
+ menuOptionClassName,
35
39
  menuContent,
36
40
  menuIsOpen,
37
41
  menuToggles=true,
@@ -92,6 +96,7 @@ export const Dropdown = forwardRef(function Dropdown({
92
96
  (isHoverable ? ' is-hoverable' : '') +
93
97
  (isActive ? ' is-active' : '') +
94
98
  (!animate ? ' no-animation' : '') +
99
+ (allowOverflow ? ' is-allowOverflow' : '') +
95
100
  ' nitro-dropdown' +
96
101
  (className ? ` ${className}` : '')
97
102
  }
@@ -108,7 +113,7 @@ export const Dropdown = forwardRef(function Dropdown({
108
113
  }
109
114
  <ul
110
115
  style={{ minWidth }}
111
- class={`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName}`}
116
+ class={twMerge(`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName}`)}
112
117
  >
113
118
  {menuContent}
114
119
  {
@@ -117,7 +122,7 @@ export const Dropdown = forwardRef(function Dropdown({
117
122
  return (
118
123
  <li
119
124
  key={i}
120
- className={`${optionStyle} ${option.className}`}
125
+ className={twMerge(`${optionStyle} ${option.className} ${menuOptionClassName}`)}
121
126
  onClick={(e: React.MouseEvent) => onClick(option, e)}
122
127
  >
123
128
  <span class="flex-auto">{option.label}</span>
@@ -137,8 +142,7 @@ const style = css`
137
142
  transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease, max-width 0s 0.15s ease, max-height 0s 0.15s ease;
138
143
  max-width: 0; // handy if the dropdown ul exceeds the viewport width
139
144
  max-height: 0; // handy if the dropdown ul exceeds the viewport height
140
- /* overflow: visible !important; // override menustyle */
141
- pointer-events: none;
145
+ pointer-events: none;
142
146
  }
143
147
  &.is-bottom-right,
144
148
  &.is-top-right {
@@ -175,6 +179,8 @@ const style = css`
175
179
  max-width: 1000px;
176
180
  max-height: 1000px;
177
181
  pointer-events: auto;
182
+ }
183
+ &.is-allowOverflow > ul {
178
184
  overflow: visible;
179
185
  }
180
186
  &.is-bottom-left > ul,
@@ -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,8 @@ type FiltersProps = {
31
32
  buttonClassName?: string
32
33
  buttonText?: string
33
34
  buttonCounterClassName?: string
35
+ filtersContainerClassName?: string
36
+ menuClassName?: string
34
37
  }
35
38
 
36
39
  export type FiltersHandleType = {
@@ -40,14 +43,27 @@ export type FiltersHandleType = {
40
43
  const debounceTime = 250
41
44
 
42
45
  export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
43
- filters, state, setState, elements, dropdownProps, buttonProps, buttonClassName, buttonText, buttonCounterClassName,
46
+ filters,
47
+ setState: setState2,
48
+ state: state2,
49
+ buttonClassName,
50
+ buttonCounterClassName,
51
+ buttonProps,
52
+ buttonText,
53
+ dropdownProps,
54
+ elements,
55
+ filtersContainerClassName,
56
+ menuClassName,
44
57
  }, ref) => {
45
58
  const location = useLocation()
46
59
  const navigate = useNavigate()
47
- const stateRef = useRef(state)
48
60
  const [lastUpdated, setLastUpdated] = useState(0)
49
61
  const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
50
- const count = Object.keys(state).length - (Object.keys(state).includes('page') ? 1 : 0)
62
+ const [state3, setState3] = useState(() => ({ ...queryObject(location.search) }))
63
+ const [state, setState] = [state2 || state3, setState2 || setState3]
64
+ const stateRef = useRef(state)
65
+ const count = useMemo(() => Object.keys(state).filter((k) => state[k] && filters?.some((f) => f.name === k)).length, [state, filters])
66
+
51
67
  const Elements = {
52
68
  Button: elements?.Button || Button,
53
69
  Dropdown: elements?.Dropdown || Dropdown,
@@ -85,9 +101,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
85
101
 
86
102
  function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
87
103
  e.preventDefault()
88
- setState((s) => ({
89
- ...(s.page ? { page: s.page } : {}), // keep pagination
90
- } as FilterState))
104
+ setState((s) => omit(s, filters?.map((f) => f.name) || []) as FilterState)
91
105
  onAfterChange()
92
106
  }
93
107
 
@@ -106,60 +120,48 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
106
120
  const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
107
121
  navigate(location.pathname + queryStr, { replace: true })
108
122
  }
109
-
110
- if (!filters) return null
123
+
111
124
  return (
112
125
  <Elements.Dropdown
113
126
  dir="bottom-right"
127
+ allowOverflow={true}
114
128
  // menuIsOpen={true}
115
- menuClassName="!rounded-lg"
129
+ menuClassName={twMerge(`!rounded-lg min-w-[330px] ${menuClassName || ''}`)}
116
130
  menuContent={
117
- <div class="w-[330px]">
131
+ <div>
118
132
  <div class="flex justify-between items-center border-b p-4 py-3.5">
119
133
  <div class="text-lg font-semibold">Filters</div>
120
134
  <Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
121
135
  </div>
122
- <div class="flex flex-col px-4 py-4 mb-[-6px]">
136
+ {/* <div class="w-[1330px] bg-red-500 absolute">
137
+ This div shouldnt produce a page scrollbar when the dropdown is closed.
138
+ But should be visibile if allowedOverflow is true.
139
+ </div> */}
140
+ <div class={twMerge(`flex flex-wrap gap-4 px-4 py-4 pb-6 ${filtersContainerClassName || ''}`)}>
123
141
  {
124
- filters.map((filter) => (
125
- <div key={filter.name}>
142
+ filters?.map(({label, rowClassName, ...filter}, i) => (
143
+ <div key={i} class={twMerge(`w-full ${rowClassName||''}`)}>
126
144
  <div class="flex justify-between">
127
- <label for={filter.name}>{filter.label || camelCaseToTitle(filter.name)}</label>
145
+ <label for={filter.id || filter.name}>{label || camelCaseToTitle(filter.name)}</label>
128
146
  <a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
129
147
  </div>
130
148
  {
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"
149
+ filter.type === 'select' &&
150
+ <Elements.Select
151
+ {...filter}
152
+ class="mb-0"
148
153
  state={state}
149
154
  onChange={onInputChange}
150
- placeholder={filter.placeholder || 'Select range...'}
155
+ type={undefined}
151
156
  />
152
157
  }
153
158
  {
154
- filter.type === 'select' &&
155
- <Elements.Select
156
- class="mb-4"
157
- name={filter.name}
158
- type="country"
159
+ filter.type !== 'select' &&
160
+ <Elements.Field
161
+ {...filter}
162
+ class="mb-0"
159
163
  state={state}
160
- options={filter.enums || []}
161
164
  onChange={onInputChange}
162
- placeholder={filter.placeholder}
163
165
  />
164
166
  }
165
167
  </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,11 +61,19 @@ 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
- const _dates = Array.isArray(value) ? value : [value]
53
- return _dates.map(date => isValid(date) ? new Date(date as number) : null) /// change to null
72
+ const arrOfNumbers = typeof value === 'string'
73
+ ? value.split(/\s*,\s*/g).map(o => parseFloat(o))
74
+ : Array.isArray(value) ? value : [value]
75
+ const out = arrOfNumbers.map(date => isValid(date) ? new Date(date as number) : null) /// changed to null
76
+ return out
54
77
  }, [value])
55
78
 
56
79
  // Hold the input value in state
@@ -59,7 +82,7 @@ export function FieldDate({
59
82
  // Update the date's inputValue (text) when the value changes outside of the component
60
83
  useEffect(() => {
61
84
  if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(dates))
62
- }, [value])
85
+ }, [dates])
63
86
 
64
87
  // Get the prefix content width
65
88
  useEffect(() => {
@@ -70,15 +93,8 @@ export function FieldDate({
70
93
  if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
71
94
  setInputValue(getInputValue(value))
72
95
  // Update the value
73
- if (onChange) {
74
- onChange({ target: { name: props.name, value: value as any } })
75
- setLastUpdated(new Date().getTime())
76
- }
77
- }
78
-
79
- function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
80
- const _dates = Array.isArray(dates) ? dates : [dates]
81
- return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
96
+ onChange({ target: { name: props.name, value: getOutputValue(value) } })
97
+ setLastUpdated(new Date().getTime())
82
98
  }
83
99
 
84
100
  function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
@@ -104,12 +120,20 @@ export function FieldDate({
104
120
 
105
121
  // Update the value
106
122
  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
- }
123
+ onChange({ target: { name: props.name, value: getOutputValue(value) }})
124
+ setLastUpdated(new Date().getTime())
111
125
  }
112
126
 
127
+ function getInputValue(value: Date|number|null|(Date|number|null)[]) {
128
+ const _dates = Array.isArray(value) ? value : [value]
129
+ return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
130
+ }
131
+
132
+ function getOutputValue(value: Date|number|null|(Date|number|null)[]): any {
133
+ // console.log(value)
134
+ return value
135
+ }
136
+
113
137
  return (
114
138
  <Dropdown
115
139
  ref={dropdownRef}
@@ -120,7 +144,8 @@ export function FieldDate({
120
144
  menuContent={
121
145
  <div className="flex">
122
146
  <Calendar
123
- {...{ mode, value, numberOfMonths, month }}
147
+ // Calendar actually accepts an array of dates, but the type is not typed correctly
148
+ {...{ mode: mode, value: dates as any, numberOfMonths: numberOfMonths, month: month }}
124
149
  preserveTime={!!showTime}
125
150
  onChange={onCalendarChange}
126
151
  className="pt-1 pb-2 px-3"
@@ -135,7 +160,7 @@ export function FieldDate({
135
160
  {
136
161
  prefix &&
137
162
  // 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">
163
+ <span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-3">
139
164
  {prefix}
140
165
  </span>
141
166
  }
@@ -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.67",
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[key] = { $gte: 0, $lte: end }
1259
+ else if (isNaN(end)) mongoQuery[key] = { $gte: start }
1260
+ else mongoQuery[key] = { $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 => {