nitro-web 0.0.46 → 0.0.48

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
@@ -29,6 +29,7 @@ export { Avatar } from '../components/partials/element/avatar'
29
29
  export { Button } from '../components/partials/element/button'
30
30
  export { Calendar, type CalendarProps } from '../components/partials/element/calendar'
31
31
  export { Dropdown } from '../components/partials/element/dropdown'
32
+ export { Filters, type FiltersHandleType, type FilterType } from '../components/partials/element/filters'
32
33
  export { GithubLink } from '../components/partials/element/github-link'
33
34
  export { Initials } from '../components/partials/element/initials'
34
35
  export { Message } from '../components/partials/element/message'
@@ -36,7 +37,6 @@ export { Modal } from '../components/partials/element/modal'
36
37
  export { Sidebar, type SidebarProps } from '../components/partials/element/sidebar'
37
38
  export { Tooltip } from '../components/partials/element/tooltip'
38
39
  export { Topbar } from '../components/partials/element/topbar'
39
-
40
40
  // Component Form
41
41
  export { Checkbox } from '../components/partials/form/checkbox'
42
42
  export { Drop } from '../components/partials/form/drop'
@@ -3,21 +3,16 @@ import { css } from 'twin.macro'
3
3
  import { IsFirstRender } from 'nitro-web'
4
4
 
5
5
  type AccordionProps = {
6
- children: React.ReactNode
6
+ ariaControls?: string // pass to add aria-controls attribute to the accordion
7
+ children: React.ReactNode // first child is the header, second child is the contents
7
8
  className?: string
8
- expanded?: boolean
9
- onChange?: (event: React.MouseEvent<HTMLDivElement>, index: number) => void
9
+ classNameWhenExpanded?: string // handy for group styling
10
+ expanded?: boolean // initial value (or controlled value if onChange is passed)
11
+ onChange?: (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>, index: number) => void
12
+ // called when the header is clicked
10
13
  }
11
14
 
12
- export function Accordion({ children, className, expanded, onChange }: AccordionProps) {
13
- /**
14
- * @param {rxjs} children - first child is the header, second child is the contents
15
- * <Accordion>
16
- * <div>Header</div><div>Contents</div>
17
- * </Accordion>
18
- * @param {boolean} <expanded> - initial value (or controlled value if onChange is passed)
19
- * @param {function} <onChange> - called when the header is clicked
20
- */
15
+ export function Accordion({ ariaControls, children, className, classNameWhenExpanded, expanded, onChange }: AccordionProps) {
21
16
  const [preState, setPreState] = useState(expanded)
22
17
  const [state, setState] = useState(expanded)
23
18
  const [height, setHeight] = useState('auto')
@@ -28,9 +23,17 @@ export function Accordion({ children, className, expanded, onChange }: Accordion
28
23
  height: 0;
29
24
  overflow: hidden;
30
25
  transition: height ease 0.2s;
26
+ a, button {
27
+ visibility: hidden; /* removes from tab order */
28
+ transition: visibility 0s 0.2s;
29
+ }
31
30
  }
32
- &.is-expanded > div:last-child {
31
+ &.is-expanded > *:last-child {
33
32
  height: ${height.replace('-', '')};
33
+ a, button {
34
+ visibility: visible;
35
+ transition: visibility 0s;
36
+ }
34
37
  }
35
38
  `
36
39
 
@@ -60,9 +63,9 @@ export function Accordion({ children, className, expanded, onChange }: Accordion
60
63
  return () => timeout && clearTimeout(timeout)
61
64
  }, [height])
62
65
 
63
- const onClick = function(e: React.MouseEvent<HTMLDivElement>) {
66
+ const onClick = function(e: React.MouseEvent<HTMLDivElement>|React.KeyboardEvent<HTMLDivElement>) {
64
67
  // Click came from inside the accordion header/summary
65
- if (e.currentTarget.children[0].contains(e.target as Node) || e.currentTarget.children[0] == e.target) {
68
+ if (e.currentTarget.children[0].contains(e.target as HTMLElement) || e.currentTarget.children[0] == e.target) {
66
69
  if (onChange) {
67
70
  onChange(e, getElementIndex(e.currentTarget))
68
71
  } else {
@@ -77,11 +80,20 @@ export function Accordion({ children, className, expanded, onChange }: Accordion
77
80
  return index
78
81
  }
79
82
 
83
+ const onKeyDown = function(e: React.KeyboardEvent<HTMLDivElement>) {
84
+ if (e.key === 'Enter' || e.key === ' ') {
85
+ onClick(e)
86
+ }
87
+ }
88
+
80
89
  return (
81
90
  <div
82
91
  ref={el}
83
- class={['accordion', className, state ? 'is-expanded' : ''].filter(o => o).join(' nitro-accordion')}
92
+ aria-controls={ariaControls}
93
+ aria-expanded={ariaControls ? state : undefined}
94
+ class={['accordion', className, state ? `is-expanded ${classNameWhenExpanded}` : '', 'nitro-accordion'].filter(o => o).join(' ')}
84
95
  onClick={onClick}
96
+ onKeyDown={onKeyDown}
85
97
  css={style}
86
98
  >
87
99
  {children}
@@ -1,8 +1,8 @@
1
- import { twMerge } from 'tailwind-merge'
1
+ import { twMerge } from 'nitro-web'
2
2
  import { ChevronDown, ChevronUp } from 'lucide-react'
3
3
 
4
4
  type Button = React.ButtonHTMLAttributes<HTMLButtonElement> & {
5
- color?: 'primary'|'secondary'|'black'|'white'
5
+ color?: 'primary'|'secondary'|'black'|'white'|'clear'
6
6
  size?: 'xs'|'sm'|'md'|'lg'
7
7
  className?: string
8
8
  isLoading?: boolean
@@ -37,14 +37,15 @@ export function Button({
37
37
  secondary: 'bg-secondary hover:bg-secondary-hover',
38
38
  black: 'bg-black hover:bg-gray-700',
39
39
  white: 'bg-white ring-1 ring-inset ring-gray-300 hover:bg-gray-50 text-gray-900 [&>.loader]:border-black',
40
+ clear: 'ring-1 ring-inset ring-gray-300 hover:bg-gray-50 text-foreground [&>.loader]:border-foreground !shadow-none',
40
41
  }
41
42
 
42
43
  // Button sizes
43
44
  const sizes = {
44
45
  xs: 'px-2 py-1 px-button-x-xs py-button-y-xs text-xs rounded',
45
- sm: 'px-2.5 py-1.5 px-button-x-sm py-button-y-sm text-sm rounded-md',
46
- md: 'px-3 py-2 px-button-x py-button-y text-sm rounded-md',
47
- lg: 'px-3.5 py-2.5 px-button-x-lg py-button-y-lg text-sm rounded-md',
46
+ sm: 'px-2.5 py-1.5 px-button-x-sm py-button-y-sm text-sm text-sm-button rounded-md',
47
+ md: 'px-3 py-[0.58rem] px-button-x-md py-button-y-md text-sm text-sm-button rounded-md', // default
48
+ lg: 'px-3.5 py-2.5 px-button-x-lg py-button-y-lg text-sm text-sm-button rounded-md',
48
49
  }
49
50
 
50
51
  const contentLayout = `gap-x-1.5 ${iconPosition == 'none' ? '' : 'inline-flex items-center justify-center'}`
@@ -16,7 +16,8 @@ type DropdownProps = {
16
16
  /** The minimum width of the menu **/
17
17
  minWidth?: number | string
18
18
  /** The content to render inside the top of the dropdown **/
19
- menuChildren?: React.ReactNode
19
+ menuContent?: React.ReactNode
20
+ menuClassName?: string
20
21
  menuIsOpen?: boolean
21
22
  menuToggles?: boolean
22
23
  toggleCallback?: (isActive: boolean) => void
@@ -30,7 +31,8 @@ export const Dropdown = forwardRef(function Dropdown({
30
31
  options,
31
32
  isHoverable,
32
33
  minWidth,
33
- menuChildren,
34
+ menuClassName,
35
+ menuContent,
34
36
  menuIsOpen,
35
37
  menuToggles=true,
36
38
  toggleCallback,
@@ -39,7 +41,7 @@ export const Dropdown = forwardRef(function Dropdown({
39
41
  isHoverable = isHoverable && !menuIsOpen
40
42
  const dropdownRef = useRef<HTMLDivElement|null>(null)
41
43
  const [isActive, setIsActive] = useState(!!menuIsOpen)
42
- const menuStyle = getSelectStyle({ name: 'menu', usePrefixes: true })
44
+ const menuStyle = getSelectStyle({ name: 'menu' })
43
45
 
44
46
  // Expose the setIsActive function to the parent component
45
47
  useImperativeHandle(ref, () => ({ setIsActive }))
@@ -106,9 +108,9 @@ export const Dropdown = forwardRef(function Dropdown({
106
108
  }
107
109
  <ul
108
110
  style={{ minWidth }}
109
- class={`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1]`}
111
+ class={`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName}`}
110
112
  >
111
- {menuChildren}
113
+ {menuContent}
112
114
  {
113
115
  options && options.map((option, i) => {
114
116
  const optionStyle = getSelectStyle({ name: 'option', usePrefixes: true, isSelected: option.isSelected })
@@ -131,10 +133,12 @@ export const Dropdown = forwardRef(function Dropdown({
131
133
  })
132
134
 
133
135
  const style = css`
134
- ul {
136
+ &>ul {
135
137
  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;
136
138
  max-width: 0; // handy if the dropdown ul exceeds the viewport width
137
139
  max-height: 0; // handy if the dropdown ul exceeds the viewport height
140
+ /* overflow: visible !important; // override menustyle */
141
+ pointer-events: none;
138
142
  }
139
143
  &.is-bottom-right,
140
144
  &.is-top-right {
@@ -145,14 +149,14 @@ const style = css`
145
149
  }
146
150
  &.is-bottom-left,
147
151
  &.is-bottom-right {
148
- ul {
152
+ &>ul {
149
153
  top: 100%;
150
154
  transform: translateY(6px);
151
155
  }
152
156
  }
153
157
  &.is-top-left,
154
158
  &.is-top-right {
155
- ul {
159
+ &>ul {
156
160
  bottom: 100%;
157
161
  transform: translateY(-10px);
158
162
  }
@@ -161,15 +165,17 @@ const style = css`
161
165
  &.is-hoverable:hover,
162
166
  &:focus,
163
167
  &.is-active,
164
- li:hover,
165
- li:focus,
166
- li.is-active {
167
- ul {
168
+ &>ul>li:hover,
169
+ &>ul>li:focus,
170
+ &>ul>li.is-active {
171
+ &>ul {
168
172
  opacity: 1;
169
173
  visibility: visible;
170
174
  transition: transform 0.15s ease, opacity 0.15s ease;
171
175
  max-width: 1000px;
172
176
  max-height: 1000px;
177
+ pointer-events: auto;
178
+ overflow: visible;
173
179
  }
174
180
  &.is-bottom-left > ul,
175
181
  &.is-bottom-right > ul {
@@ -182,7 +188,7 @@ const style = css`
182
188
  }
183
189
  // no animation
184
190
  &.no-animation {
185
- ul {
191
+ &>ul {
186
192
  transition: none;
187
193
  }
188
194
  }
@@ -0,0 +1,183 @@
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'
4
+ import { ListFilterIcon } from 'lucide-react'
5
+
6
+ export type FilterType = {
7
+ name: string
8
+ type: 'text'|'date'|'search'|'select'
9
+ label?: string
10
+ enums?: { label: string, value: string }[]
11
+ placeholder?: string
12
+ }
13
+
14
+ type FilterState = {
15
+ [key: string]: string | true
16
+ }
17
+
18
+ type FiltersProps = {
19
+ filters?: FilterType[]
20
+ state: FilterState
21
+ setState: Dispatch<SetStateAction<FilterState>>
22
+ elements?: {
23
+ Button?: typeof Button
24
+ Dropdown?: typeof Dropdown
25
+ Field?: typeof Field
26
+ Select?: typeof Select
27
+ FilterIcon?: typeof ListFilterIcon
28
+ }
29
+ dropdownProps?: Partial<React.ComponentProps<typeof Dropdown>>
30
+ buttonProps?: Partial<React.ComponentProps<typeof Button>>
31
+ buttonClassName?: string
32
+ buttonText?: string
33
+ buttonCounterClassName?: string
34
+ }
35
+
36
+ export type FiltersHandleType = {
37
+ submit: (includePagination?: boolean) => void
38
+ }
39
+
40
+ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
41
+ filters, state, setState, elements, dropdownProps, buttonProps, buttonClassName, buttonText, buttonCounterClassName,
42
+ }, ref) => {
43
+ const location = useLocation()
44
+ const stateRef = useRef(state)
45
+ const [debouncedSubmit] = useState(() => debounce(submit, 150))
46
+ const count = Object.keys(state).length - (Object.keys(state).includes('page') ? 1 : 0)
47
+ const Elements = {
48
+ Button: elements?.Button || Button,
49
+ Dropdown: elements?.Dropdown || Dropdown,
50
+ Field: elements?.Field || Field,
51
+ Select: elements?.Select || Select,
52
+ FilterIcon: elements?.FilterIcon || ListFilterIcon,
53
+ }
54
+
55
+ useImperativeHandle(ref, () => ({
56
+ submit: debouncedSubmit,
57
+ }))
58
+
59
+ useEffect(() => {
60
+ return () => debouncedSubmit.cancel()
61
+ }, [])
62
+
63
+ useEffect(() => {
64
+ stateRef.current = state
65
+ }, [state])
66
+
67
+ useEffect(() => {
68
+ setState(() => ({
69
+ ...queryObject(location.search),
70
+ }))
71
+ }, [location.search])
72
+
73
+ function reset(e: React.MouseEvent<HTMLAnchorElement>, filter: FilterType) {
74
+ e.preventDefault()
75
+ setState((s) => omit(s, [filter.name]) as FilterState)
76
+ debouncedSubmit()
77
+ }
78
+
79
+ function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
80
+ e.preventDefault()
81
+ setState((s) => ({
82
+ ...(s.page ? { page: s.page } : {}), // keep pagination
83
+ } as FilterState))
84
+ debouncedSubmit()
85
+ }
86
+
87
+ async function _onChange(e: {target: {id: string, value: unknown}}) {
88
+ await onChange(setState, e)
89
+ debouncedSubmit()
90
+ }
91
+
92
+ // Submit the filters and update the URL
93
+ function submit(includePagination?: boolean) {
94
+ const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
95
+ history.pushState(null, '', location.pathname + queryStr)
96
+ }
97
+
98
+ if (!filters) return null
99
+ return (
100
+ <Elements.Dropdown
101
+ dir="bottom-right"
102
+ // menuIsOpen={true}
103
+ menuClassName="!rounded-lg"
104
+ menuContent={
105
+ <div class="w-[330px]">
106
+ <div class="flex justify-between items-center border-b p-4 py-3.5">
107
+ <div class="text-lg font-semibold">Filters</div>
108
+ <Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
109
+ </div>
110
+ <div class="flex flex-col px-4 py-4 mb-[-6px]">
111
+ {
112
+ filters.map((filter) => (
113
+ <div key={filter.name}>
114
+ <div class="flex justify-between">
115
+ <label for={filter.name}>{filter.label || camelCaseToTitle(filter.name)}</label>
116
+ <a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
117
+ </div>
118
+ {
119
+ (filter.type === 'text' || filter.type === 'search') &&
120
+ <Elements.Field
121
+ class="mb-4"
122
+ name={filter.name}
123
+ type={filter.type}
124
+ placeholder={filter.placeholder}
125
+ state={state}
126
+ onChange={_onChange}
127
+ />
128
+ }
129
+ {
130
+ filter.type === 'date' &&
131
+ <Elements.Field
132
+ class="mb-4"
133
+ name={filter.name}
134
+ type="date"
135
+ mode="range"
136
+ state={state}
137
+ onChange={_onChange}
138
+ placeholder={filter.placeholder || 'Select range...'}
139
+ />
140
+ }
141
+ {
142
+ filter.type === 'select' &&
143
+ <Elements.Select
144
+ class="mb-4"
145
+ name={filter.name}
146
+ type="country"
147
+ state={state}
148
+ options={filter.enums || []}
149
+ onChange={_onChange}
150
+ placeholder={filter.placeholder}
151
+ />
152
+ }
153
+ </div>
154
+ ))
155
+ }
156
+ </div>
157
+ </div>
158
+ }
159
+ {...dropdownProps}
160
+ >
161
+ <Elements.Button
162
+ color="white"
163
+ IconLeft={<Elements.FilterIcon size={16} />}
164
+ className={twMerge(`flex gap-x-2.5 ${buttonClassName || ''}`)}
165
+ {...buttonProps}
166
+ >
167
+ <span class="flex items-center gap-x-2.5">
168
+ { buttonText || 'Filter By' }
169
+ {
170
+ !!count &&
171
+ <span
172
+ class={twMerge(`inline-flex items-center justify-center rounded-full text-xs text-white bg-primary w-[19px] h-[19px] ${buttonCounterClassName || ''}`)}
173
+ >
174
+ {count}
175
+ </span>
176
+ }
177
+ </span>
178
+ </Elements.Button>
179
+ </Elements.Dropdown>
180
+ )
181
+ })
182
+
183
+ Filters.displayName = 'Filters'
@@ -26,7 +26,7 @@ export function FieldColor({ defaultColor='#333', Icon, onChange, value, ...prop
26
26
  <Dropdown
27
27
  dir="bottom-left"
28
28
  menuToggles={false}
29
- menuChildren={
29
+ menuContent={
30
30
  <ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} name={props.name} value={value} onChange={onChange} />
31
31
  }
32
32
  >
@@ -148,7 +148,7 @@ export function FieldCurrency({ config, currency='nzd', onChange, value, default
148
148
  defaultValue={defaultValue}
149
149
  />
150
150
  <span
151
- class={`absolute top-[1px] bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-sm sm:text-sm/6 ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
151
+ class={`absolute top-0 bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-sm text-sm-input ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
152
152
  >
153
153
  {settings.prefix || settings.suffix}
154
154
  </span>
@@ -44,7 +44,8 @@ export function FieldDate({
44
44
  const dropdownRef = useRef<DropdownRef>(null)
45
45
  const id = props.id || props.name
46
46
  const [month, setMonth] = useState<number|undefined>()
47
-
47
+ const [lastUpdated, setLastUpdated] = useState(0)
48
+
48
49
  // Convert the value to an array of valid* dates
49
50
  const dates = useMemo(() => {
50
51
  const _dates = Array.isArray(value) ? value : [value]
@@ -54,6 +55,11 @@ export function FieldDate({
54
55
  // Hold the input value in state
55
56
  const [inputValue, setInputValue] = useState(() => getInputValue(dates))
56
57
 
58
+ // Update the date's inputValue (text) when the value changes outside of the component
59
+ useEffect(() => {
60
+ if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(dates))
61
+ }, [value])
62
+
57
63
  // Get the prefix content width
58
64
  useEffect(() => {
59
65
  setPrefixWidth(getPrefixWidth(prefix, 4))
@@ -62,7 +68,11 @@ export function FieldDate({
62
68
  function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
63
69
  if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
64
70
  setInputValue(getInputValue(value))
65
- if (onChange) onChange({ target: { id: id, value: value as any } })
71
+ // Update the value
72
+ if (onChange) {
73
+ onChange({ target: { id: id, value: value as any } })
74
+ setLastUpdated(new Date().getTime())
75
+ }
66
76
  }
67
77
 
68
78
  function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
@@ -79,7 +89,7 @@ export function FieldDate({
79
89
  })
80
90
 
81
91
  // For single/range we need limit the array
82
- if (mode == 'range') split.length = 2
92
+ if (mode == 'range' && split.length > 1) split.length = 2
83
93
  else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
84
94
 
85
95
  // Swap dates if needed
@@ -91,9 +101,12 @@ export function FieldDate({
91
101
  break
92
102
  }
93
103
 
94
- // Update
104
+ // Update the value
95
105
  const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
96
- if (onChange) onChange({ target: { id: id, value: value as any }})
106
+ if (onChange) {
107
+ onChange({ target: { id: id, value: value as any }})
108
+ setLastUpdated(new Date().getTime())
109
+ }
97
110
  }
98
111
 
99
112
  return (
@@ -103,7 +116,7 @@ export function FieldDate({
103
116
  // animate={false}
104
117
  // menuIsOpen={true}
105
118
  minWidth={0}
106
- menuChildren={
119
+ menuContent={
107
120
  <div className="flex">
108
121
  <Calendar
109
122
  {...{ mode, value, numberOfMonths, month }}
@@ -121,7 +134,9 @@ export function FieldDate({
121
134
  {
122
135
  prefix &&
123
136
  // Similar classNames to the input.tsx:IconWrapper()
124
- <span className="relative col-start-1 row-start-1 self-center select-none z-[1] justify-self-start text-sm ml-3">{prefix}</span>
137
+ <span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-sm text-sm-input ml-3">
138
+ {prefix}
139
+ </span>
125
140
  }
126
141
  <input
127
142
  {...props}
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { css } from 'twin.macro'
3
- import { twMerge } from 'tailwind-merge'
4
- import { util, FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
3
+ import { util, FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps, twMerge } from 'nitro-web'
5
4
  import { Errors, type Error } from 'nitro-web/types'
6
5
  import {
7
6
  EnvelopeIcon,
@@ -131,7 +130,7 @@ export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
131
130
 
132
131
  function FieldContainer({ children, className, error }: { children: React.ReactNode, className?: string, error?: Error }) {
133
132
  return (
134
- <div css={style} className={`mt-2.5 mb-6 mt-input-before mb-input-after grid grid-cols-1 nitro-field ${className || ''}`}>
133
+ <div css={style} className={twMerge(`mt-2.5 mb-6 mt-input-before mb-input-after grid grid-cols-1 nitro-field ${className || ''}`)}>
135
134
  {children}
136
135
  {error && <div class="mt-1.5 text-xs text-danger nitro-error">{error.detail}</div>}
137
136
  </div>
@@ -141,11 +140,11 @@ function FieldContainer({ children, className, error }: { children: React.ReactN
141
140
  function getInputClasses({ error, Icon, iconPos, type }: { error: Error, Icon?: React.ReactNode, iconPos: string, type?: string }) {
142
141
  const pl = 'pl-3 pl-input-x'
143
142
  const pr = 'pr-3 pr-input-x'
144
- const py = 'py-2 py-input-y'
143
+ const py = 'py-[0.58rem] py-input-y'
145
144
  const plWithIcon = type == 'color' ? 'pl-9' : 'pl-8' // was sm:pl-8 pl-8, etc
146
145
  const prWithIcon = type == 'color' ? 'pr-9' : 'pr-8'
147
146
  return (
148
- `block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-sm leading-[1.65] outline outline-1 -outline-offset-1 ` +
147
+ `block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-sm text-sm-input outline outline-1 -outline-offset-1 ` +
149
148
  'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
150
149
  (iconPos == 'right' && Icon ? `${pl} ${prWithIcon} ` : (Icon ? `${plWithIcon} ${pr} ` : `${pl} ${pr} `)) +
151
150
  (error
@@ -161,11 +160,11 @@ function IconWrapper({ icon, iconPos, ...props }: IconWrapperProps) {
161
160
  !!icon &&
162
161
  <div
163
162
  {...props}
164
- className={twMerge(
165
- 'relative size-[14px] col-start-1 row-start-1 self-center text-[#c6c8ce] select-none z-[1] ' +
163
+ className={
164
+ 'z-[0] size-[14px] col-start-1 row-start-1 self-center text-[#c6c8ce] select-none ' +
166
165
  (iconPos == 'right' ? 'justify-self-end mr-3 ' : 'justify-self-start ml-3 ') +
167
166
  props.className || ''
168
- )}
167
+ }
169
168
  >{icon}</div>
170
169
  )
171
170
  }
@@ -1,5 +1,5 @@
1
1
  import { css } from 'twin.macro'
2
- import { twMerge } from 'tailwind-merge'
2
+ import { twMerge } from 'nitro-web'
3
3
  import ReactSelect, { components, ControlProps, createFilter, OptionProps, SingleValueProps } from 'react-select'
4
4
  import { ClearIndicatorProps, DropdownIndicatorProps, MultiValueRemoveProps } from 'react-select'
5
5
  import { ChevronUpDownIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
@@ -31,7 +31,7 @@ type SelectProps = {
31
31
  /** The options to display in the dropdown **/
32
32
  options: { value: unknown, label: string | React.ReactNode, fixed?: boolean, [key: string]: unknown }[]
33
33
  /** The state object to get the value and check errors from **/
34
- state?: { errors: Errors, [key: string]: unknown }
34
+ state?: { errors?: Errors, [key: string]: unknown }
35
35
  /** Select variations **/
36
36
  type?: 'country'|'customer'|''
37
37
  /** All other props are passed to react-select **/
@@ -58,7 +58,7 @@ export function Select({ inputId, minMenuWidth, name, prefix='', onChange, optio
58
58
  }
59
59
 
60
60
  return (
61
- <div css={style} class="mt-2.5 mb-6 mt-input-before mb-input-after nitro-select">
61
+ <div css={style} class={twMerge(`mt-2.5 mb-6 mt-input-before mb-input-after nitro-select ${props.className||''}`)}>
62
62
  <ReactSelect
63
63
  /**
64
64
  * react-select prop quick reference (https://react-select.com/props#api):
@@ -237,11 +237,11 @@ const selectStyles = {
237
237
  // Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
238
238
  // Input container
239
239
  control: {
240
- base: 'rounded-md bg-white hover:cursor-pointer text-sm leading-[1.65] outline outline-1 -outline-offset-1 outline-input-border',
240
+ base: 'rounded-md bg-white hover:cursor-pointer text-sm text-sm-input outline outline-1 -outline-offset-1 outline-input-border',
241
241
  focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
242
242
  error: 'outline-danger',
243
243
  },
244
- valueContainer: 'py-2 px-3 py-input-y px-input-x gap-1',
244
+ valueContainer: 'py-[0.58rem] px-3 py-input-y px-input-x gap-1',
245
245
  // Input container objects
246
246
  input: {
247
247
  base: 'text-input',
@@ -261,8 +261,8 @@ const selectStyles = {
261
261
  indicatorsContainer: 'p-1 px-2 gap-1',
262
262
  indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
263
263
  // Dropdown menu
264
- menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-sm overflow-hidden shadow-dropdown-ul',
265
- groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-sm',
264
+ menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-sm text-sm-input overflow-hidden shadow-dropdown-ul',
265
+ groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-sm text-sm-input',
266
266
  noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
267
267
  option: {
268
268
  base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
@@ -1,8 +1,12 @@
1
- import { Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink, Modal, Calendar, injectedConfig } from 'nitro-web'
1
+ import {
2
+ Drop, Dropdown, Field, Select, Button, Checkbox, GithubLink, Modal, Calendar, injectedConfig,
3
+ Filters, FiltersHandleType,
4
+ FilterType,
5
+ } from 'nitro-web'
2
6
  import { getCountryOptions, getCurrencyOptions, ucFirst } from 'nitro-web/util'
3
7
  import { Check } from 'lucide-react'
4
8
 
5
- export function Styleguide() {
9
+ export function Styleguide({ className }: { className?: string }) {
6
10
  const [customerSearch, setCustomerSearch] = useState('')
7
11
  const [showModal1, setShowModal1] = useState(false)
8
12
  const [state, setState] = useState({
@@ -20,6 +24,29 @@ export function Styleguide() {
20
24
  { title: 'address', detail: 'Address is required' },
21
25
  ],
22
26
  })
27
+ const [filterState, setFilterState] = useState({})
28
+ const filtersRef = useRef<FiltersHandleType>(null)
29
+ const filters: FilterType[] = [
30
+ {
31
+ name: 'dateRange',
32
+ type: 'date',
33
+ },
34
+ {
35
+ name: 'search',
36
+ type: 'search',
37
+ label: 'Keyword Search',
38
+ placeholder: 'Job, employee name...',
39
+ },
40
+ {
41
+ name: 'status',
42
+ type: 'select',
43
+ enums: [
44
+ { label: 'Pending', value: 'pending' },
45
+ { label: 'Approved', value: 'approved' },
46
+ { label: 'Rejected', value: 'rejected' },
47
+ ],
48
+ },
49
+ ]
23
50
 
24
51
  // Example of updating state
25
52
  // useEffect(() => {
@@ -52,7 +79,7 @@ export function Styleguide() {
52
79
  }
53
80
 
54
81
  return (
55
- <div class="mb-10 text-left max-w-[1100px]">
82
+ <div class={`text-left max-w-[1100px] ${className}`}>
56
83
  <GithubLink filename={__filename} />
57
84
  <div class="mb-7">
58
85
  <h1 class="h1">{injectedConfig.isDemo ? 'Design System' : 'Style Guide'}</h1>
@@ -98,6 +125,31 @@ export function Styleguide() {
98
125
  </div>
99
126
  </div>
100
127
 
128
+ <h2 class="h3">Filters</h2>
129
+ <div class="flex flex-wrap gap-x-6 gap-y-4 mb-10">
130
+ {/* Filter dropdown */}
131
+ <Filters
132
+ ref={filtersRef}
133
+ filters={filters}
134
+ state={filterState}
135
+ setState={setFilterState}
136
+ dropdownProps={{ dir: 'bottom-left' }}
137
+ />
138
+ {/* Search bar */}
139
+ <Field
140
+ class="!my-0 min-w-[242px]"
141
+ type="search"
142
+ name="search"
143
+ iconPos="left"
144
+ state={filterState}
145
+ onChange={(e) => {
146
+ onChange(setFilterState, e)
147
+ filtersRef.current?.submit()
148
+ }}
149
+ placeholder="Linked search bar..."
150
+ />
151
+ </div>
152
+
101
153
  <h2 class="h3">Buttons</h2>
102
154
  <div class="flex flex-wrap gap-x-6 gap-y-4 mb-10">
103
155
  <div><Button color="primary">primary (default)</Button></div>
@@ -273,7 +325,7 @@ export function Styleguide() {
273
325
  </div>
274
326
 
275
327
  <h2 class="h3">File Inputs & Calendar</h2>
276
- <div class="grid grid-cols-3 gap-x-6 mb-4">
328
+ <div class="grid grid-cols-3 gap-x-6">
277
329
  <div>
278
330
  <label for="avatar">Avatar</label>
279
331
  <Drop class="is-small" name="avatar" state={state} onChange={(e) => onChange(setState, e)} awsUrl={injectedConfig.awsUrl} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
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
@@ -117,30 +117,31 @@ export function currencyToCents(currency: string): string;
117
117
  */
118
118
  export function date(date: number | Date, format?: string, timezone?: string): string;
119
119
  /**
120
+ * @template {(...args: any[]) => any} T
120
121
  * Creates a debounced function that delays invoking `func` until after `wait`
121
122
  * milliseconds have elapsed since the last time the debounced function was invoked.
122
123
  *
123
- * @param {Function} func - The function to debounce.
124
+ * @param {T} func - The function to debounce.
124
125
  * @param {number} [wait=0] - Number of milliseconds to delay.
125
126
  * @param {{
126
127
  * leading?: boolean, // invoke on the leading edge of the timeout (default: false)
127
128
  * maxWait?: number, // maximum time `func` is allowed to be delayed before it's invoked
128
129
  * trailing?: boolean, // invoke on the trailing edge of the timeout (default: true)
129
130
  * }} [options] - Options to control behavior.
130
- * @returns {(...args: any[]) => any & {
131
- * cancel: () => void,
132
- * flush: () => any
133
- * }} - A new debounced function with `cancel` and `flush` methods.
131
+ * @returns {((...args: Parameters<T>) => ReturnType<T>) & {
132
+ * cancel: () => void;
133
+ * flush: () => ReturnType<T>
134
+ * }}
134
135
  *
135
136
  * @see https://lodash.com/docs/4.17.15#debounce
136
137
  */
137
- export function debounce(func: Function, wait?: number, options?: {
138
+ export function debounce<T extends (...args: any[]) => any>(func: T, wait?: number, options?: {
138
139
  leading?: boolean;
139
140
  maxWait?: number;
140
141
  trailing?: boolean;
141
- }): (...args: any[]) => any & {
142
+ }): ((...args: Parameters<T>) => ReturnType<T>) & {
142
143
  cancel: () => void;
143
- flush: () => any;
144
+ flush: () => ReturnType<T>;
144
145
  };
145
146
  /**
146
147
  * Deep clones an object or array, preserving its type
@@ -508,6 +509,12 @@ export function pick(obj: {
508
509
  export function queryObject(searchString: string, trueDefaults?: boolean): {
509
510
  [key: string]: string | true;
510
511
  };
512
+ /**
513
+ * Parses a query string into an array of objects
514
+ * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
515
+ * @returns {object[]} - e.g. [{ page: '1' }]
516
+ */
517
+ export function queryArray(searchString: string): object[];
511
518
  /**
512
519
  * Parses an object and returns a query string
513
520
  * @param {{[key: string]: unknown}} [obj] - query object
@@ -595,7 +602,7 @@ export function sortByKey(collection: {
595
602
  }[], key: string): object[];
596
603
  /**
597
604
  * Creates a throttled function that only invokes `func` at most once per every `wait` milliseconds
598
- * @param {function} func
605
+ * @param {(...args: any[]) => any} func
599
606
  * @param {number} [wait=0] - the number of milliseconds to throttle invocations to
600
607
  * @param {{
601
608
  * leading?: boolean, // invoke on the leading edge of the timeout
@@ -605,7 +612,7 @@ export function sortByKey(collection: {
605
612
  * @example const throttled = util.throttle(updatePosition, 100)
606
613
  * @see lodash
607
614
  */
608
- export function throttle(func: Function, wait?: number, options?: {
615
+ export function throttle(func: (...args: any[]) => any, wait?: number, options?: {
609
616
  leading?: boolean;
610
617
  trailing?: boolean;
611
618
  }): Function;
@@ -622,6 +629,12 @@ export function toArray<T>(variable: T | undefined): (T extends any[] ? T : T[])
622
629
  * @returns {string}
623
630
  */
624
631
  export function trim(string: string): string;
632
+ /**
633
+ * Merge tailwind classes, but ignore classes that shouldn't be merged, and intended as an override
634
+ * @param {...string} args
635
+ * @returns {string}
636
+ */
637
+ export function twMerge(...args: string[]): string;
625
638
  /**
626
639
  * Capitalize the first letter of a string
627
640
  * @param {string} string
@@ -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
@@ -2,6 +2,7 @@ import _axios from 'axios'
2
2
  import axiosRetry from 'axios-retry'
3
3
  import dateformat from 'dateformat'
4
4
  import { loadStripe } from '@stripe/stripe-js/pure.js' // pure removes ping
5
+ import { twMerge as _twMerge } from 'tailwind-merge'
5
6
 
6
7
  /** @type {{[key: string]: {[key: string]: string|true}}} */
7
8
  let queryObjectCache = {}
@@ -191,20 +192,21 @@ export function date (date, format, timezone) {
191
192
  }
192
193
 
193
194
  /**
195
+ * @template {(...args: any[]) => any} T
194
196
  * Creates a debounced function that delays invoking `func` until after `wait`
195
197
  * milliseconds have elapsed since the last time the debounced function was invoked.
196
198
  *
197
- * @param {Function} func - The function to debounce.
199
+ * @param {T} func - The function to debounce.
198
200
  * @param {number} [wait=0] - Number of milliseconds to delay.
199
201
  * @param {{
200
202
  * leading?: boolean, // invoke on the leading edge of the timeout (default: false)
201
203
  * maxWait?: number, // maximum time `func` is allowed to be delayed before it's invoked
202
204
  * trailing?: boolean, // invoke on the trailing edge of the timeout (default: true)
203
205
  * }} [options] - Options to control behavior.
204
- * @returns {(...args: any[]) => any & {
205
- * cancel: () => void,
206
- * flush: () => any
207
- * }} - A new debounced function with `cancel` and `flush` methods.
206
+ * @returns {((...args: Parameters<T>) => ReturnType<T>) & {
207
+ * cancel: () => void;
208
+ * flush: () => ReturnType<T>
209
+ * }}
208
210
  *
209
211
  * @see https://lodash.com/docs/4.17.15#debounce
210
212
  */
@@ -1244,6 +1246,18 @@ export function queryObject (searchString, trueDefaults) {
1244
1246
  return obj
1245
1247
  }
1246
1248
 
1249
+ /**
1250
+ * Parses a query string into an array of objects
1251
+ * @param {string} searchString - location.search or location.href, e.g. '?page=1', 'https://...co.nz?page=1'
1252
+ * @returns {object[]} - e.g. [{ page: '1' }]
1253
+ */
1254
+ export function queryArray (searchString) {
1255
+ const query = queryObject(searchString)
1256
+ return Object.keys(query).map((key) => {
1257
+ return { [key]: query[key] }
1258
+ })
1259
+ }
1260
+
1247
1261
  /**
1248
1262
  * Parses an object and returns a query string
1249
1263
  * @param {{[key: string]: unknown}} [obj] - query object
@@ -1488,7 +1502,7 @@ export function sortByKey (collection, key) {
1488
1502
 
1489
1503
  /**
1490
1504
  * Creates a throttled function that only invokes `func` at most once per every `wait` milliseconds
1491
- * @param {function} func
1505
+ * @param {(...args: any[]) => any} func
1492
1506
  * @param {number} [wait=0] - the number of milliseconds to throttle invocations to
1493
1507
  * @param {{
1494
1508
  * leading?: boolean, // invoke on the leading edge of the timeout
@@ -1540,6 +1554,26 @@ export function trim (string) {
1540
1554
  return string.trim().replace(/\n\s+\n/g, '\n\n')
1541
1555
  }
1542
1556
 
1557
+ /**
1558
+ * Merge tailwind classes, but ignore classes that shouldn't be merged, and intended as an override
1559
+ * @param {...string} args
1560
+ * @returns {string}
1561
+ */
1562
+ export function twMerge(...args) {
1563
+ const ignoredClasses = /** @type {string[]} */([])
1564
+ const ignoreClasses = ['text-sm-button', 'text-sm-input']
1565
+ const classes = args.filter(Boolean).join(' ').split(' ')
1566
+
1567
+ const filteredClasses = classes.filter(c => {
1568
+ if (ignoreClasses.includes(c)) {
1569
+ ignoredClasses.push(c)
1570
+ return false
1571
+ }
1572
+ return true
1573
+ })
1574
+ return _twMerge(...filteredClasses) + ' ' + ignoredClasses.join(' ')
1575
+ }
1576
+
1543
1577
  /**
1544
1578
  * Capitalize the first letter of a string
1545
1579
  * @param {string} string