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 +1 -1
- package/components/partials/element/accordion.tsx +28 -16
- package/components/partials/element/button.tsx +6 -5
- package/components/partials/element/dropdown.tsx +19 -13
- package/components/partials/element/filters.tsx +183 -0
- package/components/partials/form/field-color.tsx +1 -1
- package/components/partials/form/field-currency.tsx +1 -1
- package/components/partials/form/field-date.tsx +22 -7
- package/components/partials/form/field.tsx +7 -8
- package/components/partials/form/select.tsx +7 -7
- package/components/partials/styleguide.tsx +56 -4
- package/package.json +1 -1
- package/types/util.d.ts +23 -10
- package/types/util.d.ts.map +1 -1
- package/util.js +40 -6
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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 >
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
{
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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="
|
|
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 '
|
|
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-
|
|
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
|
|
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={
|
|
165
|
-
'
|
|
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 '
|
|
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
|
|
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=
|
|
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
|
|
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-
|
|
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 {
|
|
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=
|
|
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
|
|
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.
|
|
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 {
|
|
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:
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* }}
|
|
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:
|
|
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:
|
|
142
|
+
}): ((...args: Parameters<T>) => ReturnType<T>) & {
|
|
142
143
|
cancel: () => void;
|
|
143
|
-
flush: () =>
|
|
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 {
|
|
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:
|
|
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
|
package/types/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"
|
|
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 {
|
|
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:
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* }}
|
|
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 {
|
|
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
|