@maestro-js/components 1.0.0-alpha.0
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 +41 -0
- package/commands/index.ts +7 -0
- package/commands/list.ts +9 -0
- package/dist/components.json +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +48 -0
- package/package.json +49 -0
- package/registry.json +1445 -0
- package/scripts/build.ts +44 -0
- package/src/components/alert-dialog.tsx +150 -0
- package/src/components/autocomplete.tsx +288 -0
- package/src/components/autosuggest.tsx +314 -0
- package/src/components/avatar.tsx +54 -0
- package/src/components/boolean-select.tsx +48 -0
- package/src/components/button-group.tsx +75 -0
- package/src/components/button-link.tsx +71 -0
- package/src/components/button.tsx +132 -0
- package/src/components/checkbox-group.tsx +172 -0
- package/src/components/checkbox.tsx +207 -0
- package/src/components/chip.tsx +158 -0
- package/src/components/container.tsx +82 -0
- package/src/components/currency-field.tsx +183 -0
- package/src/components/date-input.tsx +189 -0
- package/src/components/date-picker.tsx +211 -0
- package/src/components/date-range-picker.tsx +290 -0
- package/src/components/date-time-picker.tsx +196 -0
- package/src/components/dialog.tsx +97 -0
- package/src/components/disclosure.tsx +114 -0
- package/src/components/drawer.tsx +78 -0
- package/src/components/enum-chip.tsx +30 -0
- package/src/components/file-input.tsx +245 -0
- package/src/components/form.tsx +82 -0
- package/src/components/headless-file-input.tsx +362 -0
- package/src/components/helpers/animated-popover.tsx +24 -0
- package/src/components/helpers/button-context.ts +10 -0
- package/src/components/helpers/calendar-month-year-picker.tsx +280 -0
- package/src/components/helpers/form-field.tsx +229 -0
- package/src/components/helpers/get-button-classes.ts +138 -0
- package/src/components/helpers/headless-button.tsx +36 -0
- package/src/components/helpers/pdf-dist.client.ts +6 -0
- package/src/components/icon.tsx +26 -0
- package/src/components/image-input.tsx +265 -0
- package/src/components/img.tsx +46 -0
- package/src/components/inline-alert.tsx +54 -0
- package/src/components/labeled-value.tsx +480 -0
- package/src/components/link-tabs.tsx +118 -0
- package/src/components/menu.tsx +152 -0
- package/src/components/month-day-input.tsx +176 -0
- package/src/components/multi-file-input.tsx +244 -0
- package/src/components/multi-image-input.tsx +389 -0
- package/src/components/multicomplete.tsx +322 -0
- package/src/components/multiselect.tsx +325 -0
- package/src/components/multisuggest.tsx +357 -0
- package/src/components/number-field.tsx +143 -0
- package/src/components/numeric-tag-field.tsx +271 -0
- package/src/components/pdf-input.tsx +249 -0
- package/src/components/pdf.tsx +86 -0
- package/src/components/percentage-field.tsx +187 -0
- package/src/components/phone-number-field.tsx +166 -0
- package/src/components/radio-group.tsx +112 -0
- package/src/components/radio.tsx +91 -0
- package/src/components/select.tsx +215 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +181 -0
- package/src/components/switch.tsx +186 -0
- package/src/components/tabs.tsx +151 -0
- package/src/components/tag-field.tsx +250 -0
- package/src/components/text-area.tsx +148 -0
- package/src/components/text-field.tsx +144 -0
- package/src/components/time-input.tsx +198 -0
- package/src/components/toast.tsx +176 -0
- package/src/components/toggle-button-group.tsx +94 -0
- package/src/components/year-month-input.tsx +187 -0
- package/src/utils/colors.ts +44 -0
- package/src/utils/compose-refs.ts +32 -0
- package/src/utils/enum-colors.ts +1 -0
- package/src/utils/file-input.ts +49 -0
- package/src/utils/icons.d.ts +20 -0
- package/src/utils/tw.ts +13 -0
- package/src/utils/use-element-size.ts +35 -0
- package/src/utils/use-number-input.ts +143 -0
- package/src/utils/use-pagination.ts +38 -0
- package/src/utils/use-prevent-default.ts +27 -0
- package/src/utils/use-render-props.ts +39 -0
- package/src/utils/use-spin-delay.ts +24 -0
- package/src/utils/use-stable-accessor.ts +11 -0
- package/src/utils/use-tab-indicator.ts +106 -0
- package/tests/commands.test.ts +81 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Collection,
|
|
5
|
+
Group,
|
|
6
|
+
Header,
|
|
7
|
+
Input,
|
|
8
|
+
ListBox,
|
|
9
|
+
ListBoxItem,
|
|
10
|
+
ListBoxSection,
|
|
11
|
+
type InputProps
|
|
12
|
+
} from 'react-aria-components'
|
|
13
|
+
import { composeTailwindRenderProps, tw } from '../../utils/tw'
|
|
14
|
+
import { Icon } from '../icon'
|
|
15
|
+
|
|
16
|
+
const colorClasses = { neutral: 'text-neutral-500', negative: 'text-negative-600', notice: 'text-notice-600' } as const
|
|
17
|
+
|
|
18
|
+
type LabelProps = {
|
|
19
|
+
as?: React.ElementType<React.ComponentPropsWithRef<'label'>>
|
|
20
|
+
} & React.ComponentPropsWithRef<'label'>
|
|
21
|
+
|
|
22
|
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ as = 'label', ...props }, ref) => {
|
|
23
|
+
return React.createElement(as, {
|
|
24
|
+
...props,
|
|
25
|
+
ref,
|
|
26
|
+
className: tw('block text-sm font-medium text-neutral-700', props.className)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
Label.displayName = 'Label'
|
|
30
|
+
|
|
31
|
+
function FormFieldHelpText(props: {
|
|
32
|
+
id: string
|
|
33
|
+
isInvalid: boolean
|
|
34
|
+
validationMessage: React.ReactNode
|
|
35
|
+
hasWarning: boolean
|
|
36
|
+
warningMessage?: React.ReactNode
|
|
37
|
+
helpText?: React.ReactNode
|
|
38
|
+
UNSAFE_className?: string
|
|
39
|
+
}) {
|
|
40
|
+
const text = props.isInvalid ? props.validationMessage : props.hasWarning ? props.warningMessage : props.helpText
|
|
41
|
+
const color = props.isInvalid ? 'negative' : props.hasWarning ? 'notice' : 'neutral'
|
|
42
|
+
return text === null ? null : (
|
|
43
|
+
<div
|
|
44
|
+
id={props.id}
|
|
45
|
+
className={tw('mt-1 text-sm', !text && 'h-5', colorClasses[color], 'text-wrap', props.UNSAFE_className)}
|
|
46
|
+
>
|
|
47
|
+
{text ?? ''}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function LabelRow(
|
|
53
|
+
props: { label?: React.ReactNode; cornerHint?: React.ReactNode } & ({ labelId: string } | { htmlFor: string })
|
|
54
|
+
): React.JSX.Element | null {
|
|
55
|
+
if (!props.label) return null
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex justify-between items-center mb-1">
|
|
58
|
+
{'labelId' in props ? (
|
|
59
|
+
<Label id={props.labelId}>{props.label}</Label>
|
|
60
|
+
) : (
|
|
61
|
+
<Label htmlFor={props.htmlFor}>{props.label}</Label>
|
|
62
|
+
)}
|
|
63
|
+
{props.cornerHint && <div className="text-sm text-neutral-500">{props.cornerHint}</div>}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type FieldGroupProps = {
|
|
69
|
+
isDisabled?: boolean
|
|
70
|
+
isReadonly?: boolean
|
|
71
|
+
isInvalid?: boolean
|
|
72
|
+
hasWarning?: boolean
|
|
73
|
+
shape?: 'rectangle' | 'oval'
|
|
74
|
+
className?: string
|
|
75
|
+
children: React.ReactNode
|
|
76
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(
|
|
80
|
+
({ children, className, onClick, ...opts }, ref) => {
|
|
81
|
+
const disabled = opts.isDisabled || opts.isReadonly
|
|
82
|
+
const shape = opts.shape === 'oval' ? 'rounded-full' : 'rounded'
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Group
|
|
86
|
+
ref={ref}
|
|
87
|
+
className={tw(
|
|
88
|
+
'relative overflow-hidden border transition-all duration-200 max-w-96',
|
|
89
|
+
'focus-within:ring-1',
|
|
90
|
+
shape,
|
|
91
|
+
disabled && 'border-neutral-100 bg-neutral-50 text-neutral-400 cursor-not-allowed',
|
|
92
|
+
opts.isInvalid && 'border-negative-200 bg-negative-50 focus-within:border-negative-500 focus-within:ring-negative-500/20',
|
|
93
|
+
opts.hasWarning &&
|
|
94
|
+
'border-notice-200 bg-notice-50 focus-within:border-notice-500 focus-within:ring-notice-500/20',
|
|
95
|
+
!opts.isInvalid &&
|
|
96
|
+
!opts.hasWarning &&
|
|
97
|
+
!disabled &&
|
|
98
|
+
'border-neutral-200 hover:border-neutral-300 focus-within:border-primary-500 focus-within:ring-primary-500/20',
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
onClick={onClick}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</Group>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const FieldInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
|
110
|
+
return (
|
|
111
|
+
<Input
|
|
112
|
+
{...props}
|
|
113
|
+
ref={ref}
|
|
114
|
+
className={composeTailwindRenderProps(
|
|
115
|
+
props.className,
|
|
116
|
+
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',
|
|
118
|
+
(props.disabled || props.readOnly) && 'cursor-not-allowed'
|
|
119
|
+
)
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
function DropdownListBox<T extends object>(props: React.ComponentProps<typeof ListBox<T>>) {
|
|
126
|
+
return (
|
|
127
|
+
<ListBox<T>
|
|
128
|
+
{...props}
|
|
129
|
+
className={composeTailwindRenderProps(
|
|
130
|
+
props.className,
|
|
131
|
+
'overflow-hidden rounded bg-white border border-neutral-200 shadow-lg text-sm w-full focus:outline-none max-h-60 overflow-y-auto'
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function DropdownItem(props: {
|
|
138
|
+
id: string
|
|
139
|
+
textValue: string
|
|
140
|
+
label: React.ReactNode
|
|
141
|
+
secondary?: React.ReactNode
|
|
142
|
+
isDisabled?: boolean
|
|
143
|
+
color?: string | null
|
|
144
|
+
showCheckIcon?: boolean
|
|
145
|
+
customValuePrefix?: string
|
|
146
|
+
className?: string
|
|
147
|
+
}) {
|
|
148
|
+
const isCustom = Boolean(props.customValuePrefix)
|
|
149
|
+
const showCheck = props.showCheckIcon !== false && !isCustom
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<ListBoxItem
|
|
153
|
+
id={props.id}
|
|
154
|
+
textValue={props.textValue}
|
|
155
|
+
isDisabled={props.isDisabled}
|
|
156
|
+
className={({ isFocused, isSelected }) =>
|
|
157
|
+
tw(
|
|
158
|
+
isFocused ? 'bg-neutral-50 text-neutral-900' : 'text-neutral-900 bg-white',
|
|
159
|
+
'relative cursor-pointer text-sm transition-colors duration-150',
|
|
160
|
+
'pr-2 pl-3 py-1.5 outline-none',
|
|
161
|
+
props.isDisabled && 'opacity-50 cursor-not-allowed',
|
|
162
|
+
isSelected ? (isFocused ? 'bg-primary-100' : 'bg-primary-50') : '',
|
|
163
|
+
isCustom && 'italic',
|
|
164
|
+
props.className
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{({ isSelected }) => (
|
|
169
|
+
<>
|
|
170
|
+
{props.color && !isCustom ? (
|
|
171
|
+
<div className="absolute left-0 top-0 w-[3px] h-full" style={{ backgroundColor: props.color }} />
|
|
172
|
+
) : null}
|
|
173
|
+
<div className="flex items-center justify-between w-full">
|
|
174
|
+
<div className="flex-1 min-w-0">
|
|
175
|
+
<div className={tw(showCheck && isSelected ? 'font-medium' : 'font-normal', 'truncate')}>
|
|
176
|
+
{isCustom ? `${props.customValuePrefix} "${props.label}"` : props.label}
|
|
177
|
+
</div>
|
|
178
|
+
{props.secondary && !isCustom && (
|
|
179
|
+
<div className="text-xs mt-px truncate text-neutral-500">{props.secondary}</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
{showCheck && isSelected && (
|
|
183
|
+
<div className="ml-2 w-fit shrink-0 flex items-center text-primary-500">
|
|
184
|
+
<Icon name="check" className="h-4 w-4" />
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</ListBoxItem>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function DropdownSection<T extends object>(props: {
|
|
195
|
+
title?: string
|
|
196
|
+
items?: Iterable<T>
|
|
197
|
+
children: React.ReactNode | ((item: T) => React.ReactNode)
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<ListBoxSection>
|
|
201
|
+
{props.title && (
|
|
202
|
+
<Header className="sticky top-0 z-10 bg-white px-3 py-1.5 text-xs font-semibold text-neutral-500 border-b border-neutral-100">
|
|
203
|
+
{props.title}
|
|
204
|
+
</Header>
|
|
205
|
+
)}
|
|
206
|
+
<Collection items={props.items}>{props.children}</Collection>
|
|
207
|
+
</ListBoxSection>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function ChevronButton(props?: { className?: string }) {
|
|
212
|
+
return (
|
|
213
|
+
<Button className={tw('flex items-center px-2 py-2', props?.className)}>
|
|
214
|
+
<Icon name="chevron-up-down" className="h-4 w-4 text-neutral-400 transition-colors" />
|
|
215
|
+
</Button>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const FormFieldComponents = {
|
|
220
|
+
Label,
|
|
221
|
+
FormFieldHelpText,
|
|
222
|
+
LabelRow,
|
|
223
|
+
FieldGroup,
|
|
224
|
+
FieldInput,
|
|
225
|
+
DropdownListBox,
|
|
226
|
+
DropdownSection,
|
|
227
|
+
DropdownItem,
|
|
228
|
+
ChevronButton
|
|
229
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { tw } from '../../utils/tw'
|
|
2
|
+
|
|
3
|
+
export type ButtonVariant = 'contained' | 'outlined' | 'soft' | 'plain'
|
|
4
|
+
export type ButtonColor = 'primary' | 'negative' | 'positive' | 'notice' | 'neutral'
|
|
5
|
+
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
6
|
+
export type ButtonShape = 'rectangle' | 'circle' | 'oval'
|
|
7
|
+
|
|
8
|
+
const base =
|
|
9
|
+
'relative inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-all cursor-default select-none [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0 data-[disabled]:cursor-not-allowed'
|
|
10
|
+
|
|
11
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
12
|
+
xs: 'h-7 min-w-14 px-2.5 text-xs',
|
|
13
|
+
sm: 'h-8 min-w-16 px-3 text-sm',
|
|
14
|
+
md: 'h-9 min-w-20 px-4 text-sm',
|
|
15
|
+
lg: 'h-10 min-w-24 px-5 text-base',
|
|
16
|
+
xl: 'h-12 min-w-28 px-6 text-base'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const shapeClasses: Record<ButtonShape, string> = {
|
|
20
|
+
rectangle: 'rounded',
|
|
21
|
+
circle: 'rounded-full aspect-square px-0',
|
|
22
|
+
oval: 'rounded-full'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const variantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
26
|
+
contained: {
|
|
27
|
+
primary:
|
|
28
|
+
'bg-primary-600 text-white ring-1 ring-inset ring-primary-700 data-[hovered]:bg-primary-700 data-[hovered]:ring-primary-800 data-[pressed]:bg-primary-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',
|
|
29
|
+
negative:
|
|
30
|
+
'bg-negative-600 text-white ring-1 ring-inset ring-negative-700 data-[hovered]:bg-negative-700 data-[hovered]:ring-negative-800 data-[pressed]:bg-negative-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',
|
|
31
|
+
positive:
|
|
32
|
+
'bg-positive-600 text-white ring-1 ring-inset ring-positive-700 data-[hovered]:bg-positive-700 data-[hovered]:ring-positive-800 data-[pressed]:bg-positive-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',
|
|
33
|
+
notice:
|
|
34
|
+
'bg-notice-600 text-white ring-1 ring-inset ring-notice-700 data-[hovered]:bg-notice-700 data-[hovered]:ring-notice-800 data-[pressed]:bg-notice-800 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none',
|
|
35
|
+
neutral:
|
|
36
|
+
'bg-neutral-800 text-white ring-1 ring-inset ring-neutral-900 data-[hovered]:bg-neutral-900 data-[pressed]:bg-neutral-950 data-[disabled]:bg-neutral-200 data-[disabled]:text-neutral-400 data-[disabled]:ring-transparent data-[disabled]:shadow-none'
|
|
37
|
+
},
|
|
38
|
+
outlined: {
|
|
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-50 data-[disabled]:text-neutral-400 data-[disabled]:ring-neutral-200 data-[disabled]:shadow-none',
|
|
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-50 data-[disabled]:text-neutral-400 data-[disabled]:ring-neutral-200 data-[disabled]:shadow-none',
|
|
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-50 data-[disabled]:text-neutral-400 data-[disabled]:ring-neutral-200 data-[disabled]:shadow-none',
|
|
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-50 data-[disabled]:text-neutral-400 data-[disabled]:ring-neutral-200 data-[disabled]:shadow-none',
|
|
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-50 data-[disabled]:text-neutral-400 data-[disabled]:ring-neutral-200 data-[disabled]:shadow-none'
|
|
49
|
+
},
|
|
50
|
+
soft: {
|
|
51
|
+
primary:
|
|
52
|
+
'bg-primary-100 text-primary-800 data-[hovered]:bg-primary-200 data-[pressed]:bg-primary-300 data-[disabled]:bg-neutral-50 data-[disabled]:text-neutral-400',
|
|
53
|
+
negative:
|
|
54
|
+
'bg-negative-100 text-negative-800 data-[hovered]:bg-negative-200 data-[pressed]:bg-negative-300 data-[disabled]:bg-neutral-50 data-[disabled]:text-neutral-400',
|
|
55
|
+
positive:
|
|
56
|
+
'bg-positive-100 text-positive-800 data-[hovered]:bg-positive-200 data-[pressed]:bg-positive-300 data-[disabled]:bg-neutral-50 data-[disabled]:text-neutral-400',
|
|
57
|
+
notice:
|
|
58
|
+
'bg-notice-100 text-notice-800 data-[hovered]:bg-notice-200 data-[pressed]:bg-notice-300 data-[disabled]:bg-neutral-50 data-[disabled]:text-neutral-400',
|
|
59
|
+
neutral:
|
|
60
|
+
'bg-neutral-200 text-neutral-800 data-[hovered]:bg-neutral-300 data-[pressed]:bg-neutral-400 data-[disabled]:bg-neutral-50 data-[disabled]:text-neutral-400'
|
|
61
|
+
},
|
|
62
|
+
plain: {
|
|
63
|
+
primary:
|
|
64
|
+
'bg-transparent text-primary-700 data-[hovered]:bg-primary-50 data-[pressed]:bg-primary-100 data-[disabled]:bg-transparent data-[disabled]:text-neutral-400',
|
|
65
|
+
negative:
|
|
66
|
+
'bg-transparent text-negative-700 data-[hovered]:bg-negative-50 data-[pressed]:bg-negative-100 data-[disabled]:bg-transparent data-[disabled]:text-neutral-400',
|
|
67
|
+
positive:
|
|
68
|
+
'bg-transparent text-positive-700 data-[hovered]:bg-positive-50 data-[pressed]:bg-positive-100 data-[disabled]:bg-transparent data-[disabled]:text-neutral-400',
|
|
69
|
+
notice:
|
|
70
|
+
'bg-transparent text-notice-700 data-[hovered]:bg-notice-50 data-[pressed]:bg-notice-100 data-[disabled]:bg-transparent data-[disabled]:text-neutral-400',
|
|
71
|
+
neutral:
|
|
72
|
+
'bg-transparent text-neutral-700 data-[hovered]:bg-neutral-50 data-[pressed]:bg-neutral-100 data-[disabled]:bg-transparent data-[disabled]:text-neutral-400'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const toggledVariantColorClasses: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
77
|
+
contained: {
|
|
78
|
+
primary: 'bg-primary-800 ring-primary-900',
|
|
79
|
+
negative: 'bg-negative-800 ring-negative-900',
|
|
80
|
+
positive: 'bg-positive-800 ring-positive-900',
|
|
81
|
+
notice: 'bg-notice-800 ring-notice-900',
|
|
82
|
+
neutral: 'bg-neutral-950'
|
|
83
|
+
},
|
|
84
|
+
outlined: {
|
|
85
|
+
primary: 'bg-primary-100 ring-primary-500',
|
|
86
|
+
negative: 'bg-negative-100 ring-negative-500',
|
|
87
|
+
positive: 'bg-positive-100 ring-positive-500',
|
|
88
|
+
notice: 'bg-notice-100 ring-notice-500',
|
|
89
|
+
neutral: 'bg-neutral-100 ring-neutral-500'
|
|
90
|
+
},
|
|
91
|
+
soft: {
|
|
92
|
+
primary: 'bg-primary-200',
|
|
93
|
+
negative: 'bg-negative-200',
|
|
94
|
+
positive: 'bg-positive-200',
|
|
95
|
+
notice: 'bg-notice-200',
|
|
96
|
+
neutral: 'bg-neutral-300'
|
|
97
|
+
},
|
|
98
|
+
plain: {
|
|
99
|
+
primary: 'bg-primary-100',
|
|
100
|
+
negative: 'bg-negative-100',
|
|
101
|
+
positive: 'bg-positive-100',
|
|
102
|
+
notice: 'bg-notice-100',
|
|
103
|
+
neutral: 'bg-neutral-100'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const focusClasses =
|
|
108
|
+
'data-[focus-visible]:outline-2 data-[focus-visible]:outline-offset-2 data-[focus-visible]:outline-primary-600'
|
|
109
|
+
|
|
110
|
+
export function getButtonClasses({
|
|
111
|
+
variant = 'contained',
|
|
112
|
+
color = 'primary',
|
|
113
|
+
size = 'md',
|
|
114
|
+
shape = 'oval',
|
|
115
|
+
showSpinner,
|
|
116
|
+
isToggled,
|
|
117
|
+
className
|
|
118
|
+
}: {
|
|
119
|
+
variant?: ButtonVariant
|
|
120
|
+
color?: ButtonColor
|
|
121
|
+
size?: ButtonSize
|
|
122
|
+
shape?: ButtonShape
|
|
123
|
+
showSpinner?: boolean
|
|
124
|
+
isToggled?: boolean
|
|
125
|
+
className?: string
|
|
126
|
+
} = {}) {
|
|
127
|
+
return tw(
|
|
128
|
+
base,
|
|
129
|
+
'data-[pressed]:scale-[.98]',
|
|
130
|
+
showSpinner ? '[&>:not([data-loader])]:opacity-0 !text-transparent' : '',
|
|
131
|
+
sizeClasses[size],
|
|
132
|
+
shapeClasses[shape],
|
|
133
|
+
variantColorClasses[variant][color],
|
|
134
|
+
isToggled && toggledVariantColorClasses[variant][color],
|
|
135
|
+
focusClasses,
|
|
136
|
+
className
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { composeRefs } from '../../utils/compose-refs'
|
|
2
|
+
import { type WithRenderProps, useRenderProps } from '../../utils/use-render-props'
|
|
3
|
+
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { usePress } from '@react-aria/interactions'
|
|
6
|
+
import { mergeProps } from '@react-aria/utils'
|
|
7
|
+
|
|
8
|
+
export const HeadlessButton = React.forwardRef<
|
|
9
|
+
HTMLButtonElement,
|
|
10
|
+
WithRenderProps<React.ComponentProps<'button'>, { isPressed: boolean }>
|
|
11
|
+
>((rawProps, externalRef) => {
|
|
12
|
+
const myRef = React.useRef<HTMLButtonElement>(null)
|
|
13
|
+
const { isPressed, pressProps } = usePress({ ref: myRef, isDisabled: rawProps.disabled })
|
|
14
|
+
const renderProps = React.useMemo(
|
|
15
|
+
() => ({
|
|
16
|
+
isPressed
|
|
17
|
+
}),
|
|
18
|
+
[isPressed]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const props = useRenderProps(mergeProps(pressProps, rawProps), renderProps, ['children', 'style', 'className'])
|
|
22
|
+
const ref = React.useMemo(() => composeRefs(myRef, externalRef), [myRef, externalRef])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
{...props}
|
|
28
|
+
{...(props.disabled || props['data-disabled'] ? { ['data-disabled']: true } : {})}
|
|
29
|
+
{...((isPressed && !props.disabled) || props['data-pressed'] ? { ['data-pressed']: true } : {})}
|
|
30
|
+
aria-pressed={isPressed && !props.disabled}
|
|
31
|
+
ref={ref}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
HeadlessButton.displayName = 'HeadlessButton'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import href from '../icons/sprite.svg'
|
|
4
|
+
import type { IconName } from '~/icons/types'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Props
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface IconProps extends React.ComponentProps<'svg'> {
|
|
11
|
+
name: IconName
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Component
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const Icon = React.forwardRef<SVGSVGElement, IconProps>(({ className, name, ...props }, ref) => {
|
|
19
|
+
return (
|
|
20
|
+
<svg ref={ref} viewBox="0 0 24 24" className={className} {...props}>
|
|
21
|
+
<use href={`${href}#${name}`} />
|
|
22
|
+
</svg>
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
Icon.displayName = 'Icon'
|