From 974df83bd1ddd38f18fec9537da803465c5c791a Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 16 Aug 2026 14:33:27 +0700 Subject: [PATCH 01/15] feat(material): let every surface wear glass, not only Card Glass was a real material with one consumer. `styles/glass.ts` derives twenty-five `--glass-*` tokens from three numbers, and the only component that could read them was Card, so an app whose panels are dialogs, drawers, popovers or a navbar could set the tokens and see nothing change. `material` moves into the shared vocabulary and onto Dialog.Content, Drawer.Content, Popover.Content, Menu and Navbar. `solid` is the default on all of them, so this adds a capability without changing a single existing render. The rules live in `_shared/material.css`, keyed on `[data-material="glass"]` rather than a per-component class. Card had them inline, and copying that block five more times would be six places to update the next time the glass vocabulary moves - which it already did once, from thirty-one hand-declared properties to three numbers. Card now carries the attribute too and keeps its own classes, so its appearance is unchanged while the definition is shared. Three behaviours come along because they belong to the material rather than to any one component: nested glass is flattened to a single pane, since blurring already-blurred output is muddy rather than deeper and costs a second pass; and both `prefers-reduced-transparency` and a browser without `backdrop-filter` fall back to an opaque fill, because a tint with no blur behind it is a surface you cannot read text on. Every `--glass-*` read carries a fallback. An undefined custom property makes CSS drop the whole declaration rather than fall back to an initial value, which is how a partial token set used to produce a surface with no background at all. `CardMaterial` stays as a deprecated alias of `Material`. It is exported, it has always had exactly these two members, and removing it would break call sites for no gain. --- docs/api-contract.md | 8 +- docs/ui-usage.md | 38 ++++++- package.json | 2 +- src/components/_shared/material.css | 115 ++++++++++++++++++++++ src/components/card/Card.layout.tsx | 13 ++- src/components/dialog/Dialog.layout.tsx | 15 ++- src/components/drawer/Drawer.layout.tsx | 9 +- src/components/menu/Menu.layout.tsx | 7 +- src/components/navbar/Navbar.layout.tsx | 7 +- src/components/popover/Popover.layout.tsx | 7 +- src/components/vocabulary.ts | 19 ++++ src/index.ts | 1 + 12 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 src/components/_shared/material.css diff --git a/docs/api-contract.md b/docs/api-contract.md index 49a165f4..4a6af507 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -270,7 +270,7 @@ HTML attributes and `UIBaseProps`; that is an assertion, not a gap. ### DialogContent -`backdrop children isDismissable placement scrollBehavior shouldCloseOnBackdropClick size` +`backdrop children isDismissable material placement scrollBehavior shouldCloseOnBackdropClick size` ### DialogFooter @@ -318,7 +318,7 @@ HTML attributes and `UIBaseProps`; that is an assertion, not a gap. ### DrawerContent -`children placement scrollBehavior` +`children material placement scrollBehavior` ### DrawerDialog @@ -494,7 +494,7 @@ HTML attributes and `UIBaseProps`; that is an assertion, not a gap. ### Menu -`children defaultSelectedKeys disabled disabledKeys disallowEmptySelection items onAction onSelectionChange renderEmpty selectedKeys selectionMode state` +`children defaultSelectedKeys disabled disabledKeys disallowEmptySelection items material onAction onSelectionChange renderEmpty selectedKeys selectionMode state` ### MenuItem @@ -534,7 +534,7 @@ HTML attributes and `UIBaseProps`; that is an assertion, not a gap. ### Navbar -`as dataTheme` +`as dataTheme material` ### NoiseBackground diff --git a/docs/ui-usage.md b/docs/ui-usage.md index 2bd6e860..71295963 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -31,10 +31,44 @@ Layout components require the application compiler before the normal Solid trans ## Theming - Two themes: `light` (default when no attribute) and `dark`. Switch: `document.documentElement.setAttribute("data-theme", "dark")`. -- Tokens are CSS vars: `--color-primary(/-content)`, `--color-secondary`, `--color-accent`, `--color-neutral`, `--color-info/success/warning/error`, `--color-danger`, surfaces `--color-base-100/200/300`, `--color-base-content`, HeroUI-style `--color-default(/-foreground/-hover)`, `--color-background/foreground`, daisy short aliases `--b1/--b2/--b3/--bc`, radii `--radius-selector/-field/-box`, and a ~35-var `--glass-*` set (runtime-tweakable on `documentElement.style`). +- Tokens are CSS vars: `--color-primary(/-content)`, `--color-secondary`, `--color-accent`, `--color-neutral`, `--color-info/success/warning/error`, `--color-danger`, surfaces `--color-base-100/200/300`, `--color-base-content`, HeroUI-style `--color-default(/-foreground/-hover)`, `--color-background/foreground`, daisy short aliases `--b1/--b2/--b3/--bc`, radii `--radius-selector/-field/-box`, and the `--glass-*` set: six colours the theme owns, plus the rest derived from three numbers by `applyGlassTokens` (see [Glass](#glass)). - `src/index.css` has a Tailwind v4 `@theme` block, so in a Tailwind v4 app `bg-primary`, `text-base-content`, `bg-base-100` etc. work. - Any component accepts `dataTheme` prop → rendered as `data-theme` attr (scoped theming). +### Glass + +`material="glass"` makes a surface out of what is behind it rather than a fill. +It is on `Card`, `Dialog.Content`, `Drawer.Content`, `Popover.Content`, `Menu` +and `Navbar`; `solid` is the default everywhere, so nothing changes until it is +asked for. + +The look is driven by the `--glass-*` custom properties, and you do not set +those by hand — `applyGlassTokens` derives all twenty-five from three numbers: + +```ts +import { applyGlassTokens, GLASS_DEFAULTS, GLASS_LIMITS } from "@pathscale/ui"; + +applyGlassTokens({ blur: 9, refraction: 0.31, depth: 24 }, "dark"); +``` + +- `blur` (0–50px) how far the backdrop is smeared +- `refraction` (0–0.4) how much the surface asserts its own tint, border and highlight +- `depth` (0–30) how far off the page it sits: glow, sheen, shadow + +`GLASS_LIMITS` carries those ranges and `GLASS_DEFAULTS` the per-mode defaults, +so a settings panel should read both from here rather than restating them. +`resolveGlassTokens` returns the same set as an object, and `glassTokensToCss` +as a declaration block for a theme that wants them baked in. + +Pass a **complete** tuning. Three of the derived tokens are read without a CSS +fallback, and an undefined custom property makes CSS drop the whole declaration +rather than fall back — so a partial set renders a surface with no background at +all rather than a plainer one. + +Nested glass is flattened to one pane on purpose, and both +`prefers-reduced-transparency` and a browser without `backdrop-filter` fall back +to a more opaque fill. + ## Component conventions (consumer-facing) - Booleans are HeroUI-style `is*`: `isDisabled`, `isOpen`, `isInvalid`, `isPending`, `isIconOnly`, `isHoverable`, `isPressable`. Native `disabled` also honored. @@ -92,7 +126,7 @@ the shared fallback. This works with PathScale Fonts and application-owned font ## Component inventory (by family) -- **Layout/primitives**: Flex, Grid, Join, Surface, Card, GlassPanel, Separator, ScrollShadow, Skeleton, EmptyState, Footer, Header, Navbar, Toolbar, FloatingDock +- **Layout/primitives**: Flex, Grid, Join, Card, Separator, ScrollArea, Skeleton, Empty, Footer, Header, Navbar, Toolbar, Dock - **Typography/misc**: Text, Link, Kbd, Badge, Chip, Tag/TagGroup, Avatar, Icon, Tooltip, Breadcrumbs, Pagination, Meter, ProgressBar, ProgressCircle, Spinner (alias: Loading) - **Inputs**: Input, InputGroup, InputOTP, TextField, TextArea (and textarea), NumberField, SearchField, PasswordField (+ password-requirements/rules, `passwordRules.ts`), ColorField, Checkbox(+Group), Radio(+Group), Toggle, Slider, Select, ComboBox, ListBox, SizePicker, Form pieces (Label, Description, ErrorMessage, FieldError, Fieldset) - **Dates**: Calendar, RangeCalendar, DatePicker, DateRangePicker (internal date engine); DateField, TimeField (separate segmented editors) diff --git a/package.json b/package.json index 5d0fbb2e..9461f423 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pathscale/ui", - "version": "2.5.0", + "version": "2.6.0", "author": "pathscale", "repository": { "type": "git", diff --git a/src/components/_shared/material.css b/src/components/_shared/material.css new file mode 100644 index 00000000..a970217d --- /dev/null +++ b/src/components/_shared/material.css @@ -0,0 +1,115 @@ +/* + * `material="glass"`, for every surface that can hold content. + * + * One definition rather than one per component. Card had these rules inline, + * and copying them to Dialog, Drawer, Popover, Menu, Dock and Navbar would be + * six places to update when the glass vocabulary moves — which it did once + * already, from thirty-one hand-declared properties to three numbers and + * twenty-five derived ones. + * + * A component opts in by importing this file and adding the `material` axis to + * its recipe. The classes are keyed on a data attribute rather than a + * `--material-glass` class so the rules are genuinely shared: a + * per-component class name would need this block duplicated for each one, which + * is the thing being avoided. + * + * Every `--glass-*` read carries a fallback, and that is load-bearing rather + * than tidy. An undefined custom property does not fall back to its initial + * value: CSS drops the whole declaration. Three of these used to be read bare, + * so a theme that defined twenty-eight of the thirty-one glass tokens got a + * surface with no background at all rather than a plainer one. The two shipped + * themes both use `white` for the three colours, so that is what the fallbacks + * say. `src/styles/glass.ts` derives the rest from `blur`, `refraction` and + * `depth`. + */ + +@layer components { + [data-material="solid"] { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + [data-material="glass"] { + background-color: color-mix( + in oklab, + var(--glass-background-color, white), + transparent calc(100% - var(--glass-background-opacity, 55%)) + ); + border-color: color-mix( + in oklab, + var(--glass-border-color, white), + transparent calc(100% - var(--glass-border-opacity, 12%)) + ); + backdrop-filter: blur(var(--glass-blur, 50px)) saturate(var(--glass-saturation, 1.2)) + brightness(var(--glass-brightness, 1)); + -webkit-backdrop-filter: blur(var(--glass-blur, 50px)) saturate(var(--glass-saturation, 1.2)) + brightness(var(--glass-brightness, 1)); + } + + /* + * The inner highlight: the lit top edge that makes a pane read as a pane + * rather than as a translucent rectangle. + * + * On a pseudo-element so it cannot be overwritten by a component's own + * `box-shadow`, which is how elevation is expressed. `inherit` on the radius + * keeps it aligned to whatever corner the component chose. + */ + [data-material="glass"]::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + box-shadow: inset 0 var(--border, 1px) 0 0 + color-mix( + in oklab, + var(--glass-highlight-color, white), + transparent calc(100% - var(--glass-highlight-opacity, 18%)) + ); + } + + /* + * Glass inside glass is one pane, not two. + * + * A backdrop filter reads the pixels behind it, so nesting one inside another + * blurs already-blurred output. That is muddy rather than deeper, and it + * costs a second render pass to get there. + */ + [data-material="glass"] [data-material="glass"] { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + /* + * No backdrop support, or a reader who asked for less transparency. + * + * Falls back to a more opaque fill rather than to a thin tint over whatever + * happens to be behind it, because the tint alone is unreadable. The + * highlight goes too: it reads as a stray line without the material. + */ + @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + [data-material="glass"] { + background-color: color-mix( + in oklab, + var(--glass-background-color, white), + transparent calc(100% - var(--glass-fallback-background-opacity, 78%)) + ); + } + } + + @media (prefers-reduced-transparency: reduce) { + [data-material="glass"] { + background-color: color-mix( + in oklab, + var(--glass-background-color, white), + transparent calc(100% - var(--glass-fallback-background-opacity, 78%)) + ); + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + [data-material="glass"]::before { + box-shadow: none; + } + } +} diff --git a/src/components/card/Card.layout.tsx b/src/components/card/Card.layout.tsx index 2bc1cdca..4257849b 100644 --- a/src/components/card/Card.layout.tsx +++ b/src/components/card/Card.layout.tsx @@ -1,19 +1,25 @@ +import "../_shared/material.css"; import "./Card.css"; import { Show, type JSX } from "solid-js"; -import type { Flavor, Radius, Space, State, UIBaseProps, Variant } from "../vocabulary"; +import type { Flavor, Material, Radius, Space, State, UIBaseProps, Variant } from "../vocabulary"; import type { Layout } from "../../lib/layouts"; import { card, cardBody, cardFooter, cardHeader } from "./Card.recipe"; /* ------------------------------------------------------------------------------------------------- * Types * -----------------------------------------------------------------------------------------------*/ -export type CardMaterial = "solid" | "glass"; +/** + * @deprecated Use `Material` from the shared vocabulary. Kept as an alias + * because it is exported publicly and removing it would break call sites; it + * has always had exactly these two members. + */ +export type CardMaterial = Material; export type CardElevation = "none" | "sm" | "md" | "lg"; export type CardProps = Omit, "children"> & UIBaseProps & { variant?: Variant; - material?: CardMaterial; + material?: Material; elevation?: CardElevation; flavor?: Flavor; state?: State; @@ -66,6 +72,7 @@ export const CardLayout: Layout = () => { tabIndex={local.tabIndex ?? (local.isInteractive ? 0 : undefined)} onKeyDown={handleKeyDown} data-flavor={local.flavor ?? "neutral"} + data-material={local.material ?? "solid"} > {local.header} diff --git a/src/components/dialog/Dialog.layout.tsx b/src/components/dialog/Dialog.layout.tsx index 99f6d8bf..f16e6c4d 100644 --- a/src/components/dialog/Dialog.layout.tsx +++ b/src/components/dialog/Dialog.layout.tsx @@ -15,7 +15,8 @@ import { import { Portal } from "solid-js/web"; import { twMerge } from "tailwind-merge"; -import type { UIBaseProps } from "../vocabulary"; +import "../_shared/material.css"; +import type { Material, UIBaseProps } from "../vocabulary"; import { CLASSES } from "./Dialog.recipe"; import type { Layout } from "../../lib/layouts"; import { componentRecipe } from "./Dialog.recipe"; @@ -189,6 +190,14 @@ export type DialogContentProps = Omit, "child scrollBehavior?: DialogScrollBehavior; isDismissable?: boolean; shouldCloseOnBackdropClick?: boolean; + /** + * What the panel is made of. `solid` by default. + * + * `glass` pairs naturally with `backdrop="transparent"`: the point of a + * glass dialog is seeing the page through it, and an opaque backdrop is + * exactly the thing that would hide it. + */ + material?: Material; }; export type DialogHeaderProps = Omit, "children"> & @@ -242,7 +251,7 @@ const DialogRoot: Layout = () => { const [internalOpen, setInternalOpen] = createSignal(Boolean(local.defaultOpen)); const [animState, setAnimState] = createSignal( - Boolean(local.open ?? local.defaultOpen) ? "open" : "closed", + local.open ?? local.defaultOpen ? "open" : "closed", ); const [contentRef, setContentRef] = createSignal(undefined); const [labelledBy, setLabelledBy] = createSignal(undefined); @@ -510,6 +519,7 @@ const DialogContent: Layout = () => "scrollBehavior", "isDismissable", "shouldCloseOnBackdropClick", + "material", "role", "tabIndex", "aria-labelledby", @@ -574,6 +584,7 @@ const DialogContent: Layout = () => local.class, ) }} data-slot="dialog-content" + data-material={local.material ?? "solid"} data-placement={placement()} data-size={size()} data-scroll={scrollBehavior()} diff --git a/src/components/drawer/Drawer.layout.tsx b/src/components/drawer/Drawer.layout.tsx index f174e55f..284918da 100644 --- a/src/components/drawer/Drawer.layout.tsx +++ b/src/components/drawer/Drawer.layout.tsx @@ -12,7 +12,8 @@ import { } from "solid-js"; import { Portal } from "solid-js/web"; import { twMerge } from "tailwind-merge"; -import type { UIBaseProps } from "../vocabulary"; +import "../_shared/material.css"; +import type { Material, UIBaseProps } from "../vocabulary"; import { focusFirst, isSidePlacement, @@ -108,6 +109,8 @@ export type DrawerContentProps = Omit, "child /** @deprecated Configure placement at Drawer.Root `placement` */ placement?: DrawerPlacement; scrollBehavior?: DrawerScrollBehavior; + /** What the panel is made of. `solid` by default. */ + material?: Material; }; export type DrawerDialogSide = "left" | "right"; @@ -189,7 +192,7 @@ const DrawerRoot: Layout = () => { const [internalOpen, setInternalOpen] = createSignal(Boolean(local.defaultOpen)); const [animState, setAnimState] = createSignal( - Boolean(local.open ?? local.defaultOpen) ? "open" : "closed", + local.open ?? local.defaultOpen ? "open" : "closed", ); const [dialogRef, setDialogRef] = createSignal(); @@ -410,6 +413,7 @@ const DrawerContent: Layout = () => "style", "placement", "scrollBehavior", + "material", ]); const ctx = useDrawerContext(); @@ -434,6 +438,7 @@ const DrawerContent: Layout = () => local.class, )} data-slot="drawer-content" + data-material={local.material ?? "solid"} data-placement={placement()} data-scroll={scrollBehavior()} data-entering={ctx.animState() === "entering" ? "true" : undefined} diff --git a/src/components/menu/Menu.layout.tsx b/src/components/menu/Menu.layout.tsx index 86690aa6..aa422670 100644 --- a/src/components/menu/Menu.layout.tsx +++ b/src/components/menu/Menu.layout.tsx @@ -9,7 +9,8 @@ import { } from "solid-js"; import { twMerge } from "tailwind-merge"; -import type { UIBaseProps, State } from "../vocabulary"; +import "../_shared/material.css"; +import type { Material, State, UIBaseProps } from "../vocabulary"; import { CLASSES } from "./Menu.recipe"; import { MenuContext, @@ -67,6 +68,8 @@ export type MenuRootProps = Omit< onAction?: (key: string) => void; state?: State; disabled?: boolean; + /** What the surface is made of. `solid` by default. */ + material?: Material; }; const MenuRoot: Layout = () => { @@ -84,6 +87,7 @@ const MenuRoot: Layout = () => { "disallowEmptySelection", "onSelectionChange", "onAction", + "material", "state", "disabled", "role", @@ -300,6 +304,7 @@ const MenuRoot: Layout = () => { role={local.role ?? "menu"} aria-disabled={isDisabled() ? "true" : undefined} data-slot="menu" + data-material={local.material ?? "solid"} data-theme={local.dataTheme} data-selection-mode={selectionMode()} data-disabled={isDisabled() ? "true" : "false"} diff --git a/src/components/navbar/Navbar.layout.tsx b/src/components/navbar/Navbar.layout.tsx index 81b4d5d7..6ed17d43 100644 --- a/src/components/navbar/Navbar.layout.tsx +++ b/src/components/navbar/Navbar.layout.tsx @@ -5,7 +5,8 @@ import { twMerge } from "tailwind-merge"; import NavbarSection from "./NavbarSection.generated"; import NavbarStack from "./NavbarStack.generated"; import NavbarRow from "./NavbarRow.generated"; -import type { UIBaseProps } from "../vocabulary"; +import "../_shared/material.css"; +import type { Material, UIBaseProps } from "../vocabulary"; import { CLASSES } from "./Navbar.recipe"; import type { Layout } from "../../lib/layouts"; import { componentRecipe } from "./Navbar.recipe"; @@ -14,6 +15,8 @@ export type NavbarProps = JSX.HTMLAttributes & UIBaseProps & { as?: keyof JSX.IntrinsicElements; dataTheme?: string; + /** What the bar is made of. `solid` by default. */ + material?: Material; }; const Navbar: Layout = () => { @@ -23,6 +26,7 @@ const Navbar: Layout = () => { "style", "children", "dataTheme", + "material", ]); const Tag = (local.as || "div") as keyof JSX.IntrinsicElements; @@ -35,6 +39,7 @@ const Navbar: Layout = () => { aria-label="Navbar" {...others} data-theme={local.dataTheme} + data-material={local.material ?? "solid"} {...{ class: classes() }} style={local.style} > diff --git a/src/components/popover/Popover.layout.tsx b/src/components/popover/Popover.layout.tsx index c59332ed..bdfd6fe2 100644 --- a/src/components/popover/Popover.layout.tsx +++ b/src/components/popover/Popover.layout.tsx @@ -16,7 +16,8 @@ import { import { Portal } from "solid-js/web"; import { twMerge } from "tailwind-merge"; -import type { UIBaseProps } from "../vocabulary"; +import "../_shared/material.css"; +import type { Material, UIBaseProps } from "../vocabulary"; import { createOverlayPosition, type OverlayAnchorRect, @@ -240,6 +241,8 @@ export type PopoverContentProps = UIBaseProps & Omit, "children"> & { children: JSX.Element; sideOffset?: number; + /** What the panel is made of. `solid` by default. */ + material?: Material; }; const PopoverContent: Layout = () => { @@ -249,6 +252,7 @@ const PopoverContent: Layout = () = "dataTheme", "style", "sideOffset", + "material", ]); const ctx = usePopoverContext(); @@ -297,6 +301,7 @@ const PopoverContent: Layout = () = role="dialog" {...{ class: twMerge(CLASSES.base, local.class) }} data-slot="popover-content" + data-material={local.material ?? "solid"} data-open={ctx.isOpen() ? "true" : "false"} data-placement={ctx.placement()} data-theme={local.dataTheme} diff --git a/src/components/vocabulary.ts b/src/components/vocabulary.ts index 2111345d..d1917386 100644 --- a/src/components/vocabulary.ts +++ b/src/components/vocabulary.ts @@ -87,6 +87,25 @@ export type State = "default" | "loading" | "error" | "invalid" | "disabled" | " /** How much emphasis, and what shape. */ export type Variant = "solid" | "soft" | "outline" | "ghost" | "plain"; +/** + * What a surface is made of. + * + * Orthogonal to `variant`, which is how much emphasis it carries. A glass card + * and a solid card can both be `outline`; the difference is whether the fill is + * a colour or a blurred view of whatever is behind it. + * + * `glass` reads the `--glass-*` family, which `src/styles/glass.ts` derives + * from three numbers. Every read carries a fallback, so a theme that sets none + * of them still renders — an undefined custom property makes CSS drop the whole + * declaration rather than fall back, which is how a partial set produced a card + * with no background at all. + * + * Only surfaces that can hold content take this. A `Button` is not made of + * anything: it is a control, and blurring what is behind a 32px pill reads as a + * smudge rather than as material. + */ +export type Material = "solid" | "glass"; + /** One scale, everywhere. */ export type Size = "xs" | "sm" | "md" | "lg" | "xl"; diff --git a/src/index.ts b/src/index.ts index 1b1304e1..974451a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -540,6 +540,7 @@ export type { State, Variant, + Material, Size, Radius, Space, From 28d6c5878f5bafe27a9436fab7b0bf922a086007 Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 16 Aug 2026 14:34:14 +0700 Subject: [PATCH 02/15] fix(check): catch generated files whose component was deleted `*.generated.tsx` is gitignored and `layouts:generate` only ever adds, so removing a component leaves its generated file behind, still importing the `.recipe` and `.layout` that went with it. Nothing notices: the file is invisible to `git status`, and the failure surfaces much later as tsc errors inside a file nobody edited. 2.5.0 shipped with twenty-two of them. `bun run check` read them as components and reported "missing index.ts barrel export" for things that no longer exist, which is how the gate spent a release red for a reason that pointed nowhere, and `bun run build` failed generating declarations. A twenty-third under `table/` survived the first sweep because it sits in a directory that is still alive. Checked here because this script already walks every component directory, and because a gate that names the actual file and says it is deletable build output costs one line to act on. --- scripts/check-contracts.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/check-contracts.ts b/scripts/check-contracts.ts index 34ce978e..4a892f31 100644 --- a/scripts/check-contracts.ts +++ b/scripts/check-contracts.ts @@ -118,6 +118,33 @@ if ( ); } +// --- Stale generated output --- +// +// `*.generated.tsx` is gitignored and written by `layouts:generate`, which only +// ever adds. Deleting a component therefore leaves its generated file behind, +// still importing the `.recipe` and `.layout` that went with it, and every +// build from then on fails inside a file nobody can see in `git status`. +// +// That is exactly how 2.5.0 shipped: twenty-two deleted components left +// twenty-two orphans, `bun run check` reported them as "missing index.ts +// barrel export" for components that no longer exist, and `bun run build` died +// generating declarations. Caught here it names the real problem and the fix. +for (const entry of readdirSync(COMPONENTS_DIR, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const dir = join(COMPONENTS_DIR, entry.name); + for (const file of readdirSync(dir)) { + if (!file.endsWith(".generated.tsx")) continue; + const authored = join(dir, file.replace(/\.generated\.tsx$/, ".layout.tsx")); + if (existsSync(authored)) continue; + fail( + entry.name, + "stale-generated", + `${join(dir, file)} has no .layout.tsx source; delete it (it is gitignored build output from a component that was removed)`, + "Structure", + ); + } +} + // --- Report --- if (violations.length === 0) { From 8b1a8ee5c08a441599fc1a8f30a92c31bb3b7edd Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 16 Aug 2026 14:51:05 +0700 Subject: [PATCH 03/15] feat!: remove TanStack, and own the form engine Three TanStack packages were the last things pinning this library to Solid 1, and none of them was earning its place. `@tanstack/solid-table` backed seven public hooks that **nothing in this library used**. `createDataGrid` already answers the same questions natively - its rows derive `filteredRows -> sortedRows -> pageRows` with selection alongside - so the hooks were a second table engine kept alive by their own exports. `@tanstack/solid-virtual` was the same story in one file. `useAnchoredOverlayPosition` stays: it never touched TanStack and a grid still needs to position an overlay against a cell. `@tanstack/solid-form` was different, and it is the work here. It was load bearing: `FormField` rendered ``, `FormSubmitButton` rendered ``, and `FormApi` was literally `{ _tsForm: any }`, so twelve erased generics were part of this library's public type. The replacement is small because the field components already carry the hard part - `Input`, `Select` and the rest own their presentation, `state="invalid"` and error slot. What was missing was bookkeeping: values, touched, errors, and when to run the schema. Standard Schema is called directly, which is what TanStack was doing on our behalf anyway. Two behaviours are now explicit rather than inherited. Errors are gated on `isTouched` so a form does not open covered in red, and a failed `submit()` touches every field, so the errors it refused on all become visible at once rather than one blur at a time. There is a test for the second, which the old suite had no way to express. Also drops ten `@solid-primitives/*` packages. Every one was declared in `package.json` and imported from nowhere - not in `src`, `tests`, `scripts` or `playground`. 28 packages leave the install. BREAKING CHANGE: `useTableModel`, `useTableSorting`, `useTablePagination`, `useTableFiltering`, `useTableSelection`, `useTableExpansion`, `toSortDescriptor`, `toSortingState` and `useVirtualRows` are removed, with their option and result types. Use `createDataGrid`, which covers filtering, sorting, pagination and selection; row expansion has no replacement yet. `FormApi._tsForm` is gone, replaced by `values()`, `getFieldValue`, `getFieldMeta`, `setFieldValue`, `validateField`, `submit()`, `isSubmitting()` and `isValid()`. --- README.md | 7 +- bun.lock | 129 +------ docs/ui-usage.md | 8 +- package.json | 25 -- src/components/form/Form.layout.tsx | 2 +- src/components/form/FormField.layout.tsx | 67 ++-- .../form/FormSubmitButton.layout.tsx | 26 +- src/components/table/index.ts | 30 +- src/hooks/form/FormContext.ts | 7 +- src/hooks/form/createForm.ts | 345 +++++++++--------- src/hooks/form/useFieldNew.ts | 33 +- src/hooks/table/helpers.ts | 42 --- src/hooks/table/index.ts | 45 +-- src/hooks/table/useTableExpansion.ts | 41 --- src/hooks/table/useTableFiltering.ts | 138 ------- src/hooks/table/useTableModel.ts | 98 ----- src/hooks/table/useTablePagination.ts | 69 ---- src/hooks/table/useTableSelection.ts | 42 --- src/hooks/table/useTableSorting.ts | 56 --- src/index.ts | 38 +- src/primitives/virtualizer/index.ts | 6 - src/primitives/virtualizer/useVirtualRows.ts | 80 ---- tests/hooks/form/createForm.test.ts | 50 ++- 23 files changed, 288 insertions(+), 1096 deletions(-) delete mode 100644 src/hooks/table/helpers.ts delete mode 100644 src/hooks/table/useTableExpansion.ts delete mode 100644 src/hooks/table/useTableFiltering.ts delete mode 100644 src/hooks/table/useTableModel.ts delete mode 100644 src/hooks/table/useTablePagination.ts delete mode 100644 src/hooks/table/useTableSelection.ts delete mode 100644 src/hooks/table/useTableSorting.ts delete mode 100644 src/primitives/virtualizer/index.ts delete mode 100644 src/primitives/virtualizer/useVirtualRows.ts diff --git a/README.md b/README.md index 3bbbdfba..0aa0e457 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ The rules that hold across every component — worth two minutes before your fir flat. Parts are styleable via `data-slot` and state attributes such as `data-open`. - There is **no polymorphic `as` prop**. -Forms are TanStack Form plus any Standard Schema validator; Table is headless and assembled -from hooks. Both, with toasts, icons and dates, are covered in +Forms are built in and take any Standard Schema validator; DataGrid derives filtering, +sorting, pagination and selection itself. Both, with toasts, icons and dates, are covered in **[docs/ui-usage.md](https://github.com/pathscale/ui/blob/master/docs/ui-usage.md)**. ## Requirements @@ -137,8 +137,7 @@ line and must not be used. The complete compiler contract, hard-error behavior, commands, and porting analyzer are documented in [docs/layouts.md](docs/layouts.md). -Peers include `@solid-primitives/*`, `@tanstack/solid-form` and `@tanstack/solid-table`. -Two are optional and only needed for the features they back: `popmotion` (JS animation +Two peers are optional and only needed for the features they back: `popmotion` (JS animation driver) and `@standard-schema/spec` (schema validation). ## Subpath exports diff --git a/bun.lock b/bun.lock index 53b1c804..f2d1281a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,7 @@ "": { "name": "@pathscale/ui", "dependencies": { - "@iconify/tailwind4": "^1.2.3", - "@pathscale/rsbuild-plugin-iconify": "^1.0.4", - "@tanstack/solid-virtual": "^3.13.27", + "clsx": "^2.1.1", "tailwind-merge": "^3.6.0", }, "devDependencies": { @@ -19,23 +17,10 @@ "@rsbuild/core": "^2.0.9", "@rsbuild/plugin-solid": "^1.2.1", "@rslib/core": "^0.22.0", - "@solid-primitives/event-listener": "^2.4.5", - "@solid-primitives/intersection-observer": "^2.2.4", - "@solid-primitives/keyboard": "^1.3.5", - "@solid-primitives/media": "^2.3.5", - "@solid-primitives/props": "^3.2.3", - "@solid-primitives/resize-observer": "^2.1.5", - "@solid-primitives/scheduled": "^1.5.3", - "@solid-primitives/scroll": "^2.1.5", - "@solid-primitives/storage": "^2.1.4", - "@solid-primitives/utils": "^6.4.0", "@standard-schema/spec": "^1.1.0", - "@tanstack/solid-form": "^1.33.0", - "@tanstack/solid-table": "^8.21.3", "@types/bun": "^1.3.14", "babel-preset-solid": "^1.9.12", "cally": "^0.8.0", - "clsx": "^2.1.1", "cssnano": "^7.1.9", "postcss": "^8.5.15", "postcss-cli": "^11.0.1", @@ -48,19 +33,7 @@ "typescript": "^6.0.3", }, "peerDependencies": { - "@solid-primitives/event-listener": "^2.3.0", - "@solid-primitives/intersection-observer": "^2.1.3", - "@solid-primitives/keyboard": "^1.2.5", - "@solid-primitives/media": "^2.2.5", - "@solid-primitives/props": "^3.1.8", - "@solid-primitives/resize-observer": "^2.0.22", - "@solid-primitives/scheduled": "^1.4.1", - "@solid-primitives/scroll": "^2.0.20", - "@solid-primitives/storage": "^2.1.1", - "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", - "@tanstack/solid-form": "^1.29.0", - "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3", @@ -178,8 +151,6 @@ "@colordx/core": ["@colordx/core@5.4.3", "", {}, "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ=="], - "@cyberalien/svg-utils": ["@cyberalien/svg-utils@1.2.15", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-ZbKU6npzW5PNocdoLVJYfKzaP+c/RpT6JUkoaKrW1DOcw6lyXub8XtcNpI3xok6FnyNjS6ZbsrrtjTnS9yeZAQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -188,10 +159,6 @@ "@iconify/json": ["@iconify/json@2.2.481", "", { "dependencies": { "@iconify/types": "*", "pathe": "^2.0.3" } }, "sha512-H/bCPJN+JawFnEgSFtBGQMz2nAc3KNsLZ6fKG6KfJX7X9H0blv0Acbt6umCBVRxZ0xBM/9FcUYZst4/Zv6SB/A=="], - "@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="], - - "@iconify/tools": ["@iconify/tools@5.0.12", "", { "dependencies": { "@cyberalien/svg-utils": "^1.2.15", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "fflate": "^0.8.3", "modern-tar": "^0.7.6", "pathe": "^2.0.3", "svgo": "^4.0.1" } }, "sha512-aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw=="], - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="], @@ -220,8 +187,6 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@pathscale/rsbuild-plugin-iconify": ["@pathscale/rsbuild-plugin-iconify@1.0.4", "", { "dependencies": { "@iconify/json": "^2.2.481", "@iconify/utils": "^2.3.0", "@rspack/core": "^1.7.11", "cssnano": "^7.1.9", "postcss": "^8.5.15", "postcss-cli": "^11.0.1", "svgo": "^3.3.3" }, "peerDependencies": { "@rsbuild/core": "^1.3.22", "@rslib/core": "^0.9.1" } }, "sha512-KrCuzAa9AzbXojh7ne+yfldfipWZ39d/BFfwDhLNlDvQxpena5I/0h4eoa4x4RlaCux6h+Ay81KmHGrdR0tpyw=="], - "@pathscale/rsbuild-plugin-ui-css-purge": ["@pathscale/rsbuild-plugin-ui-css-purge@0.9.11", "", { "dependencies": { "@swc/core": "^1.15.40", "lightningcss": "^1.32.0", "postcss": "^8.5.15" }, "bin": { "rsbuild-plugin-ui-css-purge": "dist/postbuild-purge.js", "generate-manifest": "src/generate-manifest.ts" } }, "sha512-iIyScjlJ1/yfbtD+vPWQP+S5WSQp6tBtppDAMGImBPCxzoSnpQPwhvsyFGpNyVwGUZj/8QxhbpY34ev0FXKpng=="], "@rsbuild/core": ["@rsbuild/core@2.0.9", "", { "dependencies": { "@rspack/core": "~2.0.5", "@swc/helpers": "^0.5.23" }, "peerDependencies": { "core-js": ">= 3.0.0" }, "optionalPeers": ["core-js"], "bin": { "rsbuild": "./bin/rsbuild.js" } }, "sha512-lK2bMNuwh3TuLXLskS7nG3fnQk+6eaLeeZiquJWcna4JZx9iaI59JSd+iLcg5TeZLjEVygkwn/HcE+yuYDQRAw=="], @@ -256,32 +221,6 @@ "@rspack/core": ["@rspack/core@2.0.5", "", { "dependencies": { "@rspack/binding": "2.0.5" }, "peerDependencies": { "@module-federation/runtime-tools": "^0.24.1 || ^2.0.0", "@swc/helpers": "^0.5.23" }, "optionalPeers": ["@module-federation/runtime-tools", "@swc/helpers"] }, "sha512-9tv2HAnSiTote5WPH2tmz1hLZ1zKbzkiZc1eYp7LP/8jcsiJBuf40ihiWidAgbbuYtJo3kWET6q+qOm5UhNiGA=="], - "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], - - "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], - - "@solid-primitives/intersection-observer": ["@solid-primitives/intersection-observer@2.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-3+QaAT/TUdEaeKkqiTf+sk8g1/vE+QM9PucG2/JI8hLI8ALpBYBnlhtvcwIdpjaKMSpPN/1L4hneLvwqnx4YRA=="], - - "@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ=="], - - "@solid-primitives/media": ["@solid-primitives/media@2.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA=="], - - "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], - - "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], - - "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], - - "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="], - - "@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-G7t4Kt1E+f4d7iZ2dGDWsra2k1WQ5VOtXrTcg7MC+DrR5QMq8YCj9wB/C9yxHJ1lGi7Sv+mzjNLtVCFO5ECzUg=="], - - "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], - - "@solid-primitives/storage": ["@solid-primitives/storage@2.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12", "solid-start": ">=0.2.26" }, "optionalPeers": ["solid-start"] }, "sha512-ziGRMSPi2PDnY6L6YPGWYnzmUEPIwO0PaDT9N1WNW9P2tUMMhgqKLXBrYfjytpjwY3UM6kIrbONMRADjyGtzAw=="], - - "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/core": ["@swc/core@1.15.40", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.40", "@swc/core-darwin-x64": "1.15.40", "@swc/core-linux-arm-gnueabihf": "1.15.40", "@swc/core-linux-arm64-gnu": "1.15.40", "@swc/core-linux-arm64-musl": "1.15.40", "@swc/core-linux-ppc64-gnu": "1.15.40", "@swc/core-linux-s390x-gnu": "1.15.40", "@swc/core-linux-x64-gnu": "1.15.40", "@swc/core-linux-x64-musl": "1.15.40", "@swc/core-win32-arm64-msvc": "1.15.40", "@swc/core-win32-ia32-msvc": "1.15.40", "@swc/core-win32-x64-msvc": "1.15.40" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg=="], @@ -316,26 +255,6 @@ "@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], - - "@tanstack/form-core": ["@tanstack/form-core@1.33.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.11.0" } }, "sha512-AV4Pw9Dk4orFsuPBcDssfWMJFs+yMYBae7zZ4oTqrCf4ftNGQKxvrQRZeqKHG6A4TkiLeSvf2kzIjcVkrW7E6w=="], - - "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - - "@tanstack/solid-form": ["@tanstack/solid-form@1.33.0", "", { "dependencies": { "@tanstack/form-core": "1.33.0", "@tanstack/solid-store": "^0.11.0" }, "peerDependencies": { "solid-js": ">=1.9.9" } }, "sha512-+KXp+T/fD+nO5JarDj1W5GctEm4aFuz13DkHVJrdIHGo0evMXlFNWJT57n+TOyvoLoKehknKjM1o41rrKClSTQ=="], - - "@tanstack/solid-store": ["@tanstack/solid-store@0.11.0", "", { "dependencies": { "@tanstack/store": "0.11.0" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-2isL0ZnnyI1iN0V+QPrxE3OcPndohBgVlBcHZYoAOIAiU1WoWjVy0q5gb0suPu1Id0h5cKC23JnwzQTxWDZD0w=="], - - "@tanstack/solid-table": ["@tanstack/solid-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "solid-js": ">=1.3" } }, "sha512-PmhfSLBxVKiFs01LtYOYrCRhCyTUjxmb4KlxRQiqcALtip8+DOJeeezQM4RSX/GUS0SMVHyH/dNboCpcO++k2A=="], - - "@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.27", "", { "dependencies": { "@tanstack/virtual-core": "3.16.1" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-pQZJa2IOR1rr76A1l4wV4iY1mrku6B8otNGw2hg1ywKCrbl74y50AbpPYR06ziMOjGjrGZBBAdTk+VY9IJON6w=="], - - "@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], - - "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.16.1", "", {}, "sha512-br4gmJ3eI2IXth0fAAVWNdi/Iqb5ZDIUG9409Q17qnXegeHHc9vPEeMsSwpGf29GqayfmqFPtv9S4rxcZOub3Q=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -444,8 +363,6 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], @@ -464,8 +381,6 @@ "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -524,8 +439,6 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], @@ -662,8 +575,6 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "thenby": ["thenby@1.4.1", "", {}, "sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ=="], "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -698,14 +609,6 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@iconify/tailwind4/@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], - - "@iconify/tools/@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], - - "@iconify/tools/svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="], - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], @@ -720,12 +623,6 @@ "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@iconify/tools/svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "@iconify/tools/svgo/css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], @@ -734,30 +631,6 @@ "postcss-svgo/svgo/css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - "@iconify/tools/svgo/css-tree/mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="], - "postcss-svgo/svgo/css-tree/mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "@pathscale/rsbuild-plugin-iconify/@rspack/core/@rspack/binding/@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], } } diff --git a/docs/ui-usage.md b/docs/ui-usage.md index 71295963..eadca7b8 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -146,7 +146,7 @@ the same normalized hex that it displays. Surface strength, softness and accent remain consumer theme concerns; use the literal selected hex as their input rather than making the wheel silently transform it. -## Forms (TanStack Form + Standard Schema) +## Forms (built in, plus Standard Schema) ```tsx import { createForm, Form, FormField, FormSubmitButton } from "@pathscale/ui"; @@ -165,8 +165,8 @@ const form = createForm({ ``` - `Form` without a `form` prop = plain styled `
` (`FormRoot`). With `form` = context provider + wired submit. -- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!canSubmit` (not touch-gated), so the button can be disabled with no visible error. -- Escape hatch: `form._tsForm` is the raw TanStack form API (typed `any` on purpose). +- Inside a ``: `useField(name)` → `{value, error, touched, invalid, handleChange, handleBlur}`. **Errors are touch-gated** — `error()` is `undefined` until the field blurs. `FormSubmitButton` disables on `!form.isValid()` (not touch-gated), so the button can be disabled with no visible error. A failed `submit()` touches every field, so the errors it refused on all become visible at once. +- The form API is the library's own: `values()`, `getFieldValue`, `getFieldMeta`, `setFieldValue`, `validateField`, `submit()`, `isSubmitting()`, `isValid()`. There is no longer a `_tsForm` escape hatch, because there is no longer a wrapped library to escape to. - Schema validation runs on change+blur+submit; blur errors clear immediately on change once valid. ## DataGrid (assembled) @@ -250,7 +250,7 @@ const table = useTableModel({ - State-slice hooks (all controlled-or-uncontrolled): `useTableSorting`, `useTableSelection`, `useTableFiltering` (per-column popovers + `getColumnFilterProps`), `useTablePagination` (⚠️ `nextPage(max)`/`lastPage(max)` need caller-supplied max page index), `useTableExpansion`. - Parts: TableRoot/ScrollContainer/Content/Header/Column/Body/Row/Cell/ExpandedRow/Footer/PageSize/ResizableContainer/ColumnResizer/LoadMore(+Content), plus SortIcon, ExpandToggle, InlineConfirm, MobileListView (responsive card fallback), VirtualSpacerRow. -- **Virtualization is not built in**: combine `useVirtualRows` (wraps @tanstack/solid-virtual) + `VirtualSpacerRow` yourself. See the Table section above. +- **Virtualization is not built in.** `useVirtualRows` was a thin wrapper over `@tanstack/solid-virtual` that nothing in the library used; it was removed with the rest of TanStack. `VirtualSpacerRow` is still here for a caller that brings its own windowing. ## Motion diff --git a/package.json b/package.json index 9461f423..8ea74e12 100644 --- a/package.json +++ b/package.json @@ -73,19 +73,7 @@ "@rsbuild/core": "^2.0.9", "@rsbuild/plugin-solid": "^1.2.1", "@rslib/core": "^0.22.0", - "@solid-primitives/event-listener": "^2.4.5", - "@solid-primitives/intersection-observer": "^2.2.4", - "@solid-primitives/keyboard": "^1.3.5", - "@solid-primitives/media": "^2.3.5", - "@solid-primitives/props": "^3.2.3", - "@solid-primitives/resize-observer": "^2.1.5", - "@solid-primitives/scheduled": "^1.5.3", - "@solid-primitives/scroll": "^2.1.5", - "@solid-primitives/storage": "^2.1.4", - "@solid-primitives/utils": "^6.4.0", "@standard-schema/spec": "^1.1.0", - "@tanstack/solid-form": "^1.33.0", - "@tanstack/solid-table": "^8.21.3", "@types/bun": "^1.3.14", "babel-preset-solid": "^1.9.12", "cally": "^0.8.0", @@ -102,23 +90,10 @@ }, "dependencies": { "clsx": "^2.1.1", - "@tanstack/solid-virtual": "^3.13.27", "tailwind-merge": "^3.6.0" }, "peerDependencies": { - "@solid-primitives/event-listener": "^2.3.0", - "@solid-primitives/intersection-observer": "^2.1.3", - "@solid-primitives/keyboard": "^1.2.5", - "@solid-primitives/media": "^2.2.5", - "@solid-primitives/props": "^3.1.8", - "@solid-primitives/resize-observer": "^2.0.22", - "@solid-primitives/scheduled": "^1.4.1", - "@solid-primitives/scroll": "^2.0.20", - "@solid-primitives/storage": "^2.1.1", - "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", - "@tanstack/solid-form": "^1.29.0", - "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3" diff --git a/src/components/form/Form.layout.tsx b/src/components/form/Form.layout.tsx index 19759b91..a7668f6b 100644 --- a/src/components/form/Form.layout.tsx +++ b/src/components/form/Form.layout.tsx @@ -57,7 +57,7 @@ const FormWithContext: Layout = () const handleSubmit: JSX.EventHandlerUnion = (e) => { e.preventDefault(); e.stopPropagation(); - local.form._tsForm.handleSubmit(); + void local.form.submit(); if (typeof local.onSubmit === "function") { local.onSubmit(e); } diff --git a/src/components/form/FormField.layout.tsx b/src/components/form/FormField.layout.tsx index 19158f38..653033cb 100644 --- a/src/components/form/FormField.layout.tsx +++ b/src/components/form/FormField.layout.tsx @@ -1,6 +1,5 @@ import { Show, splitProps, type Component, type JSX } from "solid-js"; import { twMerge } from "tailwind-merge"; -import type { FieldApi } from "@tanstack/solid-form"; import Input from "../input"; import type { InputFieldProps } from "../input"; @@ -75,49 +74,37 @@ const FormField: Layout = () => { }; const form = resolveForm(); - const tsForm = form._tsForm; - return ( - FieldApi) => { - const errorMessage = () => - field().state.meta.isTouched - ? getFirstFieldError((field().state.meta.errors ?? []) as unknown[]) - : undefined; + // Read straight off the form rather than through a render-prop child. The + // engine is ours now, so field state is just three accessors, and a wrapper + // component that exists only to hand them down would be a layer with no job. + const meta = () => form.getFieldMeta(local.name); + const errorMessage = () => + meta().isTouched ? getFirstFieldError(meta().errors) : undefined; - return ( -
- - - + return ( +
+ + + - { - field().handleChange(e.currentTarget.value as never); - }} - onBlur={() => field().handleBlur()} - aria-invalid={Boolean(errorMessage()) ? true : undefined} - issues={errorMessage() ? [{ code: "invalid", message: String(errorMessage()) }] : undefined} - /> + { + form.setFieldValue(local.name, e.currentTarget.value); + }} + onBlur={() => form.validateField(local.name, "blur")} + aria-invalid={errorMessage() ? true : undefined} + issues={errorMessage() ? [{ code: "invalid", message: String(errorMessage()) }] : undefined} + /> - -
- ); - }} - /> + +
); }; diff --git a/src/components/form/FormSubmitButton.layout.tsx b/src/components/form/FormSubmitButton.layout.tsx index ec17ad13..6e7efa95 100644 --- a/src/components/form/FormSubmitButton.layout.tsx +++ b/src/components/form/FormSubmitButton.layout.tsx @@ -45,24 +45,18 @@ const FormSubmitButton: Layout = }; const form = resolveForm(); - const tsForm = form._tsForm; + // `` was a render prop whose only job was to deliver two + // booleans. Both are accessors on the form now, so the component reads them + // directly and Solid tracks them the same way. return ( - ({ - canSubmit: s.canSubmit, - isSubmitting: s.isSubmitting, - })} - children={(state: () => { canSubmit: boolean; isSubmitting: boolean }) => ( - - )} - /> + ); }; diff --git a/src/components/table/index.ts b/src/components/table/index.ts index 3e271e53..5dea56b2 100644 --- a/src/components/table/index.ts +++ b/src/components/table/index.ts @@ -1,33 +1,7 @@ export type { ExpandToggleProps } from "./ExpandToggle.generated"; export { default as ExpandToggle } from "./ExpandToggle.generated"; -export type { - HookSortDescriptor, - HookSortDirection, - TableSelectionState, - UseAnchoredOverlayPositionOptions, - UseTableExpansionOptions, - UseTableExpansionResult, - UseTableFilteringOptions, - UseTableFilteringResult, - UseTableModelOptions, - UseTablePaginationOptions, - UseTablePaginationResult, - UseTableSelectionOptions, - UseTableSelectionResult, - UseTableSortingOptions, - UseTableSortingResult, -} from "./hooks"; -export { - toSortDescriptor, - toSortingState, - useAnchoredOverlayPosition, - useTableExpansion, - useTableFiltering, - useTableModel, - useTablePagination, - useTableSelection, - useTableSorting, -} from "./hooks"; +export type { UseAnchoredOverlayPositionOptions } from "./hooks"; +export { useAnchoredOverlayPosition } from "./hooks"; export type { InlineConfirmProps, InlineConfirmVariant, diff --git a/src/hooks/form/FormContext.ts b/src/hooks/form/FormContext.ts index b8d057cc..832aa74b 100644 --- a/src/hooks/form/FormContext.ts +++ b/src/hooks/form/FormContext.ts @@ -5,9 +5,8 @@ import type { FormApi } from "./createForm"; // Context // --------------------------------------------------------------------------- -// We use `any` here because the form's generic parameters are deeply nested -// TanStack types — the context just needs to hold the opaque FormApi wrapper. -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// The context holds a form whose value type the provider knows and the +// consumer does not, so the stored type erases it. export type AnyFormApi = FormApi; export const FormContext = createContext(null); @@ -26,7 +25,7 @@ export const FormContext = createContext(null); * @example * // Inside a component rendered as a child of * const form = useFormContext(); - * const tsForm = form._tsForm; + * const value = form.getFieldValue("email"); */ export const useFormContext = (): AnyFormApi => { const ctx = useContext(FormContext); diff --git a/src/hooks/form/createForm.ts b/src/hooks/form/createForm.ts index cdb1ace1..3d4bd874 100644 --- a/src/hooks/form/createForm.ts +++ b/src/hooks/form/createForm.ts @@ -1,11 +1,9 @@ -import { createForm as createTSForm } from "@tanstack/solid-form"; +import { createStore, produce } from "solid-js/store"; import type { StandardSchemaV1 } from "@standard-schema/spec"; +import { getFirstFieldError } from "./getFirstFieldError"; -// --------------------------------------------------------------------------- -// Public types -// --------------------------------------------------------------------------- - -type AnyValues = Record; +// biome-ignore lint/suspicious/noExplicitAny: a form's values are the caller's shape +type AnyValues = Record; export type AsyncValidatorFn = (context: { value: TValues; @@ -20,8 +18,7 @@ export type CreateFormOptions = { /** * Any Standard Schema-compatible schema (Zod, Valibot, Arktype, ...). - * Applied as `validators.onChange`, `validators.onBlur` and - * `validators.onSubmit`. + * Runs on change, on blur and on submit. */ schema?: StandardSchemaV1; @@ -40,189 +37,197 @@ export type CreateFormOptions = { onSubmit?: (value: TValues) => void | Promise; }; +/** What a field tracks besides its value. */ +type FieldMeta = { + isTouched: boolean; + errors: string[]; +}; + /** * The form API returned by `createForm`. * - * Consumers should use `` to wire it into the component tree. - * For advanced use, access the raw TanStack Form instance via `api._tsForm`. + * Consumers should use `` to wire it into the component tree, + * and read individual fields with `useField(name)`. */ export type FormApi = { - /** - * Raw TanStack Form instance. Intended as an escape hatch for cases not - * covered by the library's abstractions (custom field rendering, ``, etc.). - * Prefixed with `_` to signal that it's an internal detail. - * - * TanStack Form's type has 12 generic parameters — we intentionally erase - * them here to keep the public API simple. Use `form._tsForm` to access the - * full typed instance in advanced scenarios. - */ - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form has 12 generic params; erased for public API simplicity - _tsForm: any; + /** Every field's current value. */ + values: () => TValues; + /** One field's current value. */ + getFieldValue: (name: string) => unknown; + /** One field's touched flag and errors. */ + getFieldMeta: (name: string) => FieldMeta; + /** Write a value and re-run the synchronous schema. */ + setFieldValue: (name: string, value: unknown) => void; + /** Mark touched and validate, which is what a blur means. */ + validateField: (name: string, cause: "change" | "blur") => void | Promise; + /** Validate everything, run async validators, then `onSubmit` if clean. */ + submit: () => Promise; + /** True while `submit()` is awaiting. */ + isSubmitting: () => boolean; + /** True when no field holds an error. */ + isValid: () => boolean; /** Phantom field: preserves TValues for type inference in consuming hooks. */ _values?: TValues; }; -// --------------------------------------------------------------------------- -// Internal validation logic -// --------------------------------------------------------------------------- - -type ValidationCause = "change" | "blur" | "submit" | "mount" | "server"; - -type ValidationLogicProps = { - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form validator map shape is internal - validators?: any; - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form API type is intentionally erased in wrapper - form: any; - event: { - type: ValidationCause; - async: boolean; - }; - runValidation: (props: { - validators: Array< - | { - // biome-ignore lint/suspicious/noExplicitAny: validator function types are internal - fn: any; - cause: ValidationCause; - } - | undefined - >; - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form API type is intentionally erased in wrapper - form: any; - }) => void; -}; - -const createValidationLogic = (props: ValidationLogicProps) => { - if (!props.validators) { - return props.runValidation({ - validators: [], - form: props.form, - }); - } - - const isAsync = props.event.async; - - const onMountValidator = isAsync - ? undefined - : ({ fn: props.validators.onMount, cause: "mount" as const }); - - const onChangeValidator = { - fn: isAsync ? props.validators.onChangeAsync : props.validators.onChange, - cause: "change" as const, - }; - - const onBlurValidator = { - fn: isAsync ? props.validators.onBlurAsync : props.validators.onBlur, - cause: "blur" as const, - }; - - const onSubmitValidator = { - fn: isAsync ? props.validators.onSubmitAsync : props.validators.onSubmit, - cause: "submit" as const, - }; - - const onServerValidator = isAsync - ? undefined - : ({ - fn: () => undefined, - cause: "server" as const, - }); - - switch (props.event.type) { - case "mount": { - return props.runValidation({ - validators: [onMountValidator], - form: props.form, - }); - } - case "submit": { - return props.runValidation({ - validators: [ - onChangeValidator, - onBlurValidator, - onSubmitValidator, - onServerValidator, - ], - form: props.form, - }); - } - case "server": { - return props.runValidation({ - validators: [], - form: props.form, - }); - } - case "blur": { - return props.runValidation({ - validators: [onBlurValidator, onServerValidator], - form: props.form, - }); - } - case "change": { - // Re-run blur validators on each change so blur-origin errors are cleared - // immediately when the value becomes valid again. - return props.runValidation({ - validators: [onChangeValidator, onBlurValidator, onServerValidator], - form: props.form, - }); - } - default: { - throw new Error(`Unknown validation event type: ${props.event.type}`); - } +/** + * Read a Standard Schema's issues into a per-field error map. + * + * The spec puts the field on `issue.path`, which is a list of keys or of + * `{ key }` segments depending on the library. Only the first segment is used: + * this form is flat, and a nested path would key an error to a field name that + * does not exist rather than to its parent. + */ +const issuesToErrors = (issues: readonly StandardSchemaV1.Issue[]): Record => { + const errors: Record = {}; + for (const issue of issues) { + const head = issue.path?.[0]; + const name = + typeof head === "object" && head !== null && "key" in head ? String(head.key) : String(head ?? ""); + if (!name) continue; + (errors[name] ??= []).push(issue.message); } + return errors; }; -// --------------------------------------------------------------------------- -// Factory -// --------------------------------------------------------------------------- - /** - * Creates a new form instance backed by TanStack Form. + * A form, without TanStack. + * + * The engine is deliberately small because the field components already carry + * the hard parts: `Input`, `Select`, `Checkbox` and the rest own their own + * presentation, `state="invalid"` and error slot. What was missing is + * bookkeeping — values, touched, errors, and when to run the schema — and that + * is what this is. * - * ```tsx - * const form = createForm({ - * defaultValues: { email: "", password: "" }, - * schema: loginSchema, // Zod or Valibot — both work - * onSubmit: async (values) => { await login(values); }, - * }); + * `@tanstack/solid-form` used to supply it, through a `_tsForm` escape hatch + * that leaked its twelve erased generics into this library's public type. It + * also peer-pinned Solid 1, which made it one of three packages standing + * between this library and Solid 2. * - * return ( - * - * - * - * Log in - * - * ); - * ``` + * Validation runs on change and on blur, and errors are only *shown* once a + * field is touched — the display gate lives in `useField`, so a submit can + * surface every error at once by touching every field. */ export const createForm = ( options: CreateFormOptions, ): FormApi => { - const hasSchema = Boolean(options.schema); - const hasAsyncValidators = Boolean(options.asyncValidators); - - // biome-ignore lint/suspicious/noExplicitAny: TanStack Form generics are erased at our wrapper boundary - const tsForm: any = createTSForm(() => ({ - defaultValues: options.defaultValues as Record, - - validators: hasSchema || hasAsyncValidators - ? { - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onChange: options.schema as any, - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onBlur: options.schema as any, - // biome-ignore lint/suspicious/noExplicitAny: Standard Schema bridges Zod/Valibot/etc. - onSubmit: options.schema as any, - onBlurAsync: options.asyncValidators?.onBlur as any, - onSubmitAsync: options.asyncValidators?.onSubmit as any, + const [values, setValues] = createStore({ ...options.defaultValues }); + const [meta, setMeta] = createStore>({}); + const [status, setStatus] = createStore({ isSubmitting: false }); + + const metaFor = (name: string): FieldMeta => meta[name] ?? { isTouched: false, errors: [] }; + + /** Run the schema and replace every field's errors with its verdict. */ + const runSchema = (): boolean => { + const schema = options.schema; + if (!schema) return true; + const result = schema["~standard"].validate(values as TValues); + // A Standard Schema may validate asynchronously. The synchronous callers + // here (change, blur) cannot wait for one, and treating a pending result as + // "no errors" would clear real ones, so it is left to `submit()`. + if (result instanceof Promise) return true; + const errors = result.issues ? issuesToErrors(result.issues) : {}; + setMeta( + produce((draft) => { + const names = new Set([...Object.keys(draft), ...Object.keys(errors)]); + for (const name of names) { + draft[name] = { isTouched: draft[name]?.isTouched ?? false, errors: errors[name] ?? [] }; } - : undefined, + }), + ); + return !result.issues?.length; + }; + + const setFieldValue = (name: string, value: unknown): void => { + // Cast at the boundary: the store is typed by the caller's values, and a + // field name is a string the caller chose, so this is the one place the two + // cannot be related without making `name` a keyof and losing dotted paths. + (setValues as unknown as (fn: (draft: AnyValues) => void) => void)( + produce((draft: AnyValues) => { + draft[name] = value; + }), + ); + runSchema(); + }; - validationLogic: createValidationLogic, + const validateField = (name: string, cause: "change" | "blur"): void => { + if (cause === "blur") { + setMeta( + produce((draft) => { + draft[name] = { isTouched: true, errors: draft[name]?.errors ?? [] }; + }), + ); + } + runSchema(); + }; - onSubmit: async ({ value }: { value: unknown }) => { - await options.onSubmit?.(value as TValues); - }, - })); + const submit = async (): Promise => { + // Touch everything first: an untouched field hides its error, and a submit + // that silently does nothing because of an error you cannot see is the + // worst version of this. + setMeta( + produce((draft) => { + for (const name of Object.keys(values as AnyValues)) { + draft[name] = { isTouched: true, errors: draft[name]?.errors ?? [] }; + } + }), + ); + + let ok = runSchema(); + + // An async schema is resolved here, where there is somewhere to await it. + const schema = options.schema; + if (schema) { + const result = schema["~standard"].validate(values as TValues); + if (result instanceof Promise) { + const resolved = await result; + const errors = resolved.issues ? issuesToErrors(resolved.issues) : {}; + setMeta( + produce((draft) => { + for (const name of new Set([...Object.keys(draft), ...Object.keys(errors)])) { + draft[name] = { isTouched: true, errors: errors[name] ?? [] }; + } + }), + ); + ok = !resolved.issues?.length; + } + } - return { _tsForm: tsForm }; + if (!ok) return; + + setStatus("isSubmitting", true); + try { + const async = options.asyncValidators?.onSubmit; + if (async) { + const serverErrors = await async({ value: values as TValues }); + if (serverErrors && Object.keys(serverErrors).length > 0) { + setMeta( + produce((draft) => { + for (const [name, message] of Object.entries(serverErrors)) { + if (!message) continue; + draft[name] = { isTouched: true, errors: [String(message)] }; + } + }), + ); + return; + } + } + await options.onSubmit?.(values as TValues); + } finally { + setStatus("isSubmitting", false); + } + }; + + return { + values: () => values as TValues, + getFieldValue: (name) => (values as AnyValues)[name], + getFieldMeta: metaFor, + setFieldValue, + validateField, + submit, + isSubmitting: () => status.isSubmitting, + isValid: () => Object.values(meta).every((m) => (m?.errors.length ?? 0) === 0), + }; }; + +export { getFirstFieldError }; diff --git a/src/hooks/form/useFieldNew.ts b/src/hooks/form/useFieldNew.ts index 27b613c1..1fde17fe 100644 --- a/src/hooks/form/useFieldNew.ts +++ b/src/hooks/form/useFieldNew.ts @@ -29,8 +29,8 @@ export type UseFieldResult = { * Reads live field state for `name` from the nearest `
` context. * * Must be called **inside** a `` descendant component. - * For fields that need fine-grained rendering, prefer using `form._tsForm.Field` - * render prop directly. + * Field components own their own presentation; this supplies the state they + * need to show it. * * ```tsx * // Inside a child of @@ -46,29 +46,22 @@ export type UseFieldResult = { */ export const useField = (name: string): UseFieldResult => { const form = useFormContext(); - const tsForm = form._tsForm; - const value = createMemo(() => tsForm.getFieldValue(name as never)); + const value = createMemo(() => form.getFieldValue(name)); + const meta = createMemo(() => form.getFieldMeta(name)); + const touched = createMemo(() => meta().isTouched); - const meta = createMemo(() => tsForm.getFieldMeta(name as never)); - - const touched = createMemo(() => Boolean(meta()?.isTouched)); - - const error = createMemo((): string | undefined => { - const m = meta(); - if (!m?.isTouched) return undefined; - return getFirstFieldError((m.errors ?? []) as unknown[]); - }); + // Gated on `isTouched` so a form does not open covered in errors for fields + // nobody has reached yet. `submit()` touches everything, which is what makes + // a failed submit show all of them at once. + const error = createMemo((): string | undefined => + meta().isTouched ? getFirstFieldError(meta().errors) : undefined, + ); const invalid = createMemo(() => Boolean(error())); - const handleChange = (value: unknown) => { - tsForm.setFieldValue(name as never, value as never); - }; - - const handleBlur = () => { - tsForm.validateField(name as never, "blur"); - }; + const handleChange = (next: unknown) => form.setFieldValue(name, next); + const handleBlur = () => form.validateField(name, "blur"); return { value, error, touched, invalid, handleChange, handleBlur }; }; diff --git a/src/hooks/table/helpers.ts b/src/hooks/table/helpers.ts deleted file mode 100644 index 74ef3b36..00000000 --- a/src/hooks/table/helpers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Accessor } from "solid-js"; -import type { SortingState, Updater } from "@tanstack/solid-table"; -import type { HookSortDescriptor } from "./useTableSorting"; - -export const resolveUpdater = (previous: T, updater: Updater): T => { - if (typeof updater === "function") { - return (updater as (prev: T) => T)(previous); - } - return updater; -}; - -export const asAccessor = (value: Accessor | T): Accessor => { - if (typeof value === "function") { - return value as Accessor; - } - return () => value; -}; - -export const toSortDescriptor = ( - sorting: SortingState, -): HookSortDescriptor | undefined => { - const activeSort = sorting[0]; - if (!activeSort) return undefined; - - return { - column: activeSort.id, - direction: activeSort.desc ? "descending" : "ascending", - }; -}; - -export const toSortingState = ( - descriptor: HookSortDescriptor | undefined, -): SortingState => { - if (!descriptor) return []; - - return [ - { - id: descriptor.column, - desc: descriptor.direction === "descending", - }, - ]; -}; diff --git a/src/hooks/table/index.ts b/src/hooks/table/index.ts index 61e6c5b5..07e48e26 100644 --- a/src/hooks/table/index.ts +++ b/src/hooks/table/index.ts @@ -1,35 +1,14 @@ -export { - useTableSorting, - type UseTableSortingOptions, - type UseTableSortingResult, - type HookSortDirection, - type HookSortDescriptor, -} from "./useTableSorting"; -export { - useTablePagination, - type UseTablePaginationOptions, - type UseTablePaginationResult, -} from "./useTablePagination"; -export { - useTableFiltering, - type UseTableFilteringOptions, - type UseTableFilteringResult, -} from "./useTableFiltering"; -export { - useTableSelection, - type UseTableSelectionOptions, - type UseTableSelectionResult, - type TableSelectionState, -} from "./useTableSelection"; -export { - useTableExpansion, - type UseTableExpansionOptions, - type UseTableExpansionResult, -} from "./useTableExpansion"; -export { - useTableModel, - type UseTableModelOptions, -} from "./useTableModel"; -export { toSortDescriptor, toSortingState } from "./helpers"; +/* + * What is left of `hooks/table` after TanStack. + * + * The sorting, filtering, pagination, selection, expansion and model hooks were + * a thin public shim over `@tanstack/solid-table`. Nothing inside this library + * used one, and `createDataGrid` answers the same questions natively — its rows + * derive `filteredRows -> sortedRows -> pageRows`, with selection alongside — so + * the shim was a second table engine kept alive only by its own exports. + * + * `useAnchoredOverlayPosition` stays because it never had anything to do with + * TanStack: it positions an overlay against a cell, which a grid still needs. + */ export { useAnchoredOverlayPosition } from "./useAnchoredOverlayPosition"; export type { UseAnchoredOverlayPositionOptions } from "./useAnchoredOverlayPosition"; diff --git a/src/hooks/table/useTableExpansion.ts b/src/hooks/table/useTableExpansion.ts deleted file mode 100644 index 71d055aa..00000000 --- a/src/hooks/table/useTableExpansion.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { ExpandedState, OnChangeFn } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export interface UseTableExpansionOptions { - expanded?: Accessor; - setExpanded?: OnChangeFn; - initialExpanded?: ExpandedState; -} - -export interface UseTableExpansionResult { - expanded: Accessor; - setExpanded: OnChangeFn; - collapseAll: () => void; -} - -export const useTableExpansion = ( - options: UseTableExpansionOptions = {}, -): UseTableExpansionResult => { - const [internalExpanded, setInternalExpanded] = createSignal( - options.initialExpanded ?? {}, - ); - - const expanded = () => options.expanded?.() ?? internalExpanded(); - - const setExpanded: OnChangeFn = (updater) => { - if (options.setExpanded) { - options.setExpanded(updater); - return; - } - setInternalExpanded((prev) => resolveUpdater(prev, updater)); - }; - - const collapseAll = () => setExpanded({}); - - return { - expanded, - setExpanded, - collapseAll, - }; -}; diff --git a/src/hooks/table/useTableFiltering.ts b/src/hooks/table/useTableFiltering.ts deleted file mode 100644 index b9c66888..00000000 --- a/src/hooks/table/useTableFiltering.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { createMemo, createSignal, type Accessor, type JSX } from "solid-js"; -import type { - ColumnFiltersState, - OnChangeFn, -} from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export interface UseTableFilteringOptions { - columnFilters?: Accessor; - setColumnFilters?: OnChangeFn; - initialColumnFilters?: ColumnFiltersState; - globalFilter?: Accessor; - setGlobalFilter?: OnChangeFn; - initialGlobalFilter?: string; -} - -export interface UseTableFilteringResult { - columnFilters: Accessor; - setColumnFilters: OnChangeFn; - globalFilter: Accessor; - setGlobalFilter: OnChangeFn; - getColumnFilterValue: (columnId: string) => string; - setColumnFilterValue: (columnId: string, value: string) => void; - getColumnFilterProps: ( - columnId: string, - options?: { - afterChange?: (value: string) => void; - }, - ) => { - readonly value: string; - onInput: JSX.EventHandler; - onChange: JSX.EventHandler< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, - Event - >; - }; - openFilterFor: Accessor; - closeFilter: () => void; - toggleFilter: (columnId: string) => void; - anyFilterActive: Accessor; -} - -export const useTableFiltering = ( - options: UseTableFilteringOptions = {}, -): UseTableFilteringResult => { - const [internalColumnFilters, setInternalColumnFilters] = - createSignal(options.initialColumnFilters ?? []); - const [internalGlobalFilter, setInternalGlobalFilter] = createSignal( - options.initialGlobalFilter ?? "", - ); - const [openFilterFor, setOpenFilterFor] = createSignal(null); - - const columnFilters = () => - options.columnFilters?.() ?? internalColumnFilters(); - const globalFilter = () => options.globalFilter?.() ?? internalGlobalFilter(); - - const setColumnFilters: OnChangeFn = (updater) => { - if (options.setColumnFilters) { - options.setColumnFilters(updater); - return; - } - setInternalColumnFilters((prev) => resolveUpdater(prev, updater)); - }; - - const setGlobalFilter: OnChangeFn = (updater) => { - if (options.setGlobalFilter) { - options.setGlobalFilter(updater); - return; - } - setInternalGlobalFilter((prev) => resolveUpdater(prev, updater)); - }; - - const closeFilter = () => setOpenFilterFor(null); - const toggleFilter = (columnId: string) => { - setOpenFilterFor((current) => (current === columnId ? null : columnId)); - }; - - const getColumnFilterValue = (columnId: string) => { - const filter = columnFilters().find((entry) => entry.id === columnId); - return typeof filter?.value === "string" ? filter.value : ""; - }; - - const setColumnFilterValue = (columnId: string, value: string) => { - setColumnFilters((previous) => { - const next = previous.filter((entry) => entry.id !== columnId); - if (value.trim().length > 0) { - next.push({ id: columnId, value }); - } - return next; - }); - }; - - const getColumnFilterProps = ( - columnId: string, - bindingOptions?: { - afterChange?: (value: string) => void; - }, - ) => { - const applyValue = (value: string) => { - setColumnFilterValue(columnId, value); - bindingOptions?.afterChange?.(value); - }; - - return { - get value() { - return getColumnFilterValue(columnId); - }, - onInput: (event: InputEvent & { - currentTarget: HTMLInputElement | HTMLTextAreaElement; - target: Element; - }) => { - applyValue(event.currentTarget.value); - }, - onChange: (event: Event & { - currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; - target: Element; - }) => { - applyValue(event.currentTarget.value); - }, - }; - }; - - const anyFilterActive = createMemo(() => columnFilters().length > 0); - - return { - columnFilters, - setColumnFilters, - globalFilter, - setGlobalFilter, - getColumnFilterValue, - setColumnFilterValue, - getColumnFilterProps, - openFilterFor, - closeFilter, - toggleFilter, - anyFilterActive, - }; -}; diff --git a/src/hooks/table/useTableModel.ts b/src/hooks/table/useTableModel.ts deleted file mode 100644 index 6e67742c..00000000 --- a/src/hooks/table/useTableModel.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Accessor } from "solid-js"; -import { - createSolidTable, - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - type ColumnDef, - type ColumnFiltersState, - type ExpandedState, - type OnChangeFn, - type PaginationState, - type Row, - type SortingState, - type Table, -} from "@tanstack/solid-table"; -import { asAccessor } from "./helpers"; -import type { TableSelectionState } from "./useTableSelection"; - -export interface UseTableModelOptions { - data: Accessor | TData[]; - columns: Accessor[]> | ColumnDef[]; - getRowId?: (originalRow: TData, index: number, parent?: Row) => string; - sorting?: Accessor; - setSorting?: OnChangeFn; - columnFilters?: Accessor; - setColumnFilters?: OnChangeFn; - pagination?: Accessor; - setPagination?: OnChangeFn; - globalFilter?: Accessor; - setGlobalFilter?: OnChangeFn; - rowSelection?: Accessor; - setRowSelection?: OnChangeFn; - expanded?: Accessor; - setExpanded?: OnChangeFn; - enableSorting?: boolean; - enableFilters?: boolean; - enablePagination?: boolean; - enableRowSelection?: boolean; - enableExpanding?: boolean; - getRowCanExpand?: (row: Row) => boolean; -} - -export const useTableModel = ( - options: UseTableModelOptions, -): Table => { - const dataAccessor = asAccessor(options.data); - const columnsAccessor = asAccessor(options.columns); - - return createSolidTable({ - get data() { - return dataAccessor(); - }, - get columns() { - return columnsAccessor(); - }, - getRowId: options.getRowId, - state: { - get sorting() { - return options.sorting?.() ?? []; - }, - get columnFilters() { - return options.columnFilters?.() ?? []; - }, - get pagination() { - return options.pagination?.() ?? { pageIndex: 0, pageSize: 10 }; - }, - get globalFilter() { - return options.globalFilter?.() ?? ""; - }, - get rowSelection() { - return options.rowSelection?.() ?? {}; - }, - get expanded() { - return options.expanded?.() ?? {}; - }, - }, - onSortingChange: options.setSorting, - onColumnFiltersChange: options.setColumnFilters, - onPaginationChange: options.setPagination, - onGlobalFilterChange: options.setGlobalFilter, - onRowSelectionChange: options.setRowSelection, - onExpandedChange: options.setExpanded, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getExpandedRowModel: getExpandedRowModel(), - enableSorting: options.enableSorting, - enableFilters: options.enableFilters, - manualPagination: - options.enablePagination === undefined ? false : !options.enablePagination, - enableRowSelection: options.enableRowSelection, - enableExpanding: options.enableExpanding, - getRowCanExpand: options.getRowCanExpand, - }); -}; diff --git a/src/hooks/table/useTablePagination.ts b/src/hooks/table/useTablePagination.ts deleted file mode 100644 index 30e1d55b..00000000 --- a/src/hooks/table/useTablePagination.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn, PaginationState } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const; - -export interface UseTablePaginationOptions { - pagination?: Accessor; - setPagination?: OnChangeFn; - initialPagination?: PaginationState; - pageSizeOptions?: readonly number[]; -} - -export interface UseTablePaginationResult { - pagination: Accessor; - setPagination: OnChangeFn; - pageSizeOptions: Accessor; - setPageIndex: (index: number) => void; - setPageSize: (size: number) => void; - firstPage: () => void; - previousPage: () => void; - nextPage: (maxPageIndex: number) => void; - lastPage: (maxPageIndex: number) => void; -} - -export const useTablePagination = ( - options: UseTablePaginationOptions = {}, -): UseTablePaginationResult => { - const [internalPagination, setInternalPagination] = createSignal( - options.initialPagination ?? { pageIndex: 0, pageSize: 10 }, - ); - - const pagination = () => options.pagination?.() ?? internalPagination(); - const pageSizeOptions = () => options.pageSizeOptions ?? DEFAULT_PAGE_SIZE_OPTIONS; - - const setPagination: OnChangeFn = (updater) => { - if (options.setPagination) { - options.setPagination(updater); - return; - } - setInternalPagination((prev) => resolveUpdater(prev, updater)); - }; - - const setPageIndex = (index: number) => { - setPagination((prev) => ({ ...prev, pageIndex: Math.max(0, index) })); - }; - - const setPageSize = (size: number) => { - setPagination((prev) => ({ ...prev, pageSize: size })); - }; - - const firstPage = () => setPageIndex(0); - const previousPage = () => setPageIndex(pagination().pageIndex - 1); - const nextPage = (maxPageIndex: number) => - setPageIndex(Math.min(maxPageIndex, pagination().pageIndex + 1)); - const lastPage = (maxPageIndex: number) => setPageIndex(maxPageIndex); - - return { - pagination, - setPagination, - pageSizeOptions, - setPageIndex, - setPageSize, - firstPage, - previousPage, - nextPage, - lastPage, - }; -}; diff --git a/src/hooks/table/useTableSelection.ts b/src/hooks/table/useTableSelection.ts deleted file mode 100644 index 175a38e5..00000000 --- a/src/hooks/table/useTableSelection.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn } from "@tanstack/solid-table"; -import { resolveUpdater } from "./helpers"; - -export type TableSelectionState = Record; - -export interface UseTableSelectionOptions { - rowSelection?: Accessor; - setRowSelection?: OnChangeFn; - initialRowSelection?: TableSelectionState; -} - -export interface UseTableSelectionResult { - rowSelection: Accessor; - setRowSelection: OnChangeFn; - clearSelection: () => void; -} - -export const useTableSelection = ( - options: UseTableSelectionOptions = {}, -): UseTableSelectionResult => { - const [internalRowSelection, setInternalRowSelection] = - createSignal(options.initialRowSelection ?? {}); - - const rowSelection = () => options.rowSelection?.() ?? internalRowSelection(); - - const setRowSelection: OnChangeFn = (updater) => { - if (options.setRowSelection) { - options.setRowSelection(updater); - return; - } - setInternalRowSelection((prev) => resolveUpdater(prev, updater)); - }; - - const clearSelection = () => setRowSelection({}); - - return { - rowSelection, - setRowSelection, - clearSelection, - }; -}; diff --git a/src/hooks/table/useTableSorting.ts b/src/hooks/table/useTableSorting.ts deleted file mode 100644 index 86a27eb3..00000000 --- a/src/hooks/table/useTableSorting.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createMemo, createSignal, type Accessor } from "solid-js"; -import type { OnChangeFn, SortingState } from "@tanstack/solid-table"; -import { resolveUpdater, toSortDescriptor, toSortingState } from "./helpers"; - -export type HookSortDirection = "ascending" | "descending"; - -export type HookSortDescriptor = { - column: string; - direction: HookSortDirection; -}; - -export interface UseTableSortingOptions { - sorting?: Accessor; - setSorting?: OnChangeFn; - initialSorting?: SortingState; -} - -export interface UseTableSortingResult { - sorting: Accessor; - setSorting: OnChangeFn; - sortDescriptor: Accessor; - setSortDescriptor: (descriptor: HookSortDescriptor) => void; -} - -export const useTableSorting = ( - options: UseTableSortingOptions = {}, -): UseTableSortingResult => { - const [internalSorting, setInternalSorting] = createSignal( - options.initialSorting ?? [], - ); - - const sorting = () => options.sorting?.() ?? internalSorting(); - - const setSorting: OnChangeFn = (updater) => { - if (options.setSorting) { - options.setSorting(updater); - return; - } - setInternalSorting((prev) => resolveUpdater(prev, updater)); - }; - - const sortDescriptor = createMemo(() => { - return toSortDescriptor(sorting()); - }); - - const setSortDescriptor = (descriptor: HookSortDescriptor) => { - setSorting(toSortingState(descriptor)); - }; - - return { - sorting, - setSorting, - sortDescriptor, - setSortDescriptor, - }; -}; diff --git a/src/index.ts b/src/index.ts index 974451a6..1da2bc4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -152,7 +152,7 @@ export type { FieldsetActionsProps, } from "./components/fieldset"; // --------------------------------------------------------------------------- -// Form components and hooks (TanStack-based API) +// Form components and hooks // --------------------------------------------------------------------------- export { default as Form, @@ -389,34 +389,8 @@ export type { InlineConfirmProps as TableInlineConfirmProps, InlineConfirmVariant as TableInlineConfirmVariant, } from "./components/table"; -export { - useTableModel, - useTableSorting, - useTablePagination, - useTableFiltering, - useTableSelection, - useTableExpansion, - toSortDescriptor, - toSortingState, - useAnchoredOverlayPosition, -} from "./hooks/table"; -export type { - UseTableModelOptions, - UseTableSortingOptions, - UseTableSortingResult, - HookSortDirection, - HookSortDescriptor, - UseTablePaginationOptions, - UseTablePaginationResult, - UseTableFilteringOptions, - UseTableFilteringResult, - UseTableSelectionOptions, - UseTableSelectionResult, - TableSelectionState, - UseTableExpansionOptions, - UseTableExpansionResult, - UseAnchoredOverlayPositionOptions, -} from "./hooks/table"; +export { useAnchoredOverlayPosition } from "./hooks/table"; +export type { UseAnchoredOverlayPositionOptions } from "./hooks/table"; export { default as Tabs } from "./components/tabs"; export type { @@ -513,12 +487,6 @@ export type { // Motion export * from "./motion"; -export { useVirtualRows } from "./primitives/virtualizer"; -export type { - UseVirtualRowsOptions, - UseVirtualRowsResult, - VirtualRowsRange, -} from "./primitives/virtualizer"; export { useStreamingBuffer, useStreamingSubscription, diff --git a/src/primitives/virtualizer/index.ts b/src/primitives/virtualizer/index.ts deleted file mode 100644 index 385935ce..00000000 --- a/src/primitives/virtualizer/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useVirtualRows } from "./useVirtualRows"; -export type { - UseVirtualRowsOptions, - UseVirtualRowsResult, - VirtualRowsRange, -} from "./useVirtualRows"; diff --git a/src/primitives/virtualizer/useVirtualRows.ts b/src/primitives/virtualizer/useVirtualRows.ts deleted file mode 100644 index 9579e1c3..00000000 --- a/src/primitives/virtualizer/useVirtualRows.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { createEffect, createMemo, type Accessor } from "solid-js"; -import { - createVirtualizer, - type PartialKeys, - type VirtualItem, - type Virtualizer, - type VirtualizerOptions, -} from "@tanstack/solid-virtual"; - -type OmittedVirtualizerOptions = - | "observeElementRect" - | "observeElementOffset" - | "scrollToFn"; - -export type UseVirtualRowsOptions< - TScrollElement extends Element, - TItemElement extends Element, -> = PartialKeys< - VirtualizerOptions, - OmittedVirtualizerOptions ->; - -export type VirtualRowsRange = { - startIndex: number; - endIndex: number; -}; - -export type UseVirtualRowsResult< - TScrollElement extends Element, - TItemElement extends Element, -> = { - virtualizer: Virtualizer; - virtualItems: Accessor; - totalSize: Accessor; - range: Accessor; - startIndex: Accessor; - endIndex: Accessor; -}; - -export const useVirtualRows = < - TScrollElement extends Element, - TItemElement extends Element, ->( - options: UseVirtualRowsOptions, -): UseVirtualRowsResult => { - const virtualizer = createVirtualizer(options); - - createEffect(() => { - const scrollElement = options.getScrollElement(); - if (!scrollElement) return; - virtualizer.measure(); - }); - - const virtualItems = () => virtualizer.getVirtualItems(); - const totalSize = () => virtualizer.getTotalSize(); - - const range = createMemo(() => { - const items = virtualItems(); - if (items.length === 0) { - return undefined; - } - - return { - startIndex: items[0].index, - endIndex: items[items.length - 1].index, - }; - }); - - const startIndex = createMemo(() => range()?.startIndex ?? -1); - const endIndex = createMemo(() => range()?.endIndex ?? -1); - - return { - virtualizer, - virtualItems, - totalSize, - range, - startIndex, - endIndex, - }; -}; diff --git a/tests/hooks/form/createForm.test.ts b/tests/hooks/form/createForm.test.ts index c06ae993..ca3b598a 100644 --- a/tests/hooks/form/createForm.test.ts +++ b/tests/hooks/form/createForm.test.ts @@ -50,19 +50,19 @@ const createPasswordForm = (onSubmit?: (value: PasswordValues) => void | Promise describe("createForm", () => { it("clears blur-origin errors after valid change", async () => { const { form, dispose } = createPasswordForm(); - const tsForm = form._tsForm; try { - tsForm.setFieldValue("password", "abc"); - await tsForm.validateField("password", "blur"); + form.setFieldValue("password", "abc"); + await form.validateField("password", "blur"); - expect(tsForm.state.canSubmit).toBeFalse(); - expect(tsForm.getFieldMeta("password")?.errorMap?.onBlur).toBeDefined(); + expect(form.isValid()).toBeFalse(); + expect(form.getFieldMeta("password").errors.length).toBeGreaterThan(0); + expect(form.getFieldMeta("password").isTouched).toBeTrue(); - tsForm.setFieldValue("password", "abcd"); + form.setFieldValue("password", "abcd"); - expect(tsForm.state.canSubmit).toBeTrue(); - expect(tsForm.getFieldMeta("password")?.errorMap?.onBlur).toBeUndefined(); + expect(form.isValid()).toBeTrue(); + expect(form.getFieldMeta("password").errors).toEqual([]); } finally { dispose(); } @@ -71,24 +71,42 @@ describe("createForm", () => { it("allows submit after correcting a blur-invalid value", async () => { const onSubmit = mock(async (_value: PasswordValues) => {}); const { form, dispose } = createPasswordForm(onSubmit); - const tsForm = form._tsForm; try { - tsForm.setFieldValue("password", "abc"); - await tsForm.validateField("password", "blur"); - await tsForm.handleSubmit(); + form.setFieldValue("password", "abc"); + await form.validateField("password", "blur"); + await form.submit(); - expect(tsForm.state.canSubmit).toBeFalse(); + expect(form.isValid()).toBeFalse(); expect(onSubmit).toHaveBeenCalledTimes(0); - tsForm.setFieldValue("password", "abcd"); - await tsForm.handleSubmit(); + form.setFieldValue("password", "abcd"); + await form.submit(); - expect(tsForm.state.canSubmit).toBeTrue(); + expect(form.isValid()).toBeTrue(); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0]?.[0]).toEqual({ password: "abcd" }); } finally { dispose(); } }); + + it("shows every error on a failed submit, not only the touched ones", async () => { + const onSubmit = mock(async (_value: PasswordValues) => {}); + const { form, dispose } = createPasswordForm(onSubmit); + + try { + // Nothing blurred, so nothing is touched and nothing displays yet. + expect(form.getFieldMeta("password").isTouched).toBeFalse(); + + form.setFieldValue("password", "abc"); + await form.submit(); + + // A submit that refuses has to say why, so it touches everything. + expect(form.getFieldMeta("password").isTouched).toBeTrue(); + expect(onSubmit).toHaveBeenCalledTimes(0); + } finally { + dispose(); + } + }); }); From 529cdb4830730e9535f9c89f07064fe517327fba Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 16 Aug 2026 15:04:49 +0700 Subject: [PATCH 04/15] wip(solid-2): the mechanical pass, 127 typecheck errors down to 2 Not finished. Committed as a checkpoint because the codemods are done and reviewable on their own, and the two that remain are judgement calls that should be read rather than scripted. Done, all verified against an installed solid-js@2.0.0-rc.0 rather than against the migration guide: splitProps -> omit 222 calls, 113 files. Every call had exactly one key list, so the four-bucket shape the toolchain had to solve does not occur here. `omit` returns only the rest, so each `local.x` became `props.x`, which is what it always meant. Context.Provider -> Context 34 sites. `Context extends ContextProviderComponent` in 2.0: the context is the provider. onMount -> onSettled 37 sites. `onCleanup` survives unchanged, so the seven bodies that nest one needed no rework. solid-js/web -> @solidjs/web 15 files. Only `Portal` and `Dynamic` are imported, and `Dynamic` kept its shape. JSX from @solidjs/web 129 files plus `jsxImportSource`. Core no longer declares the namespace; the shape of an element is the renderer's business now. This one change took 127 errors to 38. solid-js/store -> solid-js the store merged into core. store setters `setStore("items", produce(fn))` becomes `setStore(fn)`: `StoreSetter` takes one mutator over the whole store and `produce` is gone. Remaining: one `createSignal` overload resolving to `never` in hueShift, and one JSX prop mismatch in AnimatedCollapse. The 107 `useContext` sites are untouched and are the real work: 2.0 throws where 1.9 returned `undefined`, so every compound component that currently works standalone needs a decision rather than a rename. --- bun.lock | 21 +- package.json | 9 +- src/components/_shared/overlayPosition.ts | 9 +- src/components/accordion/Accordion.layout.tsx | 132 ++++----- src/components/address/Address.layout.tsx | 3 +- src/components/alert/Alert.layout.tsx | 4 +- src/components/auth-card/AuthCard.layout.tsx | 3 +- .../AuthFieldGroup.layout.tsx | 2 +- .../AuthFooterLinks.layout.tsx | 3 +- .../auth-message/AuthMessage.layout.tsx | 3 +- .../auth-powered-by/AuthPoweredBy.layout.tsx | 3 +- .../AuthSubmitButton.layout.tsx | 2 +- src/components/avatar/Avatar.layout.tsx | 82 ++---- src/components/avatar/AvatarGroup.layout.tsx | 18 +- src/components/badge/Badge.layout.tsx | 34 +-- .../breadcrumb/Breadcrumb.layout.tsx | 62 ++--- .../button-group/ButtonGroup.layout.tsx | 44 +-- src/components/button/Button.layout.tsx | 4 +- src/components/calendar/Calendar.layout.tsx | 66 ++--- src/components/card/Card.layout.tsx | 3 +- .../chatbubble/ChatBubble.layout.tsx | 18 +- .../chatbubble/ChatBubbleAvatar.layout.tsx | 2 +- .../chatbubble/ChatBubbleFooter.layout.tsx | 7 +- .../chatbubble/ChatBubbleHeader.layout.tsx | 7 +- .../chatbubble/ChatBubbleMessage.layout.tsx | 11 +- .../chatbubble/ChatBubbleTime.layout.tsx | 7 +- .../checkbox-group/CheckboxGroup.layout.tsx | 38 +-- src/components/checkbox/Checkbox.layout.tsx | 46 +-- src/components/chip/Chip.layout.tsx | 48 ++-- .../close-button/CloseButton.layout.tsx | 32 ++- .../collapsible/Collapsible.layout.tsx | 125 ++++----- .../color-area/ColorArea.layout.tsx | 22 +- .../color-field/ColorField.layout.tsx | 34 +-- .../color-picker/ColorPicker.layout.tsx | 110 +++----- .../color-slider/ColorSlider.layout.tsx | 28 +- .../ColorSwatchPicker.layout.tsx | 40 ++- .../color-swatch/ColorSwatch.layout.tsx | 48 ++-- .../ColorWheelFlower.layout.tsx | 19 +- src/components/combo-box/ComboBox.layout.tsx | 204 ++++++-------- src/components/composer/Composer.layout.tsx | 5 +- src/components/data-grid/DataGrid.layout.tsx | 100 +++---- src/components/data-grid/createDataGrid.ts | 74 +++-- src/components/data-grid/index.ts | 2 +- .../date-field/DateField.layout.tsx | 145 +++++----- .../date-picker/DatePicker.layout.tsx | 48 ++-- .../DateRangePicker.layout.tsx | 54 ++-- src/components/dialog/Dialog.layout.tsx | 246 +++++++--------- src/components/dock/Dock.layout.tsx | 111 ++++---- src/components/drawer/Drawer.layout.tsx | 263 ++++++++---------- src/components/dropdown/Dropdown.layout.tsx | 150 +++++----- src/components/empty/Empty.layout.tsx | 78 ++---- src/components/fieldset/Fieldset.layout.tsx | 63 ++--- src/components/flex/Flex.layout.tsx | 52 ++-- src/components/footer/Footer.layout.tsx | 24 +- src/components/footer/FooterTitle.layout.tsx | 15 +- .../form/FieldErrorMessage.layout.tsx | 3 +- src/components/form/Form.layout.tsx | 42 ++- src/components/form/FormField.layout.tsx | 35 +-- .../form/FormSubmitButton.layout.tsx | 8 +- src/components/glow-card/GlowCard.layout.tsx | 27 +- src/components/grid/Grid.layout.tsx | 32 +-- src/components/header/Header.layout.tsx | 18 +- src/components/icon/Icon.layout.tsx | 3 +- .../ImmersiveLanding.layout.tsx | 93 ++++--- .../ImmersiveLandingArrows.layout.tsx | 27 +- .../ImmersiveLandingNavigation.layout.tsx | 40 +-- .../ImmersiveLandingPage.layout.tsx | 17 +- .../components/CookieConsent.tsx | 4 +- .../components/FirefoxPWABanner.tsx | 4 +- .../components/PWAInstallPrompt.tsx | 4 +- src/components/immersive-landing/types.ts | 3 +- .../immersive-landing/useImmersiveLanding.ts | 4 +- src/components/input-otp/InputOTP.layout.tsx | 124 ++++----- src/components/input/Input.layout.tsx | 113 ++++---- src/components/join/Join.layout.tsx | 27 +- src/components/kbd/Kbd.layout.tsx | 56 ++-- src/components/label/Label.layout.tsx | 34 +-- .../LanguageSwitcher.layout.tsx | 33 +-- .../language-switcher/createI18n.tsx | 9 +- src/components/link/Link.layout.tsx | 49 ++-- src/components/list-box/ListBox.layout.tsx | 73 +++-- .../list-box/ListBoxItem.layout.tsx | 86 +++--- .../list-box/ListBoxSection.layout.tsx | 26 +- .../live-chat/LiveChatBubble.layout.tsx | 34 +-- .../live-chat/LiveChatPanel.layout.tsx | 55 ++-- src/components/menu/Menu.layout.tsx | 75 +++-- src/components/menu/MenuItem.layout.tsx | 105 +++---- src/components/menu/MenuSection.layout.tsx | 26 +- .../metal-border/MetalBorder.layout.tsx | 42 +-- src/components/meter/Meter.layout.tsx | 102 +++---- src/components/navbar/Navbar.layout.tsx | 25 +- src/components/navbar/NavbarRow.layout.tsx | 31 +-- .../navbar/NavbarSection.layout.tsx | 21 +- src/components/navbar/NavbarStack.layout.tsx | 29 +- .../NoiseBackground.layout.tsx | 36 +-- .../pagination/Pagination.layout.tsx | 24 +- .../PasswordField.interactions.ts | 2 +- .../password-field/PasswordField.layout.tsx | 3 +- .../PasswordRequirements.layout.tsx | 3 +- src/components/popover/Popover.layout.tsx | 153 ++++------ src/components/progress/Progress.layout.tsx | 44 +-- .../radial-progress/RadialProgress.layout.tsx | 40 +-- .../radio-group/RadioGroup.layout.tsx | 54 ++-- src/components/radio/Radio.layout.tsx | 40 +-- .../range-calendar/RangeCalendar.layout.tsx | 40 +-- .../scroll-area/ScrollArea.layout.tsx | 38 +-- src/components/select/Select.layout.tsx | 244 +++++++--------- src/components/separator/Separator.layout.tsx | 24 +- .../size-picker/SizePicker.layout.tsx | 15 +- src/components/skeleton/Skeleton.layout.tsx | 3 +- src/components/slider/Slider.layout.tsx | 64 ++--- src/components/spinner/Spinner.layout.tsx | 3 +- src/components/status/status.ts | 2 +- src/components/switch/Switch.layout.tsx | 40 +-- src/components/table/ExpandToggle.layout.tsx | 34 +-- src/components/table/InlineConfirm.layout.tsx | 31 ++- .../table/MobileListView.layout.tsx | 34 +-- src/components/table/SortIcon.layout.tsx | 26 +- src/components/table/Table.context.ts | 3 +- src/components/table/TableBody.layout.tsx | 11 +- src/components/table/TableCell.layout.tsx | 11 +- src/components/table/TableColumn.layout.tsx | 32 ++- .../table/TableColumnResizer.layout.tsx | 19 +- src/components/table/TableContent.layout.tsx | 29 +- .../table/TableExpandedRow.layout.tsx | 22 +- src/components/table/TableFooter.layout.tsx | 11 +- src/components/table/TableHeader.layout.tsx | 11 +- src/components/table/TableLoadMore.layout.tsx | 11 +- .../table/TableLoadMoreContent.layout.tsx | 11 +- src/components/table/TablePageSize.layout.tsx | 24 +- .../table/TableResizableContainer.layout.tsx | 11 +- src/components/table/TableRoot.layout.tsx | 22 +- src/components/table/TableRow.layout.tsx | 11 +- .../table/TableScrollContainer.layout.tsx | 11 +- .../table/VirtualSpacerRow.layout.tsx | 17 +- src/components/tabs/Tabs.layout.tsx | 123 ++++---- src/components/text/Text.layout.tsx | 30 +- src/components/textarea/Textarea.layout.tsx | 22 +- .../ThemeColorPicker.layout.tsx | 35 +-- src/components/theme-color-picker/hueShift.ts | 7 +- .../time-field/TimeField.layout.tsx | 145 +++++----- src/components/toast/Toast.layout.tsx | 173 +++++------- src/components/toolbar/Toolbar.layout.tsx | 26 +- src/components/tooltip/Tooltip.layout.tsx | 115 ++++---- src/components/types.ts | 2 +- src/components/utils.tsx | 4 +- .../video-preview/VideoPreview.layout.tsx | 24 +- src/components/vocabulary.ts | 2 +- src/hooks/date/usePickerOpenState.ts | 4 +- src/hooks/form/createForm.ts | 97 ++++--- src/hooks/layout/useDesktop.ts | 4 +- src/hooks/table/useAnchoredOverlayPosition.ts | 9 +- src/motion/solid/AnimatedCollapse.tsx | 10 +- src/motion/solid/MotionDiv.tsx | 35 +-- src/motion/solid/Presence.tsx | 10 +- tsconfig.json | 2 +- 156 files changed, 2930 insertions(+), 3535 deletions(-) diff --git a/bun.lock b/bun.lock index f2d1281a..0face4ae 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "@pathscale/ui", "dependencies": { + "@solidjs/web": "2.0.0-rc.0", "clsx": "^2.1.1", "tailwind-merge": "^3.6.0", }, @@ -25,10 +26,10 @@ "postcss": "^8.5.15", "postcss-cli": "^11.0.1", "postcss-selector-parser": "^7.1.1", - "rsbuild-plugin-solid-layouts": "^0.1.4", - "solid-js": "^1.9.14", - "solid-layouts": "^0.1.3", - "solid-layouts-oxc": "^0.1.7", + "rsbuild-plugin-solid-layouts": "0.2.0", + "solid-js": "2.0.0-rc.0", + "solid-layouts": "0.2.0", + "solid-layouts-oxc": "0.2.0", "svgo": "^3.3.3", "typescript": "^6.0.3", }, @@ -221,6 +222,10 @@ "@rspack/core": ["@rspack/core@2.0.5", "", { "dependencies": { "@rspack/binding": "2.0.5" }, "peerDependencies": { "@module-federation/runtime-tools": "^0.24.1 || ^2.0.0", "@swc/helpers": "^0.5.23" }, "optionalPeers": ["@module-federation/runtime-tools", "@swc/helpers"] }, "sha512-9tv2HAnSiTote5WPH2tmz1hLZ1zKbzkiZc1eYp7LP/8jcsiJBuf40ihiWidAgbbuYtJo3kWET6q+qOm5UhNiGA=="], + "@solidjs/signals": ["@solidjs/signals@2.0.0-rc.0", "", {}, "sha512-oKZSfvsCcKw1uJjOGbUkJ+OqlhXLHtZ+rShSyu9KH0lUH7UUwfMfsKeh81JPiQxDDg4YLhEwI38hg0JkwzTdvA=="], + + "@solidjs/web": ["@solidjs/web@2.0.0-rc.0", "", { "dependencies": { "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" }, "peerDependencies": { "solid-js": "^2.0.0-rc.0" } }, "sha512-pYSaA9+dH8H1h/d/ZF/P2kR6omfzFGNcdzKhWTcg9fJghXhn8+5UrXUr2iYxDdYNOXZzxxFQhYHSJ7P4HKDqgw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/core": ["@swc/core@1.15.40", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.40", "@swc/core-darwin-x64": "1.15.40", "@swc/core-linux-arm-gnueabihf": "1.15.40", "@swc/core-linux-arm64-gnu": "1.15.40", "@swc/core-linux-arm64-musl": "1.15.40", "@swc/core-linux-ppc64-gnu": "1.15.40", "@swc/core-linux-s390x-gnu": "1.15.40", "@swc/core-linux-x64-gnu": "1.15.40", "@swc/core-linux-x64-musl": "1.15.40", "@swc/core-win32-arm64-msvc": "1.15.40", "@swc/core-win32-ia32-msvc": "1.15.40", "@swc/core-win32-x64-msvc": "1.15.40" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg=="], @@ -543,7 +548,7 @@ "rsbuild-plugin-dts": ["rsbuild-plugin-dts@0.22.0", "", { "dependencies": { "@ast-grep/napi": "0.37.0" }, "peerDependencies": { "@microsoft/api-extractor": "^7", "@rsbuild/core": "^1.0.0 || ^2.0.0-0", "@typescript/native-preview": "7.x", "typescript": "^5 || ^6" }, "optionalPeers": ["@microsoft/api-extractor", "@typescript/native-preview", "typescript"] }, "sha512-Oyiu5oJDHWOnBv7yRr8eAGfoEddwxfPqUbAX6HvcJxrnR/h6Zd0VgTBWVTgqALYhLcL2ZdyZ2vwnqfhan4krwA=="], - "rsbuild-plugin-solid-layouts": ["rsbuild-plugin-solid-layouts@0.1.4", "", { "dependencies": { "solid-layouts-oxc": "0.1.7" }, "peerDependencies": { "@rsbuild/core": ">=1.3.0" } }, "sha512-6FgOrK2e9u8Bueb26AUVUAw5eE4zLMis8eA6dtF0tl+vByZEksQIUtkEHvNO17ki8sodoyOrMt+M1HFivNEZUg=="], + "rsbuild-plugin-solid-layouts": ["rsbuild-plugin-solid-layouts@0.2.0", "", { "dependencies": { "solid-layouts-oxc": "0.2.0" }, "peerDependencies": { "@rsbuild/core": ">=1.3.0" } }, "sha512-6QPrUrH08TkWdQzO7x2rMHQxbvyNfSSuj/cknHEX360Ke/a/4+nGluY7jdIGKH8YZiRhsfbZzCayDQmd1iFrRA=="], "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], @@ -555,11 +560,11 @@ "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - "solid-js": ["solid-js@1.9.14", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ=="], + "solid-js": ["solid-js@2.0.0-rc.0", "", { "dependencies": { "@solidjs/signals": "^2.0.0-rc.0", "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-3enTJ71VL69nM5p/it2InVBDBt316Cqfij+F0S7VuHZLITc8YV7Rvavjoy52nAKKxYWXM+BcXh2K7KcLTf1zdQ=="], - "solid-layouts": ["solid-layouts@0.1.3", "", { "peerDependencies": { "solid-js": "^1.9.14" } }, "sha512-SDlahgR/wbVA7x6M7Vgm1XqIShJ/1JeN0Kvbf4TZqj3CAZDjaTugkoJBogfg14+/Kfnh0BZ+86QFwjcXzMrpXA=="], + "solid-layouts": ["solid-layouts@0.2.0", "", { "peerDependencies": { "@solidjs/web": "^2.0.0-rc.0", "solid-js": "^1.9.14 || ^2.0.0-rc.0" }, "optionalPeers": ["@solidjs/web"] }, "sha512-O+tHZsxFp00g2rXtOPdDFwWgdGBKoG31z6l6RJyT1Zab5KPckIg5m6zBBMakzTdZqMLeSsA5N4FZ23gAYzJkOA=="], - "solid-layouts-oxc": ["solid-layouts-oxc@0.1.7", "", { "bin": { "solid-layouts-library": "bin/solid-layouts-library.js", "solid-layouts-application": "bin/solid-layouts-application.js", "solid-layouts-lint": "bin/solid-layouts-lint.js" } }, "sha512-1rVht4oKW6SnW8uHjXBVWUb3jr8GwuUH+23T3UCvqj+HaWfBlWtgseefTMWf9qlAVDjX5BGE2V/kNV4gHxcGpg=="], + "solid-layouts-oxc": ["solid-layouts-oxc@0.2.0", "", { "bin": { "solid-layouts-library": "bin/solid-layouts-library.js", "solid-layouts-application": "bin/solid-layouts-application.js", "solid-layouts-lint": "bin/solid-layouts-lint.js" } }, "sha512-BRz1TixmLoWyynFd6sqUon77lVOD1pJV1IO7CI8ocHUrQnpUywklrofPevR0DC8vMAtHYk50EBaxZaiaaWR43Q=="], "solid-refresh": ["solid-refresh@0.7.8", "", { "dependencies": { "@babel/generator": "^7.29.1", "@babel/types": "^7.29.0" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-iG442T3HXJp5jEebCy8okETrWnmX7CnxNOKrJQVeufh6WXSn+xtLXaptUXXMHTWcRqTJOJwQ8UwzxrRtVFSIzA=="], diff --git a/package.json b/package.json index 8ea74e12..34c2ff0a 100644 --- a/package.json +++ b/package.json @@ -81,14 +81,15 @@ "postcss": "^8.5.15", "postcss-cli": "^11.0.1", "postcss-selector-parser": "^7.1.1", - "rsbuild-plugin-solid-layouts": "^0.1.4", - "solid-js": "^1.9.14", - "solid-layouts": "^0.1.3", - "solid-layouts-oxc": "^0.1.7", + "rsbuild-plugin-solid-layouts": "0.2.0", + "solid-js": "2.0.0-rc.0", + "solid-layouts": "0.2.0", + "solid-layouts-oxc": "0.2.0", "svgo": "^3.3.3", "typescript": "^6.0.3" }, "dependencies": { + "@solidjs/web": "2.0.0-rc.0", "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, diff --git a/src/components/_shared/overlayPosition.ts b/src/components/_shared/overlayPosition.ts index aecb122d..db2f1b6e 100644 --- a/src/components/_shared/overlayPosition.ts +++ b/src/components/_shared/overlayPosition.ts @@ -1,10 +1,5 @@ -import { - createEffect, - createSignal, - onCleanup, - type Accessor, - type JSX, -} from "solid-js"; +import {createEffect, createSignal, onCleanup, type Accessor} from "solid-js"; +import type { JSX } from "@solidjs/web"; export type OverlayPlacement = "top" | "bottom" | "left" | "right"; export type OverlayAlign = "start" | "center" | "end"; diff --git a/src/components/accordion/Accordion.layout.tsx b/src/components/accordion/Accordion.layout.tsx index 7c231f7e..06f80563 100644 --- a/src/components/accordion/Accordion.layout.tsx +++ b/src/components/accordion/Accordion.layout.tsx @@ -1,16 +1,6 @@ import "./Accordion.css"; -import { - Show, - createContext, - createMemo, - createSignal, - createUniqueId, - splitProps, - useContext, - type Component, - type JSX, - type ParentComponent, -} from "solid-js"; +import type { JSX } from "@solidjs/web"; +import {Show, createContext, createMemo, createSignal, createUniqueId, omit, useContext, type Component, type ParentComponent} from "solid-js"; import { twMerge } from "tailwind-merge"; import type { UIBaseProps, State } from "../vocabulary"; @@ -113,7 +103,8 @@ export type AccordionIndicatorProps = Omit, }; const AccordionRoot: Layout = () => { - const [local, others] = splitProps(props, [ + const others = omit( + props, "children", "class", "dataTheme", @@ -127,20 +118,20 @@ const AccordionRoot: Layout = () => "state", "disabled", "ref", - ]); + ); - const selectionMode = () => local.selectionMode ?? "single"; - const variant = () => local.variant ?? "default"; - const hideSeparator = () => Boolean(local.hideSeparator); - const isDisabled = () => Boolean((local.state === "disabled")) || Boolean(local.disabled); + const selectionMode = () => props.selectionMode ?? "single"; + const variant = () => props.variant ?? "default"; + const hideSeparator = () => Boolean(props.hideSeparator); + const isDisabled = () => Boolean((props.state === "disabled")) || Boolean(props.disabled); const [internalValue, setInternalValue] = createSignal( - normalizeAccordionValue(local.defaultValue, selectionMode()), + normalizeAccordionValue(props.defaultValue, selectionMode()), ); const selectedValues = createMemo(() => - local.value !== undefined - ? normalizeAccordionValue(local.value, selectionMode()) + props.value !== undefined + ? normalizeAccordionValue(props.value, selectionMode()) : normalizeAccordionValue(internalValue(), selectionMode()), ); @@ -149,11 +140,11 @@ const AccordionRoot: Layout = () => const setSelectedValues = (nextValues: string[]) => { const normalizedNextValues = normalizeAccordionValue(nextValues, selectionMode()); - if (local.value === undefined) { + if (props.value === undefined) { setInternalValue(normalizedNextValues); } - local.onValueChange?.(normalizedNextValues); + props.onValueChange?.(normalizedNextValues); }; const toggleItem = (value: string) => { @@ -235,38 +226,39 @@ const AccordionRoot: Layout = () => }; return ( - +
{ rootRef = node; - if (typeof local.ref === "function") { - local.ref(node); + if (typeof props.ref === "function") { + props.ref(node); } }} {...{ class: twMerge( CLASSES.Root.base, CLASSES.Root.variant[variant()], - local.class, + props.class, ) }} data-slot="accordion" data-selection-mode={selectionMode()} data-hide-separator={hideSeparator() ? "true" : undefined} data-variant={variant()} data-disabled={isDisabled() ? "true" : "false"} - data-theme={local.dataTheme} - style={local.style} + data-theme={props.dataTheme} + style={props.style} aria-disabled={isDisabled() ? "true" : undefined} > - {local.children} + {props.children}
-
+ ); }; const AccordionItem: Layout = () => { const accordion = useContext(AccordionContext); - const [local, others] = splitProps(props, [ + const others = omit( + props, "value", "children", "class", @@ -274,18 +266,18 @@ const AccordionItem: Layout = () => "style", "state", "disabled", - ]); + ); const uniqueId = createUniqueId(); - const itemValue = () => local.value ?? uniqueId; + const itemValue = () => props.value ?? uniqueId; const triggerId = () => `accordion-trigger-${uniqueId}`; const contentId = () => `accordion-content-${uniqueId}`; const isExpanded = () => accordion?.isItemExpanded(itemValue()) ?? false; const isDisabled = () => - Boolean((local.state === "disabled")) || - Boolean(local.disabled) || + Boolean((props.state === "disabled")) || + Boolean(props.disabled) || Boolean(accordion?.isDisabled()); const toggle = () => { @@ -303,7 +295,7 @@ const AccordionItem: Layout = () => }; return ( - +
= () => isExpanded() && CLASSES.Item.flag.expanded, isDisabled() && CLASSES.Item.flag.disabled, accordion?.hideSeparator() && CLASSES.Item.flag.hideSeparator, - local.class, + props.class, ) }} data-slot="accordion-item" data-expanded={isExpanded() ? "true" : "false"} data-disabled={isDisabled() ? "true" : "false"} - data-theme={local.dataTheme} - style={local.style} + data-theme={props.dataTheme} + style={props.style} > - {local.children} + {props.children}
-
+ ); }; @@ -329,7 +321,8 @@ const AccordionTrigger: Layout = const accordion = useContext(AccordionContext); const item = useContext(AccordionItemContext); - const [local, others] = splitProps(props, [ + const others = omit( + props, "children", "class", "dataTheme", @@ -339,18 +332,18 @@ const AccordionTrigger: Layout = "type", "showIndicator", "indicator", - ]); + ); const isDisabled = () => Boolean(item?.isDisabled()); const handleClick: JSX.EventHandlerUnion = (event) => { - invokeEventHandler(local.onClick, event); + invokeEventHandler(props.onClick, event); if (event.defaultPrevented || isDisabled()) return; item?.toggle(); }; const handleKeyDown: JSX.EventHandlerUnion = (event) => { - invokeEventHandler(local.onKeyDown, event); + invokeEventHandler(props.onKeyDown, event); if (event.defaultPrevented || isDisabled()) return; if (event.key === "Enter" || event.key === " ") { @@ -366,12 +359,12 @@ const AccordionTrigger: Layout = ); @@ -389,16 +382,10 @@ const AccordionTrigger: Layout = const AccordionContent: Layout = () => { const item = useContext(AccordionItemContext); - const [local, others] = splitProps(props, [ - "children", - "class", - "dataTheme", - "style", - "keepMounted", - ]); + const others = omit(props, "children", "class", "dataTheme", "style", "keepMounted"); const expanded = () => Boolean(item?.isExpanded()); - const keepMounted = () => local.keepMounted ?? true; + const keepMounted = () => props.keepMounted ?? true; if (!keepMounted() && !expanded()) { return null; @@ -412,18 +399,18 @@ const AccordionContent: Layout = {...{ class: twMerge( CLASSES.Content.base, expanded() && CLASSES.Content.flag.expanded, - local.class, + props.class, ) }} data-slot="accordion-content" data-expanded={expanded() ? "true" : "false"} - data-theme={local.dataTheme} - style={local.style} + data-theme={props.dataTheme} + style={props.style} aria-hidden={expanded() ? "false" : "true"} aria-labelledby={item?.triggerId()} >
- {local.children} + {props.children}
@@ -432,12 +419,7 @@ const AccordionContent: Layout = const AccordionIndicator: Layout = () => { const item = useContext(AccordionItemContext); - const [local, others] = splitProps(props, [ - "children", - "class", - "dataTheme", - "style", - ]); + const others = omit(props, "children", "class", "dataTheme", "style"); const expanded = () => Boolean(item?.isExpanded()); @@ -448,14 +430,14 @@ const AccordionIndicator: Layout - {local.children ?? ( + {props.children ?? ( , "chi * Avatar Root * -----------------------------------------------------------------------------------------------*/ const AvatarRoot: Layout = () => { - const [local, others] = splitProps(props, [ + const others = omit( + props, "children", "class", "size", @@ -70,12 +62,12 @@ const AvatarRoot: Layout = () => { "variant", "dataTheme", "style", - ]); + ); const [imageLoaded, setImageLoaded] = createSignal(false); - const size = () => local.size ?? "md"; - const flavor = () => local.flavor ?? "neutral"; - const variant = () => local.variant ?? "default"; + const size = () => props.size ?? "md"; + const flavor = () => props.flavor ?? "neutral"; + const variant = () => props.variant ?? "default"; const ctx: AvatarContextValue = { size, @@ -86,22 +78,22 @@ const AvatarRoot: Layout = () => { }; return ( - + - {local.children} + {props.children} - + ); }; @@ -109,36 +101,28 @@ const AvatarRoot: Layout = () => { * Avatar Image * -----------------------------------------------------------------------------------------------*/ const AvatarImage: Layout = () => { - const [local, others] = splitProps(props, [ - "class", - "dataTheme", - "style", - "src", - "alt", - "onLoad", - "onError", - ]); + const others = omit(props, "class", "dataTheme", "style", "src", "alt", "onLoad", "onError"); const ctx = useAvatarContext(); const handleLoad: JSX.EventHandlerUnion = (e) => { ctx.setImageLoaded(true); - if (typeof local.onLoad === "function") local.onLoad(e); + if (typeof props.onLoad === "function") props.onLoad(e); }; const handleError = (e: Event & { currentTarget: HTMLImageElement }) => { ctx.setImageLoaded(false); - if (typeof local.onError === "function") (local.onError as (e: Event) => void)(e); + if (typeof props.onError === "function") (props.onError as (e: Event) => void)(e); }; return ( {local.alt} @@ -149,20 +133,14 @@ const AvatarImage: Layout = () => { * Avatar Fallback * -----------------------------------------------------------------------------------------------*/ const AvatarFallback: Layout = () => { - const [local, others] = splitProps(props, [ - "children", - "class", - "dataTheme", - "style", - "delayMs", - ]); + const others = omit(props, "children", "class", "dataTheme", "style", "delayMs"); const ctx = useAvatarContext(); - const [showFallback, setShowFallback] = createSignal(!local.delayMs); + const [showFallback, setShowFallback] = createSignal(!props.delayMs); - onMount(() => { - if (local.delayMs) { - const timer = setTimeout(() => setShowFallback(true), local.delayMs); + onSettled(() => { + if (props.delayMs) { + const timer = setTimeout(() => setShowFallback(true), props.delayMs); return () => clearTimeout(timer); } }); @@ -174,13 +152,13 @@ const AvatarFallback: Layout = () = {...{ class: twMerge( CLASSES.slot.fallback, (CLASSES.flavor[ctx.flavor() as keyof typeof CLASSES.flavor] ?? `avatar__fallback--flavor-${ctx.flavor()}`), - local.class, + props.class, ) }} data-slot="avatar-fallback" - data-theme={local.dataTheme} - style={local.style} + data-theme={props.dataTheme} + style={props.style} > - {local.children} + {props.children} ); diff --git a/src/components/avatar/AvatarGroup.layout.tsx b/src/components/avatar/AvatarGroup.layout.tsx index 4bb3de06..42d0c2aa 100644 --- a/src/components/avatar/AvatarGroup.layout.tsx +++ b/src/components/avatar/AvatarGroup.layout.tsx @@ -1,5 +1,5 @@ -import { type JSX, splitProps } from "solid-js"; -import { Dynamic } from "solid-js/web"; +import {omit} from "solid-js"; +import { Dynamic, type JSX} from "@solidjs/web"; import { twMerge } from "tailwind-merge"; import type { UIBaseProps } from "../vocabulary"; @@ -44,17 +44,19 @@ const VoidElementList: ElementType[] = [ ]; const AvatarGroup: Layout = () => { - const [local, others] = splitProps( + const others = omit( props as AvatarGroupBaseProps & Record, - ["as", "class", "children"], + "as", + "class", + "children", ); - const Tag = local.as || "div"; + const Tag = props.as || "div"; const classes = () => - twMerge(CLASSES.group.base, CLASSES.group.overlap, local.class); + twMerge(CLASSES.group.base, CLASSES.group.overlap, props.class); // Build an aria-label like "Group of N avatar photos" - const ariaLabel = `Group of ${local.children.length} avatar photos`; + const ariaLabel = `Group of ${props.children.length} avatar photos`; if (VoidElementList.includes(Tag)) { return ( @@ -74,7 +76,7 @@ const AvatarGroup: Layout = () => { aria-label={ariaLabel} {...{ class: classes() }} > - {local.children} + {props.children} ); }; diff --git a/src/components/badge/Badge.layout.tsx b/src/components/badge/Badge.layout.tsx index ce4cc69a..444bcf69 100644 --- a/src/components/badge/Badge.layout.tsx +++ b/src/components/badge/Badge.layout.tsx @@ -1,5 +1,6 @@ import { clsx } from "clsx"; -import { type JSX, splitProps } from "solid-js"; +import type { JSX } from "@solidjs/web"; +import {omit} from "solid-js"; import { twMerge } from "tailwind-merge"; import "./Badge.css"; @@ -17,15 +18,15 @@ interface BadgeAnchorProps extends JSX.HTMLAttributes { } const BadgeAnchor: Layout = () => { - const [local, others] = splitProps(props, ["children", "class"]); + const others = omit(props, "children", "class"); return ( - {local.children} + {props.children} ); }; @@ -47,7 +48,8 @@ interface BadgeRootProps extends Omit, "colo } const BadgeRoot: Layout = () => { - const [local, others] = splitProps(props, [ + const others = omit( + props, "children", "class", "flavor", @@ -55,18 +57,18 @@ const BadgeRoot: Layout = () => { "state", "size", "placement", - ]); + ); const classes = () => { - const size = local.size ?? "md"; + const size = props.size ?? "md"; /* Flavor is open, so a name the library does not ship still produces a class an app can style, rather than silently rendering unflavoured. */ - const flavor = local.flavor ?? "neutral"; + const flavor = props.flavor ?? "neutral"; const flavorClass = CLASSES.flavor[flavor as keyof typeof CLASSES.flavor] ?? `badge--flavor-${flavor}`; - const variant = local.variant ?? "solid"; - const state = resolveState(local.state); - const placement = local.placement ?? "top-right"; + const variant = props.variant ?? "solid"; + const state = resolveState(props.state); + const placement = props.placement ?? "top-right"; return twMerge( clsx( @@ -76,13 +78,13 @@ const BadgeRoot: Layout = () => { CLASSES.variant[variant], CLASSES.state[state], CLASSES.placement[placement], - local.class, + props.class, ), ); }; const badgeChildren = () => { - const c = local.children; + const c = props.children; if (typeof c === "string" || typeof c === "number") { return {c}; } @@ -104,15 +106,15 @@ interface BadgeLabelProps extends JSX.HTMLAttributes { } const BadgeLabel: Layout = () => { - const [local, others] = splitProps(props, ["children", "class"]); + const others = omit(props, "children", "class"); return ( - {local.children} + {props.children} ); }; diff --git a/src/components/breadcrumb/Breadcrumb.layout.tsx b/src/components/breadcrumb/Breadcrumb.layout.tsx index 7744f670..d786b882 100644 --- a/src/components/breadcrumb/Breadcrumb.layout.tsx +++ b/src/components/breadcrumb/Breadcrumb.layout.tsx @@ -1,13 +1,6 @@ import "./Breadcrumb.css"; -import { - createContext, - splitProps, - useContext, - Show, - type Component, - type JSX, - type ParentComponent, -} from "solid-js"; +import type { JSX } from "@solidjs/web"; +import {createContext, omit, useContext, Show, type Component, type ParentComponent} from "solid-js"; import { twMerge } from "tailwind-merge"; import type { UIBaseProps } from "../vocabulary"; import { CLASSES } from "./Breadcrumb.recipe"; @@ -72,31 +65,25 @@ const ChevronRight = () => ( * Breadcrumb Root * -----------------------------------------------------------------------------------------------*/ const BreadcrumbRoot: Layout = () => { - const [local, others] = splitProps(props, [ - "children", - "class", - "separator", - "dataTheme", - "style", - ]); + const others = omit(props, "children", "class", "separator", "dataTheme", "style"); const ctx: BreadcrumbContextValue = { - separator: () => local.separator, + separator: () => props.separator, }; return ( - + - + ); }; @@ -106,47 +93,40 @@ const BreadcrumbRoot: Layout = () = const [ITEM_CLASS, LINK_CLASS, SEPARATOR_CLASS] = CLASSES.Item.base; const BreadcrumbItem: Layout = () => { - const [local, others] = splitProps(props, [ - "children", - "class", - "href", - "isCurrent", - "dataTheme", - "style", - ]); + const others = omit(props, "children", "class", "href", "isCurrent", "dataTheme", "style"); const ctx = useBreadcrumbContext(); return (
  • - {local.children} + {props.children} } > - {local.children} + {props.children} - + {ctx.separator() ?? } diff --git a/src/components/button-group/ButtonGroup.layout.tsx b/src/components/button-group/ButtonGroup.layout.tsx index c92c93a6..de2a1cd7 100644 --- a/src/components/button-group/ButtonGroup.layout.tsx +++ b/src/components/button-group/ButtonGroup.layout.tsx @@ -1,5 +1,6 @@ import "./ButtonGroup.css"; -import { splitProps, type Component, type JSX, type ParentComponent } from "solid-js"; +import type { JSX } from "@solidjs/web"; +import {omit, type Component, type ParentComponent} from "solid-js"; import { twMerge } from "tailwind-merge"; import { CLASSES } from "./ButtonGroup.recipe"; @@ -24,7 +25,8 @@ export type ButtonGroupSeparatorProps = Omit UIBaseProps; const ButtonGroupRoot: Layout = () => { - const [local, others] = splitProps(props, [ + const others = omit( + props, "children", "class", "dataTheme", @@ -35,17 +37,17 @@ const ButtonGroupRoot: Layout = () "state", "fullWidth", "role", - ]); + ); - const orientation = () => local.orientation ?? "horizontal"; + const orientation = () => props.orientation ?? "horizontal"; return ( - local.size, - variant: () => local.variant, - isDisabled: () => (local.state === "disabled"), - fullWidth: () => local.fullWidth, + size: () => props.size, + variant: () => props.variant, + isDisabled: () => (props.state === "disabled"), + fullWidth: () => props.fullWidth, }} >
    = () {...{ class: twMerge( CLASSES.Root.base, CLASSES.Root.orientation[orientation()], - local.fullWidth && CLASSES.Root.flag.fullWidth, - local.class, + props.fullWidth && CLASSES.Root.flag.fullWidth, + props.class, ) }} data-slot="button-group" data-orientation={orientation()} - data-theme={local.dataTheme} - style={local.style} - role={local.role ?? "group"} - aria-disabled={(local.state === "disabled") ? "true" : undefined} + data-theme={props.dataTheme} + style={props.style} + role={props.role ?? "group"} + aria-disabled={(props.state === "disabled") ? "true" : undefined} > - {local.children} + {props.children}
    -
    + ); }; const ButtonGroupSeparator: Layout = () => { - const [local, others] = splitProps(props, ["class", "dataTheme", "style"]); + const others = omit(props, "class", "dataTheme", "style"); return (