diff --git a/packages/core/src/components/alert/alert.props.ts b/packages/core/src/components/alert/alert.props.ts new file mode 100644 index 0000000..4d454bc --- /dev/null +++ b/packages/core/src/components/alert/alert.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/alert/alert.tsx b/packages/core/src/components/alert/alert.tsx index da9e6a4..10c73aa 100644 --- a/packages/core/src/components/alert/alert.tsx +++ b/packages/core/src/components/alert/alert.tsx @@ -1,9 +1,7 @@ import type { JSX } from "solid-js"; import { splitProps } from "solid-js"; - -export interface AlertProps extends JSX.HTMLAttributes { - 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 { diff --git a/packages/core/src/components/alert/index.ts b/packages/core/src/components/alert/index.ts index 74a66f0..172f1ab 100644 --- a/packages/core/src/components/alert/index.ts +++ b/packages/core/src/components/alert/index.ts @@ -1,2 +1,3 @@ export { Alert } from "./alert"; export type { AlertProps } from "./alert"; +export { AlertPropsSchema, AlertMeta } from "./alert.props"; diff --git a/packages/core/src/components/badge/badge.props.ts b/packages/core/src/components/badge/badge.props.ts new file mode 100644 index 0000000..d40918b --- /dev/null +++ b/packages/core/src/components/badge/badge.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/badge/badge.tsx b/packages/core/src/components/badge/badge.tsx index 04015d9..73c6995 100644 --- a/packages/core/src/components/badge/badge.tsx +++ b/packages/core/src/components/badge/badge.tsx @@ -1,9 +1,7 @@ import type { JSX } from "solid-js"; import { splitProps } from "solid-js"; - -export interface BadgeProps extends JSX.HTMLAttributes { - 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 { diff --git a/packages/core/src/components/badge/index.ts b/packages/core/src/components/badge/index.ts index 5389c10..0544ce1 100644 --- a/packages/core/src/components/badge/index.ts +++ b/packages/core/src/components/badge/index.ts @@ -1,2 +1,3 @@ export { Badge } from "./badge"; export type { BadgeProps } from "./badge"; +export { BadgePropsSchema, BadgeMeta } from "./badge.props"; diff --git a/packages/core/src/components/button/button.props.ts b/packages/core/src/components/button/button.props.ts new file mode 100644 index 0000000..363a524 --- /dev/null +++ b/packages/core/src/components/button/button.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/button/button.tsx b/packages/core/src/components/button/button.tsx index 5b69744..f0dda39 100644 --- a/packages/core/src/components/button/button.tsx +++ b/packages/core/src/components/button/button.tsx @@ -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 { - /** 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. diff --git a/packages/core/src/components/button/index.ts b/packages/core/src/components/button/index.ts index 8827201..69d0299 100644 --- a/packages/core/src/components/button/index.ts +++ b/packages/core/src/components/button/index.ts @@ -1,2 +1,3 @@ export { Button } from "./button"; export type { ButtonProps } from "./button"; +export { ButtonPropsSchema, ButtonMeta } from "./button.props"; diff --git a/packages/core/src/components/link/index.ts b/packages/core/src/components/link/index.ts index e2a01b5..b2f278d 100644 --- a/packages/core/src/components/link/index.ts +++ b/packages/core/src/components/link/index.ts @@ -1,2 +1,3 @@ export { Link } from "./link"; export type { LinkProps } from "./link"; +export { LinkPropsSchema, LinkMeta } from "./link.props"; diff --git a/packages/core/src/components/link/link.props.ts b/packages/core/src/components/link/link.props.ts new file mode 100644 index 0000000..6a39323 --- /dev/null +++ b/packages/core/src/components/link/link.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/link/link.tsx b/packages/core/src/components/link/link.tsx index cb3264d..a0c8435 100644 --- a/packages/core/src/components/link/link.tsx +++ b/packages/core/src/components/link/link.tsx @@ -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 { - /** 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. diff --git a/packages/core/src/components/progress/index.ts b/packages/core/src/components/progress/index.ts index 26987b8..f57ebb8 100644 --- a/packages/core/src/components/progress/index.ts +++ b/packages/core/src/components/progress/index.ts @@ -1,2 +1,3 @@ export { Progress } from "./progress"; export type { ProgressProps } from "./progress"; +export { ProgressRootPropsSchema, ProgressMeta } from "./progress.props"; diff --git a/packages/core/src/components/progress/progress.props.ts b/packages/core/src/components/progress/progress.props.ts new file mode 100644 index 0000000..0199f68 --- /dev/null +++ b/packages/core/src/components/progress/progress.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/progress/progress.tsx b/packages/core/src/components/progress/progress.tsx index b5ec93e..2add5e3 100644 --- a/packages/core/src/components/progress/progress.tsx +++ b/packages/core/src/components/progress/progress.tsx @@ -1,14 +1,7 @@ import type { JSX } from "solid-js"; import { splitProps } from "solid-js"; - -export interface ProgressProps extends JSX.HTMLAttributes { - /** 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. diff --git a/packages/core/src/components/skeleton/index.ts b/packages/core/src/components/skeleton/index.ts index 8ab981e..d851d67 100644 --- a/packages/core/src/components/skeleton/index.ts +++ b/packages/core/src/components/skeleton/index.ts @@ -1,2 +1,3 @@ export { Skeleton } from "./skeleton"; export type { SkeletonProps } from "./skeleton"; +export { SkeletonPropsSchema, SkeletonMeta } from "./skeleton.props"; diff --git a/packages/core/src/components/skeleton/skeleton.props.ts b/packages/core/src/components/skeleton/skeleton.props.ts new file mode 100644 index 0000000..f3344fc --- /dev/null +++ b/packages/core/src/components/skeleton/skeleton.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/skeleton/skeleton.tsx b/packages/core/src/components/skeleton/skeleton.tsx index 8338c3f..72232ea 100644 --- a/packages/core/src/components/skeleton/skeleton.tsx +++ b/packages/core/src/components/skeleton/skeleton.tsx @@ -1,16 +1,8 @@ import type { JSX } from "solid-js"; import { splitProps } from "solid-js"; -export interface SkeletonProps extends JSX.HTMLAttributes { - /** 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 { diff --git a/packages/core/src/components/toggle/index.ts b/packages/core/src/components/toggle/index.ts index b9937d7..1b7d97b 100644 --- a/packages/core/src/components/toggle/index.ts +++ b/packages/core/src/components/toggle/index.ts @@ -1,2 +1,3 @@ export { Toggle } from "./toggle"; export type { ToggleProps } from "./toggle"; +export { TogglePropsSchema, ToggleMeta } from "./toggle.props"; diff --git a/packages/core/src/components/toggle/toggle.props.ts b/packages/core/src/components/toggle/toggle.props.ts new file mode 100644 index 0000000..f20f5a7 --- /dev/null +++ b/packages/core/src/components/toggle/toggle.props.ts @@ -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, Omit, keyof z.infer> { 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; diff --git a/packages/core/src/components/toggle/toggle.tsx b/packages/core/src/components/toggle/toggle.tsx index 5cfb901..671e0d7 100644 --- a/packages/core/src/components/toggle/toggle.tsx +++ b/packages/core/src/components/toggle/toggle.tsx @@ -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 { - /** 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. diff --git a/packages/core/tests/schemas/collection-components.test.ts b/packages/core/tests/schemas/collection-components.test.ts new file mode 100644 index 0000000..2abd416 --- /dev/null +++ b/packages/core/tests/schemas/collection-components.test.ts @@ -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); + } + }); +}); diff --git a/packages/core/tests/schemas/simple-components.test.ts b/packages/core/tests/schemas/simple-components.test.ts new file mode 100644 index 0000000..712088e --- /dev/null +++ b/packages/core/tests/schemas/simple-components.test.ts @@ -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); + } + }); +});