feat(switch): component created

This commit is contained in:
Junior Garcia 2023-03-14 21:40:22 -03:00
parent 71752ad04b
commit a3ca1e8ed8
30 changed files with 1094 additions and 126 deletions

View File

@ -33,27 +33,17 @@ export default {
},
} as ComponentMeta<typeof AvatarGroup>;
const pics = [
"https://i.pravatar.cc/300?u=a042581f4e29026705d",
"https://i.pravatar.cc/300?u=a042581f4e29026706d",
"https://i.pravatar.cc/300?u=a042581f4e29026707d",
"https://i.pravatar.cc/300?u=a042581f4e29026709d",
"https://i.pravatar.cc/300?u=a042581f4f29026709d",
"https://i.pravatar.cc/300?u=a042581f4e29026710d",
"https://i.pravatar.cc/300?u=a042581f4e29026711d",
];
const Template: ComponentStory<typeof AvatarGroup> = (args: AvatarGroupProps) => (
<AvatarGroup {...args}>
<Avatar src={pics[0]} />
<Avatar src={pics[1]} />
<Avatar src={pics[2]} />
<Avatar src={pics[3]} />
<Avatar src={pics[4]} />
<Avatar src={pics[5]} />
<Avatar src={pics[2]} />
<Avatar src={pics[3]} />
<Avatar src={pics[6]} />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026705d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026706d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026707d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026709d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4f29026709d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026710d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026711d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026712d" />
<Avatar src="https://i.pravatar.cc/300?u=a042581f4e29026713d" />
</AvatarGroup>
);

View File

@ -2,7 +2,7 @@ import type {CheckboxVariantProps, CheckboxSlots, SlotsToClasses} from "@nextui-
import type {AriaCheckboxProps} from "@react-types/checkbox";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {ReactNode, Ref, useCallback} from "react";
import {ReactNode, Ref, useCallback, useId} from "react";
import {useMemo, useRef} from "react";
import {useToggleState} from "@react-stately/toggle";
import {checkbox} from "@nextui-org/theme";
@ -79,6 +79,7 @@ export function useCheckbox(props: UseCheckboxProps) {
name,
isRequired = false,
isReadOnly = false,
autoFocus = false,
isSelected: isSelectedProp,
size = groupContext?.size ?? "md",
color = groupContext?.color ?? "primary",
@ -115,11 +116,19 @@ export function useCheckbox(props: UseCheckboxProps) {
const inputRef = useRef(null);
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
const labelId = useId();
const inputId = useId();
const ariaCheckboxProps = useMemo(() => {
const ariaLabel =
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
return {
id: inputId,
name,
value,
children,
autoFocus,
defaultSelected,
isSelected: isSelectedProp,
isDisabled,
@ -127,14 +136,17 @@ export function useCheckbox(props: UseCheckboxProps) {
isRequired,
isReadOnly,
validationState,
"aria-label": otherProps["aria-label"],
"aria-labelledby": otherProps["aria-labelledby"],
"aria-label": ariaLabel,
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
onChange,
};
}, [
value,
name,
inputId,
labelId,
children,
autoFocus,
isIndeterminate,
isDisabled,
isSelectedProp,
@ -223,6 +235,8 @@ export function useCheckbox(props: UseCheckboxProps) {
const getLabelProps: PropGetter = useCallback(
() => ({
id: labelId,
htmlFor: inputProps.id || inputId,
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
"data-invalid": dataAttr(isInvalid),

View File

@ -58,6 +58,24 @@ const defaultProps: CheckboxProps = {
const Template: ComponentStory<typeof Checkbox> = (args: CheckboxProps) => <Checkbox {...args} />;
const ControlledTemplate: ComponentStory<typeof Checkbox> = (args: CheckboxProps) => {
const [selected, setSelected] = React.useState<boolean>(true);
React.useEffect(() => {
// eslint-disable-next-line no-console
console.log("Checkbox ", selected);
}, [selected]);
return (
<div className="flex flex-col gap-2">
<Checkbox isSelected={selected} onChange={setSelected} {...args}>
Subscribe (controlled)
</Checkbox>
<p className="text-neutral-500">Selected: {selected ? "true" : "false"}</p>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
...defaultProps,
@ -111,21 +129,9 @@ DisableAnimation.args = {
disableAnimation: true,
};
export const Controlled = () => {
const [selected, setSelected] = React.useState<boolean>(true);
React.useEffect(() => {
// eslint-disable-next-line no-console
console.log("Checkbox ", selected);
}, [selected]);
return (
<div className="flex flex-row gap-2">
<Checkbox isSelected={selected} onChange={setSelected} {...checkbox.defaultVariants}>
Subscribe (controlled)
</Checkbox>
</div>
);
export const Controlled = ControlledTemplate.bind({});
Controlled.args = {
...defaultProps,
};
interface CustomCheckboxProps extends CheckboxProps {

View File

@ -1,7 +1,7 @@
import type {AriaRadioProps} from "@react-types/radio";
import type {RadioVariantProps, RadioSlots, SlotsToClasses} from "@nextui-org/theme";
import {Ref, ReactNode, useCallback} from "react";
import {Ref, ReactNode, useCallback, useId} from "react";
import {useMemo, useRef} from "react";
import {useFocusRing} from "@react-aria/focus";
import {useHover} from "@react-aria/interactions";
@ -85,6 +85,11 @@ export function useRadio(props: UseRadioProps) {
const domRef = useDOMRef(ref);
const inputRef = useRef<HTMLInputElement>(null);
const labelId = useId();
const inputGenId = useId();
const inputId = id || inputGenId;
const isDisabled = useMemo(() => !!isDisabledProp, [isDisabledProp]);
const isRequired = useMemo(() => groupContext.isRequired ?? false, [groupContext.isRequired]);
const isInvalid = useMemo(
@ -101,17 +106,17 @@ export function useRadio(props: UseRadioProps) {
: undefined;
return {
id: inputId,
isDisabled,
isRequired,
"aria-label": ariaLabel,
"aria-labelledby": otherProps["aria-labelledby"] || ariaLabel,
"aria-describedby": otherProps["aria-describedby"] || ariaDescribedBy,
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
"aria-describedby": ariaDescribedBy,
};
}, [isDisabled, isRequired]);
}, [labelId, inputId, isDisabled, isRequired]);
const {inputProps} = useReactAriaRadio(
{
id,
value,
children,
...groupContext,
@ -190,6 +195,8 @@ export function useRadio(props: UseRadioProps) {
const getLabelProps: PropGetter = useCallback(
(props = {}) => ({
...props,
id: labelId,
htmlFor: inputId,
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
"data-invalid": dataAttr(isInvalid),

View File

@ -107,6 +107,27 @@ const Template: ComponentStory<typeof RadioGroup> = (args: RadioGroupProps) => {
);
};
const ControlledTemplate: ComponentStory<typeof RadioGroup> = (args: RadioGroupProps) => {
const [selectedItem, setSelectedItem] = React.useState<string>("london");
React.useEffect(() => {
// eslint-disable-next-line no-console
console.log("isSelected:", selectedItem);
}, [selectedItem]);
return (
<div className="flex flex-col gap-2">
<RadioGroup label="Select city" value={selectedItem} onChange={setSelectedItem} {...args}>
<Radio value="buenos-aires">Buenos Aires</Radio>
<Radio value="sydney">Sydney</Radio>
<Radio value="london">London</Radio>
<Radio value="tokyo">Tokyo</Radio>
</RadioGroup>
<p className="text-neutral-500">Selected: {selectedItem}</p>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
...defaultProps,
@ -156,22 +177,9 @@ DisableAnimation.args = {
disableAnimation: true,
};
export const Controlled = () => {
const [isSelected, setIsSelected] = React.useState<string>("london");
React.useEffect(() => {
// eslint-disable-next-line no-console
console.log("isSelected:", isSelected);
}, [isSelected]);
return (
<RadioGroup label="Select city" value={isSelected} onChange={setIsSelected}>
<Radio value="buenos-aires">Buenos Aires</Radio>
<Radio value="sydney">Sydney</Radio>
<Radio value="london">London</Radio>
<Radio value="tokyo">Tokyo</Radio>
</RadioGroup>
);
export const Controlled = ControlledTemplate.bind({});
Controlled.args = {
...defaultProps,
};
const CustomRadio = (props: RadioProps) => {

View File

@ -1,11 +1,11 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {act, render} from "@testing-library/react";
import {Switch} from "../src";
describe("Switch", () => {
it("should render correctly", () => {
const wrapper = render(<Switch />);
const wrapper = render(<Switch aria-label="switch" />);
expect(() => wrapper.unmount()).not.toThrow();
});
@ -13,7 +13,188 @@ describe("Switch", () => {
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Switch ref={ref} />);
render(<Switch ref={ref} aria-label="switch" />);
expect(ref.current).not.toBeNull();
});
it("should check and uncheck", () => {
const {getByRole} = render(<Switch aria-label="switch" />);
const checkbox = getByRole("switch");
expect(checkbox).not.toBeChecked();
act(() => {
checkbox.click();
});
expect(checkbox).toBeChecked();
act(() => {
checkbox.click();
});
expect(checkbox).not.toBeChecked();
});
it("should not check if disabled", () => {
const {getByRole} = render(<Switch isDisabled aria-label="switch" />);
const checkbox = getByRole("switch");
act(() => {
checkbox.click();
});
expect(checkbox).not.toBeChecked();
});
it("should be checked if defaultSelected", () => {
const {getByRole} = render(<Switch defaultSelected aria-label="switch" />);
const checkbox = getByRole("switch");
expect(checkbox).toBeChecked();
});
it("should not check if readOnly", () => {
const {getByRole} = render(<Switch isReadOnly aria-label="switch" />);
const checkbox = getByRole("switch");
act(() => {
checkbox.click();
});
expect(checkbox).not.toBeChecked();
});
it("should check and uncheck with controlled state", () => {
const ControlledSwitch = ({onChange}: any) => {
const [isSelected, setIsSelected] = React.useState(false);
return (
<Switch
aria-label="switch"
isSelected={isSelected}
onChange={(selected) => {
onChange?.(selected);
setIsSelected(selected);
}}
/>
);
};
const onChange = jest.fn();
const {getByRole} = render(<ControlledSwitch onChange={onChange} />);
const checkbox = getByRole("switch");
expect(checkbox).not.toBeChecked();
act(() => {
checkbox.click();
});
expect(checkbox).toBeChecked();
expect(onChange).toHaveBeenCalledWith(true);
});
it("should render the thumbIcon", () => {
const wrapper = render(
<Switch aria-label="switch" thumbIcon={<svg data-testid="thumb-icon" />} />,
);
expect(wrapper.getByTestId("thumb-icon")).toBeInTheDocument();
});
it('should work with thumbIcon as "function"', () => {
const thumbIcon = jest.fn(() => <svg data-testid="thumb-icon" />);
const wrapper = render(<Switch aria-label="switch" thumbIcon={thumbIcon} />);
expect(thumbIcon).toHaveBeenCalled();
expect(wrapper.getByTestId("thumb-icon")).toBeInTheDocument();
});
it("should change the thumbIcon when clicked", () => {
const thumbIcon = jest.fn((props) => {
const {isSelected} = props;
return isSelected ? (
<svg data-testid="checked-thumb-icon" />
) : (
<svg data-testid="unchecked-thumb-icon" />
);
});
const {getByRole, container} = render(<Switch aria-label="switch" thumbIcon={thumbIcon} />);
const checkbox = getByRole("switch");
expect(checkbox).not.toBeChecked();
act(() => {
checkbox.click();
});
expect(checkbox).toBeChecked();
expect(thumbIcon).toHaveBeenCalledWith(
expect.objectContaining({
isSelected: true,
}),
);
const checkedThumbIcon = container.querySelector("[data-testid=checked-thumb-icon]");
expect(checkedThumbIcon).toBeInTheDocument();
act(() => {
checkbox.click();
});
expect(checkbox).not.toBeChecked();
expect(thumbIcon).toHaveBeenCalledWith(
expect.objectContaining({
isSelected: false,
}),
);
const uncheckedThumbIcon = container.querySelector("[data-testid=unchecked-thumb-icon]");
expect(uncheckedThumbIcon).toBeInTheDocument();
});
it('should work with "leftIcon"', () => {
const wrapper = render(
<Switch aria-label="switch" leftIcon={<svg data-testid="left-icon" />} />,
);
expect(wrapper.getByTestId("left-icon")).toBeInTheDocument();
});
it('should work with "rightIcon"', () => {
const wrapper = render(
<Switch aria-label="switch" rightIcon={<svg data-testid="right-icon" />} />,
);
expect(wrapper.getByTestId("right-icon")).toBeInTheDocument();
});
it('should work with "leftIcon" and "rightIcon"', () => {
const wrapper = render(
<Switch
aria-label="switch"
leftIcon={<svg data-testid="left-icon" />}
rightIcon={<svg data-testid="right-icon" />}
/>,
);
expect(wrapper.getByTestId("left-icon")).toBeInTheDocument();
expect(wrapper.getByTestId("right-icon")).toBeInTheDocument();
});
});

View File

@ -37,14 +37,22 @@
"react": ">=18"
},
"dependencies": {
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/dom-utils": "workspace:*"
"@react-aria/focus": "^3.11.0",
"@react-aria/interactions": "^3.14.0",
"@react-aria/switch": "^3.4.0",
"@react-aria/utils": "^3.15.0",
"@react-aria/visually-hidden": "^3.7.0",
"@react-stately/toggle": "^3.5.0",
"@react-types/shared": "^3.17.0"
},
"devDependencies": {
"clean-package": "2.2.0",
"react": "^18.0.0"
"react": "^18.0.0",
"@nextui-org/shared-icons": "workspace:*"
},
"tsup": {
"clean": true,

View File

@ -2,6 +2,7 @@ import Switch from "./switch";
// export types
export type {SwitchProps} from "./switch";
export type {SwitchThumbIconProps} from "./use-switch";
// export hooks
export {useSwitch} from "./use-switch";

View File

@ -1,22 +1,51 @@
import {forwardRef} from "@nextui-org/system";
import {__DEV__} from "@nextui-org/shared-utils";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {cloneElement, ReactElement} from "react";
import {UseSwitchProps, useSwitch} from "./use-switch";
export interface SwitchProps extends Omit<UseSwitchProps, "ref"> {}
const Switch = forwardRef<SwitchProps, "div">((props, ref) => {
const {Component, domRef, children, styles, ...otherProps} = useSwitch({ref, ...props});
const {
Component,
children,
leftIcon,
rightIcon,
thumbIcon,
getBaseProps,
getInputProps,
getWrapperProps,
getThumbProps,
getThumbIconProps,
getLabelProps,
getLeftIconProps,
getRightIconProps,
} = useSwitch({ref, ...props});
const clonedThumbIcon =
typeof thumbIcon === "function"
? thumbIcon(getThumbIconProps({includeStateProps: true}))
: thumbIcon && cloneElement(thumbIcon as ReactElement, getThumbIconProps());
const clonedLeftIcon = leftIcon && cloneElement(leftIcon as ReactElement, getLeftIconProps());
const clonedRightIcon = rightIcon && cloneElement(rightIcon as ReactElement, getRightIconProps());
return (
<Component ref={domRef} className={styles} {...otherProps}>
{children}
<Component {...getBaseProps()}>
<VisuallyHidden>
<input {...getInputProps()} />
</VisuallyHidden>
<span {...getWrapperProps()}>
{leftIcon && clonedLeftIcon}
<span {...getThumbProps()}>{thumbIcon && clonedThumbIcon}</span>
{rightIcon && clonedRightIcon}
</span>
{children && <span {...getLabelProps()}>{children}</span>}
</Component>
);
});
if (__DEV__) {
Switch.displayName = "NextUI.Switch";
}
Switch.displayName = "NextUI.Switch";
export default Switch;

View File

@ -1,37 +1,285 @@
import type {ToggleVariantProps} from "@nextui-org/theme";
import type {ToggleVariantProps, ToggleSlots, SlotsToClasses} from "@nextui-org/theme";
import type {FocusableRef} from "@react-types/shared";
import type {AriaSwitchProps} from "@react-aria/switch";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {ReactNode, Ref, useCallback, useId, useRef} from "react";
import {mapPropsVariants} from "@nextui-org/system";
import {useHover} from "@react-aria/interactions";
import {toggle} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {ReactRef} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useFocusableRef} from "@nextui-org/dom-utils";
import {useSwitch as useReactAriaSwitch} from "@react-aria/switch";
import {useMemo} from "react";
import {useToggleState} from "@react-stately/toggle";
import {useFocusRing} from "@react-aria/focus";
export interface UseSwitchProps extends HTMLNextUIProps<"div">, ToggleVariantProps {
export type SwitchThumbIconProps = {
width: string;
height: string;
"data-checked": string;
isSelected: boolean;
className: string;
};
interface Props extends HTMLNextUIProps<"label"> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
ref?: Ref<HTMLElement>;
/**
* The label of the switch.
*/
children?: ReactNode;
/**
* Whether the switch is disabled.
* @default false
*/
isDisabled?: boolean;
/**
* The icon to be displayed inside the thumb.
*/
thumbIcon?: ReactNode | ((props: SwitchThumbIconProps) => ReactNode);
/**
* Left icon to be displayed inside the switch.
*/
leftIcon?: ReactNode;
/**
* Right icon to be displayed inside the switch.
*/
rightIcon?: ReactNode;
/**
* Classname or List of classes to change the styles of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Switch styles={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* thumb: "thumb-classes",
* thumbIcon: "thumbIcon-classes",
* label: "label-classes",
* }} />
* ```
*/
styles?: SlotsToClasses<ToggleSlots>;
}
export type UseSwitchProps = Omit<Props, "defaultChecked" | "onChange"> &
Omit<AriaSwitchProps, keyof ToggleVariantProps> &
ToggleVariantProps;
export function useSwitch(originalProps: UseSwitchProps) {
const [props, variantProps] = mapPropsVariants(originalProps, toggle.variantKeys);
const {ref, as, className, ...otherProps} = props;
const {
ref,
as,
name,
value = "",
isReadOnly = false,
autoFocus = false,
leftIcon,
rightIcon,
defaultSelected,
isSelected: isSelectedProp,
children,
thumbIcon,
className,
styles,
onChange,
...otherProps
} = props;
const Component = as || "div";
const Component = as || "label";
const domRef = useDOMRef(ref);
const inputRef = useRef(null);
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
const styles = useMemo(
const labelId = useId();
const inputId = useId();
const ariaSwitchProps = useMemo(() => {
const ariaLabel =
otherProps["aria-label"] || typeof children === "string" ? (children as string) : undefined;
return {
name,
value,
children,
autoFocus,
defaultSelected,
isSelected: isSelectedProp,
isDisabled: !!originalProps.isDisabled,
isReadOnly,
"aria-label": ariaLabel,
"aria-labelledby": otherProps["aria-labelledby"] || labelId,
onChange,
};
}, [
value,
name,
labelId,
children,
autoFocus,
isSelectedProp,
defaultSelected,
originalProps.isDisabled,
otherProps["aria-label"],
otherProps["aria-labelledby"],
onChange,
]);
const state = useToggleState(ariaSwitchProps);
const {inputProps} = useReactAriaSwitch(ariaSwitchProps, state, inputRef);
const {focusProps, isFocused, isFocusVisible} = useFocusRing({autoFocus: inputProps.autoFocus});
const {hoverProps, isHovered} = useHover({
isDisabled: inputProps.disabled,
});
const isSelected = inputProps.checked;
const isDisabled = inputProps.disabled;
const slots = useMemo(
() =>
toggle({
...variantProps,
className,
isFocusVisible,
}),
[...Object.values(variantProps), className],
[...Object.values(variantProps), isFocusVisible],
);
return {Component, styles, domRef, ...otherProps};
const baseStyles = clsx(styles?.base, className);
const getBaseProps: PropGetter = () => {
return {
ref: domRef,
className: slots.base({class: baseStyles}),
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
...mergeProps(hoverProps, otherProps),
};
};
const getWrapperProps: PropGetter = useCallback(() => {
return {
"data-hover": dataAttr(isHovered),
"data-checked": dataAttr(isSelected),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
"data-disabled": dataAttr(isDisabled),
"data-readonly": dataAttr(inputProps.readOnly),
"aria-hidden": true,
className: clsx(slots.wrapper({class: styles?.wrapper})),
};
}, [
slots,
styles?.wrapper,
isHovered,
isSelected,
isFocused,
isFocusVisible,
isDisabled,
inputProps.readOnly,
]);
const getInputProps: PropGetter = () => {
return {
ref: inputRef,
id: inputProps.id || inputId,
...mergeProps(inputProps, focusProps),
};
};
const getThumbProps: PropGetter = useCallback(
() => ({
"data-checked": dataAttr(isSelected),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
"data-disabled": dataAttr(isDisabled),
"data-readonly": dataAttr(inputProps.readOnly),
className: slots.thumb({class: styles?.thumb}),
}),
[slots, styles?.thumb, isSelected, isFocused, isFocusVisible, isDisabled, inputProps.readOnly],
);
const getLabelProps: PropGetter = useCallback(
() => ({
id: labelId,
htmlFor: inputProps.id || inputId,
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
className: slots.label({class: styles?.label}),
}),
[slots, styles?.label, isDisabled, isSelected],
);
const getThumbIconProps = useCallback(
(
props = {
includeStateProps: false,
},
) =>
mergeProps(
{
width: "1em",
height: "1em",
"data-checked": dataAttr(isSelected),
className: slots.thumbIcon({class: styles?.thumbIcon}),
},
props.includeStateProps
? {
isSelected: isSelected,
}
: {},
) as unknown as SwitchThumbIconProps,
[slots, styles?.thumbIcon, isSelected, originalProps.disableAnimation],
);
const getLeftIconProps = useCallback(
() => ({
width: "1em",
height: "1em",
"data-checked": dataAttr(isSelected),
className: slots.leftIcon({class: styles?.leftIcon}),
}),
[slots, styles?.leftIcon, isSelected],
);
const getRightIconProps = useCallback(
() => ({
width: "1em",
height: "1em",
"data-checked": dataAttr(isSelected),
className: slots.rightIcon({class: styles?.rightIcon}),
}),
[slots, styles?.rightIcon, isSelected],
);
return {
Component,
slots,
styles,
domRef,
children,
thumbIcon,
leftIcon,
rightIcon,
isHovered,
isSelected,
isFocused,
isFocusVisible,
isDisabled,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
getThumbProps,
getThumbIconProps,
getLeftIconProps,
getRightIconProps,
};
}
export type UseSwitchReturn = ReturnType<typeof useSwitch>;

View File

@ -1,11 +1,14 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {toggle} from "@nextui-org/theme";
import {VisuallyHidden} from "@react-aria/visually-hidden";
import {SunFilledIcon, MoonFilledIcon} from "@nextui-org/shared-icons";
import {clsx} from "@nextui-org/shared-utils";
import {Switch, SwitchProps} from "../src";
import {Switch, SwitchProps, SwitchThumbIconProps, useSwitch} from "../src";
export default {
title: "Switch",
title: "Components/Switch",
component: Switch,
argTypes: {
color: {
@ -14,12 +17,6 @@ export default {
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
control: {
type: "select",
@ -31,6 +28,11 @@ export default {
type: "boolean",
},
},
disableAnimation: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof Switch>;
@ -40,7 +42,147 @@ const defaultProps = {
const Template: ComponentStory<typeof Switch> = (args: SwitchProps) => <Switch {...args} />;
const WithIconsTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
const [isSelected, setIsSelected] = React.useState<boolean>(true);
return (
<div className="flex flex-col gap-2">
<Switch
{...args}
isSelected={isSelected}
leftIcon={<SunFilledIcon />}
rightIcon={<MoonFilledIcon />}
styles={{
leftIcon: "text-white",
}}
onChange={setIsSelected}
/>
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
</div>
);
};
const ControlledTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
const [isSelected, setIsSelected] = React.useState<boolean>(true);
return (
<div className="flex flex-col gap-2">
<Switch {...args} isSelected={isSelected} onChange={setIsSelected} />
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
</div>
);
};
const CustomWithStylesTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
const [isSelected, setIsSelected] = React.useState<boolean>(true);
return (
<div className="flex flex-col gap-2">
<Switch
isSelected={isSelected}
size="lg"
styles={{
base: clsx(
"inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center justify-between cursor-pointer rounded-lg gap-2 p-4 border-1.5 border-transparent",
{
"border-primary": isSelected,
},
),
}}
onChange={setIsSelected}
{...args}
>
<div className="flex flex-col gap-1">
<p className="text-base">Enable early access</p>
<p className="text-xs text-neutral-400">
Get access to new features before they are released.
</p>
</div>
</Switch>
<p className="text-neutral-500">Selected: {isSelected ? "true" : "false"}</p>
</div>
);
};
const CustomWithHooksTemplate: ComponentStory<typeof Switch> = (args: SwitchProps) => {
const {Component, slots, isSelected, getBaseProps, getInputProps, getWrapperProps} =
useSwitch(args);
return (
<div className="flex flex-col gap-2">
<Component {...getBaseProps()}>
<VisuallyHidden>
<input {...getInputProps()} />
</VisuallyHidden>
<div
{...getWrapperProps()}
className={slots.wrapper({
class: [
"w-8 h-8",
"flex items-center justify-center",
"rounded-lg bg-neutral-100 hover:bg-neutral-200",
],
})}
>
{isSelected ? <SunFilledIcon /> : <MoonFilledIcon />}
</div>
</Component>
<p className="text-neutral-500 select-none">Lights: {isSelected ? "on" : "off"}</p>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};
export const IsReadOnly = Template.bind({});
IsReadOnly.args = {
...defaultProps,
isReadOnly: true,
defaultSelected: true,
};
export const WithLabel = Template.bind({});
WithLabel.args = {
...defaultProps,
children: "Bluetooth",
};
export const DisableAnimation = Template.bind({});
DisableAnimation.args = {
...defaultProps,
disableAnimation: true,
};
export const WithThumbIcon = Template.bind({});
WithThumbIcon.args = {
...defaultProps,
thumbIcon: (props: SwitchThumbIconProps) =>
props.isSelected ? (
<SunFilledIcon className={props.className} />
) : (
<MoonFilledIcon className={props.className} />
),
};
export const WithIcons = WithIconsTemplate.bind({});
WithIcons.args = {
...defaultProps,
};
export const Controlled = ControlledTemplate.bind({});
Controlled.args = {
...defaultProps,
};
export const CustomWithStyles = CustomWithStylesTemplate.bind({});
CustomWithStyles.args = {
...defaultProps,
};
export const CustomWithHooks = CustomWithHooksTemplate.bind({});
CustomWithHooks.args = {
...defaultProps,
};

View File

@ -20,19 +20,19 @@ const base: SemanticBaseColors = {
},
content1: {
DEFAULT: twColors.zinc[50],
contrastText: readableColor(twColors.zinc[50]),
contrastText: twColors.zinc[900],
},
content2: {
DEFAULT: twColors.zinc[100],
contrastText: readableColor(twColors.zinc[100]),
contrastText: twColors.zinc[800],
},
content3: {
DEFAULT: twColors.zinc[200],
contrastText: readableColor(twColors.zinc[200]),
contrastText: twColors.zinc[700],
},
content4: {
DEFAULT: twColors.zinc[300],
contrastText: readableColor(twColors.zinc[300]),
contrastText: twColors.zinc[600],
},
},
dark: {
@ -47,19 +47,19 @@ const base: SemanticBaseColors = {
},
content1: {
DEFAULT: twColors.zinc[900],
contrastText: readableColor(twColors.zinc[900]),
contrastText: twColors.zinc[50],
},
content2: {
DEFAULT: twColors.zinc[800],
contrastText: readableColor(twColors.zinc[800]),
contrastText: twColors.zinc[100],
},
content3: {
DEFAULT: twColors.zinc[700],
contrastText: readableColor(twColors.zinc[700]),
contrastText: twColors.zinc[200],
},
content4: {
DEFAULT: twColors.zinc[600],
contrastText: readableColor(twColors.zinc[600]),
contrastText: twColors.zinc[300],
},
},
};

View File

@ -77,8 +77,8 @@ const button = tv({
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
},
disableAnimation: {
false: "transition-transform",
true: "!transition-none",
false: "transition-transform-background",
},
},
defaultVariants: {
@ -220,32 +220,32 @@ const button = tv({
{
variant: "light",
color: "neutral",
class: colorVariants.light.neutral,
class: [colorVariants.light.neutral, "hover:!bg-neutral-100"],
},
{
variant: "light",
color: "primary",
class: colorVariants.light.primary,
class: [colorVariants.light.primary, "hover:!bg-primary-50"],
},
{
variant: "light",
color: "secondary",
class: colorVariants.light.secondary,
class: [colorVariants.light.secondary, "hover:!bg-secondary-100"],
},
{
variant: "light",
color: "success",
class: colorVariants.light.success,
class: [colorVariants.light.success, "hover:!bg-success-50"],
},
{
variant: "light",
color: "warning",
class: colorVariants.light.warning,
class: [colorVariants.light.warning, "hover:!bg-warning-50"],
},
{
variant: "light",
color: "danger",
class: colorVariants.light.danger,
class: [colorVariants.light.danger, "hover:!bg-danger-50"],
},
// ghost / color
{

View File

@ -9,7 +9,7 @@ import {ringClasses} from "../utils";
*
* @example
* <label className={base())}>
* // input
* // hidden input
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
* <svg className={icon()}>
* // check icon
@ -152,6 +152,8 @@ const checkbox = tv({
disableAnimation: {
true: {
wrapper: "transition-none",
icon: "transition-none",
label: "transition-none",
},
false: {
wrapper: ["before:transition-background", "after:transition-transform-opacity"],

View File

@ -109,7 +109,10 @@ const pagination = tv({
true: {},
},
disableAnimation: {
true: {},
true: {
item: "transition-none",
cursor: "transition-none",
},
false: {
item: "transition-background",
cursor: ["transition-transform", "!duration-300"],

View File

@ -1,22 +1,173 @@
import {tv, type VariantProps} from "tailwind-variants";
import {ringClasses} from "../utils";
/**
* Toggle (Switch) wrapper **Tailwind Variants** component
*
* const {base, content, dot, avatar, closeButton} = toggle({...})
* const {base, wrapper, thumb, thumbIcon, label} = toggle({...})
*
* @example
* <div className={base())}>
* // left content
* <span className={avatar()}/>
* <svg className={dot()}/>
* <span className={content()}>Default</span>
* <svg className={closeButton()}>close button</svg>
* // right content
* </div>
* <label className={base())}>
* // hidden input
* <span className={wrapper()} aria-hidden="true" data-checked={checked}>
* <svg className={leftIcon()}>...</svg>
* <span className={thumb()}>
* <svg className={thumbIcon()}>...</svg>
* </span>
* <svg className={rightIcon()}>...</svg>
* </span>
* <span className={label()}>Label</span>
* </label>
*/
const toggle = tv({
variants: {},
slots: {
base: "relative max-w-fit inline-flex items-center justify-start cursor-pointer",
wrapper: [
"group",
"px-1",
"relative",
"inline-flex",
"items-center",
"justify-start",
"flex-shrink-0",
"overflow-hidden",
"bg-neutral-200",
"rounded-full",
],
thumb: [
"z-10",
"flex",
"items-center",
"justify-center",
"bg-white",
"shadow-sm",
"rounded-full",
"data-[checked=true]:translate-x-full",
],
leftIcon: "z-0 absolute left-1.5 text-current",
rightIcon: "z-0 absolute right-1.5 text-neutral-600",
thumbIcon: "text-black",
label: "relative text-foreground select-none",
},
variants: {
color: {
neutral: {
wrapper: [
"data-[checked=true]:bg-neutral-400",
"data-[checked=true]:text-neutral-contrastText",
],
},
primary: {
wrapper: [
"data-[checked=true]:bg-primary",
"data-[checked=true]:text-primary-contrastText",
],
},
secondary: {
wrapper: [
"data-[checked=true]:bg-secondary",
"data-[checked=true]:text-secondary-contrastText",
],
},
success: {
wrapper: [
"data-[checked=true]:bg-success",
"data-[checked=true]:text-success-contrastText",
],
},
warning: {
wrapper: [
"data-[checked=true]:bg-warning",
"data-[checked=true]:text-warning-contrastText",
],
},
danger: {
wrapper: ["data-[checked=true]:bg-danger", "data-[checked=true]:text-danger-contrastText"],
},
},
size: {
xs: {
wrapper: "px-0.5 w-7 h-4 mr-1",
thumb: "w-3 h-3 text-[0.5rem]",
leftIcon: "text-[0.5rem] left-1",
rightIcon: "text-[0.5rem] right-1",
right: "text-[0.5rem]",
label: "text-xs",
},
sm: {
wrapper: "w-8 h-5 mr-1",
thumb: "w-3 h-3 text-[0.6rem]",
leftIcon: "text-[0.6rem] left-1",
rightIcon: "text-[0.6rem] right-1",
label: "text-sm",
},
md: {
wrapper: "w-10 h-6 mr-2",
thumb: "w-4 h-4 text-xs",
rightIcon: "text-xs",
leftIcon: "text-xs",
label: "text-base",
},
lg: {
wrapper: "w-12 h-7 mr-2",
thumb: "w-5 h-5 text-sm",
rightIcon: "text-sm",
leftIcon: "text-sm",
label: "text-lg",
},
xl: {
wrapper: "w-14 h-8 mr-2",
thumb: "w-6 h-6 text-base",
rightIcon: "text-base",
leftIcon: "text-base",
label: "text-xl",
},
},
isFocusVisible: {
true: {
wrapper: [...ringClasses],
},
},
isDisabled: {
true: {
base: "opacity-50 pointer-events-none",
},
},
disableAnimation: {
true: {
wrapper: "transition-none",
thumb: "transition-none",
},
false: {
wrapper: "transition-background",
thumb: [
"transition",
"group-active:translate-x-1",
"data-[checked=true]:group-active:translate-x-[90%]",
],
leftIcon: [
"opacity-0",
"scale-50",
"transition-transform-opacity",
"data-[checked=true]:scale-100",
"data-[checked=true]:opacity-100",
],
rightIcon: [
"opacity-100",
"transition-transform-opacity",
"data-[checked=true]:translate-x-3",
"data-[checked=true]:opacity-0",
],
},
},
},
defaultVariants: {
color: "primary",
size: "md",
isDisabled: false,
disableAnimation: false,
},
});
export type ToggleVariantProps = VariantProps<typeof toggle>;

View File

@ -66,4 +66,9 @@ export const utilities = {
"transition-timing-function": "ease",
"transition-duration": DEFAULT_TRANSITION_DURATION,
},
".transition-transform-background": {
"transition-property": "transform, background",
"transition-timing-function": "ease",
"transition-duration": DEFAULT_TRANSITION_DURATION,
},
};

View File

@ -49,7 +49,7 @@ const faded = {
};
const light = {
neutral: "bg-transparent text-foreground",
neutral: "bg-transparent text-neutral-contrastText",
primary: "bg-transparent text-primary",
secondary: "bg-transparent text-secondary",
success: "bg-transparent text-success",

View File

@ -15,6 +15,7 @@ module.exports = {
"../../components/checkbox/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/radio/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/pagination/stories/*.stories.@(js|jsx|ts|tsx)",
"../../components/switch/stories/*.stories.@(js|jsx|ts|tsx)",
],
staticDirs: ["../public"],
addons: [

View File

@ -134,7 +134,7 @@ import {link, button} from "@nextui-org/theme";
<br/>
<div class="block text-xs text-neutral-400">
Last updated on <time datetime="2023-03-07">March 7, 2023</time>
Last updated on <time datetime="2023-03-07">March 13, 2023</time>
</div>

View File

@ -0,0 +1,27 @@
import {IconSvgProps} from "./types";
export const HeadphonesIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path
d="M2.74982 18.6508C2.33982 18.6508 1.99982 18.3108 1.99982 17.9008V12.2008C1.94982 9.49078 2.95982 6.93078 4.83982 5.01078C6.71982 3.10078 9.23982 2.05078 11.9498 2.05078C17.4898 2.05078 21.9998 6.56078 21.9998 12.1008V17.8008C21.9998 18.2108 21.6598 18.5508 21.2498 18.5508C20.8398 18.5508 20.4998 18.2108 20.4998 17.8008V12.1008C20.4998 7.39078 16.6698 3.55078 11.9498 3.55078C9.63982 3.55078 7.49982 4.44078 5.90982 6.06078C4.30982 7.69078 3.45982 9.86078 3.49982 12.1808V17.8908C3.49982 18.3108 3.16982 18.6508 2.74982 18.6508Z"
fill="currentColor"
/>
<path
d="M5.94 12.4492H5.81C3.71 12.4492 2 14.1592 2 16.2592V18.1392C2 20.2392 3.71 21.9492 5.81 21.9492H5.94C8.04 21.9492 9.75 20.2392 9.75 18.1392V16.2592C9.75 14.1592 8.04 12.4492 5.94 12.4492Z"
fill="currentColor"
/>
<path
d="M18.19 12.4492H18.06C15.96 12.4492 14.25 14.1592 14.25 16.2592V18.1392C14.25 20.2392 15.96 21.9492 18.06 21.9492H18.19C20.29 21.9492 22 20.2392 22 18.1392V16.2592C22 14.1592 20.29 12.4492 18.19 12.4492Z"
fill="currentColor"
/>
</svg>
);

View File

@ -7,3 +7,9 @@ export * from "./close-filled";
export * from "./chevron";
export * from "./ellipsis";
export * from "./forward";
export * from "./sun";
export * from "./sun-filled";
export * from "./mail";
export * from "./moon";
export * from "./moon-filled";
export * from "./headphones";

View File

@ -0,0 +1,24 @@
import {IconSvgProps} from "./types";
export const MailIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<g
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
>
<path d="M12 20.5H7c-3 0-5-1.5-5-5v-7c0-3.5 2-5 5-5h10c3 0 5 1.5 5 5v3" />
<path d="M17 9l-3.13 2.5a3.166 3.166 0 01-3.75 0L7 9M19.21 14.77l-3.539 3.54a1.232 1.232 0 00-.3.59l-.19 1.35a.635.635 0 00.76.76l1.35-.19a1.189 1.189 0 00.59-.3l3.54-3.54a1.365 1.365 0 000-2.22 1.361 1.361 0 00-2.211.01zM18.7 15.28a3.185 3.185 0 002.22 2.22" />
</g>
</svg>
);

View File

@ -0,0 +1,18 @@
import {IconSvgProps} from "./types";
export const MoonFilledIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
fill="currentColor"
/>
</svg>
);

View File

@ -0,0 +1,22 @@
import {IconSvgProps} from "./types";
export const MoonIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 512 512"
width="1em"
{...props}
>
<path
d="M160 136c0-30.62 4.51-61.61 16-88C99.57 81.27 48 159.32 48 248c0 119.29 96.71 216 216 216 88.68 0 166.73-51.57 200-128-26.39 11.49-57.38 16-88 16-119.29 0-216-96.71-216-216z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
/>
</svg>
);

View File

@ -0,0 +1,18 @@
import {IconSvgProps} from "./types";
export const SunFilledIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<g fill="currentColor">
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
</g>
</svg>
);

View File

@ -0,0 +1,32 @@
import {IconSvgProps} from "./types";
export const SunIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 512 512"
width="1em"
{...props}
>
<path
d="M256 48v48M256 416v48M403.08 108.92l-33.94 33.94M142.86 369.14l-33.94 33.94M464 256h-48M96 256H48M403.08 403.08l-33.94-33.94M142.86 142.86l-33.94-33.94"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit={10}
strokeWidth={32}
/>
<circle
cx={256}
cy={256}
fill="none"
r={80}
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit={10}
strokeWidth={32}
/>
</svg>
);

View File

@ -1,5 +1,4 @@
import {forwardRef} from "@nextui-org/system";
import {__DEV__} from "@nextui-org/shared-utils";
import { Use{{capitalize componentName}}Props, use{{capitalize componentName}} } from "./use-{{componentName}}";
@ -15,8 +14,6 @@ const {{capitalize componentName}} = forwardRef<{{capitalize componentName}}Prop
);
});
if (__DEV__) {
{{capitalize componentName}}.displayName = "NextUI.{{capitalize componentName}}";
}
{{capitalize componentName}}.displayName = "NextUI.{{capitalize componentName}}";
export default {{capitalize componentName}};

View File

@ -5,7 +5,7 @@ import { {{componentName}} } from "@nextui-org/theme";
import { {{capitalize componentName}}, {{capitalize componentName}}Props } from "../src";
export default {
title: "{{capitalize componentName}}",
title: "Components/{{capitalize componentName}}",
component: {{capitalize componentName}},
argTypes: {
color: {

28
pnpm-lock.yaml generated
View File

@ -686,9 +686,17 @@ importers:
packages/components/switch:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/shared-icons': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/focus': ^3.11.0
'@react-aria/interactions': ^3.14.0
'@react-aria/switch': ^3.4.0
'@react-aria/utils': ^3.15.0
'@react-aria/visually-hidden': ^3.7.0
'@react-stately/toggle': ^3.5.0
'@react-types/shared': ^3.17.0
clean-package: 2.2.0
react: ^18.2.0
dependencies:
@ -696,7 +704,15 @@ importers:
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/focus': 3.11.0_react@18.2.0
'@react-aria/interactions': 3.14.0_react@18.2.0
'@react-aria/switch': 3.4.0_react@18.2.0
'@react-aria/utils': 3.15.0_react@18.2.0
'@react-aria/visually-hidden': 3.7.0_react@18.2.0
'@react-stately/toggle': 3.5.0_react@18.2.0
'@react-types/shared': 3.17.0_react@18.2.0
devDependencies:
'@nextui-org/shared-icons': link:../../utilities/shared-icons
clean-package: 2.2.0
react: 18.2.0
@ -5288,6 +5304,18 @@ packages:
'@swc/helpers': 0.4.14
react: 18.2.0
/@react-aria/switch/3.4.0_react@18.2.0:
resolution: {integrity: sha512-FWbjqNcY+fAjRNXVQTvOx93udhaySgXLsMNLRAVUcFm45FqTaxAhHhNGtRnVhDvzHTJY/Y+kpcGzCcW2rXzPxg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/toggle': 3.5.0_react@18.2.0
'@react-stately/toggle': 3.5.0_react@18.2.0
'@react-types/switch': 3.3.0_react@18.2.0
'@swc/helpers': 0.4.14
react: 18.2.0
dev: false
/@react-aria/toggle/3.5.0_react@18.2.0:
resolution: {integrity: sha512-K49OmHBmYW8pk0rXJU1TNRzR+PxLVvfL/ni6ifV5gcxoxV6DmFsNFj+5B/U3AMnCEQeyKQeiY6z9X7EBVX6j9Q==}
peerDependencies: