From f4ba527db7ae8cbdc281adfdc37dad3357bcce2f Mon Sep 17 00:00:00 2001 From: meh Date: Tue, 8 Sep 2026 01:11:52 +0700 Subject: [PATCH 01/32] chore(deps): take @pathscale/ui 4.0.0 4.0.0, not 3.0.0. 3.0.0 predates the value-change callback contract and still defaults Input to md, so a converted onChange={(checked) => ...} would receive an Event there. An Event is always truthy, which latches every toggle on permanently, and it typechecks, so nothing catches it. The resolved peer set is the corroboration: no @tanstack/solid-form and no @tanstack/solid-table. --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index bdb8b72..20a1941 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "@iconify-json/lucide": "^1.2.127", "@iconify-json/mdi": "^1.2.3", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "^2.11.10", + "@pathscale/ui": "^4.0.0", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@solidjs/router": "2.0.0-next.19", @@ -192,7 +192,7 @@ "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.13.1", "", { "dependencies": { "@module-federation/runtime": "0.13.1", "@module-federation/sdk": "0.13.1" } }, "sha512-QSuSIGa09S8mthbB1L6xERqrz+AzPlHR6D7RwAzssAc+IHf40U6NiTLPzUqp9mmKDhC5Tm0EISU0ZHNeJpnpBQ=="], - "@pathscale/ui": ["@pathscale/ui@2.11.10", "", { "dependencies": { "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@solidjs/web": ">=2.0.0-rc.0", "@standard-schema/spec": "^1.0.0", "popmotion": "^11.0.5", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.0" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-3h03mALgWQse1Ebr6CusnYOz/CWGRHIxMcMdM3K7DoBdAq5VKtXdkIF7BQoOlHBnLv/l2grNdaQz2M3oLHpG0A=="], + "@pathscale/ui": ["@pathscale/ui@4.0.0", "", { "dependencies": { "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@solidjs/web": ">=2.0.0-rc.0", "@standard-schema/spec": "^1.0.0", "popmotion": "^11.0.5", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.0" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-+nesq+UWopoN9CjXKtc3t+uNJaCigFpQdfXVZPJBWRheyMZLV0NvdBFThNlWcIokSYSwISJIyHyOMvGANigv9A=="], "@rsbuild/core": ["@rsbuild/core@1.3.20", "", { "dependencies": { "@rspack/core": "1.3.10", "@rspack/lite-tapable": "~1.0.1", "@swc/helpers": "^0.5.17", "core-js": "~3.42.0", "jiti": "^2.4.2" }, "bin": { "rsbuild": "bin/rsbuild.js" } }, "sha512-5VxOddgGHaq5x4ONdKOZvLYLj8dhVfCAz+cERNLXrKLzBISouY1A9TJcbZBK4xoH/0DJrDtDzapNA+dI9Jr07Q=="], diff --git a/package.json b/package.json index ad2ce92..4fad42b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@iconify-json/lucide": "^1.2.127", "@iconify-json/mdi": "^1.2.3", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "^2.11.10", + "@pathscale/ui": "^4.0.0", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@solidjs/router": "2.0.0-next.19", From 0b0ff85f8c19f65e8dd98c3d655a0c76c0f911c5 Mon Sep 17 00:00:00 2001 From: meh Date: Tue, 8 Sep 2026 01:12:00 +0700 Subject: [PATCH 02/32] fix(build): resolve solid-layouts to its Solid 2 arm solid-layouts ships one build arm per Solid major and its bare entry is the 1.9 one, whose renderer calls splitProps. Solid 2 replaced that with omit, and the bundler links both arms of the package's runtime check, so the default entry fails at link time on a missing createComponent export. The site documented these aliases on its own installation page without applying them to its own config. Matches support.cafe, which is the known-good reference. --- rsbuild.config.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 6924a9d..3b144cf 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -23,8 +23,24 @@ export default defineConfig({ }, }), ], + resolve: { + alias: { + "~": "./src", + /* + * solid-layouts ships one build arm per Solid major and its bare entry is + * the 1.9 one, whose renderer calls `splitProps`. Solid 2 replaced that + * with `omit`, and a bundler links both arms of the package's runtime + * check, so the default entry fails at link time with "export + * 'createComponent' was not found in './renderer.js'". `./solid-2` is the + * arm built against this major; these aliases are how a consumer picks it. + */ + "solid-layouts/recipe": "solid-layouts/solid-2/recipe", + "solid-layouts/cx": "solid-layouts/solid-2/cx", + "solid-layouts/application-boundary": "solid-layouts/solid-2/application-boundary", + "solid-layouts$": "solid-layouts/solid-2", + }, + }, source: { - alias: { "~": "./src" }, define: { "import.meta.env.VERSION": JSON.stringify( process.env.GITHUB_RUN_NUMBER || "0.0.1" From 36e5bc2869cf4dc1e42b9ce047f0a10fb5be2771 Mon Sep 17 00:00:00 2001 From: meh Date: Tue, 8 Sep 2026 01:12:15 +0700 Subject: [PATCH 03/32] refactor(showcases): convert to the 4.0 component API Switch, Checkbox and Radio report their new value from onChange rather than the native Event, and each grows a controlled demo so the contract is visible rather than only described. The event is still reachable as onNativeChange, which is where preventDefault belongs. ThemeEditor's five switches are the real call sites and move with them. Also: - Input, Select and Textarea documented a width prop taking the Width vocabulary. They take fullWidth, a boolean; width="full" is a Button prop and is silently dropped on a field. - Button's flavor row advertised tertiary, which the recipe does not style. Flavor is an open union, so it typechecks and renders unstyled. Shown as accent, which is the flavor that actually fills that slot. - The state axis replaces isDisabled, isLoading and isInvalid, which could previously disagree with each other. - className is gone from UIBaseProps; the props tables say so once instead of listing class twice. --- src/components/AccordionShowcase.tsx | 5 --- src/components/AlertShowcase.tsx | 5 --- src/components/BadgeShowcase.tsx | 3 +- src/components/BreadcrumbShowcase.tsx | 5 --- src/components/ButtonShowcase.tsx | 2 +- src/components/CardShowcase.tsx | 5 --- src/components/ChatBubbleShowcase.tsx | 5 --- src/components/CheckboxShowcase.tsx | 49 +++++++++++++++++++++++ src/components/ComponentVariants.tsx | 2 +- src/components/CoverageShowcase.tsx | 2 +- src/components/DialogShowcase.tsx | 5 --- src/components/FieldsetShowcase.tsx | 5 --- src/components/FooterShowcase.tsx | 10 ----- src/components/FormShowcase.tsx | 1 - src/components/IconShowcase.tsx | 4 +- src/components/InputShowcase.tsx | 15 +++---- src/components/PaginationShowcase.tsx | 5 --- src/components/RadioShowcase.tsx | 55 +++++++++++++++++++++++++- src/components/SelectShowcase.tsx | 7 ++-- src/components/SpinnerShowcase.tsx | 5 --- src/components/SwitchShowcase.tsx | 41 ++++++++++++++++++- src/components/TextareaShowcase.tsx | 7 ++-- src/components/ToastShowcase.tsx | 5 --- src/components/layout/Header/types.ts | 1 - src/components/theming/ThemeEditor.tsx | 10 ++--- 25 files changed, 170 insertions(+), 89 deletions(-) diff --git a/src/components/AccordionShowcase.tsx b/src/components/AccordionShowcase.tsx index 09bbcd5..756d7b5 100644 --- a/src/components/AccordionShowcase.tsx +++ b/src/components/AccordionShowcase.tsx @@ -56,11 +56,6 @@ export default function AccordionShowcase() { default: "false", description: "What is happening to the component. Replaces isDisabled, isLoading and isInvalid, which could disagree.", }, - { - name: "class", - type: "string", - description: "Additional CSS classes to apply", - }, { name: "class", type: `string`, diff --git a/src/components/AlertShowcase.tsx b/src/components/AlertShowcase.tsx index 097115a..9edc4f6 100644 --- a/src/components/AlertShowcase.tsx +++ b/src/components/AlertShowcase.tsx @@ -28,11 +28,6 @@ const AlertShowcase: Component = () => { type: "JSX.Element", description: "Flat in 2.3: title, icon and onDismiss are props, and the body is the children", }, - { - name: "class", - type: "string", - description: "Additional CSS classes to apply", - }, { name: "class", type: `string`, diff --git a/src/components/BadgeShowcase.tsx b/src/components/BadgeShowcase.tsx index 27386af..21380ab 100644 --- a/src/components/BadgeShowcase.tsx +++ b/src/components/BadgeShowcase.tsx @@ -7,7 +7,8 @@ import { ShowcaseSection } from "./showcase/ShowcaseSection"; const badgeProps = [ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Indicator size." }, { name: "flavor", type: "Flavor", default: '"neutral"', description: "What the badge is. Open: your own name yields a class you can style." }, - { name: "variant", type: '"primary" | "secondary" | "soft"', default: '"primary"', description: "Indicator treatment." }, + { name: "variant", type: '"solid" | "soft" | "outline"', default: '"solid"', description: "Indicator treatment. This is shape only: primary is a flavor, not a variant." }, + { name: "state", type: `"default" | "loading" | "error" | "invalid" | "disabled" | "hidden"`, default: '"default"', description: "What is happening to the component. Replaces isDisabled, isLoading and isInvalid, which could disagree." }, { name: "placement", type: '"top-right" | "top-left" | "bottom-right" | "bottom-left"', default: '"top-right"', description: "Position relative to Badge.Anchor." }, ]; diff --git a/src/components/BreadcrumbShowcase.tsx b/src/components/BreadcrumbShowcase.tsx index b67e07b..7567f30 100644 --- a/src/components/BreadcrumbShowcase.tsx +++ b/src/components/BreadcrumbShowcase.tsx @@ -14,11 +14,6 @@ export default function BreadcrumbShowcase() { ] as const; const breadcrumbsProps = [ - { - name: "class", - type: "string", - description: "Additional CSS classes to apply", - }, { name: "class", type: `string`, diff --git a/src/components/ButtonShowcase.tsx b/src/components/ButtonShowcase.tsx index a4352f7..aa673bd 100644 --- a/src/components/ButtonShowcase.tsx +++ b/src/components/ButtonShowcase.tsx @@ -26,7 +26,7 @@ export default function ButtonShowcase() { - + diff --git a/src/components/CardShowcase.tsx b/src/components/CardShowcase.tsx index 0071a7c..fdb7ee8 100644 --- a/src/components/CardShowcase.tsx +++ b/src/components/CardShowcase.tsx @@ -47,11 +47,6 @@ export default function CardShowcase() { type: "string", description: "Theme data attribute value", }, - { - name: "class", - type: "string", - description: "Additional CSS classes to apply", - }, { name: "class", type: `string`, diff --git a/src/components/ChatBubbleShowcase.tsx b/src/components/ChatBubbleShowcase.tsx index 3a7be7f..474fde7 100644 --- a/src/components/ChatBubbleShowcase.tsx +++ b/src/components/ChatBubbleShowcase.tsx @@ -28,11 +28,6 @@ export default function ChatBubbleShowcase() { type: "string", description: "Theme data attribute value", }, - { - name: "class", - type: "string", - description: "Additional CSS classes to apply", - }, { name: "class", type: `string`, diff --git a/src/components/CheckboxShowcase.tsx b/src/components/CheckboxShowcase.tsx index 3c19685..559e80c 100644 --- a/src/components/CheckboxShowcase.tsx +++ b/src/components/CheckboxShowcase.tsx @@ -1,3 +1,4 @@ +import { createSignal } from "solid-js"; import { Checkbox, Flex } from "@pathscale/ui"; import ShowcaseLayout from "./ShowcaseLayout"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; @@ -5,8 +6,11 @@ import { CodeBlock } from "./showcase/CodeBlock"; import { PropsTable } from "./showcase/PropsTable"; export default function CheckboxShowcase() { + const [accepted, setAccepted] = createSignal(false); + const sections = [ { id: "default", title: "Default" }, + { id: "controlled", title: "Controlled" }, { id: "indeterminate", title: "Indeterminate" }, { id: "form-control", title: "Form Control" }, { id: "variants", title: "Variants" }, @@ -19,6 +23,30 @@ export default function CheckboxShowcase() { type: "boolean", description: "Controls the checked state", }, + { + name: "onChange", + type: "(checked: boolean) => void", + description: + "Reports the new checked state. It is the value, not the event. A checkbox inside a CheckboxGroup fires this as well as the group's own onChange.", + }, + { + name: "onNativeChange", + type: "JSX.EventHandlerUnion", + description: + "The underlying input event, and still where preventDefault() belongs. Vetoing here also suppresses onChange.", + }, + { + name: "issues", + type: "Issue[]", + description: + 'Validation results. Validity is derived from them, not asserted; force it with state="invalid".', + }, + { + name: "state", + type: `"default" | "loading" | "error" | "invalid" | "disabled" | "hidden"`, + description: + "What is happening to the component. Replaces isDisabled, isLoading and isInvalid, which could disagree.", + }, { name: "indeterminate", type: "boolean", @@ -64,6 +92,27 @@ export default function CheckboxShowcase() { + + + + + + Terms {accepted() ? "accepted" : "not accepted"} + + +

+ onChange hands over the new checked state. Reach for onNativeChange + when you need the event itself, such as to call preventDefault. +

+ + e.preventDefault()} />`} + /> +
+
+ diff --git a/src/components/ComponentVariants.tsx b/src/components/ComponentVariants.tsx index 3fbff39..fc872d8 100644 --- a/src/components/ComponentVariants.tsx +++ b/src/components/ComponentVariants.tsx @@ -10,7 +10,7 @@ export default function ComponentVariants() { - + diff --git a/src/components/CoverageShowcase.tsx b/src/components/CoverageShowcase.tsx index c6dda32..802d231 100644 --- a/src/components/CoverageShowcase.tsx +++ b/src/components/CoverageShowcase.tsx @@ -146,7 +146,7 @@ const EXAMPLES: Example[] = [ ), }, { name: "Text", render: () => Body text. }, - { name: "TextArea", render: () =>