@maestro-js/components 1.0.0-alpha.1 → 1.0.0-alpha.10
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/commands/add.ts +16 -1
- package/dist/components.json +1 -1
- package/dist/index.js +14 -1
- package/package.json +5 -5
- package/registry.json +610 -802
- package/scripts/build.ts +11 -4
- package/src/components/alert-dialog.tsx +3 -3
- package/src/components/avatar.tsx +1 -1
- package/src/components/button-link.tsx +3 -0
- package/src/components/button.tsx +4 -0
- package/src/components/checkbox.tsx +7 -7
- package/src/components/chip.tsx +7 -7
- package/src/components/container.tsx +3 -3
- package/src/components/date-range.tsx +161 -0
- package/src/components/dialog.tsx +21 -7
- package/src/components/disclosure.tsx +1 -1
- package/src/components/drawer.tsx +19 -5
- package/src/components/form.tsx +27 -5
- package/src/components/helpers/form-field.tsx +7 -8
- package/src/components/helpers/get-button-classes.ts +23 -23
- package/src/components/helpers/headless-button.tsx +1 -1
- package/src/components/inline-alert.tsx +3 -3
- package/src/components/labeled-value.tsx +2 -2
- package/src/components/menu.tsx +60 -26
- package/src/components/month-day-input.tsx +2 -1
- package/src/components/multiselect.tsx +74 -70
- package/src/components/optional-link.tsx +15 -0
- package/src/components/radio.tsx +8 -8
- package/src/components/select.tsx +9 -8
- package/src/components/stepper.tsx +6 -6
- package/src/components/switch.tsx +7 -7
- package/src/components/table/headless-templated-row-table.ts +550 -0
- package/src/components/table/index.tsx +41 -0
- package/src/components/table/server-table.ts +143 -0
- package/src/components/table/table-actions.tsx +76 -0
- package/src/components/table/table-container.tsx +173 -0
- package/src/components/table/table-container.type-test.ts +155 -0
- package/src/components/table/table-filters/date-range-filter.tsx +227 -0
- package/src/components/table/table-filters/select-filter.tsx +211 -0
- package/src/components/table/table-filters/sort.tsx +85 -0
- package/src/components/table/table-primitives.tsx +226 -0
- package/src/components/table/table-toolbar.tsx +300 -0
- package/src/components/table/table-types.ts +61 -0
- package/src/components/table/use-client-table.ts +157 -0
- package/src/components/table/use-client-table.type-test.ts +217 -0
- package/src/components/table/use-server-table.ts +192 -0
- package/src/components/table/use-server-table.type-test.ts +174 -0
- package/src/components/tabs.tsx +1 -1
- package/src/components/tag-field.tsx +3 -3
- package/src/components/text-area.tsx +1 -1
- package/src/components/toast.tsx +6 -6
- package/src/components/toggle-button-group.tsx +2 -2
- package/src/utils/icons.d.ts +2 -1
- package/src/utils/use-query-state.ts +626 -0
- package/src/utils/use-tab-indicator.ts +9 -9
package/scripts/build.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface RegistryFile {
|
|
|
9
9
|
|
|
10
10
|
interface RegistryEntry {
|
|
11
11
|
name: string
|
|
12
|
+
dependencies?: string[]
|
|
12
13
|
files: RegistryFile[]
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ interface OutputFile {
|
|
|
19
20
|
|
|
20
21
|
interface OutputEntry {
|
|
21
22
|
name: string
|
|
23
|
+
dependencies?: string[]
|
|
22
24
|
files: OutputFile[]
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -29,14 +31,19 @@ const registry: RegistryEntry[] = JSON.parse(await readFile(resolve(packageRoot,
|
|
|
29
31
|
const output: OutputEntry[] = await Promise.all(
|
|
30
32
|
registry.map(async (entry) => {
|
|
31
33
|
const files = await Promise.all(
|
|
32
|
-
entry.files.map(async (file) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
entry.files.map(async (file) => {
|
|
35
|
+
const content = await readFile(resolve(packageRoot, file.path), 'utf-8')
|
|
36
|
+
const header = `// From @maestro-js/components\n// Install with: pnpm maestro components:add ${entry.name} --directory app\n\n`
|
|
37
|
+
return {
|
|
38
|
+
name: `${file.type}/${basename(file.path)}`,
|
|
39
|
+
content: header + content
|
|
40
|
+
}
|
|
41
|
+
})
|
|
36
42
|
)
|
|
37
43
|
|
|
38
44
|
return {
|
|
39
45
|
name: entry.name,
|
|
46
|
+
...(entry.dependencies?.length && { dependencies: entry.dependencies }),
|
|
40
47
|
files
|
|
41
48
|
}
|
|
42
49
|
})
|
|
@@ -86,7 +86,7 @@ function AlertDialog_({
|
|
|
86
86
|
isDismissable={isDismissable}
|
|
87
87
|
isKeyboardDismissDisabled={!isDismissable}
|
|
88
88
|
className={tw(
|
|
89
|
-
'fixed inset-0 z-[4000] flex items-center justify-center
|
|
89
|
+
'fixed inset-0 z-[4000] flex items-center justify-center bg-black/25 backdrop-blur-sm',
|
|
90
90
|
'transition-opacity duration-200 ease-out',
|
|
91
91
|
'data-[entering]:opacity-0',
|
|
92
92
|
'data-[exiting]:opacity-0'
|
|
@@ -100,14 +100,14 @@ function AlertDialog_({
|
|
|
100
100
|
'data-[exiting]:scale-95 data-[exiting]:opacity-0 data-[exiting]:translate-y-2'
|
|
101
101
|
)}
|
|
102
102
|
>
|
|
103
|
-
<Dialog className={tw('outline-none bg-white rounded-
|
|
103
|
+
<Dialog className={tw('outline-none bg-white rounded-xl shadow-xl ring-1 ring-black/5 w-full', className)} role="alertdialog">
|
|
104
104
|
{({ close }) => (
|
|
105
105
|
<AlertDialogContext.Provider value={{ close }}>
|
|
106
106
|
<ButtonGroupContext.Provider
|
|
107
107
|
value={{ orientation: 'horizontal', alignment: 'center', fullWidth: true, className: 'flex-nowrap' }}
|
|
108
108
|
>
|
|
109
109
|
<ButtonContext.Provider value={{ shape: 'rectangle', variant: 'soft', fullWidth: true }}>
|
|
110
|
-
<div className={tw('border rounded-
|
|
110
|
+
<div className={tw('border rounded-xl p-6', styles.container)}>
|
|
111
111
|
<div className="flex flex-col items-center text-center">
|
|
112
112
|
{displayIcon && (
|
|
113
113
|
<div className={tw('w-12 h-12 rounded-xl flex items-center justify-center mb-4', styles.iconBg)}>
|
|
@@ -22,7 +22,7 @@ export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
|
22
22
|
<div
|
|
23
23
|
ref={ref}
|
|
24
24
|
className={tw(
|
|
25
|
-
'size-10 relative inline-flex items-center justify-center overflow-hidden font-medium text-white select-none',
|
|
25
|
+
'size-10 relative inline-flex items-center justify-center overflow-hidden font-medium text-white select-none ring-1 ring-black/5',
|
|
26
26
|
!showImage && 'bg-neutral-300',
|
|
27
27
|
shape === 'circle' && 'rounded-full',
|
|
28
28
|
shape === 'rounded' && 'rounded',
|
|
@@ -56,6 +56,9 @@ export const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|
|
56
56
|
className: tw(fullWidth && 'w-full', className)
|
|
57
57
|
})}
|
|
58
58
|
>
|
|
59
|
+
{(size === 'xs' || size === 'sm' || shape === 'circle') && (
|
|
60
|
+
<span className="absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2" aria-hidden="true" />
|
|
61
|
+
)}
|
|
59
62
|
<span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>
|
|
60
63
|
<span
|
|
61
64
|
aria-hidden={!showSpinner ? true : undefined}
|
|
@@ -74,6 +74,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
74
74
|
ref={ref}
|
|
75
75
|
data-pending={isPending}
|
|
76
76
|
onClick={onClick}
|
|
77
|
+
{...(isToggled != null ? { 'aria-pressed': isToggled } : {})}
|
|
77
78
|
className={composeTailwindRenderProps(
|
|
78
79
|
props.className,
|
|
79
80
|
tw(getButtonClasses({ variant, color, size, shape, isToggled }), fullWidth && 'w-full')
|
|
@@ -81,6 +82,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
81
82
|
>
|
|
82
83
|
{composeRenderProps(props.children, (children) => (
|
|
83
84
|
<>
|
|
85
|
+
{(size === 'xs' || size === 'sm' || shape === 'circle') && (
|
|
86
|
+
<span className="absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2" aria-hidden="true" />
|
|
87
|
+
)}
|
|
84
88
|
<span className={tw('inline-flex items-center gap-2', showSpinner && 'opacity-0')}>{children}</span>
|
|
85
89
|
<span
|
|
86
90
|
aria-hidden={!showSpinner ? true : undefined}
|
|
@@ -78,7 +78,7 @@ export function PlainCheckbox(props: PlainCheckboxProps) {
|
|
|
78
78
|
isDisabled={props.isDisabled}
|
|
79
79
|
isInvalid={props.isInvalid || validationInvalid}
|
|
80
80
|
aria-label={props['aria-label']}
|
|
81
|
-
className="group inline-flex items-center focus:outline-none
|
|
81
|
+
className="group inline-flex items-center focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
|
|
82
82
|
>
|
|
83
83
|
{({ isSelected, isIndeterminate, isDisabled }) => (
|
|
84
84
|
<>
|
|
@@ -139,7 +139,7 @@ export function LabeledCheckbox(props: LabeledCheckboxProps) {
|
|
|
139
139
|
isDisabled={props.isDisabled}
|
|
140
140
|
isInvalid={isInvalid}
|
|
141
141
|
aria-describedby={helpTextId}
|
|
142
|
-
className="group relative flex items-start gap-3 select-none focus:outline-none
|
|
142
|
+
className="group relative flex items-start gap-3 select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
|
|
143
143
|
>
|
|
144
144
|
{({ isSelected, isIndeterminate, isDisabled }) => (
|
|
145
145
|
<>
|
|
@@ -191,16 +191,16 @@ function CheckboxBox({
|
|
|
191
191
|
return (
|
|
192
192
|
<div
|
|
193
193
|
className={tw(
|
|
194
|
-
'relative
|
|
195
|
-
isSelected || isIndeterminate ? 'bg-primary-
|
|
194
|
+
'relative size-5 rounded border transition-colors duration-200',
|
|
195
|
+
isSelected || isIndeterminate ? 'bg-primary-600 border-primary-600' : 'bg-white border-neutral-300',
|
|
196
196
|
!isDisabled && !(isSelected || isIndeterminate) && 'group-hovered:border-neutral-400',
|
|
197
|
-
isDisabled && '
|
|
197
|
+
isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'
|
|
198
198
|
)}
|
|
199
199
|
>
|
|
200
200
|
{isIndeterminate ? (
|
|
201
|
-
<Icon name="minus" className="h-5/6 absolute inset-0 m-auto aspect-square text-white" />
|
|
201
|
+
<Icon name="minus" className="h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white" />
|
|
202
202
|
) : (
|
|
203
|
-
<Icon name="check" className="h-5/6 absolute inset-0 m-auto aspect-square text-white" />
|
|
203
|
+
<Icon name="check" className="h-5/6 absolute inset-0 m-auto aspect-square stroke-white text-white" />
|
|
204
204
|
)}
|
|
205
205
|
</div>
|
|
206
206
|
)
|
package/src/components/chip.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function Chip(props: ChipProps) {
|
|
|
60
60
|
className={tw(
|
|
61
61
|
'inline-flex items-center justify-center rounded-full transition-colors translate-x-1',
|
|
62
62
|
removeBtnSizeClasses[size],
|
|
63
|
-
!isDisabled && 'hover:bg-
|
|
63
|
+
!isDisabled && 'hover:bg-neutral-950/10',
|
|
64
64
|
isDisabled && 'cursor-not-allowed'
|
|
65
65
|
)}
|
|
66
66
|
aria-label="Remove"
|
|
@@ -146,13 +146,13 @@ const darkHoverColorClasses = {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const removeBtnSizeClasses = {
|
|
149
|
-
sm: '
|
|
150
|
-
md: '
|
|
151
|
-
lg: '
|
|
149
|
+
sm: 'size-4 -m-0.5 p-0.5',
|
|
150
|
+
md: 'size-5 -m-1 p-1',
|
|
151
|
+
lg: 'size-6 -m-1.5 p-1.5'
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
const removeIconSizeClasses = {
|
|
155
|
-
sm: '
|
|
156
|
-
md: '
|
|
157
|
-
lg: '
|
|
155
|
+
sm: 'size-2',
|
|
156
|
+
md: 'size-3',
|
|
157
|
+
lg: 'size-4'
|
|
158
158
|
}
|
|
@@ -71,9 +71,9 @@ const sizeClasses: Record<ContainerSize, string> = {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const elevationClasses: Record<string, string> = {
|
|
74
|
-
'0': 'bg-white
|
|
75
|
-
'-1': 'bg-neutral-50
|
|
76
|
-
'-2': 'bg-neutral-100
|
|
74
|
+
'0': 'bg-white ring-1 ring-neutral-950/5',
|
|
75
|
+
'-1': 'bg-neutral-50 ring-1 ring-neutral-950/5',
|
|
76
|
+
'-2': 'bg-neutral-100 ring-1 ring-neutral-950/10',
|
|
77
77
|
inherit: ''
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
RangeCalendar,
|
|
4
|
+
CalendarGrid,
|
|
5
|
+
CalendarCell,
|
|
6
|
+
Heading,
|
|
7
|
+
CalendarGridHeader,
|
|
8
|
+
CalendarHeaderCell,
|
|
9
|
+
CalendarGridBody,
|
|
10
|
+
Button,
|
|
11
|
+
I18nProvider
|
|
12
|
+
} from 'react-aria-components'
|
|
13
|
+
import { twMerge } from 'tailwind-merge'
|
|
14
|
+
import { parseDate, type CalendarDate, type DateValue } from '@internationalized/date'
|
|
15
|
+
import type { Iso } from 'iso-fns'
|
|
16
|
+
|
|
17
|
+
export type DateRangeRootAdditionalProps = {
|
|
18
|
+
defaultStart: Iso.Date | null
|
|
19
|
+
defaultEnd: Iso.Date | null
|
|
20
|
+
shouldDisableDate: (date: string) => boolean
|
|
21
|
+
shouldDisableStartDate: (date: string) => boolean
|
|
22
|
+
shouldDisableEndDate: (date: string) => boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type DateRangePickerProps = {
|
|
26
|
+
start: Iso.Date | null
|
|
27
|
+
end: Iso.Date | null
|
|
28
|
+
onStartChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void
|
|
29
|
+
onEndChange: (value: Iso.Date | null, field: 'start' | 'end' | null) => void
|
|
30
|
+
currentField: 'start' | 'end' | null
|
|
31
|
+
shouldDisableDate?: (date: string) => boolean
|
|
32
|
+
shouldDisableStartDate?: (date: string) => boolean
|
|
33
|
+
shouldDisableEndDate?: (date: string) => boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function DateRangePicker(props: DateRangePickerProps) {
|
|
37
|
+
const ariaValue = React.useMemo(() => {
|
|
38
|
+
if (!props.start || !props.end) return null
|
|
39
|
+
try {
|
|
40
|
+
return { start: parseDate(props.start), end: parseDate(props.end) }
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}, [props.start, props.end])
|
|
45
|
+
|
|
46
|
+
const handleChange = (range: { start: CalendarDate; end: CalendarDate } | null) => {
|
|
47
|
+
if (!range) {
|
|
48
|
+
props.onStartChange(null, null)
|
|
49
|
+
props.onEndChange(null, null)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const start = range.start.toString() as Iso.Date
|
|
53
|
+
const end = range.end.toString() as Iso.Date
|
|
54
|
+
props.onStartChange(start, 'end')
|
|
55
|
+
props.onEndChange(end, null)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isDateUnavailable = React.useCallback(
|
|
59
|
+
(date: DateValue) => {
|
|
60
|
+
const dateStr = date.toString()
|
|
61
|
+
return props.shouldDisableDate?.(dateStr) ?? false
|
|
62
|
+
},
|
|
63
|
+
[props.shouldDisableDate]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<I18nProvider locale="en">
|
|
68
|
+
<RangeCalendar
|
|
69
|
+
value={ariaValue}
|
|
70
|
+
onChange={handleChange}
|
|
71
|
+
isDateUnavailable={isDateUnavailable}
|
|
72
|
+
visibleDuration={{ months: 2 }}
|
|
73
|
+
className="text-sm"
|
|
74
|
+
>
|
|
75
|
+
<header className="flex items-center justify-between mb-2 px-1">
|
|
76
|
+
<Button slot="previous" className="p-1 hover:bg-neutral-100 rounded cursor-pointer">
|
|
77
|
+
<ChevronLeft />
|
|
78
|
+
</Button>
|
|
79
|
+
<Heading className="text-sm font-semibold text-neutral-900 flex-1 text-center" />
|
|
80
|
+
<Button slot="next" className="p-1 hover:bg-neutral-100 rounded cursor-pointer">
|
|
81
|
+
<ChevronRight />
|
|
82
|
+
</Button>
|
|
83
|
+
</header>
|
|
84
|
+
<div className="flex gap-4">
|
|
85
|
+
<CalendarGrid>
|
|
86
|
+
<CalendarGridHeader>
|
|
87
|
+
{(day) => (
|
|
88
|
+
<CalendarHeaderCell className="text-xs text-neutral-500 font-medium w-8 h-8">{day}</CalendarHeaderCell>
|
|
89
|
+
)}
|
|
90
|
+
</CalendarGridHeader>
|
|
91
|
+
<CalendarGridBody>
|
|
92
|
+
{(date) => (
|
|
93
|
+
<CalendarCell
|
|
94
|
+
date={date}
|
|
95
|
+
className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>
|
|
96
|
+
twMerge(
|
|
97
|
+
'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',
|
|
98
|
+
'hover:bg-neutral-100',
|
|
99
|
+
isOutsideMonth && 'text-neutral-300',
|
|
100
|
+
(isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',
|
|
101
|
+
isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',
|
|
102
|
+
(isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</CalendarGridBody>
|
|
108
|
+
</CalendarGrid>
|
|
109
|
+
<CalendarGrid offset={{ months: 1 }}>
|
|
110
|
+
<CalendarGridHeader>
|
|
111
|
+
{(day) => (
|
|
112
|
+
<CalendarHeaderCell className="text-xs text-neutral-500 font-medium w-8 h-8">{day}</CalendarHeaderCell>
|
|
113
|
+
)}
|
|
114
|
+
</CalendarGridHeader>
|
|
115
|
+
<CalendarGridBody>
|
|
116
|
+
{(date) => (
|
|
117
|
+
<CalendarCell
|
|
118
|
+
date={date}
|
|
119
|
+
className={({ isSelected, isSelectionStart, isSelectionEnd, isOutsideMonth, isDisabled, isUnavailable }) =>
|
|
120
|
+
twMerge(
|
|
121
|
+
'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center',
|
|
122
|
+
'hover:bg-neutral-100',
|
|
123
|
+
isOutsideMonth && 'text-neutral-300',
|
|
124
|
+
(isDisabled || isUnavailable) && 'text-neutral-300 cursor-not-allowed',
|
|
125
|
+
isSelected && !isSelectionStart && !isSelectionEnd && 'bg-primary-500/20',
|
|
126
|
+
(isSelectionStart || isSelectionEnd) && 'bg-primary-500 text-white hover:bg-primary-500'
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</CalendarGridBody>
|
|
132
|
+
</CalendarGrid>
|
|
133
|
+
</div>
|
|
134
|
+
</RangeCalendar>
|
|
135
|
+
</I18nProvider>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ChevronLeft() {
|
|
140
|
+
return (
|
|
141
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
|
|
142
|
+
<path
|
|
143
|
+
fillRule="evenodd"
|
|
144
|
+
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
|
145
|
+
clipRule="evenodd"
|
|
146
|
+
/>
|
|
147
|
+
</svg>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ChevronRight() {
|
|
152
|
+
return (
|
|
153
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
|
|
154
|
+
<path
|
|
155
|
+
fillRule="evenodd"
|
|
156
|
+
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
|
157
|
+
clipRule="evenodd"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import type { To, RelativeRoutingType } from 'react-router'
|
|
3
|
+
import { useNavigate } from 'react-router'
|
|
2
4
|
import { Modal, ModalOverlay, Dialog as AriaDialog, Heading } from 'react-aria-components'
|
|
3
5
|
import { tw } from '../utils/tw'
|
|
4
6
|
import { Button } from './button'
|
|
@@ -7,8 +9,10 @@ export type DialogProps = {
|
|
|
7
9
|
children?: React.ReactNode
|
|
8
10
|
title?: React.ReactNode
|
|
9
11
|
isOpen?: boolean
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
close?:
|
|
13
|
+
| ((isOpen: boolean) => void)
|
|
14
|
+
| { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }
|
|
15
|
+
size?: 'xs' | 'sm' | 'lg'
|
|
12
16
|
className?: string
|
|
13
17
|
isDismissable?: boolean
|
|
14
18
|
actions?: React.ReactNode
|
|
@@ -24,12 +28,22 @@ function Dialog_({
|
|
|
24
28
|
children,
|
|
25
29
|
title,
|
|
26
30
|
isOpen = false,
|
|
27
|
-
|
|
31
|
+
close,
|
|
28
32
|
size = 'sm',
|
|
29
33
|
className,
|
|
30
34
|
isDismissable = false,
|
|
31
35
|
actions
|
|
32
36
|
}: DialogProps) {
|
|
37
|
+
const navigate = useNavigate()
|
|
38
|
+
|
|
39
|
+
const onOpenChange = close
|
|
40
|
+
? typeof close === 'function'
|
|
41
|
+
? close
|
|
42
|
+
: (open: boolean) => {
|
|
43
|
+
if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })
|
|
44
|
+
}
|
|
45
|
+
: undefined
|
|
46
|
+
|
|
33
47
|
return (
|
|
34
48
|
<ModalOverlay
|
|
35
49
|
isOpen={isOpen}
|
|
@@ -37,7 +51,7 @@ function Dialog_({
|
|
|
37
51
|
isDismissable={isDismissable}
|
|
38
52
|
isKeyboardDismissDisabled={!isDismissable}
|
|
39
53
|
className={tw(
|
|
40
|
-
'fixed inset-0 z-[2000] flex items-center justify-center bg-black/
|
|
54
|
+
'fixed inset-0 z-[2000] flex items-center justify-center bg-black/25 backdrop-blur-sm',
|
|
41
55
|
'transition-opacity duration-300 ease-in-out',
|
|
42
56
|
'data-[entering]:opacity-0',
|
|
43
57
|
'data-[exiting]:opacity-0'
|
|
@@ -46,7 +60,7 @@ function Dialog_({
|
|
|
46
60
|
<Modal
|
|
47
61
|
className={tw(
|
|
48
62
|
'w-[calc(100%-4rem)] m-auto',
|
|
49
|
-
size === 'lg' ? 'max-w-[1080px]' : 'max-w-[640px]',
|
|
63
|
+
size === 'lg' ? 'max-w-[1080px]' : size === 'xs' ? 'max-w-[400px]' : 'max-w-[640px]',
|
|
50
64
|
'transition-all duration-300 ease-in-out',
|
|
51
65
|
'data-[entering]:scale-95 data-[entering]:opacity-0',
|
|
52
66
|
'data-[exiting]:scale-95 data-[exiting]:opacity-0'
|
|
@@ -54,7 +68,7 @@ function Dialog_({
|
|
|
54
68
|
>
|
|
55
69
|
<AriaDialog
|
|
56
70
|
className={tw(
|
|
57
|
-
'w-full max-h-[calc(100vh-6rem)] bg-white shadow-xl outline-none rounded-
|
|
71
|
+
'w-full max-h-[calc(100vh-6rem)] bg-white shadow-xl ring-1 ring-black/5 outline-none rounded-xl',
|
|
58
72
|
'flex flex-col overflow-x-hidden overflow-y-auto',
|
|
59
73
|
className
|
|
60
74
|
)}
|
|
@@ -63,7 +77,7 @@ function Dialog_({
|
|
|
63
77
|
<DialogContext.Provider value={{ close }}>
|
|
64
78
|
<div className="flex items-start justify-between gap-4 pt-6 px-6">
|
|
65
79
|
{title ? (
|
|
66
|
-
<Heading slot="title" className="flex-1 text-
|
|
80
|
+
<Heading slot="title" className="flex-1 text-lg font-semibold text-neutral-900">
|
|
67
81
|
{title}
|
|
68
82
|
</Heading>
|
|
69
83
|
) : (
|
|
@@ -85,7 +85,7 @@ function DisclosureHeader({
|
|
|
85
85
|
name="chevron-right"
|
|
86
86
|
className={tw(
|
|
87
87
|
'shrink-0 transition-transform duration-200 ease-in-out',
|
|
88
|
-
size === 'md' || size === 'lg' ? '
|
|
88
|
+
size === 'md' || size === 'lg' ? 'size-4' : 'size-3',
|
|
89
89
|
isExpanded && 'rotate-90',
|
|
90
90
|
isDisabled ? 'text-neutral-300' : 'text-neutral-500'
|
|
91
91
|
)}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import type { To, RelativeRoutingType } from 'react-router'
|
|
3
|
+
import { useNavigate } from 'react-router'
|
|
2
4
|
import { Drawer as VaulDrawer } from 'vaul'
|
|
3
5
|
import { tw } from '../utils/tw'
|
|
4
6
|
|
|
@@ -6,7 +8,9 @@ export type DrawerProps = {
|
|
|
6
8
|
children?: React.ReactNode
|
|
7
9
|
title?: React.ReactNode
|
|
8
10
|
isOpen?: boolean
|
|
9
|
-
|
|
11
|
+
close?:
|
|
12
|
+
| ((isOpen: boolean) => void)
|
|
13
|
+
| { to: To; replace?: boolean; relative?: RelativeRoutingType; reloadDocument?: boolean }
|
|
10
14
|
size?: 'sm' | 'md' | 'lg'
|
|
11
15
|
className?: string
|
|
12
16
|
isDismissable?: boolean
|
|
@@ -19,7 +23,7 @@ export function Drawer({
|
|
|
19
23
|
children,
|
|
20
24
|
title,
|
|
21
25
|
isOpen = false,
|
|
22
|
-
|
|
26
|
+
close,
|
|
23
27
|
size = 'md',
|
|
24
28
|
className,
|
|
25
29
|
isDismissable = true,
|
|
@@ -27,10 +31,20 @@ export function Drawer({
|
|
|
27
31
|
actions,
|
|
28
32
|
contentsContainerClassName
|
|
29
33
|
}: DrawerProps) {
|
|
34
|
+
const navigate = useNavigate()
|
|
35
|
+
|
|
36
|
+
const onOpenChange = close
|
|
37
|
+
? typeof close === 'function'
|
|
38
|
+
? close
|
|
39
|
+
: (open: boolean) => {
|
|
40
|
+
if (!open) navigate(close.to, { replace: close.replace, relative: close.relative })
|
|
41
|
+
}
|
|
42
|
+
: undefined
|
|
43
|
+
|
|
30
44
|
return (
|
|
31
45
|
<VaulDrawer.Root open={isOpen} onOpenChange={onOpenChange} direction={position} dismissible={isDismissable}>
|
|
32
46
|
<VaulDrawer.Portal>
|
|
33
|
-
<VaulDrawer.Overlay className="fixed inset-0 z-49 bg-black/
|
|
47
|
+
<VaulDrawer.Overlay className="fixed inset-0 z-49 bg-black/25 backdrop-blur-sm" />
|
|
34
48
|
<VaulDrawer.Content
|
|
35
49
|
className={tw(
|
|
36
50
|
'flex h-auto flex-col text-sm group/drawer-content fixed z-50',
|
|
@@ -50,7 +64,7 @@ export function Drawer({
|
|
|
50
64
|
{isDismissable ? (
|
|
51
65
|
<div
|
|
52
66
|
className={tw(
|
|
53
|
-
'absolute bg-neutral-
|
|
67
|
+
'absolute bg-neutral-300 rounded-full shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block',
|
|
54
68
|
'group-data-[vaul-drawer-direction=left]/drawer-content:h-25 group-data-[vaul-drawer-direction=left]/drawer-content:w-1 group-data-[vaul-drawer-direction=left]/drawer-content:right-2 group-data-[vaul-drawer-direction=left]/drawer-content:top-[calc(50%-50px)]',
|
|
55
69
|
'group-data-[vaul-drawer-direction=right]/drawer-content:h-25 group-data-[vaul-drawer-direction=right]/drawer-content:w-1 group-data-[vaul-drawer-direction=right]/drawer-content:left-2 group-data-[vaul-drawer-direction=right]/drawer-content:top-[calc(50%-50px)]',
|
|
56
70
|
'group-data-[vaul-drawer-direction=top]/drawer-content:w-25 group-data-[vaul-drawer-direction=top]/drawer-content:h-1 group-data-[vaul-drawer-direction=top]/drawer-content:bottom-2 group-data-[vaul-drawer-direction=top]/drawer-content:left-[calc(50%-50px)]',
|
|
@@ -59,7 +73,7 @@ export function Drawer({
|
|
|
59
73
|
/>
|
|
60
74
|
) : null}
|
|
61
75
|
|
|
62
|
-
<div className="bg-white size-full pt-6 px-6 pb-6 rounded-
|
|
76
|
+
<div className="bg-white size-full pt-6 px-6 pb-6 rounded-xl shadow-xl ring-1 ring-black/5">
|
|
63
77
|
<div className="flex items-start justify-between gap-4 pb-5">
|
|
64
78
|
{title ? (
|
|
65
79
|
<VaulDrawer.Title className="flex-1 text-xl font-semibold text-neutral-900">{title}</VaulDrawer.Title>
|
package/src/components/form.tsx
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
type FormProps as ReactRouterFormProps,
|
|
3
4
|
Form as ReactRouterForm,
|
|
4
5
|
data,
|
|
5
6
|
type ClientActionFunctionArgs
|
|
6
7
|
} from 'react-router'
|
|
7
|
-
import { HeadlessForm } from '@maestro-js/form'
|
|
8
|
+
import { HeadlessForm, type StandardSchema, type ParseFormDataOptions, type ParseFormDataResult } from '@maestro-js/form'
|
|
8
9
|
import { serverOnly$ } from 'vite-env-only/macros'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const _Form = forwardRef<HTMLFormElement, ReactRouterFormProps>(function Form({ children, ...props }, ref) {
|
|
11
12
|
return (
|
|
12
|
-
<HeadlessForm as={ReactRouterForm} {...props}>
|
|
13
|
+
<HeadlessForm as={ReactRouterForm} ref={ref} {...props}>
|
|
13
14
|
{children}
|
|
14
15
|
</HeadlessForm>
|
|
15
16
|
)
|
|
16
|
-
}
|
|
17
|
+
})
|
|
17
18
|
|
|
18
19
|
export const Form = Object.assign(_Form, {
|
|
19
20
|
HiddenInput: HeadlessForm.HiddenInput,
|
|
@@ -23,7 +24,28 @@ export const Form = Object.assign(_Form, {
|
|
|
23
24
|
useTopLevelFormValidation: HeadlessForm.useTopLevelFormValidation,
|
|
24
25
|
useReset: HeadlessForm.useReset,
|
|
25
26
|
clientSafeParse: HeadlessForm.parseFormDataClientSide,
|
|
26
|
-
serverSafeParse
|
|
27
|
+
serverSafeParse<T>(
|
|
28
|
+
schema: StandardSchema<T, T>,
|
|
29
|
+
formData: FormData | Promise<FormData>,
|
|
30
|
+
options?: {
|
|
31
|
+
handleFile: ParseFormDataOptions<T>['handleFile']
|
|
32
|
+
}
|
|
33
|
+
): Promise<ParseFormDataResult<T>> {
|
|
34
|
+
return HeadlessForm.parseFormData(schema, formData, {
|
|
35
|
+
...options,
|
|
36
|
+
jsonSchemaLibraryOptions: {
|
|
37
|
+
unrepresentable: 'any',
|
|
38
|
+
override: (ctx) => {
|
|
39
|
+
const meta = ctx.zodSchema.meta()
|
|
40
|
+
const custom = meta?.jsonSchema
|
|
41
|
+
|
|
42
|
+
if (custom && typeof custom === 'object') {
|
|
43
|
+
Object.assign(ctx.jsonSchema, custom)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
},
|
|
27
49
|
validationError: serverOnly$(validationError) as typeof validationError,
|
|
28
50
|
customValidationError: serverOnly$(customValidationError) as typeof customValidationError,
|
|
29
51
|
composeValidators: HeadlessForm.composeValidators,
|
|
@@ -85,17 +85,16 @@ const FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(
|
|
|
85
85
|
<Group
|
|
86
86
|
ref={ref}
|
|
87
87
|
className={tw(
|
|
88
|
-
'relative overflow-hidden
|
|
89
|
-
'focus-within:
|
|
88
|
+
'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',
|
|
89
|
+
'focus-within:outline-2 focus-within:-outline-offset-1',
|
|
90
90
|
shape,
|
|
91
|
-
disabled && '
|
|
92
|
-
opts.isInvalid && '
|
|
93
|
-
opts.hasWarning &&
|
|
94
|
-
'border-notice-200 bg-notice-50 focus-within:border-notice-500 focus-within:ring-notice-500/20',
|
|
91
|
+
disabled && 'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',
|
|
92
|
+
opts.isInvalid && 'ring-negative-300 bg-negative-50 focus-within:outline-negative-500',
|
|
93
|
+
opts.hasWarning && 'ring-notice-300 bg-notice-50 focus-within:outline-notice-500',
|
|
95
94
|
!opts.isInvalid &&
|
|
96
95
|
!opts.hasWarning &&
|
|
97
96
|
!disabled &&
|
|
98
|
-
'
|
|
97
|
+
'ring-black/10 hover:ring-black/20 focus-within:outline-primary-500',
|
|
99
98
|
className
|
|
100
99
|
)}
|
|
101
100
|
onClick={onClick}
|
|
@@ -114,7 +113,7 @@ const FieldInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) =
|
|
|
114
113
|
className={composeTailwindRenderProps(
|
|
115
114
|
props.className,
|
|
116
115
|
tw(
|
|
117
|
-
'flex-1 bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',
|
|
116
|
+
'flex-1 bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',
|
|
118
117
|
(props.disabled || props.readOnly) && 'cursor-not-allowed'
|
|
119
118
|
)
|
|
120
119
|
)}
|