feat(input): api improved

This commit is contained in:
Junior Garcia 2023-04-02 16:50:52 -03:00
parent 1ee0293bb3
commit 59003bd698
14 changed files with 428 additions and 77 deletions

View File

@ -102,6 +102,7 @@ CustomIconNode.args = {
export const CustomIconFunction = Template.bind({});
CustomIconFunction.args = {
...defaultProps,
// eslint-disable-next-line react/display-name
icon: (props: CheckboxIconProps) => <CloseIcon {...props} />,
};
@ -213,11 +214,17 @@ export const CustomWithStyles = (props: CustomCheckboxProps) => {
};
export const CustomWithHooks = (props: CheckboxProps) => {
const {children, isSelected, isFocusVisible, getBaseProps, getLabelProps, getInputProps} =
useCheckbox({
"aria-label": props["aria-label"] || "Toggle status",
...props,
});
const {
children,
isSelected,
isFocusVisible,
getBaseProps,
getLabelProps,
getInputProps,
} = useCheckbox({
"aria-label": props["aria-label"] || "Toggle status",
...props,
});
return (
<label {...getBaseProps()}>
@ -226,12 +233,11 @@ export const CustomWithHooks = (props: CheckboxProps) => {
</VisuallyHidden>
<Chip
color="primary"
leftContent={isSelected ? <CheckIcon className="ml-1" color={colors.white} /> : null}
startContent={isSelected ? <CheckIcon className="ml-1" color={colors.white} /> : null}
styles={{
base: clsx("border-neutral hover:bg-neutral-200", {
"border-primary bg-primary hover:bg-primary-600 hover:border-primary-600": isSelected,
"outline-none ring-2 !ring-primary ring-offset-2 ring-offset-background dark:ring-offset-background-dark":
isFocusVisible,
"outline-none ring-2 !ring-primary ring-offset-2 ring-offset-background dark:ring-offset-background-dark": isFocusVisible,
}),
content: clsx("text-primary", {
"text-primary-contrastText pl-1": isSelected,

View File

@ -31,13 +31,13 @@ describe("Chip", () => {
expect(wrapper.getByTestId("avatar")).not.toBeNull();
});
it("should support leftContent", () => {
const wrapper = render(<Chip leftContent={<span data-testid="left-icon" />} />);
it("should support startContent", () => {
const wrapper = render(<Chip startContent={<span data-testid="left-icon" />} />);
expect(wrapper.getByTestId("left-icon")).not.toBeNull();
});
it("should support rightContent", () => {
it("should support endContent", () => {
const wrapper = render(<Chip avatar={<span data-testid="close-icon" />} />);
expect(wrapper.getByTestId("close-icon")).not.toBeNull();

View File

@ -15,8 +15,8 @@ const Chip = forwardRef<ChipProps, "div">((props, ref) => {
styles,
isDot,
isCloseable,
leftContent,
rightContent,
startContent,
endContent,
getCloseButtonProps,
getChipProps,
} = useChip({
@ -24,29 +24,27 @@ const Chip = forwardRef<ChipProps, "div">((props, ref) => {
...props,
});
const left = useMemo(() => {
if (isDot && !leftContent) {
const start = useMemo(() => {
if (isDot && !startContent) {
return <span className={slots.dot({class: styles?.dot})} />;
}
return leftContent;
}, [slots, leftContent, isDot]);
return startContent;
}, [slots, startContent, isDot]);
const right = useMemo(() => {
const end = useMemo(() => {
if (isCloseable) {
return (
<span {...getCloseButtonProps()}>{!rightContent ? <CloseFilledIcon /> : rightContent}</span>
);
return <span {...getCloseButtonProps()}>{endContent || <CloseFilledIcon />}</span>;
}
return rightContent;
}, [rightContent, isCloseable, getCloseButtonProps]);
return endContent;
}, [endContent, isCloseable, getCloseButtonProps]);
return (
<Component {...getChipProps()}>
{left}
{start}
<span className={slots.content({class: styles?.content})}>{children}</span>
{right}
{end}
</Component>
);
});

View File

@ -24,11 +24,14 @@ export interface UseChipProps extends HTMLNextUIProps<"div">, ChipVariantProps {
* Element to be rendered in the left side of the chip.
* this props overrides the `avatar` prop.
*/
leftContent?: React.ReactNode;
startContent?: React.ReactNode;
/**
* Element to be rendered in the right side of the chip.
* if you pass this prop and the `onClose` prop, the passed element
* will have the close button props and it will be rendered instead of the
* default close button.
*/
rightContent?: React.ReactNode;
endContent?: React.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.
@ -47,7 +50,7 @@ export interface UseChipProps extends HTMLNextUIProps<"div">, ChipVariantProps {
styles?: SlotsToClasses<ChipSlots>;
/**
* Callback fired when the chip is closed. if you pass this prop,
* the chip will display a close button (rightContent).
* the chip will display a close button (endContent).
* @param e PressEvent
*/
onClose?: (e: PressEvent) => void;
@ -61,8 +64,8 @@ export function useChip(originalProps: UseChipProps) {
as,
children,
avatar,
leftContent,
rightContent,
startContent,
endContent,
onClose,
styles,
className,
@ -80,28 +83,27 @@ export function useChip(originalProps: UseChipProps) {
const {focusProps: closeFocusProps, isFocusVisible: isCloseButtonFocusVisible} = useFocusRing();
const isOneChar = useMemo(
() => typeof children === "string" && children?.length === 1,
[children],
);
const isOneChar = useMemo(() => typeof children === "string" && children?.length === 1, [
children,
]);
const hasLeftContent = useMemo(() => !!avatar || !!leftContent, [avatar, leftContent]);
const hasRightContent = useMemo(() => !!rightContent || isCloseable, [rightContent, isCloseable]);
const hasStartContent = useMemo(() => !!avatar || !!startContent, [avatar, startContent]);
const hasEndContent = useMemo(() => !!endContent || isCloseable, [endContent, isCloseable]);
const slots = useMemo(
() =>
chip({
...variantProps,
hasLeftContent,
hasRightContent,
hasStartContent,
hasEndContent,
isOneChar,
isCloseButtonFocusVisible,
}),
[
...Object.values(variantProps),
isCloseButtonFocusVisible,
hasLeftContent,
hasRightContent,
hasStartContent,
hasEndContent,
isOneChar,
],
);
@ -152,8 +154,8 @@ export function useChip(originalProps: UseChipProps) {
styles,
isDot: isDotVariant,
isCloseable,
leftContent: getAvatarClone(avatar) || getContentClone(leftContent),
rightContent: getContentClone(rightContent),
startContent: getAvatarClone(avatar) || getContentClone(startContent),
endContent: getContentClone(endContent),
getCloseButtonProps,
getChipProps,
};

View File

@ -54,16 +54,24 @@ Default.args = {
...defaultProps,
};
export const LeftContent = Template.bind({});
LeftContent.args = {
export const StartContent = Template.bind({});
StartContent.args = {
...defaultProps,
leftContent: <span className="ml-1">🎉</span>,
startContent: (
<span aria-label="celebration" className="ml-1" role="img">
🎉
</span>
),
};
export const RightContent = Template.bind({});
RightContent.args = {
export const EndContent = Template.bind({});
EndContent.args = {
...defaultProps,
rightContent: <span className="mr-1">🚀</span>,
endContent: (
<span aria-label="rocket" className="mr-1" role="img">
🚀
</span>
),
};
export const Closeable = Template.bind({});
@ -76,7 +84,7 @@ Closeable.args = {
export const CustomCloseIcon = Template.bind({});
CustomCloseIcon.args = {
...defaultProps,
rightContent: <CheckIcon />,
endContent: <CheckIcon />,
// eslint-disable-next-line
onClose: () => console.log("Close"),
};

View File

@ -39,12 +39,14 @@
"dependencies": {
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/use-aria-field": "workspace:*",
"@react-aria/focus": "^3.11.0",
"@react-aria/utils": "^3.15.0",
"@react-stately/utils": "^3.6.0"
"@react-stately/utils": "^3.6.0",
"@react-aria/interactions": "^3.14.0"
},
"devDependencies": {
"@react-types/shared": "^3.15.0",

View File

@ -1,33 +1,62 @@
import {forwardRef} from "@nextui-org/system";
import {CloseFilledIcon} from "@nextui-org/shared-icons";
import {useMemo} from "react";
import {UseInputProps, useInput} from "./use-input";
export interface InputProps extends Omit<UseInputProps, "ref"> {}
export interface InputProps extends Omit<UseInputProps, "ref" | "isClearButtonFocusVisible"> {}
const Input = forwardRef<InputProps, "input">((props, ref) => {
const {
Component,
label,
description,
isClearable,
startContent,
endContent,
shouldLabelBeOutside,
shouldLabelBeInside,
errorMessage,
getBaseProps,
getLabelProps,
getInputProps,
getInnerWrapperProps,
getInputWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
} = useInput({ref, ...props});
const labelContent = <label {...getLabelProps()}>{label}</label>;
const end = useMemo(() => {
if (isClearable) {
return <span {...getClearButtonProps()}>{endContent || <CloseFilledIcon />}</span>;
}
return endContent;
}, [isClearable, getClearButtonProps]);
const innerWrapper = useMemo(() => {
if (startContent || end) {
return (
<div {...getInnerWrapperProps()}>
{startContent}
<input {...getInputProps()} />
{end}
</div>
);
}
return <input {...getInputProps()} />;
}, [startContent, end, getInputProps, getInnerWrapperProps]);
return (
<Component {...getBaseProps()}>
{shouldLabelBeOutside ? labelContent : null}
<div {...getInputWrapperProps()}>
{shouldLabelBeInside ? labelContent : null}
<input {...getInputProps()} />
{innerWrapper}
</div>
{description && <div {...getDescriptionProps()}>{description}</div>}
{errorMessage && <div {...getErrorMessageProps()}>{errorMessage}</div>}

View File

@ -5,6 +5,7 @@ import {AriaTextFieldProps} from "@react-types/textfield";
import {useFocusRing} from "@react-aria/focus";
import {input} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {usePress} from "@react-aria/interactions";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useControlledState} from "@react-stately/utils";
import {useMemo, Ref} from "react";
@ -17,17 +18,34 @@ export interface Props extends HTMLNextUIProps<"input"> {
* Ref to the DOM node.
*/
ref?: Ref<HTMLInputElement>;
/**
* Element to be rendered in the left side of the input.
*/
startContent?: React.ReactNode;
/**
* Element to be rendered in the right side of the input.
* if you pass this prop and the `onClear` prop, the passed element
* will have the clear button props and it will be rendered instead of the
* default clear button.
*/
endContent?: React.ReactNode;
/**
* Callback fired when the value is cleared.
* if you pass this prop, the clear button will be shown.
*/
onClear?: () => void;
/**
* 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
* <Chip styles={{
* <Input styles={{
* base:"base-classes",
* label: "label-classes",
* inputWrapper: "input-wrapper-classes",
* input: "input-classes",
* clearButton: "clear-button-classes",
* description: "description-classes",
* helperText: "helper-text-classes",
* }} />
@ -50,19 +68,33 @@ export function useInput(originalProps: UseInputProps) {
className,
styles,
autoFocus,
startContent,
endContent,
onClear,
...otherProps
} = props;
const [inputValue, setInputValue] = useControlledState(props.value, props.defaultValue, () => {});
const Component = as || "div";
const baseStyles = clsx(styles?.base, className);
const baseStyles = clsx(styles?.base, className, !!inputValue ? "is-filled" : "");
const domRef = useDOMRef<HTMLInputElement>(ref);
const [inputValue, setInputValue] = useControlledState(props.value, props.defaultValue, () => {});
const handleClear = () => {
setInputValue(undefined);
if (domRef.current) {
domRef.current.value = "";
domRef.current.focus();
}
onClear?.();
};
const {labelProps, inputProps, descriptionProps, errorMessageProps} = useAriaTextField(
{
...originalProps,
value: inputValue,
onChange: chain(props.onChange, setInputValue),
},
domRef,
@ -73,22 +105,43 @@ export function useInput(originalProps: UseInputProps) {
isTextInput: true,
});
const {focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible} = useFocusRing();
const {pressProps: clearPressProps} = usePress({
isDisabled: !!originalProps?.isDisabled,
onPress: handleClear,
});
const isInvalid = props.validationState === "invalid";
const labelPosition = originalProps.labelPosition || "outside";
const isLabelPlaceholder = !props.placeholder;
const isLabelPlaceholder = !props.placeholder && labelPosition !== "outside-left";
const isClearable = !!onClear || originalProps.isClearable;
const shouldLabelBeOutside = labelPosition === "outside" || labelPosition === "outside-left";
const shouldLabelBeInside = labelPosition === "inside";
const hasStartContent = useMemo(() => !!startContent, [startContent]);
const hasEndContent = useMemo(() => !!endContent || isClearable, [endContent, isClearable]);
const slots = useMemo(
() =>
input({
...variantProps,
isInvalid,
isLabelPlaceholder,
isClearable,
isFocusVisible,
isLabelPlaceholder: isLabelPlaceholder && !hasStartContent,
isClearButtonFocusVisible,
}),
[...Object.values(variantProps), isInvalid, isLabelPlaceholder, isFocusVisible],
[
...Object.values(variantProps),
isInvalid,
isClearable,
isClearButtonFocusVisible,
isLabelPlaceholder,
hasStartContent,
isFocusVisible,
],
);
const getBaseProps: PropGetter = (props = {}) => {
@ -112,7 +165,7 @@ export function useInput(originalProps: UseInputProps) {
const getInputProps: PropGetter = (props = {}) => {
return {
ref: domRef,
className: slots.input({class: styles?.input}),
className: slots.input({class: clsx(styles?.input, !!inputValue ? "is-filled" : "")}),
"data-focus-visible": dataAttr(isFocusVisible),
"data-focused": dataAttr(isFocused),
"data-invalid": dataAttr(isInvalid),
@ -129,6 +182,15 @@ export function useInput(originalProps: UseInputProps) {
};
};
const getInnerWrapperProps: PropGetter = (props = {}) => {
return {
className: slots.innerWrapper({
class: styles?.innerWrapper,
}),
...props,
};
};
const getDescriptionProps: PropGetter = (props = {}) => {
return {
className: slots.description({class: styles?.description}),
@ -145,13 +207,25 @@ export function useInput(originalProps: UseInputProps) {
};
};
const getClearButtonProps: PropGetter = () => {
return {
role: "button",
tabIndex: 0,
className: slots.clearButton({class: styles?.clearButton}),
...mergeProps(clearPressProps, clearFocusProps),
};
};
return {
Component,
styles,
domRef,
label,
description,
startContent,
endContent,
labelPosition,
isClearable,
shouldLabelBeOutside,
shouldLabelBeInside,
errorMessage,
@ -159,8 +233,10 @@ export function useInput(originalProps: UseInputProps) {
getLabelProps,
getInputProps,
getInputWrapperProps,
getInnerWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
};
}

View File

@ -1,6 +1,7 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {input} from "@nextui-org/theme";
import {MailFilledIcon} from "@nextui-org/shared-icons";
import {Input, InputProps} from "../src";
@ -44,6 +45,13 @@ export default {
},
},
},
decorators: [
(Story) => (
<div className="flex items-center justify-center w-screen h-screen">
<Story />
</div>
),
],
} as ComponentMeta<typeof Input>;
const defaultProps = {
@ -52,9 +60,37 @@ const defaultProps = {
};
const Template: ComponentStory<typeof Input> = (args: InputProps) => (
<div className="max-w-md flex flex-row gap-4">
<div className="w-full max-w-[240px]">
<Input {...args} />
<Input {...args} placeholder="Enter your email" />
</div>
);
const LabelPositionTemplate: ComponentStory<typeof Input> = (args: InputProps) => (
<div className="w-full max-w-xl flex flex-row items-end gap-4">
<Input {...args} />
<Input {...args} labelPosition="outside" />
<Input {...args} labelPosition="outside-left" />
</div>
);
const StartContentTemplate: ComponentStory<typeof Input> = (args: InputProps) => (
<div className="w-full max-w-xl flex flex-row items-end gap-4">
<Input
{...args}
placeholder="you@example.com"
startContent={<MailFilledIcon className="text-2xl pointer-events-none" />}
/>
<Input
{...args}
label="Price"
placeholder="0.00"
startContent={
<div className="pointer-events-none flex items-center">
<span className="text-gray-500 sm:text-sm">$</span>
</div>
}
type="number"
/>
</div>
);
@ -69,6 +105,37 @@ WithPlaceholder.args = {
placeholder: "Enter your email",
};
export const Required = Template.bind({});
Required.args = {
...defaultProps,
isRequired: true,
};
export const LabelPosition = LabelPositionTemplate.bind({});
LabelPosition.args = {
...defaultProps,
};
export const Clearable = Template.bind({});
Clearable.args = {
...defaultProps,
variant: "bordered",
placeholder: "Enter your email",
defaultValue: "junior@nextui.org",
onClear: () => console.log("input cleared"),
};
export const StartContent = StartContentTemplate.bind({});
StartContent.args = {
...defaultProps,
};
export const LabelPositionWithPlaceholder = LabelPositionTemplate.bind({});
LabelPositionWithPlaceholder.args = {
...defaultProps,
placeholder: "Enter your email",
};
export const WithErrorMessage = Template.bind({});
WithErrorMessage.args = {
...defaultProps,
@ -79,6 +146,7 @@ export const InvalidValidationState = Template.bind({});
InvalidValidationState.args = {
...defaultProps,
variant: "bordered",
defaultValue: "invalid@email.com",
validationState: "invalid",
errorMessage: "Please enter a valid email address",
};

View File

@ -36,6 +36,7 @@ const chip = tv({
"opacity-70",
"hover:opacity-100",
"cursor-pointer",
"active:opacity-70",
],
},
variants: {
@ -119,10 +120,10 @@ const chip = tv({
"3xl": {base: "rounded-3xl"},
full: {base: "rounded-full"},
},
hasLeftContent: {
hasStartContent: {
true: {},
},
hasRightContent: {
hasEndContent: {
true: {},
},
isOneChar: {
@ -442,31 +443,31 @@ const chip = tv({
base: "w-8 h-8",
},
},
// hasLeftContent / size
// hasStartContent / size
{
hasLeftContent: true,
hasStartContent: true,
size: ["xs", "sm"],
class: {
content: "pl-0.5",
},
},
{
hasLeftContent: true,
hasStartContent: true,
size: ["md", "lg", "xl"],
class: {
content: "pl-1",
},
},
// hasRightContent / size
// hasEndContent / size
{
hasRightContent: true,
hasEndContent: true,
size: ["xs", "sm"],
class: {
content: "pr-0.5",
},
},
{
hasRightContent: true,
hasEndContent: true,
size: ["md", "lg", "xl"],
class: {
content: "pr-1",

View File

@ -9,12 +9,13 @@ import {ringClasses} from "../utils";
*
* @example
* ```js
* const {base, label, inputWrapper, input, description, helperText} = input({...})
* const {base, label, inputWrapper, input, clearButton, description, helperText} = input({...})
*
* <div className={base())}>
* <label className={label()}>Label</label>
* <div className={inputWrapper()}>
* <input className={input()}/>
* <button className={clearButton()}>Clear</button>
* </div>
* <span className={description()}>Description</span>
* <span className={helperText()}>Helper text</span>
@ -25,8 +26,22 @@ const input = tv({
slots: {
base: "flex flex-col gap-2",
label: "block text-sm font-medium text-neutral-600",
inputWrapper: "w-full flex flex-row items-center shadow-sm px-3 gap-3",
inputWrapper: "relative w-full inline-flex flex-row items-center shadow-sm px-3 gap-3",
innerWrapper: "inline-flex items-center w-full gap-1.5",
input: "w-full h-full bg-transparent outline-none placeholder:text-neutral-500",
clearButton: [
"z-10",
"absolute",
"right-3",
"appearance-none",
"outline-none",
"select-none",
"opacity-0",
"hover:opacity-100",
"cursor-pointer",
"active:opacity-70",
"rounded-full",
],
description: "text-xs text-neutral-500",
errorMessage: "text-xs text-danger",
},
@ -97,23 +112,28 @@ const input = tv({
label: "text-xs",
inputWrapper: "h-6 px-1",
input: "text-xs",
clearButton: "right-2",
},
sm: {
label: "text-xs",
inputWrapper: "h-8 px-2",
input: "text-xs",
clearButton: "text-lg",
},
md: {
inputWrapper: "h-10",
input: "text-sm",
clearButton: "text-xl",
},
lg: {
inputWrapper: "h-12",
input: "text-base",
clearButton: "text-xl",
},
xl: {
inputWrapper: "h-14",
input: "text-md",
clearButton: "text-2xl",
},
},
radius: {
@ -156,7 +176,13 @@ const input = tv({
},
isLabelPlaceholder: {
true: {
label: "absolute",
label: "absolute z-10 pointer-events-none",
},
},
isClearable: {
true: {
input: "peer",
clearButton: "peer-[.is-filled]:opacity-70",
},
},
isDisabled: {
@ -167,25 +193,42 @@ const input = tv({
isFocusVisible: {
true: {},
},
isClearButtonFocusVisible: {
true: {
clearButton: [...ringClasses],
},
},
isInvalid: {
true: {
label: "text-danger",
input: "placeholder:text-danger",
},
},
isRequired: {
true: {
label: "after:content-['*'] after:text-danger after:ml-0.5",
},
},
disableAnimation: {
true: {
inputWrapper: "transition-none",
label: "transition-none",
},
false: {
inputWrapper: "transition-background motion-reduce:transition-none",
inputWrapper: "transition-background motion-reduce:transition-none !duration-150",
label: [
"will-change-auto",
"transition-all",
"!duration-200",
"!ease-[cubic-bezier(0,0,0.2,1)]",
"motion-reduce:transition-none",
],
clearButton: [
"transition-transform-opacity",
"motion-reduce:transition-none",
"translate-x-1/2",
"peer-[.is-filled]:translate-x-0",
],
},
},
},
@ -195,7 +238,7 @@ const input = tv({
size: "md",
radius: "xl",
fullWidth: true,
labelPosition: "outside",
labelPosition: "inside",
isDisabled: false,
disableAnimation: false,
},
@ -513,9 +556,8 @@ const input = tv({
// !isLabelPlaceholder & labelPosition
{
isLabelPlaceholder: true,
labelPosition: "inside",
labelPosition: ["inside", "outside"],
class: {
inputWrapper: "group",
label: [
"font-normal",
"text-neutral-500",
@ -526,7 +568,22 @@ const input = tv({
],
},
},
// isLabelPlaceholder & labelPosition & size
{
isLabelPlaceholder: true,
labelPosition: "inside",
class: {
inputWrapper: "group",
},
},
{
isLabelPlaceholder: true,
labelPosition: "outside",
class: {
base: "group relative justify-end",
label: ["group-focus-within:left-0", "group-[.is-filled]:left-0"],
},
},
// isLabelPlaceholder & inside & size
{
isLabelPlaceholder: true,
labelPosition: "inside",
@ -599,6 +656,81 @@ const input = tv({
input: "pt-8",
},
},
// isLabelPlaceholder & outside & size
{
isLabelPlaceholder: true,
labelPosition: "outside",
size: "xs",
class: {
label: [
"text-xs",
"bottom-1",
"left-1",
"group-focus-within:bottom-8",
"group-[.is-filled]:bottom-8",
],
},
},
{
isLabelPlaceholder: true,
labelPosition: "outside",
size: "sm",
class: {
label: [
"text-xs",
"bottom-2",
"left-2",
"group-focus-within:bottom-10",
"group-[.is-filled]:bottom-10",
],
},
},
{
isLabelPlaceholder: true,
labelPosition: "outside",
size: "md",
class: {
label: [
"text-sm",
"bottom-2.5",
"left-3",
"group-focus-within:bottom-12",
"group-[.is-filled]:bottom-12",
],
},
},
{
isLabelPlaceholder: true,
labelPosition: "outside",
size: "lg",
class: {
label: [
"text-base",
"bottom-3",
"left-3",
"group-focus-within:text-sm",
"group-[.is-filled]:bottom-sm",
"group-focus-within:bottom-14",
"group-[.is-filled]:bottom-14",
],
},
},
{
isLabelPlaceholder: true,
labelPosition: "outside",
size: "xl",
class: {
label: [
"text-base",
"bottom-4",
"left-3",
"group-focus-within:text-sm",
"group-[.is-filled]:bottom-sm",
"group-focus-within:bottom-16",
"group-[.is-filled]:bottom-16",
],
},
},
],
});

View File

@ -10,6 +10,7 @@ export * from "./forward";
export * from "./sun";
export * from "./sun-filled";
export * from "./mail";
export * from "./mail-filled";
export * from "./moon";
export * from "./moon-filled";
export * from "./headphones";

View File

@ -0,0 +1,19 @@
import {IconSvgProps} from "./types";
export const MailFilledIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path
d="M17 3.5H7C4 3.5 2 5 2 8.5V15.5C2 19 4 20.5 7 20.5H17C20 20.5 22 19 22 15.5V8.5C22 5 20 3.5 17 3.5ZM17.47 9.59L14.34 12.09C13.68 12.62 12.84 12.88 12 12.88C11.16 12.88 10.31 12.62 9.66 12.09L6.53 9.59C6.21 9.33 6.16 8.85 6.41 8.53C6.67 8.21 7.14 8.15 7.46 8.41L10.59 10.91C11.35 11.52 12.64 11.52 13.4 10.91L16.53 8.41C16.85 8.15 17.33 8.2 17.58 8.53C17.84 8.85 17.79 9.33 17.47 9.59Z"
fill="currentColor"
/>
</svg>
);

9
pnpm-lock.yaml generated
View File

@ -822,6 +822,9 @@ importers:
'@nextui-org/dom-utils':
specifier: workspace:*
version: link:../../utilities/dom-utils
'@nextui-org/shared-icons':
specifier: workspace:*
version: link:../../utilities/shared-icons
'@nextui-org/shared-utils':
specifier: workspace:*
version: link:../../utilities/shared-utils
@ -837,6 +840,12 @@ importers:
'@react-aria/focus':
specifier: ^3.11.0
version: 3.11.0(react@18.2.0)
'@react-aria/i18n':
specifier: ^3.7.0
version: 3.7.0(react@18.2.0)
'@react-aria/interactions':
specifier: ^3.14.0
version: 3.14.0(react@18.2.0)
'@react-aria/utils':
specifier: ^3.15.0
version: 3.15.0(react@18.2.0)