Migrate basic components to Zod props

This commit is contained in:
Mats Bosson 2026-03-29 20:38:44 +07:00
parent 38ef3b0934
commit c4547473a4
23 changed files with 198 additions and 55 deletions

View File

@ -0,0 +1,11 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const AlertPropsSchema = z.object({});
export interface AlertProps extends z.infer<typeof AlertPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof AlertPropsSchema>> { children?: JSX.Element; }
export const AlertMeta: ComponentMeta = {
name: "Alert",
description: "Inline status message for important information, warnings, errors, or success states",
parts: ["Root", "Title", "Description"] as const,
requiredParts: ["Root"] as const,
} as const;

View File

@ -1,9 +1,7 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
export interface AlertProps extends JSX.HTMLAttributes<HTMLDivElement> {
children?: JSX.Element;
}
import type { AlertProps } from "./alert.props";
export type { AlertProps } from "./alert.props";
/** An alert element that announces important messages to screen readers via role="alert". */
export function Alert(props: AlertProps): JSX.Element {

View File

@ -1,2 +1,3 @@
export { Alert } from "./alert";
export type { AlertProps } from "./alert";
export { AlertPropsSchema, AlertMeta } from "./alert.props";

View File

@ -0,0 +1,11 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const BadgePropsSchema = z.object({});
export interface BadgeProps extends z.infer<typeof BadgePropsSchema>, Omit<JSX.HTMLAttributes<HTMLSpanElement>, keyof z.infer<typeof BadgePropsSchema>> { children?: JSX.Element; }
export const BadgeMeta: ComponentMeta = {
name: "Badge",
description: "Small status indicator label, typically used for counts, tags, or status",
parts: ["Badge"] as const,
requiredParts: ["Badge"] as const,
} as const;

View File

@ -1,9 +1,7 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
export interface BadgeProps extends JSX.HTMLAttributes<HTMLSpanElement> {
children?: JSX.Element;
}
import type { BadgeProps } from "./badge.props";
export type { BadgeProps } from "./badge.props";
/** A status badge that announces its content to screen readers via role="status". */
export function Badge(props: BadgeProps): JSX.Element {

View File

@ -1,2 +1,3 @@
export { Badge } from "./badge";
export type { BadgeProps } from "./badge";
export { BadgePropsSchema, BadgeMeta } from "./badge.props";

View File

@ -0,0 +1,14 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const ButtonPropsSchema = z.object({
type: z.enum(["button", "submit", "reset"]).optional().describe("Button type attribute. Defaults to 'button' to prevent accidental form submission."),
disabled: z.boolean().optional().describe("When true, disables the button and prevents click handlers."),
});
export interface ButtonProps extends z.infer<typeof ButtonPropsSchema>, Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, keyof z.infer<typeof ButtonPropsSchema>> { children?: JSX.Element; }
export const ButtonMeta: ComponentMeta = {
name: "Button",
description: "Clickable element that triggers an action. Defaults type to 'button' to prevent form submission",
parts: ["Button"] as const,
requiredParts: ["Button"] as const,
} as const;

View File

@ -1,14 +1,7 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
/** Props for the accessible Button component. */
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
/** Button type attribute. @default "button" */
type?: "button" | "submit" | "reset" | undefined;
/** When true, disables the button and prevents click handlers. */
disabled?: boolean | undefined;
children?: JSX.Element | undefined;
}
import type { ButtonProps } from "./button.props";
export type { ButtonProps } from "./button.props";
/**
* An accessible button component.

View File

@ -1,2 +1,3 @@
export { Button } from "./button";
export type { ButtonProps } from "./button";
export { ButtonPropsSchema, ButtonMeta } from "./button.props";

View File

@ -1,2 +1,3 @@
export { Link } from "./link";
export type { LinkProps } from "./link";
export { LinkPropsSchema, LinkMeta } from "./link.props";

View File

@ -0,0 +1,15 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const LinkPropsSchema = z.object({
href: z.string().optional().describe("The URL the link navigates to. Removed from the DOM when disabled."),
external: z.boolean().optional().describe("When true, opens the link in a new tab with rel='noopener noreferrer'."),
disabled: z.boolean().optional().describe("When true, prevents navigation and marks the link as aria-disabled."),
});
export interface LinkProps extends z.infer<typeof LinkPropsSchema>, Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, keyof z.infer<typeof LinkPropsSchema>> { children?: JSX.Element; }
export const LinkMeta: ComponentMeta = {
name: "Link",
description: "Navigation anchor element with external link and disabled support",
parts: ["Link"] as const,
requiredParts: ["Link"] as const,
} as const;

View File

@ -1,14 +1,7 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
/** Props for the accessible Link component. */
export interface LinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {
/** The URL the link navigates to. Removed when disabled. */
href?: string | undefined;
/** When true, prevents navigation and marks the link as aria-disabled. */
disabled?: boolean | undefined;
children?: JSX.Element | undefined;
}
import type { LinkProps } from "./link.props";
export type { LinkProps } from "./link.props";
/**
* An accessible link component.

View File

@ -1,2 +1,3 @@
export { Progress } from "./progress";
export type { ProgressProps } from "./progress";
export { ProgressRootPropsSchema, ProgressMeta } from "./progress.props";

View File

@ -0,0 +1,15 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const ProgressRootPropsSchema = z.object({
value: z.number().optional().describe("Current progress value. Omit or pass null for indeterminate state."),
max: z.number().optional().describe("Maximum value for the progress bar. Defaults to 100."),
getValueLabel: z.function(z.tuple([z.number(), z.number()]), z.string()).optional().describe("Custom function to generate the aria-valuetext label. Receives (value, max)."),
});
export interface ProgressRootProps extends z.infer<typeof ProgressRootPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof ProgressRootPropsSchema>> { children?: JSX.Element; }
export const ProgressMeta: ComponentMeta = {
name: "Progress",
description: "Visual indicator showing completion progress of a task or operation",
parts: ["Root", "Track", "Fill", "Label"] as const,
requiredParts: ["Root"] as const,
} as const;

View File

@ -1,14 +1,7 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
export interface ProgressProps extends JSX.HTMLAttributes<HTMLDivElement> {
/** Current value. Pass null for indeterminate. */
value?: number | null;
/** Maximum value. @default 100 */
max?: number;
/** Custom label function for aria-valuetext. */
getValueLabel?: (value: number, max: number) => string;
}
import type { ProgressRootProps as ProgressProps } from "./progress.props";
export type { ProgressRootProps as ProgressProps } from "./progress.props";
/**
* Displays the progress of a task. Supports determinate and indeterminate states.

View File

@ -1,2 +1,3 @@
export { Skeleton } from "./skeleton";
export type { SkeletonProps } from "./skeleton";
export { SkeletonPropsSchema, SkeletonMeta } from "./skeleton.props";

View File

@ -0,0 +1,16 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const SkeletonPropsSchema = z.object({
visible: z.boolean().optional().describe("Whether the skeleton is visible (animating). Defaults to true."),
circle: z.boolean().optional().describe("Render as a circle. Defaults to false."),
width: z.string().optional().describe("Width in CSS units, e.g. '100px' or '50%'."),
height: z.string().optional().describe("Height in CSS units, e.g. '20px' or '2rem'."),
});
export interface SkeletonProps extends z.infer<typeof SkeletonPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof SkeletonPropsSchema>> { children?: JSX.Element; }
export const SkeletonMeta: ComponentMeta = {
name: "Skeleton",
description: "Placeholder loading indicator that mimics the shape of content being loaded",
parts: ["Skeleton"] as const,
requiredParts: ["Skeleton"] as const,
} as const;

View File

@ -1,16 +1,8 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
export interface SkeletonProps extends JSX.HTMLAttributes<HTMLDivElement> {
/** Whether the skeleton is visible (animating). @default true */
visible?: boolean | undefined;
/** Render as a circle. @default false */
circle?: boolean | undefined;
/** Width in CSS units. */
width?: string | undefined;
/** Height in CSS units. */
height?: string | undefined;
}
import type { SkeletonProps } from "./skeleton.props";
export type { SkeletonProps } from "./skeleton.props";
/** A loading placeholder skeleton with data attributes for styling. */
export function Skeleton(props: SkeletonProps): JSX.Element {

View File

@ -1,2 +1,3 @@
export { Toggle } from "./toggle";
export type { ToggleProps } from "./toggle";
export { TogglePropsSchema, ToggleMeta } from "./toggle.props";

View File

@ -0,0 +1,15 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const TogglePropsSchema = z.object({
pressed: z.boolean().optional().describe("Controlled pressed state. When provided, the component is in controlled mode."),
defaultPressed: z.boolean().optional().describe("Default pressed state for uncontrolled usage. Defaults to false."),
disabled: z.boolean().optional().describe("When true, disables the toggle and prevents state changes."),
});
export interface ToggleProps extends z.infer<typeof TogglePropsSchema>, Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, keyof z.infer<typeof TogglePropsSchema>> { onPressedChange?: (pressed: boolean) => void; children?: JSX.Element; }
export const ToggleMeta: ComponentMeta = {
name: "Toggle",
description: "Two-state button that can be toggled on or off",
parts: ["Toggle"] as const,
requiredParts: ["Toggle"] as const,
} as const;

View File

@ -1,16 +1,8 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import { createControllableSignal } from "../../primitives/create-controllable-signal";
export interface ToggleProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
/** Controlled pressed state. */
pressed?: boolean;
/** Default pressed state (uncontrolled). @default false */
defaultPressed?: boolean;
/** Called when pressed state changes. */
onPressedChange?: (pressed: boolean) => void;
children?: JSX.Element;
}
import type { ToggleProps } from "./toggle.props";
export type { ToggleProps } from "./toggle.props";
/**
* A two-state button that can be toggled on and off.

View File

@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { SelectRootPropsSchema, SelectMeta } from "../../src/components/select/select.props";
import { ComboboxRootPropsSchema, ComboboxMeta } from "../../src/components/combobox/combobox.props";
import { DropdownMenuRootPropsSchema, DropdownMenuMeta } from "../../src/components/dropdown-menu/dropdown-menu.props";
import { ListboxRootPropsSchema, ListboxMeta } from "../../src/components/listbox/listbox.props";
import { SeparatorPropsSchema, SeparatorMeta } from "../../src/components/separator/separator.props";
import { PaginationRootPropsSchema, PaginationMeta } from "../../src/components/pagination/pagination.props";
describe("Collection component schemas", () => {
it("Select validates", () => {
expect(SelectRootPropsSchema.safeParse({ value: "opt1", placeholder: "Choose..." }).success).toBe(true);
});
it("Combobox validates", () => {
expect(ComboboxRootPropsSchema.safeParse({ inputValue: "search", allowCustomValue: true }).success).toBe(true);
});
it("DropdownMenu validates", () => {
expect(DropdownMenuRootPropsSchema.safeParse({ open: true }).success).toBe(true);
});
it("Listbox validates", () => {
expect(ListboxRootPropsSchema.safeParse({ value: "a" }).success).toBe(true);
expect(ListboxRootPropsSchema.safeParse({ value: ["a", "b"], multiple: true }).success).toBe(true);
});
it("Separator validates", () => {
expect(SeparatorPropsSchema.safeParse({ orientation: "vertical" }).success).toBe(true);
expect(SeparatorPropsSchema.safeParse({ orientation: "angled" }).success).toBe(false);
});
it("Pagination validates", () => {
expect(PaginationRootPropsSchema.safeParse({ page: 3, count: 10, siblingCount: 2 }).success).toBe(true);
});
it("all Meta objects valid", () => {
const metas = [SelectMeta, ComboboxMeta, DropdownMenuMeta, ListboxMeta, SeparatorMeta, PaginationMeta];
for (const meta of metas) {
expect(meta.name).toBeTruthy();
expect(meta.description).toBeTruthy();
expect(meta.parts.length).toBeGreaterThan(0);
expect(meta.requiredParts.length).toBeGreaterThan(0);
}
});
});

View File

@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { ButtonPropsSchema, ButtonMeta } from "../../src/components/button/button.props";
import { BadgeMeta } from "../../src/components/badge/badge.props";
import { AlertMeta } from "../../src/components/alert/alert.props";
import { SkeletonMeta } from "../../src/components/skeleton/skeleton.props";
import { LinkPropsSchema, LinkMeta } from "../../src/components/link/link.props";
import { TogglePropsSchema, ToggleMeta } from "../../src/components/toggle/toggle.props";
import { ProgressRootPropsSchema, ProgressMeta } from "../../src/components/progress/progress.props";
describe("Simple component schemas", () => {
it("Button schema validates", () => {
const validBtn = ButtonPropsSchema.safeParse({ type: "submit" });
const invalidBtn = ButtonPropsSchema.safeParse({ type: "invalid" });
expect(validBtn.success).toBe(true);
expect(invalidBtn.success).toBe(false);
});
it("Link schema validates", () => {
const result = LinkPropsSchema.safeParse({ href: "/about", external: true });
expect(result.success).toBe(true);
});
it("Toggle schema validates", () => {
const result = TogglePropsSchema.safeParse({ pressed: true, disabled: false });
expect(result.success).toBe(true);
});
it("Progress schema validates", () => {
const result = ProgressRootPropsSchema.safeParse({ value: 50, max: 100 });
expect(result.success).toBe(true);
});
it("all Meta objects have required fields", () => {
const metas = [ButtonMeta, BadgeMeta, AlertMeta, SkeletonMeta, LinkMeta, ToggleMeta, ProgressMeta];
for (const meta of metas) {
expect(meta.name).toBeTruthy();
expect(meta.description).toBeTruthy();
expect(meta.parts.length).toBeGreaterThan(0);
expect(meta.requiredParts.length).toBeGreaterThan(0);
}
});
});