From e44c5ee637e774b672207bf1d08406813ed63d8e Mon Sep 17 00:00:00 2001 From: tsai <916003388@qq.com> Date: Wed, 22 Jul 2026 06:48:17 +0800 Subject: [PATCH] feat(react): modernize component APIs and layouts --- .changeset/modernize-component-library.md | 25 + .size-limit.mjs | 16 +- CONTRIBUTING.md | 16 + README.md | 2 +- apps/docs/src/ComponentPreview.tsx | 206 +- apps/docs/src/DocsPages.tsx | 26 +- apps/docs/src/component-data.ts | 34 +- apps/docs/src/component-examples.ts | 191 +- apps/docs/src/router.tsx | 112 +- apps/docs/src/template-source.ts | 24 +- apps/docs/vite.config.ts | 6 + apps/storybook/.storybook/main.ts | 15 + apps/storybook/package.json | 6 +- apps/storybook/src/colors.stories.tsx | 6 +- apps/storybook/src/generate-theme.stories.tsx | 8 +- .../src/porcelain-foundations.stories.tsx | 30 +- apps/storybook/src/tokens.stories.tsx | 10 +- package.json | 10 + packages/react/package.json | 34 +- packages/react/src/alert/Alert.a11y.test.tsx | 45 + packages/react/src/alert/Alert.stories.tsx | 78 + packages/react/src/alert/Alert.test.tsx | 175 ++ packages/react/src/alert/Alert.tsx | 229 ++ packages/react/src/alert/Alert.types.ts | 36 + packages/react/src/alert/index.ts | 7 + packages/react/src/box/Box.stories.tsx | 11 + packages/react/src/box/Box.test.tsx | 13 + packages/react/src/box/Box.tsx | 13 +- packages/react/src/box/Box.types.ts | 7 +- .../src/breadcrumb/Breadcrumb.a11y.test.tsx | 39 + .../src/breadcrumb/Breadcrumb.stories.tsx | 39 + .../react/src/breadcrumb/Breadcrumb.test.tsx | 122 + packages/react/src/breadcrumb/Breadcrumb.tsx | 167 ++ .../react/src/breadcrumb/Breadcrumb.types.ts | 35 + packages/react/src/breadcrumb/index.ts | 2 + packages/react/src/button/Button.classes.ts | 115 + packages/react/src/button/Button.stories.tsx | 29 +- packages/react/src/button/Button.test.tsx | 67 + packages/react/src/button/Button.tsx | 101 +- packages/react/src/button/Button.types.ts | 24 +- packages/react/src/button/index.ts | 3 + packages/react/src/checkbox/Checkbox.test.tsx | 42 + packages/react/src/checkbox/Checkbox.tsx | 169 +- packages/react/src/checkbox/Checkbox.types.ts | 5 + packages/react/src/checkbox/index.ts | 3 + .../react/src/combobox/Combobox.a11y.test.tsx | 85 + .../react/src/combobox/Combobox.stories.tsx | 97 + packages/react/src/combobox/Combobox.test.tsx | 213 ++ packages/react/src/combobox/Combobox.tsx | 675 +++++ packages/react/src/combobox/Combobox.types.ts | 56 + packages/react/src/combobox/index.ts | 11 + .../react/src/container/Container.stories.tsx | 6 + .../react/src/container/Container.test.tsx | 23 + packages/react/src/container/Container.tsx | 45 +- .../react/src/container/Container.types.ts | 11 +- .../react/src/date-picker/DatePicker.test.tsx | 9 + packages/react/src/date-picker/DatePicker.tsx | 8 +- .../date-range-picker/DateRangeCalendar.tsx | 375 ++- .../src/date-range-picker/DateRangeField.tsx | 235 ++ .../DateRangePicker.test.tsx | 53 +- .../src/date-range-picker/DateRangePicker.tsx | 318 ++- packages/react/src/drawer/Drawer.test.tsx | 10 +- packages/react/src/drawer/Drawer.tsx | 255 +- .../react/src/dropdown/Dropdown.a11y.test.tsx | 5 +- .../react/src/dropdown/Dropdown.stories.tsx | 26 +- packages/react/src/dropdown/Dropdown.test.tsx | 29 +- packages/react/src/dropdown/Dropdown.tsx | 77 +- packages/react/src/dropdown/Dropdown.types.ts | 58 +- packages/react/src/dropdown/DropdownMenu.tsx | 385 ++- packages/react/src/flex/Flex.stories.tsx | 14 + packages/react/src/flex/Flex.test.tsx | 21 + packages/react/src/flex/Flex.tsx | 62 +- packages/react/src/flex/Flex.types.ts | 15 +- packages/react/src/form/Form.stories.tsx | 41 + packages/react/src/form/Form.test.tsx | 118 + packages/react/src/form/Form.tsx | 34 +- packages/react/src/form/Form.types.ts | 8 + packages/react/src/form/index.ts | 1 + packages/react/src/form/standard-schema.ts | 92 + packages/react/src/grid/Grid.stories.tsx | 8 +- packages/react/src/grid/Grid.test.tsx | 22 + packages/react/src/grid/Grid.tsx | 31 +- packages/react/src/grid/Grid.types.ts | 11 +- packages/react/src/index.ts | 57 +- packages/react/src/input/Input.classes.ts | 52 + packages/react/src/input/Input.tsx | 29 +- packages/react/src/input/index.ts | 2 + packages/react/src/modal/Modal.test.tsx | 4 + packages/react/src/modal/Modal.tsx | 245 +- .../react/src/pagination/Pagination.test.tsx | 29 +- packages/react/src/pagination/Pagination.tsx | 15 +- .../react/src/pagination/Pagination.types.ts | 2 + packages/react/src/popover/Popover.test.tsx | 9 +- packages/react/src/popover/Popover.tsx | 50 +- packages/react/src/select/Select.tsx | 11 +- packages/react/src/shared/dialog.tsx | 261 ++ .../src/shared/polymorphic.type-test.tsx | 2 + packages/react/src/shared/responsive.ts | 61 + packages/react/src/shared/slot.test.tsx | 79 + packages/react/src/shared/slot.tsx | 133 + .../react/src/slider/Slider.a11y.test.tsx | 38 + packages/react/src/slider/Slider.stories.tsx | 82 + packages/react/src/slider/Slider.test.tsx | 108 + packages/react/src/slider/Slider.tsx | 263 ++ packages/react/src/slider/Slider.types.ts | 43 + packages/react/src/slider/index.ts | 8 + packages/react/src/spinner/Spinner.test.tsx | 10 + packages/react/src/spinner/Spinner.tsx | 5 +- packages/react/src/spinner/Spinner.types.ts | 2 + packages/react/src/stack/Stack.stories.tsx | 13 +- packages/react/src/stack/Stack.test.tsx | 46 + packages/react/src/stack/Stack.tsx | 69 +- packages/react/src/stack/Stack.types.ts | 17 +- packages/react/src/switch/Switch.stories.tsx | 3 + packages/react/src/switch/Switch.test.tsx | 16 + packages/react/src/switch/Switch.tsx | 3 + packages/react/src/switch/Switch.types.ts | 2 +- packages/react/src/table/Table.test.tsx | 5 + packages/react/src/table/TableView.tsx | 4 +- packages/react/src/tag/Tag.classes.ts | 52 + packages/react/src/tag/Tag.test.tsx | 4 + packages/react/src/tag/Tag.tsx | 31 +- packages/react/src/tag/index.ts | 2 + packages/react/src/tailwind.css | 37 +- .../react/src/text-area/TextArea.test.tsx | 19 + packages/react/src/text-area/TextArea.tsx | 12 +- .../react/src/text-area/TextArea.types.ts | 7 + packages/react/src/text-area/index.ts | 1 + packages/react/src/text/Text.stories.tsx | 2 +- packages/react/src/text/Text.test.tsx | 8 + packages/react/src/text/Text.types.ts | 2 +- .../react/src/theme/components/slider.css | 12 + .../react/src/theme/components/switch.css | 5 + packages/react/src/theme/tokens.css | 13 + packages/react/src/theme/tokens.ts | 13 + packages/react/src/toast/Toast.test.tsx | 46 + packages/react/src/toast/Toast.types.ts | 13 + packages/react/src/toast/ToastItem.tsx | 24 +- packages/react/src/toast/ToastProvider.tsx | 102 +- packages/react/src/toast/index.ts | 1 + packages/react/src/toast/toast.ts | 64 +- packages/react/src/tooltip/Tooltip.test.tsx | 14 +- packages/react/src/tooltip/Tooltip.tsx | 64 +- packages/react/src/wizard/Wizard.test.tsx | 10 + packages/react/src/wizard/Wizard.tsx | 2 +- packages/react/tokens.json | 13 + packages/react/tsup.config.ts | 4 + packages/velune/package.json | 4 +- pnpm-lock.yaml | 2181 ++++++++--------- skills/velune-react/references/components.md | 87 +- skills/velune-react/references/patterns.md | 10 +- tooling/scripts/check-package-exports.mjs | 3 +- tooling/scripts/check-packed-package.mjs | 4 +- .../scripts/check-storybook-performance.mjs | 36 +- 154 files changed, 7960 insertions(+), 2616 deletions(-) create mode 100644 .changeset/modernize-component-library.md create mode 100644 packages/react/src/alert/Alert.a11y.test.tsx create mode 100644 packages/react/src/alert/Alert.stories.tsx create mode 100644 packages/react/src/alert/Alert.test.tsx create mode 100644 packages/react/src/alert/Alert.tsx create mode 100644 packages/react/src/alert/Alert.types.ts create mode 100644 packages/react/src/alert/index.ts create mode 100644 packages/react/src/breadcrumb/Breadcrumb.a11y.test.tsx create mode 100644 packages/react/src/breadcrumb/Breadcrumb.stories.tsx create mode 100644 packages/react/src/breadcrumb/Breadcrumb.test.tsx create mode 100644 packages/react/src/breadcrumb/Breadcrumb.tsx create mode 100644 packages/react/src/breadcrumb/Breadcrumb.types.ts create mode 100644 packages/react/src/breadcrumb/index.ts create mode 100644 packages/react/src/button/Button.classes.ts create mode 100644 packages/react/src/combobox/Combobox.a11y.test.tsx create mode 100644 packages/react/src/combobox/Combobox.stories.tsx create mode 100644 packages/react/src/combobox/Combobox.test.tsx create mode 100644 packages/react/src/combobox/Combobox.tsx create mode 100644 packages/react/src/combobox/Combobox.types.ts create mode 100644 packages/react/src/combobox/index.ts create mode 100644 packages/react/src/date-range-picker/DateRangeField.tsx create mode 100644 packages/react/src/form/standard-schema.ts create mode 100644 packages/react/src/input/Input.classes.ts create mode 100644 packages/react/src/shared/dialog.tsx create mode 100644 packages/react/src/shared/responsive.ts create mode 100644 packages/react/src/shared/slot.test.tsx create mode 100644 packages/react/src/shared/slot.tsx create mode 100644 packages/react/src/slider/Slider.a11y.test.tsx create mode 100644 packages/react/src/slider/Slider.stories.tsx create mode 100644 packages/react/src/slider/Slider.test.tsx create mode 100644 packages/react/src/slider/Slider.tsx create mode 100644 packages/react/src/slider/Slider.types.ts create mode 100644 packages/react/src/slider/index.ts create mode 100644 packages/react/src/tag/Tag.classes.ts create mode 100644 packages/react/src/theme/components/slider.css diff --git a/.changeset/modernize-component-library.md b/.changeset/modernize-component-library.md new file mode 100644 index 0000000..4e0f606 --- /dev/null +++ b/.changeset/modernize-component-library.md @@ -0,0 +1,25 @@ +--- +"velune": minor +--- + +Modernize the component library with new components, responsive layouts, and +consistent public APIs: + +- Add Alert, Breadcrumb, Slider, and Combobox, including accessible compound + slots, controlled state, keyboard support, and customizable presentation. +- Add Standard Schema validation to Form and share Modal/Drawer dialog + behavior through an internal primitive. +- Add breakpoint-responsive props to Box, Container, Flex, Grid, and Stack. +- Standardize polymorphic elements on `as`, including Container support. +- Add Button `asChild`, `tone="danger"`, and `buttonClasses()`; add style + recipes for Tag and Input. +- Add Combobox.NoMatches, controlled Alert visibility, Checkbox group slots, + Switch `lg`, TextArea `resize`, Pagination `hideOnSinglePage`, + `toast.promise`, Stack dividers, and Spinner labels. +- Remove the deprecated Stack `spacing` prop; use `gap`, including responsive + values, as the sole spacing API. +- Replace react-aria-components and @internationalized/date with in-house + Slider, Dropdown, and DateRangePicker implementations, reducing consumer + bundle sizes while preserving documented behavior. +- Improve floating-layer and modal motion, and show selection checks in + Select and Combobox. diff --git a/.size-limit.mjs b/.size-limit.mjs index ffcf747..8f76587 100644 --- a/.size-limit.mjs +++ b/.size-limit.mjs @@ -51,15 +51,19 @@ function configureConsumerBuild(config) { const componentBudgets = { "relief-card": "3 kB", + alert: "3 kB", avatar: "2.5 kB", badge: "2 kB", + breadcrumb: "2.5 kB", + combobox: "8 kB", + slider: "3 kB", box: "1.5 kB", button: "5.2 kB", card: "2.5 kB", checkbox: "4.5 kB", collapse: "4 kB", "date-picker": "9.5 kB", - "date-range-picker": "60 kB", + "date-range-picker": "6.5 kB", container: "1.2 kB", divider: "1.5 kB", drawer: "5.5 kB", @@ -83,7 +87,7 @@ const componentBudgets = { tabs: "4 kB", tag: "2.5 kB", text: "2 kB", - textarea: "4 kB", + "text-area": "4 kB", toast: "6 kB", tooltip: "5 kB", "virtual-table": "17 kB", @@ -102,7 +106,9 @@ const componentEntries = Object.entries(reactPackage.exports).flatMap( typeof definition === "object" && definition !== null ? definition.import?.match(/^\.\/dist\/([^/]+)\/index\.mjs$/) : null; - if (subpath === "." || !match || match[1] === "theme") return []; + // ./textarea is a deprecated alias of ./text-area; measure it once. + if (subpath === "." || subpath === "./textarea" || !match) return []; + if (match[1] === "theme") return []; const name = subpath.slice(2); const limit = componentBudgets[name]; @@ -131,7 +137,7 @@ export default [ packageArtifact( "velune/react (full package)", ["packages/velune/dist/react/*.mjs", "packages/velune/dist/react/*.css"], - "140 kB", + "160 kB", ), ...componentEntries, { @@ -139,7 +145,7 @@ export default [ path: "tooling/size-limit/react-basket.mjs", gzip: true, ignore: ["react", "react-dom"], - limit: "19.1 kB", + limit: "19.2 kB", modifyEsbuildConfig: configureConsumerBuild, }, packageArtifact("@velune/hooks", "packages/hooks/dist/index.mjs", "1.25 kB"), diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9b3c14..eecfa1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,6 +115,22 @@ Do not hand-roll established interaction engines when a proven dependency is already used by the repository. Keep public APIs consistent with neighboring components and prefer composition over a large set of presentation props. +### Polymorphism: `as` by default + +Velune standardizes on the `as` prop for element polymorphism. When a +component should render as a different element, type it with +`PolymorphicProps`/`PolymorphicComponent` from `packages/react/src/shared/polymorphic.ts` +and cover it in `packages/react/src/shared/polymorphic.type-test.tsx` +(see `Box`, `Container`, `Flex`, `Grid`, `Stack`, `Text`, `Card`, and the +`Modal.Title`/`Drawer.Title` slots). Constrained unions are acceptable when +only specific elements are safe, such as `Button`'s `as?: "button" | "a"`. + +Do not introduce Radix-style `asChild` props outside Button. When a component needs to +attach behavior to caller-provided markup, expose a compound trigger slot +that clones its single child instead (`Tooltip.Trigger`, `Popover.Trigger`, +`Dropdown.Trigger`), keeping behavior injection and element identity as two +separate, predictable APIs. + ## Verification Run the smallest relevant checks while iterating, followed by the complete diff --git a/README.md b/README.md index b323973..510f16e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ interaction states. ## Highlights -- 38 accessible React components with compound APIs and per-component exports +- 42 accessible React components with compound APIs and per-component exports - Tailwind CSS v4 utilities backed by public semantic design tokens - Light, dark, high-contrast, reduced-motion, and generated brand themes - ESM, CommonJS, type declarations, and automated quality gates diff --git a/apps/docs/src/ComponentPreview.tsx b/apps/docs/src/ComponentPreview.tsx index bea1a4f..d5e5ec4 100644 --- a/apps/docs/src/ComponentPreview.tsx +++ b/apps/docs/src/ComponentPreview.tsx @@ -2,13 +2,16 @@ import { useState, type ReactNode } from "react"; import { ArrowRight, Check } from "lucide-react"; import { ReliefCard, + Alert, Avatar, Badge, Box, + Breadcrumb, Button, Card, Checkbox, Collapse, + Combobox, Container, DatePicker, DateRangePicker, @@ -27,6 +30,7 @@ import { Radio, Select, Skeleton, + Slider, Spinner, Stack, Switch, @@ -260,6 +264,36 @@ function WizardPreview() { ); } +function ComboboxPreview({ controlled = false }: { controlled?: boolean }) { + const [value, setValue] = useState("berlin"); + + if (controlled) { + return ( + + + City + Berlin + Lisbon + Tokyo + No matching city. + + + ); + } + + return ( + + + Framework + React + Vue + Svelte + Solid + + + ); +} + function ExampleVariantPreview({ entry, exampleId, @@ -270,6 +304,91 @@ function ExampleVariantPreview({ const key = `${entry.slug}:${exampleId}`; switch (key) { + case "alert:tones": + return ( + + Scheduled maintenance starts at 22:00 UTC. + Workspace settings saved. + Your trial ends in 3 days. + Payment failed. Update your billing details. + + ); + case "alert:structured": + return ( + + + Storage almost full + + You have used 9.5 GB of your 10 GB quota. Remove unused assets or + upgrade your plan. + + + + ); + case "alert:closable": + return ( + + + New in Velune + + Slider and Combobox are now available. + + + + ); + case "breadcrumb:basic": + return ( + + Home + Projects + Northstar + + ); + case "breadcrumb:separator": + return ( + + Home + Docs + Components + + ); + case "combobox:basic": + return ; + case "combobox:controlled": + return ; + case "slider:basic": + return ( + + + Volume + + + + ); + case "slider:range": + return ( + + + Price range + + + + ); + case "slider:format": + return ( + + + Opacity + + + + ); case "relief-card:hero": return ( @@ -375,7 +494,7 @@ function ExampleVariantPreview({ - + ); case "button:sizes": @@ -410,6 +529,14 @@ function ExampleVariantPreview({ ); + case "button:as-child": + return ( + + + + ); case "card:variants": return ( @@ -617,7 +744,7 @@ function ExampleVariantPreview({ Account - Security preferences + Security preferences ); case "drawer:placements": @@ -638,10 +765,10 @@ function ExampleVariantPreview({ - Edit project - Duplicate + Edit project + Duplicate - + Delete project @@ -656,7 +783,7 @@ function ExampleVariantPreview({ Workspace - + Profile Manage your public identity @@ -665,7 +792,7 @@ function ExampleVariantPreview({ - Sign out + Sign out ); @@ -680,10 +807,10 @@ function ExampleVariantPreview({ selectionMode="multiple" defaultSelectedKeys={["name", "status"]} > - Name - Owner - Status - Last updated + Name + Owner + Status + Last updated ); @@ -948,7 +1075,7 @@ function ExampleVariantPreview({ return ( Profile - Account preferences + Account preferences ); @@ -1068,7 +1195,7 @@ function ExampleVariantPreview({ Heading Readable body copy. - + Supporting information. @@ -1189,6 +1316,37 @@ export function ComponentPreview({ } switch (entry.slug) { + case "alert": + return ( + + Scheduled maintenance starts at 22:00 UTC. + + Workspace settings saved + + Changes are visible to everyone in this workspace. + + + + ); + case "breadcrumb": + return ( + + Home + Projects + Northstar + + ); + case "combobox": + return ; + case "slider": + return ( + + + Volume + + + + ); case "button": return ( @@ -1267,10 +1425,10 @@ export function ComponentPreview({ - Edit project - Duplicate + Edit project + Duplicate - + Delete project @@ -1383,7 +1541,7 @@ export function ComponentPreview({ Account - + Profile and security preferences. @@ -1397,7 +1555,7 @@ export function ComponentPreview({ Body text stays calm and readable across dense product surfaces. - + Supporting information uses a quieter tone. @@ -1410,7 +1568,7 @@ export function ComponentPreview({ className="max-w-md rounded-gs-md bg-gs-surface" > Token-aware surface - + Spacing and element semantics are controlled through props. @@ -1428,7 +1586,7 @@ export function ComponentPreview({ - + A quiet filled surface for related project information. @@ -1444,7 +1602,7 @@ export function ComponentPreview({ return ( Constrained content - + Container keeps page content aligned at a predictable width. @@ -1517,7 +1675,7 @@ export function ComponentPreview({ Share project - + Anyone with the link can view this board. @@ -1529,7 +1687,7 @@ export function ComponentPreview({ return ( Profile - + Vertical rhythm stays aligned to the global spacing scale. @@ -1583,7 +1741,7 @@ export function ComponentPreview({ Available - + Theme-aware, accessible, and typed. diff --git a/apps/docs/src/DocsPages.tsx b/apps/docs/src/DocsPages.tsx index 3e0839f..df1e2f2 100644 --- a/apps/docs/src/DocsPages.tsx +++ b/apps/docs/src/DocsPages.tsx @@ -158,7 +158,7 @@ function DocsShell({ {title} - + {description} @@ -216,7 +216,7 @@ function DocSection({ {title} {description ? ( - + {description} ) : null} @@ -245,7 +245,7 @@ function DocCode({ gap="3" className="h-11 border-b border-gs-default px-3" > - + {label ?? language} -`, +`, }, { id: "sizes", @@ -175,6 +238,15 @@ const specs: Record = { body: ` `, }, + { + id: "as-child", + title: "Render as child", + description: + "Use asChild to style an existing element, such as a router link, as a button.", + body: ``, + }, ], card: [ { @@ -303,6 +375,14 @@ const specs: Record = { Medium content Large content Extra-large content`, + }, + { + id: "responsive", + title: "Responsive size", + description: "Change the maximum content width at a chosen breakpoint.", + body: ` + Content grows with the viewport. +`, }, { id: "semantic", @@ -409,7 +489,7 @@ const specs: Record = { description: "Separate nearby content regions with a quiet rule.", body: `Account -Security preferences`, +Security preferences`, imports: ["Text"], }, { @@ -465,10 +545,10 @@ const specs: Record = { - Edit project - Duplicate + Edit project + Duplicate - Delete project + Delete project `, imports: ["Button"], @@ -485,14 +565,14 @@ const specs: Record = { Workspace - + Profile Manage your public identity ⌘P - Sign out + Sign out `, imports: ["Button"], @@ -511,10 +591,10 @@ const specs: Record = { selectionMode="multiple" defaultSelectedKeys={["name", "status"]} > - Name - Owner - Status - Last updated + Name + Owner + Status + Last updated `, imports: ["Button"], @@ -543,6 +623,16 @@ const specs: Record = { `, imports: ["Tag"], }, + { + id: "responsive", + title: "Responsive direction", + description: "Stack controls on narrow screens and align them horizontally when space allows.", + body: ` + + +`, + imports: ["Button"], + }, ], form: [ { @@ -599,7 +689,7 @@ const specs: Record = { id: "responsive", title: "Responsive grid", description: "Collapse dense layouts for smaller viewports.", - body: ` + body: ` {items.map((item) => {item.name})} `, imports: ["Card"], @@ -915,7 +1005,7 @@ const specs: Record = { description: "Arrange related content with tokenized spacing.", body: ` Profile - Account preferences + Account preferences `, imports: ["Button", "Text"], @@ -930,6 +1020,16 @@ const specs: Record = { `, imports: ["Button", "Text"], }, + { + id: "responsive", + title: "Responsive rhythm", + description: "Change spacing or order without adding breakpoint wrappers.", + body: ` + Primary content + Supporting content +`, + imports: ["Text"], + }, ], switch: [ { @@ -1060,7 +1160,7 @@ const members = [ description: "Express semantic hierarchy with the shared type scale.", body: `Heading Readable body copy. -Supporting information.`, +Supporting information.`, }, { id: "tones", @@ -1191,6 +1291,69 @@ const rows = Array.from({ length: 1000 }, (_, index) => ({ }));`, }, ], + combobox: [ + { + id: "basic", + title: "Searchable selection", + description: "Type to filter options, then pick a suggestion.", + body: ` + Framework + React + Vue + Svelte + Solid +`, + }, + { + id: "controlled", + title: "Controlled value", + description: "Own the selected key in application state.", + body: ` + City + Berlin + Lisbon + Tokyo + No matching city. +`, + reactImports: ["useState"], + setup: `const [value, setValue] = useState("berlin");`, + }, + ], + slider: [ + { + id: "basic", + title: "Single value", + description: "Label and live output pair with the slider track.", + body: ` + Volume + +`, + }, + { + id: "range", + title: "Range selection", + description: "Pass an array to render one thumb per value.", + body: ` + Price range + +`, + }, + { + id: "format", + title: "Formatted output", + description: "Format values with Intl.NumberFormat options.", + body: ` + Opacity + +`, + }, + ], wizard: [ { id: "steps", diff --git a/apps/docs/src/router.tsx b/apps/docs/src/router.tsx index ff1415d..22aad3e 100644 --- a/apps/docs/src/router.tsx +++ b/apps/docs/src/router.tsx @@ -200,7 +200,7 @@ function SidebarContent({ onClick={onNavigate} > Components{" "} - + {components.length} @@ -301,7 +301,7 @@ function GlobalSearch() { key={item.slug} type="button" variant="ghost" - block + fullWidth className="h-auto justify-start px-3 py-2 text-left [&_.gs-button-content]:w-full [&_.gs-button-icon]:text-gs-secondary [&_.gs-button-label]:min-w-0 [&_.gs-button-label]:flex-1" onClick={() => { setQuery(""); @@ -316,7 +316,7 @@ function GlobalSearch() { {item.name} - + {item.category} @@ -513,7 +513,7 @@ function HomeDashboardPreview() { - + example.com / overview @@ -542,7 +542,7 @@ function HomeDashboardPreview() { > - - - - or - - + New to Velune?{" "} - Product workspace - Design system + Product workspace + Design system - New workspace + New workspace ) : null} @@ -248,10 +248,10 @@ export function SidebarTemplate() { - Profile - Preferences + Profile + Preferences - + Sign out @@ -302,7 +302,7 @@ export function SidebarTemplate() { Overview - + Thursday, July 16 @@ -319,7 +319,7 @@ export function SidebarTemplate() { Welcome back, Maya - Here is what changed across your workspace. + Here is what changed across your workspace. - + --easing-standard diff --git a/apps/storybook/src/tokens.stories.tsx b/apps/storybook/src/tokens.stories.tsx index ef9eef1..1f595d7 100644 --- a/apps/storybook/src/tokens.stories.tsx +++ b/apps/storybook/src/tokens.stories.tsx @@ -28,7 +28,7 @@ function TokenRow({ name, value }: { name: string; value: string }) { --{name} - + {value} @@ -47,7 +47,7 @@ export const TokenIndex: Story = { Public token index - + Runtime values generated from packages/react/tokens.json. New components consume semantic or component tokens only. @@ -76,7 +76,7 @@ export const Spacing: Story = { Velune spacing - + Page layout follows an 8pt rhythm. The 4px atomic token remains for optical correction and compact component internals. @@ -88,7 +88,7 @@ export const Spacing: Story = { {name} - + {value} · {use} @@ -109,7 +109,7 @@ export const Spacing: Story = { control-hit-target - + 44px · minimum interactive area diff --git a/package.json b/package.json index ea06492..1bf2b91 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,16 @@ "node": ">=20", "pnpm": ">=9" }, + "pnpm": { + "overrides": { + "@babel/core": "7.29.7", + "brace-expansion@5.0.5": "5.0.7", + "esbuild": "0.28.1", + "fast-uri@3.1.2": "3.1.4", + "js-yaml@3.14.2": "3.15.0", + "js-yaml@4.1.1": "4.3.0" + } + }, "scripts": { "dev": "pnpm --filter @velune/docs dev", "build": "pnpm -r --if-present run build", diff --git a/packages/react/package.json b/packages/react/package.json index 24778c2..f839372 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -200,6 +200,11 @@ "import": "./dist/text/index.mjs", "require": "./dist/text/index.cjs" }, + "./text-area": { + "types": "./dist/text-area/index.d.ts", + "import": "./dist/text-area/index.mjs", + "require": "./dist/text-area/index.cjs" + }, "./textarea": { "types": "./dist/text-area/index.d.ts", "import": "./dist/text-area/index.mjs", @@ -225,6 +230,26 @@ "import": "./dist/wizard/index.mjs", "require": "./dist/wizard/index.cjs" }, + "./alert": { + "types": "./dist/alert/index.d.ts", + "import": "./dist/alert/index.mjs", + "require": "./dist/alert/index.cjs" + }, + "./breadcrumb": { + "types": "./dist/breadcrumb/index.d.ts", + "import": "./dist/breadcrumb/index.mjs", + "require": "./dist/breadcrumb/index.cjs" + }, + "./combobox": { + "types": "./dist/combobox/index.d.ts", + "import": "./dist/combobox/index.mjs", + "require": "./dist/combobox/index.cjs" + }, + "./slider": { + "types": "./dist/slider/index.d.ts", + "import": "./dist/slider/index.mjs", + "require": "./dist/slider/index.cjs" + }, "./tailwind.css": "./dist/tailwind.css", "./theme.css": "./dist/theme/index.css", "./theme": { @@ -253,16 +278,15 @@ "tailwindcss": ">=4" }, "dependencies": { - "@velune/hooks": "workspace:*", - "@internationalized/date": "^3.12.2", "@tanstack/react-virtual": "^3.14.5", - "clsx": "^2.1.1", - "react-aria-components": "^1.19.0" + "@velune/hooks": "workspace:*", + "clsx": "^2.1.1" }, "devDependencies": { "@testing-library/react": "^16.3.2", "jsdom": "^29.1.1", - "lucide-react": "^1.24.0" + "lucide-react": "^1.24.0", + "zod": "^4.4.3" }, "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/react/src/alert/Alert.a11y.test.tsx b/packages/react/src/alert/Alert.a11y.test.tsx new file mode 100644 index 0000000..c38cc58 --- /dev/null +++ b/packages/react/src/alert/Alert.a11y.test.tsx @@ -0,0 +1,45 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { Alert } from "./Alert"; + +afterEach(cleanup); + +describe("Alert a11y", () => { + it("uses role=status for calm tones and role=alert for urgent tones", () => { + const { rerender } = render(Heads up); + expect(screen.getByRole("status")).toBeTruthy(); + + rerender(Saved); + expect(screen.getByRole("status")).toBeTruthy(); + + rerender(Careful); + expect(screen.getByRole("alert")).toBeTruthy(); + + rerender(Failed); + expect(screen.getByRole("alert")).toBeTruthy(); + }); + + it("lets an explicit role win over the tone mapping", () => { + render( + + Archived + , + ); + + expect(screen.getByRole("note")).toBeTruthy(); + }); + + it("exposes an accessible name on the dismiss button and hides icons", () => { + render( + + Storage almost full + , + ); + + expect(screen.getByRole("button", { name: "Dismiss" })).toBeTruthy(); + const icon = document.querySelector(".gs-alert-icon")!; + expect(icon.getAttribute("aria-hidden")).toBe("true"); + }); +}); diff --git a/packages/react/src/alert/Alert.stories.tsx b/packages/react/src/alert/Alert.stories.tsx new file mode 100644 index 0000000..71b0884 --- /dev/null +++ b/packages/react/src/alert/Alert.stories.tsx @@ -0,0 +1,78 @@ +import type { CSSProperties } from "react"; +import { Alert } from "./Alert"; + +const meta = { + title: "React/Alert", + component: Alert, +}; + +export default meta; + +const stackStyle: CSSProperties = { + display: "grid", + gap: "var(--space-3)", + maxWidth: 560, +}; + +export const Tones = { + render: () => ( +
+ A neutral note about this workspace. + A new version is available. + Your changes have been saved. + Storage is almost full. + Deployment failed. Check the logs. +
+ ), +}; + +export const WithTitleAndDescription = { + render: () => ( +
+ + Deploy complete + + All 12 checks passed. The new revision is now serving traffic. + + +
+ ), +}; + +export const Closable = { + render: () => ( +
+ + Scheduled maintenance + + The API will be read-only on Sunday between 02:00 and 03:00 UTC. + + +
+ ), +}; + +export const CustomIcon = { + render: () => ( +
+ + + An icon-less alert for dense layouts. + +
+ ), +}; diff --git a/packages/react/src/alert/Alert.test.tsx b/packages/react/src/alert/Alert.test.tsx new file mode 100644 index 0000000..1f599bb --- /dev/null +++ b/packages/react/src/alert/Alert.test.tsx @@ -0,0 +1,175 @@ +// @vitest-environment jsdom + +import { createRef } from "react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Alert } from "./Alert"; + +afterEach(cleanup); + +describe("Alert", () => { + it("sets readable displayNames", () => { + expect(Alert.displayName).toBe("Alert"); + expect((Alert.Title as { displayName?: string }).displayName).toBe( + "Alert.Title", + ); + expect((Alert.Description as { displayName?: string }).displayName).toBe( + "Alert.Description", + ); + }); + + it("forwards its ref and DOM props", () => { + const ref = createRef(); + render( + + Saved + , + ); + + const element = screen.getByTestId("alert"); + expect(ref.current).toBe(element); + expect(element.getAttribute("data-state")).toBe("ready"); + }); + + it("maps tones to data-tone and tone utilities", () => { + const { rerender } = render(Note); + const alert = screen.getByTestId("alert"); + expect(alert.getAttribute("data-tone")).toBe("info"); + expect(alert.classList.contains("bg-gs-info-subtle")).toBe(true); + + rerender( + + Failed + , + ); + expect(alert.getAttribute("data-tone")).toBe("error"); + expect(alert.classList.contains("bg-gs-error-subtle")).toBe(true); + }); + + it("renders title and description slots with plain children", () => { + render( + + Deploy complete + All checks passed. + View logs + , + ); + + expect( + screen.getByText("Deploy complete").classList.contains("gs-alert-title"), + ).toBe(true); + expect( + screen + .getByText("All checks passed.") + .classList.contains("gs-alert-description"), + ).toBe(true); + expect(screen.getByRole("link", { name: "View logs" })).toBeTruthy(); + }); + + it("forwards refs on title and description subcomponents", () => { + const titleRef = createRef(); + const descriptionRef = createRef(); + render( + + Deploy complete + + All checks passed. + + , + ); + + expect(titleRef.current?.textContent).toBe("Deploy complete"); + expect(descriptionRef.current?.textContent).toBe("All checks passed."); + }); + + it("keeps subcomponents in author order and supports wrappers", () => { + render( + + Details first + Title second + , + ); + + const content = screen + .getByTestId("alert") + .querySelector(".gs-alert-content"); + expect( + content?.children[0]?.classList.contains("gs-alert-description"), + ).toBe(true); + expect(content?.children[1]?.classList.contains("gs-alert-title")).toBe( + true, + ); + }); + + it("dismisses itself and calls onClose when closable", () => { + const onClose = vi.fn(); + render( + + Temporary note + , + ); + + const close = screen.getByRole("button", { name: "Dismiss" }); + expect(close.classList.contains("active:scale-95")).toBe(true); + expect(close.classList.contains("motion-reduce:transition-none")).toBe( + true, + ); + fireEvent.click(close); + expect(onClose).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId("alert")).toBeNull(); + }); + + it("supports controlled visibility via open/onOpenChange", () => { + const onOpenChange = vi.fn(); + const { rerender } = render( + + Controlled note + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); + expect(onOpenChange).toHaveBeenCalledWith(false); + // Controlled: stays visible until the owner flips the prop. + expect(screen.getByTestId("alert")).toBeTruthy(); + + rerender( + + Controlled note + , + ); + expect(screen.queryByTestId("alert")).toBeNull(); + }); + + it("supports a custom close label", () => { + render( + + Note + , + ); + + expect(screen.getByRole("button", { name: "Hide alert" })).toBeTruthy(); + }); + + it("renders a custom icon and hides the icon for icon={null}", () => { + const { rerender } = render( + }> + Note + , + ); + expect(screen.getByTestId("custom-icon")).toBeTruthy(); + + rerender( + + Note + , + ); + expect( + screen.getByTestId("alert").querySelector(".gs-alert-icon"), + ).toBeNull(); + }); +}); diff --git a/packages/react/src/alert/Alert.tsx b/packages/react/src/alert/Alert.tsx new file mode 100644 index 0000000..2654117 --- /dev/null +++ b/packages/react/src/alert/Alert.tsx @@ -0,0 +1,229 @@ +import type { ForwardedRef, ReactNode } from "react"; +import { forwardRef } from "react"; +import { useControllableState } from "@velune/hooks"; +import { clsx } from "clsx"; +import type { + AlertDescriptionProps, + AlertProps, + AlertTitleProps, + AlertTone, +} from "./Alert.types"; + +const toneClasses: Record = { + neutral: "border-gs-default bg-gs-surface-mist text-gs-text", + info: "border-gs-info bg-gs-info-subtle text-gs-text", + success: "border-gs-success bg-gs-success-subtle text-gs-text", + warning: "border-gs-warning bg-gs-warning-subtle text-gs-text", + error: "border-gs-error bg-gs-error-subtle text-gs-text", +}; + +const toneIconClasses: Record = { + neutral: "text-gs-text-secondary", + info: "text-gs-info", + success: "text-gs-success", + warning: "text-gs-warning", + error: "text-gs-error", +}; + +function InfoIcon() { + return ( + + ); +} + +function SuccessIcon() { + return ( + + ); +} + +function WarningIcon() { + return ( + + ); +} + +function ErrorIcon() { + return ( + + ); +} + +function CloseIcon() { + return ( + + ); +} + +const toneIcons: Record = { + neutral: , + info: , + success: , + warning: , + error: , +}; + +function AlertImpl( + { + tone = "info", + icon, + closable = false, + open: openProp, + defaultOpen = true, + onOpenChange, + onClose, + closeLabel = "Dismiss", + role, + className, + children, + ...props + }: AlertProps, + ref: ForwardedRef, +) { + const [open, setOpen] = useControllableState({ + value: openProp, + defaultValue: defaultOpen, + onChange: onOpenChange, + }); + + if (!open) { + return null; + } + + const resolvedRole = + role ?? (tone === "error" || tone === "warning" ? "alert" : "status"); + const resolvedIcon = icon === undefined ? toneIcons[tone] : icon; + + return ( +
+ {resolvedIcon != null ? ( + *]:block [&>*]:size-full", + toneIconClasses[tone], + )} + aria-hidden="true" + > + {resolvedIcon} + + ) : null} +
+ {children} +
+ {closable ? ( + + ) : null} +
+ ); +} + +const AlertRoot = forwardRef(AlertImpl); +AlertRoot.displayName = "Alert"; + +function AlertTitleImpl( + { className, ...props }: AlertTitleProps, + ref: ForwardedRef, +) { + return ( +
+ ); +} + +const AlertTitle = forwardRef(AlertTitleImpl); +AlertTitle.displayName = "Alert.Title"; + +function AlertDescriptionImpl( + { className, ...props }: AlertDescriptionProps, + ref: ForwardedRef, +) { + return ( +
+ ); +} + +const AlertDescription = forwardRef(AlertDescriptionImpl); +AlertDescription.displayName = "Alert.Description"; + +export const Alert = Object.assign(AlertRoot, { + Title: AlertTitle, + Description: AlertDescription, +}); diff --git a/packages/react/src/alert/Alert.types.ts b/packages/react/src/alert/Alert.types.ts new file mode 100644 index 0000000..96e593b --- /dev/null +++ b/packages/react/src/alert/Alert.types.ts @@ -0,0 +1,36 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +export type AlertTone = "neutral" | "info" | "success" | "warning" | "error"; + +export interface AlertProps extends HTMLAttributes { + /** + * Visual and semantic tone. Defaults to `"info"`. + * + * @example + * Saved. + */ + tone?: AlertTone; + /** Custom leading icon. Pass `null` to hide the tone icon. */ + icon?: ReactNode; + /** Shows a dismiss button that hides the alert. */ + closable?: boolean; + /** Controls visibility. Use with `onOpenChange` for controlled dismissal. */ + open?: boolean; + /** Initial visibility for uncontrolled usage. Default: `true`. */ + defaultOpen?: boolean; + /** Fires when the dismiss button requests a visibility change. */ + onOpenChange?: (open: boolean) => void; + /** Fires when the dismiss button is pressed. */ + onClose?: () => void; + /** Accessible name for the dismiss button. Defaults to `"Dismiss"`. */ + closeLabel?: string; + children?: ReactNode; +} + +export interface AlertTitleProps extends HTMLAttributes { + children?: ReactNode; +} + +export interface AlertDescriptionProps extends HTMLAttributes { + children?: ReactNode; +} diff --git a/packages/react/src/alert/index.ts b/packages/react/src/alert/index.ts new file mode 100644 index 0000000..08d9312 --- /dev/null +++ b/packages/react/src/alert/index.ts @@ -0,0 +1,7 @@ +export { Alert } from "./Alert"; +export type { + AlertDescriptionProps, + AlertProps, + AlertTitleProps, + AlertTone, +} from "./Alert.types"; diff --git a/packages/react/src/box/Box.stories.tsx b/packages/react/src/box/Box.stories.tsx index 718fd27..9a528a6 100644 --- a/packages/react/src/box/Box.stories.tsx +++ b/packages/react/src/box/Box.stories.tsx @@ -10,3 +10,14 @@ export default meta; export const Default = { render: () => Box, }; + +export const Responsive = { + render: () => ( + + Responsive box + + ), +}; diff --git a/packages/react/src/box/Box.test.tsx b/packages/react/src/box/Box.test.tsx index 04a1e3c..f904164 100644 --- a/packages/react/src/box/Box.test.tsx +++ b/packages/react/src/box/Box.test.tsx @@ -33,4 +33,17 @@ describe("Box", () => { ]), ); }); + + it("maps responsive spacing and display values", () => { + render( + , + ); + expect(Array.from(screen.getByTestId("box").classList)).toEqual( + expect.arrayContaining(["p-2", "md:p-6", "block", "md:grid"]), + ); + }); }); diff --git a/packages/react/src/box/Box.tsx b/packages/react/src/box/Box.tsx index d5b8836..31671c1 100644 --- a/packages/react/src/box/Box.tsx +++ b/packages/react/src/box/Box.tsx @@ -4,6 +4,7 @@ import { clsx } from "clsx"; import type { BoxProps } from "./Box.types"; import type { PolymorphicComponent } from "../shared/polymorphic"; import { marginClasses, paddingClasses } from "../shared/tailwind-classes"; +import { responsiveClasses } from "../shared/responsive"; const displayClasses = { block: "block", @@ -33,14 +34,14 @@ function BoxImpl( ref, className: clsx( "gs-box box-border", - padding && paddingClasses[padding], - margin && marginClasses[margin], - display && displayClasses[display], + responsiveClasses(padding, paddingClasses), + responsiveClasses(margin, marginClasses), + responsiveClasses(display, displayClasses), className, ), - "data-padding": padding, - "data-margin": margin, - "data-display": display, + "data-padding": typeof padding === "string" ? padding : undefined, + "data-margin": typeof margin === "string" ? margin : undefined, + "data-display": typeof display === "string" ? display : undefined, ...props, }, children, diff --git a/packages/react/src/box/Box.types.ts b/packages/react/src/box/Box.types.ts index ebab37f..4730431 100644 --- a/packages/react/src/box/Box.types.ts +++ b/packages/react/src/box/Box.types.ts @@ -1,5 +1,6 @@ import type { ElementType } from "react"; import type { PolymorphicProps } from "../shared/polymorphic"; +import type { Responsive } from "../shared/responsive"; export type BoxSpacing = | "0" @@ -27,9 +28,9 @@ export type BoxDisplay = | "none"; export interface BoxOwnProps { - padding?: BoxSpacing; - margin?: BoxSpacing; - display?: BoxDisplay; + padding?: Responsive; + margin?: Responsive; + display?: Responsive; } export type BoxProps = PolymorphicProps< diff --git a/packages/react/src/breadcrumb/Breadcrumb.a11y.test.tsx b/packages/react/src/breadcrumb/Breadcrumb.a11y.test.tsx new file mode 100644 index 0000000..50e5b47 --- /dev/null +++ b/packages/react/src/breadcrumb/Breadcrumb.a11y.test.tsx @@ -0,0 +1,39 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { Breadcrumb } from "./Breadcrumb"; + +afterEach(cleanup); + +describe("Breadcrumb a11y", () => { + it("exposes a named navigation landmark with the current page", () => { + render( + + Home + Projects + Velune + , + ); + + const navigation = screen.getByRole("navigation", { name: "Breadcrumb" }); + const current = screen.getByText("Velune"); + expect(navigation.contains(current)).toBe(true); + expect(current.getAttribute("aria-current")).toBe("page"); + }); + + it("supports a custom landmark label and hides separators", () => { + render( + + Home + Docs + , + ); + + expect( + screen.getByRole("navigation", { name: "You are here" }), + ).toBeTruthy(); + const separator = document.querySelector(".gs-breadcrumb-separator")!; + expect(separator.getAttribute("aria-hidden")).toBe("true"); + }); +}); diff --git a/packages/react/src/breadcrumb/Breadcrumb.stories.tsx b/packages/react/src/breadcrumb/Breadcrumb.stories.tsx new file mode 100644 index 0000000..18c2a2b --- /dev/null +++ b/packages/react/src/breadcrumb/Breadcrumb.stories.tsx @@ -0,0 +1,39 @@ +import { Breadcrumb } from "./Breadcrumb"; + +const meta = { + title: "React/Breadcrumb", + component: Breadcrumb, +}; + +export default meta; + +export const Default = { + render: () => ( + + Home + Workspace + Projects + Velune design system + + ), +}; + +export const CustomSeparator = { + render: () => ( + + Home + Settings + Notifications + + ), +}; + +export const WithoutLinks = { + render: () => ( + + Wizard + Billing details + Confirmation + + ), +}; diff --git a/packages/react/src/breadcrumb/Breadcrumb.test.tsx b/packages/react/src/breadcrumb/Breadcrumb.test.tsx new file mode 100644 index 0000000..ca3baa0 --- /dev/null +++ b/packages/react/src/breadcrumb/Breadcrumb.test.tsx @@ -0,0 +1,122 @@ +// @vitest-environment jsdom + +import { createRef } from "react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Breadcrumb } from "./Breadcrumb"; + +afterEach(cleanup); + +function BasicBreadcrumb() { + return ( + + Home + Projects + Velune + + ); +} + +describe("Breadcrumb", () => { + it("sets readable displayNames", () => { + expect(Breadcrumb.displayName).toBe("Breadcrumb"); + expect((Breadcrumb.Item as { displayName?: string }).displayName).toBe( + "Breadcrumb.Item", + ); + }); + + it("forwards its ref and renders a nav > ol > li structure", () => { + const ref = createRef(); + render( + + Home + Docs + , + ); + + const nav = screen.getByTestId("breadcrumb"); + expect(ref.current).toBe(nav); + expect(nav.tagName).toBe("NAV"); + const list = nav.querySelector("ol")!; + expect(list).toBeTruthy(); + expect(list.querySelectorAll("li.gs-breadcrumb-item")).toHaveLength(2); + }); + + it("renders separators between items and supports a custom separator", () => { + const { rerender } = render(); + let separators = document.querySelectorAll(".gs-breadcrumb-separator"); + expect(separators).toHaveLength(2); + expect(separators[0]!.querySelector("svg")).toBeTruthy(); + + rerender( + + Home + Docs + , + ); + separators = document.querySelectorAll(".gs-breadcrumb-separator"); + expect(separators).toHaveLength(1); + expect(separators[0]!.textContent).toBe("/"); + }); + + it("marks the last item as the current page automatically", () => { + render(); + + const current = screen.getByText("Velune"); + expect(current.getAttribute("aria-current")).toBe("page"); + expect(current.closest("li")!.getAttribute("data-current")).toBe("true"); + expect(screen.getByText("Home").getAttribute("aria-current")).toBeNull(); + }); + + it("lets an explicit current item override the default", () => { + render( + + Home + Projects + Velune + , + ); + + expect(screen.getByText("Projects").getAttribute("aria-current")).toBe( + "page", + ); + expect(screen.getByText("Velune").getAttribute("aria-current")).toBeNull(); + }); + + it("renders anchors for linked items and fires onClick", () => { + const onClick = vi.fn((event: { preventDefault: () => void }) => + event.preventDefault(), + ); + render( + + + Projects + + Velune + , + ); + + const link = screen.getByRole("link", { name: "Projects" }); + expect(link.getAttribute("href")).toBe("/projects"); + fireEvent.click(link); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it("renders disabled items as inert text", () => { + const onClick = vi.fn(); + render( + + + Restricted + + Velune + , + ); + + expect(screen.queryByRole("link", { name: "Restricted" })).toBeNull(); + const item = screen.getByText("Restricted"); + fireEvent.click(item); + expect(onClick).not.toHaveBeenCalled(); + expect(item.closest("li")!.getAttribute("data-disabled")).toBe("true"); + }); +}); diff --git a/packages/react/src/breadcrumb/Breadcrumb.tsx b/packages/react/src/breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000..176779f --- /dev/null +++ b/packages/react/src/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,167 @@ +import type { ForwardedRef, ReactElement, ReactNode } from "react"; +import { forwardRef } from "react"; +import { clsx } from "clsx"; +import { + createCompoundSlot, + dispatchCompoundSlots, +} from "../shared/compound-slot"; +import type { BreadcrumbItemProps, BreadcrumbProps } from "./Breadcrumb.types"; + +const itemContentClasses = + "gs-breadcrumb-item-content inline-flex max-w-full min-w-0 items-center truncate rounded-gs-xs leading-gs-normal transition-colors duration-150 ease-gs-standard motion-reduce:transition-none"; + +const linkClasses = + "cursor-pointer text-gs-text-secondary no-underline hover:text-gs-text hover:underline focus-visible:outline-none focus-visible:shadow-gs-button-focus-border"; + +function ChevronIcon() { + return ( + + ); +} + +function renderItem( + props: BreadcrumbItemProps, + key: number, + isCurrent: boolean, +): ReactElement { + const { + href, + target, + current: _current, + disabled = false, + onClick, + className, + children, + ...itemProps + } = props; + void _current; + + const handleClick: BreadcrumbItemProps["onClick"] = (event) => { + if (disabled) { + event.preventDefault(); + return; + } + onClick?.(event); + }; + + const content = + href !== undefined && !disabled ? ( + + {children} + + ) : ( + + {children} + + ); + + return ( +
  • + {content} +
  • + ); +} + +function BreadcrumbImpl( + { + separator, + className, + children, + "aria-label": ariaLabel = "Breadcrumb", + ...props + }: BreadcrumbProps, + ref: ForwardedRef, +) { + const items: BreadcrumbItemProps[] = []; + dispatchCompoundSlots( + children, + { + "Breadcrumb.Item": (child) => { + items.push(child.props as BreadcrumbItemProps); + }, + }, + () => {}, + ); + + const hasExplicitCurrent = items.some((item) => item.current); + const nodes: ReactNode[] = []; + items.forEach((item, index) => { + const isCurrent = hasExplicitCurrent + ? Boolean(item.current) + : index === items.length - 1; + nodes.push(renderItem(item, index, isCurrent)); + if (index < items.length - 1) { + nodes.push( + , + ); + } + }); + + return ( + + ); +} + +const BreadcrumbRoot = forwardRef(BreadcrumbImpl); +BreadcrumbRoot.displayName = "Breadcrumb"; + +const BreadcrumbItem = + createCompoundSlot("Breadcrumb.Item"); +(BreadcrumbItem as { displayName?: string }).displayName = "Breadcrumb.Item"; + +export const Breadcrumb = Object.assign(BreadcrumbRoot, { + Item: BreadcrumbItem, +}); diff --git a/packages/react/src/breadcrumb/Breadcrumb.types.ts b/packages/react/src/breadcrumb/Breadcrumb.types.ts new file mode 100644 index 0000000..f40682d --- /dev/null +++ b/packages/react/src/breadcrumb/Breadcrumb.types.ts @@ -0,0 +1,35 @@ +import type { + HTMLAttributes, + LiHTMLAttributes, + MouseEventHandler, + ReactNode, +} from "react"; + +export interface BreadcrumbProps extends HTMLAttributes { + /** + * Node rendered between items. Defaults to a chevron glyph. + * + * @example + * + */ + separator?: ReactNode; + children?: ReactNode; +} + +export interface BreadcrumbItemProps extends Omit< + LiHTMLAttributes, + "onClick" +> { + /** Renders the item content as a link. */ + href?: string; + target?: string; + /** + * Marks this item as the current page (`aria-current="page"`). When no item + * sets it explicitly, the last item is the current page. + */ + current?: boolean; + disabled?: boolean; + /** Fires on the item content (link or text). Ignored when disabled. */ + onClick?: MouseEventHandler; + children?: ReactNode; +} diff --git a/packages/react/src/breadcrumb/index.ts b/packages/react/src/breadcrumb/index.ts new file mode 100644 index 0000000..92c543a --- /dev/null +++ b/packages/react/src/breadcrumb/index.ts @@ -0,0 +1,2 @@ +export { Breadcrumb } from "./Breadcrumb"; +export type { BreadcrumbItemProps, BreadcrumbProps } from "./Breadcrumb.types"; diff --git a/packages/react/src/button/Button.classes.ts b/packages/react/src/button/Button.classes.ts new file mode 100644 index 0000000..82a254d --- /dev/null +++ b/packages/react/src/button/Button.classes.ts @@ -0,0 +1,115 @@ +import { clsx } from "clsx"; +import type { ButtonSize, ButtonTone, ButtonVariant } from "./Button.types"; + +const sizeClasses: Record = { + sm: "min-h-[max(var(--button-height-sm),var(--control-hit-target))] px-gs-button-padding-x-sm py-gs-button-padding-y-sm text-gs-button-font-size-sm", + md: "min-h-[max(var(--button-height-md),var(--control-hit-target))] px-gs-button-padding-x py-gs-button-padding-y text-gs-button-font-size", + lg: "min-h-[max(var(--button-height-lg),var(--control-hit-target))] px-gs-button-padding-x-lg py-gs-button-padding-y-lg text-gs-button-font-size-lg", +}; + +const iconOnlyClasses: Record = { + sm: "w-[max(var(--button-height-sm),var(--control-hit-target))] min-w-[max(var(--button-height-sm),var(--control-hit-target))] p-gs-button-padding-y-sm", + md: "w-[max(var(--button-height-md),var(--control-hit-target))] min-w-[max(var(--button-height-md),var(--control-hit-target))] p-gs-button-padding-y", + lg: "w-[max(var(--button-height-lg),var(--control-hit-target))] min-w-[max(var(--button-height-lg),var(--control-hit-target))] p-gs-button-padding-y-lg", +}; + +type ResolvedVariant = "primary" | "secondary" | "ghost" | "text"; + +function variantToneClasses( + variant: ResolvedVariant, + tone: ButtonTone, + interactive: boolean, +): string { + if (tone === "danger") { + return clsx( + "focus-visible:shadow-gs-danger-focus", + variant === "primary" && + "border-gs-button-color-danger bg-gs-button-bg-danger text-gs-button-color-danger [[data-high-contrast=true]_&]:border-gs-button-border", + variant === "primary" && + interactive && + "hover:border-gs-button-bg-danger-hover hover:bg-gs-button-bg-danger-hover hover:text-gs-button-color-on-fill focus-visible:border-gs-button-bg-danger-hover focus-visible:bg-gs-button-bg-danger-hover focus-visible:text-gs-button-color-on-fill active:border-gs-button-bg-danger-active active:bg-gs-button-bg-danger-active active:text-gs-button-color-on-fill", + variant === "secondary" && + "border-gs-error bg-gs-button-bg-secondary text-gs-error", + variant === "secondary" && + interactive && + "hover:bg-gs-error-subtle focus-visible:bg-gs-error-subtle active:bg-gs-error-tint", + variant === "ghost" && + "border-transparent bg-gs-button-bg-ghost text-gs-error", + variant === "ghost" && + interactive && + "hover:bg-gs-error-subtle focus-visible:bg-gs-error-subtle active:bg-gs-error-tint", + variant === "text" && + "border-transparent bg-transparent px-2 text-gs-error", + ); + } + + return clsx( + variant === "primary" && + "border-gs-button-bg-primary bg-gs-button-bg-primary text-gs-button-color-on-primary shadow-gs-surface-sheen focus-visible:shadow-gs-surface-button-focus [[data-high-contrast=true]_&]:border-gs-button-border", + variant === "primary" && + interactive && + "hover:border-gs-button-bg-primary-hover hover:bg-gs-button-bg-primary-hover focus-visible:border-gs-button-bg-primary-hover focus-visible:bg-gs-button-bg-primary-hover active:border-gs-button-bg-primary-active active:bg-gs-button-bg-primary-active", + variant === "secondary" && + "border-gs-button-border-secondary bg-gs-button-bg-secondary text-gs-button-color", + variant === "secondary" && + interactive && + "hover:border-gs-button-border-secondary-hover hover:bg-gs-button-bg-secondary-hover focus-visible:border-gs-button-border-secondary-hover focus-visible:bg-gs-button-bg-secondary-hover active:border-gs-button-border-secondary-active active:bg-gs-button-bg-secondary-active", + variant === "ghost" && + "border-transparent bg-gs-button-bg-ghost text-gs-button-color", + variant === "ghost" && + interactive && + "hover:bg-gs-button-bg-ghost-hover focus-visible:bg-gs-button-bg-ghost-hover active:bg-gs-button-bg-ghost-active", + variant === "text" && + "border-transparent bg-transparent px-2 text-gs-button-color-text", + variant === "text" && + interactive && + "hover:text-gs-button-color-text-hover focus-visible:text-gs-button-color-text-hover active:text-gs-button-color-text-hover", + ); +} + +export type ButtonClassesOptions = { + /** Default: `"primary"`. The deprecated `"danger"` maps to `primary` + `tone: "danger"`. */ + variant?: ButtonVariant; + /** Default: `"default"`. */ + tone?: ButtonTone; + /** Default: `"md"`. */ + size?: ButtonSize; + fullWidth?: boolean; + /** Square hit target for icon-only content. */ + iconOnly?: boolean; + disabled?: boolean; + loading?: boolean; +}; + +/** + * Style recipe for the Button component. Use it to give any element the + * Button appearance without rendering a ` + ))} +
    + ), +}; + export const WithIcons = { render: () => (
    @@ -179,7 +192,7 @@ export const States = { Syncing - - - -
    @@ -269,7 +282,7 @@ export const Composition = {
    - ); + const button = screen.getByRole("button", { name: "Save" }); + expect(button.classList.contains("w-full")).toBe(true); + expect(button.getAttribute("data-full-width")).toBe("true"); + + rerender(); + expect(button.classList.contains("w-full")).toBe(false); + }); + + it("treats the deprecated danger variant as primary + danger tone", () => { + render( + , + ); + const button = screen.getByRole("button", { name: "Delete legacy" }); + + expect(button.getAttribute("data-variant")).toBe("primary"); + expect(button.getAttribute("data-tone")).toBe("danger"); + }); + + it("applies the danger tone to non-primary variants", () => { + render( + , + ); + const button = screen.getByRole("button", { name: "Remove" }); + + expect(button.getAttribute("data-variant")).toBe("ghost"); + expect(button.getAttribute("data-tone")).toBe("danger"); + expect(button.classList.contains("text-gs-error")).toBe(true); + }); + + it("renders onto the child element with asChild", () => { + const onClick = vi.fn(); + render( + , + ); + + const link = screen.getByTestId("link"); + expect(link.tagName).toBe("A"); + expect(link.getAttribute("data-variant")).toBe("secondary"); + expect(link.classList.contains("gs-button")).toBe(true); + fireEvent.click(link); + expect(onClick).toHaveBeenCalledOnce(); + }); + + it("blocks interaction on disabled asChild buttons", () => { + const onClick = vi.fn(); + render( + , + ); + + const link = screen.getByRole("link", { name: "Docs" }); + expect(link.getAttribute("aria-disabled")).toBe("true"); + fireEvent.click(link); + expect(onClick).not.toHaveBeenCalled(); + }); }); diff --git a/packages/react/src/button/Button.tsx b/packages/react/src/button/Button.tsx index 92e86a1..846dfcf 100644 --- a/packages/react/src/button/Button.tsx +++ b/packages/react/src/button/Button.tsx @@ -4,6 +4,7 @@ import type { ForwardedRef, KeyboardEvent, MouseEvent, + ReactElement, ReactNode, } from "react"; import { forwardRef, isValidElement } from "react"; @@ -12,9 +13,12 @@ import { createCompoundSlot, dispatchCompoundSlots, } from "../shared/compound-slot"; +import { Slot } from "../shared/slot"; +import { buttonClasses } from "./Button.classes"; import type { ButtonLeadingProps, ButtonProps, + ButtonTone, ButtonTrailingProps, } from "./Button.types"; @@ -43,24 +47,12 @@ function collectButtonContent(children: ReactNode): { return { content, leading, trailing }; } -const sizeClasses = { - sm: "min-h-[max(var(--button-height-sm),var(--control-hit-target))] px-gs-button-padding-x-sm py-gs-button-padding-y-sm text-gs-button-font-size-sm", - md: "min-h-[max(var(--button-height-md),var(--control-hit-target))] px-gs-button-padding-x py-gs-button-padding-y text-gs-button-font-size", - lg: "min-h-[max(var(--button-height-lg),var(--control-hit-target))] px-gs-button-padding-x-lg py-gs-button-padding-y-lg text-gs-button-font-size-lg", -} as const; - const iconSizeClasses = { sm: "size-gs-button-icon-size-sm", md: "size-gs-button-icon-size", lg: "size-gs-button-icon-size-lg", } as const; -const iconOnlyClasses = { - sm: "w-[max(var(--button-height-sm),var(--control-hit-target))] min-w-[max(var(--button-height-sm),var(--control-hit-target))] p-gs-button-padding-y-sm", - md: "w-[max(var(--button-height-md),var(--control-hit-target))] min-w-[max(var(--button-height-md),var(--control-hit-target))] p-gs-button-padding-y", - lg: "w-[max(var(--button-height-lg),var(--control-hit-target))] min-w-[max(var(--button-height-lg),var(--control-hit-target))] p-gs-button-padding-y-lg", -} as const; - function hasRenderableLabel(children: ReactNode): boolean { if (children == null || children === false || children === true) { return false; @@ -171,9 +163,11 @@ function ButtonContent({ function ButtonImpl(props: ButtonProps, ref: ForwardedRef) { const { as, + asChild, variant = "primary", + tone, size = "md", - block, + fullWidth = false, loading = false, className, children, @@ -182,42 +176,19 @@ function ButtonImpl(props: ButtonProps, ref: ForwardedRef) { onClick: providedOnClick, onKeyDown: providedOnKeyDown, ...domProps - } = props; + } = props as ButtonProps & { disabled?: boolean }; + + const resolvedTone: ButtonTone = + tone ?? (variant === "danger" ? "danger" : "default"); + const resolvedVariant = variant === "danger" ? "primary" : variant; const { content, leading, trailing } = collectButtonContent(children); const hasLabel = hasRenderableLabel(content); - const iconOnly = !hasLabel && Boolean(leading || trailing || loading); + const iconOnly = + !asChild && !hasLabel && Boolean(leading || trailing || loading); const isDisabled = Boolean(disabled || loading); - const variantClassName = clsx( - variant === "primary" && - "border-gs-button-bg-primary bg-gs-button-bg-primary text-gs-button-color-on-primary shadow-gs-surface-sheen focus-visible:shadow-gs-surface-button-focus [[data-high-contrast=true]_&]:border-gs-button-border", - variant === "primary" && - !isDisabled && - "hover:border-gs-button-bg-primary-hover hover:bg-gs-button-bg-primary-hover focus-visible:border-gs-button-bg-primary-hover focus-visible:bg-gs-button-bg-primary-hover active:border-gs-button-bg-primary-active active:bg-gs-button-bg-primary-active", - variant === "secondary" && - "border-gs-button-border-secondary bg-gs-button-bg-secondary text-gs-button-color", - variant === "secondary" && - !isDisabled && - "hover:border-gs-button-border-secondary-hover hover:bg-gs-button-bg-secondary-hover focus-visible:border-gs-button-border-secondary-hover focus-visible:bg-gs-button-bg-secondary-hover active:border-gs-button-border-secondary-active active:bg-gs-button-bg-secondary-active", - variant === "ghost" && - "border-transparent bg-gs-button-bg-ghost text-gs-button-color", - variant === "ghost" && - !isDisabled && - "hover:bg-gs-button-bg-ghost-hover focus-visible:bg-gs-button-bg-ghost-hover active:bg-gs-button-bg-ghost-active", - variant === "text" && - "border-transparent bg-transparent px-2 text-gs-button-color-text", - variant === "text" && - !isDisabled && - "hover:text-gs-button-color-text-hover focus-visible:text-gs-button-color-text-hover active:text-gs-button-color-text-hover", - variant === "danger" && - "border-gs-button-color-danger bg-gs-button-bg-danger text-gs-button-color-danger focus-visible:shadow-gs-danger-focus [[data-high-contrast=true]_&]:border-gs-button-border", - variant === "danger" && - !isDisabled && - "hover:border-gs-button-bg-danger-hover hover:bg-gs-button-bg-danger-hover hover:text-gs-button-color-on-fill focus-visible:border-gs-button-bg-danger-hover focus-visible:bg-gs-button-bg-danger-hover focus-visible:text-gs-button-color-on-fill active:border-gs-button-bg-danger-active active:bg-gs-button-bg-danger-active active:text-gs-button-color-on-fill", - ); - const preventDisabledInteraction = ( event: MouseEvent | KeyboardEvent, ) => { @@ -228,23 +199,47 @@ function ButtonImpl(props: ButtonProps, ref: ForwardedRef) { }; const sharedProps = { - "data-variant": variant, + "data-variant": resolvedVariant, + "data-tone": resolvedTone === "danger" ? "danger" : undefined, "data-size": size, - "data-block": block ? "true" : undefined, + "data-full-width": fullWidth ? "true" : undefined, "data-loading": loading ? "true" : undefined, "data-icon-only": iconOnly ? "true" : undefined, "aria-busy": loading || props["aria-busy"], className: clsx( - "gs-button relative inline-flex min-w-gs-control-hit-target touch-manipulation select-none appearance-none items-center justify-center overflow-hidden whitespace-nowrap rounded-gs-button-radius border text-center font-inherit font-gs-button-font-weight leading-none tracking-normal no-underline align-middle transition-[background-color,border-color,color,box-shadow,opacity] duration-200 ease-gs-standard [-webkit-tap-highlight-color:transparent] focus-visible:outline-none focus-visible:shadow-gs-button-focus motion-reduce:transition-none [[data-reduced-motion=true]_&]:transition-none", - sizeClasses[size], - variantClassName, - iconOnly ? iconOnlyClasses[size] : block && "flex w-full", - isDisabled ? "cursor-not-allowed opacity-gs-disabled" : "cursor-pointer", - loading && "cursor-progress opacity-100", + buttonClasses({ + variant: resolvedVariant, + tone: resolvedTone, + size, + fullWidth, + iconOnly, + disabled: Boolean(disabled), + loading, + }), className, ), }; + if (asChild) { + return ( + ) => { + preventDisabledInteraction(event); + if (!isDisabled) { + (providedOnClick as (e: MouseEvent) => void)?.(event); + } + }} + {...(providedOnKeyDown ? { onKeyDown: providedOnKeyDown } : {})} + > + {children as ReactElement} + + ); + } + if (as === "a") { const anchorProps = domProps as AnchorHTMLAttributes; const onClick = @@ -292,7 +287,7 @@ function ButtonImpl(props: ButtonProps, ref: ForwardedRef) { loading={loading} hasLabel={hasLabel} size={size} - variant={variant} + variant={resolvedVariant} > {content} @@ -330,7 +325,7 @@ function ButtonImpl(props: ButtonProps, ref: ForwardedRef) { loading={loading} hasLabel={hasLabel} size={size} - variant={variant} + variant={resolvedVariant} > {content} diff --git a/packages/react/src/button/Button.types.ts b/packages/react/src/button/Button.types.ts index 8f65b88..34901ff 100644 --- a/packages/react/src/button/Button.types.ts +++ b/packages/react/src/button/Button.types.ts @@ -2,6 +2,7 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, + ReactElement, } from "react"; export type ButtonVariant = @@ -9,19 +10,26 @@ export type ButtonVariant = | "secondary" | "ghost" | "text" + /** @deprecated Use `variant="primary"` together with `tone="danger"`. */ | "danger"; +export type ButtonTone = "default" | "danger"; export type ButtonSize = "sm" | "md" | "lg"; export type ButtonCommonProps = { /** Visual style. Default: `primary`. */ variant?: ButtonVariant; + /** + * Semantic color intent, combinable with any `variant`. + * Default: `default`. + */ + tone?: ButtonTone; /** Control height and type scale. Default: `md`. */ size?: ButtonSize; /** Shows a spinner, sets `aria-busy`, and disables interaction. */ loading?: boolean; disabled?: boolean; /** Stretch to the full width of the parent. */ - block?: boolean; + fullWidth?: boolean; type?: "button" | "submit" | "reset"; }; @@ -32,8 +40,22 @@ export type ButtonProps = | (ButtonCommonProps & Omit, "type"> & { as?: "button"; + asChild?: never; }) | (ButtonCommonProps & AnchorHTMLAttributes & { as: "a"; + asChild?: never; + }) + | (ButtonCommonProps & + Omit, "children"> & { + /** + * Render the single element child instead of a ` +
    + + ); + + if (!hasFieldChrome) { + return ( + <> + {control} + {panel ? {panel} : null} + + ); + } + + return ( +
    + {label?.children != null && label.children !== false ? ( + + ) : null} + {control} + {panel ? {panel} : null} + {errorMessage?.children ? ( + + {errorMessage.children} + + ) : null} + {description?.children ? ( + + {description.children} + + ) : null} +
    + ); +} + +const ComboboxRoot = forwardRef(ComboboxImpl); +ComboboxRoot.displayName = "Combobox"; + +const ComboboxLabel = createCompoundSlot("Combobox.Label"); +const ComboboxDescription = createCompoundSlot( + "Combobox.Description", +); +const ComboboxErrorMessage = createCompoundSlot( + "Combobox.ErrorMessage", +); +const ComboboxItem = createCompoundSlot("Combobox.Item"); +const ComboboxEmpty = createCompoundSlot("Combobox.Empty"); +const ComboboxNoMatches = + createCompoundSlot("Combobox.NoMatches"); + +export const Combobox = Object.assign(ComboboxRoot, { + Label: ComboboxLabel, + Description: ComboboxDescription, + ErrorMessage: ComboboxErrorMessage, + Item: ComboboxItem, + Empty: ComboboxEmpty, + NoMatches: ComboboxNoMatches, +}); diff --git a/packages/react/src/combobox/Combobox.types.ts b/packages/react/src/combobox/Combobox.types.ts new file mode 100644 index 0000000..fa2af21 --- /dev/null +++ b/packages/react/src/combobox/Combobox.types.ts @@ -0,0 +1,56 @@ +import type { HTMLAttributes, LabelHTMLAttributes, ReactNode } from "react"; +import type { InputSize } from "../input"; + +export type ComboboxValue = string; + +export interface ComboboxProps extends Omit< + HTMLAttributes, + "defaultValue" | "onChange" +> { + /** `Combobox.Label`, `Combobox.Item`, and `Combobox.Empty` slots. */ + children: ReactNode; + /** Controlled selected value. */ + value?: ComboboxValue; + defaultValue?: ComboboxValue; + onValueChange?: (value: ComboboxValue) => void; + /** Called when the typed filter query changes. */ + onInputChange?: (query: string) => void; + /** Placeholder shown while no option is selected. */ + placeholder?: string; + size?: InputSize; + /** Render the options panel in a portal. Default: `true`. */ + portal?: boolean; + invalid?: boolean; + disabled?: boolean; + fullWidth?: boolean; + required?: boolean; + /** Native form field name, submitted through a hidden input. */ + name?: string; + /** Associates the hidden input with a form outside its DOM ancestry. */ + form?: string; + "aria-label"?: string; +} + +export interface ComboboxItemProps extends HTMLAttributes { + value: ComboboxValue; + disabled?: boolean | undefined; + /** Text used for filtering when children are not plain text. */ + textValue?: string | undefined; + children?: ReactNode; +} + +export type ComboboxLabelProps = LabelHTMLAttributes; + +export type ComboboxDescriptionProps = HTMLAttributes; + +export type ComboboxErrorMessageProps = HTMLAttributes; + +/** Shown when the typed query matches no options. */ +/** Shown when the combobox has no options at all. */ +export type ComboboxEmptyProps = HTMLAttributes; + +/** + * Shown when a filter query matches no options. Falls back to + * `Combobox.Empty` content when not provided. + */ +export type ComboboxNoMatchesProps = HTMLAttributes; diff --git a/packages/react/src/combobox/index.ts b/packages/react/src/combobox/index.ts new file mode 100644 index 0000000..32cb38e --- /dev/null +++ b/packages/react/src/combobox/index.ts @@ -0,0 +1,11 @@ +export { Combobox } from "./Combobox"; +export type { + ComboboxDescriptionProps, + ComboboxEmptyProps, + ComboboxErrorMessageProps, + ComboboxItemProps, + ComboboxLabelProps, + ComboboxNoMatchesProps, + ComboboxProps, + ComboboxValue, +} from "./Combobox.types"; diff --git a/packages/react/src/container/Container.stories.tsx b/packages/react/src/container/Container.stories.tsx index 192f0f7..3f0158d 100644 --- a/packages/react/src/container/Container.stories.tsx +++ b/packages/react/src/container/Container.stories.tsx @@ -10,3 +10,9 @@ export default meta; export const Default = { render: () => Container, }; + +export const Responsive = { + render: () => ( + Container + ), +}; diff --git a/packages/react/src/container/Container.test.tsx b/packages/react/src/container/Container.test.tsx index 3bb5e5b..ae5ed98 100644 --- a/packages/react/src/container/Container.test.tsx +++ b/packages/react/src/container/Container.test.tsx @@ -24,4 +24,27 @@ describe("Container", () => { ]), ); }); + + it("supports responsive max-widths", () => { + render( + , + ); + expect(Array.from(screen.getByTestId("container").classList)).toEqual( + expect.arrayContaining([ + "max-w-gs-breakpoint-md", + "lg:max-w-gs-breakpoint-2xl", + ]), + ); + }); + + it("renders a custom element through the as prop", () => { + render( + + Page content + , + ); + + expect(screen.getByTestId("container").tagName).toBe("MAIN"); + expect(screen.getByRole("main")).toBeTruthy(); + }); }); diff --git a/packages/react/src/container/Container.tsx b/packages/react/src/container/Container.tsx index d920e2d..30f5099 100644 --- a/packages/react/src/container/Container.tsx +++ b/packages/react/src/container/Container.tsx @@ -1,7 +1,9 @@ -import type { ForwardedRef } from "react"; -import { forwardRef } from "react"; +import type { ElementType, ForwardedRef } from "react"; +import { createElement, forwardRef } from "react"; import { clsx } from "clsx"; +import type { PolymorphicComponent } from "../shared/polymorphic"; import type { ContainerProps } from "./Container.types"; +import { responsiveClasses } from "../shared/responsive"; const sizeClasses = { xs: "max-w-gs-breakpoint-sm", @@ -12,24 +14,35 @@ const sizeClasses = { } as const; function ContainerImpl( - { size = "lg", className, children, ...props }: ContainerProps, - ref: ForwardedRef, + { + as = "div", + size = "lg", + className, + children, + ...props + }: ContainerProps, + ref: ForwardedRef, ) { - return ( -
    - {children} -
    + ), + "data-size": typeof size === "string" ? size : undefined, + ...props, + }, + children, ); } -export const Container = forwardRef(ContainerImpl); +export const Container = forwardRef( + ContainerImpl, +) as unknown as PolymorphicComponent< + "div", + import("./Container.types").ContainerOwnProps +>; Container.displayName = "Container"; diff --git a/packages/react/src/container/Container.types.ts b/packages/react/src/container/Container.types.ts index 2baa1f1..d8c2ba6 100644 --- a/packages/react/src/container/Container.types.ts +++ b/packages/react/src/container/Container.types.ts @@ -1,8 +1,13 @@ -import type { HTMLAttributes, ReactNode } from "react"; +import type { ElementType, ReactNode } from "react"; +import type { PolymorphicProps } from "../shared/polymorphic"; +import type { Responsive } from "../shared/responsive"; export type ContainerSize = "xs" | "sm" | "md" | "lg" | "xl"; -export interface ContainerProps extends HTMLAttributes { - size?: ContainerSize; +export interface ContainerOwnProps { + size?: Responsive; children?: ReactNode; } + +export type ContainerProps = + PolymorphicProps; diff --git a/packages/react/src/date-picker/DatePicker.test.tsx b/packages/react/src/date-picker/DatePicker.test.tsx index f064f56..4020180 100644 --- a/packages/react/src/date-picker/DatePicker.test.tsx +++ b/packages/react/src/date-picker/DatePicker.test.tsx @@ -210,6 +210,15 @@ describe("DatePicker", () => { day.classList.contains("data-[selected=true]:font-medium"), ), ).toBe(false); + expect(days[0]?.classList.contains("active:scale-95")).toBe(true); + expect(days[0]?.classList.contains("motion-reduce:transition-none")).toBe( + true, + ); + expect( + document + .querySelector(".gs-datepicker-today") + ?.classList.contains("active:scale-95"), + ).toBe(true); }); it("stays closed after becoming read-only and editable again", () => { diff --git a/packages/react/src/date-picker/DatePicker.tsx b/packages/react/src/date-picker/DatePicker.tsx index 6822d2a..7613957 100644 --- a/packages/react/src/date-picker/DatePicker.tsx +++ b/packages/react/src/date-picker/DatePicker.tsx @@ -175,7 +175,7 @@ const DatePickerDay = memo(function DatePickerDay({ ref={metadata.setRef} type="button" role="gridcell" - className="gs-datepicker-day m-0 inline-flex w-full min-h-[max(var(--datepicker-cell-size),var(--control-hit-target))] cursor-pointer appearance-none items-center justify-center box-border rounded-gs-datepicker-cell-radius border-0 bg-transparent p-0 font-inherit text-gs-datepicker-cell-font-size font-normal text-gs-text hover:not-disabled:not-data-[selected=true]:bg-gs-datepicker-day-bg-hover data-[outside=true]:text-gs-datepicker-day-color-outside data-[today=true]:not-data-[selected=true]:bg-gs-datepicker-day-bg-today data-[selected=true]:bg-gs-datepicker-day-bg-selected data-[selected=true]:text-gs-datepicker-day-color-selected focus-visible:outline-none focus-visible:shadow-gs-input-focus data-[focused=true]:focus:outline-none data-[focused=true]:focus:shadow-gs-input-focus disabled:cursor-not-allowed disabled:text-gs-text-disabled" + className="gs-datepicker-day m-0 inline-flex w-full min-h-[max(var(--datepicker-cell-size),var(--control-hit-target))] cursor-pointer appearance-none items-center justify-center box-border rounded-gs-datepicker-cell-radius border-0 bg-transparent p-0 font-inherit text-gs-datepicker-cell-font-size font-normal text-gs-text transition-[background-color,color,box-shadow,transform] duration-150 ease-gs-standard active:scale-95 hover:not-disabled:not-data-[selected=true]:bg-gs-datepicker-day-bg-hover data-[outside=true]:text-gs-datepicker-day-color-outside data-[today=true]:not-data-[selected=true]:bg-gs-datepicker-day-bg-today data-[selected=true]:bg-gs-datepicker-day-bg-selected data-[selected=true]:text-gs-datepicker-day-color-selected focus-visible:outline-none focus-visible:shadow-gs-input-focus data-[focused=true]:focus:outline-none data-[focused=true]:focus:shadow-gs-input-focus disabled:cursor-not-allowed disabled:text-gs-text-disabled motion-reduce:transition-none motion-reduce:active:scale-100 [[data-reduced-motion=true]_&]:transition-none [[data-reduced-motion=true]_&]:active:scale-100" tabIndex={tabStop ? 0 : -1} disabled={metadata.disabled} aria-label={metadata.label} @@ -327,7 +327,7 @@ const DatePickerCalendarFooter = memo(function DatePickerCalendarFooter({
    +
    + {monthLabel} +
    + +
    + + - - - {(day) => ( - - {day} - - )} - - - {(date) => ( - - clsx( - "relative m-0 inline-flex h-gs-control-hit-target w-full cursor-pointer items-center justify-center border-0 p-0 text-gs-datepicker-cell-font-size font-normal outline-none transition-[color,background-color,box-shadow] duration-200 ease-gs-standard motion-reduce:transition-none", - !isSelected && - "bg-transparent hover:bg-gs-datepicker-day-bg-hover", - isSelected && - !isSelectionStart && - !isSelectionEnd && - "bg-gs-datepicker-day-bg-today text-gs-text", - (isSelectionStart || isSelectionEnd) && - "rounded-gs-datepicker-cell-radius bg-gs-datepicker-day-bg-selected text-gs-datepicker-day-color-selected", - isToday && !isSelected && "bg-gs-datepicker-day-bg-today", - isToday && - isSelected && - "after:absolute after:bottom-1 after:size-1 after:rounded-full after:bg-current", - isOutsideMonth && "text-gs-datepicker-day-color-outside", - isDisabled && "cursor-not-allowed text-gs-text-disabled", - isFocusVisible && "relative z-10 shadow-gs-input-focus", - ) - } - /> - )} - - - - - +
    setHoverDate(null)} + > + {Array.from({ length: 6 }, (_, week) => ( +
    + {days.slice(week * 7, week * 7 + 7).map((day) => { + const key = serializeDate(day); + const disabled = isDateDisabled(day, min, max); + const isStart = + rangeStart != null && isSameDay(day, rangeStart); + const isEnd = rangeEnd != null && isSameDay(day, rangeEnd); + const inRange = + rangeStart != null && + rangeEnd != null && + isBetween(day, rangeStart, rangeEnd); + const today = isSameDay(day, new Date()); + return ( + + ); + })} +
    + ))} +
    + + ); } diff --git a/packages/react/src/date-range-picker/DateRangeField.tsx b/packages/react/src/date-range-picker/DateRangeField.tsx new file mode 100644 index 0000000..ab07e91 --- /dev/null +++ b/packages/react/src/date-range-picker/DateRangeField.tsx @@ -0,0 +1,235 @@ +import type { KeyboardEvent } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { clsx } from "clsx"; +import { serializeDate } from "../date-picker/date-picker-utils"; + +type SegmentType = "year" | "month" | "day"; + +type DraftValue = Record; + +type SegmentPart = + | { kind: "segment"; type: SegmentType } + | { kind: "literal"; text: string }; + +const SEGMENT_MAX: Record = { + year: 9999, + month: 12, + day: 31, +}; + +const SEGMENT_MIN: Record = { + year: 1, + month: 1, + day: 1, +}; + +const SEGMENT_PLACEHOLDER: Record = { + year: "yyyy", + month: "mm", + day: "dd", +}; + +const SEGMENT_NAME: Record = { + year: "year", + month: "month", + day: "day", +}; + +function getSegmentParts(locale: string): SegmentPart[] { + const formatter = new Intl.DateTimeFormat(locale, { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const parts: SegmentPart[] = []; + for (const part of formatter.formatToParts(new Date(2026, 11, 31))) { + if (part.type === "year" || part.type === "month" || part.type === "day") { + parts.push({ kind: "segment", type: part.type }); + } else if (part.type === "literal") { + parts.push({ kind: "literal", text: part.value }); + } + } + return parts; +} + +function draftFromDate(date: Date | null): DraftValue { + return date + ? { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + } + : { year: null, month: null, day: null }; +} + +function draftToDate(draft: DraftValue): Date | null { + if (draft.year == null || draft.month == null || draft.day == null) { + return null; + } + const daysInMonth = new Date(draft.year, draft.month, 0).getDate(); + return new Date( + draft.year, + draft.month - 1, + Math.min(draft.day, daysInMonth), + ); +} + +export type DateRangeFieldProps = { + value: Date | null; + onCommit: (date: Date | null) => void; + /** Accessible name prefix, e.g. `Start date`. */ + fieldLabel: string; + locale: string; + disabled: boolean; + readOnly: boolean; +}; + +/** + * A segmented date field: one spinbutton per year/month/day segment ordered + * by the locale's date pattern. Commits a full Date once every segment has a + * value, and commits null when a previously complete value is cleared. + */ +export function DateRangeField({ + value, + onCommit, + fieldLabel, + locale, + disabled, + readOnly, +}: DateRangeFieldProps) { + const parts = useMemo(() => getSegmentParts(locale), [locale]); + const [draft, setDraft] = useState(() => draftFromDate(value)); + const rootRef = useRef(null); + const valueKey = serializeDate(value); + const lastCommittedRef = useRef(valueKey); + + useEffect(() => { + if (lastCommittedRef.current !== valueKey) { + lastCommittedRef.current = valueKey; + setDraft(draftFromDate(value)); + } + }, [value, valueKey]); + + const applyDraft = (next: DraftValue) => { + setDraft(next); + const nextDate = draftToDate(next); + const nextKey = serializeDate(nextDate); + if (nextKey === lastCommittedRef.current) return; + // Only commit complete dates, or a clear of a previously complete value. + if (nextDate !== null || lastCommittedRef.current !== "") { + lastCommittedRef.current = nextKey; + onCommit(nextDate); + } + }; + + const moveFocus = (from: HTMLElement, direction: 1 | -1) => { + const segments = Array.from( + rootRef.current?.querySelectorAll('[role="spinbutton"]') ?? + [], + ); + const index = segments.indexOf(from); + segments[index + direction]?.focus(); + }; + + const handleKeyDown = ( + type: SegmentType, + event: KeyboardEvent, + ) => { + if (disabled) return; + const { key } = event; + if (key === "ArrowLeft" || key === "ArrowRight") { + event.preventDefault(); + moveFocus(event.currentTarget, key === "ArrowRight" ? 1 : -1); + return; + } + if (readOnly) return; + const current = draft[type]; + const min = SEGMENT_MIN[type]; + const max = SEGMENT_MAX[type]; + + if (key === "ArrowUp" || key === "ArrowDown") { + event.preventDefault(); + const offset = key === "ArrowUp" ? 1 : -1; + const base = + current ?? + (type === "year" + ? new Date().getFullYear() + : key === "ArrowUp" + ? min - 1 + : max + 1); + let next = base + offset; + if (type !== "year") { + if (next > max) next = min; + if (next < min) next = max; + } else { + next = Math.min(max, Math.max(min, next)); + } + applyDraft({ ...draft, [type]: next }); + return; + } + if (key === "Backspace" || key === "Delete") { + event.preventDefault(); + const next = current == null ? null : Math.floor(current / 10) || null; + applyDraft({ ...draft, [type]: next }); + return; + } + if (/^\d$/.test(key)) { + event.preventDefault(); + const digit = Number(key); + let next = (current ?? 0) * 10 + digit; + if (next > max) next = digit; + applyDraft({ ...draft, [type]: next === 0 ? null : next }); + if (next > 0 && next * 10 > max) { + moveFocus(event.currentTarget, 1); + } + } + }; + + return ( + + {parts.map((part, index) => { + if (part.kind === "literal") { + return ( + + ); + } + const segmentValue = draft[part.type]; + const text = + segmentValue == null + ? SEGMENT_PLACEHOLDER[part.type] + : String(segmentValue).padStart(part.type === "year" ? 4 : 2, "0"); + return ( + handleKeyDown(part.type, event)} + > + {text} + + ); + })} + + ); +} diff --git a/packages/react/src/date-range-picker/DateRangePicker.test.tsx b/packages/react/src/date-range-picker/DateRangePicker.test.tsx index d7130a2..5ada8a7 100644 --- a/packages/react/src/date-range-picker/DateRangePicker.test.tsx +++ b/packages/react/src/date-range-picker/DateRangePicker.test.tsx @@ -11,7 +11,26 @@ import { import { afterEach, describe, expect, it, vi } from "vitest"; import { DateRangePicker } from "./DateRangePicker"; -afterEach(cleanup); +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +// jsdom reports zero-sized rects, which keeps the floating calendar in its +// hidden pre-positioning state; give elements a measurable size. +function mockElementGeometry() { + vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({ + x: 0, + y: 0, + width: 288, + height: 44, + top: 0, + right: 288, + bottom: 44, + left: 0, + toJSON: () => ({}), + }); +} describe("DateRangePicker", () => { it("sets a readable displayName", () => { @@ -40,10 +59,10 @@ describe("DateRangePicker", () => { expect(screen.getByRole("group", { name: "Travel dates" })).toBeTruthy(); expect( - screen.getAllByRole("spinbutton", { name: /Start Date/ }), + screen.getAllByRole("spinbutton", { name: /Start date/ }), ).toHaveLength(3); expect( - screen.getAllByRole("spinbutton", { name: /End Date/ }), + screen.getAllByRole("spinbutton", { name: /End date/ }), ).toHaveLength(3); expect(screen.getByRole("button", { name: /Open calendar/ })).toBeTruthy(); }); @@ -94,21 +113,31 @@ describe("DateRangePicker", () => { }); it("selects a range from the shared calendar", async () => { + mockElementGeometry(); const onValueChange = vi.fn(); render(); - fireEvent.click( - await screen.findByRole("button", { name: /July 20, 2026/ }), + // The calendar panel is lazy-loaded; allow for slow full-suite runs. + const firstDay = await screen.findByRole( + "gridcell", + { name: /July 20, 2026/ }, + { timeout: 8000 }, ); - fireEvent.click(screen.getByRole("button", { name: /July 23, 2026/ })); + expect(firstDay.classList.contains("active:scale-95")).toBe(true); + expect(firstDay.classList.contains("motion-reduce:transition-none")).toBe( + true, + ); + fireEvent.click(firstDay); + fireEvent.click(screen.getByRole("gridcell", { name: /July 23, 2026/ })); expect(onValueChange).toHaveBeenLastCalledWith({ start: new Date(2026, 6, 20), end: new Date(2026, 6, 23), }); - }); + }, 15000); it("dispatches bubbling native change events for user selections", async () => { + mockElementGeometry(); const { container } = render(
    @@ -119,9 +148,13 @@ describe("DateRangePicker", () => { form.addEventListener("change", onChange); fireEvent.click( - await screen.findByRole("button", { name: /July 20, 2026/ }), + await screen.findByRole( + "gridcell", + { name: /July 20, 2026/ }, + { timeout: 8000 }, + ), ); - fireEvent.click(screen.getByRole("button", { name: /July 23, 2026/ })); + fireEvent.click(screen.getByRole("gridcell", { name: /July 23, 2026/ })); await waitFor(() => expect(onChange).toHaveBeenCalled()); const changedNames = onChange.mock.calls.map( @@ -129,7 +162,7 @@ describe("DateRangePicker", () => { ); expect(changedNames).toContain("from"); expect(changedNames).toContain("to"); - }); + }, 15000); it("does not dispatch native change events for value prop updates", () => { const first = { diff --git a/packages/react/src/date-range-picker/DateRangePicker.tsx b/packages/react/src/date-range-picker/DateRangePicker.tsx index cc548a5..df92c9d 100644 --- a/packages/react/src/date-range-picker/DateRangePicker.tsx +++ b/packages/react/src/date-range-picker/DateRangePicker.tsx @@ -1,5 +1,3 @@ -import type { CalendarDate } from "@internationalized/date"; -import { CalendarDate as InternationalizedCalendarDate } from "@internationalized/date"; import type { ForwardedRef, ReactNode } from "react"; import { forwardRef, @@ -12,15 +10,6 @@ import { useRef, } from "react"; import { useControllableState } from "@velune/hooks"; -import { - Button as AriaButton, - DateInput, - DateRangePicker as AriaDateRangePicker, - DateSegment, - Group, - I18nProvider, - Label, -} from "react-aria-components"; import { clsx } from "clsx"; import { serializeDate } from "../date-picker/date-picker-utils"; import { toDateValue } from "../date-picker/date-utils"; @@ -37,6 +26,12 @@ import { inputLabelSizeClasses, inputRequiredClasses, } from "../shared/input-tailwind-classes"; +import { + isTopEscapeLayer, + popEscapeLayer, + pushEscapeLayer, +} from "../shared/overlay-stack"; +import { DateRangeField } from "./DateRangeField"; import type { DateRangeInput, DateRangePickerDescriptionProps, @@ -107,21 +102,6 @@ function rangeKey(value?: DateRangeInput): string { return `${serializeDate(toDateValue(value?.start))}/${serializeDate(toDateValue(value?.end))}`; } -function toCalendarDate(value: Date | string | null | undefined) { - const date = toDateValue(value); - return date - ? new InternationalizedCalendarDate( - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - ) - : null; -} - -function fromCalendarDate(value: CalendarDate): Date { - return new Date(value.year, value.month - 1, value.day); -} - type DateRangePickerComposition = { label?: DateRangePickerLabelProps; description?: DateRangePickerDescriptionProps; @@ -180,11 +160,16 @@ function DateRangePickerImpl( const { label, description, errorMessage } = collectComposition(children); const generatedId = useId(); const fieldId = id ?? `${generatedId}-date-range`; + const labelId = `${fieldId}-label`; const descriptionId = `${fieldId}-description`; const errorId = `${fieldId}-error`; + const panelId = `${fieldId}-panel`; + const resolvedLocale = + locale ?? (typeof navigator !== "undefined" ? navigator.language : "en"); const initialValueRef = useRef(parseRange(defaultValue)); const rootRef = useRef(null); const groupRef = useRef(null); + const calendarButtonRef = useRef(null); const startNativeInputRef = useRef(null); const endNativeInputRef = useRef(null); const composedRef = useComposedRefs(ref, rootRef); @@ -211,13 +196,8 @@ function DateRangePickerImpl( selected.start.getTime() > selected.end.getTime(), ); const isInvalid = invalid || hasOrderError || Boolean(errorMessage?.children); - const ariaValue = useMemo(() => { - const start = toCalendarDate(selected.start); - const end = toCalendarDate(selected.end); - return start && end && start.compare(end) <= 0 ? { start, end } : null; - }, [selected.end, selected.start]); - const minValue = useMemo(() => toCalendarDate(min), [min]); - const maxValue = useMemo(() => toCalendarDate(max), [max]); + const minDate = useMemo(() => toDateValue(min), [min]); + const maxDate = useMemo(() => toDateValue(max), [max]); const commit = useCallback( (next: DateRangeValue) => { @@ -257,7 +237,7 @@ function DateRangePickerImpl( } }, [disabled, readOnly, setStateOpen, stateOpen]); - const handleOpenChange = useCallback( + const setOpenState = useCallback( (next: boolean) => { if ((disabled || readOnly) && next) return; setStateOpen(next); @@ -265,6 +245,44 @@ function DateRangePickerImpl( [disabled, readOnly, setStateOpen], ); + const closeAndRestoreFocus = useCallback(() => { + setOpenState(false); + requestAnimationFrame(() => calendarButtonRef.current?.focus()); + }, [setOpenState]); + + useEffect(() => { + if (!isOpen) return; + const escapeLayer = pushEscapeLayer(); + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape" && isTopEscapeLayer(escapeLayer)) { + event.preventDefault(); + event.stopPropagation(); + closeAndRestoreFocus(); + } + }; + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + popEscapeLayer(escapeLayer); + }; + }, [closeAndRestoreFocus, isOpen]); + + useEffect(() => { + if (!isOpen) return; + const onPointerDown = (event: PointerEvent) => { + const target = event.target as Node | null; + if (!target) return; + const panel = document.getElementById(panelId); + if (rootRef.current?.contains(target) || panel?.contains(target)) { + return; + } + setOpenState(false); + }; + document.addEventListener("pointerdown", onPointerDown, true); + return () => + document.removeEventListener("pointerdown", onPointerDown, true); + }, [isOpen, panelId, setOpenState]); + const focusVisibleControl = useCallback(() => { groupRef.current ?.querySelector( @@ -273,42 +291,49 @@ function DateRangePickerImpl( ?.focus(); }, []); - const dateRangePicker = ( - - commit( - next - ? { - start: fromCalendarDate(next.start), - end: fromCalendarDate(next.end), - } - : { start: null, end: null }, - ) - } - {...(minValue ? { minValue } : {})} - {...(maxValue ? { maxValue } : {})} - isDisabled={disabled} - isReadOnly={readOnly} - isRequired={required} - isInvalid={isInvalid} - shouldCloseOnSelect - isOpen={isOpen} - onOpenChange={handleOpenChange} - className="contents" + const describedBy = [ + description?.children ? descriptionId : null, + errorMessage?.children ? errorId : null, + ] + .filter(Boolean) + .join(" "); + + return ( +
    {label?.children != null ? ( - + ) : null} - - clsx( - "gs-date-range-picker-group inline-flex h-[max(var(--gs-drp-box),var(--control-hit-target))] min-h-[max(var(--gs-drp-box),var(--control-hit-target))] w-full min-w-0 max-w-full items-center gap-1 overflow-hidden rounded-gs-xs border border-gs-default bg-gs-surface bg-gs-surface-highlight text-gs-input-color shadow-gs-surface-sheen transition-[background-color,border-color,box-shadow] duration-200 ease-gs-standard hover:border-gs-strong motion-reduce:transition-none", - fieldSizeClasses[size], - fullWidth && "w-full", - isFocusWithin && - "border-gs-focus bg-gs-surface-raised shadow-gs-input-surface-focus", - isInvalid && - "border-gs-error bg-gs-error-subtle shadow-gs-input-invalid-focus", - disabled && "cursor-not-allowed opacity-gs-disabled", - ) - } - {...([ - description?.children ? descriptionId : null, - errorMessage?.children ? errorId : null, - ] - .filter(Boolean) - .join(" ") - ? { - "aria-describedby": [ - description?.children ? descriptionId : null, - errorMessage?.children ? errorId : null, - ] - .filter(Boolean) - .join(" "), - } - : {})} + role="group" + {...(label?.children != null + ? { "aria-labelledby": labelId } + : { "aria-label": `${startLabel} ${endLabel}` })} + className={clsx( + "gs-date-range-picker-group inline-flex h-[max(var(--gs-drp-box),var(--control-hit-target))] min-h-[max(var(--gs-drp-box),var(--control-hit-target))] w-full min-w-0 max-w-full items-center gap-1 overflow-hidden rounded-gs-xs border border-gs-default bg-gs-surface bg-gs-surface-highlight text-gs-input-color shadow-gs-surface-sheen transition-[background-color,border-color,box-shadow] duration-200 ease-gs-standard hover:border-gs-strong focus-within:border-gs-focus focus-within:bg-gs-surface-raised focus-within:shadow-gs-input-surface-focus motion-reduce:transition-none", + fieldSizeClasses[size], + fullWidth && "w-full", + isInvalid && + "border-gs-error bg-gs-error-subtle shadow-gs-input-invalid-focus", + disabled && "cursor-not-allowed opacity-gs-disabled", + )} + {...(describedBy ? { "aria-describedby": describedBy } : {})} {...(isInvalid ? { "aria-invalid": true } : {})} > - - {(segment) => ( - - clsx( - "rounded-gs-xs outline-none transition-[color,background-color,box-shadow] duration-200 ease-gs-standard motion-reduce:transition-none", - segment.type === "literal" ? "px-0" : "px-px", - isPlaceholder && "text-gs-input-placeholder", - isFocused && - "bg-gs-datepicker-day-bg-selected text-gs-datepicker-day-color-selected", - ) - } - /> - )} - + commit({ start: date, end: selected.end })} + fieldLabel={startLabel} + locale={resolvedLocale} + disabled={disabled} + readOnly={readOnly} + /> - - {(segment) => ( - - clsx( - "rounded-gs-xs outline-none transition-[color,background-color,box-shadow] duration-200 ease-gs-standard motion-reduce:transition-none", - segment.type === "literal" ? "px-0" : "px-px", - isPlaceholder && "text-gs-input-placeholder", - isFocused && - "bg-gs-datepicker-day-bg-selected text-gs-datepicker-day-color-selected", - ) - } - /> - )} - + commit({ start: selected.start, end: date })} + fieldLabel={endLabel} + locale={resolvedLocale} + disabled={disabled} + readOnly={readOnly} + /> - {clearable && - (selected.start || selected.end) && - !disabled && - !readOnly ? ( + {clearable && !disabled && !readOnly ? ( ) : null} - void loadDateRangeCalendar()} onPointerEnter={() => void loadDateRangeCalendar()} + onClick={() => setOpenState(!isOpen)} > - + - +
    {hasNativeControl && ( <> @@ -471,6 +467,8 @@ function DateRangePickerImpl( id={descriptionId} className={clsx( inputDescriptionClasses, + // Wrap to the field width instead of widening the w-fit root. + "w-0 min-w-full", disabled && "text-gs-text-disabled", description.className, )} @@ -482,7 +480,11 @@ function DateRangePickerImpl( @@ -151,13 +151,13 @@ export const MultipleSelection = { if (keys !== "all") setSelected(new Set(keys)); }} > - Name - Owner - Status - Last updated + Name + Owner + Status + Last updated - + {selected.size} columns visible diff --git a/packages/react/src/dropdown/Dropdown.test.tsx b/packages/react/src/dropdown/Dropdown.test.tsx index c5b44b7..11b933c 100644 --- a/packages/react/src/dropdown/Dropdown.test.tsx +++ b/packages/react/src/dropdown/Dropdown.test.tsx @@ -70,14 +70,41 @@ describe("Dropdown", () => { const trigger = screen.getByRole("button", { name: "Actions" }); fireEvent.click(trigger); + // Generous timeouts: the menu is lazy-loaded and populates its + // collection asynchronously, which is slow under full-suite load. await waitFor(() => expect(getMenu("Project actions")).toBeTruthy(), { - timeout: 3000, + timeout: 8000, + }); + await waitFor(() => expect(screen.getByText("Duplicate")).toBeTruthy(), { + timeout: 8000, }); fireEvent.click(screen.getByText("Duplicate").closest("[role=menuitem]")!); expect(onAction).toHaveBeenCalledWith("duplicate"); expect(screen.queryByRole("menu")).toBeNull(); await waitFor(() => expect(document.activeElement).toBe(trigger)); + }, 20000); + + it("accepts value as the item key alongside the deprecated id", async () => { + mockElementGeometry(); + const onAction = vi.fn(); + render( + + + + + + Rename + Delete + + , + ); + await waitFor(() => expect(getMenu("Item keys")).toBeTruthy(), { + timeout: 3000, + }); + + fireEvent.click(screen.getByText("Rename").closest("[role=menuitem]")!); + expect(onAction).toHaveBeenCalledWith("rename"); }); it("fills its parent width by default", () => { diff --git a/packages/react/src/dropdown/Dropdown.tsx b/packages/react/src/dropdown/Dropdown.tsx index 4aa41c1..3ee2a74 100644 --- a/packages/react/src/dropdown/Dropdown.tsx +++ b/packages/react/src/dropdown/Dropdown.tsx @@ -6,14 +6,7 @@ import type { PointerEvent, ReactElement, } from "react"; -import { - cloneElement, - forwardRef, - lazy, - Suspense, - useId, - useState, -} from "react"; +import { forwardRef, lazy, Suspense, useId, useState } from "react"; import { useControllableState } from "@velune/hooks"; import { clsx } from "clsx"; import { Popover } from "../popover"; @@ -21,6 +14,7 @@ import { collectCompoundSlotProps, createCompoundSlot, } from "../shared/compound-slot"; +import { Slot } from "../shared/slot"; import type { DropdownItemDescriptionProps, DropdownItemLeadingProps, @@ -149,38 +143,24 @@ function DropdownImpl( )} {...(trigger.style !== undefined ? { style: trigger.style } : {})} > - {cloneElement(triggerElement as ReactElement, { - id: triggerId, - ...(triggerSupportsDisabled ? { disabled: triggerDisabled } : {}), - tabIndex: - triggerDisabled && !triggerSupportsDisabled - ? -1 - : triggerProps.tabIndex, - "aria-haspopup": "menu", - "aria-disabled": triggerDisabled - ? true - : triggerProps["aria-disabled"], - "data-disabled": triggerDisabled ? "" : triggerProps["data-disabled"], - onFocus: (event: FocusEvent) => { - triggerProps.onFocus?.(event); - void loadDropdownMenu(); - }, - onPointerEnter: (event: PointerEvent) => { - triggerProps.onPointerEnter?.(event); - void loadDropdownMenu(); - }, - onClick: (event: MouseEvent) => { + ) => { if (triggerDisabled) { event.preventDefault(); - return; + event.stopPropagation(); } - triggerProps.onClick?.(event); - if (!event.defaultPrevented) { - void loadDropdownMenu(); - setFocusStrategy("first"); - } - }, - onKeyDown: (event: KeyboardEvent) => { + }} + onKeyDownCapture={(event: KeyboardEvent) => { if (triggerDisabled) { if ( event.key === "Enter" || @@ -190,10 +170,25 @@ function DropdownImpl( ) { event.preventDefault(); } + event.stopPropagation(); + } + }} + onFocus={() => { + void loadDropdownMenu(); + }} + onPointerEnter={() => { + void loadDropdownMenu(); + }} + onClick={(event: MouseEvent) => { + if (triggerDisabled || event.defaultPrevented) { return; } - triggerProps.onKeyDown?.(event); + void loadDropdownMenu(); + setFocusStrategy("first"); + }} + onKeyDown={(event: KeyboardEvent) => { if ( + triggerDisabled || event.defaultPrevented || (event.key !== "ArrowDown" && event.key !== "ArrowUp") ) { @@ -203,8 +198,10 @@ function DropdownImpl( void loadDropdownMenu(); setFocusStrategy(event.key === "ArrowUp" ? "last" : "first"); setOpen(true); - }, - })} + }} + > + {triggerElement as ReactElement} + ; + export type DropdownSelectionMode = "none" | "single" | "multiple"; -export type DropdownSelectedKeys = Selection; +export type DropdownSelectedKeys = DropdownSelection; export interface DropdownProps extends Omit< HTMLAttributes, @@ -36,24 +32,17 @@ export interface DropdownTriggerProps { } export interface DropdownMenuProps extends Omit< - MenuProps, - | "children" - | "className" - | "style" - | "selectionMode" - | "selectedKeys" - | "defaultSelectedKeys" - | "onSelectionChange" - | "onAction" + HTMLAttributes, + "children" | "onSelect" > { children?: ReactNode; /** Accessible menu name when the trigger does not provide one. */ "aria-label"?: string; selectionMode?: DropdownSelectionMode; - selectedKeys?: Iterable | "all"; - defaultSelectedKeys?: Iterable | "all"; - onSelectionChange?: (keys: Selection) => void; - onAction?: (key: Key) => void; + selectedKeys?: Iterable | "all"; + defaultSelectedKeys?: Iterable | "all"; + onSelectionChange?: (keys: DropdownSelection) => void; + onAction?: (key: DropdownKey) => void; /** Default: false for multiple selection, true otherwise. */ closeOnSelect?: boolean; className?: string; @@ -61,18 +50,13 @@ export interface DropdownMenuProps extends Omit< } export interface DropdownItemProps extends Omit< - MenuItemProps, - | "children" - | "id" - | "className" - | "textValue" - | "isDisabled" - | "href" - | "target" - | "onAction" - | "shouldCloseOnSelect" + HTMLAttributes, + "children" | "id" | "className" | "onSelect" > { - id: Key; + /** Selection key for the item. Matches the `value` naming used by Select. */ + value?: DropdownKey; + /** @deprecated Use `value` instead. */ + id?: DropdownKey; children?: ReactNode; textValue?: string; disabled?: boolean; @@ -85,15 +69,15 @@ export interface DropdownItemProps extends Omit< } export interface DropdownSectionProps extends Omit< - MenuSectionProps, + HTMLAttributes, "children" | "className" > { children?: ReactNode; className?: string; } -export type DropdownSectionTitleProps = HeaderProps; -export type DropdownSeparatorProps = SeparatorProps; +export type DropdownSectionTitleProps = HTMLAttributes; +export type DropdownSeparatorProps = HTMLAttributes; export type DropdownItemLeadingProps = HTMLAttributes; export type DropdownItemDescriptionProps = HTMLAttributes; export type DropdownItemTrailingProps = HTMLAttributes; diff --git a/packages/react/src/dropdown/DropdownMenu.tsx b/packages/react/src/dropdown/DropdownMenu.tsx index 7f55045..780dbca 100644 --- a/packages/react/src/dropdown/DropdownMenu.tsx +++ b/packages/react/src/dropdown/DropdownMenu.tsx @@ -1,12 +1,7 @@ -import { useEffect, useRef, type ReactNode } from "react"; +import type { KeyboardEvent, MouseEvent, ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { clsx } from "clsx"; -import { - Header, - Menu, - MenuItem, - MenuSection, - Separator, -} from "react-aria-components"; +import { useControllableState } from "@velune/hooks"; import { collectCompoundSlotProps, dispatchCompoundSlots, @@ -16,9 +11,11 @@ import type { DropdownItemLeadingProps, DropdownItemProps, DropdownItemTrailingProps, + DropdownKey, DropdownMenuProps, DropdownSectionProps, DropdownSectionTitleProps, + DropdownSelection, DropdownSeparatorProps, } from "./Dropdown.types"; @@ -56,144 +53,163 @@ function itemSlotSchemaToHandlers() { }; } -function renderItem( - props: DropdownItemProps, - closeMenu: () => void, - menuCloseOnSelect: boolean, -) { +const itemRoleForMode = { + none: "menuitem", + single: "menuitemradio", + multiple: "menuitemcheckbox", +} as const; + +type MenuBehavior = { + selectionMode: "none" | "single" | "multiple"; + isSelected: (key: DropdownKey) => boolean; + activate: (key: DropdownKey, item: DropdownItemProps) => void; +}; + +function renderItem(props: DropdownItemProps, behavior: MenuBehavior) { const { + value, id, children, - textValue, + textValue: _textValue, disabled = false, tone = "default", href, target, - onAction, - closeOnSelect = menuCloseOnSelect, + onAction: _onAction, + closeOnSelect: _closeOnSelect, className, ...itemProps } = props; + void _textValue; + void _onAction; + void _closeOnSelect; + const itemKey = value ?? id; + if (itemKey === undefined) { + throw new Error("Dropdown.Item requires a `value` prop."); + } const { leading, description, trailing } = collectCompoundSlotProps(children, itemSlotSchema); const label: ReactNode[] = []; dispatchCompoundSlots(children, itemSlotSchemaToHandlers(), (child) => label.push(child), ); + const selected = behavior.isSelected(itemKey); + const role = itemRoleForMode[behavior.selectionMode]; - return ( - { - onAction?.(); - if (closeOnSelect) closeMenu(); - }} - data-tone={tone} - className={({ isDisabled, isFocused, isSelected }) => - clsx( - "gs-dropdown-item relative m-0 grid min-h-gs-control-hit-target cursor-pointer items-center gap-x-3 rounded-gs-xs px-3 py-2 text-start text-sm leading-gs-normal outline-none transition-colors duration-150 ease-gs-standard motion-reduce:transition-none", - leading?.children != null - ? "grid-cols-[1rem_minmax(0,1fr)_auto]" - : "grid-cols-[minmax(0,1fr)_auto]", - tone === "default" && "text-gs-text", - tone === "danger" && "text-gs-error", - isSelected && - !isFocused && - tone === "default" && - "bg-gs-action-active", - isSelected && !isFocused && tone === "danger" && "bg-gs-error-subtle", - isFocused && tone === "default" && "bg-gs-action-hover", - isFocused && tone === "danger" && "bg-gs-error-subtle", - isDisabled - ? "cursor-not-allowed text-gs-text-disabled opacity-gs-disabled" - : "cursor-pointer", - className, - ) + const sharedProps = { + ...itemProps, + role, + tabIndex: -1, + "data-tone": tone, + ...(behavior.selectionMode !== "none" ? { "aria-checked": selected } : {}), + ...(disabled ? { "aria-disabled": true as const } : {}), + className: clsx( + "gs-dropdown-item relative m-0 grid min-h-gs-control-hit-target items-center gap-x-3 rounded-gs-xs px-3 py-2 text-start text-sm leading-gs-normal no-underline outline-none transition-colors duration-150 ease-gs-standard motion-reduce:transition-none", + leading?.children != null + ? "grid-cols-[1rem_minmax(0,1fr)_auto]" + : "grid-cols-[minmax(0,1fr)_auto]", + tone === "default" && + "text-gs-text focus:bg-gs-action-hover data-[selected=true]:not-focus:bg-gs-action-active", + tone === "danger" && + "text-gs-error focus:bg-gs-error-subtle data-[selected=true]:bg-gs-error-subtle", + disabled + ? "cursor-not-allowed text-gs-text-disabled opacity-gs-disabled" + : "cursor-pointer", + className, + ), + "data-selected": selected ? "true" : undefined, + onClick: (event: MouseEvent) => { + if (disabled) { + event.preventDefault(); + return; } - > - {({ isSelected }) => ( - <> - {leading?.children != null ? ( + behavior.activate(itemKey, props); + }, + // Menus use "hover moves focus" semantics so focus and hover styling + // stay unified, matching the previous library behavior. + onMouseEnter: (event: MouseEvent) => { + if (!disabled) event.currentTarget.focus(); + }, + children: ( + <> + {leading?.children != null ? ( + *]:block [&>*]:size-full", + tone === "danger" && "text-current", + leading.className, + )} + > + {leading.children} + + ) : null} + + + {label} + + {description?.children != null ? ( *]:block [&>*]:size-full", - tone === "danger" && "text-current", - leading.className, + "gs-dropdown-item-description mt-0.5 block text-xs font-normal leading-gs-normal text-gs-text-secondary", + description.className, )} > - {leading.children} + {description.children} ) : null} - - - {label} - - {description?.children != null ? ( + + {trailing?.children != null || selected ? ( + + {trailing?.children != null ? ( - {description.children} + {trailing.children} + + ) : null} + {selected ? ( + ) : null} - {trailing?.children != null || isSelected ? ( - - {trailing?.children != null ? ( - - {trailing.children} - - ) : null} - {isSelected ? ( - - ) : null} - - ) : null} - - )} - - ); + ) : null} + + ), + }; + + if (href !== undefined && !disabled) { + return ( + + ); + } + return
    ; } function renderMenuChildren( children: ReactNode, - closeMenu: () => void, - closeOnSelect: boolean, + behavior: MenuBehavior, ): ReactNode[] { const result: ReactNode[] = []; dispatchCompoundSlots( children, { "Dropdown.Item": (child) => { - result.push( - renderItem( - child.props as DropdownItemProps, - closeMenu, - closeOnSelect, - ), - ); + result.push(renderItem(child.props as DropdownItemProps, behavior)); }, "Dropdown.Section": (child) => { const section = child.props as DropdownSectionProps; @@ -207,36 +223,41 @@ function renderMenuChildren( items.push(sectionChild.props as DropdownItemProps); }, }); + const { children: _sectionChildren, ...sectionProps } = section; + void _sectionChildren; result.push( - {title?.children != null ? ( -
    {title.children} -
    + ) : null} - {items.map((item) => renderItem(item, closeMenu, closeOnSelect))} -
    , + {items.map((item) => renderItem(item, behavior))} +
    , ); }, "Dropdown.Separator": (child) => { const separator = child.props as DropdownSeparatorProps; result.push( - | "all" | undefined, +): DropdownSelection | undefined { + if (keys === undefined) return undefined; + return keys === "all" ? "all" : new Set(keys); +} + +function collectAllKeys(children: ReactNode): DropdownKey[] { + const keys: DropdownKey[] = []; + const pushItemKey = (props: DropdownItemProps) => { + const key = props.value ?? props.id; + if (key !== undefined) keys.push(key); + }; + dispatchCompoundSlots( + children, + { + "Dropdown.Item": (child) => pushItemKey(child.props as DropdownItemProps), + "Dropdown.Section": (child) => { + dispatchCompoundSlots( + (child.props as DropdownSectionProps).children, + { + "Dropdown.Item": (sectionChild) => + pushItemKey(sectionChild.props as DropdownItemProps), + }, + () => {}, + ); + }, + "Dropdown.Separator": () => {}, + }, + () => {}, + ); + return keys; +} + export type DropdownMenuContentProps = { menu: DropdownMenuProps; labelledBy?: string; @@ -258,6 +313,15 @@ export type DropdownMenuContentProps = { autoFocus: "first" | "last"; }; +function getEnabledItems(menu: HTMLElement | null): HTMLElement[] { + if (!menu) return []; + return Array.from( + menu.querySelectorAll( + '[role^="menuitem"]:not([aria-disabled="true"])', + ), + ); +} + export default function DropdownMenuContent({ menu, labelledBy, @@ -282,35 +346,106 @@ export default function DropdownMenuContent({ void _closeOnSelect; void _style; + const [selection, setSelection] = useControllableState({ + value: normalizeSelection(selectedKeys), + defaultValue: normalizeSelection(defaultSelectedKeys) ?? new Set(), + onChange: onSelectionChange, + }); + + const behavior = useMemo( + () => ({ + selectionMode, + isSelected: (key) => + selectionMode !== "none" && + (selection === "all" ? true : selection.has(key)), + activate: (key, item) => { + if (selectionMode === "single") { + setSelection(new Set([key])); + } else if (selectionMode === "multiple") { + const next = + selection === "all" + ? new Set(collectAllKeys(children)) + : new Set(selection); + if (next.has(key)) next.delete(key); + else next.add(key); + setSelection(next); + } + item.onAction?.(); + onAction?.(key); + if (item.closeOnSelect ?? closeOnSelect) closeMenu(); + }, + }), + [ + children, + closeMenu, + closeOnSelect, + onAction, + selection, + selectionMode, + setSelection, + ], + ); + useEffect(() => { const frame = requestAnimationFrame(() => { - const items = menuRef.current?.querySelectorAll( - '[role^="menuitem"]:not([aria-disabled="true"])', - ); - const target = - autoFocus === "last" ? items?.[items.length - 1] : items?.[0]; + const items = getEnabledItems(menuRef.current); + const target = autoFocus === "last" ? items[items.length - 1] : items[0]; target?.focus(); }); return () => cancelAnimationFrame(frame); }, [autoFocus]); + const handleKeyDown = useCallback((event: KeyboardEvent) => { + const items = getEnabledItems(menuRef.current); + if (items.length === 0) return; + const currentIndex = items.findIndex( + (item) => item === document.activeElement, + ); + + const focusAt = (index: number) => { + event.preventDefault(); + items[(index + items.length) % items.length]?.focus(); + }; + + switch (event.key) { + case "ArrowDown": + focusAt(currentIndex + 1); + break; + case "ArrowUp": + focusAt(currentIndex - 1); + break; + case "Home": + focusAt(0); + break; + case "End": + focusAt(items.length - 1); + break; + case "Enter": + case " ": + if (currentIndex >= 0) { + event.preventDefault(); + items[currentIndex]?.click(); + } + break; + default: + break; + } + }, []); + return ( - onAction?.(key)} className={clsx( "gs-dropdown-menu grid max-h-[min(24rem,calc(100vh-var(--space-8)))] min-w-0 gap-0.5 overflow-y-auto outline-none", className, )} + onKeyDown={handleKeyDown} > - {renderMenuChildren(children, closeMenu, closeOnSelect)} - + {renderMenuChildren(children, behavior)} + ); } diff --git a/packages/react/src/flex/Flex.stories.tsx b/packages/react/src/flex/Flex.stories.tsx index 9233f42..f6cd0da 100644 --- a/packages/react/src/flex/Flex.stories.tsx +++ b/packages/react/src/flex/Flex.stories.tsx @@ -59,3 +59,17 @@ export const Wrap = { ), }; + +export const Responsive = { + render: () => ( +
    + +
    Primary
    +
    Secondary
    +
    +
    + ), +}; diff --git a/packages/react/src/flex/Flex.test.tsx b/packages/react/src/flex/Flex.test.tsx index 00af05d..8592f42 100644 --- a/packages/react/src/flex/Flex.test.tsx +++ b/packages/react/src/flex/Flex.test.tsx @@ -37,4 +37,25 @@ describe("Flex", () => { ]), ); }); + + it("supports responsive direction, gap, and wrapping", () => { + render( + , + ); + expect(Array.from(screen.getByTestId("flex").classList)).toEqual( + expect.arrayContaining([ + "flex-col", + "md:flex-row", + "gap-2", + "lg:gap-6", + "flex-nowrap", + "md:flex-wrap", + ]), + ); + }); }); diff --git a/packages/react/src/flex/Flex.tsx b/packages/react/src/flex/Flex.tsx index bf1d721..76306de 100644 --- a/packages/react/src/flex/Flex.tsx +++ b/packages/react/src/flex/Flex.tsx @@ -1,13 +1,17 @@ import type { ElementType, ForwardedRef } from "react"; import { createElement, forwardRef } from "react"; import { clsx } from "clsx"; -import type { FlexProps, FlexWrap } from "./Flex.types"; +import type { FlexProps } from "./Flex.types"; import type { PolymorphicComponent } from "../shared/polymorphic"; import { alignItemsClasses, gapClasses, justifyContentClasses, } from "../shared/tailwind-classes"; +import { + responsiveBooleanClasses, + responsiveClasses, +} from "../shared/responsive"; const directionClasses = { row: "flex-row", @@ -17,26 +21,13 @@ const directionClasses = { } as const; const wrapClasses = { + true: "flex-wrap", + false: "flex-nowrap", wrap: "flex-wrap", nowrap: "flex-nowrap", "wrap-reverse": "flex-wrap-reverse", } as const; -function resolveWrap( - wrap: FlexWrap | undefined, -): "wrap" | "nowrap" | "wrap-reverse" | undefined { - if (wrap === true || wrap === "wrap") { - return "wrap"; - } - if (wrap === "wrap-reverse") { - return "wrap-reverse"; - } - if (wrap === false || wrap === "nowrap") { - return "nowrap"; - } - return undefined; -} - function FlexImpl( { as = "div", @@ -53,30 +44,37 @@ function FlexImpl( }: FlexProps, ref: ForwardedRef, ) { - const resolvedWrap = resolveWrap(wrap); - return createElement( as, { ref, className: clsx( "gs-flex min-w-0", - inline ? "inline-flex" : "flex", - fullWidth && "w-full", - directionClasses[direction], - align && alignItemsClasses[align], - justify && justifyContentClasses[justify], - gap && gapClasses[gap], - resolvedWrap && wrapClasses[resolvedWrap], + "flex", + responsiveBooleanClasses(inline, "inline-flex", "flex"), + responsiveBooleanClasses(fullWidth, "w-full", "w-auto"), + responsiveClasses(direction, directionClasses, "row"), + responsiveClasses(align, alignItemsClasses), + responsiveClasses(justify, justifyContentClasses), + responsiveClasses(gap, gapClasses), + responsiveClasses(wrap, wrapClasses, false), className, ), - "data-direction": direction, - "data-align": align, - "data-justify": justify, - "data-gap": gap, - "data-wrap": resolvedWrap, - "data-inline": inline ? "true" : undefined, - "data-full-width": fullWidth ? "true" : undefined, + "data-direction": typeof direction === "string" ? direction : undefined, + "data-align": typeof align === "string" ? align : undefined, + "data-justify": typeof justify === "string" ? justify : undefined, + "data-gap": typeof gap === "string" ? gap : undefined, + "data-wrap": + typeof wrap === "boolean" + ? wrap + ? "wrap" + : "nowrap" + : typeof wrap === "string" + ? wrap + : undefined, + "data-inline": typeof inline === "boolean" && inline ? "true" : undefined, + "data-full-width": + typeof fullWidth === "boolean" && fullWidth ? "true" : undefined, ...props, }, children, diff --git a/packages/react/src/flex/Flex.types.ts b/packages/react/src/flex/Flex.types.ts index 045d92f..b367d03 100644 --- a/packages/react/src/flex/Flex.types.ts +++ b/packages/react/src/flex/Flex.types.ts @@ -1,6 +1,7 @@ import type { ElementType } from "react"; import type { BoxSpacing } from "../box"; import type { PolymorphicProps } from "../shared/polymorphic"; +import type { Responsive } from "../shared/responsive"; export type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse"; export type FlexAlign = "start" | "center" | "end" | "stretch" | "baseline"; @@ -14,16 +15,16 @@ export type FlexJustify = export type FlexWrap = boolean | "wrap" | "nowrap" | "wrap-reverse"; export interface FlexOwnProps { - direction?: FlexDirection; - align?: FlexAlign; - justify?: FlexJustify; - gap?: BoxSpacing; + direction?: Responsive; + align?: Responsive; + justify?: Responsive; + gap?: Responsive; /** Enable wrapping. Pass `"wrap-reverse"` for reverse wrap. */ - wrap?: FlexWrap; + wrap?: Responsive; /** `display: inline-flex` instead of `flex`. */ - inline?: boolean; + inline?: Responsive; /** Stretch to parent width. */ - fullWidth?: boolean; + fullWidth?: Responsive; } export type FlexProps = PolymorphicProps< diff --git a/packages/react/src/form/Form.stories.tsx b/packages/react/src/form/Form.stories.tsx index a84ff54..0bbeccc 100644 --- a/packages/react/src/form/Form.stories.tsx +++ b/packages/react/src/form/Form.stories.tsx @@ -1,5 +1,6 @@ import type { CSSProperties } from "react"; import { useState } from "react"; +import { z } from "zod"; import { Button } from "../button"; import { Input } from "../input"; import { Switch } from "../switch"; @@ -70,6 +71,46 @@ export const Default = { }, }; +const signUpSchema = z.object({ + email: z.string().email("Enter a valid email"), + password: z.string().min(8, "At least 8 characters"), +}); + +export const WithZodSchema = { + render: function WithZodSchemaStory() { + const [result, setResult] = useState(""); + return ( +
    + { + setResult(JSON.stringify(values, null, 2)); + }} + > + + + Email + + + + + Password + Validated by a zod schema. + + + + + {result ? ( + + {result} + + ) : null} +
    + ); + }, +}; + export const Nested = { render: () => (
    diff --git a/packages/react/src/form/Form.test.tsx b/packages/react/src/form/Form.test.tsx index 32569df..960349f 100644 --- a/packages/react/src/form/Form.test.tsx +++ b/packages/react/src/form/Form.test.tsx @@ -8,6 +8,7 @@ import { waitFor, } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; import { Form } from "./Form"; import { getValueByName, setValueByName } from "./form-utils"; import { runRules } from "./validate"; @@ -112,4 +113,121 @@ describe("Form", () => { expect(onSubmitFailed).not.toHaveBeenCalled(); }); + + describe("schema validation (Standard Schema / zod)", () => { + it("blocks submit and shows zod issues on their fields", async () => { + const schema = z.object({ + email: z.string().email("Enter a valid email"), + profile: z.object({ + age: z.coerce.number().min(18, "Must be 18 or older"), + }), + }); + const onSubmit = vi.fn(); + const onSubmitFailed = vi.fn(); + render( +
    + + + + + + + +
    , + ); + + fireEvent.click(screen.getByRole("button", { name: "Submit" })); + + await waitFor(() => + expect(screen.getByText("Enter a valid email")).toBeTruthy(), + ); + expect(screen.getByText("Must be 18 or older")).toBeTruthy(); + expect(onSubmit).not.toHaveBeenCalled(); + expect(onSubmitFailed).toHaveBeenCalledWith( + expect.objectContaining({ + email: "Enter a valid email", + "profile.age": "Must be 18 or older", + }), + expect.anything(), + ); + }); + + it("submits the parsed schema output including transforms", async () => { + const schema = z.object({ + name: z.string().trim().min(1), + age: z.coerce.number(), + }); + const onSubmit = vi.fn(); + render( +
    + + + + + + + +
    , + ); + + fireEvent.click(screen.getByRole("button", { name: "Submit" })); + + await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + expect(onSubmit).toHaveBeenCalledWith({ name: "Ada", age: 36 }); + }); + + it("validates a field against the schema on blur", async () => { + const schema = z.object({ + email: z.string().email("Enter a valid email"), + }); + render( +
    + + + +
    , + ); + + fireEvent.blur(screen.getByLabelText("Email")); + + await waitFor(() => + expect(screen.getByText("Enter a valid email")).toBeTruthy(), + ); + }); + + it("prefers explicit field rules over schema issues", async () => { + const schema = z.object({ + email: z.string().email("Schema message"), + }); + const onSubmitFailed = vi.fn(); + render( +
    + + + + +
    , + ); + + fireEvent.click(screen.getByRole("button", { name: "Submit" })); + + await waitFor(() => + expect(screen.getByText("Rule message")).toBeTruthy(), + ); + expect(screen.queryByText("Schema message")).toBeNull(); + }); + }); }); diff --git a/packages/react/src/form/Form.tsx b/packages/react/src/form/Form.tsx index be41328..2525610 100644 --- a/packages/react/src/form/Form.tsx +++ b/packages/react/src/form/Form.tsx @@ -29,6 +29,7 @@ import type { FormProps, FormRule, } from "./Form.types"; +import { runStandardSchema } from "./standard-schema"; import { runRules, validateFields } from "./validate"; const FormContext = createContext(null); @@ -46,6 +47,7 @@ function FormImpl( { initialValues = {}, values: valuesProp, + schema, onValuesChange, onSubmit, onSubmitFailed, @@ -65,6 +67,8 @@ function FormImpl( const validationRunsRef = useRef(new Map()); const submitRunRef = useRef(0); const mountedRef = useRef(true); + const schemaRef = useRef(schema); + schemaRef.current = schema; const formId = useId(); useEffect(() => { @@ -142,11 +146,17 @@ function FormImpl( validating: true, }, })); - const error = await runRules( + let error = await runRules( getValueByName(current, name), current, rulesRef.current.get(name) ?? [], ); + if (error === undefined && schemaRef.current) { + const result = await runStandardSchema(schemaRef.current, current); + if (!result.success) { + error = result.errors[name]; + } + } if (!mountedRef.current || validationRunsRef.current.get(name) !== run) { return error; } @@ -175,6 +185,20 @@ function FormImpl( ); }); const nextErrors = await validateFields(values, rulesRef.current); + let submitValues = values; + if (schemaRef.current) { + const result = await runStandardSchema(schemaRef.current, values); + if (result.success) { + submitValues = result.value as FormValues; + } else { + for (const [name, message] of Object.entries(result.errors)) { + // Field rules take precedence over schema issues on the same path. + if (nextErrors[name] === undefined) { + nextErrors[name] = message; + } + } + } + } if (!mountedRef.current || submitRunRef.current !== submitRun) { return; } @@ -204,7 +228,7 @@ function FormImpl( return; } - await onSubmit?.(values); + await onSubmit?.(submitValues); }; const ctx = useMemo( @@ -359,12 +383,16 @@ function FormItemImpl( name: childProps.name ?? name, onChange: handleChange, onBlur: handleBlur, - invalid: Boolean(error) || childProps.invalid, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy || undefined, required: isRequired || undefined, }; + // `invalid` is a library prop; native DOM elements only get aria-invalid. + if (typeof child.type !== "string") { + injected.invalid = Boolean(error) || childProps.invalid; + } + // Boolean values bind to checked (Switch / checkbox); otherwise value. if (typeof value === "boolean" || childProps.checked !== undefined) { injected.checked = Boolean(value); diff --git a/packages/react/src/form/Form.types.ts b/packages/react/src/form/Form.types.ts index a14b467..0629748 100644 --- a/packages/react/src/form/Form.types.ts +++ b/packages/react/src/form/Form.types.ts @@ -6,6 +6,7 @@ import type { ReactNode, } from "react"; import type { FormValues } from "./form-utils"; +import type { FormSchema } from "./standard-schema"; export type RuleObject = { required?: boolean | string; @@ -37,7 +38,14 @@ export interface FormProps extends Omit< > { initialValues?: FormValues; values?: FormValues; + /** + * Standard Schema (zod 3.24+, valibot, arktype, …) validated against the + * whole values object. Issue paths map to `Form.Item` names as dot paths. + * Explicit field `rules` take precedence for the same field. + */ + schema?: FormSchema; onValuesChange?: (changed: FormValues, all: FormValues) => void; + /** With `schema`, receives the parsed (possibly transformed) output. */ onSubmit?: (values: FormValues) => void | Promise; onSubmitFailed?: (errors: FormErrors, values: FormValues) => void; /** Validate on change after first blur. Default: `true`. */ diff --git a/packages/react/src/form/index.ts b/packages/react/src/form/index.ts index 3961b64..be1b819 100644 --- a/packages/react/src/form/index.ts +++ b/packages/react/src/form/index.ts @@ -8,3 +8,4 @@ export type { RuleObject, } from "./Form.types"; export type { FormValues } from "./form-utils"; +export type { FormSchema, StandardSchemaV1 } from "./standard-schema"; diff --git a/packages/react/src/form/standard-schema.ts b/packages/react/src/form/standard-schema.ts new file mode 100644 index 0000000..7cb14f9 --- /dev/null +++ b/packages/react/src/form/standard-schema.ts @@ -0,0 +1,92 @@ +import type { FormErrors } from "./Form.types"; + +/** + * The Standard Schema interface (https://standardschema.dev), implemented by + * zod 3.24+, valibot, arktype, and others. The spec recommends vendoring + * these types so no runtime or type dependency is required. + */ +export interface StandardSchemaV1 { + readonly "~standard": StandardSchemaV1Props; +} + +export interface StandardSchemaV1Props { + readonly version: 1; + readonly vendor: string; + readonly validate: ( + value: unknown, + ) => StandardSchemaV1Result | Promise>; + readonly types?: StandardSchemaV1Types | undefined; +} + +export type StandardSchemaV1Result = + | StandardSchemaV1SuccessResult + | StandardSchemaV1FailureResult; + +export interface StandardSchemaV1SuccessResult { + readonly value: Output; + readonly issues?: undefined; +} + +export interface StandardSchemaV1FailureResult { + readonly issues: ReadonlyArray; +} + +export interface StandardSchemaV1Issue { + readonly message: string; + readonly path?: + | ReadonlyArray + | undefined; +} + +export interface StandardSchemaV1PathSegment { + readonly key: PropertyKey; +} + +export interface StandardSchemaV1Types { + readonly input: Input; + readonly output: Output; +} + +/** Schema accepted by `
    `. */ +export type FormSchema = StandardSchemaV1; + +export function issuePathToName(path: StandardSchemaV1Issue["path"]): string { + if (!path || path.length === 0) { + return ""; + } + return path + .map((segment) => + typeof segment === "object" && segment != null && "key" in segment + ? String(segment.key) + : String(segment), + ) + .join("."); +} + +export type SchemaValidationResult = + | { success: true; value: unknown } + | { success: false; errors: FormErrors }; + +/** + * Runs a Standard Schema against the form values and maps its issues to the + * dot-path field names used by `Form.Item`. Issues without a path are + * reported under the empty-string key. + */ +export async function runStandardSchema( + schema: StandardSchemaV1, + values: unknown, +): Promise { + const result = await schema["~standard"].validate(values); + if (result.issues == null) { + return { success: true, value: result.value }; + } + const errors: FormErrors = {}; + for (const issue of result.issues) { + const name = issuePathToName(issue.path); + // The first issue per field wins, matching rule evaluation order. + if (errors[name] === undefined) { + errors[name] = issue.message; + } + } + return { success: false, errors }; +} diff --git a/packages/react/src/grid/Grid.stories.tsx b/packages/react/src/grid/Grid.stories.tsx index 99fbb86..5d11736 100644 --- a/packages/react/src/grid/Grid.stories.tsx +++ b/packages/react/src/grid/Grid.stories.tsx @@ -38,11 +38,15 @@ export const Default = { export const Responsive = { render: () => (
    - + Collapses to 1 column below 768px
    - + {Array.from({ length: 4 }, (_, i) => (
    Col {i + 1} diff --git a/packages/react/src/grid/Grid.test.tsx b/packages/react/src/grid/Grid.test.tsx index 0f711fc..12ce454 100644 --- a/packages/react/src/grid/Grid.test.tsx +++ b/packages/react/src/grid/Grid.test.tsx @@ -34,4 +34,26 @@ describe("Grid", () => { ]), ); }); + + it("supports responsive columns and gaps", () => { + render( + , + ); + expect(Array.from(screen.getByTestId("grid").classList)).toEqual( + expect.arrayContaining([ + "grid-cols-1", + "sm:grid-cols-2", + "lg:grid-cols-4", + "gap-2", + "md:gap-6", + ]), + ); + expect( + screen.getByTestId("grid").classList.contains("max-md:grid-cols-1"), + ).toBe(false); + }); }); diff --git a/packages/react/src/grid/Grid.tsx b/packages/react/src/grid/Grid.tsx index 80586a2..119b2fe 100644 --- a/packages/react/src/grid/Grid.tsx +++ b/packages/react/src/grid/Grid.tsx @@ -4,6 +4,10 @@ import { clsx } from "clsx"; import type { GridProps } from "./Grid.types"; import type { PolymorphicComponent } from "../shared/polymorphic"; import { alignItemsClasses, gapClasses } from "../shared/tailwind-classes"; +import { + responsiveBooleanClasses, + responsiveClasses, +} from "../shared/responsive"; const columnClasses = { 1: "grid-cols-1", @@ -42,20 +46,25 @@ function GridImpl( ref, className: clsx( "gs-grid grid min-w-0", - columnClasses[columns], - responsive && "max-md:grid-cols-1", - fullWidth && "w-full", - gapClasses[gap], - align && alignItemsClasses[align], - justify && justifyItemsClasses[justify], + responsiveClasses(columns, columnClasses, 12), + responsive && typeof columns !== "object" && "max-md:grid-cols-1", + responsiveBooleanClasses(fullWidth, "w-full", "w-auto", true), + responsiveClasses(gap, gapClasses, "4"), + responsiveClasses(align, alignItemsClasses), + responsiveClasses(justify, justifyItemsClasses), className, ), - "data-columns": columns, - "data-gap": gap, + "data-columns": typeof columns === "number" ? columns : undefined, + "data-gap": typeof gap === "string" ? gap : undefined, "data-responsive": responsive ? "true" : undefined, - "data-align": align, - "data-justify": justify, - "data-full-width": fullWidth ? "true" : undefined, + "data-align": typeof align === "string" ? align : undefined, + "data-justify": typeof justify === "string" ? justify : undefined, + "data-full-width": + typeof fullWidth === "boolean" + ? fullWidth + ? "true" + : undefined + : undefined, ...props, }, children, diff --git a/packages/react/src/grid/Grid.types.ts b/packages/react/src/grid/Grid.types.ts index a1c2476..b87ddef 100644 --- a/packages/react/src/grid/Grid.types.ts +++ b/packages/react/src/grid/Grid.types.ts @@ -2,19 +2,20 @@ import type { ElementType } from "react"; import type { BoxSpacing } from "../box"; import type { FlexAlign } from "../flex"; import type { PolymorphicProps } from "../shared/polymorphic"; +import type { Responsive } from "../shared/responsive"; export type GridColumns = 1 | 2 | 3 | 4 | 6 | 12; export type GridJustify = "start" | "center" | "end" | "stretch"; export interface GridOwnProps { - columns?: GridColumns; - gap?: BoxSpacing; + columns?: Responsive; + gap?: Responsive; /** Collapse to a single column below the md breakpoint. Default: `true`. */ responsive?: boolean; - align?: FlexAlign; - justify?: GridJustify; + align?: Responsive; + justify?: Responsive; /** Stretch to parent width. Default: `true`. */ - fullWidth?: boolean; + fullWidth?: Responsive; } export type GridProps = PolymorphicProps< diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 59a6195..dc13c65 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -20,11 +20,14 @@ export { Badge } from "./badge"; export type { BadgeProps, BadgeTone } from "./badge"; export { Box } from "./box"; export type { BoxDisplay, BoxProps, BoxSpacing } from "./box"; -export { Button } from "./button"; +export type { Breakpoint, Responsive } from "./shared/responsive"; +export { Button, buttonClasses } from "./button"; export type { + ButtonClassesOptions, ButtonLeadingProps, ButtonProps, ButtonSize, + ButtonTone, ButtonTrailingProps, ButtonVariant, } from "./button"; @@ -43,8 +46,13 @@ export type { export { Checkbox } from "./checkbox"; export type { CheckboxDescriptionProps, + CheckboxGroupDescriptionProps, + CheckboxGroupErrorMessageProps, + CheckboxGroupLabelProps, + CheckboxGroupOrientation, CheckboxGroupProps, CheckboxProps, + CheckboxSize, CheckboxValue, } from "./checkbox"; export { Collapse } from "./collapse"; @@ -59,6 +67,7 @@ export type { CollapseTriggerProps, CollapseType, CollapseValue, + CollapseVariant, } from "./collapse"; export { Container } from "./container"; export type { ContainerProps, ContainerSize } from "./container"; @@ -76,6 +85,7 @@ export type { DatePickerErrorMessageProps, DatePickerLabelProps, DatePickerProps, + DatePickerSize, DatePickerWeekStartsOn, } from "./date-picker"; export { DateRangePicker } from "./date-range-picker"; @@ -130,19 +140,22 @@ export type { FormItemProps, FormProps, FormRule, + FormSchema, FormValues, RuleObject, + StandardSchemaV1, } from "./form"; export { Grid } from "./grid"; export type { GridColumns, GridJustify, GridProps } from "./grid"; -export { Input } from "./input"; +export { Input, inputShellClasses } from "./input"; export type { InputDescriptionProps, InputErrorMessageProps, InputLabelProps, InputPrefixProps, InputProps, + InputShellClassesOptions, InputSize, InputSuffixProps, } from "./input"; @@ -248,8 +261,14 @@ export type { TabsVariant, TabsValue, } from "./tabs"; -export { Tag } from "./tag"; -export type { TagIconProps, TagProps, TagSize, TagTone } from "./tag"; +export { Tag, tagClasses } from "./tag"; +export type { + TagClassesOptions, + TagIconProps, + TagProps, + TagSize, + TagTone, +} from "./tag"; export { Text } from "./text"; export type { TextAlign, @@ -267,12 +286,14 @@ export type { TextAreaErrorMessageProps, TextAreaLabelProps, TextAreaProps, + TextAreaResize, } from "./text-area"; export { toast, ToastProvider } from "./toast"; export type { ToastAction, ToastOptions, ToastPosition, + ToastPromiseMessages, ToastProviderProps, ToastRecord, ToastShowInput, @@ -336,3 +357,31 @@ export type { UseThemeToggleOptions, UseThemeToggleReturn, } from "./theme"; +export { Alert } from "./alert"; +export type { + AlertDescriptionProps, + AlertProps, + AlertTitleProps, + AlertTone, +} from "./alert"; +export { Breadcrumb } from "./breadcrumb"; +export type { BreadcrumbItemProps, BreadcrumbProps } from "./breadcrumb"; +export { Combobox } from "./combobox"; +export type { + ComboboxDescriptionProps, + ComboboxEmptyProps, + ComboboxErrorMessageProps, + ComboboxItemProps, + ComboboxLabelProps, + ComboboxNoMatchesProps, + ComboboxProps, + ComboboxValue, +} from "./combobox"; +export { Slider } from "./slider"; +export type { + SliderLabelProps, + SliderOrientation, + SliderOutputProps, + SliderProps, + SliderValue, +} from "./slider"; diff --git a/packages/react/src/input/Input.classes.ts b/packages/react/src/input/Input.classes.ts new file mode 100644 index 0000000..45f18f4 --- /dev/null +++ b/packages/react/src/input/Input.classes.ts @@ -0,0 +1,52 @@ +import { clsx } from "clsx"; +import type { InputSize } from "./Input.types"; + +export const inputShellSizeClasses: Record = { + sm: "[--gs-input-box:var(--input-height-sm)] [--gs-input-pad-y:var(--input-padding-y-sm)] [--gs-input-pad-x:var(--space-2)] [--gs-input-font:var(--input-font-size-sm)] [--gs-input-icon:var(--input-icon-size-sm)] [--gs-input-action:var(--input-action-size-sm)] [--gs-input-gap:var(--input-gap-sm)]", + md: "[--gs-input-box:var(--input-height-md)] [--gs-input-pad-y:var(--input-padding-y)] [--gs-input-pad-x:var(--space-3)] [--gs-input-font:var(--input-font-size)] [--gs-input-icon:var(--input-icon-size)] [--gs-input-action:var(--input-action-size)] [--gs-input-gap:var(--input-gap)]", + lg: "[--gs-input-box:var(--input-height-lg)] [--gs-input-pad-y:var(--input-padding-y-lg)] [--gs-input-pad-x:var(--space-4)] [--gs-input-font:var(--input-font-size-lg)] [--gs-input-icon:var(--input-icon-size-lg)] [--gs-input-action:var(--input-action-size-lg)] [--gs-input-gap:var(--input-gap)]", +}; + +export type InputShellClassesOptions = { + /** Default: `"md"`. */ + size?: InputSize; + invalid?: boolean; + disabled?: boolean; + readOnly?: boolean; + fullWidth?: boolean; +}; + +/** + * Style recipe for the Input control shell (the bordered box around the + * native input). Use it to give custom field controls the Input appearance: + * + * ```tsx + * + * + * + * ``` + */ +export function inputShellClasses({ + size = "md", + invalid = false, + disabled = false, + readOnly = false, + fullWidth = false, +}: InputShellClassesOptions = {}): string { + return clsx( + "gs-input-shell group/input-shell inline-flex h-[max(var(--gs-input-box),var(--control-hit-target))] min-h-[max(var(--gs-input-box),var(--control-hit-target))] min-w-gs-control-hit-target max-w-full items-center gap-gs-local-input-gap box-border rounded-gs-xs border border-gs-default bg-gs-surface bg-gs-surface-highlight px-gs-input-pad-x py-gs-input-pad-y align-middle text-gs-input-font font-gs-input-font-weight leading-[1.25] text-gs-input-color shadow-gs-surface-sheen transition-[background-color,border-color,box-shadow,opacity] duration-200 ease-gs-standard focus-within:border-gs-focus focus-within:bg-gs-surface-raised focus-within:shadow-gs-input-focus-border has-[.gs-input:focus-visible]:shadow-gs-input-surface-focus motion-reduce:transition-none [[data-reduced-motion=true]_&]:transition-none [[data-high-contrast=true]_&]:border [[data-high-contrast=true]_&]:border-gs-input-border [[data-high-contrast=true]_&]:focus-within:border-gs-focus", + inputShellSizeClasses[size], + fullWidth && "flex w-full", + invalid + ? "border-gs-error bg-gs-error-subtle focus-within:border-gs-error focus-within:bg-gs-error-subtle focus-within:shadow-gs-input-invalid-border has-[.gs-input:focus-visible]:shadow-gs-input-invalid-focus [[data-high-contrast=true]_&]:border-gs-error" + : !disabled && + !readOnly && + "hover:not-focus-within:border-gs-strong hover:not-focus-within:bg-gs-surface-muted", + invalid && + !disabled && + "hover:not-focus-within:border-gs-error hover:not-focus-within:bg-gs-error-tint", + disabled + ? "cursor-not-allowed opacity-gs-disabled" + : readOnly && "cursor-default", + ); +} diff --git a/packages/react/src/input/Input.tsx b/packages/react/src/input/Input.tsx index 37cabcf..c764f26 100644 --- a/packages/react/src/input/Input.tsx +++ b/packages/react/src/input/Input.tsx @@ -26,13 +26,13 @@ import { inputLabelSizeClasses, inputRequiredClasses, } from "../shared/input-tailwind-classes"; +import { inputShellClasses } from "./Input.classes"; import type { InputDescriptionProps, InputErrorMessageProps, InputLabelProps, InputPrefixProps, InputProps, - InputSize, InputSuffixProps, } from "./Input.types"; @@ -68,12 +68,6 @@ function collectInputComposition(children: ReactNode): InputComposition { return composition; } -const inputShellSizeClasses: Record = { - sm: "[--gs-input-box:var(--input-height-sm)] [--gs-input-pad-y:var(--input-padding-y-sm)] [--gs-input-pad-x:var(--space-2)] [--gs-input-font:var(--input-font-size-sm)] [--gs-input-icon:var(--input-icon-size-sm)] [--gs-input-action:var(--input-action-size-sm)] [--gs-input-gap:var(--input-gap-sm)]", - md: "[--gs-input-box:var(--input-height-md)] [--gs-input-pad-y:var(--input-padding-y)] [--gs-input-pad-x:var(--space-3)] [--gs-input-font:var(--input-font-size)] [--gs-input-icon:var(--input-icon-size)] [--gs-input-action:var(--input-action-size)] [--gs-input-gap:var(--input-gap)]", - lg: "[--gs-input-box:var(--input-height-lg)] [--gs-input-pad-y:var(--input-padding-y-lg)] [--gs-input-pad-x:var(--space-4)] [--gs-input-font:var(--input-font-size-lg)] [--gs-input-icon:var(--input-icon-size-lg)] [--gs-input-action:var(--input-action-size-lg)] [--gs-input-gap:var(--input-gap)]", -}; - const inputActionClasses = "gs-input-action m-0 inline-flex size-gs-input-action cursor-pointer items-center justify-center box-border rounded-gs-xs border-0 bg-transparent p-0 font-inherit leading-none text-gs-text-secondary transition-[color,background-color,opacity] duration-200 ease-gs-standard hover:not-disabled:bg-gs-action-hover hover:not-disabled:text-gs-text active:not-disabled:bg-gs-action-active focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:bg-gs-action-active focus-visible:text-gs-text focus-visible:outline-none focus-visible:shadow-gs-input-focus disabled:cursor-not-allowed disabled:opacity-gs-disabled motion-reduce:transition-none [[data-reduced-motion=true]_&]:transition-none"; @@ -271,21 +265,14 @@ function InputImpl( const shell = ( { expect(body.classList.contains("overflow-y-auto")).toBe(true); expect(footer.classList.contains("mt-5")).toBe(true); expect(close.classList.contains("absolute")).toBe(true); + expect(close.classList.contains("active:scale-95")).toBe(true); + expect(close.classList.contains("motion-reduce:transition-none")).toBe( + true, + ); }); }); diff --git a/packages/react/src/modal/Modal.tsx b/packages/react/src/modal/Modal.tsx index 9e87498..495b0a7 100644 --- a/packages/react/src/modal/Modal.tsx +++ b/packages/react/src/modal/Modal.tsx @@ -1,38 +1,16 @@ -import type { - ElementType, - ForwardedRef, - MouseEvent, - MutableRefObject, -} from "react"; -import { - createContext, - createElement, - forwardRef, - useContext, - useEffect, - useId, - useMemo, - useRef, - useState, -} from "react"; -import { useControllableState } from "@velune/hooks"; +import type { ElementType, ForwardedRef, MouseEvent } from "react"; +import { createElement, forwardRef } from "react"; import { clsx } from "clsx"; import { useComposedRefs } from "../shared/compose-refs"; import { - createFocusTrap, - focusFirst, - isFocusWithinTrapBoundary, -} from "../shared/focus-trap"; -import { isolateOthers } from "../shared/isolate-others"; -import { - acquireOverlayLayer, - isTopEscapeLayer, - popEscapeLayer, - pushEscapeLayer, - releaseOverlayLayer, -} from "../shared/overlay-stack"; + createDialogScope, + DialogCloseIcon, + handleDialogOverlayClick, + useDialogController, + useDialogSlotFlag, + type DialogContextValue, +} from "../shared/dialog"; import { Portal } from "../shared/portal"; -import { useScrollLock } from "../shared/use-scroll-lock"; import type { PolymorphicComponent } from "../shared/polymorphic"; import type { ModalBodyProps, @@ -46,46 +24,14 @@ import type { } from "./Modal.types"; const modalCloseClasses = - "gs-modal-close absolute right-3 top-3 inline-flex size-gs-control-hit-target cursor-pointer items-center justify-center rounded-gs-sm border-0 bg-transparent p-0 text-gs-text-secondary transition-colors duration-150 ease-gs-standard hover:bg-gs-action-hover hover:text-gs-text focus-visible:outline-none focus-visible:shadow-gs-button-focus-border [&_svg]:block [&_svg]:size-4"; - -type ModalContextValue = { - open: boolean; - setOpen: (open: boolean) => void; - titleId: string; - descriptionId: string; - contentRef: MutableRefObject; - hasTitle: boolean; - hasDescription: boolean; - setHasTitle: (value: boolean) => void; - setHasDescription: (value: boolean) => void; -}; + "gs-modal-close absolute right-3 top-3 inline-flex size-gs-control-hit-target cursor-pointer items-center justify-center rounded-gs-sm border-0 bg-transparent p-0 text-gs-text-secondary transition-[background-color,color,box-shadow,transform] duration-150 ease-gs-standard active:scale-95 hover:bg-gs-action-hover hover:text-gs-text focus-visible:outline-none focus-visible:shadow-gs-button-focus-border motion-reduce:transition-none motion-reduce:active:scale-100 [[data-reduced-motion=true]_&]:transition-none [[data-reduced-motion=true]_&]:active:scale-100 [&_svg]:block [&_svg]:size-4"; -const ModalContext = createContext(null); - -function useModalContext(component: string): ModalContextValue { - const ctx = useContext(ModalContext); - if (!ctx) { - throw new Error(`${component} must be used within `); - } - return ctx; -} - -function CloseIcon() { - return ( - - ); -} +const { Provider: ModalProvider, useDialogScope: useModalContext } = + createDialogScope("Modal"); function ModalImpl( { - open: openProp, + open, defaultOpen = false, onOpenChange, size = "md", @@ -105,147 +51,36 @@ function ModalImpl( }: ModalProps, ref: ForwardedRef, ) { - const [open, setOpen] = useControllableState({ - value: openProp, - defaultValue: defaultOpen, - onChange: onOpenChange, + const { context, layer, rootRef } = useDialogController({ + open, + defaultOpen, + onOpenChange, + closeOnEsc, + lockScroll, + finalFocusRef, + initialFocusRef, + onOpenAutoFocus, + onCloseAutoFocus, + onEscapeKeyDown, + eventPrefix: "velune.modal", }); - const baseId = useId(); - const titleId = `${baseId}-title`; - const descriptionId = `${baseId}-description`; - const contentRef = useRef(null); - const [rootNode, setRootNode] = useState(null); - const restoreFocusRef = useRef(null); - const [layer, setLayer] = useState(0); - const [hasTitle, setHasTitle] = useState(false); - const [hasDescription, setHasDescription] = useState(false); - - const setOpenRef = useRef(setOpen); - const closeOnEscRef = useRef(closeOnEsc); - const finalFocusRefRef = useRef(finalFocusRef); - const initialFocusRefRef = useRef(initialFocusRef); - const onOpenAutoFocusRef = useRef(onOpenAutoFocus); - const onCloseAutoFocusRef = useRef(onCloseAutoFocus); - const onEscapeKeyDownRef = useRef(onEscapeKeyDown); - const onOverlayClickRef = useRef(onOverlayClick); - setOpenRef.current = setOpen; - closeOnEscRef.current = closeOnEsc; - finalFocusRefRef.current = finalFocusRef; - initialFocusRefRef.current = initialFocusRef; - onOpenAutoFocusRef.current = onOpenAutoFocus; - onCloseAutoFocusRef.current = onCloseAutoFocus; - onEscapeKeyDownRef.current = onEscapeKeyDown; - onOverlayClickRef.current = onOverlayClick; - - const composedRootRef = useComposedRefs(setRootNode, ref); - useScrollLock(lockScroll && open); + const composedRootRef = useComposedRefs(rootRef, ref); - useEffect(() => { - if (!open || !rootNode) { - return; - } - restoreFocusRef.current = - (document.activeElement as HTMLElement | null) ?? null; - const overlayLayer = acquireOverlayLayer(); - setLayer(overlayLayer); - const escapeLayer = pushEscapeLayer(); - const restoreIsolation = isolateOthers(rootNode); - - let trapRelease: () => void = () => {}; - const finalFocusNode = finalFocusRefRef.current?.current ?? null; - const restoreTarget = finalFocusNode ?? restoreFocusRef.current; - const frame = requestAnimationFrame(() => { - if (contentRef.current) { - trapRelease = createFocusTrap(contentRef.current); - } - const event = new Event("velune.modal.openAutoFocus", { - cancelable: true, - }); - onOpenAutoFocusRef.current?.(event); - if (event.defaultPrevented) { - return; - } - if (initialFocusRefRef.current?.current) { - initialFocusRefRef.current.current.focus({ preventScroll: true }); - } else if ( - contentRef.current && - !isFocusWithinTrapBoundary(contentRef.current) - ) { - focusFirst(contentRef.current); - } - }); - - const onKeyDown = (event: KeyboardEvent) => { - // Only the top overlay may close; document-level listeners of the - // overlays below still receive the same event. - if ( - closeOnEscRef.current && - event.key === "Escape" && - isTopEscapeLayer(escapeLayer) - ) { - if (event.defaultPrevented) { - return; - } - onEscapeKeyDownRef.current?.(event); - if (event.defaultPrevented) { - return; - } - event.preventDefault(); - event.stopPropagation(); - setOpenRef.current(false); - } - }; - document.addEventListener("keydown", onKeyDown); - - return () => { - cancelAnimationFrame(frame); - trapRelease(); - document.removeEventListener("keydown", onKeyDown); - popEscapeLayer(escapeLayer); - releaseOverlayLayer(overlayLayer); - restoreIsolation(); - const event = new Event("velune.modal.closeAutoFocus", { - cancelable: true, - }); - onCloseAutoFocusRef.current?.(event); - if (!event.defaultPrevented && restoreTarget?.isConnected) { - restoreTarget.focus({ preventScroll: true }); - } - }; - }, [open, rootNode]); - - const ctx = useMemo( - () => ({ - open, - setOpen, - titleId, - descriptionId, - contentRef, - hasTitle, - hasDescription, - setHasTitle, - setHasDescription, - }), - [descriptionId, hasDescription, hasTitle, open, setOpen, titleId], - ); - - if (!open) { + if (!context.open) { return null; } const handleOverlayClick = (event: MouseEvent) => { - if (event.target === event.currentTarget) { - onOverlayClickRef.current?.(event); - if (event.defaultPrevented || !closeOnOverlayClick) { - return; - } - setOpen(false); - } + handleDialogOverlayClick(event, { + closeOnOverlayClick, + onOverlayClick, + setOpen: context.setOpen, + }); }; return ( - +
    -
    + ); } @@ -344,10 +179,7 @@ function ModalTitleImpl( ref: ForwardedRef, ) { const { titleId, setHasTitle } = useModalContext("Modal.Title"); - useEffect(() => { - setHasTitle(true); - return () => setHasTitle(false); - }, [setHasTitle]); + useDialogSlotFlag(setHasTitle); return createElement( as, @@ -378,10 +210,7 @@ const ModalDescription = forwardRef< >(({ className, children, ...props }, ref) => { const { descriptionId, setHasDescription } = useModalContext("Modal.Description"); - useEffect(() => { - setHasDescription(true); - return () => setHasDescription(false); - }, [setHasDescription]); + useDialogSlotFlag(setHasDescription); return (

    - {children ?? } + {children ?? } ); } diff --git a/packages/react/src/pagination/Pagination.test.tsx b/packages/react/src/pagination/Pagination.test.tsx index 88be66b..eadd0d2 100644 --- a/packages/react/src/pagination/Pagination.test.tsx +++ b/packages/react/src/pagination/Pagination.test.tsx @@ -39,7 +39,12 @@ describe("Pagination", () => { fireEvent.click(screen.getByRole("button", { name: "Page 2" })); expect(onChange).not.toHaveBeenCalled(); const active = screen.getByRole("button", { name: "Page 2" }); - expect(active.classList.contains("bg-gs-pagination-bg-active")).toBe(true); + expect( + active.classList.contains( + "data-[active=true]:bg-gs-pagination-bg-active", + ), + ).toBe(true); + expect(active.getAttribute("data-active")).toBe("true"); expect(active.classList.contains("font-normal")).toBe(true); expect(active.classList.contains("font-medium")).toBe(false); expect( @@ -104,7 +109,14 @@ describe("Pagination", () => { expect(screen.getByRole("button", { name: "Result page 1" })).toBeTruthy(); expect(screen.getByText("Results per page")).toBeTruthy(); expect(screen.getByText("Open")).toBeTruthy(); - expect(screen.getByRole("textbox", { name: "Page number" })).toBeTruthy(); + const input = screen.getByRole("textbox", { name: "Page number" }); + expect(input).toBeTruthy(); + expect( + input.classList.contains("transition-[border-color,box-shadow]"), + ).toBe(true); + expect(input.classList.contains("motion-reduce:transition-none")).toBe( + true, + ); }); it("preserves the last controlled page and size when changing modes", () => { @@ -118,4 +130,17 @@ describe("Pagination", () => { fireEvent.click(screen.getByRole("button", { name: "Next page" })); expect(screen.getByText("5 / 10")).toBeTruthy(); }); + + it("renders nothing on a single page when hideOnSinglePage is set", () => { + const { rerender } = render( + , + ); + expect(screen.queryByRole("navigation")).toBeNull(); + + rerender(); + expect(screen.getByRole("navigation")).toBeTruthy(); + + rerender(); + expect(screen.getByRole("navigation")).toBeTruthy(); + }); }); diff --git a/packages/react/src/pagination/Pagination.tsx b/packages/react/src/pagination/Pagination.tsx index aa449bd..635b86c 100644 --- a/packages/react/src/pagination/Pagination.tsx +++ b/packages/react/src/pagination/Pagination.tsx @@ -7,7 +7,7 @@ import type { PaginationProps } from "./Pagination.types"; import { buildPageList, clampPage, getTotalPages } from "./pagination-utils"; const itemClasses = - "gs-pagination-item m-0 inline-flex min-h-[max(var(--pagination-item-size),var(--control-hit-target))] min-w-[max(var(--pagination-item-size),var(--control-hit-target))] cursor-pointer appearance-none items-center justify-center rounded-gs-pagination-item-radius border-0 bg-transparent px-2 py-0 font-inherit font-normal text-inherit text-gs-text transition-colors duration-200 ease-gs-standard hover:not-disabled:not-data-[active=true]:bg-gs-pagination-bg-hover focus-visible:outline-none focus-visible:shadow-gs-button-focus-border disabled:cursor-not-allowed disabled:opacity-gs-disabled motion-reduce:transition-none [[data-reduced-motion=true]_&]:transition-none"; + "gs-pagination-item m-0 inline-flex min-h-[max(var(--pagination-item-size),var(--control-hit-target))] min-w-[max(var(--pagination-item-size),var(--control-hit-target))] cursor-pointer appearance-none items-center justify-center rounded-gs-pagination-item-radius border-0 bg-transparent px-2 py-0 font-inherit font-normal text-inherit text-gs-text transition-[background-color,color,box-shadow,transform] duration-150 ease-gs-standard active:scale-95 hover:not-disabled:not-data-[active=true]:bg-gs-pagination-bg-hover focus-visible:outline-none focus-visible:shadow-gs-button-focus-border disabled:cursor-not-allowed disabled:opacity-gs-disabled motion-reduce:transition-none motion-reduce:active:scale-100 [[data-reduced-motion=true]_&]:transition-none [[data-reduced-motion=true]_&]:active:scale-100 data-[active=true]:bg-gs-pagination-bg-active data-[active=true]:text-gs-pagination-color-active"; function defaultGetPageLabel(page: number): string { return `Page ${page}`; @@ -46,6 +46,7 @@ function PaginationImpl( total, onPageChange, simple = false, + hideOnSinglePage = false, showSizeChanger = false, pageSizeOptions = [10, 20, 50, 100], showQuickJumper = false, @@ -120,6 +121,10 @@ function PaginationImpl( setJumpValue(""); }; + if (hideOnSinglePage && totalPages <= 1) { + return null; + } + return (

    ); }; @@ -892,11 +900,12 @@ function SelectImpl( {...contentProps} ref={setFloatingNode} className={clsx( - "gs-select-panel z-gs-select grid overflow-hidden rounded-gs-xs border border-gs-surface-border bg-gs-surface-raised bg-gs-surface-highlight shadow-gs-select-list-shadow", + "gs-select-panel z-gs-select grid overflow-hidden rounded-gs-xs border border-gs-surface-border bg-gs-surface-raised bg-gs-surface-highlight shadow-gs-select-list-shadow data-[ready=true]:animate-gs-float-in data-[placement^=top]:[--gs-float-from:0_var(--space-1)] motion-reduce:animate-none [[data-reduced-motion=true]_&]:animate-none", content?.className, )} data-gs-overlay-branch="" data-ready={ready ? "true" : undefined} + data-placement={coords.placement} dir={dir} onKeyDown={handleKeyDown} style={{ diff --git a/packages/react/src/shared/dialog.tsx b/packages/react/src/shared/dialog.tsx new file mode 100644 index 0000000..67fa892 --- /dev/null +++ b/packages/react/src/shared/dialog.tsx @@ -0,0 +1,261 @@ +import type { MouseEvent, MutableRefObject, RefObject } from "react"; +import { + createContext, + useContext, + useEffect, + useId, + useMemo, + useRef, + useState, +} from "react"; +import { useControllableState } from "@velune/hooks"; +import { + createFocusTrap, + focusFirst, + isFocusWithinTrapBoundary, +} from "./focus-trap"; +import { isolateOthers } from "./isolate-others"; +import { + acquireOverlayLayer, + isTopEscapeLayer, + popEscapeLayer, + pushEscapeLayer, + releaseOverlayLayer, +} from "./overlay-stack"; +import { useScrollLock } from "./use-scroll-lock"; + +/** + * Shared behavior for modal dialog surfaces (Modal, Drawer): controllable + * open state, focus trap and restore, Escape/overlay dismissal layered + * against other overlays, scroll lock, and title/description wiring. + */ + +export type DialogContextValue = { + open: boolean; + setOpen: (open: boolean) => void; + titleId: string; + descriptionId: string; + contentRef: MutableRefObject; + hasTitle: boolean; + hasDescription: boolean; + setHasTitle: (value: boolean) => void; + setHasDescription: (value: boolean) => void; +}; + +export function createDialogScope( + rootName: string, +) { + const Context = createContext(null); + + function useDialogScope(component: string): TContext { + const ctx = useContext(Context); + if (!ctx) { + throw new Error(`${component} must be used within <${rootName}>`); + } + return ctx; + } + + return { Provider: Context.Provider, useDialogScope }; +} + +export type DialogControllerOptions = { + open?: boolean | undefined; + defaultOpen?: boolean; + onOpenChange?: ((open: boolean) => void) | undefined; + /** Close on Escape. Default: `true`. */ + closeOnEsc?: boolean; + /** Lock body scroll while open. Default: `true`. */ + lockScroll?: boolean; + finalFocusRef?: RefObject | undefined; + initialFocusRef?: RefObject | undefined; + onOpenAutoFocus?: ((event: Event) => void) | undefined; + onCloseAutoFocus?: ((event: Event) => void) | undefined; + onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; + /** Prefix for the cancelable auto-focus events, e.g. `velune.modal`. */ + eventPrefix: string; +}; + +export type DialogController = { + /** Memoized context value shared with the compound sub-components. */ + context: DialogContextValue; + /** Offset added to the surface z-index so later overlays stack on top. */ + layer: number; + /** Compose into the root element ref so the lifecycle can observe it. */ + rootRef: (node: HTMLDivElement | null) => void; +}; + +export function useDialogController({ + open: openProp, + defaultOpen = false, + onOpenChange, + closeOnEsc = true, + lockScroll = true, + finalFocusRef, + initialFocusRef, + onOpenAutoFocus, + onCloseAutoFocus, + onEscapeKeyDown, + eventPrefix, +}: DialogControllerOptions): DialogController { + const [open, setOpen] = useControllableState({ + value: openProp, + defaultValue: defaultOpen, + onChange: onOpenChange, + }); + const baseId = useId(); + const titleId = `${baseId}-title`; + const descriptionId = `${baseId}-description`; + const contentRef = useRef(null); + const [rootNode, setRootNode] = useState(null); + const restoreFocusRef = useRef(null); + const [layer, setLayer] = useState(0); + const [hasTitle, setHasTitle] = useState(false); + const [hasDescription, setHasDescription] = useState(false); + + const setOpenRef = useRef(setOpen); + const closeOnEscRef = useRef(closeOnEsc); + const finalFocusRefRef = useRef(finalFocusRef); + const initialFocusRefRef = useRef(initialFocusRef); + const onOpenAutoFocusRef = useRef(onOpenAutoFocus); + const onCloseAutoFocusRef = useRef(onCloseAutoFocus); + const onEscapeKeyDownRef = useRef(onEscapeKeyDown); + setOpenRef.current = setOpen; + closeOnEscRef.current = closeOnEsc; + finalFocusRefRef.current = finalFocusRef; + initialFocusRefRef.current = initialFocusRef; + onOpenAutoFocusRef.current = onOpenAutoFocus; + onCloseAutoFocusRef.current = onCloseAutoFocus; + onEscapeKeyDownRef.current = onEscapeKeyDown; + + useScrollLock(lockScroll && open); + + useEffect(() => { + if (!open || !rootNode) { + return; + } + restoreFocusRef.current = + (document.activeElement as HTMLElement | null) ?? null; + const overlayLayer = acquireOverlayLayer(); + setLayer(overlayLayer); + const escapeLayer = pushEscapeLayer(); + const restoreIsolation = isolateOthers(rootNode); + + let trapRelease: () => void = () => {}; + const finalFocusNode = finalFocusRefRef.current?.current ?? null; + const restoreTarget = finalFocusNode ?? restoreFocusRef.current; + const frame = requestAnimationFrame(() => { + if (contentRef.current) { + trapRelease = createFocusTrap(contentRef.current); + } + const event = new Event(`${eventPrefix}.openAutoFocus`, { + cancelable: true, + }); + onOpenAutoFocusRef.current?.(event); + if (event.defaultPrevented) { + return; + } + if (initialFocusRefRef.current?.current) { + initialFocusRefRef.current.current.focus({ preventScroll: true }); + } else if ( + contentRef.current && + !isFocusWithinTrapBoundary(contentRef.current) + ) { + focusFirst(contentRef.current); + } + }); + + const onKeyDown = (event: KeyboardEvent) => { + // Only the top overlay may close; document-level listeners of the + // overlays below still receive the same event. + if ( + closeOnEscRef.current && + event.key === "Escape" && + isTopEscapeLayer(escapeLayer) + ) { + if (event.defaultPrevented) { + return; + } + onEscapeKeyDownRef.current?.(event); + if (event.defaultPrevented) { + return; + } + event.preventDefault(); + event.stopPropagation(); + setOpenRef.current(false); + } + }; + document.addEventListener("keydown", onKeyDown); + + return () => { + cancelAnimationFrame(frame); + trapRelease(); + document.removeEventListener("keydown", onKeyDown); + popEscapeLayer(escapeLayer); + releaseOverlayLayer(overlayLayer); + restoreIsolation(); + const event = new Event(`${eventPrefix}.closeAutoFocus`, { + cancelable: true, + }); + onCloseAutoFocusRef.current?.(event); + if (!event.defaultPrevented && restoreTarget?.isConnected) { + restoreTarget.focus({ preventScroll: true }); + } + }; + }, [open, rootNode, eventPrefix]); + + const context = useMemo( + () => ({ + open, + setOpen, + titleId, + descriptionId, + contentRef, + hasTitle, + hasDescription, + setHasTitle, + setHasDescription, + }), + [descriptionId, hasDescription, hasTitle, open, setOpen, titleId], + ); + + return { context, layer, rootRef: setRootNode }; +} + +export function handleDialogOverlayClick( + event: MouseEvent, + options: { + closeOnOverlayClick: boolean; + onOverlayClick: ((event: MouseEvent) => void) | undefined; + setOpen: (open: boolean) => void; + }, +): void { + if (event.target !== event.currentTarget) { + return; + } + options.onOverlayClick?.(event); + if (event.defaultPrevented || !options.closeOnOverlayClick) { + return; + } + options.setOpen(false); +} + +/** Registers a title/description slot for the lifetime of the component. */ +export function useDialogSlotFlag(setFlag: (value: boolean) => void): void { + useEffect(() => { + setFlag(true); + return () => setFlag(false); + }, [setFlag]); +} + +export function DialogCloseIcon() { + return ( + + ); +} diff --git a/packages/react/src/shared/polymorphic.type-test.tsx b/packages/react/src/shared/polymorphic.type-test.tsx index 0a110a9..2770978 100644 --- a/packages/react/src/shared/polymorphic.type-test.tsx +++ b/packages/react/src/shared/polymorphic.type-test.tsx @@ -1,6 +1,7 @@ import { createRef } from "react"; import { Box } from "../box"; import { Card } from "../card"; +import { Container } from "../container"; import { Drawer } from "../drawer"; import { Flex } from "../flex"; import { Grid } from "../grid"; @@ -14,6 +15,7 @@ const labelRef = createRef(); const headingRef = createRef(); ; +; ; ; ; diff --git a/packages/react/src/shared/responsive.ts b/packages/react/src/shared/responsive.ts new file mode 100644 index 0000000..ecce63d --- /dev/null +++ b/packages/react/src/shared/responsive.ts @@ -0,0 +1,61 @@ +export type Breakpoint = "base" | "sm" | "md" | "lg" | "xl" | "2xl"; + +/** A value that can change at the design-system breakpoints. */ +export type Responsive = T | Partial>; + +const breakpoints: Exclude[] = [ + "sm", + "md", + "lg", + "xl", + "2xl", +]; + +export function responsiveClasses( + value: Responsive | undefined, + classes: Record, + defaultValue?: T, +): string[] { + if (value == null) return []; + if (typeof value !== "object" || Array.isArray(value)) { + const className = classes[String(value)]; + return className ? [className] : []; + } + + const result: string[] = []; + const values = value as Partial>; + const baseValue = values.base ?? defaultValue; + const baseClass = baseValue == null ? undefined : classes[String(baseValue)]; + if (baseClass) { + result.push(baseClass); + } + for (const breakpoint of breakpoints) { + const item = values[breakpoint]; + const className = item == null ? undefined : classes[String(item)]; + if (className) { + result.push(`${breakpoint}:${className}`); + } + } + return result; +} + +export function responsiveBooleanClasses( + value: Responsive | undefined, + className: string, + falseClass?: string, + defaultValue = false, +): string[] { + if (value == null) return []; + if (typeof value === "boolean") return value ? [className] : []; + const result: string[] = []; + const baseValue = value.base ?? defaultValue; + if (baseValue === true) result.push(className); + if (baseValue === false && falseClass) result.push(falseClass); + for (const breakpoint of breakpoints) { + if (value[breakpoint] === true) result.push(`${breakpoint}:${className}`); + if (value[breakpoint] === false && falseClass) { + result.push(`${breakpoint}:${falseClass}`); + } + } + return result; +} diff --git a/packages/react/src/shared/slot.test.tsx b/packages/react/src/shared/slot.test.tsx new file mode 100644 index 0000000..7e75918 --- /dev/null +++ b/packages/react/src/shared/slot.test.tsx @@ -0,0 +1,79 @@ +// @vitest-environment jsdom + +import { createRef } from "react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Slot, mergeSlotProps } from "./slot"; + +afterEach(cleanup); + +describe("Slot", () => { + it("renders the child element with merged className", () => { + render( + +
    + Docs + + , + ); + + const anchor = screen.getByTestId("target"); + expect(anchor.tagName).toBe("A"); + expect(anchor.className).toBe("from-slot from-child"); + }); + + it("composes event handlers child-first", () => { + const order: string[] = []; + render( + order.push("slot")}> + + , + ); + + fireEvent.click(screen.getByRole("button")); + expect(order).toEqual(["child", "slot"]); + }); + + it("lets the slot handler observe defaultPrevented from the child", () => { + const slotHandler = vi.fn((event: { defaultPrevented: boolean }) => { + expect(event.defaultPrevented).toBe(true); + }); + render( + + + , + ); + + fireEvent.click(screen.getByRole("button")); + expect(slotHandler).toHaveBeenCalledTimes(1); + }); + + it("composes the forwarded ref with the child's ref", () => { + const slotRef = createRef(); + const childRef = createRef(); + render( + + + , + ); + + expect(slotRef.current).toBeTruthy(); + expect(slotRef.current).toBe(childRef.current); + }); + + it("lets child props win for plain values and merges style", () => { + const merged = mergeSlotProps( + { id: "slot-id", style: { color: "red", margin: 1 } }, + { id: "child-id", style: { color: "blue" } }, + ); + + expect(merged.id).toBe("child-id"); + expect(merged.style).toEqual({ color: "blue", margin: 1 }); + }); +}); diff --git a/packages/react/src/shared/slot.tsx b/packages/react/src/shared/slot.tsx new file mode 100644 index 0000000..e531a72 --- /dev/null +++ b/packages/react/src/shared/slot.tsx @@ -0,0 +1,133 @@ +import type { + CSSProperties, + ForwardedRef, + HTMLAttributes, + ReactElement, + Ref, +} from "react"; +import { Children, cloneElement, forwardRef, isValidElement } from "react"; +import { clsx } from "clsx"; + +type AnyProps = Record; + +export interface SlotProps extends HTMLAttributes { + children: ReactElement; +} + +function setRef(ref: Ref | undefined, node: T | null) { + if (typeof ref === "function") { + ref(node); + } else if (ref) { + (ref as { current: T | null }).current = node; + } +} + +function composeRefs(...refs: Array | undefined>) { + return (node: T | null) => { + refs.forEach((ref) => setRef(ref, node)); + }; +} + +function isEventHandlerName(name: string): boolean { + return /^on[A-Z]/.test(name); +} + +const ARIA_ID_LIST_PROPS = new Set([ + "aria-describedby", + "aria-labelledby", + "aria-controls", +]); + +function joinIdLists(a: unknown, b: unknown): string | undefined { + const tokens = [ + ...String(a ?? "").split(/\s+/), + ...String(b ?? "").split(/\s+/), + ].filter(Boolean); + const unique = Array.from(new Set(tokens)); + return unique.length > 0 ? unique.join(" ") : undefined; +} + +/** + * Merge slot-provided props into the child's own props. + * + * - Event handlers compose: the child's handler runs first; the slot's + * handler runs after and can check `event.defaultPrevented`. + * - `className` values are concatenated, `style` objects shallow-merged + * with the child taking precedence. + * - `aria-describedby`/`aria-labelledby`/`aria-controls` id lists are + * merged as unique tokens from both sides. + * - All other props: the child's own value wins when both are set. + */ +export function mergeSlotProps( + slotProps: AnyProps, + childProps: AnyProps, +): AnyProps { + const merged: AnyProps = { ...slotProps, ...childProps }; + + for (const name of Object.keys(slotProps)) { + const slotValue = slotProps[name]; + const childValue = childProps[name]; + + if (isEventHandlerName(name)) { + if (typeof slotValue === "function" && typeof childValue === "function") { + merged[name] = (...args: unknown[]) => { + (childValue as (...a: unknown[]) => void)(...args); + (slotValue as (...a: unknown[]) => void)(...args); + }; + } else { + merged[name] = childValue ?? slotValue; + } + continue; + } + + if (name === "className") { + merged[name] = + clsx(slotValue as string, childValue as string) || undefined; + continue; + } + + if (name === "style") { + merged[name] = { + ...(slotValue as CSSProperties), + ...(childValue as CSSProperties), + }; + continue; + } + + if (ARIA_ID_LIST_PROPS.has(name)) { + merged[name] = joinIdLists(slotValue, childValue); + continue; + } + } + + return merged; +} + +function SlotImpl( + { children, ...slotProps }: SlotProps, + ref: ForwardedRef, +) { + const child = Children.only(children); + if (!isValidElement(child)) { + throw new Error("Slot expects a single React element child."); + } + + const childRef = (child as { ref?: Ref }).ref; + return cloneElement(child, { + ...mergeSlotProps(slotProps as AnyProps, child.props as AnyProps), + ref: composeRefs(ref, childRef), + } as AnyProps); +} + +/** + * Renders its single element child with the slot's props merged in, instead + * of rendering any DOM of its own. This is the primitive behind `asChild`: + * + * ```tsx + * + * Docs + * + * ``` + */ +export const Slot = forwardRef(SlotImpl); +Slot.displayName = "Slot"; diff --git a/packages/react/src/slider/Slider.a11y.test.tsx b/packages/react/src/slider/Slider.a11y.test.tsx new file mode 100644 index 0000000..d20ac8f --- /dev/null +++ b/packages/react/src/slider/Slider.a11y.test.tsx @@ -0,0 +1,38 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { Slider } from "./Slider"; + +afterEach(cleanup); + +describe("Slider a11y", () => { + it("names the slider from its composed label", () => { + render( + + Brightness + , + ); + + expect(screen.getByRole("slider", { name: "Brightness" })).toBeTruthy(); + }); + + it("supports aria-label for label-less usage", () => { + render(); + + expect(screen.getByRole("slider", { name: "Volume" })).toBeTruthy(); + }); + + it("exposes vertical orientation", () => { + render( + , + ); + + expect( + document.querySelector(".gs-slider")!.getAttribute("data-orientation"), + ).toBe("vertical"); + expect(screen.getByRole("slider").getAttribute("aria-orientation")).toBe( + "vertical", + ); + }); +}); diff --git a/packages/react/src/slider/Slider.stories.tsx b/packages/react/src/slider/Slider.stories.tsx new file mode 100644 index 0000000..cb75953 --- /dev/null +++ b/packages/react/src/slider/Slider.stories.tsx @@ -0,0 +1,82 @@ +import type { CSSProperties } from "react"; +import { Slider } from "./Slider"; + +const meta = { + title: "React/Slider", + component: Slider, +}; + +export default meta; + +const frame: CSSProperties = { + display: "grid", + gap: "var(--space-6)", + maxWidth: 360, +}; + +export const Default = { + render: () => ( +
    + + Volume + + +
    + ), +}; + +export const Range = { + render: () => ( +
    + + Price range + + +
    + ), +}; + +export const Steps = { + render: () => ( +
    + + Zoom + + +
    + ), +}; + +export const Percent = { + render: () => ( +
    + + Opacity + + +
    + ), +}; + +export const Disabled = { + render: () => ( +
    + + Volume + + +
    + ), +}; + +export const Vertical = { + render: () => ( + + ), +}; diff --git a/packages/react/src/slider/Slider.test.tsx b/packages/react/src/slider/Slider.test.tsx new file mode 100644 index 0000000..18bd8e8 --- /dev/null +++ b/packages/react/src/slider/Slider.test.tsx @@ -0,0 +1,108 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Slider } from "./Slider"; + +afterEach(cleanup); + +describe("Slider", () => { + it("sets readable displayNames", () => { + expect(Slider.displayName).toBe("Slider"); + expect((Slider.Label as { displayName?: string }).displayName).toBe( + "Slider.Label", + ); + expect((Slider.Output as { displayName?: string }).displayName).toBe( + "Slider.Output", + ); + }); + + it("exposes range semantics on the slider input", () => { + render(); + + const slider = screen.getByRole("slider", { name: "Volume" }); + expect(slider.getAttribute("min")).toBe("10"); + expect(slider.getAttribute("max")).toBe("90"); + expect((slider as HTMLInputElement).value).toBe("30"); + }); + + it("fires onValueChange when the value changes", () => { + const onValueChange = vi.fn(); + render( + , + ); + + fireEvent.change(screen.getByRole("slider"), { target: { value: "55" } }); + expect(onValueChange).toHaveBeenCalledWith(55); + }); + + it("supports a controlled value", () => { + const { rerender } = render(); + const slider = screen.getByRole("slider") as HTMLInputElement; + expect(slider.value).toBe("20"); + + rerender(); + expect(slider.value).toBe("60"); + }); + + it("renders one thumb per value for ranges", () => { + const onValueChange = vi.fn(); + render( + , + ); + + const sliders = screen.getAllByRole("slider"); + expect(sliders).toHaveLength(2); + fireEvent.change(sliders[0]!, { target: { value: "35" } }); + expect(onValueChange).toHaveBeenCalledWith([35, 80]); + }); + + it("disables interaction through the disabled prop", () => { + render(); + + const slider = screen.getByRole("slider") as HTMLInputElement; + expect(slider.disabled).toBe(true); + expect( + document.querySelector(".gs-slider")!.getAttribute("data-disabled"), + ).toBe("true"); + }); + + it("renders label and output slots", () => { + render( + + Opacity + + , + ); + + expect(screen.getByText("Opacity")).toBeTruthy(); + const output = document.querySelector("output")!; + expect(output.textContent).toBe("25"); + expect(screen.getByRole("slider", { name: "Opacity" })).toBeTruthy(); + }); + + it("formats the output with formatOptions", () => { + render( + + Opacity + + , + ); + + expect(document.querySelector("output")!.textContent).toBe("50%"); + }); +}); diff --git a/packages/react/src/slider/Slider.tsx b/packages/react/src/slider/Slider.tsx new file mode 100644 index 0000000..fae1702 --- /dev/null +++ b/packages/react/src/slider/Slider.tsx @@ -0,0 +1,263 @@ +import type { ChangeEvent, ForwardedRef } from "react"; +import { forwardRef, useId, useMemo } from "react"; +import { clsx } from "clsx"; +import { useControllableState } from "@velune/hooks"; +import { + collectCompoundSlotProps, + createCompoundSlot, + omitCompoundSlotProps, +} from "../shared/compound-slot"; +import type { + SliderLabelProps, + SliderOutputProps, + SliderProps, + SliderValue, +} from "./Slider.types"; + +type SliderComposition = { + label?: SliderLabelProps; + output?: SliderOutputProps; +}; + +const slotSchema = { + "Slider.Label": "label", + "Slider.Output": "output", +} as const satisfies Readonly>; + +function toArray(value: SliderValue | undefined): number[] | undefined { + if (value === undefined) return undefined; + return Array.isArray(value) ? value : [value]; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +/** + * Native range inputs stacked over a custom-drawn rail. Each thumb is the + * input's own ::-webkit-slider-thumb / ::-moz-range-thumb, so keyboard, + * pointer, and form semantics come from the platform. With multiple thumbs + * the inputs ignore pointer events except on the thumb itself so overlapping + * inputs do not shadow each other. + */ +const thumbClasses = [ + // WebKit/Blink thumb + "[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-gs-slider-thumb-size [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-solid [&::-webkit-slider-thumb]:border-gs-slider-thumb-border [&::-webkit-slider-thumb]:bg-gs-slider-thumb-bg [&::-webkit-slider-thumb]:shadow-gs-xs [&::-webkit-slider-thumb]:transition-shadow [&::-webkit-slider-thumb]:duration-150 [&::-webkit-slider-thumb]:ease-gs-standard [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:active:cursor-grabbing", + "[&:focus-visible::-webkit-slider-thumb]:shadow-gs-button-focus-border", + "[&:disabled::-webkit-slider-thumb]:cursor-not-allowed", + // Firefox thumb + "[&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-gs-slider-thumb-size [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-solid [&::-moz-range-thumb]:border-gs-slider-thumb-border [&::-moz-range-thumb]:bg-gs-slider-thumb-bg [&::-moz-range-thumb]:shadow-gs-xs [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:active:cursor-grabbing", + "[&:focus-visible::-moz-range-thumb]:shadow-gs-button-focus-border", + "[&:disabled::-moz-range-thumb]:cursor-not-allowed", +].join(" "); + +function SliderImpl( + { + value, + defaultValue, + onValueChange, + onValueChangeEnd, + min = 0, + max = 100, + step = 1, + disabled = false, + orientation = "horizontal", + formatOptions, + id, + className, + children, + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy, + ...props + }: SliderProps, + ref: ForwardedRef, +) { + const { label, output } = collectCompoundSlotProps( + children, + slotSchema, + ); + const horizontal = orientation === "horizontal"; + const generatedId = useId(); + const labelId = label != null ? `${generatedId}-label` : undefined; + const multiThumb = + (toArray(value) ?? toArray(defaultValue) ?? [0]).length > 1; + + const [values, setValues] = useControllableState({ + value: toArray(value), + defaultValue: toArray(defaultValue) ?? [min], + }); + + const emit = (next: number[]) => + multiThumb || Array.isArray(value) || Array.isArray(defaultValue) + ? next + : (next[0] ?? min); + + const handleChange = ( + index: number, + event: ChangeEvent, + ) => { + const raw = Number(event.target.value); + // Thumbs may not cross: clamp against the neighbouring values. + const lower = index > 0 ? (values[index - 1] ?? min) : min; + const upper = index < values.length - 1 ? (values[index + 1] ?? max) : max; + const nextValue = clamp(raw, lower, upper); + if (nextValue === values[index]) { + if (raw !== nextValue) event.target.value = String(nextValue); + return; + } + const next = values.slice(); + next[index] = nextValue; + setValues(next); + onValueChange?.(emit(next)); + }; + + const handleChangeEnd = () => { + onValueChangeEnd?.(emit(values)); + }; + + const formatter = useMemo( + () => + new Intl.NumberFormat( + typeof navigator !== "undefined" ? navigator.language : "en", + formatOptions, + ), + [formatOptions], + ); + + const percents = values.map((current) => + max === min ? 0 : ((clamp(current, min, max) - min) / (max - min)) * 100, + ); + const startPercent = values.length > 1 ? (percents[0] ?? 0) : 0; + const endPercent = percents[percents.length - 1] ?? 0; + const formattedValue = values.map((v) => formatter.format(v)).join(" – "); + + return ( +
    + {label != null || output != null ? ( +
    + {label != null ? ( + + {label.children} + + ) : ( +
    + ) : null} +
    +
    +
    + ); +} + +const SliderRoot = forwardRef(SliderImpl); +SliderRoot.displayName = "Slider"; + +const SliderLabel = createCompoundSlot("Slider.Label"); +const SliderOutput = createCompoundSlot("Slider.Output"); + +export const Slider = Object.assign(SliderRoot, { + Label: SliderLabel, + Output: SliderOutput, +}); diff --git a/packages/react/src/slider/Slider.types.ts b/packages/react/src/slider/Slider.types.ts new file mode 100644 index 0000000..d443b29 --- /dev/null +++ b/packages/react/src/slider/Slider.types.ts @@ -0,0 +1,43 @@ +import type { CSSProperties, HTMLAttributes, ReactNode } from "react"; + +export type SliderOrientation = "horizontal" | "vertical"; + +/** A single value or one value per thumb for range selection. */ +export type SliderValue = number | number[]; + +export interface SliderProps { + /** Controlled value. Pass an array for a multi-thumb range. */ + value?: SliderValue; + defaultValue?: SliderValue; + /** Fires on every value change while dragging or pressing keys. */ + onValueChange?: (value: SliderValue) => void; + /** Fires once interaction settles (pointer up / key release). */ + onValueChangeEnd?: (value: SliderValue) => void; + /** Default: `0`. */ + min?: number; + /** Default: `100`. */ + max?: number; + /** Default: `1`. */ + step?: number; + disabled?: boolean; + /** Default: `"horizontal"`. */ + orientation?: SliderOrientation; + /** Formats the output and thumb value labels. */ + formatOptions?: Intl.NumberFormatOptions; + id?: string; + className?: string; + style?: CSSProperties; + "aria-label"?: string; + "aria-labelledby"?: string; + /** `Slider.Label` and `Slider.Output` slots. */ + children?: ReactNode; +} + +export interface SliderLabelProps extends HTMLAttributes { + children?: ReactNode; +} + +export interface SliderOutputProps extends HTMLAttributes { + /** Custom output content. Defaults to the formatted value(s). */ + children?: ReactNode; +} diff --git a/packages/react/src/slider/index.ts b/packages/react/src/slider/index.ts new file mode 100644 index 0000000..7f714fd --- /dev/null +++ b/packages/react/src/slider/index.ts @@ -0,0 +1,8 @@ +export { Slider } from "./Slider"; +export type { + SliderLabelProps, + SliderOrientation, + SliderOutputProps, + SliderProps, + SliderValue, +} from "./Slider.types"; diff --git a/packages/react/src/spinner/Spinner.test.tsx b/packages/react/src/spinner/Spinner.test.tsx index 675a5d4..e0b173a 100644 --- a/packages/react/src/spinner/Spinner.test.tsx +++ b/packages/react/src/spinner/Spinner.test.tsx @@ -18,4 +18,14 @@ describe("Spinner", () => { expect(markup).toContain("motion-reduce:animate-none"); expect(markup).toContain("custom-spinner"); }); + + it("resolves the accessible name from label with an aria-label override", () => { + expect(renderToStaticMarkup()).toContain('aria-label="Loading"'); + expect(renderToStaticMarkup()).toContain( + 'aria-label="Saving draft"', + ); + expect( + renderToStaticMarkup(), + ).toContain('aria-label="Uploading"'); + }); }); diff --git a/packages/react/src/spinner/Spinner.tsx b/packages/react/src/spinner/Spinner.tsx index 2cb2f9f..27f2364 100644 --- a/packages/react/src/spinner/Spinner.tsx +++ b/packages/react/src/spinner/Spinner.tsx @@ -23,7 +23,8 @@ function SpinnerImpl( { size = "md", tone = "primary", - "aria-label": ariaLabel = "Loading", + label, + "aria-label": ariaLabel, className, ...props }: SpinnerProps, @@ -34,7 +35,7 @@ function SpinnerImpl( ref={ref} role="status" aria-live="polite" - aria-label={ariaLabel} + aria-label={ariaLabel ?? label ?? "Loading"} className={clsx( "gs-spinner inline-flex shrink-0 items-center justify-center align-middle leading-none", sizeClasses[size], diff --git a/packages/react/src/spinner/Spinner.types.ts b/packages/react/src/spinner/Spinner.types.ts index 432666d..ecc9e1f 100644 --- a/packages/react/src/spinner/Spinner.types.ts +++ b/packages/react/src/spinner/Spinner.types.ts @@ -15,4 +15,6 @@ export interface SpinnerProps extends HTMLAttributes { size?: SpinnerSize; /** Color tone. Default: `primary`. Use `current` to inherit text color. */ tone?: SpinnerTone; + /** Accessible name announced by screen readers. Default: `Loading`. */ + label?: string; } diff --git a/packages/react/src/stack/Stack.stories.tsx b/packages/react/src/stack/Stack.stories.tsx index 5a3db8d..bd24a08 100644 --- a/packages/react/src/stack/Stack.stories.tsx +++ b/packages/react/src/stack/Stack.stories.tsx @@ -18,7 +18,7 @@ export const Default = {
    Profile - Vertical rhythm with token spacing. + Vertical rhythm with token spacing.
    @@ -37,3 +37,14 @@ export const Align = {
    ), }; + +export const Responsive = { + render: () => ( +
    + + Responsive rhythm + Spacing and order change at larger viewports. + +
    + ), +}; diff --git a/packages/react/src/stack/Stack.test.tsx b/packages/react/src/stack/Stack.test.tsx index aa84315..2e57813 100644 --- a/packages/react/src/stack/Stack.test.tsx +++ b/packages/react/src/stack/Stack.test.tsx @@ -34,4 +34,50 @@ describe("Stack", () => { ]), ); }); + + it("renders a divider between each pair of children", () => { + render( + }> + First + Second + Third + , + ); + + const stack = screen.getByTestId("stack"); + expect(screen.getAllByTestId("divider")).toHaveLength(2); + expect( + Array.from(stack.children).map((child) => child.tagName.toLowerCase()), + ).toEqual(["span", "hr", "span", "hr", "span"]); + }); + + it("skips dividers around null children and single children", () => { + render( + }> + {null} + Only + {false} + , + ); + + expect(screen.queryByTestId("divider")).toBeNull(); + }); + + it("supports responsive gaps and direction", () => { + render( + , + ); + expect(Array.from(screen.getByTestId("stack").classList)).toEqual( + expect.arrayContaining([ + "gap-2", + "md:gap-5", + "flex-col", + "lg:flex-col-reverse", + ]), + ); + }); }); diff --git a/packages/react/src/stack/Stack.tsx b/packages/react/src/stack/Stack.tsx index 0e988f8..539821c 100644 --- a/packages/react/src/stack/Stack.tsx +++ b/packages/react/src/stack/Stack.tsx @@ -1,5 +1,12 @@ -import type { ElementType, ForwardedRef } from "react"; -import { createElement, forwardRef } from "react"; +import type { ElementType, ForwardedRef, ReactElement, ReactNode } from "react"; +import { + Children, + cloneElement, + createElement, + forwardRef, + Fragment, + isValidElement, +} from "react"; import { clsx } from "clsx"; import type { StackProps } from "./Stack.types"; import type { PolymorphicComponent } from "../shared/polymorphic"; @@ -8,23 +15,46 @@ import { gapClasses, justifyContentClasses, } from "../shared/tailwind-classes"; +import { + responsiveBooleanClasses, + responsiveClasses, +} from "../shared/responsive"; + +function interleaveDivider( + children: ReactNode, + divider: ReactElement, +): ReactNode { + // Children.toArray already removes null, undefined, and boolean children. + const items = Children.toArray(children); + return items.map((child, index) => ( + + {index > 0 ? cloneElement(divider) : null} + {child} + + )); +} function StackImpl( { as = "div", - spacing, gap, align, justify, reverse = false, fullWidth = false, + divider, className, children, ...props }: StackProps, ref: ForwardedRef, ) { - const resolvedSpacing = gap ?? spacing ?? "4"; + const content = + divider && isValidElement(divider) + ? interleaveDivider(children, divider) + : children; return createElement( as, @@ -32,21 +62,30 @@ function StackImpl( ref, className: clsx( "gs-stack flex min-w-0", - reverse ? "flex-col-reverse" : "flex-col", - fullWidth && "w-full", - gapClasses[resolvedSpacing], - align && alignItemsClasses[align], - justify && justifyContentClasses[justify], + responsiveClasses( + reverse, + { + true: "flex-col-reverse", + false: "flex-col", + }, + false, + ), + responsiveBooleanClasses(fullWidth, "w-full", "w-auto"), + responsiveClasses(gap, gapClasses, "4"), + responsiveClasses(align, alignItemsClasses), + responsiveClasses(justify, justifyContentClasses), className, ), - "data-spacing": resolvedSpacing, - "data-align": align, - "data-justify": justify, - "data-reverse": reverse ? "true" : undefined, - "data-full-width": fullWidth ? "true" : undefined, + "data-gap": typeof gap === "string" ? gap : "4", + "data-align": typeof align === "string" ? align : undefined, + "data-justify": typeof justify === "string" ? justify : undefined, + "data-reverse": + typeof reverse === "boolean" && reverse ? "true" : undefined, + "data-full-width": + typeof fullWidth === "boolean" && fullWidth ? "true" : undefined, ...props, }, - children, + content, ); } diff --git a/packages/react/src/stack/Stack.types.ts b/packages/react/src/stack/Stack.types.ts index 0207daa..59565ac 100644 --- a/packages/react/src/stack/Stack.types.ts +++ b/packages/react/src/stack/Stack.types.ts @@ -1,19 +1,20 @@ -import type { ElementType } from "react"; +import type { ElementType, ReactElement } from "react"; import type { BoxSpacing } from "../box"; import type { FlexAlign, FlexJustify } from "../flex"; import type { PolymorphicProps } from "../shared/polymorphic"; +import type { Responsive } from "../shared/responsive"; export interface StackOwnProps { /** Vertical gap between children. Defaults to `4`. */ - spacing?: BoxSpacing; - /** Alias of `spacing`. */ - gap?: BoxSpacing; - align?: FlexAlign; - justify?: FlexJustify; + gap?: Responsive; + align?: Responsive; + justify?: Responsive; /** Reverse column order. */ - reverse?: boolean; + reverse?: Responsive; /** Stretch to parent width. */ - fullWidth?: boolean; + fullWidth?: Responsive; + /** Element cloned between each pair of children, e.g. ``. */ + divider?: ReactElement; } export type StackProps = PolymorphicProps< diff --git a/packages/react/src/switch/Switch.stories.tsx b/packages/react/src/switch/Switch.stories.tsx index 9f84d6b..59792fd 100644 --- a/packages/react/src/switch/Switch.stories.tsx +++ b/packages/react/src/switch/Switch.stories.tsx @@ -36,6 +36,9 @@ export const Sizes = { Medium + + Large +
    ), }; diff --git a/packages/react/src/switch/Switch.test.tsx b/packages/react/src/switch/Switch.test.tsx index c8a33ca..78dbf8c 100644 --- a/packages/react/src/switch/Switch.test.tsx +++ b/packages/react/src/switch/Switch.test.tsx @@ -249,4 +249,20 @@ describe("Switch", () => { expect(spinner.textContent).toBe(""); expect(description.textContent).toBe("Saving"); }); + + it("maps the large size to its track tokens", () => { + render(Notifications); + const control = screen.getByRole("switch"); + const track = control.querySelector(".gs-switch-track")!; + + expect(control.getAttribute("data-size")).toBe("lg"); + expect( + track.classList.contains( + "[--gs-switch-track-w:var(--switch-track-width-lg)]", + ), + ).toBe(true); + expect(control.classList.contains("text-gs-switch-font-size-lg")).toBe( + true, + ); + }); }); diff --git a/packages/react/src/switch/Switch.tsx b/packages/react/src/switch/Switch.tsx index 0cba378..5895b27 100644 --- a/packages/react/src/switch/Switch.tsx +++ b/packages/react/src/switch/Switch.tsx @@ -145,6 +145,7 @@ function SwitchImpl( className={clsx( "gs-switch group m-0 inline-flex min-h-gs-control-hit-target min-w-gs-control-hit-target max-w-full touch-manipulation select-none items-start gap-gs-switch-gap border-0 bg-transparent p-0 text-start font-inherit text-gs-switch-font-size font-normal leading-gs-normal text-gs-text outline-none [-webkit-tap-highlight-color:transparent]", size === "sm" && "text-gs-switch-font-size-sm", + size === "lg" && "text-gs-switch-font-size-lg", isDisabled ? "cursor-not-allowed opacity-gs-disabled" : "cursor-pointer", @@ -162,6 +163,8 @@ function SwitchImpl( "gs-switch-track inline-flex shrink-0 self-start items-center rounded-full bg-gs-switch-bg p-gs-switch-pad [--gs-switch-track-w:var(--switch-track-width)] [--gs-switch-track-h:var(--switch-track-height)] [--gs-switch-thumb:var(--switch-thumb-size)] [--gs-switch-pad:var(--switch-padding)] [--gs-switch-travel:calc(var(--gs-switch-track-w)-var(--gs-switch-thumb)-(var(--gs-switch-pad)*2))] h-gs-switch-track-h w-gs-switch-track-w mt-[calc((1lh-var(--gs-switch-track-h))/2)] transition-colors duration-200 ease-gs-standard motion-reduce:transition-none [[data-reduced-motion=true]_&]:transition-none", size === "sm" && "[--gs-switch-track-w:var(--switch-track-width-sm)] [--gs-switch-track-h:var(--switch-track-height-sm)] [--gs-switch-thumb:var(--switch-thumb-size-sm)] [--gs-switch-pad:var(--switch-padding-sm)]", + size === "lg" && + "[--gs-switch-track-w:var(--switch-track-width-lg)] [--gs-switch-track-h:var(--switch-track-height-lg)] [--gs-switch-thumb:var(--switch-thumb-size-lg)] [--gs-switch-pad:var(--switch-padding-lg)]", currentChecked ? isDisabled ? "bg-gs-switch-bg-checked" diff --git a/packages/react/src/switch/Switch.types.ts b/packages/react/src/switch/Switch.types.ts index 74d84a9..29b2cf6 100644 --- a/packages/react/src/switch/Switch.types.ts +++ b/packages/react/src/switch/Switch.types.ts @@ -1,6 +1,6 @@ import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react"; -export type SwitchSize = "sm" | "md"; +export type SwitchSize = "sm" | "md" | "lg"; export interface SwitchProps extends Omit< ButtonHTMLAttributes, diff --git a/packages/react/src/table/Table.test.tsx b/packages/react/src/table/Table.test.tsx index 7eb85cb..6e56d8c 100644 --- a/packages/react/src/table/Table.test.tsx +++ b/packages/react/src/table/Table.test.tsx @@ -188,6 +188,11 @@ describe("Table", () => { fireEvent.click(getByRole("button", { name: "Name" })); expect(statusReads).toBe(3); + expect( + getByRole("button", { name: "Name" }).classList.contains( + "active:scale-[.98]", + ), + ).toBe(true); }); it("makes a constrained scroll area keyboard accessible", () => { diff --git a/packages/react/src/table/TableView.tsx b/packages/react/src/table/TableView.tsx index 81fde95..4e2d1fa 100644 --- a/packages/react/src/table/TableView.tsx +++ b/packages/react/src/table/TableView.tsx @@ -197,7 +197,7 @@ export function TableView({ {column.sortable ? ( @@ -46,14 +46,10 @@ describe("Tooltip", () => { ); const panel = document.querySelector(".gs-tooltip")!; - expect(panel.classList.contains("opacity-0")).toBe(true); - expect(panel.classList.contains("data-[ready=true]:opacity-100")).toBe( - true, - ); - expect(panel.classList.contains("transition-opacity")).toBe(true); - expect(panel.classList.contains("motion-reduce:transition-none")).toBe( - true, - ); + expect( + panel.classList.contains("data-[ready=true]:animate-gs-float-in"), + ).toBe(true); + expect(panel.classList.contains("motion-reduce:animate-none")).toBe(true); }); it("composes panel hover handlers and respects preventDefault", () => { diff --git a/packages/react/src/tooltip/Tooltip.tsx b/packages/react/src/tooltip/Tooltip.tsx index 5400b84..4ac00b0 100644 --- a/packages/react/src/tooltip/Tooltip.tsx +++ b/packages/react/src/tooltip/Tooltip.tsx @@ -1,15 +1,12 @@ import type { FocusEvent, ForwardedRef, - KeyboardEvent, MouseEvent, PointerEvent as ReactPointerEvent, ReactElement, ReactNode, - Ref, } from "react"; import { - cloneElement, forwardRef, isValidElement, useCallback, @@ -30,6 +27,7 @@ import { pushEscapeLayer, } from "../shared/overlay-stack"; import { Portal } from "../shared/portal"; +import { Slot } from "../shared/slot"; import type { Placement } from "../shared/position"; import { useLatestRef } from "../shared/use-latest-ref"; import { useFloatingPosition } from "../shared/use-floating-position"; @@ -278,25 +276,6 @@ function TooltipImpl( return null; } - const childProps = child.props as { - onMouseEnter?: (event: MouseEvent) => void; - onMouseLeave?: (event: MouseEvent) => void; - onFocus?: (event: FocusEvent) => void; - onBlur?: (event: FocusEvent) => void; - onClick?: (event: MouseEvent) => void; - onPointerDown?: (event: ReactPointerEvent) => void; - onKeyDown?: (event: KeyboardEvent) => void; - "aria-describedby"?: string; - ref?: Ref; - }; - - const composedDescribedBy = [ - childProps["aria-describedby"], - open ? tooltipId : null, - ] - .filter(Boolean) - .join(" "); - // Wrap trigger so we always own a real DOM node for measurement, // even when the child does not forward refs. const triggerNode = ( @@ -305,23 +284,19 @@ function TooltipImpl( className="gs-tooltip-trigger inline-flex max-w-full align-middle" data-open={open ? "true" : undefined} > - {cloneElement(child as ReactElement, { - ref: childProps.ref, - "aria-describedby": composedDescribedBy || undefined, - onMouseEnter: (event: MouseEvent) => { - childProps.onMouseEnter?.(event); + ) => { if (!event.defaultPrevented && triggers.has("hover")) { scheduleOpen(); } - }, - onMouseLeave: (event: MouseEvent) => { - childProps.onMouseLeave?.(event); + }} + onMouseLeave={(event: MouseEvent) => { if (!event.defaultPrevented && triggers.has("hover")) { scheduleClose(); } - }, - onFocus: (event: FocusEvent) => { - childProps.onFocus?.(event); + }} + onFocus={(event: FocusEvent) => { if ( !event.defaultPrevented && !isPointerDownRef.current && @@ -330,16 +305,14 @@ function TooltipImpl( clearTimers(); setOpenRef.current(true); } - }, - onBlur: (event: FocusEvent) => { - childProps.onBlur?.(event); + }} + onBlur={(event: FocusEvent) => { if (!event.defaultPrevented && triggers.has("focus")) { clearTimers(); setOpenRef.current(false); } - }, - onPointerDown: (event: ReactPointerEvent) => { - childProps.onPointerDown?.(event); + }} + onPointerDown={(event: ReactPointerEvent) => { if (event.defaultPrevented) { return; } @@ -351,9 +324,8 @@ function TooltipImpl( clearTimers(); setOpenRef.current(false); } - }, - onClick: (event: MouseEvent) => { - childProps.onClick?.(event); + }} + onClick={(event: MouseEvent) => { if (event.defaultPrevented) { return; } @@ -363,8 +335,10 @@ function TooltipImpl( } else { setOpenRef.current(false); } - }, - })} + }} + > + {child as ReactElement} + ); const floating = open ? ( @@ -376,7 +350,7 @@ function TooltipImpl( role="tooltip" data-gs-overlay-branch="" className={clsx( - "gs-tooltip pointer-events-auto z-gs-tooltip box-border max-w-gs-tooltip-max-width wrap-anywhere rounded-gs-tooltip-radius border border-gs-surface-border bg-gs-tooltip-bg bg-gs-surface-highlight px-gs-tooltip-padding-x py-gs-tooltip-padding-y font-inherit text-gs-tooltip-font-size font-medium leading-gs-normal tracking-normal text-gs-tooltip-color opacity-0 shadow-gs-tooltip-shadow transition-opacity duration-200 ease-gs-decelerate data-[ready=true]:opacity-100 motion-reduce:transition-none", + "gs-tooltip pointer-events-auto z-gs-tooltip box-border max-w-gs-tooltip-max-width wrap-anywhere rounded-gs-tooltip-radius border border-gs-surface-border bg-gs-tooltip-bg bg-gs-surface-highlight px-gs-tooltip-padding-x py-gs-tooltip-padding-y font-inherit text-gs-tooltip-font-size font-medium leading-gs-normal tracking-normal text-gs-tooltip-color shadow-gs-tooltip-shadow data-[ready=true]:animate-gs-float-in data-[placement^=top]:[--gs-float-from:0_var(--space-1)] data-[placement^=left]:[--gs-float-from:var(--space-1)_0] data-[placement^=right]:[--gs-float-from:calc(var(--space-1)*-1)_0] motion-reduce:animate-none [[data-reduced-motion=true]_&]:animate-none", className, contentClassName, )} diff --git a/packages/react/src/wizard/Wizard.test.tsx b/packages/react/src/wizard/Wizard.test.tsx index 9d4fc16..2b158ed 100644 --- a/packages/react/src/wizard/Wizard.test.tsx +++ b/packages/react/src/wizard/Wizard.test.tsx @@ -183,6 +183,16 @@ describe("Wizard", () => { .querySelector(".gs-wizard-marker") ?.classList.contains("bg-gs-wizard-marker-bg-current"), ).toBe(true); + expect( + document + .querySelector(".gs-wizard-marker") + ?.classList.contains("group-active:scale-95"), + ).toBe(true); + expect( + document + .querySelector(".gs-wizard-marker") + ?.classList.contains("motion-reduce:transition-none"), + ).toBe(true); rerender( diff --git a/packages/react/src/wizard/Wizard.tsx b/packages/react/src/wizard/Wizard.tsx index a280b73..e938d6d 100644 --- a/packages/react/src/wizard/Wizard.tsx +++ b/packages/react/src/wizard/Wizard.tsx @@ -448,7 +448,7 @@ function WizardImpl( > ({ diff --git a/packages/velune/package.json b/packages/velune/package.json index 06b1ea2..4156b6a 100644 --- a/packages/velune/package.json +++ b/packages/velune/package.json @@ -119,10 +119,8 @@ } }, "dependencies": { - "@internationalized/date": "^3.12.2", "@tanstack/react-virtual": "^3.14.5", - "clsx": "^2.1.1", - "react-aria-components": "^1.19.0" + "clsx": "^2.1.1" }, "devDependencies": { "@velune/react-internal": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a795c1..4ab98a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,14 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@babel/core': 7.29.7 + brace-expansion@5.0.5: 5.0.7 + esbuild: 0.28.1 + fast-uri@3.1.2: 3.1.4 + js-yaml@3.14.2: 3.15.0 + js-yaml@4.1.1: 4.3.0 + importers: .: @@ -116,9 +124,6 @@ importers: '@tanstack/react-router': specifier: ^1.170.17 version: 1.170.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - velune: - specifier: workspace:* - version: link:../../packages/velune lucide-react: specifier: ^1.24.0 version: 1.24.0(react@18.3.1) @@ -131,6 +136,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + velune: + specifier: workspace:* + version: link:../../packages/velune devDependencies: '@tailwindcss/vite': specifier: ^4.3.2 @@ -159,22 +167,22 @@ importers: apps/storybook: dependencies: - velune: - specifier: workspace:* - version: link:../../packages/velune react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + velune: + specifier: workspace:* + version: link:../../packages/velune devDependencies: '@storybook/react': - specifier: ^8.6.14 - version: 8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.18(prettier@3.8.3))(typescript@5.8.3) + specifier: ^10.5.3 + version: 10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(typescript@5.8.3) '@storybook/react-vite': - specifier: ^8.6.14 - version: 8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.3)(storybook@8.6.18(prettier@3.8.3))(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + specifier: ^10.5.3 + version: 10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.28.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) '@tailwindcss/vite': specifier: ^4.3.2 version: 4.3.2(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -182,8 +190,8 @@ importers: specifier: ^4.3.4 version: 4.7.0(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) storybook: - specifier: ^8.6.14 - version: 8.6.18(prettier@3.8.3) + specifier: ^10.5.3 + version: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) tailwindcss: specifier: ^4.3.2 version: 4.3.2 @@ -194,34 +202,6 @@ importers: specifier: ^6.4.3 version: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) - packages/velune: - dependencies: - '@internationalized/date': - specifier: ^3.12.2 - version: 3.12.2 - '@tanstack/react-virtual': - specifier: ^3.14.5 - version: 3.14.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - clsx: - specifier: ^2.1.1 - version: 2.1.1 - react: - specifier: '>=18' - version: 18.3.1 - react-aria-components: - specifier: ^1.19.0 - version: 1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: - specifier: '>=18' - version: 18.3.1(react@18.3.1) - tailwindcss: - specifier: '>=4' - version: 4.3.2 - devDependencies: - '@velune/react-internal': - specifier: workspace:* - version: link:../react - packages/hooks: dependencies: react: @@ -243,24 +223,18 @@ importers: packages/react: dependencies: - '@velune/hooks': - specifier: workspace:* - version: link:../hooks - '@internationalized/date': - specifier: ^3.12.2 - version: 3.12.2 '@tanstack/react-virtual': specifier: ^3.14.5 version: 3.14.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@velune/hooks': + specifier: workspace:* + version: link:../hooks clsx: specifier: ^2.1.1 version: 2.1.1 react: specifier: '>=18' version: 18.3.1 - react-aria-components: - specifier: ^1.19.0 - version: 1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: '>=18' version: 18.3.1(react@18.3.1) @@ -277,11 +251,39 @@ importers: lucide-react: specifier: ^1.24.0 version: 1.24.0(react@18.3.1) + zod: + specifier: ^4.4.3 + version: 4.4.3 packages/utils: {} + packages/velune: + dependencies: + '@tanstack/react-virtual': + specifier: ^3.14.5 + version: 3.14.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + react: + specifier: '>=18' + version: 18.3.1 + react-dom: + specifier: '>=18' + version: 18.3.1(react@18.3.1) + tailwindcss: + specifier: '>=4' + version: 4.3.2 + devDependencies: + '@velune/react-internal': + specifier: workspace:* + version: link:../react + packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@andrewbranch/untar.js@1.0.3': resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} @@ -318,35 +320,39 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils@7.28.6': resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} @@ -356,16 +362,24 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} '@babel/parser@7.29.3': @@ -373,34 +387,43 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-source@7.27.1': resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': 7.29.7 '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@braidai/lang@1.1.2': resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} @@ -604,17 +627,23 @@ packages: '@dual-bundle/import-meta-resolve@4.2.1': resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -622,450 +651,150 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -1148,24 +877,11 @@ packages: '@types/node': optional: true - '@internationalized/date@3.12.2': - resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} - - '@internationalized/number@3.6.7': - resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} - - '@internationalized/string@3.2.9': - resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0': - resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true @@ -1204,6 +920,12 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1216,9 +938,222 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} @@ -1229,11 +1164,6 @@ packages: resolution: {integrity: sha512-edgyN2pP07uXiP4tJs0s8KVmU8M8i60YPbbI0/WDeok1mIJHRXz+CgD8I0nelwDkoCh3EWL/G5kGfbuHjsdbvw==} engines: {node: '>=18'} - '@react-types/shared@3.36.0': - resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1387,86 +1317,81 @@ packages: peerDependencies: size-limit: 12.1.0 - '@storybook/builder-vite@8.6.18': - resolution: {integrity: sha512-XLqnOv4C36jlTd4uC8xpWBxv+7GV4/05zWJ0wAcU4qflorropUTirt4UQPGkwIzi+BVAhs9pJj+m4k0IWJtpHg==} - peerDependencies: - storybook: ^8.6.18 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - - '@storybook/components@8.6.18': - resolution: {integrity: sha512-55yViiZzPS/cPBuOeW4QGxGqrusjXVyxuknmbYCIwDtFyyvI/CgbjXRHdxNBaIjz+IlftxvBmmSaOqFG5+/dkA==} + '@storybook/builder-vite@10.5.3': + resolution: {integrity: sha512-qX1jb1nbG1mWJrCn3YrDkpbii+KA1uxdVgENeNusD80RrWCwVG8ce+awjZxKuT8qjYyALSAPBvTHUqZ4C1b7Pg==} peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + storybook: ^10.5.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/core@8.6.18': - resolution: {integrity: sha512-dRBP2TnX6fGdS0T2mXBHjkS/3Nlu1ra1huovZVFuM67CYMzrhM/3hX/zru1vWSC5rqY93ZaAhjMciPW4pK5mMQ==} + '@storybook/csf-plugin@10.5.3': + resolution: {integrity: sha512-mkPq6zru8fN5+46uC1cZEbKW2ws1hh9KvF4g4/Gu8pNbKnvqULPhk0/Bf0ZCtlr7zI7DvcFhyCy3dbvN+2n4Gw==} peerDependencies: - prettier: ^2 || ^3 + esbuild: 0.28.1 + rollup: '*' + storybook: ^10.5.3 + vite: '*' + webpack: '*' peerDependenciesMeta: - prettier: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: optional: true - - '@storybook/csf-plugin@8.6.18': - resolution: {integrity: sha512-x1ioz/L0CwaelCkHci3P31YtvwayN3FBftvwQOPbvRh9qeb4Cpz5IdVDmyvSxxYwXN66uAORNoqgjTi7B4/y5Q==} - peerDependencies: - storybook: ^8.6.18 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - '@storybook/manager-api@8.6.18': - resolution: {integrity: sha512-BjIp12gEMgzFkEsgKpDIbZdnSWTZpm2dlws8WiPJCpgJtG+HWSxZ0/Ms30Au9yfwzQEKRSbV/5zpsKMGc2SIJw==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - - '@storybook/preview-api@8.6.18': - resolution: {integrity: sha512-joXRXh3GdVvzhbfIgmix1xs90p8Q/nja7AhEAC2egn5Pl7SKsIYZUCYI6UdrQANb2myg9P552LKXfPect8llKg==} + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@8.6.18': - resolution: {integrity: sha512-N4xULcAWZQTUv4jy1/d346Tyb4gufuC3UaLCuU/iVSZ1brYF4OW3ANr+096btbMxY8pR/65lmtoqr5CTGwnBvA==} + '@storybook/react-dom-shim@10.5.3': + resolution: {integrity: sha512-eUWBsRRax5R3MDJVFs/CrFDF1bYS58AMB9tX02lLRuiZe6xy1cKh3CRFS+2xH571l0fNaXQ+7j69TOJ0fk2tmA==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.18 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.3 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@storybook/react-vite@8.6.18': - resolution: {integrity: sha512-qpSYyH2IizlEsI95MJTdIL6xpLSgiNCMoJpHu+IEqLnyvmecRR/YEZvcHalgdtawuXlimH0bAYuwIu3l8Vo6FQ==} - engines: {node: '>=18.0.0'} + '@storybook/react-vite@10.5.3': + resolution: {integrity: sha512-9cXaeU3+Kos4M3+Ezur2u/eBn3JIkED6ckxi7lhVQ6r2lK9NAGh5tfHSTQ/206KNSjvaHxZMAhPpxJP3/e2vfQ==} peerDependencies: - '@storybook/test': 8.6.18 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.18 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.3 + typescript: '>= 4.9.x' + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: - '@storybook/test': + typescript: optional: true - '@storybook/react@8.6.18': - resolution: {integrity: sha512-BuLpzMkKtF+UCQCbi+lYVX9cdcAMG86Lu2dDn7UFkPi5HRNFq/zHPSvlz1XDgL0OYMtcqB1aoVzFzcyzUBhhjw==} - engines: {node: '>=18.0.0'} + '@storybook/react@10.5.3': + resolution: {integrity: sha512-d/CK78xgA7DDvqnxkqcYmiTjomE4ch2TWvk0O8/xHQWW6y0nMjKtsZbmUBfZ0QcdYdWq7dErzfbG7YAzxDi7Ig==} peerDependencies: - '@storybook/test': 8.6.18 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.18 - typescript: '>= 4.2.x' + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.3 + typescript: '>= 4.9.x' peerDependenciesMeta: - '@storybook/test': + '@types/react': + optional: true + '@types/react-dom': optional: true typescript: optional: true - '@storybook/theming@8.6.18': - resolution: {integrity: sha512-n6OEjEtHupa2PdTwWzRepr7cO8NkDd4rgF6BKLitRbujOspLxzMBEqdphs+QLcuiCIgf33SqmEA64QWnbSMhPw==} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - - '@swc/helpers@0.5.23': - resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tailwindcss/node@4.3.2': resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} @@ -1594,6 +1519,10 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} @@ -1609,6 +1538,15 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1730,6 +1668,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -1744,6 +1685,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@3.2.7': resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} @@ -1753,12 +1697,21 @@ packages: '@vitest/snapshot@3.2.7': resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@3.2.7': resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1827,10 +1780,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1923,10 +1872,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - better-opn@3.0.2: - resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} - engines: {node: '>=12.0.0'} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1937,27 +1882,28 @@ packages: brace-expansion@1.1.16: resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-assert@1.2.1: - resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: - esbuild: '>=0.18' + esbuild: 0.28.1 bytes-iec@3.1.1: resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} @@ -2036,9 +1982,6 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -2136,6 +2079,9 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2186,13 +2132,21 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} @@ -2230,6 +2184,9 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -2238,9 +2195,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.351: resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==} @@ -2256,6 +2210,10 @@ packages: emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + enhanced-resolve@5.21.0: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} @@ -2318,21 +2276,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -2454,8 +2397,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -2516,10 +2459,6 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2597,10 +2536,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -2724,8 +2662,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2742,10 +2681,6 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} - is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2781,9 +2716,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true is-extglob@2.1.1: @@ -2814,6 +2749,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-installed-globally@1.0.0: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} engines: {node: '>=18'} @@ -2901,9 +2841,9 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2919,9 +2859,6 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -2940,18 +2877,14 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true - jsdoc-type-pratt-parser@4.8.0: - resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} - engines: {node: '>=12.0.0'} - jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -2986,6 +2919,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -3168,9 +3104,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -3187,10 +3120,6 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3246,6 +3175,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -3253,10 +3186,6 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.7: - resolution: {integrity: sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3348,9 +3277,9 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -3363,6 +3292,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -3399,9 +3335,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} @@ -3447,9 +3380,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -3564,10 +3497,6 @@ packages: peerDependencies: react: '>=16.0.0' - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3594,26 +3523,14 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-aria-components@1.19.0: - resolution: {integrity: sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - react-aria@3.50.0: - resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-docgen-typescript@2.4.0: resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} peerDependencies: typescript: '>= 4.3.x' - react-docgen@7.1.1: - resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} - engines: {node: '>=16.14.0'} + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -3630,11 +3547,6 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-stately@3.48.0: - resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3655,6 +3567,10 @@ packages: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3712,6 +3628,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3871,14 +3791,20 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@8.6.18: - resolution: {integrity: sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==} + storybook@10.5.3: + resolution: {integrity: sha512-c8Wumu5qz0N2fnzWBxcPzUsY+8BpKBKChNyl4BEh9qhMV6KW587gL8il8emRB+4Hay+zMjDHA7cIeTkl4FKYuw==} hasBin: true peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 peerDependenciesMeta: + '@types/react': + optional: true prettier: optional: true + vite-plus: + optional: true string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} @@ -3888,10 +3814,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3935,6 +3857,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -4230,9 +4156,9 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unplugin@1.16.1: - resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} - engines: {node: '>=14.0.0'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} @@ -4251,9 +4177,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4393,10 +4316,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -4417,6 +4336,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -4460,8 +4383,13 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@adobe/css-tools@4.5.0': {} + '@andrewbranch/untar.js@1.0.3': {} '@arethetypeswrong/cli@0.18.5': @@ -4516,19 +4444,25 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.3': {} + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -4538,37 +4472,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -4576,45 +4510,53 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.29.2': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 '@babel/runtime@7.29.2': {} - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4624,6 +4566,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@braidai/lang@1.1.2': {} '@bramus/specificity@2.4.2': @@ -4750,7 +4697,7 @@ snapshots: '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 - js-yaml: 4.1.1 + js-yaml: 4.3.0 '@changesets/pre@2.0.2': dependencies: @@ -4939,237 +4886,113 @@ snapshots: '@dual-bundle/import-meta-resolve@4.2.1': {} - '@esbuild/aix-ppc64@0.25.12': + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.7': + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.28.1': + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.25.12': + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.27.7': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.28.1': + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - '@esbuild/win32-x64@0.28.1': optional: true @@ -5204,7 +5027,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5235,105 +5058,221 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.4.3': {} + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/external-editor@1.0.3(@types/node@22.19.17)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.17 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + optionalDependencies: + typescript: 5.8.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true - '@inquirer/external-editor@1.0.3(@types/node@22.19.17)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 22.19.17 + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true - '@internationalized/date@3.12.2': - dependencies: - '@swc/helpers': 0.5.23 + '@oxc-project/types@0.127.0': {} - '@internationalized/number@3.6.7': - dependencies: - '@swc/helpers': 0.5.23 + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true - '@internationalized/string@3.2.9': - dependencies: - '@swc/helpers': 0.5.23 + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': - dependencies: - glob: 10.5.0 - magic-string: 0.27.0 - react-docgen-typescript: 2.4.0(typescript@5.8.3) - vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) - optionalDependencies: - typescript: 5.8.3 + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true - '@jridgewell/resolve-uri@3.1.2': {} + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true - '@keyv/bigmap@1.3.1(keyv@5.6.0)': - dependencies: - hashery: 1.5.1 - hookified: 1.15.1 - keyv: 5.6.0 + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true - '@keyv/serialize@1.1.1': {} + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true - '@loaderkit/resolve@1.0.6': - dependencies: - '@braidai/lang': 1.1.2 + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true - '@manypkg/find-root@1.1.0': - dependencies: - '@babel/runtime': 7.29.2 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true - '@manypkg/get-packages@1.1.3': - dependencies: - '@babel/runtime': 7.29.2 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true - '@nodelib/fs.stat@2.0.5': {} + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true - '@nodelib/fs.walk@1.2.8': + '@oxc-resolver/binding-wasm32-wasi@11.24.2': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true - '@pkgjs/parseargs@0.11.0': + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true '@playwright/test@1.59.1': @@ -5344,10 +5283,6 @@ snapshots: dependencies: tinyexec: 1.2.4 - '@react-types/shared@3.36.0(react@18.3.1)': - dependencies: - react: 18.3.1 - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.60.3)': @@ -5445,101 +5380,81 @@ snapshots: dependencies: size-limit: 12.1.0(jiti@2.7.0) - '@storybook/builder-vite@8.6.18(storybook@8.6.18(prettier@3.8.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': + '@storybook/builder-vite@10.5.3(esbuild@0.28.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: - '@storybook/csf-plugin': 8.6.18(storybook@8.6.18(prettier@3.8.3)) - browser-assert: 1.2.1 - storybook: 8.6.18(prettier@3.8.3) + '@storybook/csf-plugin': 10.5.3(esbuild@0.28.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + storybook: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) ts-dedent: 2.2.0 vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) - - '@storybook/components@8.6.18(storybook@8.6.18(prettier@3.8.3))': - dependencies: - storybook: 8.6.18(prettier@3.8.3) - - '@storybook/core@8.6.18(prettier@3.8.3)(storybook@8.6.18(prettier@3.8.3))': - dependencies: - '@storybook/theming': 8.6.18(storybook@8.6.18(prettier@3.8.3)) - better-opn: 3.0.2 - browser-assert: 1.2.1 - esbuild: 0.25.12 - esbuild-register: 3.6.0(esbuild@0.25.12) - jsdoc-type-pratt-parser: 4.8.0 - process: 0.11.10 - recast: 0.23.11 - semver: 7.7.4 - util: 0.12.5 - ws: 8.21.0 - optionalDependencies: - prettier: 3.8.3 transitivePeerDependencies: - - bufferutil - - storybook - - supports-color - - utf-8-validate + - esbuild + - rollup + - webpack - '@storybook/csf-plugin@8.6.18(storybook@8.6.18(prettier@3.8.3))': + '@storybook/csf-plugin@10.5.3(esbuild@0.28.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: - storybook: 8.6.18(prettier@3.8.3) - unplugin: 1.16.1 + storybook: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + rollup: 4.60.3 + vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) '@storybook/global@5.0.0': {} - '@storybook/manager-api@8.6.18(storybook@8.6.18(prettier@3.8.3))': - dependencies: - storybook: 8.6.18(prettier@3.8.3) - - '@storybook/preview-api@8.6.18(storybook@8.6.18(prettier@3.8.3))': + '@storybook/icons@2.1.0(react@18.3.1)': dependencies: - storybook: 8.6.18(prettier@3.8.3) + react: 18.3.1 - '@storybook/react-dom-shim@8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.18(prettier@3.8.3))': + '@storybook/react-dom-shim@10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.18(prettier@3.8.3) + storybook: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) - '@storybook/react-vite@8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.3)(storybook@8.6.18(prettier@3.8.3))(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': + '@storybook/react-vite@10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.28.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.8.3)(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) '@rollup/pluginutils': 5.3.0(rollup@4.60.3) - '@storybook/builder-vite': 8.6.18(storybook@8.6.18(prettier@3.8.3))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/react': 8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.18(prettier@3.8.3))(typescript@5.8.3) - find-up: 5.0.0 + '@storybook/builder-vite': 10.5.3(esbuild@0.28.1)(rollup@4.60.3)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)) + '@storybook/react': 10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(typescript@5.8.3) + empathic: 2.0.1 magic-string: 0.30.21 react: 18.3.1 - react-docgen: 7.1.1 + react-docgen: 8.0.3 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.12 - storybook: 8.6.18(prettier@3.8.3) + storybook: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) tsconfig-paths: 4.2.0 vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + optionalDependencies: + typescript: 5.8.3 transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild - rollup - supports-color - - typescript + - webpack - '@storybook/react@8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.18(prettier@3.8.3))(typescript@5.8.3)': + '@storybook/react@10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1))(typescript@5.8.3)': dependencies: - '@storybook/components': 8.6.18(storybook@8.6.18(prettier@3.8.3)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.6.18(storybook@8.6.18(prettier@3.8.3)) - '@storybook/preview-api': 8.6.18(storybook@8.6.18(prettier@3.8.3)) - '@storybook/react-dom-shim': 8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.18(prettier@3.8.3)) - '@storybook/theming': 8.6.18(storybook@8.6.18(prettier@3.8.3)) + '@storybook/react-dom-shim': 10.5.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1)) react: 18.3.1 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.8.3) react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.18(prettier@3.8.3) + storybook: 10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1) optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) typescript: 5.8.3 - - '@storybook/theming@8.6.18(storybook@8.6.18(prettier@3.8.3))': - dependencies: - storybook: 8.6.18(prettier@3.8.3) - - '@swc/helpers@0.5.23': - dependencies: - tslib: 2.8.1 + transitivePeerDependencies: + - supports-color '@tailwindcss/node@4.3.2': dependencies: @@ -5646,7 +5561,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -5655,6 +5570,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -5665,6 +5589,15 @@ snapshots: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -5819,9 +5752,9 @@ snapshots: '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 @@ -5829,6 +5762,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + '@vitest/expect@3.2.7': dependencies: '@types/chai': 5.2.3 @@ -5845,6 +5786,10 @@ snapshots: optionalDependencies: vite: 6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4) + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.7': dependencies: tinyrainbow: 2.0.0 @@ -5861,16 +5806,28 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@3.2.7': dependencies: tinyspy: 4.0.4 + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@3.2.7': dependencies: '@vitest/pretty-format': 3.2.7 loupe: 3.2.1 tinyrainbow: 2.0.0 + '@webcontainer/env@1.1.1': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -5902,7 +5859,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5932,10 +5889,6 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -6033,10 +5986,6 @@ snapshots: baseline-browser-mapping@2.10.27: {} - better-opn@3.0.2: - dependencies: - open: 8.4.2 - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -6050,7 +5999,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.5: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -6058,8 +6007,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-assert@1.2.1: {} - browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.27 @@ -6068,9 +6015,13 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) - bundle-require@5.1.0(esbuild@0.27.7): + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bundle-require@5.1.0(esbuild@0.28.1): dependencies: - esbuild: 0.27.7 + esbuild: 0.28.1 load-tsconfig: 0.2.5 bytes-iec@3.1.1: {} @@ -6157,8 +6108,6 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 - client-only@0.0.1: {} - cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -6230,7 +6179,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.8.3 @@ -6248,6 +6197,8 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 + css.escape@1.5.1: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -6291,13 +6242,20 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} + define-lazy-prop@3.0.0: {} define-properties@1.2.1: dependencies: @@ -6350,6 +6308,8 @@ snapshots: dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -6360,8 +6320,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.351: {} emoji-regex@10.6.0: {} @@ -6372,6 +6330,8 @@ snapshots: emojilib@2.4.0: {} + empathic@2.0.1: {} + enhanced-resolve@5.21.0: dependencies: graceful-fs: 4.2.11 @@ -6500,71 +6460,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-register@3.6.0(esbuild@0.25.12): - dependencies: - debug: 4.4.3 - esbuild: 0.25.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -6757,7 +6652,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.2: {} + fast-uri@3.1.4: {} fastest-levenshtein@1.0.16: {} @@ -6822,11 +6717,6 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -6910,14 +6800,11 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.5.0: + glob@13.0.6: dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.7 + minimatch: 10.2.5 minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 + path-scurry: 2.0.2 global-directory@4.0.1: dependencies: @@ -7020,7 +6907,7 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} + indent-string@4.0.0: {} ini@1.3.8: {} @@ -7034,11 +6921,6 @@ snapshots: interpret@3.1.1: {} - is-arguments@1.2.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -7081,7 +6963,7 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-docker@2.2.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -7109,6 +6991,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-installed-globally@1.0.0: dependencies: global-directory: 4.0.1 @@ -7184,9 +7070,9 @@ snapshots: is-windows@1.0.2: {} - is-wsl@2.2.0: + is-wsl@3.1.1: dependencies: - is-docker: 2.2.1 + is-inside-container: 1.0.0 isarray@2.0.5: {} @@ -7203,12 +7089,6 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jiti@2.6.1: {} jiti@2.7.0: {} @@ -7219,17 +7099,15 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.14.2: + js-yaml@3.15.0: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.1: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 - jsdoc-type-pratt-parser@4.8.0: {} - jsdom@29.1.1: dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -7270,6 +7148,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -7433,8 +7313,6 @@ snapshots: loupe@3.2.1: {} - lru-cache@10.4.3: {} - lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -7447,10 +7325,6 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.27.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7495,18 +7369,16 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: brace-expansion: 1.1.16 - minimatch@9.0.7: - dependencies: - brace-expansion: 5.0.5 - minimist@1.2.8: {} minipass@7.1.3: {} @@ -7604,11 +7476,12 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@8.4.2: + open@10.2.0: dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 optionator@0.9.4: dependencies: @@ -7627,6 +7500,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -7659,8 +7579,6 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 @@ -7700,9 +7618,9 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: + path-scurry@2.0.2: dependencies: - lru-cache: 10.4.3 + lru-cache: 11.5.2 minipass: 7.1.3 path-type@4.0.0: {} @@ -7785,8 +7703,6 @@ snapshots: clsx: 2.1.1 react: 18.3.1 - process@0.11.10: {} - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -7815,40 +7731,15 @@ snapshots: queue-microtask@1.2.3: {} - react-aria-components@1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@internationalized/date': 3.12.2 - '@react-types/shared': 3.36.0(react@18.3.1) - '@swc/helpers': 0.5.23 - client-only: 0.0.1 - react: 18.3.1 - react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.48.0(react@18.3.1) - - react-aria@3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@internationalized/date': 3.12.2 - '@internationalized/number': 3.6.7 - '@internationalized/string': 3.2.9 - '@react-types/shared': 3.36.0(react@18.3.1) - '@swc/helpers': 0.5.23 - aria-hidden: 1.2.6 - clsx: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.48.0(react@18.3.1) - use-sync-external-store: 1.6.0(react@18.3.1) - react-docgen-typescript@2.4.0(typescript@5.8.3): dependencies: typescript: 5.8.3 - react-docgen@7.1.1: + react-docgen@8.0.3: dependencies: - '@babel/core': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 @@ -7871,16 +7762,6 @@ snapshots: react-refresh@0.17.0: {} - react-stately@3.48.0(react@18.3.1): - dependencies: - '@internationalized/date': 3.12.2 - '@internationalized/number': 3.6.7 - '@internationalized/string': 3.2.9 - '@react-types/shared': 3.36.0(react@18.3.1) - '@swc/helpers': 0.5.23 - react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) - react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -7888,7 +7769,7 @@ snapshots: read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.2 + js-yaml: 3.15.0 pify: 4.0.1 strip-bom: 3.0.0 @@ -7906,6 +7787,11 @@ snapshots: dependencies: resolve: 1.22.12 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -7994,6 +7880,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -8163,14 +8051,31 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@8.6.18(prettier@3.8.3): + storybook@10.5.3(@types/react@18.3.28)(prettier@3.8.3)(react@18.3.1): dependencies: - '@storybook/core': 8.6.18(prettier@3.8.3)(storybook@8.6.18(prettier@3.8.3)) + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@18.3.1) + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.28.1 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.24.2 + recast: 0.23.11 + semver: 7.7.4 + use-sync-external-store: 1.6.0(react@18.3.1) + ws: 8.21.0 optionalDependencies: + '@types/react': 18.3.28 prettier: 3.8.3 transitivePeerDependencies: - bufferutil - - supports-color + - react - utf-8-validate string-argv@0.3.2: {} @@ -8181,12 +8086,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -8255,6 +8154,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-indent@4.1.1: {} strip-json-comments@3.1.1: {} @@ -8471,12 +8374,12 @@ snapshots: tsup@8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.4): dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) + bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 debug: 4.4.3 - esbuild: 0.27.7 + esbuild: 0.28.1 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 @@ -8499,7 +8402,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.7 + esbuild: 0.28.1 get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 @@ -8575,9 +8478,11 @@ snapshots: universalify@0.1.2: {} - unplugin@1.16.1: + unplugin@2.3.11: dependencies: + '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 update-browserslist-db@1.2.3(browserslist@4.28.2): @@ -8596,14 +8501,6 @@ snapshots: util-deprecate@1.0.2: {} - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.2 - is-typed-array: 1.1.15 - which-typed-array: 1.1.20 - validate-npm-package-name@5.0.1: {} vite-node@3.2.4(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4): @@ -8629,7 +8526,7 @@ snapshots: vite@6.4.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4): dependencies: - esbuild: 0.25.12 + esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.14 @@ -8767,12 +8664,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -8786,6 +8677,10 @@ snapshots: ws@8.21.0: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -8823,3 +8718,5 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} + + zod@4.4.3: {} diff --git a/skills/velune-react/references/components.md b/skills/velune-react/references/components.md index eda03d5..c5ae1a6 100644 --- a/skills/velune-react/references/components.md +++ b/skills/velune-react/references/components.md @@ -3,10 +3,10 @@ ## Contents - Foundations and layout: Box, Container, Flex, Grid, Stack, Text, Divider -- Inputs: Button, Input, TextArea, Checkbox, Radio, Switch, Select, DatePicker, DateRangePicker, Form -- Navigation: Tabs, Pagination, Collapse, Wizard +- Inputs: Button, Input, TextArea, Checkbox, Radio, Switch, Select, Combobox, Slider, DatePicker, DateRangePicker, Form +- Navigation: Breadcrumb, Tabs, Pagination, Collapse, Wizard - Overlays: Modal, Drawer, Dropdown, Popover, Tooltip -- Feedback: Progress, Spinner, Skeleton, Toast +- Feedback: Alert, Progress, Spinner, Skeleton, Toast - Data display: Card, ReliefCard, Avatar, Badge, Tag, List, Table, VirtualTable All imports come from `velune/react`. Query current types with `node scripts/get_component_docs.mjs ComponentName`. @@ -45,17 +45,17 @@ Count, dot, or standalone status indicator. Tones: `default | primary | success ## Box -Polymorphic layout primitive with semantic `as`, display, padding, and margin props. Use Tailwind for responsive tracks and exceptional layout. +Polymorphic layout primitive with semantic `as`, display, padding, and margin props. These props accept either a value or a breakpoint object. ```tsx - + Content ``` ## Button -Command or link control. Variants: `primary | secondary | ghost | text | danger`; sizes: `sm | md | lg`. Supports loading, icons, and block layout. +Command or link control. Variants: `primary | secondary | ghost | text`; use `tone="danger"` for destructive actions. Supports loading, icons, full-width layout, and `asChild` for one child element. ```tsx - Duplicate - + Duplicate + Delete project @@ -181,10 +205,10 @@ Action or selection menu with React Aria menu semantics, keyboard navigation, se ## Flex -One-dimensional layout primitive with direction, align, justify, wrap, gap, and full-width props. +One-dimensional layout primitive with direction, align, justify, wrap, gap, and full-width props. Each layout prop accepts a breakpoint object. ```tsx - + ... ``` @@ -205,10 +229,14 @@ Declarative values, validation, submission, and nested field paths. Compose `For ## Grid -Grid layout primitive with typed column counts, gap, justification, and responsive collapse. +Grid layout primitive with typed column counts, gap, justification, and responsive collapse. For explicit breakpoint layouts, provide objects and set `responsive={false}`. ```tsx - + {items} ``` @@ -308,6 +336,29 @@ Single or multiple selection with option groups, searching, keyboard navigation, ``` +## Combobox + +Searchable single-select with controlled or uncontrolled value and input state. Compose Label, Item, Empty, and NoMatches slots. + +```tsx + + Assignee + Ada Lovelace + No matching people. + +``` + +## Slider + +Native range control with single-value or range values, keyboard support, value formatting, and horizontal or vertical orientation. + +```tsx + + Volume + + +``` + ## Skeleton Loading placeholder with text, rectangular, rounded, and circular variants plus pulse/wave/none animation. @@ -326,10 +377,10 @@ Indeterminate status with `sm | md | lg` sizes and semantic tones. Always provid ## Stack -Vertical by default, or horizontal, with tokenized gaps and alignment. +Vertical layout with tokenized gaps, alignment, optional reverse order, and dividers. Use `gap`; the previous `spacing` prop was removed. Layout props accept breakpoint objects. ```tsx - +}> Email @@ -339,7 +390,7 @@ Vertical by default, or horizontal, with tokenized gaps and alignment. ## Switch -Immediate boolean setting with `sm | md` sizes, loading, description, and form participation. `onChange` receives a boolean. +Immediate boolean setting with `sm | md | lg` sizes, loading, description, and form participation. `onChange` receives a boolean. ```tsx @@ -390,7 +441,7 @@ Polymorphic typography with token sizes, weights, tones, alignment, font familie ## TextArea -Long-form input with Input-like field states, autosizing, character count, and `sm | md | lg` sizes. +Long-form input with Input-like field states, autosizing, character count, `sm | md | lg` sizes, and `resize="none" | "vertical" | "horizontal" | "both"`. ```tsx