@maestro-js/components 1.0.0-alpha.1 → 1.0.0-alpha.11
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
|
@@ -6,19 +6,19 @@ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
|
6
6
|
export type ButtonShape = 'rectangle' | 'circle' | 'oval'
|
|
7
7
|
|
|
8
8
|
const base =
|
|
9
|
-
'relative inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-
|
|
9
|
+
'relative inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-150 cursor-default select-none [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0 data-[disabled]:cursor-not-allowed'
|
|
10
10
|
|
|
11
11
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
12
|
-
xs: 'h-7 min-w-14 px-2
|
|
13
|
-
sm: 'h-8 min-w-16 px-
|
|
14
|
-
md: 'h-9 min-w-20 px-
|
|
15
|
-
lg: 'h-10 min-w-24 px-
|
|
16
|
-
xl: 'h-12 min-w-28 px-
|
|
12
|
+
xs: 'h-7 min-w-14 px-2 text-xs',
|
|
13
|
+
sm: 'h-8 min-w-16 px-2.5 text-sm',
|
|
14
|
+
md: 'h-9 min-w-20 px-3 text-sm',
|
|
15
|
+
lg: 'h-10 min-w-24 px-4 text-base',
|
|
16
|
+
xl: 'h-12 min-w-28 px-5 text-base'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const shapeClasses: Record<ButtonShape, string> = {
|
|
20
20
|
rectangle: 'rounded',
|
|
21
|
-
circle: 'rounded-full aspect-square px-0',
|
|
21
|
+
circle: 'rounded-full aspect-square px-0 min-w-0',
|
|
22
22
|
oval: 'rounded-full'
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -37,39 +37,39 @@ const variantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> =
|
|
|
37
37
|
},
|
|
38
38
|
outlined: {
|
|
39
39
|
primary:
|
|
40
|
-
'bg-white ring-1 ring-inset ring-primary-300 text-primary-700 data-[hovered]:bg-primary-50 data-[hovered]:ring-primary-400 data-[pressed]:bg-primary-100 data-[pressed]:ring-primary-500 data-[disabled]:bg-neutral-
|
|
40
|
+
'bg-white ring-1 ring-inset ring-primary-300 text-primary-700 data-[hovered]:bg-primary-50 data-[hovered]:ring-primary-400 data-[pressed]:bg-primary-100 data-[pressed]:ring-primary-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',
|
|
41
41
|
negative:
|
|
42
|
-
'bg-white ring-1 ring-inset ring-negative-300 text-negative-700 data-[hovered]:bg-negative-50 data-[hovered]:ring-negative-400 data-[pressed]:bg-negative-100 data-[pressed]:ring-negative-500 data-[disabled]:bg-neutral-
|
|
42
|
+
'bg-white ring-1 ring-inset ring-negative-300 text-negative-700 data-[hovered]:bg-negative-50 data-[hovered]:ring-negative-400 data-[pressed]:bg-negative-100 data-[pressed]:ring-negative-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',
|
|
43
43
|
positive:
|
|
44
|
-
'bg-white ring-1 ring-inset ring-positive-300 text-positive-700 data-[hovered]:bg-positive-50 data-[hovered]:ring-positive-400 data-[pressed]:bg-positive-100 data-[pressed]:ring-positive-500 data-[disabled]:bg-neutral-
|
|
44
|
+
'bg-white ring-1 ring-inset ring-positive-300 text-positive-700 data-[hovered]:bg-positive-50 data-[hovered]:ring-positive-400 data-[pressed]:bg-positive-100 data-[pressed]:ring-positive-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',
|
|
45
45
|
notice:
|
|
46
|
-
'bg-white ring-1 ring-inset ring-notice-300 text-notice-700 data-[hovered]:bg-notice-50 data-[hovered]:ring-notice-400 data-[pressed]:bg-notice-100 data-[pressed]:ring-notice-500 data-[disabled]:bg-neutral-
|
|
46
|
+
'bg-white ring-1 ring-inset ring-notice-300 text-notice-700 data-[hovered]:bg-notice-50 data-[hovered]:ring-notice-400 data-[pressed]:bg-notice-100 data-[pressed]:ring-notice-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent',
|
|
47
47
|
neutral:
|
|
48
|
-
'bg-white ring-1 ring-inset ring-neutral-300 text-neutral-700 data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400 data-[pressed]:bg-neutral-100 data-[pressed]:ring-neutral-500 data-[disabled]:bg-neutral-
|
|
48
|
+
'bg-white ring-1 ring-inset ring-neutral-300 text-neutral-700 data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400 data-[pressed]:bg-neutral-100 data-[pressed]:ring-neutral-500 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent'
|
|
49
49
|
},
|
|
50
50
|
soft: {
|
|
51
51
|
primary:
|
|
52
|
-
'bg-primary-100 text-primary-800 data-[hovered]:bg-primary-200 data-[pressed]:bg-primary-300 data-[disabled]:bg-neutral-
|
|
52
|
+
'bg-primary-100 text-primary-800 data-[hovered]:bg-primary-200 data-[pressed]:bg-primary-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
53
53
|
negative:
|
|
54
|
-
'bg-negative-100 text-negative-800 data-[hovered]:bg-negative-200 data-[pressed]:bg-negative-300 data-[disabled]:bg-neutral-
|
|
54
|
+
'bg-negative-100 text-negative-800 data-[hovered]:bg-negative-200 data-[pressed]:bg-negative-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
55
55
|
positive:
|
|
56
|
-
'bg-positive-100 text-positive-800 data-[hovered]:bg-positive-200 data-[pressed]:bg-positive-300 data-[disabled]:bg-neutral-
|
|
56
|
+
'bg-positive-100 text-positive-800 data-[hovered]:bg-positive-200 data-[pressed]:bg-positive-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
57
57
|
notice:
|
|
58
|
-
'bg-notice-100 text-notice-800 data-[hovered]:bg-notice-200 data-[pressed]:bg-notice-300 data-[disabled]:bg-neutral-
|
|
58
|
+
'bg-notice-100 text-notice-800 data-[hovered]:bg-notice-200 data-[pressed]:bg-notice-300 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
59
59
|
neutral:
|
|
60
|
-
'bg-neutral-200 text-neutral-800 data-[hovered]:bg-neutral-300 data-[pressed]:bg-neutral-400 data-[disabled]:bg-neutral-
|
|
60
|
+
'bg-neutral-200 text-neutral-800 data-[hovered]:bg-neutral-300 data-[pressed]:bg-neutral-400 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'
|
|
61
61
|
},
|
|
62
62
|
plain: {
|
|
63
63
|
primary:
|
|
64
|
-
'bg-transparent text-primary-700 data-[hovered]:bg-primary-50 data-[pressed]:bg-primary-100 data-[disabled]:bg-
|
|
64
|
+
'bg-transparent text-primary-700 data-[hovered]:bg-primary-50 data-[pressed]:bg-primary-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
65
65
|
negative:
|
|
66
|
-
'bg-transparent text-negative-700 data-[hovered]:bg-negative-50 data-[pressed]:bg-negative-100 data-[disabled]:bg-
|
|
66
|
+
'bg-transparent text-negative-700 data-[hovered]:bg-negative-50 data-[pressed]:bg-negative-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
67
67
|
positive:
|
|
68
|
-
'bg-transparent text-positive-700 data-[hovered]:bg-positive-50 data-[pressed]:bg-positive-100 data-[disabled]:bg-
|
|
68
|
+
'bg-transparent text-positive-700 data-[hovered]:bg-positive-50 data-[pressed]:bg-positive-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
69
69
|
notice:
|
|
70
|
-
'bg-transparent text-notice-700 data-[hovered]:bg-notice-50 data-[pressed]:bg-notice-100 data-[disabled]:bg-
|
|
70
|
+
'bg-transparent text-notice-700 data-[hovered]:bg-notice-50 data-[pressed]:bg-notice-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400',
|
|
71
71
|
neutral:
|
|
72
|
-
'bg-transparent text-neutral-700 data-[hovered]:bg-neutral-50 data-[pressed]:bg-neutral-100 data-[disabled]:bg-
|
|
72
|
+
'bg-transparent text-neutral-700 data-[hovered]:bg-neutral-50 data-[pressed]:bg-neutral-100 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400'
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -105,7 +105,7 @@ const toggledVariantColorClasses: Record<ButtonVariant, Record<ButtonColor, stri
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const focusClasses =
|
|
108
|
-
'data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2 data-[focus-visible]:outline-primary-600'
|
|
108
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2 data-[focus-visible]:outline-primary-600'
|
|
109
109
|
|
|
110
110
|
export function getButtonClasses({
|
|
111
111
|
variant = 'contained',
|
|
@@ -27,7 +27,7 @@ export const HeadlessButton = React.forwardRef<
|
|
|
27
27
|
{...props}
|
|
28
28
|
{...(props.disabled || props['data-disabled'] ? { ['data-disabled']: true } : {})}
|
|
29
29
|
{...((isPressed && !props.disabled) || props['data-pressed'] ? { ['data-pressed']: true } : {})}
|
|
30
|
-
aria-pressed={isPressed && !props.disabled}
|
|
30
|
+
aria-pressed={props['aria-pressed'] ?? (isPressed && !props.disabled)}
|
|
31
31
|
ref={ref}
|
|
32
32
|
/>
|
|
33
33
|
)
|
|
@@ -13,11 +13,11 @@ export type InlineAlertProps = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const variantClasses = {
|
|
16
|
-
info: 'bg-info-
|
|
16
|
+
info: 'bg-info-50 text-info-800 border-info-200',
|
|
17
17
|
positive: 'bg-positive-50 text-positive-800 border-positive-200',
|
|
18
18
|
notice: 'bg-notice-50 text-notice-800 border-notice-200',
|
|
19
19
|
negative: 'bg-negative-50 text-negative-800 border-negative-200',
|
|
20
|
-
neutral: 'bg-neutral-
|
|
20
|
+
neutral: 'bg-neutral-50 text-neutral-700 border-neutral-200'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const defaultIconNames = {
|
|
@@ -34,7 +34,7 @@ export function InlineAlert({ children, actions, variant, icon, className, title
|
|
|
34
34
|
return (
|
|
35
35
|
<div
|
|
36
36
|
className={tw(
|
|
37
|
-
'rounded-
|
|
37
|
+
'rounded-lg border px-4 py-3 flex gap-3 items-start justify-between text-left',
|
|
38
38
|
variantClasses[variant],
|
|
39
39
|
className
|
|
40
40
|
)}
|
|
@@ -158,7 +158,7 @@ export const LabeledValue = React.forwardRef<HTMLDivElement, LabeledValueProps>(
|
|
|
158
158
|
<LabeledValueLabel
|
|
159
159
|
id={actualLabelId}
|
|
160
160
|
className={tw(
|
|
161
|
-
orientation === 'horizontal' ? '
|
|
161
|
+
orientation === 'horizontal' ? 'shrink-0' : 'mb-1',
|
|
162
162
|
labelAlign === 'end' && 'text-right',
|
|
163
163
|
labelClassName
|
|
164
164
|
)}
|
|
@@ -474,7 +474,7 @@ export function LabeledValueLabel(props: React.ComponentProps<'label'>) {
|
|
|
474
474
|
return (
|
|
475
475
|
<label
|
|
476
476
|
{...props}
|
|
477
|
-
className={tw('block uppercase text-xs tracking-
|
|
477
|
+
className={tw('block uppercase text-xs tracking-wider font-medium text-neutral-500', props.className)}
|
|
478
478
|
/>
|
|
479
479
|
)
|
|
480
480
|
}
|
package/src/components/menu.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Pressable,
|
|
8
8
|
Separator as AriaSeparator
|
|
9
9
|
} from 'react-aria-components'
|
|
10
|
+
import { Link, useHref, type RelativeRoutingType, type To } from 'react-router'
|
|
10
11
|
import { AnimatedPopover } from './helpers/animated-popover'
|
|
11
12
|
import { tw } from '../utils/tw'
|
|
12
13
|
|
|
@@ -18,20 +19,26 @@ export type MenuProps = {
|
|
|
18
19
|
children?: React.ReactNode
|
|
19
20
|
className?: string
|
|
20
21
|
containerClassName?: string
|
|
21
|
-
placement?: 'bottom' | 'bottom start' | 'bottom end' | 'top' | 'top start' | 'top end'
|
|
22
|
+
placement?: 'bottom' | 'bottom start' | 'bottom end' | 'top' | 'top start' | 'top end' | 'left' | 'right'
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export type MenuItemProps = {
|
|
25
26
|
children?: React.ReactNode
|
|
26
27
|
className?: string
|
|
27
28
|
isDisabled?: boolean
|
|
28
|
-
onAction?: () => void
|
|
29
29
|
id?: string
|
|
30
30
|
textValue?: string
|
|
31
|
+
type?: 'button' | 'submit'
|
|
32
|
+
onAction?: () => void
|
|
33
|
+
onClick?: ((e: React.MouseEvent<Element & HTMLOrSVGElement, MouseEvent>) => void) | undefined
|
|
34
|
+
form?: string
|
|
35
|
+
name?: string
|
|
36
|
+
value?: string
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export type MenuLinkItemProps = {
|
|
34
|
-
|
|
40
|
+
to: To
|
|
41
|
+
relative?: RelativeRoutingType
|
|
35
42
|
children?: React.ReactNode
|
|
36
43
|
className?: string
|
|
37
44
|
isDisabled?: boolean
|
|
@@ -45,6 +52,17 @@ export type MenuGroupProps = {
|
|
|
45
52
|
children?: React.ReactNode
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function itemClassName({ isFocused, isDisabled }: { isFocused: boolean; isDisabled: boolean }, className?: string) {
|
|
58
|
+
return tw(
|
|
59
|
+
isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-700',
|
|
60
|
+
'group flex w-full items-center px-4 py-2 text-sm gap-2 outline-none',
|
|
61
|
+
isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
|
62
|
+
className
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
// ── MenuMain ──────────────────────────────────────────────────────────────────
|
|
49
67
|
|
|
50
68
|
function MenuMain({ Button: ButtonProp, children, className, containerClassName, placement = 'bottom end' }: MenuProps) {
|
|
@@ -67,7 +85,7 @@ function MenuMain({ Button: ButtonProp, children, className, containerClassName,
|
|
|
67
85
|
<AnimatedPopover placement={placement} offset={8}>
|
|
68
86
|
<AriaMenu
|
|
69
87
|
className={tw(
|
|
70
|
-
'overflow-hidden min-w-56 w-max rounded bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none',
|
|
88
|
+
'overflow-hidden min-w-56 w-max rounded-lg bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none',
|
|
71
89
|
className
|
|
72
90
|
)}
|
|
73
91
|
autoFocus="first"
|
|
@@ -84,46 +102,62 @@ MenuMain.displayName = 'Menu'
|
|
|
84
102
|
|
|
85
103
|
// ── MenuItem ──────────────────────────────────────────────────────────────────
|
|
86
104
|
|
|
87
|
-
function Item({
|
|
105
|
+
function Item({
|
|
106
|
+
children,
|
|
107
|
+
className,
|
|
108
|
+
isDisabled,
|
|
109
|
+
id,
|
|
110
|
+
textValue,
|
|
111
|
+
type,
|
|
112
|
+
onAction,
|
|
113
|
+
onClick,
|
|
114
|
+
form,
|
|
115
|
+
name,
|
|
116
|
+
value
|
|
117
|
+
}: MenuItemProps) {
|
|
118
|
+
const itemRef = React.useRef<HTMLDivElement>(null)
|
|
119
|
+
const isSubmit = type === 'submit'
|
|
120
|
+
|
|
88
121
|
return (
|
|
89
122
|
<AriaMenuItem
|
|
90
123
|
id={id}
|
|
124
|
+
ref={isSubmit ? itemRef : undefined}
|
|
125
|
+
onClick={onClick}
|
|
91
126
|
textValue={textValue ?? (typeof children === 'string' ? children : undefined)}
|
|
92
|
-
onAction={
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
onAction={
|
|
128
|
+
isSubmit
|
|
129
|
+
? () => {
|
|
130
|
+
onAction?.()
|
|
131
|
+
const targetForm = form
|
|
132
|
+
? (document.getElementById(form) as HTMLFormElement | null)
|
|
133
|
+
: (itemRef.current?.closest('form') ?? null)
|
|
134
|
+
if (!targetForm) return
|
|
135
|
+
targetForm.requestSubmit()
|
|
136
|
+
}
|
|
137
|
+
: onAction
|
|
101
138
|
}
|
|
139
|
+
isDisabled={isDisabled}
|
|
140
|
+
className={(renderProps) => itemClassName(renderProps, className)}
|
|
102
141
|
>
|
|
103
142
|
{children}
|
|
143
|
+
{isSubmit && <input type="submit" className="sr-only" name={name} value={value ?? ''} form={form} />}
|
|
104
144
|
</AriaMenuItem>
|
|
105
145
|
)
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
// ── LinkItem ──────────────────────────────────────────────────────────────────
|
|
109
149
|
|
|
110
|
-
function LinkItem({
|
|
150
|
+
function LinkItem({ to, children, className, relative, isDisabled, target, rel, id, textValue }: MenuLinkItemProps) {
|
|
111
151
|
return (
|
|
112
152
|
<AriaMenuItem
|
|
113
153
|
id={id}
|
|
114
|
-
|
|
115
|
-
href={href}
|
|
154
|
+
href={useHref(to, { relative })}
|
|
116
155
|
target={target}
|
|
117
156
|
rel={rel}
|
|
157
|
+
textValue={textValue ?? (typeof children === 'string' ? children : undefined)}
|
|
118
158
|
isDisabled={isDisabled}
|
|
119
|
-
className={(
|
|
120
|
-
|
|
121
|
-
isFocused ? 'bg-neutral-100 text-neutral-900' : 'text-neutral-700',
|
|
122
|
-
'group flex items-center px-4 py-2 text-sm gap-2 outline-none',
|
|
123
|
-
isDisabled ? 'cursor-not-allowed opacity-50 pointer-events-none' : 'cursor-pointer',
|
|
124
|
-
className
|
|
125
|
-
)
|
|
126
|
-
}
|
|
159
|
+
className={(renderProps) => itemClassName(renderProps, className)}
|
|
160
|
+
render={(props) => <Link {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)} relative={relative} to={to} />}
|
|
127
161
|
>
|
|
128
162
|
{children}
|
|
129
163
|
</AriaMenuItem>
|
|
@@ -139,7 +173,7 @@ function Group({ children }: MenuGroupProps) {
|
|
|
139
173
|
// ── Separator ─────────────────────────────────────────────────────────────────
|
|
140
174
|
|
|
141
175
|
function MenuSeparator() {
|
|
142
|
-
return <AriaSeparator className="border-t border-neutral-
|
|
176
|
+
return <AriaSeparator className="border-t border-neutral-950/5" />
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
// ── Export ─────────────────────────────────────────────────────────────────────
|
|
@@ -165,7 +165,8 @@ export function MonthDayInput(props: MonthDayInputProps) {
|
|
|
165
165
|
function parseMonthDay(monthDay: Iso.MonthDay): CalendarDate | null {
|
|
166
166
|
if (!monthDayFns.isValid(monthDay)) return null
|
|
167
167
|
// Use a leap-year placeholder so Feb 29 is valid
|
|
168
|
-
|
|
168
|
+
const { month, day } = monthDayFns.getFields(monthDay)
|
|
169
|
+
return new CalendarDate(1972, month, day)
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
function formatMonthDay(date: CalendarDate | null): Iso.MonthDay | null {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { ListBox, ListBoxItem } from 'react-aria-components'
|
|
3
|
-
import { AnimatedPopover } from './helpers/animated-popover'
|
|
4
3
|
import type { Selection } from 'react-aria-components'
|
|
5
4
|
import { HeadlessForm } from '@maestro-js/form'
|
|
6
5
|
import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
|
|
@@ -58,6 +57,7 @@ export type MultiselectProps<T extends MultiselectOption = MultiselectOption> =
|
|
|
58
57
|
export function Multiselect<T extends MultiselectOption = MultiselectOption>(props: MultiselectProps<T>) {
|
|
59
58
|
const hiddenInputRef = React.useRef<HTMLInputElement>(null)
|
|
60
59
|
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
|
60
|
+
const popoverContentRef = React.useRef<HTMLDivElement>(null)
|
|
61
61
|
const [containerRef, setContainerRef] = React.useState<HTMLButtonElement | null>(null)
|
|
62
62
|
const helpTextId = React.useId()
|
|
63
63
|
const labelId = React.useId()
|
|
@@ -89,6 +89,19 @@ export function Multiselect<T extends MultiselectOption = MultiselectOption>(pro
|
|
|
89
89
|
hiddenInputRef
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
+
// Close dropdown on outside click
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
if (!isOpen) return
|
|
95
|
+
const handlePointerDown = (e: PointerEvent) => {
|
|
96
|
+
const target = e.target as Node
|
|
97
|
+
if (popoverContentRef.current?.contains(target) || buttonRef.current?.contains(target)) return
|
|
98
|
+
setIsOpen(false)
|
|
99
|
+
commitValidation()
|
|
100
|
+
}
|
|
101
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
102
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
103
|
+
})
|
|
104
|
+
|
|
92
105
|
const isInvalid = props.isInvalid || validationInvalid
|
|
93
106
|
const hasWarning = Boolean(props.warningMessage) && !isInvalid
|
|
94
107
|
|
|
@@ -235,82 +248,73 @@ export function Multiselect<T extends MultiselectOption = MultiselectOption>(pro
|
|
|
235
248
|
</button>
|
|
236
249
|
|
|
237
250
|
{/* Options dropdown */}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
className={tw(
|
|
252
|
-
'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'
|
|
253
|
-
)}
|
|
254
|
-
>
|
|
255
|
-
{props.showSelectAll && (
|
|
256
|
-
<button
|
|
257
|
-
type="button"
|
|
258
|
-
onClick={handleSelectAll}
|
|
259
|
-
className="w-full px-3 py-2 text-left hover:bg-neutral-50 border-b border-neutral-100 text-sm font-medium"
|
|
260
|
-
>
|
|
261
|
-
{selectedValues.length === props.options.filter((o) => !o.isDisabled).length ? 'Deselect All' : 'Select All'}
|
|
262
|
-
</button>
|
|
263
|
-
)}
|
|
264
|
-
<ListBox
|
|
265
|
-
aria-label={props.label ? String(props.label) : 'Multiselect'}
|
|
266
|
-
selectionMode="multiple"
|
|
267
|
-
selectedKeys={selectedKeys}
|
|
268
|
-
onSelectionChange={handleSelectionChange}
|
|
269
|
-
className="max-h-60 overflow-y-auto focus:outline-none"
|
|
270
|
-
items={props.options.map((option, index) => ({ ...option, index, id: String(option.value) }))}
|
|
271
|
-
renderEmptyState={() => <div className="px-3 py-2 text-neutral-500 text-sm">No options available</div>}
|
|
251
|
+
{isOpen && !props.isDisabled && (
|
|
252
|
+
<div
|
|
253
|
+
ref={popoverContentRef}
|
|
254
|
+
className={tw(
|
|
255
|
+
'absolute z-50 mt-1 overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none'
|
|
256
|
+
)}
|
|
257
|
+
style={{ width: containerSize.width, minWidth: containerSize.width }}
|
|
258
|
+
>
|
|
259
|
+
{props.showSelectAll && (
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={handleSelectAll}
|
|
263
|
+
className="w-full px-3 py-2 text-left hover:bg-neutral-50 border-b border-neutral-100 text-sm font-medium"
|
|
272
264
|
>
|
|
273
|
-
{(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
option,
|
|
289
|
-
selected: isSelected,
|
|
290
|
-
colored: props.colored ?? false,
|
|
291
|
-
index: option.index
|
|
292
|
-
})}
|
|
293
|
-
</>
|
|
294
|
-
)}
|
|
295
|
-
</ListBoxItem>
|
|
296
|
-
)
|
|
297
|
-
}
|
|
265
|
+
{selectedValues.length === props.options.filter((o) => !o.isDisabled).length ? 'Deselect All' : 'Select All'}
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
<ListBox
|
|
269
|
+
aria-label={props.label ? String(props.label) : 'Multiselect'}
|
|
270
|
+
selectionMode="multiple"
|
|
271
|
+
selectedKeys={selectedKeys}
|
|
272
|
+
onSelectionChange={handleSelectionChange}
|
|
273
|
+
className="max-h-60 overflow-y-auto focus:outline-none"
|
|
274
|
+
items={props.options.map((option, index) => ({ ...option, index, id: String(option.value) }))}
|
|
275
|
+
renderEmptyState={() => <div className="px-3 py-2 text-neutral-500 text-sm">No options available</div>}
|
|
276
|
+
>
|
|
277
|
+
{(item) => {
|
|
278
|
+
const option = item as T & { index: number; id: string }
|
|
279
|
+
const color = props.colored ? getColor(stringToNumber(String(option.value))) : null
|
|
298
280
|
|
|
281
|
+
if (props.renderOption) {
|
|
299
282
|
return (
|
|
300
|
-
<
|
|
283
|
+
<ListBoxItem
|
|
301
284
|
id={option.id}
|
|
302
285
|
textValue={option.label}
|
|
303
|
-
label={option.label}
|
|
304
|
-
secondary={option.secondary}
|
|
305
286
|
isDisabled={option.isDisabled}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
287
|
+
className="outline-none"
|
|
288
|
+
>
|
|
289
|
+
{({ isSelected }) => (
|
|
290
|
+
<>
|
|
291
|
+
{props.renderOption!({
|
|
292
|
+
option,
|
|
293
|
+
selected: isSelected,
|
|
294
|
+
colored: props.colored ?? false,
|
|
295
|
+
index: option.index
|
|
296
|
+
})}
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</ListBoxItem>
|
|
309
300
|
)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<FormFieldComponents.DropdownItem
|
|
305
|
+
id={option.id}
|
|
306
|
+
textValue={option.label}
|
|
307
|
+
label={option.label}
|
|
308
|
+
secondary={option.secondary}
|
|
309
|
+
isDisabled={option.isDisabled}
|
|
310
|
+
color={color}
|
|
311
|
+
className="px-3"
|
|
312
|
+
/>
|
|
313
|
+
)
|
|
314
|
+
}}
|
|
315
|
+
</ListBox>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
314
318
|
|
|
315
319
|
<FormFieldComponents.FormFieldHelpText
|
|
316
320
|
id={helpTextId}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type LinkProps, Link } from 'react-router'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export type OptionalLinkProps = Omit<LinkProps, 'to'> & { to?: LinkProps['to']; disabled?: boolean }
|
|
5
|
+
|
|
6
|
+
export const OptionalLink = React.forwardRef<HTMLAnchorElement, OptionalLinkProps>(
|
|
7
|
+
({ to, onClick, disabled, ...props }, ref) => {
|
|
8
|
+
if (to && !disabled) {
|
|
9
|
+
return <Link to={to} onClick={onClick} {...props} ref={ref} />
|
|
10
|
+
} else {
|
|
11
|
+
return <span {...props} ref={ref} />
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
OptionalLink.displayName = 'OptionalLink'
|
package/src/components/radio.tsx
CHANGED
|
@@ -25,7 +25,7 @@ export function PlainRadio(props: PlainRadioProps) {
|
|
|
25
25
|
value={props.value}
|
|
26
26
|
isDisabled={props.isDisabled}
|
|
27
27
|
aria-label={props['aria-label']}
|
|
28
|
-
className="group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none
|
|
28
|
+
className="group inline-flex items-center cursor-pointer data-[disabled]:cursor-not-allowed focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
|
|
29
29
|
>
|
|
30
30
|
{({ isSelected, isDisabled }) => <RadioCircle isSelected={isSelected} isDisabled={isDisabled} />}
|
|
31
31
|
</AriaRadio>
|
|
@@ -41,11 +41,11 @@ export function LabeledRadio(props: LabeledRadioProps) {
|
|
|
41
41
|
<AriaRadio
|
|
42
42
|
value={props.value}
|
|
43
43
|
isDisabled={props.isDisabled}
|
|
44
|
-
className="group flex items-start gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none
|
|
44
|
+
className="group flex items-start gap-3 cursor-pointer data-[disabled]:cursor-not-allowed select-none focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
|
|
45
45
|
>
|
|
46
46
|
{({ isSelected, isDisabled }) => (
|
|
47
47
|
<>
|
|
48
|
-
<div className="
|
|
48
|
+
<div className="shrink-0">
|
|
49
49
|
<RadioCircle isSelected={isSelected} isDisabled={isDisabled} />
|
|
50
50
|
</div>
|
|
51
51
|
{props.label && (
|
|
@@ -74,16 +74,16 @@ function RadioCircle({ isSelected, isDisabled }: { isSelected: boolean; isDisabl
|
|
|
74
74
|
return (
|
|
75
75
|
<div
|
|
76
76
|
className={tw(
|
|
77
|
-
'
|
|
78
|
-
isSelected ? 'border-primary-
|
|
77
|
+
'size-5 rounded-full border-2 transition-colors duration-200 flex items-center justify-center',
|
|
78
|
+
isSelected ? 'border-primary-600 bg-primary-600' : 'border-neutral-300 bg-white',
|
|
79
79
|
!isDisabled && !isSelected && 'group-hovered:border-neutral-400',
|
|
80
|
-
isDisabled && '
|
|
80
|
+
isDisabled && 'cursor-not-allowed border-neutral-300 bg-neutral-100'
|
|
81
81
|
)}
|
|
82
82
|
>
|
|
83
83
|
<div
|
|
84
84
|
className={tw(
|
|
85
|
-
'
|
|
86
|
-
isSelected ? 'scale-100
|
|
85
|
+
'size-2 rounded-full bg-white transition-transform duration-200',
|
|
86
|
+
isSelected ? 'scale-100' : 'scale-0'
|
|
87
87
|
)}
|
|
88
88
|
/>
|
|
89
89
|
</div>
|
|
@@ -114,18 +114,19 @@ export function Select<T extends SelectOption = SelectOption>(props: SelectProps
|
|
|
114
114
|
<Button
|
|
115
115
|
ref={buttonRef}
|
|
116
116
|
className={tw(
|
|
117
|
-
'relative overflow-hidden
|
|
117
|
+
'relative overflow-hidden ring-1 shadow-sm transition-all duration-200 max-w-96',
|
|
118
118
|
props.shape === 'oval' ? 'rounded-full' : 'rounded',
|
|
119
|
-
'focus:outline-
|
|
120
|
-
(props.isDisabled || props.isReadonly) &&
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
'focus:outline-2 focus:-outline-offset-1',
|
|
120
|
+
(props.isDisabled || props.isReadonly) &&
|
|
121
|
+
'ring-neutral-200 bg-neutral-50 text-neutral-400 shadow-none cursor-not-allowed',
|
|
122
|
+
isInvalid && 'ring-negative-300 bg-negative-50 focus:outline-negative-500',
|
|
123
|
+
hasWarning && 'ring-notice-300 bg-notice-50 focus:outline-notice-500',
|
|
123
124
|
!isInvalid &&
|
|
124
125
|
!hasWarning &&
|
|
125
126
|
!props.isDisabled &&
|
|
126
127
|
!props.isReadonly &&
|
|
127
|
-
'
|
|
128
|
-
'w-full cursor-default py-2 pl-3 pr-10 text-left placeholder:text-neutral-400'
|
|
128
|
+
'ring-black/10 hover:ring-black/20 focus:outline-primary-500',
|
|
129
|
+
'w-full cursor-default py-2 pl-3 pr-10 text-left text-sm max-sm:text-base placeholder:text-neutral-400'
|
|
129
130
|
)}
|
|
130
131
|
aria-invalid={isInvalid || undefined}
|
|
131
132
|
aria-describedby={helpTextId}
|
|
@@ -139,7 +140,7 @@ export function Select<T extends SelectOption = SelectOption>(props: SelectProps
|
|
|
139
140
|
{selectedOption ? (
|
|
140
141
|
<span className="block truncate text-sm min-h-5">{selectedOption.label}</span>
|
|
141
142
|
) : (
|
|
142
|
-
<span className="block truncate text-sm min-h-5 text-neutral-
|
|
143
|
+
<span className="block truncate text-sm min-h-5 text-neutral-400 italic">
|
|
143
144
|
{props.placeholder ?? 'Select an option'}
|
|
144
145
|
</span>
|
|
145
146
|
)}
|
|
@@ -100,12 +100,12 @@ function Step({
|
|
|
100
100
|
<div className={tw('flex flex-row items-start flex-1 relative', className)}>
|
|
101
101
|
{index !== 0 ? (
|
|
102
102
|
<div className="flex flex-1 items-center pt-4 w-1/2 absolute left-0">
|
|
103
|
-
<div className={tw('h-
|
|
103
|
+
<div className={tw('h-0.5 w-full transition-colors', firstConnectorColor)} />
|
|
104
104
|
</div>
|
|
105
105
|
) : null}
|
|
106
106
|
{!isLast ? (
|
|
107
107
|
<div className="flex flex-1 items-center pt-4 w-1/2 absolute right-0">
|
|
108
|
-
<div className={tw('h-
|
|
108
|
+
<div className={tw('h-0.5 w-full transition-colors', lastConnectorColor)} />
|
|
109
109
|
</div>
|
|
110
110
|
) : null}
|
|
111
111
|
<div className="flex flex-col items-center w-full relative">{children}</div>
|
|
@@ -119,12 +119,12 @@ function Step({
|
|
|
119
119
|
<div className={tw('relative flex flex-col min-h-10', className)}>
|
|
120
120
|
{index !== 0 ? (
|
|
121
121
|
<div className="flex flex-1 justify-center pl-4 h-1/2 absolute top-0">
|
|
122
|
-
<div className={tw('w-
|
|
122
|
+
<div className={tw('w-0.5 h-full transition-colors', firstConnectorColor)} />
|
|
123
123
|
</div>
|
|
124
124
|
) : null}
|
|
125
125
|
{!isLast ? (
|
|
126
126
|
<div className="flex flex-1 items-center pl-4 h-1/2 absolute bottom-0">
|
|
127
|
-
<div className={tw('w-
|
|
127
|
+
<div className={tw('w-0.5 h-full transition-colors', lastConnectorColor)} />
|
|
128
128
|
</div>
|
|
129
129
|
) : null}
|
|
130
130
|
<div className="flex flex-row w-full relative">{children}</div>
|
|
@@ -154,11 +154,11 @@ function StepLabel({ children, className }: StepLabelProps) {
|
|
|
154
154
|
return (
|
|
155
155
|
<div className={tw('flex items-center', orientation === 'horizontal' ? 'flex-col text-center' : 'gap-3', className)}>
|
|
156
156
|
<div
|
|
157
|
-
className={tw('flex
|
|
157
|
+
className={tw('flex size-8 shrink-0 items-center justify-center rounded-full border-2 transition-colors', iconBg)}
|
|
158
158
|
aria-current={active ? 'step' : undefined}
|
|
159
159
|
>
|
|
160
160
|
{completed ? (
|
|
161
|
-
<Icon name="check" className="
|
|
161
|
+
<Icon name="check" className="size-4" aria-hidden="true" />
|
|
162
162
|
) : (
|
|
163
163
|
<span className="text-sm font-semibold">{index + 1}</span>
|
|
164
164
|
)}
|