feat: introduce NumberInput (#4475)

* feat(number-field): init structure

* feat(deps): add `@nextui-org/button` & `@react-types/button`

* feat(theme): export number-field

* feat(number-field): storybook init structure

* feat(number-field): add NumberFieldHorizontalStepper

* feat(number-field): add NumberFieldHorizontalStepper

* feat(theme): init number field theme

* feat(number-field): number-field draft

* refactor(number-field): revise stepper icons

* feat(shared-icons): add ChevronLeftIcon

* feat(theme): stepperButton styles

* feat(theme): number-field styles

* fix(number-field): label layout

* feat(number-field): vertical stepper wrapper

* feat(number-field): use-number-field (wip)

* feat(number-field): add data-direction

* feat(theme): center the text if it is horizontal stepper

* feat(number-field): add HorizontalStepper

* feat(number-field): add HideStepper

* chore(number-field): revise minValue & defaultValue

* feat(docs): init number field structure

* fix(theme): outside-left styles

* refactor(theme): remove labelPlacement styles

* refactor(number-field): remove labelContent logic

* refactor(number-field): remove labelPlacement args

* feat(number-field): helper text

* feat(number-field): revise number field stories

* feat(number-field): description

* refactor(number-field): revise number field stories

* feat(theme): numberFieldLabelClasses

* fix(number-field): incorrect button props

* fix(number-field): typing issue on stepper buttons

* chore(number-field): add aria-label

* refactor(number-field): merge props

* fix(number-field): pass originalProps instead

* chore(number-field): revise Required story args

* feat(number-field): add WithStepValue & WithWheelDisabled & revise stories

* chore(number-field): add label to Required

* feat(docs): number-field doc page

* fix(number-field): typing issue

* fix(number-field): test cases

* fix(number-field): user.keyboard & defaultValue

* fix(number-field): should work with defaultValues

* chore(number-field): add type: number

* chore(number-field): remove hidden related code

* fix(number-field): numeric value

* chore(changeset): add changeset

* feat(deps): add "@nextui-org/number-field" to docs

* feat(react): export `@nextui-org/number-field`

* feat(changeset): add @nextui-org/react

* feat(docs): number-field examples

* chore(number-field): use text instead

* refactor(number-field): remove unnecessary filled-within

* fix(number-field): test case

* chore(number-field): remove aria-label for stepper buttons

* feat(docs): add incrementAriaLabel & decrementAriaLabel to NumberField

* chore(number-field): reorder WithFormatOptions

* fix(deps): update number-field's peerDependencies & dependencies

* feat(number-field): hidden input for holding numeric vaule

* fix(docs): number field title

* feat(docs): add format options to number field

* chore(docs): revise number field content

* chore(number-field): add type to useDOMRef

* fix(number-field): clear button

* fix(theme): clear button styles

* refactor(theme): stepper button styles

* chore(number-field): accept stepperButton class

* fix(theme): helper wrapper padding

* feat(deps): add `@react-aria/i18n`

* fix(number-field): use locale from `@react-aria/i18n`

* fix(deps): dependency order

* fix(docs): incorrect command

* chore(docs): remove type=number

* chore(theme): add padding to stepper wrapper

* fix(number-field): avoid resetting value

* fix(number-field): storybook

* chore(docs): remove custom impl

* chore(docs): update docs code & content

* chore(number-field):  migrate to heroui

* chore(number-field): migrate to heroui

* chore(number-field): migrate to heroui

* chore: rename to number input

* fix(number-input): incorrect import

* chore(docs): rename to number input

* chore: change to number input

* refactor(number-input): change label to amount

* fix(docs): use heroui commands

* chore(changeset): update package name

* refactor(number-input): remove steps

* refactor: remove helper text

* feat(number-input): label placement

* refactor(number-input): rename stepper

* fix(theme): isClearable

* feat(docs): add label placements

* refactor(docs): update number-input content

* fix(docs): incorrect file

* feat(docs): add lablePlacement

* refactor(docs): remove labelPlacement & startContent

* refactor(docs): remove helperText

* refactor(docs): remove helperText

* refactor(docs): revise description

* feat(number-input): add data-slot for stepper-wrapper

* fix(number-input): test cases

* fix(docs): unexpected change

* refactor(number-input): update outdated info

* fix(docs): coderabbitai comments

* refactor: remove validationState

* fix(docs): typo

* chore(deps): remove unnecessary dep

* chore(deps): bump RA versions

* chore(number-input): apply latest labelPlacement change

* refactor(number-input): update author

* refactor(number-input): revise stepper wrapper alignment

* refactor(number-input): stepper button styles

* chore(number-input): add disableRipple

* fix(theme): increase stepper button click area

* fix(number-input): sync latest validationBehavior changes

* fix(number-input): pass validationBehavior to useAriaNumberInput

* chore(docs): add import react

* chore(number-input): remove HorizontalStepper story

* chore(number-input): enable ripple

* fix(number-input): remove number type

* refactor(theme): follow input clear button styles

* feat(theme): add color for stepperButton

* fix(theme): revise stepperButton size for outside & outside-left cases

* fix(number-input): typo

* chore(docs): update description for wheel

* chore(theme): change opacity when pressed

* chore(number-input): add disableRipple

* Update .changeset/witty-flies-reflect.md

* fix(theme): add hover opacity effect

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
աӄա 2025-02-15 04:17:18 +08:00 committed by GitHub
parent e99ddc45b9
commit 5f979617d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 4278 additions and 121 deletions

View File

@ -0,0 +1,8 @@
---
"@heroui/number-input": patch
"@heroui/shared-icons": patch
"@heroui/theme": patch
"@heroui/react": patch
---
introduce NumberInput

View File

@ -338,6 +338,13 @@
"keywords": "navbar, navigation, top menu, website header",
"path": "/docs/components/navbar.mdx"
},
{
"key": "number-input",
"title": "Number Input",
"keywords": "input, numeric input, number input",
"path": "/docs/components/number-input.mdx",
"newPost": true
},
{
"key": "pagination",
"title": "Pagination",

View File

@ -34,3 +34,4 @@ export * from "./table";
export * from "./autocomplete";
export * from "./alert";
export * from "./drawer";
export * from "./number-input";

View File

@ -0,0 +1,16 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
isClearable
className="max-w-xs"
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
variant="bordered"
// eslint-disable-next-line no-console
onClear={() => console.log("number input cleared")}
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./clear-button.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,20 @@
import {NumberInput} from "@heroui/react";
export default function App() {
const colors = ["default", "primary", "secondary", "success", "warning", "danger"];
return (
<div className="w-full flex flex-row flex-wrap gap-4">
{colors.map((color) => (
<NumberInput
key={color}
className="max-w-[220px]"
color={color}
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
/>
))}
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./colors.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,18 @@
import React from "react";
import {NumberInput} from "@heroui/react";
export default function App() {
const [value, setValue] = React.useState();
return (
<div className="w-full flex flex-col gap-2 max-w-[240px]">
<NumberInput
label="Amount"
placeholder="Enter the amount"
value={value}
onValueChange={setValue}
/>
<p className="text-default-500 text-small">NumberInput value: {value}</p>
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./controlled.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,39 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<div className="w-[340px] h-[240px] px-8 rounded-2xl flex justify-center items-center bg-gradient-to-tr from-pink-500 to-yellow-500 text-white shadow-lg">
<NumberInput
isClearable
classNames={{
label: "text-black/50 dark:text-white/90",
input: [
"bg-transparent",
"text-black/90 dark:text-white/90",
"placeholder:text-default-700/50 dark:placeholder:text-white/60",
],
innerWrapper: "bg-transparent",
inputWrapper: [
"shadow-xl",
"bg-default-200/50",
"dark:bg-default/60",
"backdrop-blur-xl",
"backdrop-saturate-200",
"hover:bg-default-200/70",
"dark:hover:bg-default/70",
"group-data-[focus=true]:bg-default-200/50",
"dark:group-data-[focus=true]:bg-default/60",
"!cursor-text",
],
helperText: "text-black/50 dark:text-white/90",
}}
description="The number of apples that Marcus bought"
helperText="Must be equal or greater than 1"
label="Number of apples"
minValue={1}
placeholder="Enter a number..."
radius="lg"
/>
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./custom-styles.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,43 @@
import React from "react";
import {Button, Form, NumberInput} from "@heroui/react";
export default function App() {
const [submitted, setSubmitted] = React.useState(null);
const onSubmit = (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
setSubmitted(data);
};
return (
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<NumberInput
isRequired
label="Amount"
name="amount"
placeholder="Enter a number"
validate={(value) => {
if (value < 100) {
return "Number must be greater than 100";
}
if (value > 1000) {
return "Number must be less than 1000";
}
return value === 777 ? "Nice try!" : null;
}}
/>
<Button color="primary" type="submit">
Submit
</Button>
{submitted && (
<div className="text-small text-default-500">
You submitted: <code>{JSON.stringify(submitted)}</code>
</div>
)}
</Form>
);
}

View File

@ -0,0 +1,9 @@
import App from "./custom-validation.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,12 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
className="max-w-xs"
defaultValue={1024}
description="Enter the amount"
label="Amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./description.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,13 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
isDisabled
aria-label="Amount"
className="max-w-xs"
defaultValue={1024}
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./disabled.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,15 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
className="max-w-xs"
defaultValue={1024}
errorMessage="Please enter a valid number"
isInvalid={true}
label="Amount"
placeholder="Enter the amount"
variant="bordered"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./error-message.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,60 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<div className="flex flex-col gap-4">
<div className="flex w-full flex-wrap md:flex-nowrap mb-6 md:mb-0 gap-4">
<NumberInput
className="max-w-xs"
defaultValue={6}
formatOptions={{
style: "currency",
currency: "USD",
}}
label="With Currency"
/>
<NumberInput
className="max-w-xs"
defaultValue={6}
formatOptions={{
signDisplay: "exceptZero",
minimumFractionDigits: 1,
maximumFractionDigits: 2,
}}
label="With Sign"
/>
<NumberInput
className="max-w-xs"
defaultValue={6}
formatOptions={{
style: "percent",
}}
label="With Percent"
/>
</div>
<div className="flex w-full flex-wrap md:flex-nowrap mb-6 md:mb-0 gap-4">
<NumberInput
className="max-w-xs"
defaultValue={6}
formatOptions={{
style: "currency",
currency: "EUR",
currencyDisplay: "code",
currencySign: "accounting",
}}
label="With Currency Vaule"
/>
<NumberInput
className="max-w-xs"
defaultValue={6}
formatOptions={{
style: "unit",
unit: "inch",
unitDisplay: "long",
}}
label="With Unit"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./format-options.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,13 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
hideStepper
className="max-w-xs"
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./hide-stepper.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,51 @@
import usage from "./usage";
import disabled from "./disabled";
import readOnly from "./readonly";
import required from "./required";
import sizes from "./sizes";
import colors from "./colors";
import variants from "./variants";
import radius from "./radius";
import description from "./description";
import isWheelDisabled from "./is-wheel-disabled";
import label from "./label";
import minValue from "./min-value";
import maxValue from "./max-value";
import hideStepper from "./hide-stepper";
import clearButton from "./clear-button";
import startEndContent from "./start-end-content";
import errorMessage from "./error-message";
import controlled from "./controlled";
import customValidation from "./custom-validation";
import realTimeValidation from "./real-time-validation";
import serverValidation from "./server-validation";
import customStyles from "./custom-styles";
import formatOptions from "./format-options";
import labelPlacements from "./label-placements";
export const numberInputContent = {
usage,
disabled,
readOnly,
required,
sizes,
colors,
variants,
radius,
description,
label,
isWheelDisabled,
minValue,
maxValue,
clearButton,
hideStepper,
startEndContent,
errorMessage,
controlled,
customValidation,
realTimeValidation,
serverValidation,
customStyles,
formatOptions,
labelPlacements,
};

View File

@ -0,0 +1,13 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
isWheelDisabled
className="max-w-xs"
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./is-wheel-disabled.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,37 @@
import {NumberInput} from "@heroui/react";
export default function App() {
const placements = ["inside", "outside", "outside-left"];
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<h3 className="text-default-500 text-small">Without placeholder</h3>
<div className="flex w-full flex-wrap items-end md:flex-nowrap mb-6 md:mb-0 gap-4">
{placements.map((placement) => (
<NumberInput
key={placement}
description={placement}
label="Amount"
labelPlacement={placement}
/>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-default-500 text-small">With placeholder</h3>
<div className="flex w-full flex-wrap items-end md:flex-nowrap mb-6 md:mb-0 gap-4">
{placements.map((placement) => (
<NumberInput
key={placement}
description={placement}
label="Amount"
labelPlacement={placement}
placeholder="Enter a number"
/>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./label-placements.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,5 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return <NumberInput className="max-w-xs" defaultValue={1024} label="Amount" />;
}

View File

@ -0,0 +1,9 @@
import App from "./label.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,14 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
hideStepper
aria-label="Amount"
className="max-w-xs"
description="The value should be less than or equal to 100"
maxValue={100}
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./max-value.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,14 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
hideStepper
aria-label="Amount"
className="max-w-xs"
description="The value should be greater than or equal to 100"
minValue={100}
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./min-value.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,21 @@
import {NumberInput} from "@heroui/react";
export default function App() {
const radius = ["full", "lg", "md", "sm", "none"];
return (
<div className="w-full flex flex-row flex-wrap gap-4">
{radius.map((r) => (
<NumberInput
key={r}
aria-label={`${r} radius`}
className="max-w-[220px]"
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
radius={r}
/>
))}
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./radius.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,14 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
isReadOnly
aria-label="Amount"
className="max-w-xs"
defaultValue={1024}
placeholder="Enter the amount"
variant="bordered"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./readonly.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,54 @@
import {Button, Form, NumberInput} from "@heroui/react";
export default function App() {
const [submitted, setSubmitted] = React.useState(null);
const [amount, setAmount] = React.useState(null);
const errors = [];
const onSubmit = (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
setSubmitted(data);
};
if (!amount) {
errors.push("The value must not be empty");
}
if (amount < 100) {
errors.push("The value must be greater than 100");
}
if (amount > 1000) {
errors.push("The value must be less than 1000");
}
return (
<Form className="w-full max-w-xs" onSubmit={onSubmit}>
<NumberInput
errorMessage={() => (
<ul>
{errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
)}
isInvalid={errors.length > 0}
label="Amount"
name="amount"
placeholder="Enter a number"
value={amount}
onValueChange={setAmount}
/>
<Button color="primary" type="submit">
Submit
</Button>
{submitted && (
<div className="text-small text-default-500">
You submitted: <code>{JSON.stringify(submitted)}</code>
</div>
)}
</Form>
);
}

View File

@ -0,0 +1,9 @@
import App from "./real-time-validation.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,13 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<NumberInput
isRequired
className="max-w-xs"
defaultValue={1024}
label="Amount"
placeholder="Enter the amount"
/>
);
}

View File

@ -0,0 +1,9 @@
import App from "./required.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,43 @@
import {Button, Form, NumberInput} from "@heroui/react";
export default function App() {
const [isLoading, setIsLoading] = React.useState(false);
const [errors, setErrors] = React.useState({});
const onSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
const data = Object.fromEntries(new FormData(e.currentTarget));
const result = await callServer(data);
setErrors(result.errors);
setIsLoading(false);
};
return (
<Form className="w-full max-w-xs" validationErrors={errors} onSubmit={onSubmit}>
<NumberInput
isRequired
isDisabled={isLoading}
label="Amount"
name="amount"
placeholder="Enter a number"
/>
<Button color="primary" isLoading={isLoading} type="submit">
Submit
</Button>
</Form>
);
}
// Fake server used in this example.
async function callServer(_) {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
errors: {
amount: "Sorry, this amount is not valid.",
},
};
}

View File

@ -0,0 +1,9 @@
import App from "./server-validation.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,15 @@
import {NumberInput} from "@heroui/react";
export default function App() {
const sizes = ["sm", "md", "lg"];
return (
<div className="w-full flex flex-col gap-4">
{sizes.map((size) => (
<div key={size} className="flex w-full flex-wrap md:flex-nowrap mb-6 md:mb-0 gap-4">
<NumberInput label="Amount" placeholder="Enter the amount" size={size} />
</div>
))}
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./sizes.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,47 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return (
<div className="flex flex-col gap-4">
<div className="flex w-full flex-wrap md:flex-nowrap mb-6 md:mb-0 gap-4">
<NumberInput
label="Price"
placeholder="0.00"
startContent={
<div className="pointer-events-none flex items-center">
<span className="text-default-400 text-small">$</span>
</div>
}
/>
<NumberInput
endContent={
<div className="flex items-center">
<label className="sr-only" htmlFor="currency">
Currency
</label>
<select
aria-label="Select currency"
className="outline-none border-0 bg-transparent text-default-400 text-small"
defaultValue="USD"
id="currency"
name="currency"
>
<option aria-label="US Dollar" value="USD">
USD
</option>
<option aria-label="Argentine Peso" value="ARS">
ARS
</option>
<option aria-label="Euro" value="EUR">
EUR
</option>
</select>
</div>
}
label="Price"
placeholder="0.00"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./start-end-content.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,5 @@
import {NumberInput} from "@heroui/react";
export default function App() {
return <NumberInput className="max-w-xs" placeholder="Enter the amount" />;
}

View File

@ -0,0 +1,9 @@
import App from "./usage.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,16 @@
import {NumberInput} from "@heroui/react";
export default function App() {
const variants = ["flat", "bordered", "underlined", "faded"];
return (
<div className="w-full flex flex-col gap-4">
{variants.map((variant) => (
<div key={variant} className="flex w-full flex-wrap md:flex-nowrap mb-6 md:mb-0 gap-4">
<NumberInput label="Amount" variant={variant} />
<NumberInput label="Amount" placeholder="Enter the amount" variant={variant} />
</div>
))}
</div>
);
}

View File

@ -0,0 +1,9 @@
import App from "./variants.raw.jsx?raw";
const react = {
"/App.jsx": App,
};
export default {
...react,
};

View File

@ -0,0 +1,473 @@
---
title: "Number Input"
description: "The numeric input component is designed for users to enter a number, and increase or decrease the value using stepper buttons"
---
import {numberInputContent} from "@/content/components/number-input";
# Number Input
The numeric input component is designed for users to enter a number, and increase or decrease the value using stepper buttons
<ComponentLinks component="number-input" reactAriaHook="useNumberField" />
---
<CarbonAd/>
## Installation
<PackageManagers
showGlobalInstallWarning
commands={{
cli: "npx heroui-cli@latest add number-input",
npm: "npm install @heroui/number-input",
yarn: "yarn add @heroui/number-input",
pnpm: "pnpm add @heroui/number-input",
bun: "bun add @heroui/number-input"
}}
/>
## Usage
<CodeDemo title="Usage" files={numberInputContent.usage} />
### Disabled
<CodeDemo title="Disabled" files={numberInputContent.disabled} />
### Read Only
<CodeDemo title="Read Only" files={numberInputContent.readOnly} />
### Required
If you pass the `isRequired` property to the input, it will have a `danger` asterisk at
the end of the label and the input will be required.
<CodeDemo title="Required" files={numberInputContent.required} />
### Sizes
<CodeDemo title="Sizes" files={numberInputContent.sizes} />
### Colors
<CodeDemo title="Colors" files={numberInputContent.colors} />
### Variants
<CodeDemo title="Variants" files={numberInputContent.variants} />
### Radius
<CodeDemo title="Radius" files={numberInputContent.radius} />
### Label Placements
You can change the position of the label by setting the `labelPlacement` property to `inside`, `outside` or `outside-left`.
<CodeDemo title="Label Placements" files={numberInputContent.labelPlacements} />
> **Note**: If the `label` is not passed, the `labelPlacement` property will be `outside` by default.
### Clear Button
If you pass the `isClearable` property to the input, it will have a clear button at the
end of input, it will be visible when input has a value.
<CodeDemo title="Clear Button" files={numberInputContent.clearButton} />
### Hide Stepper
You can hide the stepper buttons by passing the `hideStepper` property.
<CodeDemo title="Hide Stepper" files={numberInputContent.hideStepper} />
### Start & End Content
You can use the `startContent` and `endContent` properties to add content to the start and end of NumberInput.
<CodeDemo title="Start and End Content" files={numberInputContent.startEndContent} />
### With Label
You can add a label to the input by passing the `label` property.
<CodeDemo title="With Label" files={numberInputContent.label} />
### With Description
You can add a description to the input by passing the `description` property.
<CodeDemo title="With Description" files={numberInputContent.description} />
### With Min Value
You can set the minimum value of the input by passing the `minValue` property.
<CodeDemo title="With Min Value" files={numberInputContent.minValue} />
### With Max Value
You can set the maximum value of the input by passing the `maxValue` property.
<CodeDemo title="With Max Value" files={numberInputContent.maxValue} />
### With Wheel Disabled
By default, you can increase or decrease the value with scroll wheel. You can disable changing the vaule with scroll in NumberInput by passing the `isWheelDisabled` property.
<CodeDemo title="With Wheel Disabled" files={numberInputContent.isWheelDisabled} />
### With Format Options
You can format the value of the input by passing the `formatOptions` property.
<CodeDemo title="With Format Options" files={numberInputContent.formatOptions} />
### With Error Message
You can combine the `isInvalid` and `errorMessage` properties to show an invalid input. `errorMessage` is only shown when `isInvalid` is set to `true`.
<CodeDemo title="With Error Message" files={numberInputContent.errorMessage} />
### Controlled
You can use the `value` and `onValueChange` properties to control the input value.
<CodeDemo title="Controlled" files={numberInputContent.controlled} />
> **Note**: HeroUI `NumberInput` also supports native events like `onChange`, useful for form libraries
> such as [Formik](https://formik.org/) and [React Hook Form](https://react-hook-form.com/).
### With Form
`NumberInput` can be used with a `Form` component to leverage form state management. For more on form and validation behaviors, see the [Forms](/docs/guide/forms) guide.
#### Custom Validation
In addition to built-in constraints, you can provide a function to the `validate` property for custom validation.
<CodeDemo title="Custom Validation" files={numberInputContent.customValidation} />
#### Realtime Validation
If you want to display validation errors while the user is typing, you can control the field value and use the `isInvalid` prop along with the `errorMessage` prop.
<CodeDemo title="Realtime Validation" files={numberInputContent.realTimeValidation} />
#### Server Validation
Client-side validation provides immediate feedback, but you should also validate data on the server to ensure accuracy and security.
HeroUI allows you to display server-side validation errors by using the `validationErrors` prop in the `Form` component.
This prop should be an object where each key is the field `name` and the value is the error message.
<CodeDemo title="Server Validation" files={numberInputContent.serverValidation} />
## Slots
- **base**: Input wrapper, it handles alignment, placement, and general appearance.
- **label**: Label of the input, it is the one that is displayed above, inside or left of the input.
- **mainWrapper**: Wraps the `inputWrapper`
- **inputWrapper**: Wraps the `label` (when it is inside) and the `innerWrapper`.
- **innerWrapper**: Wraps the `input`, the `startContent` and the `endContent`.
- **input**: The input element.
- **clearButton**: The clear button, it is at the end of the input.
- **stepperButton**: The stepper button to increase or decrease the value.
- **stepperWrapper**: The wrapper for the stepper.
- **description**: The description of NumberInput.
- **errorMessage**: The error message of NumberInput.
### Custom Styles
You can customize the `NumberInput` component by passing custom Tailwind CSS classes to the component slots.
<CodeDemo title="Custom Styles" files={numberInputContent.customStyles} />
<Spacer y={4} />
## Data Attributes
`NumberInput` has the following attributes on the `base` element:
- **data-invalid**:
When the input is invalid. Based on `isInvalid` prop.
- **data-required**:
When the input is required. Based on `isRequired` prop.
- **data-readonly**:
When the input is readonly. Based on `isReadOnly` prop.
- **data-hover**:
When the input is being hovered. Based on [useHover](https://react-spectrum.adobe.com/react-aria/useHover.html)
- **data-focus**:
When the input is being focused. Based on [useFocusRing](https://react-spectrum.adobe.com/react-aria/useFocusRing.html).
- **data-focus-within**:
When the input is being focused or any of its children. Based on [useFocusWithin](https://react-spectrum.adobe.com/react-aria/useFocusWithin.html).
- **data-focus-visible**:
When the input is being focused with the keyboard. Based on [useFocusRing](https://react-spectrum.adobe.com/react-aria/useFocusRing.html).
- **data-disabled**:
When the input is disabled. Based on `isDisabled` prop.
- **data-filled**:
When the input has content, placeholder, start content or the placeholder is shown.
- **data-has-elements**:
When the input has any element (label, helper text, description, error message).
- **data-has-helper**:
When the input has helper text.
- **data-has-description**:
When the input has a description.
- **data-has-label**:
When the input has a label.
- **data-has-value**:
When the input has a value (placeholder is not shown).
<Spacer y={4} />
## Accessibility
- Built with a native `<input>` element with `type="number"`.
- Visual and ARIA labeling support.
- Change, clipboard, composition, selection, and input event support.
- Required and invalid states exposed to assistive technology via ARIA.
- Support for description, helper text, and error message linked to the input via ARIA.
<Spacer y={4} />
## API
### NumberInput Props
<APITable
data={[
{
attribute: "children",
type: "ReactNode",
description: "The content of the input.",
default: "-"
},
{
attribute: "variant",
type: "flat | bordered | faded | underlined",
description: "The variant of the input.",
default: "flat"
},
{
attribute: "color",
type: "default | primary | secondary | success | warning | danger",
description: "The color of the input.",
default: "default"
},
{
attribute: "size",
type: "sm | md | lg",
description: "The size of the input.",
default: "md"
},
{
attribute: "radius",
type: "none | sm | md | lg | full",
description: "The radius of the input.",
default: "-"
},
{
attribute: "name",
type: "string",
description: "The name of the input element, used when submitting an HTML form.",
default: "-"
},
{
attribute: "label",
type: "ReactNode",
description: "The content to display as the label.",
default: "-"
},
{
attribute: "description",
type: "ReactNode",
description: "A description for the input. Provides a description for the input.",
default: "-"
},
{
attribute: "value",
type: "string",
description: "The current value of the input (controlled).",
default: "-"
},
{
attribute: "defaultValue",
type: "string",
description: "The default value of the input (uncontrolled).",
default: "-"
},
{
attribute: "placeholder",
type: "string",
description: "The placeholder of the input.",
default: "-"
},
{
attribute: "errorMessage",
type: "ReactNode | ((v: ValidationResult) => ReactNode)",
description: "An error message for the input. It is only shown when isInvalid is set to true",
default: "-"
},
{
attribute: "validate",
type: "(value: string) => ValidationError | true | null | undefined",
description: "Validate input values when committing (e.g. on blur), returning error messages for invalid values.",
default: "-"
},
{
attribute: "validationBehavior",
type: "native | aria",
description: "Whether to use native HTML form validation or ARIA validation. When wrapped in a Form component, the default is `aria`. Otherwise, the default is `native`.",
default: "native"
},
{
attribute: "minValue",
type: "number",
description: "The minimum value of the input.",
default: "-"
},
{
attribute: "maxValue",
type: "number",
description: "The maximum value of the input.",
default: "-"
},
{
attribute: "formatOptions",
type: "Intl.NumberFormatOptions",
description: "The format options for the input.",
default: "-"
},
{
attribute: "step",
type: "number",
description: "The amount that the input value changes with each increment or decrement tick.",
default: "1"
},
{
attribute: "hideStepper",
type: "boolean",
description: "Whether the stepper buttons should be hidden.",
default: "-"
},
{
attribute: "isWheelDisabled",
type: "boolean",
description: "Whether the wheel should be disabled.",
default: "-"
},
{
attribute: "startContent",
type: "ReactNode",
description: "Element to be rendered in the left side of the input.",
default: "-"
},
{
attribute: "endContent",
type: "ReactNode",
description: "Element to be rendered in the right side of the input.",
default: "-"
},
{
attribute: "labelPlacement",
type: "inside | outside | outside-left",
description: "The position of the label.",
default: "inside"
},
{
attribute: "fullWidth",
type: "boolean",
description: "Whether the input should take up the width of its parent.",
default: "true"
},
{
attribute: "isClearable",
type: "boolean",
description: "Whether the input should have a clear button.",
default: "false"
},
{
attribute: "isRequired",
type: "boolean",
description: "Whether user input is required on the input before form submission.",
default: "false"
},
{
attribute: "isReadOnly",
type: "boolean",
description: "Whether the input can be selected but not changed by the user.",
default: "false"
},
{
attribute: "isDisabled",
type: "boolean",
description: "Whether the input is disabled.",
default: "false"
},
{
attribute: "isInvalid",
type: "boolean",
description: "Whether the input is invalid.",
default: "false"
},
{
attribute: "incrementAriaLabel",
type: "string",
description: "A custom aria-label for the increment button. If not provided, the localized string `Increment` is used.",
default: "-"
},
{
attribute: "decrementAriaLabel",
type: "string",
description: "A custom aria-label for the decrement button. If not provided, the localized string `Decrement` is used.",
default: "-"
},
{
attribute: "baseRef",
type: "RefObject<HTMLDivElement>",
description: "The ref to the base element.",
default: "-"
},
{
attribute: "disableAnimation",
type: "boolean",
description: "Whether the input should be animated.",
default: "false"
},
{
attribute: "classNames",
type: "Partial<Record<'base' | 'label' | 'inputWrapper' | 'innerWrapper' | 'mainWrapper' | 'input' | 'clearButton' | 'stepperButton' | 'helperWrapper' | 'stepperWrapper' | 'description' | 'errorMessage', string>>",
description: "Allows to set custom class names for the Input slots.",
default: "-"
}
]}
/>
### NumberInput Events
<APITable
data={[
{
attribute: "onChange",
type: "React.ChangeEvent<HTMLInputElement>",
description: "Handler that is called when the element's value changes. You can pull out the new value by accessing event.target.value (string).",
default: "-"
},
{
attribute: "onValueChange",
type: "(value: number) => void",
description: "Handler that is called when the element's value changes.",
default: "-"
},
{
attribute: "onClear",
type: "() => void",
description: "Handler that is called when the clear button is clicked.",
default: "-"
}
]}
/>

View File

@ -232,7 +232,7 @@ You can change the form defaults for your entire app using [HeroUI Provider](/do
Supported constraints include:
- `isRequired` indicates that a field must have a value before the form can be submitted.
- `minValue` and `maxValue` specify the minimum and maximum value in a date picker or number field.
- `minValue` and `maxValue` specify the minimum and maximum value in a date picker or number input.
- `minLength` and `maxLength` specify the minimum and length of text input.
- `pattern` provides a custom regular expression that a text input must conform to.
- `type="email"` and `type="url"` provide built-in validation for email addresses and URLs.

View File

@ -0,0 +1,24 @@
# @heroui/number-input
NumberInput is a component that allows users to enter number. It can be used to get user inputs in forms, search fields, and more.
Please refer to the [documentation](https://heroui.com/docs/components/number-input) for more information.
## Installation
```sh
yarn add @heroui/number-input
# or
npm i @heroui/number-input
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/heroui-inc/heroui/blob/master/CONTRIBUTING.md)
for details.
## License
This project is licensed under the terms of the
[MIT license](https://github.com/heroui-inc/heroui/blob/master/LICENSE).

View File

@ -0,0 +1,510 @@
import * as React from "react";
import {render, renderHook, fireEvent, act} from "@testing-library/react";
import userEvent, {UserEvent} from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Form} from "@heroui/form";
import {NumberInput} from "../src";
describe("NumberInput", () => {
let user: UserEvent;
beforeEach(() => {
user = userEvent.setup();
});
it("should render correctly", () => {
const wrapper = render(<NumberInput label="test number input" />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLInputElement>();
render(<NumberInput ref={ref} label="test number input" />);
expect(ref.current).not.toBeNull();
});
it("should have aria-invalid when invalid", () => {
const {container} = render(<NumberInput isInvalid={true} label="test number input" />);
expect(container.querySelector("input")).toHaveAttribute("aria-invalid", "true");
});
it("should have aria-readonly when isReadOnly", () => {
const {container} = render(<NumberInput isReadOnly label="test number input" />);
expect(container.querySelector("input")).toHaveAttribute("aria-readonly", "true");
});
it("should have disabled attribute when isDisabled", () => {
const {container} = render(<NumberInput isDisabled label="test number input" />);
expect(container.querySelector("input")).toHaveAttribute("disabled");
});
it("should disable the clear button when isDisabled", () => {
const {getByRole} = render(
<NumberInput hideStepper isClearable isDisabled label="test number input" />,
);
const clearButton = getByRole("button");
expect(clearButton).toBeDisabled();
});
it("should not allow clear button to be focusable", () => {
const {getByRole} = render(<NumberInput hideStepper isClearable label="test number input" />);
const clearButton = getByRole("button");
expect(clearButton).toHaveAttribute("tabIndex", "-1");
});
it("should have required attribute when isRequired with native validationBehavior", () => {
const {container} = render(
<NumberInput isRequired label="test number input" validationBehavior="native" />,
);
expect(container.querySelector("input")).toHaveAttribute("required");
expect(container.querySelector("input")).not.toHaveAttribute("aria-required");
});
it("should have aria-required attribute when isRequired with aria validationBehavior", () => {
const {container} = render(
<NumberInput isRequired label="test number input" validationBehavior="aria" />,
);
expect(container.querySelector("input")).not.toHaveAttribute("required");
expect(container.querySelector("input")).toHaveAttribute("aria-required", "true");
});
it("should have aria-describedby when description is provided", () => {
const {container} = render(<NumberInput description="description" label="test number input" />);
expect(container.querySelector("input")).toHaveAttribute("aria-describedby");
});
it("should have aria-describedby when errorMessage is provided", () => {
const {container} = render(
<NumberInput isInvalid errorMessage="error text" label="test number input" />,
);
expect(container.querySelector("input")).toHaveAttribute("aria-describedby");
});
it("should have the same aria-labelledby as label id", () => {
const {container} = render(<NumberInput label="test number input" />);
const labelId = container.querySelector("label")?.id;
const labelledBy = container.querySelector("input")?.getAttribute("aria-labelledby");
expect(labelledBy?.includes(labelId as string)).toBeTruthy();
});
it("should call dom event handlers only once", () => {
const onFocus = jest.fn();
const {container} = render(<NumberInput label="test number input" onFocus={onFocus} />);
act(() => {
container.querySelector("input")?.focus();
container.querySelector("input")?.blur();
expect(onFocus).toHaveBeenCalledTimes(1);
});
});
it("ref should update the value", () => {
const ref = React.createRef<HTMLInputElement>();
const {container} = render(<NumberInput ref={ref} label="test number input" />);
if (!ref.current) {
throw new Error("ref is null");
}
const value = "1234";
ref.current!.value = value;
act(() => {
container.querySelector("input")?.focus();
expect(ref.current?.value)?.toBe(value);
});
});
it("should clear the value and onClear is triggered", async () => {
const onClear = jest.fn();
const ref = React.createRef<HTMLInputElement>();
const {getByRole} = render(
<NumberInput
ref={ref}
hideStepper
isClearable
defaultValue={1234}
label="test number-input"
onClear={onClear}
/>,
);
const clearButton = getByRole("button")!;
expect(clearButton).not.toBeNull();
const user = userEvent.setup();
await user.click(clearButton);
expect(ref.current?.value)?.toBe("");
expect(onClear).toHaveBeenCalledTimes(1);
});
it("should disable clear button when isReadOnly is true", async () => {
const onClear = jest.fn();
const ref = React.createRef<HTMLInputElement>();
const {getByRole} = render(
<NumberInput
ref={ref}
hideStepper
isClearable
isReadOnly
defaultValue={1234}
label="test number-input"
onClear={onClear}
/>,
);
const clearButton = getByRole("button")!;
expect(clearButton).not.toBeNull();
const user = userEvent.setup();
await user.click(clearButton);
expect(onClear).toHaveBeenCalledTimes(0);
});
it("should reset to max value if the value exceeds", async () => {
const {container} = render(
<NumberInput isInvalid={true} label="test number input" maxValue={100} />,
);
const input = container.querySelector("input") as HTMLInputElement;
await user.click(input);
await user.keyboard("1024");
await user.tab();
expect(input).toHaveValue("100");
});
it("should reset to min value if the value subceed", async () => {
const {container} = render(
<NumberInput isInvalid={true} label="test number input" minValue={100} />,
);
const input = container.querySelector("input") as HTMLInputElement;
await user.click(input);
await user.keyboard("50");
await user.tab();
expect(input).toHaveValue("100");
});
it("should render stepper", async () => {
const {container} = render(<NumberInput isInvalid={true} label="test number input" />);
const stepperButton = container.querySelector("[data-slot='stepper-wrapper'] button")!;
expect(stepperButton).not.toBeNull();
});
it("should hide stepper", async () => {
const {container} = render(
<NumberInput hideStepper isInvalid={true} label="test number input" />,
);
const stepperButton = container.querySelector("[data-slot='stepper-wrapper'] button")!;
expect(stepperButton).toBeNull();
});
});
describe("NumberInput with React Hook Form", () => {
let input1: HTMLInputElement;
let input2: HTMLInputElement;
let input3: HTMLInputElement;
let submitButton: HTMLButtonElement;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: 1234,
withoutDefaultValue: undefined,
requiredField: undefined,
},
}),
);
const {
handleSubmit,
register,
formState: {errors},
} = result.current;
onSubmit = jest.fn();
render(
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<NumberInput isClearable label="With default value" {...register("withDefaultValue")} />
<NumberInput
data-testid="input-2"
label="Without default value"
{...register("withoutDefaultValue")}
/>
<NumberInput
data-testid="input-3"
label="Required"
{...register("requiredField", {required: true})}
/>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button type="submit">Submit</button>
</form>,
);
input1 = document.querySelector("input[name=withDefaultValue]")!;
input2 = document.querySelector("input[name=withoutDefaultValue]")!;
input3 = document.querySelector("input[name=requiredField]")!;
submitButton = document.querySelector('button[type="submit"]')!;
});
it("should work with defaultValues", () => {
expect(input1).toHaveValue("1234");
expect(input2).not.toHaveValue();
expect(input3).not.toHaveValue();
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
fireEvent.change(input3, {target: {value: 123}});
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
describe("validation", () => {
let user: UserEvent;
beforeEach(() => {
user = userEvent.setup();
});
describe("validationBehavior=native", () => {
it("supports isRequired", async () => {
const {getByTestId} = render(
<Form data-testid="form" validationBehavior="native">
<NumberInput isRequired data-testid="input" label="Name" />
</Form>,
);
const input = getByTestId("input") as HTMLInputElement;
expect(input).toHaveAttribute("required");
expect(input).not.toHaveAttribute("aria-required");
expect(input).not.toHaveAttribute("aria-describedby");
expect(input.validity.valid).toBe(false);
act(() => {
(getByTestId("form") as HTMLFormElement).checkValidity();
});
expect(document.activeElement).toBe(input);
expect(input).toHaveAttribute("aria-describedby");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Constraints not satisfied",
);
await user.keyboard("1234");
expect(input).toHaveAttribute("aria-describedby");
expect(input.validity.valid).toBe(true);
await user.tab();
expect(input).not.toHaveAttribute("aria-describedby");
});
it("supports validate function", async () => {
const {getByTestId} = render(
<Form data-testid="form" validationBehavior="native">
<NumberInput
data-testid="input"
defaultValue={1234}
label="Name"
validate={(v) => (v === 1234 ? "Invalid amount" : null)}
/>
</Form>,
);
const input = getByTestId("input") as HTMLInputElement;
expect(input).not.toHaveAttribute("aria-describedby");
expect(input.validity.valid).toBe(false);
act(() => {
(getByTestId("form") as HTMLFormElement).checkValidity();
});
expect(document.activeElement).toBe(input);
expect(input).toHaveAttribute("aria-describedby");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid amount",
);
await user.keyboard("4321");
await user.tab();
act(() => {
(getByTestId("form") as HTMLFormElement).checkValidity();
});
expect(input.validity.valid).toBe(true);
expect(input).not.toHaveAttribute("aria-describedby");
});
it("supports server validation", async () => {
function Test() {
let [serverErrors, setServerErrors] = React.useState({});
let onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setServerErrors({
name: "Invalid amount.",
});
};
return (
<Form
data-testid="form"
validationBehavior="native"
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<NumberInput data-testid="input" label="Name" name="name" />
<button data-testid="submit" type="submit">
Submit
</button>
</Form>
);
}
const {getByTestId} = render(<Test />);
const input = getByTestId("input") as HTMLInputElement;
const submitButton = getByTestId("submit");
expect(input).not.toHaveAttribute("aria-describedby");
await user.click(submitButton);
act(() => {
(getByTestId("form") as HTMLFormElement).checkValidity();
});
expect(input).toHaveAttribute("aria-describedby");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid amount.",
);
expect(input.validity.valid).toBe(false);
// Clicking twice doesn't clear server errors.
await user.click(submitButton);
act(() => {
(getByTestId("form") as HTMLFormElement).checkValidity();
});
expect(document.activeElement).toBe(input);
expect(input).toHaveAttribute("aria-describedby");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid amount.",
);
expect(input.validity.valid).toBe(false);
await user.keyboard("1234");
await user.tab();
expect(input).not.toHaveAttribute("aria-describedby");
expect(input.validity.valid).toBe(true);
});
});
describe('validationBehavior="aria"', () => {
it("supports validate function", async () => {
const {getByTestId} = render(
<Form data-testid="form" validationBehavior="aria">
<NumberInput
data-testid="input"
defaultValue={1234}
label="Amount"
validate={(v) => (v === 1234 ? "Invalid amount" : null)}
/>
</Form>,
);
const input = getByTestId("input") as HTMLInputElement;
expect(input).toHaveAttribute("aria-describedby");
expect(input).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid amount",
);
expect(input.validity.valid).toBe(true);
await user.tab();
await user.keyboard("1234");
});
it("supports server validation", async () => {
const {getByTestId} = render(
<Form validationBehavior="aria" validationErrors={{name: "Invalid amount"}}>
<NumberInput data-testid="input" label="Name" name="name" />
</Form>,
);
const input = getByTestId("input");
expect(input).toHaveAttribute("aria-describedby");
expect(input).toHaveAttribute("aria-invalid", "true");
expect(document.getElementById(input.getAttribute("aria-describedby")!)).toHaveTextContent(
"Invalid amount",
);
await user.tab();
await user.keyboard("1234");
await user.tab();
});
});
});
});

View File

@ -0,0 +1,71 @@
{
"name": "@heroui/number-input",
"version": "2.0.0",
"description": "The numeric input component is designed for users to enter a number, and increase or decrease the value using stepper buttons",
"keywords": [
"input",
"number",
"numeric input"
],
"author": "HeroUI <support@heroui.com>",
"homepage": "https://heroui.com",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/heroui-inc/heroui.git",
"directory": "packages/components/number-input"
},
"bugs": {
"url": "https://github.com/heroui-inc/heroui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "pnpm build:fast --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0",
"@heroui/theme": ">=2.4.7",
"@heroui/system": ">=2.4.8"
},
"dependencies": {
"@heroui/form": "workspace:*",
"@heroui/button": "workspace:*",
"@heroui/react-utils": "workspace:*",
"@heroui/shared-icons": "workspace:*",
"@heroui/shared-utils": "workspace:*",
"@heroui/use-safe-layout-effect": "workspace:*",
"@react-aria/focus": "3.19.1",
"@react-aria/i18n": "3.12.5",
"@react-aria/interactions": "3.23.0",
"@react-aria/numberfield": "3.11.10",
"@react-aria/utils": "3.27.0",
"@react-stately/utils": "3.10.5",
"@react-stately/numberfield": "3.9.9",
"@react-types/shared": "3.27.0",
"@react-types/numberfield": "3.8.8",
"@react-types/button": "3.10.2"
},
"devDependencies": {
"@heroui/system": "workspace:*",
"@heroui/theme": "workspace:*",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}

View File

@ -0,0 +1,10 @@
import NumberInput from "./number-input";
// export types
export type {NumberInputProps} from "./number-input";
// export hooks
export {useNumberInput} from "./use-number-input";
// export component
export {NumberInput};

View File

@ -0,0 +1,21 @@
import type {AriaButtonProps} from "@react-types/button";
import type {ButtonProps} from "@heroui/button";
import {Button} from "@heroui/button";
import {ChevronUpIcon, ChevronDownIcon} from "@heroui/shared-icons";
export interface NumberInputStepperProps extends Omit<ButtonProps, keyof AriaButtonProps> {
direction: "up" | "down";
}
const NumberInputStepper = ({direction, ...otherProps}: NumberInputStepperProps) => {
return (
<Button disableRipple isIconOnly {...otherProps}>
{direction == "up" ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
);
};
NumberInputStepper.displayName = "HeroUI.NumberInputStepper";
export default NumberInputStepper;

View File

@ -0,0 +1,146 @@
import {CloseFilledIcon} from "@heroui/shared-icons";
import {useMemo} from "react";
import {forwardRef} from "@heroui/system";
import {UseNumberInputProps, useNumberInput} from "./use-number-input";
import NumberInputStepper from "./number-input-stepper";
export interface NumberInputProps extends UseNumberInputProps {}
const NumberInput = forwardRef<"input", NumberInputProps>((props, ref) => {
const {
Component,
label,
description,
isClearable,
startContent,
endContent,
labelPlacement,
hasHelper,
isOutsideLeft,
shouldLabelBeOutside,
errorMessage,
isInvalid,
hideStepper,
getBaseProps,
getLabelProps,
getNumberInputProps,
getHiddenNumberInputProps,
getInnerWrapperProps,
getInputWrapperProps,
getMainWrapperProps,
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
getStepperIncreaseButtonProps,
getStepperDecreaseButtonProps,
getStepperWrapperProps,
} = useNumberInput({...props, ref});
const labelContent = label ? <label {...getLabelProps()}>{label}</label> : null;
const end = useMemo(() => {
if (isClearable) {
return (
<>
<button {...getClearButtonProps()}>
<CloseFilledIcon />
</button>
{endContent}
</>
);
}
return endContent;
}, [isClearable, getClearButtonProps]);
const helperWrapper = useMemo(() => {
const shouldShowError = isInvalid && errorMessage;
const hasContent = shouldShowError || description;
if (!hasHelper || !hasContent) return null;
return (
<div {...getHelperWrapperProps()}>
{shouldShowError ? (
<div {...getErrorMessageProps()}>{errorMessage}</div>
) : (
<div {...getDescriptionProps()}>{description}</div>
)}
</div>
);
}, [
hasHelper,
isInvalid,
errorMessage,
description,
getHelperWrapperProps,
getErrorMessageProps,
getDescriptionProps,
]);
const innerWrapper = useMemo(() => {
return (
<div {...getInnerWrapperProps()}>
{startContent}
<input {...getNumberInputProps()} />
<input {...getHiddenNumberInputProps()} />
{end}
{!hideStepper && (
<div {...getStepperWrapperProps()}>
<NumberInputStepper {...getStepperIncreaseButtonProps()} direction="up" />
<NumberInputStepper {...getStepperDecreaseButtonProps()} direction="down" />
</div>
)}
</div>
);
}, [startContent, end, getNumberInputProps, getInnerWrapperProps]);
const mainWrapper = useMemo(() => {
if (shouldLabelBeOutside) {
return (
<div {...getMainWrapperProps()}>
<div {...getInputWrapperProps()}>
{!isOutsideLeft ? labelContent : null}
{innerWrapper}
</div>
{helperWrapper}
</div>
);
}
return (
<>
<div {...getInputWrapperProps()}>
{labelContent}
{innerWrapper}
</div>
{helperWrapper}
</>
);
}, [
labelPlacement,
helperWrapper,
shouldLabelBeOutside,
labelContent,
innerWrapper,
errorMessage,
description,
getMainWrapperProps,
getInputWrapperProps,
getErrorMessageProps,
getDescriptionProps,
]);
return (
<Component {...getBaseProps()}>
{isOutsideLeft ? labelContent : null}
{mainWrapper}
</Component>
);
});
NumberInput.displayName = "HeroUI.NumberInput";
export default NumberInput;

View File

@ -0,0 +1,562 @@
import type {NumberInputVariantProps, SlotsToClasses, NumberInputSlots} from "@heroui/theme";
import type {AriaNumberFieldProps} from "@react-types/numberfield";
import type {NumberFieldStateOptions} from "@react-stately/numberfield";
import type {HTMLHeroUIProps, PropGetter} from "@heroui/system";
import {useLabelPlacement, mapPropsVariants, useProviderContext} from "@heroui/system";
import {useSafeLayoutEffect} from "@heroui/use-safe-layout-effect";
import {useFocusRing} from "@react-aria/focus";
import {numberInput} from "@heroui/theme";
import {useDOMRef, filterDOMProps} from "@heroui/react-utils";
import {useFocusWithin, useHover, usePress} from "@react-aria/interactions";
import {useLocale} from "@react-aria/i18n";
import {clsx, dataAttr, isEmpty, objectToDeps} from "@heroui/shared-utils";
import {useNumberFieldState} from "@react-stately/numberfield";
import {useNumberField as useAriaNumberInput} from "@react-aria/numberfield";
import {useMemo, Ref, useCallback, useState} from "react";
import {chain, mergeProps} from "@react-aria/utils";
import {FormContext, useSlottedContext} from "@heroui/form";
export interface Props extends Omit<HTMLHeroUIProps<"input">, keyof NumberInputVariantProps> {
/**
* Ref to the DOM node.
*/
ref?: Ref<HTMLInputElement>;
/**
* Ref to the container DOM node.
*/
baseRef?: Ref<HTMLDivElement>;
/**
* Ref to the input wrapper DOM node.
* This is the element that wraps the input label and the innerWrapper when the labelPlacement="inside"
* and the input has start/end content.
*/
wrapperRef?: Ref<HTMLDivElement>;
/**
* Ref to the input inner wrapper DOM node.
* This is the element that wraps the input and the start/end content when passed.
*/
innerWrapperRef?: Ref<HTMLDivElement>;
/**
* 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;
/**
* Classname or List of classes to change the classNames of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Input classNames={{
* base:"base-classes",
* label: "label-classes",
* mainWrapper: "main-wrapper-classes",
* inputWrapper: "input-wrapper-classes",
* innerWrapper: "inner-wrapper-classes",
* input: "input-classes",
* clearButton: "clear-button-classes",
* helperWrapper: "helper-wrapper-classes",
* stepperWrapper: "stepper-wrapper-classes",
* description: "description-classes",
* errorMessage: "error-message-classes",
* }} />
* ```
*/
classNames?: SlotsToClasses<NumberInputSlots>;
/**
* Whether to hide the increment and decrement buttons.
*/
hideStepper?: boolean;
/**
* Callback fired when the value is cleared.
* if you pass this prop, the clear button will be shown.
*/
onClear?: () => void;
/**
* React aria onChange event.
*/
onValueChange?: AriaNumberFieldProps["onChange"];
}
export type UseNumberInputProps = Props &
Omit<NumberFieldStateOptions, "locale"> &
Omit<AriaNumberFieldProps, "onChange"> &
NumberInputVariantProps;
export function useNumberInput(originalProps: UseNumberInputProps) {
const globalContext = useProviderContext();
const {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
const [props, variantProps] = mapPropsVariants(originalProps, numberInput.variantKeys);
const {
ref,
as,
label,
baseRef,
wrapperRef,
description,
className,
classNames,
autoFocus,
startContent,
endContent,
onClear,
onChange,
validationBehavior = formValidationBehavior ?? globalContext?.validationBehavior ?? "native",
innerWrapperRef: innerWrapperRefProp,
onValueChange,
hideStepper,
...otherProps
} = props;
const [isFocusWithin, setFocusWithin] = useState(false);
const Component = as || "div";
const disableAnimation =
originalProps.disableAnimation ?? globalContext?.disableAnimation ?? false;
const domRef = useDOMRef<HTMLInputElement>(ref);
const baseDomRef = useDOMRef<HTMLDivElement>(baseRef);
const inputWrapperRef = useDOMRef<HTMLDivElement>(wrapperRef);
const innerWrapperRef = useDOMRef<HTMLDivElement>(innerWrapperRefProp);
const {locale} = useLocale();
const state = useNumberFieldState({
...originalProps,
validationBehavior,
locale,
onChange: onValueChange,
});
const {
groupProps,
labelProps,
inputProps,
incrementButtonProps,
decrementButtonProps,
descriptionProps,
errorMessageProps,
isInvalid,
validationErrors,
validationDetails,
} = useAriaNumberInput({...originalProps, validationBehavior}, state, domRef);
const inputValue = isNaN(state.numberValue) ? "" : state.numberValue;
const isFilled = !isEmpty(inputValue);
const isFilledWithin = isFilled || isFocusWithin;
const baseStyles = clsx(classNames?.base, className, isFilled ? "is-filled" : "");
const handleClear = useCallback(() => {
state.setInputValue("");
onClear?.();
domRef.current?.focus();
}, [state.setInputValue, onClear]);
// if we use `react-hook-form`, it will set the input value using the ref in register
// i.e. setting ref.current.value to something which is uncontrolled
// hence, sync the state with `ref.current.value`
useSafeLayoutEffect(() => {
if (!domRef.current) return;
state.setInputValue(domRef.current.value);
}, [domRef.current]);
const {isFocusVisible, isFocused, focusProps} = useFocusRing({
autoFocus,
isTextInput: true,
});
const {isHovered, hoverProps} = useHover({isDisabled: !!originalProps?.isDisabled});
const {isHovered: isLabelHovered, hoverProps: labelHoverProps} = useHover({
isDisabled: !!originalProps?.isDisabled,
});
const {focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible} = useFocusRing();
const {focusWithinProps} = useFocusWithin({
onFocusWithinChange: setFocusWithin,
});
const {pressProps: clearPressProps} = usePress({
isDisabled: !!originalProps?.isDisabled || !!originalProps?.isReadOnly,
onPress: handleClear,
});
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const errorMessage =
typeof props.errorMessage === "function"
? props.errorMessage({isInvalid, validationErrors, validationDetails})
: props.errorMessage || validationErrors?.join(" ");
const isClearable = !!onClear || originalProps.isClearable;
const hasElements = !!label || !!description || !!errorMessage;
const hasPlaceholder = !!props.placeholder;
const hasLabel = !!label;
const hasHelper = !!description || !!errorMessage;
const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";
const shouldLabelBeInside = labelPlacement === "inside";
const isPlaceholderShown = domRef.current
? (!domRef.current.value || domRef.current.value === "" || !inputValue) && hasPlaceholder
: false;
const isOutsideLeft = labelPlacement === "outside-left";
const hasStartContent = !!startContent;
const isLabelOutside = shouldLabelBeOutside
? labelPlacement === "outside-left" ||
hasPlaceholder ||
(labelPlacement === "outside" && hasStartContent)
: false;
const isLabelOutsideAsPlaceholder =
labelPlacement === "outside" && !hasPlaceholder && !hasStartContent;
const slots = useMemo(
() =>
numberInput({
...variantProps,
isInvalid,
isClearable,
disableAnimation,
}),
[objectToDeps(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation],
);
const getBaseProps: PropGetter = useCallback(
(props = {}) => {
return {
ref: baseDomRef,
className: slots.base({class: baseStyles}),
"data-slot": "base",
"data-filled": dataAttr(
isFilled || hasPlaceholder || hasStartContent || isPlaceholderShown,
),
"data-filled-within": dataAttr(
isFilledWithin || hasPlaceholder || hasStartContent || isPlaceholderShown,
),
"data-focus-within": dataAttr(isFocusWithin),
"data-focus-visible": dataAttr(isFocusVisible),
"data-readonly": dataAttr(originalProps.isReadOnly),
"data-focus": dataAttr(isFocused),
"data-hover": dataAttr(isHovered || isLabelHovered),
"data-required": dataAttr(originalProps.isRequired),
"data-invalid": dataAttr(isInvalid),
"data-disabled": dataAttr(originalProps.isDisabled),
"data-has-elements": dataAttr(hasElements),
"data-has-helper": dataAttr(hasHelper),
"data-has-label": dataAttr(hasLabel),
"data-has-value": dataAttr(!isPlaceholderShown),
...focusWithinProps,
...props,
};
},
[
slots,
baseStyles,
isFilled,
isFocused,
isHovered,
isLabelHovered,
isInvalid,
hasHelper,
hasLabel,
hasElements,
isPlaceholderShown,
hasStartContent,
isFocusWithin,
isFocusVisible,
hasPlaceholder,
focusWithinProps,
originalProps.isReadOnly,
originalProps.isRequired,
originalProps.isDisabled,
],
);
const getLabelProps: PropGetter = useCallback(
(props = {}) => {
return {
"data-slot": "label",
className: slots.label({class: classNames?.label}),
...mergeProps(labelProps, labelHoverProps, props),
};
},
[slots, isLabelHovered, labelProps, classNames?.label],
);
const getNumberInputProps: PropGetter = useCallback(
(props = {}) => {
return {
"data-slot": "input",
"data-filled": dataAttr(isFilled),
"data-has-start-content": dataAttr(hasStartContent),
"data-has-end-content": dataAttr(!!endContent),
className: slots.input({
class: clsx(classNames?.input, isFilled ? "is-filled" : ""),
}),
...mergeProps(
focusProps,
inputProps,
filterDOMProps(otherProps, {
enabled: true,
labelable: true,
omitEventNames: new Set(Object.keys(inputProps)),
omitPropNames: new Set(["value"]),
}),
props,
),
"aria-readonly": dataAttr(originalProps.isReadOnly),
onChange: chain(inputProps.onChange, onChange),
ref: domRef,
};
},
[
slots,
focusProps,
inputProps,
otherProps,
isFilled,
hasStartContent,
endContent,
classNames?.input,
originalProps.isReadOnly,
originalProps.isRequired,
onChange,
],
);
const getHiddenNumberInputProps: PropGetter = useCallback(
(props = {}) => {
return {
name: originalProps.name,
value: inputValue,
"data-slot": "hidden-input",
type: "hidden",
...props,
};
},
[inputValue, originalProps.name],
);
const getInputWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
ref: inputWrapperRef,
"data-slot": "input-wrapper",
"data-hover": dataAttr(isHovered || isLabelHovered),
"data-focus-visible": dataAttr(isFocusVisible),
"data-focus": dataAttr(isFocused),
className: slots.inputWrapper({
class: clsx(classNames?.inputWrapper, isFilled ? "is-filled" : ""),
}),
...mergeProps(props, hoverProps),
onClick: (e) => {
if (domRef.current && e.currentTarget === e.target) {
domRef.current.focus();
}
},
style: {
cursor: "text",
...props.style,
},
};
},
[
slots,
isHovered,
isLabelHovered,
isFocusVisible,
isFocused,
inputValue,
classNames?.inputWrapper,
],
);
const getInnerWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
ref: innerWrapperRef,
"data-slot": "inner-wrapper",
onClick: (e) => {
if (domRef.current && e.currentTarget === e.target) {
domRef.current.focus();
}
},
className: slots.innerWrapper({
class: clsx(classNames?.innerWrapper, props?.className),
}),
...mergeProps(groupProps, props),
};
},
[slots, classNames?.innerWrapper],
);
const getMainWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
"data-slot": "main-wrapper",
className: slots.mainWrapper({
class: clsx(classNames?.mainWrapper, props?.className),
}),
};
},
[slots, classNames?.mainWrapper],
);
const getHelperWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
"data-slot": "helper-wrapper",
className: slots.helperWrapper({
class: clsx(classNames?.helperWrapper, props?.className),
}),
};
},
[slots, classNames?.helperWrapper],
);
const getDescriptionProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
...descriptionProps,
"data-slot": "description",
className: slots.description({class: clsx(classNames?.label, props?.className)}),
};
},
[slots, classNames?.description],
);
const getErrorMessageProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
...errorMessageProps,
"data-slot": "error-message",
className: slots.errorMessage({class: clsx(classNames?.errorMessage, props?.className)}),
};
},
[slots, errorMessageProps, classNames?.errorMessage],
);
const getClearButtonProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
type: "button",
tabIndex: -1,
disabled: originalProps.isDisabled,
"aria-label": "clear input",
"data-slot": "clear-button",
"data-focus-visible": dataAttr(isClearButtonFocusVisible),
className: slots.clearButton({class: clsx(classNames?.clearButton, props?.className)}),
...mergeProps(clearPressProps, clearFocusProps),
};
},
[slots, isClearButtonFocusVisible, clearPressProps, clearFocusProps, classNames?.clearButton],
);
const getStepperWrapperProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
"data-slot": "stepper-wrapper",
className: slots.stepperWrapper({
class: clsx(classNames?.stepperWrapper, props?.className),
}),
};
},
[slots],
);
const getStepperIncreaseButtonProps: PropGetter = useCallback(
(props = {}) => {
return {
...props,
type: "button",
disabled: originalProps.isDisabled,
"data-slot": "increase-button",
className: slots.stepperButton({
class: clsx(classNames?.stepperButton, props?.className),
}),
...mergeProps(incrementButtonProps, props),
};
},
[slots],
);
const getStepperDecreaseButtonProps: PropGetter = useCallback(
(props = {}) => {
return {
type: "button",
disabled: originalProps.isDisabled,
"data-slot": "decrease-button",
className: slots.stepperButton({
class: clsx(classNames?.stepperButton, props?.className),
}),
...mergeProps(decrementButtonProps, props),
};
},
[slots],
);
return {
Component,
classNames,
domRef,
label,
description,
startContent,
endContent,
labelPlacement,
isClearable,
hasHelper,
hasStartContent,
isLabelOutside,
isOutsideLeft,
isLabelOutsideAsPlaceholder,
shouldLabelBeOutside,
shouldLabelBeInside,
hasPlaceholder,
isInvalid,
errorMessage,
hideStepper,
incrementButtonProps,
decrementButtonProps,
getBaseProps,
getLabelProps,
getNumberInputProps,
getHiddenNumberInputProps,
getMainWrapperProps,
getInputWrapperProps,
getInnerWrapperProps,
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
getStepperIncreaseButtonProps,
getStepperDecreaseButtonProps,
getStepperWrapperProps,
};
}
export type UseNumberInputReturn = ReturnType<typeof useNumberInput>;

View File

@ -0,0 +1,555 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import type {ValidationResult} from "@react-types/shared";
import React from "react";
import {Meta} from "@storybook/react";
import {button} from "@heroui/theme";
import {Form} from "@heroui/form";
import {numberInput} from "@heroui/theme";
import {NumberInput, NumberInputProps} from "../src";
export default {
title: "Components/NumberInput",
component: NumberInput,
argTypes: {
variant: {
control: {
type: "select",
},
options: ["flat", "faded", "bordered", "underlined"],
},
color: {
control: {
type: "select",
},
options: ["default", "primary", "secondary", "success", "warning", "danger"],
},
radius: {
control: {
type: "select",
},
options: ["none", "sm", "md", "lg", "full"],
},
size: {
control: {
type: "select",
},
options: ["sm", "md", "lg"],
},
labelPlacement: {
control: {
type: "select",
},
options: ["inside", "outside", "outside-left"],
},
isDisabled: {
control: {
type: "boolean",
},
},
validationBehavior: {
control: {
type: "select",
},
options: ["aria", "native"],
},
},
decorators: [
(Story) => (
<div className="flex items-center justify-center w-screen h-screen">
<Story />
</div>
),
],
} as Meta<typeof NumberInput>;
const defaultProps = {
...numberInput.defaultVariants,
defaultValue: 24,
};
const Template = (args) => (
<div className="w-full max-w-[240px]">
<NumberInput {...args} />
</div>
);
const FormTemplate = (args) => (
<form
className="w-full max-w-xl flex flex-row items-end gap-4"
onSubmit={(e) => {
alert(`Submitted value: ${e.target["example"].value}`);
e.preventDefault();
}}
>
<NumberInput {...args} name="example" />
<button className={button({color: "primary"})} type="submit">
Submit
</button>
</form>
);
const ControlledTemplate = (args) => {
const [value, setValue] = React.useState(0);
return (
<div className="w-full flex flex-col gap-2 max-w-[240px]">
<NumberInput {...args} value={value} onValueChange={setValue} />
<p className="text-default-500 text-sm">NumberInput value: {value}</p>
</div>
);
};
const LabelPlacementTemplate = (args) => (
<div className="w-full flex flex-col items-center gap-12">
<div className="flex flex-col gap-3">
<h3>Without placeholder</h3>
<div className="w-full max-w-3xl flex flex-row items-end gap-4">
<NumberInput {...args} description="inside" />
<NumberInput {...args} description="outside" labelPlacement="outside" />
<NumberInput {...args} description="outside-left" labelPlacement="outside-left" />
</div>
</div>
<div className="flex flex-col gap-3">
<h3>With placeholder</h3>
<div className="w-full max-w-3xl flex flex-row items-end gap-4">
<NumberInput {...args} description="inside" placeholder="Enter a number" />
<NumberInput
{...args}
description="outside"
labelPlacement="outside"
placeholder="Enter a number"
/>
<NumberInput
{...args}
description="outside-left"
labelPlacement="outside-left"
placeholder="Enter a number"
/>
</div>
</div>
</div>
);
// const WithReactHookFormTemplate = (args: NumberInputProps) => {
// const {
// register,
// formState: {errors},
// handleSubmit,
// } = useForm({
// defaultValues: {
// withDefaultValue: 24,
// withoutDefaultValue: "",
// requiredField: "",
// },
// });
// const onSubmit = (data: any) => {
// // eslint-disable-next-line no-console
// console.log(data);
// alert("Submitted value: " + JSON.stringify(data));
// };
// return (
// <form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
// <NumberInput
// {...args}
// isClearable
// label="With default value"
// {...register("withDefaultValue")}
// />
// <NumberInput {...args} label="Without default value" {...register("withoutDefaultValue")} />
// <NumberInput {...args} label="Required" {...register("requiredField", {required: true})} />
// {errors.requiredField && <span className="text-danger">This field is required</span>}
// <button className={button({class: "w-fit"})} type="submit">
// Submit
// </button>
// </form>
// );
// };
const ServerValidationTemplate = (args: NumberInputProps) => {
const [serverErrors, setServerErrors] = React.useState({});
const onSubmit = (e) => {
e.preventDefault();
setServerErrors({
amount: "Please provide a valid number.",
});
};
return (
<Form
className="flex flex-col items-start gap-2"
validationErrors={serverErrors}
onSubmit={onSubmit}
>
<NumberInput {...args} />
<button className={button({color: "primary"})} type="submit">
Submit
</button>
</Form>
);
};
export const Default = {
render: Template,
args: {
...defaultProps,
"aria-label": "Amount",
},
};
export const WithLabel = {
render: Template,
args: {
...defaultProps,
label: "Amount",
},
};
export const WithDescription = {
render: Template,
args: {
...defaultProps,
label: "Amount",
description: "Specify the amount",
},
};
export const WithStepValue = {
render: Template,
args: {
...defaultProps,
label: "Amount",
step: 10,
description: "Set `step` to `10` to increment / decrement the value by 10.",
},
};
export const WithWheelDisabled = {
render: Template,
args: {
...defaultProps,
label: "Amount",
step: 10,
description: "Set `isWheelDisabled` to `true` to disable the wheel.",
isWheelDisabled: true,
},
};
export const WithFormatOptions = {
render: Template,
args: {
...defaultProps,
label: "Transaction amount",
formatOptions: {
style: "currency",
currency: "EUR",
currencyDisplay: "code",
currencySign: "accounting",
},
},
};
export const HideStepper = {
render: Template,
args: {
...defaultProps,
hideStepper: true,
label: "Hide Stepper",
description: "Set `hideStepper` to `true` to hide the stepper.",
},
};
export const Required = {
render: FormTemplate,
args: {
...defaultProps,
label: "Amount",
isRequired: true,
defaultValue: undefined,
placeholder: "Enter a number",
},
};
export const Disabled = {
render: Template,
args: {
...defaultProps,
variant: "faded",
isDisabled: true,
"aria-label": "amount",
},
};
export const ReadOnly = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
isReadOnly: true,
"aria-label": "amount",
},
};
export const LabelPlacement = {
render: LabelPlacementTemplate,
args: {
...defaultProps,
label: "Amount",
defaultValue: undefined,
},
};
export const Clearable = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
placeholder: "Enter a number",
// eslint-disable-next-line no-console
onClear: () => console.log("number input cleared"),
"aria-label": "amount",
},
};
export const StartContent = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
label: "Price",
placeholder: "0.00",
startContent: (
<div className="pointer-events-none flex items-center">
<span className="text-default-400 text-sm">$</span>
</div>
),
},
};
export const EndContent = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
label: "Price",
placeholder: "0.00",
endContent: (
<div className="pointer-events-none flex items-center">
<span className="text-default-400 text-sm"></span>
</div>
),
},
};
export const StartAndEndContent = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
label: "Price",
placeholder: "0.00",
endContent: (
<div className="flex items-center">
<label className="sr-only" htmlFor="currency">
Currency
</label>
<select
className="outline-none border-0 bg-transparent text-default-400 text-sm"
id="currency"
name="currency"
>
<option>USD</option>
<option>ARS</option>
<option>EUR</option>
</select>
</div>
),
startContent: (
<div className="pointer-events-none flex items-center">
<span className="text-default-400 text-sm">$</span>
</div>
),
},
};
export const WithErrorMessage = {
render: Template,
args: {
...defaultProps,
isInvalid: true,
errorMessage: "Please enter a valid number",
"aria-label": "amount",
},
};
export const WithErrorMessageFunction = {
render: FormTemplate,
args: {
...defaultProps,
min: "0",
max: "100",
isRequired: true,
label: "Number",
validationBehavior: "native",
placeholder: "Enter a number(0-100)",
errorMessage: (value: ValidationResult) => {
if (value.validationDetails.rangeOverflow) {
return "Value is too high";
}
if (value.validationDetails.rangeUnderflow) {
return "Value is too low";
}
if (value.validationDetails.valueMissing) {
return "Value is required";
}
},
},
};
export const WithValidation = {
render: FormTemplate,
args: {
...defaultProps,
validate: (value) => {
if (value < 0 || value > 100) {
return "Value must be between 0 and 100";
}
},
isRequired: true,
label: "Number",
placeholder: "Enter a number(0-100)",
},
};
export const WithServerValidation = {
render: ServerValidationTemplate,
args: {
...defaultProps,
label: "amount",
name: "amount",
},
};
export const IsInvalid = {
render: Template,
args: {
...defaultProps,
variant: "bordered",
isInvalid: true,
placeholder: "Enter a number",
errorMessage: "Please enter a valid range of numbers",
"aria-label": "amount",
},
};
export const Controlled = {
render: ControlledTemplate,
args: {
...defaultProps,
variant: "bordered",
placeholder: "Enter a number",
"aria-label": "amount",
},
};
export const MinValue = {
render: Template,
args: {
...defaultProps,
label: "Enter a number (min value: 60)",
minValue: 60,
defaultValue: 64,
},
};
export const MaxValue = {
render: Template,
args: {
...defaultProps,
label: "Enter a number (max value: 100)",
defaultValue: 0,
maxValue: 100,
},
};
export const CustomWithClassNames = {
render: Template,
args: {
...defaultProps,
classNames: {
label: "hidden",
inputWrapper: [
"bg-slate-100",
"border",
"shadow",
"hover:bg-slate-200",
"focus-within:!bg-slate-100",
"dark:bg-slate-900",
"dark:hover:bg-slate-800",
"dark:border-slate-800",
"dark:focus-within:!bg-slate-900",
],
innerWrapper: "gap-3",
input: [
"text-base",
"text-slate-500",
"placeholder:text-slate-500",
"dark:text-slate-400",
"dark:placeholder:text-slate-400",
],
},
endContent: <div className="pointer-events-none flex items-center"></div>,
placeholder: "Enter the amount",
"aria-label": "amount",
},
};
// export const CustomWithHooks = {
// render: CustomWithHooksTemplate,
// args: {
// ...defaultProps,
// label: "Search",
// type: "search",
// placeholder: "Type to search...",
// startContent: (
// <SearchIcon className="text-black/50 mb-0.5 dark:text-white/90 text-slate-400 pointer-events-none flex-shrink-0" />
// ),
// },
// };
// export const WithReactHookForm = {
// render: WithReactHookFormTemplate,
// args: {
// ...defaultProps,
// },
// };

View File

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -0,0 +1,8 @@
import {defineConfig} from "tsup";
export default defineConfig({
clean: true,
target: "es2019",
format: ["cjs", "esm"],
banner: {js: '"use client";'},
});

View File

@ -89,6 +89,7 @@
"@heroui/drawer": "workspace:*",
"@heroui/form": "workspace:*",
"@heroui/alert": "workspace:*",
"@heroui/number-input": "workspace:*",
"@heroui/toast": "workspace:*",
"@react-aria/visually-hidden": "3.8.19"
},

View File

@ -47,6 +47,7 @@ export * from "@heroui/form";
export * from "@heroui/alert";
export * from "@heroui/drawer";
export * from "@heroui/input-otp";
export * from "@heroui/number-input";
export * from "@heroui/toast";
/**

View File

@ -41,4 +41,5 @@ export * from "./date-picker";
export * from "./alert";
export * from "./drawer";
export * from "./form";
export * from "./number-input";
export * from "./toast";

View File

@ -0,0 +1,854 @@
import type {VariantProps} from "tailwind-variants";
import {tv} from "../utils/tv";
import {dataFocusVisibleClasses, groupDataFocusVisibleClasses} from "../utils";
/**
* NumberInput wrapper **Tailwind Variants** component
*
* @example
* ```js
* const {base, label, inputWrapper, input, clearButton, description, errorMessage} = numberInput({...})
*
* <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={errorMessage()}>Invalid input</span>
* </div>
* ```
*/
const numberInput = tv({
slots: {
base: "group flex flex-col data-[hidden=true]:hidden",
label: [
"absolute",
"z-10",
"pointer-events-none",
"origin-top-left",
"flex-shrink-0",
// Using RTL here as Tailwind CSS doesn't support `start` and `end` logical properties for transforms yet.
"rtl:origin-top-right",
"subpixel-antialiased",
"block",
"text-small",
"text-foreground-500",
],
mainWrapper: "h-full",
inputWrapper:
"relative w-full inline-flex tap-highlight-transparent flex-row items-center shadow-sm px-3 gap-3",
innerWrapper: "inline-flex w-full items-center h-full box-border",
input: [
"w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none",
"data-[has-start-content=true]:ps-1.5",
"data-[has-end-content=true]:pe-1.5",
"autofill:bg-transparent bg-clip-text",
],
clearButton: [
"p-2",
"-m-2",
"z-10",
"end-3",
"start-auto",
"pointer-events-none",
"appearance-none",
"outline-none",
"select-none",
"opacity-0",
"hover:!opacity-100",
"cursor-pointer",
"active:!opacity-70",
"rounded-full",
// focus ring
...dataFocusVisibleClasses,
],
stepperButton: [
"bg-transparent",
"flex",
"justify-center",
"items-center",
"before:absolute",
"before:w-8", // the max width that won't block clear button
"before:h-8",
"before:rounded-full",
"after:shadow-small",
"after:bg-background",
"data-[focused=true]:z-10",
"min-w-5",
"w-5",
"h-5",
"overflow-visible",
"transition-opacity",
"data-[hover=true]:opacity-70",
"data-[pressed=true]:opacity-disabled",
],
stepperWrapper: ["flex", "flex-col", "ps-1", "h-full", "justify-center"],
helperWrapper: "hidden group-data-[has-helper=true]:flex py-2 relative flex-col gap-1.5",
description: "text-tiny text-foreground-400",
errorMessage: "text-tiny text-danger",
},
variants: {
variant: {
flat: {
inputWrapper: [
"bg-default-100",
"data-[hover=true]:bg-default-200",
"group-data-[focus=true]:bg-default-100",
],
},
faded: {
inputWrapper: [
"bg-default-100",
"border-medium",
"border-default-200",
"data-[hover=true]:border-default-400 focus-within:border-default-400",
],
value: "group-data-[has-value=true]:text-default-foreground",
},
bordered: {
inputWrapper: [
"border-medium",
"border-default-200",
"data-[hover=true]:border-default-400",
"group-data-[focus=true]:border-default-foreground",
],
},
underlined: {
inputWrapper: [
"!px-1",
"!pb-0",
"!gap-0",
"relative",
"box-border",
"border-b-medium",
"shadow-[0_1px_0px_0_rgba(0,0,0,0.05)]",
"border-default-200",
"!rounded-none",
"hover:border-default-300",
"after:content-['']",
"after:w-0",
"after:origin-center",
"after:bg-default-foreground",
"after:absolute",
"after:left-1/2",
"after:-translate-x-1/2",
"after:-bottom-[2px]",
"after:h-[2px]",
"group-data-[focus=true]:after:w-full",
],
innerWrapper: "pb-1",
label: "group-data-[filled-within=true]:text-foreground",
},
},
color: {
default: {},
primary: {
stepperButton: "text-primary",
},
secondary: {
stepperButton: "text-secondary",
},
success: {
stepperButton: "text-success",
},
warning: {
stepperButton: "text-warning",
},
danger: {
stepperButton: "text-danger",
},
},
size: {
sm: {
label: "text-tiny",
inputWrapper: "h-8 min-h-8 px-2 rounded-small",
input: "text-small",
clearButton: "text-medium",
},
md: {
inputWrapper: "h-10 min-h-10 rounded-medium",
input: "text-small",
clearButton: "text-large",
},
lg: {
label: "text-medium",
inputWrapper: "h-12 min-h-12 rounded-large",
input: "text-medium",
clearButton: "text-large",
},
},
radius: {
none: {
inputWrapper: "rounded-none",
},
sm: {
inputWrapper: "rounded-small",
},
md: {
inputWrapper: "rounded-medium",
},
lg: {
inputWrapper: "rounded-large",
},
full: {
inputWrapper: "rounded-full",
},
},
labelPlacement: {
outside: {
mainWrapper: "flex flex-col",
stepperButton: "min-w-3 w-3 h-3",
},
"outside-left": {
base: "flex-row items-center flex-nowrap data-[has-helper=true]:items-start",
inputWrapper: "flex-1",
mainWrapper: "flex flex-col",
label: "relative text-foreground pe-2 ps-2 pointer-events-auto",
stepperButton: "min-w-3 w-3 h-3",
},
inside: {
label: "cursor-text",
inputWrapper: "flex-col items-start justify-center gap-0",
innerWrapper: "group-data-[has-label=true]:items-end",
},
},
fullWidth: {
true: {
base: "w-full",
},
false: {},
},
isClearable: {
true: {
input: "peer pe-6 input-search-cancel-button-none",
clearButton: [
"peer-data-[filled=true]:pointer-events-auto",
"peer-data-[filled=true]:opacity-70 peer-data-[filled=true]:block",
"peer-data-[filled=true]:scale-100",
],
},
},
isDisabled: {
true: {
base: "opacity-disabled pointer-events-none",
inputWrapper: "pointer-events-none",
label: "pointer-events-none",
},
},
isInvalid: {
true: {
label: "!text-danger",
input: "!placeholder:text-danger !text-danger",
},
},
isRequired: {
true: {
label: "after:content-['*'] after:text-danger after:ms-0.5",
},
},
disableAnimation: {
true: {
input: "transition-none",
inputWrapper: "transition-none",
label: "transition-none",
},
false: {
inputWrapper: "transition-background motion-reduce:transition-none !duration-150",
label: [
"will-change-auto",
"!duration-200",
"!ease-out",
"motion-reduce:transition-none",
"transition-[transform,color,left,opacity]",
],
clearButton: [
"scale-90",
"ease-out",
"duration-150",
"transition-[opacity,transform]",
"motion-reduce:transition-none",
"motion-reduce:scale-100",
],
},
},
},
defaultVariants: {
variant: "flat",
color: "default",
size: "md",
fullWidth: true,
labelPlacement: "inside",
isDisabled: false,
},
compoundVariants: [
// flat & color
{
variant: "flat",
color: "default",
class: {
input: "group-data-[has-value=true]:text-default-foreground",
},
},
{
variant: "flat",
color: "primary",
class: {
inputWrapper: [
"bg-primary-100",
"data-[hover=true]:bg-primary-50",
"text-primary",
"group-data-[focus=true]:bg-primary-50",
"placeholder:text-primary",
],
input: "placeholder:text-primary",
label: "text-primary",
},
},
{
variant: "flat",
color: "secondary",
class: {
inputWrapper: [
"bg-secondary-100",
"text-secondary",
"data-[hover=true]:bg-secondary-50",
"group-data-[focus=true]:bg-secondary-50",
"placeholder:text-secondary",
],
input: "placeholder:text-secondary",
label: "text-secondary",
},
},
{
variant: "flat",
color: "success",
class: {
inputWrapper: [
"bg-success-100",
"text-success-600",
"dark:text-success",
"placeholder:text-success-600",
"dark:placeholder:text-success",
"data-[hover=true]:bg-success-50",
"group-data-[focus=true]:bg-success-50",
],
input: "placeholder:text-success-600 dark:placeholder:text-success",
label: "text-success-600 dark:text-success",
},
},
{
variant: "flat",
color: "warning",
class: {
inputWrapper: [
"bg-warning-100",
"text-warning-600",
"dark:text-warning",
"placeholder:text-warning-600",
"dark:placeholder:text-warning",
"data-[hover=true]:bg-warning-50",
"group-data-[focus=true]:bg-warning-50",
],
input: "placeholder:text-warning-600 dark:placeholder:text-warning",
label: "text-warning-600 dark:text-warning",
},
},
{
variant: "flat",
color: "danger",
class: {
inputWrapper: [
"bg-danger-100",
"text-danger",
"dark:text-danger-500",
"placeholder:text-danger",
"dark:placeholder:text-danger-500",
"data-[hover=true]:bg-danger-50",
"group-data-[focus=true]:bg-danger-50",
],
input: "placeholder:text-danger dark:placeholder:text-danger-500",
label: "text-danger dark:text-danger-500",
},
},
// faded & color
{
variant: "faded",
color: "primary",
class: {
label: "text-primary",
inputWrapper: "data-[hover=true]:border-primary focus-within:border-primary",
},
},
{
variant: "faded",
color: "secondary",
class: {
label: "text-secondary",
inputWrapper: "data-[hover=true]:border-secondary focus-within:border-secondary",
},
},
{
variant: "faded",
color: "success",
class: {
label: "text-success",
inputWrapper: "data-[hover=true]:border-success focus-within:border-success",
},
},
{
variant: "faded",
color: "warning",
class: {
label: "text-warning",
inputWrapper: "data-[hover=true]:border-warning focus-within:border-warning",
},
},
{
variant: "faded",
color: "danger",
class: {
label: "text-danger",
inputWrapper: "data-[hover=true]:border-danger focus-within:border-danger",
},
},
// underlined & color
{
variant: "underlined",
color: "default",
class: {
input: "group-data-[has-value=true]:text-foreground",
},
},
{
variant: "underlined",
color: "primary",
class: {
inputWrapper: "after:bg-primary",
label: "text-primary",
},
},
{
variant: "underlined",
color: "secondary",
class: {
inputWrapper: "after:bg-secondary",
label: "text-secondary",
},
},
{
variant: "underlined",
color: "success",
class: {
inputWrapper: "after:bg-success",
label: "text-success",
},
},
{
variant: "underlined",
color: "warning",
class: {
inputWrapper: "after:bg-warning",
label: "text-warning",
},
},
{
variant: "underlined",
color: "danger",
class: {
inputWrapper: "after:bg-danger",
label: "text-danger",
},
},
// bordered & color
{
variant: "bordered",
color: "primary",
class: {
inputWrapper: "group-data-[focus=true]:border-primary",
label: "text-primary",
},
},
{
variant: "bordered",
color: "secondary",
class: {
inputWrapper: "group-data-[focus=true]:border-secondary",
label: "text-secondary",
},
},
{
variant: "bordered",
color: "success",
class: {
inputWrapper: "group-data-[focus=true]:border-success",
label: "text-success",
},
},
{
variant: "bordered",
color: "warning",
class: {
inputWrapper: "group-data-[focus=true]:border-warning",
label: "text-warning",
},
},
{
variant: "bordered",
color: "danger",
class: {
inputWrapper: "group-data-[focus=true]:border-danger",
label: "text-danger",
},
},
// labelPlacement=inside & default
{
labelPlacement: "inside",
color: "default",
class: {
label: "group-data-[filled-within=true]:text-default-600",
},
},
// labelPlacement=outside & default
{
labelPlacement: "outside",
color: "default",
class: {
label: "group-data-[filled-within=true]:text-foreground",
},
},
// radius-full & size
{
radius: "full",
size: ["sm"],
class: {
inputWrapper: "px-3",
},
},
{
radius: "full",
size: "md",
class: {
inputWrapper: "px-4",
},
},
{
radius: "full",
size: "lg",
class: {
inputWrapper: "px-5",
},
},
// !disableAnimation & variant
{
disableAnimation: false,
variant: ["faded", "bordered"],
class: {
inputWrapper: "transition-colors motion-reduce:transition-none",
},
},
{
disableAnimation: false,
variant: "underlined",
class: {
inputWrapper: "after:transition-width motion-reduce:after:transition-none",
},
},
// flat & faded
{
variant: ["flat", "faded"],
class: {
inputWrapper: [
// focus ring
...groupDataFocusVisibleClasses,
],
},
},
// isInvalid & variant
{
isInvalid: true,
variant: "flat",
class: {
inputWrapper: [
"!bg-danger-50",
"data-[hover=true]:!bg-danger-100",
"group-data-[focus=true]:!bg-danger-50",
],
},
},
{
isInvalid: true,
variant: "bordered",
class: {
inputWrapper: "!border-danger group-data-[focus=true]:!border-danger",
},
},
{
isInvalid: true,
variant: "underlined",
class: {
inputWrapper: "after:!bg-danger",
},
},
// size & labelPlacement
{
labelPlacement: "inside",
size: "sm",
class: {
inputWrapper: "h-12 py-1.5 px-3",
},
},
{
labelPlacement: "inside",
size: "md",
class: {
inputWrapper: "h-14 py-2",
},
},
{
labelPlacement: "inside",
size: "lg",
class: {
inputWrapper: "h-16 py-2.5 gap-0",
},
},
// size & labelPlacement & variant=[faded, bordered]
{
labelPlacement: "inside",
size: "sm",
variant: ["bordered", "faded"],
class: {
inputWrapper: "py-1",
},
},
// labelPlacement=[inside,outside]
{
labelPlacement: ["inside", "outside"],
class: {
label: ["group-data-[filled-within=true]:pointer-events-auto"],
},
},
// labelPlacement=[outside]
{
labelPlacement: "outside",
class: {
base: "relative justify-end",
label: [
"pb-0",
"z-20",
"top-1/2",
"-translate-y-1/2",
"group-data-[filled-within=true]:start-0",
],
},
},
// labelPlacement=[inside]
{
labelPlacement: ["inside"],
class: {
label: ["group-data-[filled-within=true]:scale-85"],
},
},
{
labelPlacement: "inside",
size: "sm",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.tiny)/2_-_8px)]",
],
},
},
{
labelPlacement: "inside",
size: "md",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_6px)]",
],
},
},
{
labelPlacement: "inside",
size: "lg",
class: {
label: [
"text-medium",
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_8px)]",
],
},
},
// labelPlacement=[inside] & variant=flat
{
labelPlacement: ["inside"],
variant: "flat",
class: {
innerWrapper: "pb-0.5",
},
},
// variant=underlined & size
{
variant: "underlined",
size: "sm",
class: {
innerWrapper: "pb-1",
},
},
{
variant: "underlined",
size: ["md", "lg"],
class: {
innerWrapper: "pb-1.5",
},
},
// inside & size
{
labelPlacement: "inside",
size: ["sm", "md"],
class: {
label: "text-small",
},
},
// inside & size & [faded, bordered]
{
labelPlacement: "inside",
variant: ["faded", "bordered"],
size: "sm",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.tiny)/2_-_8px_-_theme(borderWidth.medium))]",
],
},
},
{
labelPlacement: "inside",
variant: ["faded", "bordered"],
isMultiline: false,
size: "md",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_6px_-_theme(borderWidth.medium))]",
],
},
},
{
labelPlacement: "inside",
variant: ["faded", "bordered"],
size: "lg",
class: {
label: [
"text-medium",
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_8px_-_theme(borderWidth.medium))]",
],
},
},
// inside & size & underlined
{
labelPlacement: "inside",
variant: "underlined",
size: "sm",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.tiny)/2_-_5px)]",
],
},
},
{
labelPlacement: "inside",
variant: "underlined",
size: "md",
class: {
label: [
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_3.5px)]",
],
},
},
{
labelPlacement: "inside",
variant: "underlined",
size: "lg",
class: {
label: [
"text-medium",
"group-data-[filled-within=true]:-translate-y-[calc(50%_+_theme(fontSize.small)/2_-_4px)]",
],
},
},
// outside & size
{
labelPlacement: "outside",
size: "sm",
class: {
label: [
"start-2",
"text-tiny",
"group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.tiny)/2_+_16px)]",
],
base: "data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_8px)]",
},
},
{
labelPlacement: "outside",
size: "md",
class: {
label: [
"start-3",
"end-auto",
"text-small",
"group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)]",
],
base: "data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]",
},
},
{
labelPlacement: "outside",
size: "lg",
class: {
label: [
"start-3",
"end-auto",
"text-medium",
"group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_24px)]",
],
base: "data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_12px)]",
stepperButton: "min-4 w-4 h-4",
},
},
// outside-left & size & hasHelper
{
labelPlacement: "outside-left",
size: "sm",
class: {
label: "group-data-[has-helper=true]:pt-2",
},
},
{
labelPlacement: "outside-left",
size: "md",
class: {
label: "group-data-[has-helper=true]:pt-3",
},
},
{
labelPlacement: "outside-left",
size: "lg",
class: {
label: "group-data-[has-helper=true]:pt-4",
stepperButton: "min-4 w-4 h-4",
},
},
// text truncate labelPlacement=[inside,outside]
{
labelPlacement: ["inside", "outside"],
class: {
label: ["pe-2", "max-w-full", "text-ellipsis", "overflow-hidden"],
},
},
],
});
export type NumberInputVariantProps = VariantProps<typeof numberInput>;
export type NumberInputSlots = keyof ReturnType<typeof numberInput>;
export {numberInput};

View File

@ -0,0 +1,20 @@
import {IconSvgProps} from "./types";
export const ChevronLeftIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path d="m15 18-6-6 6-6" />
</svg>
);

View File

@ -5,9 +5,10 @@ export * from "./avatar";
export * from "./close";
export * from "./close-filled";
export * from "./chevron";
export * from "./chevron-down";
export * from "./chevron-right";
export * from "./chevron-up";
export * from "./chevron-down";
export * from "./chevron-left";
export * from "./chevron-right";
export * from "./ellipsis";
export * from "./forward";
export * from "./sun";

282
pnpm-lock.yaml generated
View File

@ -123,16 +123,16 @@ importers:
version: 7.32.0
eslint-config-airbnb:
specifier: ^18.2.1
version: 18.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)
version: 18.2.1(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)
eslint-config-airbnb-typescript:
specifier: ^12.3.1
version: 12.3.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3)
version: 12.3.1(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3)
eslint-config-prettier:
specifier: ^8.2.0
version: 8.10.0(eslint@7.32.0)
eslint-config-react-app:
specifier: ^6.0.0
version: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(babel-eslint@10.1.0(eslint@7.32.0))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3)
version: 6.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(babel-eslint@10.1.0(eslint@7.32.0))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0)(eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3)
eslint-config-ts-lambdas:
specifier: ^1.2.3
version: 1.2.3(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3)
@ -141,7 +141,7 @@ importers:
version: 2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0)
eslint-loader:
specifier: ^4.0.2
version: 4.0.2(eslint@7.32.0)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12))
version: 4.0.2(eslint@7.32.0)(webpack@5.97.1)
eslint-plugin-import:
specifier: ^2.26.0
version: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
@ -195,13 +195,13 @@ importers:
version: 10.7.11
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
version: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
jest-environment-jsdom:
specifier: ^29.7.0
version: 29.7.0
jest-watch-typeahead:
specifier: 2.2.2
version: 2.2.2(jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)))
version: 2.2.2(jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)))
lint-staged:
specifier: ^13.0.3
version: 13.3.0(enquirer@2.4.1)
@ -252,7 +252,7 @@ importers:
version: 5.7.3
webpack:
specifier: ^5.53.0
version: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)
version: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12(webpack@5.97.1))
webpack-bundle-analyzer:
specifier: ^4.4.2
version: 4.10.2
@ -2217,6 +2217,76 @@ importers:
specifier: 0.13.0
version: 0.13.0(react@18.3.0)
packages/components/number-input:
dependencies:
'@heroui/button':
specifier: workspace:*
version: link:../button
'@heroui/form':
specifier: workspace:*
version: link:../form
'@heroui/react-utils':
specifier: workspace:*
version: link:../../utilities/react-utils
'@heroui/shared-icons':
specifier: workspace:*
version: link:../../utilities/shared-icons
'@heroui/shared-utils':
specifier: workspace:*
version: link:../../utilities/shared-utils
'@heroui/use-safe-layout-effect':
specifier: workspace:*
version: link:../../hooks/use-safe-layout-effect
'@react-aria/focus':
specifier: 3.19.1
version: 3.19.1(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/i18n':
specifier: 3.12.5
version: 3.12.5(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/interactions':
specifier: 3.23.0
version: 3.23.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/numberfield':
specifier: 3.11.10
version: 3.11.10(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/utils':
specifier: 3.27.0
version: 3.27.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-stately/numberfield':
specifier: 3.9.9
version: 3.9.9(react@18.3.0)
'@react-stately/utils':
specifier: 3.10.5
version: 3.10.5(react@18.3.0)
'@react-types/button':
specifier: 3.10.2
version: 3.10.2(react@18.3.0)
'@react-types/numberfield':
specifier: 3.8.8
version: 3.8.8(react@18.3.0)
'@react-types/shared':
specifier: 3.27.0
version: 3.27.0(react@18.3.0)
devDependencies:
'@heroui/system':
specifier: workspace:*
version: link:../../core/system
'@heroui/theme':
specifier: workspace:*
version: link:../../core/theme
clean-package:
specifier: 2.2.0
version: 2.2.0
react:
specifier: 18.3.0
version: 18.3.0
react-dom:
specifier: 18.3.0
version: 18.3.0(react@18.3.0)
react-hook-form:
specifier: ^7.51.3
version: 7.54.2(react@18.3.0)
packages/components/pagination:
dependencies:
'@heroui/react-utils':
@ -3284,6 +3354,9 @@ importers:
'@heroui/navbar':
specifier: workspace:*
version: link:../../components/navbar
'@heroui/number-input':
specifier: workspace:*
version: link:../../components/number-input
'@heroui/pagination':
specifier: workspace:*
version: link:../../components/pagination
@ -3427,7 +3500,7 @@ importers:
version: 18.3.0
tailwind-variants:
specifier: ^0.3.0
version: 0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)))
version: 0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)))
packages/core/theme:
dependencies:
@ -3454,7 +3527,7 @@ importers:
version: 2.5.4
tailwind-variants:
specifier: 0.3.0
version: 0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)))
version: 0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)))
devDependencies:
'@types/color':
specifier: ^4.2.0
@ -3467,7 +3540,7 @@ importers:
version: 2.2.0
tailwindcss:
specifier: ^3.4.16
version: 3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
version: 3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
packages/hooks/use-aria-accordion:
dependencies:
@ -7011,6 +7084,12 @@ packages:
react: 18.3.0
react-dom: 18.3.0
'@react-aria/numberfield@3.11.10':
resolution: {integrity: sha512-bYbTfO9NbAKMFOfEGGs+lvlxk0I9L0lU3WD2PFQZWdaoBz9TCkL+vK0fJk1zsuKaVjeGsmHP9VesBPRmaP0MiA==}
peerDependencies:
react: 18.3.0
react-dom: 18.3.0
'@react-aria/overlays@3.25.0':
resolution: {integrity: sha512-UEqJJ4duowrD1JvwXpPZreBuK79pbyNjNxFUVpFSskpGEJe3oCWwsSDKz7P1O7xbx5OYp+rDiY8fk/sE5rkaKw==}
peerDependencies:
@ -7189,6 +7268,11 @@ packages:
peerDependencies:
react: 18.3.0
'@react-stately/numberfield@3.9.9':
resolution: {integrity: sha512-hZsLiGGHTHmffjFymbH1qVmA633rU2GNjMFQTuSsN4lqqaP8fgxngd5pPCoTCUFEkUgWjdHenw+ZFByw8lIE+g==}
peerDependencies:
react: 18.3.0
'@react-stately/overlays@3.6.13':
resolution: {integrity: sha512-WsU85Gf/b+HbWsnnYw7P/Ila3wD+C37Uk/WbU4/fHgJ26IEOWsPE6wlul8j54NZ1PnLNhV9Fn+Kffi+PaJMQXQ==}
peerDependencies:
@ -7320,6 +7404,11 @@ packages:
peerDependencies:
react: 18.3.0
'@react-types/numberfield@3.8.8':
resolution: {integrity: sha512-825JPppxDaWh0Zxb0Q+wSslgRQYOtQPCAuhszPuWEy6d2F/M+hLR+qQqvQm9+LfMbdwiTg6QK5wxdWFCp2t7jw==}
peerDependencies:
react: 18.3.0
'@react-types/overlays@3.8.12':
resolution: {integrity: sha512-ZvR1t0YV7/6j+6OD8VozKYjvsXT92+C/2LOIKozy7YUNS5KI4MkXbRZzJvkuRECVZOmx8JXKTUzhghWJM/3QuQ==}
peerDependencies:
@ -16896,7 +16985,7 @@ snapshots:
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
resolve-from: 5.0.0
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)
typescript: 5.7.3
transitivePeerDependencies:
- '@swc/core'
@ -17615,7 +17704,7 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))':
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
@ -17629,7 +17718,7 @@ snapshots:
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -19257,6 +19346,22 @@ snapshots:
react: 18.3.0
react-dom: 18.3.0(react@18.3.0)
'@react-aria/numberfield@3.11.10(react-dom@18.3.0(react@18.3.0))(react@18.3.0)':
dependencies:
'@react-aria/i18n': 3.12.5(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/interactions': 3.23.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/spinbutton': 3.6.11(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/textfield': 3.16.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-aria/utils': 3.27.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@react-stately/form': 3.1.1(react@18.3.0)
'@react-stately/numberfield': 3.9.9(react@18.3.0)
'@react-types/button': 3.10.2(react@18.3.0)
'@react-types/numberfield': 3.8.8(react@18.3.0)
'@react-types/shared': 3.27.0(react@18.3.0)
'@swc/helpers': 0.5.15
react: 18.3.0
react-dom: 18.3.0(react@18.3.0)
'@react-aria/overlays@3.25.0(react-dom@18.3.0(react@18.3.0))(react@18.3.0)':
dependencies:
'@react-aria/focus': 3.19.1(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
@ -19608,6 +19713,15 @@ snapshots:
'@swc/helpers': 0.5.15
react: 18.3.0
'@react-stately/numberfield@3.9.9(react@18.3.0)':
dependencies:
'@internationalized/number': 3.6.0
'@react-stately/form': 3.1.1(react@18.3.0)
'@react-stately/utils': 3.10.5(react@18.3.0)
'@react-types/numberfield': 3.8.8(react@18.3.0)
'@swc/helpers': 0.5.15
react: 18.3.0
'@react-stately/overlays@3.6.13(react@18.3.0)':
dependencies:
'@react-stately/utils': 3.10.5(react@18.3.0)
@ -19786,6 +19900,11 @@ snapshots:
'@react-types/shared': 3.27.0(react@18.3.0)
react: 18.3.0
'@react-types/numberfield@3.8.8(react@18.3.0)':
dependencies:
'@react-types/shared': 3.27.0(react@18.3.0)
react: 18.3.0
'@react-types/overlays@3.8.12(react@18.3.0)':
dependencies:
'@react-types/shared': 3.27.0(react@18.3.0)
@ -22158,7 +22277,7 @@ snapshots:
dependencies:
'@types/node': 20.5.1
cosmiconfig: 8.3.6(typescript@5.7.3)
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)
typescript: 5.7.3
cosmiconfig@8.3.6(typescript@5.7.3):
@ -22179,13 +22298,13 @@ snapshots:
optionalDependencies:
typescript: 5.7.3
create-jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)):
create-jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -22848,7 +22967,7 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-airbnb-base@14.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint@7.32.0):
eslint-config-airbnb-base@14.2.1(eslint-plugin-import@2.31.0)(eslint@7.32.0):
dependencies:
confusing-browser-globals: 1.0.11
eslint: 7.32.0
@ -22856,11 +22975,11 @@ snapshots:
object.assign: 4.1.7
object.entries: 1.1.8
eslint-config-airbnb-typescript@12.3.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3):
eslint-config-airbnb-typescript@12.3.1(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3):
dependencies:
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.7.3)
eslint-config-airbnb: 18.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)
eslint-config-airbnb-base: 14.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint@7.32.0)
eslint-config-airbnb: 18.2.1(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)
eslint-config-airbnb-base: 14.2.1(eslint-plugin-import@2.31.0)(eslint@7.32.0)
transitivePeerDependencies:
- eslint
- eslint-plugin-import
@ -22870,10 +22989,10 @@ snapshots:
- supports-color
- typescript
eslint-config-airbnb@18.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0):
eslint-config-airbnb@18.2.1(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0):
dependencies:
eslint: 7.32.0
eslint-config-airbnb-base: 14.2.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint@7.32.0)
eslint-config-airbnb-base: 14.2.1(eslint-plugin-import@2.31.0)(eslint@7.32.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@7.32.0)
eslint-plugin-react: 7.37.3(eslint@7.32.0)
@ -22905,7 +23024,7 @@ snapshots:
dependencies:
eslint: 7.32.0
eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(babel-eslint@10.1.0(eslint@7.32.0))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0))(eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3):
eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(babel-eslint@10.1.0(eslint@7.32.0))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0)(eslint-plugin-jest@24.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.2(eslint@7.32.0))(eslint-plugin-react@7.37.3(eslint@7.32.0))(eslint@7.32.0)(typescript@5.7.3):
dependencies:
'@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3)
'@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.7.3)
@ -22964,7 +23083,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-loader@4.0.2(eslint@7.32.0)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)):
eslint-loader@4.0.2(eslint@7.32.0)(webpack@5.97.1):
dependencies:
eslint: 7.32.0
find-cache-dir: 3.3.2
@ -22972,9 +23091,9 @@ snapshots:
loader-utils: 2.0.4
object-hash: 2.2.0
schema-utils: 2.7.1
webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)
webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12(webpack@5.97.1))
eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0):
eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -23008,7 +23127,7 @@ snapshots:
doctrine: 2.1.0
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -24736,16 +24855,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)):
jest-cli@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
create-jest: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
exit: 0.1.2
import-local: 3.2.0
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
jest-config: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -24755,7 +24874,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)):
jest-config@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
dependencies:
'@babel/core': 7.26.0
'@jest/test-sequencer': 29.7.0
@ -24781,7 +24900,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 15.14.9
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -24998,11 +25117,11 @@ snapshots:
leven: 3.1.0
pretty-format: 29.7.0
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))):
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))):
dependencies:
ansi-escapes: 6.2.1
chalk: 5.4.1
jest: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
jest: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
jest-regex-util: 29.6.3
jest-watcher: 29.7.0
slash: 5.1.0
@ -25033,12 +25152,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)):
jest@29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
'@jest/types': 29.6.3
import-local: 3.2.0
jest-cli: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
jest-cli: 29.7.0(@types/node@15.14.9)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -27082,14 +27201,6 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
dependencies:
lilconfig: 3.1.3
yaml: 2.7.0
optionalDependencies:
postcss: 8.4.49
ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)
postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.2.5)(typescript@5.7.3)):
dependencies:
lilconfig: 3.1.3
@ -28702,10 +28813,10 @@ snapshots:
tailwind-merge: 2.5.4
tailwindcss: 3.4.14(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.2.5)(typescript@5.7.3))
tailwind-variants@0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))):
tailwind-variants@0.3.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))):
dependencies:
tailwind-merge: 2.5.4
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
tailwindcss@3.4.14(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.2.5)(typescript@5.7.3)):
dependencies:
@ -28761,7 +28872,7 @@ snapshots:
transitivePeerDependencies:
- ts-node
tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3)):
tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@ -28780,7 +28891,7 @@ snapshots:
postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3))
postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.5.1)(typescript@5.7.3))
postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
resolve: 1.22.10
@ -28836,7 +28947,7 @@ snapshots:
term-size@2.2.1: {}
terser-webpack-plugin@5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12(webpack@5.97.1))):
terser-webpack-plugin@5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
@ -28848,18 +28959,6 @@ snapshots:
'@swc/core': 1.10.6(@swc/helpers@0.5.15)
esbuild: 0.24.2
terser-webpack-plugin@5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 4.3.0
serialize-javascript: 6.0.2
terser: 5.37.0
webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)
optionalDependencies:
'@swc/core': 1.10.6(@swc/helpers@0.5.15)
esbuild: 0.24.2
terser@5.37.0:
dependencies:
'@jridgewell/source-map': 0.3.6
@ -28997,26 +29096,6 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@15.14.9)(typescript@5.7.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 15.14.9
acorn: 8.14.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.7.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.10.6(@swc/helpers@0.5.15)
ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.2.5)(typescript@5.7.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
@ -29057,7 +29136,6 @@ snapshots:
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.10.6(@swc/helpers@0.5.15)
optional: true
ts-pattern@5.6.0: {}
@ -29695,7 +29773,7 @@ snapshots:
loader-utils: 1.4.2
supports-color: 6.1.0
v8-compile-cache: 2.4.0
webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12)
webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12(webpack@5.97.1))
yargs: 13.3.2
webpack-merge@5.10.0:
@ -29730,39 +29808,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12(webpack@5.97.1)))
watchpack: 2.4.2
webpack-sources: 3.2.3
optionalDependencies:
webpack-cli: 3.3.12(webpack@5.97.1)
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.6
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.14.0
browserslist: 4.24.3
chrome-trace-event: 1.0.4
enhanced-resolve: 5.18.0
es-module-lexer: 1.6.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.0
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack-cli@3.3.12))
terser-webpack-plugin: 5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(esbuild@0.24.2)(webpack@5.97.1)
watchpack: 2.4.2
webpack-sources: 3.2.3
optionalDependencies: