diff --git a/packages/design-system/.ladle/components.css.ts b/packages/design-system/.ladle/components.css.ts index 13babe57..dcf899df 100644 --- a/packages/design-system/.ladle/components.css.ts +++ b/packages/design-system/.ladle/components.css.ts @@ -12,6 +12,8 @@ import { Checkbox, Chip, ComboBox, + DateField, + DateInput, Dialog, Drawer, Group, @@ -30,6 +32,7 @@ import { Tabs, TextArea, TextField, + TimeField, Tooltip, Tree, } from './theme'; @@ -62,6 +65,8 @@ export const theme: ThemeContext = { Checkbox, Chip, ComboBox, + DateField, + DateInput, Dialog, Drawer, Group, @@ -80,6 +85,7 @@ export const theme: ThemeContext = { Tabs, TextArea, TextField, + TimeField, Tooltip, Tree, }; diff --git a/packages/design-system/.ladle/theme/date-field.css.ts b/packages/design-system/.ladle/theme/date-field.css.ts new file mode 100644 index 00000000..cd0de25f --- /dev/null +++ b/packages/design-system/.ladle/theme/date-field.css.ts @@ -0,0 +1,64 @@ +import { style } from '@vanilla-extract/css'; +import { + type ThemeContext, + applyThemeVars, + assignPartialVars, + dateFieldColorVars, + dateFieldSpaceVars, + dateFieldStateVars, + genericColorVars, + semanticColorVars, + sizeVars, +} from '../../src'; +import type { DateFieldState } from '../../src/components/date-field/types'; + +export const DateField: ThemeContext['DateField'] = { + input: style( + applyThemeVars(dateFieldStateVars, [ + { + vars: assignPartialVars(dateFieldSpaceVars, { + gap: sizeVars.v03, + }), + }, + ]), + ), + group: style( + applyThemeVars(dateFieldStateVars, [ + { + vars: assignPartialVars(dateFieldColorVars, { + border: semanticColorVars.border.interactive.default, + }), + }, + ]), + ), + description: style( + applyThemeVars(dateFieldStateVars, [ + { + vars: assignPartialVars(dateFieldColorVars, { + description: { + color: genericColorVars.neutral.v03, + }, + }), + }, + { + query: { isDisabled: true }, + vars: assignPartialVars(dateFieldColorVars, { + description: { + color: semanticColorVars.foreground.interactive.disabled, + }, + }), + }, + ]), + ), + error: style( + applyThemeVars(dateFieldStateVars, [ + { + vars: assignPartialVars(dateFieldColorVars, { + error: { + color: semanticColorVars.border.serious, + }, + }), + }, + ]), + ), +}; diff --git a/packages/design-system/.ladle/theme/date-input.css.ts b/packages/design-system/.ladle/theme/date-input.css.ts new file mode 100644 index 00000000..93c61a9e --- /dev/null +++ b/packages/design-system/.ladle/theme/date-input.css.ts @@ -0,0 +1,75 @@ +import { style } from '@vanilla-extract/css'; +import { + type DateInputState, + type ThemeContext, + applyThemeVars, + assignPartialVars, + dateInputColorVars, + dateInputSpaceVars, + dateInputStateVars, + semanticColorVars, + sizeVars, +} from '../../src'; + +export const DateInput: ThemeContext['DateInput'] = { + input: { + input: style( + applyThemeVars(dateInputStateVars, [ + { + vars: assignPartialVars( + { color: dateInputColorVars, space: dateInputSpaceVars }, + { + color: { + border: semanticColorVars.border.interactive.default, + }, + space: { + input: { + gap: sizeVars.v03, + y: sizeVars.v04, + x: sizeVars.v04, + }, + }, + }, + ), + }, + { + query: { size: 'sm' }, + vars: assignPartialVars(dateInputSpaceVars, { + input: { + maxWidth: '200px', + x: sizeVars.v03, + y: sizeVars.v02, + }, + }), + }, + { + query: { size: 'lg' }, + vars: assignPartialVars(dateInputSpaceVars, { + input: { + maxWidth: '400px', + x: sizeVars.v04, + y: sizeVars.v03, + }, + }), + }, + { + query: { isHovered: true }, + vars: assignPartialVars(dateInputColorVars, { + border: semanticColorVars.border.interactive.hover, + }), + }, + ]), + ), + segments: style( + applyThemeVars(dateInputStateVars, [ + { + vars: assignPartialVars(dateInputSpaceVars, { + segments: { + gap: sizeVars.v03, + }, + }), + }, + ]), + ), + }, +}; diff --git a/packages/design-system/.ladle/theme/index.ts b/packages/design-system/.ladle/theme/index.ts index 2755b935..64d8bc9b 100644 --- a/packages/design-system/.ladle/theme/index.ts +++ b/packages/design-system/.ladle/theme/index.ts @@ -2,6 +2,8 @@ export { Button } from './button.css'; export { Checkbox } from './checkbox.css'; export { Chip } from './chip.css'; export { ComboBox } from './combo-box.css'; +export { DateField } from './date-field.css'; +export { DateInput } from './date-input.css'; export { Dialog } from './dialog.css'; export { Drawer } from './drawer.css'; export { Group } from './group.css'; @@ -20,5 +22,6 @@ export { Switch } from './switch.css'; export { Tabs } from './tabs.css'; export { TextArea } from './textarea.css'; export { TextField } from './text-field.css'; +export { TimeField } from './time-field.css'; export { Tooltip } from './tooltip.css'; export { Tree } from './tree.css'; diff --git a/packages/design-system/.ladle/theme/time-field.css.ts b/packages/design-system/.ladle/theme/time-field.css.ts new file mode 100644 index 00000000..94679e7e --- /dev/null +++ b/packages/design-system/.ladle/theme/time-field.css.ts @@ -0,0 +1,22 @@ +import { style } from '@vanilla-extract/css'; +import { + type ThemeContext, + type TimeFieldState, + applyThemeVars, + assignPartialVars, + sizeVars, + timeFieldSpaceVars, + timeFieldStateVars, +} from '../../src'; + +export const TimeField: ThemeContext['TimeField'] = { + input: style( + applyThemeVars(timeFieldStateVars, [ + { + vars: assignPartialVars(timeFieldSpaceVars, { + gap: sizeVars.v03, + }), + }, + ]), + ), +}; diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 6df71536..384b0c4b 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "@accelint/converters": "workspace:*", + "@internationalized/date": "^3.5.6", "@react-aria/collections": "3.0.0-alpha.5", "@react-aria/ssr": "^3.9.6", "@react-aria/utils": "^3.25.3", diff --git a/packages/design-system/src/components/aria/aria.tsx b/packages/design-system/src/components/aria/aria.tsx index 21f8af08..f8fb1d03 100644 --- a/packages/design-system/src/components/aria/aria.tsx +++ b/packages/design-system/src/components/aria/aria.tsx @@ -1,11 +1,13 @@ import { - createContext, - forwardRef, type ForwardedRef, - type RefAttributes, type ReactNode, + type RefAttributes, + createContext, + forwardRef, } from 'react'; import { + type ContextValue, + DateInput, FieldError, Group, Header, @@ -13,12 +15,11 @@ import { Keyboard, Label, Section, + type SectionProps, SelectValue, + type SelectValueProps, Separator, Text, - type ContextValue, - type SectionProps, - type SelectValueProps, } from 'react-aria-components'; import { useContextProps } from '../../hooks'; @@ -44,6 +45,9 @@ function wrap

( }; } +export const { Component: AriaDateInput, Context: AriaDateInputContext } = + wrap(DateInput); + export const { Component: AriaFieldError, Context: AriaFieldErrorContext } = wrap(FieldError); diff --git a/packages/design-system/src/components/aria/index.ts b/packages/design-system/src/components/aria/index.ts index 5867498a..23103ba6 100644 --- a/packages/design-system/src/components/aria/index.ts +++ b/packages/design-system/src/components/aria/index.ts @@ -1,5 +1,7 @@ // __private-exports export { + AriaDateInput, + AriaDateInputContext, AriaFieldError, AriaFieldErrorContext, AriaGroup, diff --git a/packages/design-system/src/components/checkbox/checkbox.tsx b/packages/design-system/src/components/checkbox/checkbox.tsx index d6e16d01..55ba0da9 100644 --- a/packages/design-system/src/components/checkbox/checkbox.tsx +++ b/packages/design-system/src/components/checkbox/checkbox.tsx @@ -1,17 +1,17 @@ import { + type ForwardedRef, createContext, forwardRef, useCallback, useMemo, - type ForwardedRef, } from 'react'; import { + type ContextValue, + type LabelProps, Provider, Checkbox as RACCheckbox, CheckboxGroup as RACCheckboxGroup, TextContext, - type ContextValue, - type LabelProps, type TextProps, } from 'react-aria-components'; import { @@ -62,7 +62,7 @@ export const Checkbox = forwardRef(function Checkbox( ); const style = useCallback( - ({ ...renderProps }: CheckboxRenderProps) => + (renderProps: CheckboxRenderProps) => inlineVars(checkboxStateVars, { ...renderProps, alignInput, diff --git a/packages/design-system/src/components/date-field/date-field.css.ts b/packages/design-system/src/components/date-field/date-field.css.ts new file mode 100644 index 00000000..6086574e --- /dev/null +++ b/packages/design-system/src/components/date-field/date-field.css.ts @@ -0,0 +1,73 @@ +import { + createContainer, + createThemeContract, + style, +} from '@vanilla-extract/css'; +import { label, layers } from '../../styles'; +import type { DateFieldClassNames } from './types'; + +export const dateFieldContainer = createContainer(); + +export const dateFieldSpaceVars = createThemeContract({ + x: '', + y: '', + gap: '', + minWidth: '', + width: '', + maxWidth: '', +}); + +export const dateFieldColorVars = createThemeContract({ + border: '', + description: { + color: '', + }, + error: { + color: '', + }, +}); + +export const dateFieldStateVars = createThemeContract({ + size: '', + isDisabled: '', + isFocused: '', + isHovered: '', + isInvalid: '', + isReadOnly: '', + isRequired: '', +}); + +export const dateFieldClassNames: DateFieldClassNames = { + container: style({ + '@layer': { + [layers.components.l1]: { + containerName: dateFieldContainer, + }, + }, + }), + dateField: style({ + '@layer': { + [layers.components.l1]: { + width: 'fit-content', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + }, + }, + }), + label: style([label]), + description: style({ + '@layer': { + [layers.components.l1]: { + color: dateFieldColorVars.description.color, + }, + }, + }), + error: style({ + '@layer': { + [layers.components.l1]: { + color: dateFieldColorVars.error.color, + }, + }, + }), +}; diff --git a/packages/design-system/src/components/date-field/date-field.stories.tsx b/packages/design-system/src/components/date-field/date-field.stories.tsx new file mode 100644 index 00000000..a7aae554 --- /dev/null +++ b/packages/design-system/src/components/date-field/date-field.stories.tsx @@ -0,0 +1,235 @@ +import { + type CalendarDateTime, + type CalendarDate as CalendarDateType, + parseDate, + parseDateTime, +} from '@internationalized/date'; +import { type Story, type StoryDefault, action } from '@ladle/react'; +import { I18nProvider } from 'react-aria'; +import type { DateSegmentRenderProps, DateValue } from 'react-aria-components'; +import type { DateSegment as TDateSegment } from 'react-stately'; +import { AriaFieldError, AriaLabel, AriaText } from '../aria'; +import type { DateInputRenderProps } from '../date-input'; +import { DateInput, DateSegment, DateSegments } from '../date-input'; +import { Icon } from '../icon'; +import { DateField } from './date-field'; +import type { DateFieldProps } from './types'; + +type DateFieldStoryProps = DateFieldProps & { + description?: string; + errorMessage?: string; + label?: string; +}; + +export default { + title: 'Components / DateField', + argTypes: { + isDisabled: { + control: { + type: 'boolean', + }, + }, + isReadOnly: { + control: { + type: 'boolean', + }, + }, + description: { + control: { + type: 'text', + }, + defaultValue: 'Format: dd mmm yyyy', + }, + errorMessage: { + control: { + type: 'text', + }, + }, + label: { + control: { + type: 'text', + }, + defaultValue: 'Birth Date', + }, + size: { + control: { + type: 'select', + }, + options: ['sm', 'lg'], + defaultValue: 'lg', + }, + }, +} satisfies StoryDefault; + +const DateIcon = () => ( + + + calender icon + + + + +); + +const months = [ + 'JAN', + 'FEB', + 'MAR', + 'APR', + 'MAY', + 'JUN', + 'JUL', + 'AUG', + 'SEP', + 'OCT', + 'NOV', + 'DEC', +]; + +const MonthDateSegment = (props: DateSegmentRenderProps) => { + const { value, isFocused } = props; + return isFocused ? `${value}`.padStart(2, '0') : months[(value ?? 0) - 1]; +}; + +const FormattedDateSegment = (segment: TDateSegment) => { + if (segment.type === 'literal') { + return <>; + } + if (segment.type === 'month') { + return ( + + {(renderProps) => } + + ); + } + return ; +}; + +export const CalendarDateExample: Story< + DateFieldStoryProps +> = ({ value, label, description, errorMessage, ...rest }) => { + console.log(rest); + return ( + + + {label} + + {/**/} + {/* {(segment: TDateSegment) => }*/} + {/**/} + + + + {(segment: TDateSegment) => } + + + + {description && {description}} + {errorMessage} + + + ); +}; + +CalendarDateExample.storyName = 'Calendar Date'; + +export const ProviderExample: Story> = ({ + value, + label, + description, + errorMessage, + ...rest +}) => ( + + + {label} + + {(renderProps: DateInputRenderProps) => { + console.log({ renderProps }); + return ( + <> + + + {(segment: TDateSegment) => ( + + )} + + + ); + }} + + {description && {description}} + {errorMessage} + + +); + +ProviderExample.storyName = 'Provider'; + +export const CalendarDateTimeExample: Story< + DateFieldStoryProps +> = ({ value, description, errorMessage, ...rest }) => ( + + + {...rest} + defaultValue={parseDateTime('2020-01-23T14:56:26')} + onChange={action('onChange')} + > + Birth Date And Time + + + + {(segment: TDateSegment) => } + + + {description && {description}} + {errorMessage} + + +); + +CalendarDateTimeExample.storyName = 'Calendar Datetime'; + +CalendarDateTimeExample.argTypes = { + hourCycle: { + control: { + type: 'select', + }, + options: [12, 24], + defaultValue: 24, + }, +}; + +export const StandaloneExample = () => { + return ( + + {(segment: TDateSegment) => } + + ); +}; + +StandaloneExample.storyName = 'Date Input Standalone'; diff --git a/packages/design-system/src/components/date-field/date-field.test.tsx b/packages/design-system/src/components/date-field/date-field.test.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/design-system/src/components/date-field/date-field.tsx b/packages/design-system/src/components/date-field/date-field.tsx new file mode 100644 index 00000000..7f4dce79 --- /dev/null +++ b/packages/design-system/src/components/date-field/date-field.tsx @@ -0,0 +1,147 @@ +import { + type ForwardedRef, + createContext, + forwardRef, + useCallback, + useMemo, +} from 'react'; +import { + type ContextValue, + type DateValue, + type FieldErrorProps, + LabelContext, + type LabelProps, + Provider, + DateField as RACDateField, + type TextProps, +} from 'react-aria-components'; +import { useContextProps, useDefaultProps, useTheme } from '../../hooks'; +import { bodies } from '../../styles'; +import { callRenderProps, inlineVars, mergeClassNames } from '../../utils'; +import { + AriaFieldErrorContext, + type AriaLabelContext, + AriaTextContext, +} from '../aria'; +import { DateInputContext, type DateInputProps } from '../date-input'; +import { dateFieldClassNames, dateFieldStateVars } from './date-field.css'; +import type { DateFieldProps, DateFieldRenderProps } from './types'; + +const defaultMapping = { + description: { + sm: bodies.xs, + lg: bodies.xs, + }, + error: { + sm: bodies.xs, + lg: bodies.xs, + }, +}; + +const defaultSize = 'lg'; + +export const DateFieldContext = + createContext, HTMLDivElement>>(null); + +export const DateField = forwardRef(function DateField( + props: DateFieldProps, + ref: ForwardedRef, +) { + [props, ref] = useContextProps(props, ref, DateFieldContext); + props = useDefaultProps( + props as DateFieldProps, + 'DateField', + ) as DateFieldProps; + + const { + children: childrenProp, + classNames: classNamesProp, + mapping: mappingProp, + size = defaultSize, + value, + ...rest + } = props; + + const theme = useTheme(); + + const mapping = useMemo( + () => ({ + ...defaultMapping, + ...mappingProp, + }), + [mappingProp], + ); + + const classNames = useMemo( + () => + mergeClassNames(dateFieldClassNames, theme.DateField, classNamesProp, { + description: mapping.description[size], + error: mapping.error[size], + }), + [theme.DateField, classNamesProp, mapping, size], + ); + + const style = useCallback( + (renderProps: DateFieldRenderProps) => + inlineVars(dateFieldStateVars, { + ...renderProps, + size, + }), + [size], + ); + + const values = useMemo< + [ + [typeof DateInputContext, ContextValue], + [typeof AriaLabelContext, ContextValue], + [typeof AriaTextContext, ContextValue], + [ + typeof AriaFieldErrorContext, + ContextValue, + ], + ] + >( + () => [ + [DateInputContext, { classNames: classNames?.input, size }], + [LabelContext, { className: classNames?.label }], + [ + AriaTextContext, + { + slots: { + description: { className: classNames?.description }, + }, + }, + ], + [AriaFieldErrorContext, { className: classNames?.error }], + ], + [classNames], + ); + + const children = useCallback( + (renderProps: DateFieldRenderProps) => { + return ( + +

+ {callRenderProps(childrenProp, { + ...renderProps, + defaultChildren: null, + })} +
+ + ); + }, + [childrenProp, values, classNames?.dateField], + ); + + return ( + + {children} + + ); +}); diff --git a/packages/design-system/src/components/date-field/index.ts b/packages/design-system/src/components/date-field/index.ts new file mode 100644 index 00000000..993d09fd --- /dev/null +++ b/packages/design-system/src/components/date-field/index.ts @@ -0,0 +1,17 @@ +// __private-exports +export { DateField, DateFieldContext } from './date-field'; +export type { + DateFieldRenderProps, + DateFieldClassNames, + DateFieldProps, + DateFieldMapping, + DateFieldSizes, + DateFieldState, +} from './types'; +export { + dateFieldContainer, + dateFieldSpaceVars, + dateFieldColorVars, + dateFieldStateVars, + dateFieldClassNames, +} from './date-field.css'; diff --git a/packages/design-system/src/components/date-field/types.ts b/packages/design-system/src/components/date-field/types.ts new file mode 100644 index 00000000..dab066ab --- /dev/null +++ b/packages/design-system/src/components/date-field/types.ts @@ -0,0 +1,41 @@ +import type { + DateValue, + DateFieldProps as RACDateFieldProps, + DateFieldRenderProps as RACDateFieldRenderProps, +} from 'react-aria-components'; +import type { PartialDeep } from 'type-fest'; +import type { AsType } from '../../types'; +import type { DateInputClassNames } from '../date-input/types'; + +export type DateFieldClassNames = PartialDeep<{ + container: string; + dateField: string; + input: DateInputClassNames; + description: string; + error: string; + label: string; +}>; + +export type DateFieldSizes = 'sm' | 'lg'; + +export type DateFieldMapping = { + description: Partial>; + error: Partial>; +}; + +type BaseDateFieldProps = { + classNames?: DateFieldClassNames; + mapping?: Partial; + size?: DateFieldSizes; +}; + +export type DateFieldRenderProps = AsType; + +export type DateFieldProps = Omit< + RACDateFieldProps, + 'className' | 'style' +> & + BaseDateFieldProps; + +export type DateFieldState = Omit & + Required>; diff --git a/packages/design-system/src/components/date-input/date-input.css.ts b/packages/design-system/src/components/date-input/date-input.css.ts new file mode 100644 index 00000000..d1e1dbb8 --- /dev/null +++ b/packages/design-system/src/components/date-input/date-input.css.ts @@ -0,0 +1,100 @@ +import { + createContainer, + createThemeContract, + fallbackVar, + style, +} from '@vanilla-extract/css'; +import { layers, radiusVars } from '../../styles'; +import { containerQueries } from '../../utils'; +import type { DateInputClassNames, DateInputState } from './types'; + +export const dateInputContainer = createContainer(); +export const dateSegmentsContainer = createContainer(); + +export const dateInputStateVars = createThemeContract({ + size: '', + isHovered: '', + isFocusWithin: '', + isFocusVisible: '', + isDisabled: '', + isInvalid: '', +}); + +export const dateSegmentStateVars = createThemeContract({ + isHovered: '', + isFocusWithin: '', + isFocusVisible: '', + isDisabled: '', + isInvalid: '', +}); + +export const dateInputSpaceVars = createThemeContract({ + input: { + x: '', + y: '', + gap: '', + minWidth: '', + width: '', + maxWidth: '', + }, + segments: { + gap: '', + }, +}); + +export const dateInputColorVars = createThemeContract({ + border: '', + description: { + color: '', + }, + error: { + color: '', + }, +}); + +export const dateInputClassNames: DateInputClassNames = { + input: { + container: style({ + '@layer': { + [layers.components.l1]: { + containerName: dateInputContainer, + }, + }, + }), + input: style({ + '@layer': { + [layers.components.l1]: { + display: 'flex', + gap: dateInputSpaceVars.input.gap, + padding: `${fallbackVar(dateInputSpaceVars.input.y, '0')} ${fallbackVar(dateInputSpaceVars.input.x, '0')}`, + border: `1px solid ${fallbackVar(dateInputColorVars.border, 'transparent')}`, + borderRadius: radiusVars.sm, + minWidth: fallbackVar(dateInputSpaceVars.input.minWidth, 'auto'), + width: fallbackVar(dateInputSpaceVars.input.width, 'fit-content'), + maxWidth: fallbackVar(dateInputSpaceVars.input.maxWidth, '100%'), + '@container': containerQueries(dateInputStateVars, { + query: { isDisabled: true }, + cursor: 'not-allowed', + }), + }, + }, + }), + segments: style({ + '@layer': { + [layers.components.l1]: { + display: 'flex', + gap: dateInputSpaceVars.segments.gap, + }, + }, + }), + }, + segment: { + container: style({ + '@layer': { + [layers.components.l1]: { + containerName: dateSegmentsContainer, + }, + }, + }), + }, +}; diff --git a/packages/design-system/src/components/date-input/date-input.tsx b/packages/design-system/src/components/date-input/date-input.tsx new file mode 100644 index 00000000..4057e97f --- /dev/null +++ b/packages/design-system/src/components/date-input/date-input.tsx @@ -0,0 +1,258 @@ +import { createCalendar } from '@internationalized/date'; +import { + type ForwardedRef, + Fragment, + cloneElement, + createContext, + forwardRef, + useCallback, + useContext, + useMemo, + useRef, +} from 'react'; +import { useDateField } from 'react-aria'; +import { + type ContextValue, + DateFieldStateContext, + Group, + Provider, + DateSegment as RACDateSegment, + type SlotProps, + TimeFieldStateContext, + useLocale, +} from 'react-aria-components'; +import { useDateFieldState } from 'react-stately'; +import { useContextProps, useDefaultProps, useTheme } from '../../hooks'; +import { inputs } from '../../styles'; +import { callRenderProps, inlineVars, mergeClassNames } from '../../utils'; +import { AriaGroupContext } from '../aria'; +import type { DateFieldProps } from '../date-field'; +import { DateFieldContext } from '../date-field/date-field'; +import { Input } from '../input'; +import { + dateInputClassNames, + dateInputStateVars, + dateSegmentStateVars, +} from './date-input.css'; +import type { + DateInputProps, + DateInputRenderProps, + DateSegmentProps, + DateSegmentRenderProps, + DateSegmentsProps, +} from './types'; + +const defaultMapping = { + input: { + sm: inputs.sm, + lg: inputs.lg, + }, +}; + +const defaultSize = 'lg'; + +export const DateInputContext = + createContext>(null); + +export const DateInput = forwardRef(function DateInput( + props: DateInputProps, + ref: ForwardedRef, +) { + [props, ref] = useContextProps(props, ref, DateInputContext); + props = useDefaultProps(props, 'DateInput'); + + const dateFieldState = useContext(DateFieldStateContext); + const timeFieldState = useContext(TimeFieldStateContext); + + return dateFieldState || timeFieldState ? ( + + ) : ( + + ); +}); + +const DateInputInner = forwardRef( + (props: DateInputProps, ref: ForwardedRef) => { + const { + children: childrenProp, + classNames: classNamesProp, + mapping: mappingProp, + size = defaultSize, + provider, + ...rest + } = props; + + const dateFieldState = useContext(DateFieldStateContext); + const timeFieldState = useContext(TimeFieldStateContext); + const state = dateFieldState ?? timeFieldState ?? null; + + const theme = useTheme(); + + const mapping = useMemo( + () => ({ + ...defaultMapping, + ...mappingProp, + }), + [mappingProp], + ); + + const classNames = useMemo( + () => + mergeClassNames(dateInputClassNames, theme.DateInput, classNamesProp, { + input: { input: mapping.input[size] }, + }), + [theme.DateInput, classNamesProp, mapping, size], + ); + + const style = useCallback( + (renderProps: DateInputRenderProps) => + inlineVars(dateInputStateVars, { ...renderProps, size }), + [size], + ); + + // TODO: clone element here is really gross + const children = useCallback( + (renderProps: DateInputRenderProps) => ( +
+ {childrenProp && + (provider ? ( + callRenderProps(childrenProp, { ...renderProps, ...state }) + ) : ( + <> + {state.segments.map((segment, i) => ( + {childrenProp(segment)} + ))} + + ))} +
+ ), + [childrenProp, state, provider, classNames?.input], + ); + + return ( + <> + + {children} + + + + ); + }, +); + +// TODO: slot context? what slots? +// TODO: this is copy pasta from react-aria and needs work +const DateInputStandalone = forwardRef( + (props: DateInputProps, ref: ForwardedRef) => { + const [dateFieldProps, fieldRef] = useContextProps( + { slot: props.slot } as DateFieldProps, + ref, + DateFieldContext, + ); + const { locale } = useLocale(); + const state = useDateFieldState({ + ...dateFieldProps, + locale, + createCalendar, + }); + + const inputRef = useRef(null); + const { fieldProps } = useDateField( + { ...dateFieldProps, inputRef }, + state, + fieldRef, + ); + + return ( + + + + ); + }, +); + +export const DateSegments = forwardRef( + (props: DateSegmentsProps, ref: ForwardedRef) => { + const { children, classNames: classNamesProp } = props; + + const dateFieldState = useContext(DateFieldStateContext); + const timeFieldState = useContext(TimeFieldStateContext); + const state = dateFieldState ?? timeFieldState ?? null; + + const theme = useTheme(); + + const classNames = useMemo( + () => + mergeClassNames(dateInputClassNames, theme.DateInput, classNamesProp), + [theme.DateInput, classNamesProp], + ); + + return ( +
+ {state.segments.map((segment, i) => ( + {children(segment)} + ))} +
+ ); + }, +); + +export const DateSegmentContext = + createContext>(null); + +export const DateSegment = forwardRef(function DateSegment( + props: DateSegmentProps, + ref: ForwardedRef, +) { + [props, ref] = useContextProps(props, ref, DateSegmentContext); + + const { classNames: classNamesProp, children: childrenProp, ...rest } = props; + + const classNames = useMemo( + () => mergeClassNames(dateInputClassNames, classNamesProp), + [classNamesProp], + ); + + const style = useCallback( + (renderProps: DateSegmentRenderProps) => + inlineVars(dateSegmentStateVars, { + ...renderProps, + }), + [], + ); + + // const children = useCallback( + // (renderProps: DateSegmentRenderProps) => { + // return ( + //
+ // {callRenderProps(childrenProp, { + // ...renderProps, + // defaultChildren: null, + // })} + //
+ // ); + // }, + // [childrenProp, classNames?.segment], + // ); + + return ( + + {childrenProp} + + ); +}); diff --git a/packages/design-system/src/components/date-input/index.ts b/packages/design-system/src/components/date-input/index.ts new file mode 100644 index 00000000..2f252999 --- /dev/null +++ b/packages/design-system/src/components/date-input/index.ts @@ -0,0 +1,28 @@ +// __private-exports +export { + DateInput, + DateSegmentContext, + DateSegments, + DateSegment, + DateInputContext, +} from './date-input'; +export { + dateInputSpaceVars, + dateInputClassNames, + dateInputStateVars, + dateInputColorVars, + dateInputContainer, + dateSegmentsContainer, + dateSegmentStateVars, +} from './date-input.css'; +export type { + DateInputProps, + DateInputClassNames, + DateInputRenderProps, + DateInputMapping, + DateSegmentProps, + DateSegmentRenderProps, + DateInputState, + DateInputSizes, + DateSegmentsProps, +} from './types'; diff --git a/packages/design-system/src/components/date-input/types.ts b/packages/design-system/src/components/date-input/types.ts new file mode 100644 index 00000000..4712fe38 --- /dev/null +++ b/packages/design-system/src/components/date-input/types.ts @@ -0,0 +1,57 @@ +import type { ReactElement } from 'react'; +import type { + DateInputProps as RACDateInputProps, + DateInputRenderProps as RACDateInputRenderProps, + DateSegmentProps as RACDateSegmentProps, + DateSegmentRenderProps as RACDateSegmentRenderProps, +} from 'react-aria-components'; +import type { DateSegment as TDateSegment } from 'react-stately'; +import type { PartialDeep } from 'type-fest'; +import type { AsType, RenderPropsChildren } from '../../types'; + +export type DateInputSizes = 'sm' | 'lg'; + +export type DateInputMapping = { + input: Partial>; +}; + +type BaseDateInputProps = { + classNames?: DateInputClassNames; + mapping?: DateInputMapping; + size?: DateInputSizes; +}; + +export type DateInputClassNames = PartialDeep<{ + input: { + container: string; + input: string; + segments: string; + }; + segment: { + container: string; + segment: string; + }; +}>; + +export type DateInputProps = Omit< + RACDateInputProps, + 'className' | 'style' | 'children' +> & + ( + | { provider?: false; children?: (segment: TDateSegment) => ReactElement } + | { provider: true; children?: RenderPropsChildren } + ) & + BaseDateInputProps; + +export type DateInputRenderProps = AsType; + +export type DateInputState = DateInputRenderProps & + Required>; + +export type DateSegmentsProps = { + children: any; //TODO +} & BaseDateInputProps; + +export type DateSegmentProps = AsType & BaseDateInputProps; + +export type DateSegmentRenderProps = RACDateSegmentRenderProps; diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts index 0f45fb8f..2a3c2383 100644 --- a/packages/design-system/src/components/index.ts +++ b/packages/design-system/src/components/index.ts @@ -101,6 +101,43 @@ export { type ComboBoxSizes, type ComboBoxState, } from './combo-box'; +export { + DateField, + type DateFieldRenderProps, + type DateFieldClassNames, + type DateFieldProps, + type DateFieldMapping, + type DateFieldSizes, + type DateFieldState, + dateFieldContainer, + dateFieldSpaceVars, + dateFieldColorVars, + dateFieldStateVars, + dateFieldClassNames, +} from './date-field'; +export { + DateInput, + DateSegmentContext, + DateSegments, + DateSegment, + DateInputContext, + type DateInputProps, + type DateInputClassNames, + type DateInputRenderProps, + type DateInputMapping, + type DateSegmentProps, + type DateSegmentRenderProps, + type DateInputState, + type DateInputSizes, + type DateSegmentsProps, + dateInputSpaceVars, + dateInputClassNames, + dateInputStateVars, + dateInputColorVars, + dateInputContainer, + dateSegmentsContainer, + dateSegmentStateVars, +} from './date-input'; export { Dialog, DialogContext, @@ -410,6 +447,21 @@ export { type TextFieldSizes, type TextFieldState, } from './text-field'; +export { + TimeField, + TimeFieldContext, + type TimeFieldRenderProps, + type TimeFieldClassNames, + type TimeFieldProps, + type TimeFieldState, + type TimeFieldSizes, + type TimeFieldMapping, + timeFieldStateVars, + timeFieldColorVars, + timeFieldContainer, + timeFieldSpaceVars, + timeFieldClassNames, +} from './time-field'; export { Tooltip, TooltipContext, diff --git a/packages/design-system/src/components/number-field/number-field.tsx b/packages/design-system/src/components/number-field/number-field.tsx index 58835354..b71e5209 100644 --- a/packages/design-system/src/components/number-field/number-field.tsx +++ b/packages/design-system/src/components/number-field/number-field.tsx @@ -54,6 +54,8 @@ const defaultMapping: NumberFieldMapping = { }, }; +const defaultSize = 'lg'; + export const NumberFieldContext = createContext>(null); @@ -69,7 +71,7 @@ export const NumberField = forwardRef(function NumberField( children: childrenProp, classNames: classNamesProp, mapping: mappingProp, - size = 'lg', + size = defaultSize, ...rest } = props; diff --git a/packages/design-system/src/components/text-field/text-field.tsx b/packages/design-system/src/components/text-field/text-field.tsx index fc289a2b..5e328822 100644 --- a/packages/design-system/src/components/text-field/text-field.tsx +++ b/packages/design-system/src/components/text-field/text-field.tsx @@ -39,6 +39,8 @@ const defaultMapping: TextFieldMapping = { }, }; +const defaultSize = 'lg'; + export const TextFieldContext = createContext>(null); @@ -54,7 +56,7 @@ export const TextField = forwardRef(function TextField( children: childrenProp, classNames: classNamesProp, mapping: mappingProp, - size = 'lg', + size = defaultSize, ...rest } = props; diff --git a/packages/design-system/src/components/time-field/index.ts b/packages/design-system/src/components/time-field/index.ts new file mode 100644 index 00000000..90f00e0a --- /dev/null +++ b/packages/design-system/src/components/time-field/index.ts @@ -0,0 +1,17 @@ +// __private-exports +export { TimeField, TimeFieldContext } from './time-field'; +export type { + TimeFieldRenderProps, + TimeFieldClassNames, + TimeFieldProps, + TimeFieldState, + TimeFieldSizes, + TimeFieldMapping, +} from './types'; +export { + timeFieldStateVars, + timeFieldColorVars, + timeFieldContainer, + timeFieldSpaceVars, + timeFieldClassNames, +} from './time-field.css'; diff --git a/packages/design-system/src/components/time-field/time-field.css.ts b/packages/design-system/src/components/time-field/time-field.css.ts new file mode 100644 index 00000000..35e989d5 --- /dev/null +++ b/packages/design-system/src/components/time-field/time-field.css.ts @@ -0,0 +1,68 @@ +import { + createContainer, + createThemeContract, + style, +} from '@vanilla-extract/css'; +import { label, layers } from '../../styles'; +import { dateFieldColorVars } from '../date-field'; +import type { TimeFieldClassNames } from './types'; + +export const timeFieldContainer = createContainer(); + +export const timeFieldSpaceVars = createThemeContract({ + x: '', + y: '', + gap: '', + minWidth: '', + width: '', + maxWidth: '', +}); + +export const timeFieldColorVars = createThemeContract({ + border: '', +}); + +export const timeFieldStateVars = createThemeContract({ + size: '', + isDisabled: '', + isFocused: '', + isHovered: '', + isInvalid: '', + isReadOnly: '', + isRequired: '', +}); + +export const timeFieldClassNames: TimeFieldClassNames = { + container: style({ + '@layer': { + [layers.components.l1]: { + containerName: timeFieldContainer, + }, + }, + }), + timeField: style({ + '@layer': { + [layers.components.l1]: { + width: 'fit-content', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + }, + }, + }), + label: style([label]), + description: style({ + '@layer': { + [layers.components.l1]: { + color: dateFieldColorVars.description.color, + }, + }, + }), + error: style({ + '@layer': { + [layers.components.l1]: { + color: dateFieldColorVars.error.color, + }, + }, + }), +}; diff --git a/packages/design-system/src/components/time-field/time-field.stories.tsx b/packages/design-system/src/components/time-field/time-field.stories.tsx new file mode 100644 index 00000000..80d9bcd2 --- /dev/null +++ b/packages/design-system/src/components/time-field/time-field.stories.tsx @@ -0,0 +1,108 @@ +import { Time, type Time as TimeType } from '@internationalized/date'; +import type { Story } from '@ladle/react'; +import type { TimeValue } from 'react-aria'; +import { DateSegment } from 'react-aria-components'; +import type { DateSegment as TDateSegment } from 'react-stately'; +import { AriaFieldError, AriaLabel, AriaText } from '../aria'; +import { DateInput, DateSegments } from '../date-input/date-input'; +import { Icon } from '../icon'; +import { TimeField } from './time-field'; +import type { TimeFieldProps } from './types'; + +type TimeFieldStoryProps = TimeFieldProps & { + description?: string; + errorMessage?: string; + label?: string; +}; + +export default { + title: 'Components / Time', + argTypes: { + isDisabled: { + control: { + type: 'boolean', + }, + }, + isReadOnly: { + control: { + type: 'boolean', + }, + }, + description: { + control: { + type: 'text', + }, + defaultValue: 'Format: dd mmm yyyy', + }, + errorMessage: { + control: { + type: 'text', + }, + }, + label: { + control: { + type: 'text', + }, + defaultValue: 'Birth Date', + }, + size: { + control: { + type: 'select', + }, + options: ['sm', 'lg'], + defaultValue: 'sm', + }, + }, +}; + +const TimeIcon = () => ( + + + tempus fugit + + + + +); + +export const BasicExample: Story> = ({ + description, + errorMessage, + ...rest +}) => ( + + Time of Occurrence + + + + {(segment: TDateSegment) => { + return segment.type === 'literal' ? ( + <> + ) : ( + + ); + }} + + + {description && {description}} + {errorMessage} + +); + +BasicExample.storyName = 'Basic Example'; diff --git a/packages/design-system/src/components/time-field/time-field.test.tsx b/packages/design-system/src/components/time-field/time-field.test.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/design-system/src/components/time-field/time-field.tsx b/packages/design-system/src/components/time-field/time-field.tsx new file mode 100644 index 00000000..680b7aa2 --- /dev/null +++ b/packages/design-system/src/components/time-field/time-field.tsx @@ -0,0 +1,147 @@ +import { + type ForwardedRef, + createContext, + forwardRef, + useCallback, + useMemo, +} from 'react'; +import { + type ContextValue, + type FieldErrorProps, + LabelContext, + type LabelProps, + Provider, + TimeField as RACTimeField, + type TextProps, +} from 'react-aria-components'; +import { callRenderProps, inlineVars, mergeClassNames } from '../../utils'; +import { + AriaFieldErrorContext, + type AriaLabelContext, + AriaTextContext, +} from '../aria'; +import type { TimeFieldProps, TimeFieldRenderProps } from './types'; + +import type { TimeValue } from 'react-aria'; +import { useContextProps, useDefaultProps, useTheme } from '../../hooks'; +import { bodies } from '../../styles'; +import type { DateFieldRenderProps } from '../date-field'; +import { DateInputContext } from '../date-input/date-input'; +import type { DateInputProps } from '../date-input/types'; +import { timeFieldClassNames, timeFieldStateVars } from './time-field.css'; + +const defaultMapping = { + description: { + sm: bodies.xs, + lg: bodies.xs, + }, + error: { + sm: bodies.xs, + lg: bodies.xs, + }, +}; + +const defaultSize = 'lg'; + +export const TimeFieldContext = + createContext, HTMLDivElement>>(null); + +export const TimeField = forwardRef(function TimeField( + props: TimeFieldProps, + ref: ForwardedRef, +) { + [props, ref] = useContextProps(props, ref, TimeFieldContext); + props = useDefaultProps(props, 'TimeField'); // TODO + + const { + children: childrenProp, + classNames: classNamesProp, + mapping: mappingProp, + size = defaultSize, + value, + ...rest + } = props; + + const theme = useTheme(); + + const mapping = useMemo( + () => ({ + ...defaultMapping, + ...mappingProp, + }), + [mappingProp], + ); + + const classNames = useMemo( + () => + mergeClassNames(timeFieldClassNames, theme.TimeField, classNamesProp, { + description: mapping.description[size], + error: mapping.error[size], + }), + [theme.TimeField, classNamesProp, mapping, size], + ); + + const style = useCallback( + (renderProps: DateFieldRenderProps) => + inlineVars(timeFieldStateVars, { + ...renderProps, + size, + }), + [size], + ); + + const values = useMemo< + [ + [typeof DateInputContext, ContextValue], + [typeof AriaLabelContext, ContextValue], + [typeof AriaTextContext, ContextValue], + [ + typeof AriaFieldErrorContext, + ContextValue, + ], + ] + >( + () => [ + [DateInputContext, { classNames: classNames?.dateInput }], + [LabelContext, { className: classNames?.label }], + [ + AriaTextContext, + { + slots: { + description: { className: classNames?.description }, + }, + }, + ], + [AriaFieldErrorContext, { className: classNames?.error }], + ], + [classNames], + ); + + const children = useCallback( + (renderProps: TimeFieldRenderProps) => { + return ( + +
+ {callRenderProps(childrenProp, { + ...renderProps, + defaultChildren: null, + })} +
+
+ ); + }, + [childrenProp, values, classNames], + ); + + return ( + + {children} + + ); +}); diff --git a/packages/design-system/src/components/time-field/types.ts b/packages/design-system/src/components/time-field/types.ts new file mode 100644 index 00000000..5991beb4 --- /dev/null +++ b/packages/design-system/src/components/time-field/types.ts @@ -0,0 +1,42 @@ +import type { + TimeFieldProps as RACTimeFieldProps, + DateFieldRenderProps as RACTimeFieldRenderProps, + TimeValue, +} from 'react-aria-components'; +import type { PartialDeep } from 'type-fest'; +import type { AsType } from '../../types'; +import type { DateFieldSizes } from '../date-field/types'; +import type { DateInputClassNames } from '../date-input/types'; + +export type TimeFieldClassNames = PartialDeep<{ + container: string; + dateInput: DateInputClassNames; + description: string; + error: string; + label: string; + timeField: string; +}>; + +export type TimeFieldSizes = 'sm' | 'lg'; + +export type TimeFieldMapping = { + description: Partial>; + error: Partial>; +}; + +type BaseTimeFieldProps = { + classNames?: TimeFieldClassNames; + mapping?: Partial; + size?: TimeFieldSizes; +}; + +export type TimeFieldRenderProps = AsType; + +export type TimeFieldProps = Omit< + RACTimeFieldProps, + 'className' | 'style' +> & + BaseTimeFieldProps; + +export type TimeFieldState = Omit & + Required>; diff --git a/packages/design-system/src/hooks/use-defaults/types.ts b/packages/design-system/src/hooks/use-defaults/types.ts index 5f133bac..087c70d3 100644 --- a/packages/design-system/src/hooks/use-defaults/types.ts +++ b/packages/design-system/src/hooks/use-defaults/types.ts @@ -1,4 +1,7 @@ import type { PropsWithChildren } from 'react'; +import type { TimeValue } from 'react-aria'; +import type { DateValue } from 'react-aria-components'; +import type { DateFieldProps, TimeFieldProps } from '../../components'; import type { ButtonProps, LinkButtonProps, @@ -10,6 +13,7 @@ import type { } from '../../components/checkbox/types'; import type { ChipGroupProps, ChipProps } from '../../components/chip/types'; import type { ComboBoxProps } from '../../components/combo-box/types'; +import type { DateInputProps } from '../../components/date-input/types'; import type { DialogProps } from '../../components/dialog/types'; import type { DrawerProps, @@ -63,6 +67,8 @@ export type DefaultsContext = DefaultsOf<{ Chip: ChipProps; ChipGroup: ChipGroupProps; ComboBox: ComboBoxProps; + DateField: DateFieldProps; + DateInput: DateInputProps; Dialog: DialogProps; Drawer: DrawerProps; DrawerTab: DrawerTabProps; @@ -91,6 +97,7 @@ export type DefaultsContext = DefaultsOf<{ Tabs: TabsProps; TextArea: TextAreaProps; TextField: TextFieldProps; + TimeField: TimeFieldProps; ToggleButton: ToggleButtonProps; Tooltip: TooltipProps; TooltipTarget: TooltipTargetProps; diff --git a/packages/design-system/src/hooks/use-theme/types.ts b/packages/design-system/src/hooks/use-theme/types.ts index 9737d5fa..9cd8f5cc 100644 --- a/packages/design-system/src/hooks/use-theme/types.ts +++ b/packages/design-system/src/hooks/use-theme/types.ts @@ -3,6 +3,8 @@ import type { ButtonClassNames } from '../../components/button/types'; import type { CheckboxClassNames } from '../../components/checkbox/types'; import type { ChipClassNames } from '../../components/chip/types'; import type { ComboBoxClassNames } from '../../components/combo-box/types'; +import type { DateFieldClassNames } from '../../components/date-field/types'; +import type { DateInputClassNames } from '../../components/date-input/types'; import type { DialogClassNames } from '../../components/dialog/types'; import type { DrawerClassNames } from '../../components/drawer/types'; import type { GroupClassNames } from '../../components/group/types'; @@ -21,6 +23,7 @@ import type { SwitchClassNames } from '../../components/switch/types'; import type { TabsClassNames } from '../../components/tabs/types'; import type { TextFieldClassNames } from '../../components/text-field/types'; import type { TextAreaClassNames } from '../../components/textarea/types'; +import type { TimeFieldClassNames } from '../../components/time-field'; import type { TooltipClassNames } from '../../components/tooltip/types'; import type { TreeClassNames } from '../../components/tree/types'; @@ -40,6 +43,8 @@ export type ThemeContext = { Checkbox?: CheckboxClassNames; Chip?: ChipClassNames; ComboBox?: ComboBoxClassNames; + DateField?: DateFieldClassNames; + DateInput?: DateInputClassNames; Dialog?: DialogClassNames; Drawer?: DrawerClassNames; Group?: GroupClassNames; @@ -58,6 +63,7 @@ export type ThemeContext = { Tabs?: TabsClassNames; TextArea?: TextAreaClassNames; TextField?: TextFieldClassNames; + TimeField?: TimeFieldClassNames; Tooltip?: TooltipClassNames; Tree?: TreeClassNames; }; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index 5ce23a0d..b3c84538 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -37,6 +37,12 @@ export { ChipList, ComboBox, ComboBoxContext, + DateField, + DateInput, + DateInputContext, + DateSegment, + DateSegmentContext, + DateSegments, Dialog, DialogContext, Drawer, @@ -100,6 +106,8 @@ export { TextAreaContext, TextField, TextFieldContext, + TimeField, + TimeFieldContext, ToggleButton, ToggleButtonContext, Tooltip, @@ -132,6 +140,18 @@ export { comboBoxSpaceVars, comboBoxStateVars, createCollectionRenderer, + dateFieldClassNames, + dateFieldColorVars, + dateFieldContainer, + dateFieldSpaceVars, + dateFieldStateVars, + dateInputClassNames, + dateInputColorVars, + dateInputContainer, + dateInputSpaceVars, + dateInputStateVars, + dateSegmentStateVars, + dateSegmentsContainer, dialogClassNames, dialogColorVars, dialogContainer, @@ -229,6 +249,11 @@ export { textFieldContainer, textFieldSpaceVars, textFieldStateVars, + timeFieldClassNames, + timeFieldColorVars, + timeFieldContainer, + timeFieldSpaceVars, + timeFieldStateVars, tooltipClassNames, tooltipContainers, tooltipSpaceVars, @@ -275,6 +300,21 @@ export type { ComboBoxRenderProps, ComboBoxSizes, ComboBoxState, + DateFieldClassNames, + DateFieldMapping, + DateFieldProps, + DateFieldRenderProps, + DateFieldSizes, + DateFieldState, + DateInputClassNames, + DateInputMapping, + DateInputProps, + DateInputRenderProps, + DateInputSizes, + DateInputState, + DateSegmentProps, + DateSegmentRenderProps, + DateSegmentsProps, DialogClassNames, DialogMapping, DialogProps, @@ -394,6 +434,12 @@ export type { TextFieldProps, TextFieldSizes, TextFieldState, + TimeFieldClassNames, + TimeFieldMapping, + TimeFieldProps, + TimeFieldRenderProps, + TimeFieldSizes, + TimeFieldState, ToggleButtonProps, TooltipClassNames, TooltipMapping, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc10fd16..15672141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: '@accelint/converters': specifier: workspace:* version: link:../converters + '@internationalized/date': + specifier: ^3.5.6 + version: 3.5.6 '@react-aria/collections': specifier: 3.0.0-alpha.5 version: 3.0.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)