Migrate navigation components to Zod props

This commit is contained in:
Mats Bosson 2026-03-29 20:41:10 +07:00
parent fdd12f95c6
commit c0019d57e7
12 changed files with 163 additions and 67 deletions

View File

@ -0,0 +1,17 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const AccordionRootPropsSchema = z.object({
value: z.union([z.string(), z.array(z.string())]).optional().describe("Controlled expanded item value(s)"),
defaultValue: z.union([z.string(), z.array(z.string())]).optional().describe("Initial expanded item value(s) when uncontrolled"),
multiple: z.boolean().optional().describe("Allow multiple items to be expanded simultaneously"),
collapsible: z.boolean().optional().describe("Allow the currently open item to be closed by clicking it again"),
disabled: z.boolean().optional().describe("Disable all accordion items"),
});
export interface AccordionRootProps extends z.infer<typeof AccordionRootPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof AccordionRootPropsSchema>> { onValueChange?: (value: string | string[]) => void; children: JSX.Element; }
export const AccordionMeta: ComponentMeta = {
name: "Accordion",
description: "Vertically stacked sections that expand/collapse to show content one at a time or multiple",
parts: ["Root", "Item", "Header", "Trigger", "Content"] as const,
requiredParts: ["Root", "Item", "Trigger", "Content"] as const,
} as const;

View File

@ -4,18 +4,10 @@ import { AccordionHeader } from "./accordion-header";
import { AccordionItem } from "./accordion-item"; import { AccordionItem } from "./accordion-item";
import { AccordionRoot } from "./accordion-root"; import { AccordionRoot } from "./accordion-root";
import { AccordionTrigger } from "./accordion-trigger"; import { AccordionTrigger } from "./accordion-trigger";
export { AccordionRootPropsSchema, AccordionItemPropsSchema, AccordionMeta } from "./accordion.props";
export const Accordion = Object.assign(AccordionRoot, { export type { AccordionRootProps, AccordionItemProps } from "./accordion.props";
Item: AccordionItem,
Header: AccordionHeader,
Trigger: AccordionTrigger,
Content: AccordionContent,
useContext: useAccordionRootContext,
});
export type { AccordionRootProps } from "./accordion-root";
export type { AccordionItemProps } from "./accordion-item";
export type { AccordionHeaderProps } from "./accordion-header"; export type { AccordionHeaderProps } from "./accordion-header";
export type { AccordionTriggerProps } from "./accordion-trigger"; export type { AccordionTriggerProps } from "./accordion-trigger";
export type { AccordionContentProps } from "./accordion-content"; export type { AccordionContentProps } from "./accordion-content";
export type { AccordionRootContextValue, AccordionItemContextValue } from "./accordion-context"; export type { AccordionRootContextValue, AccordionItemContextValue } from "./accordion-context";
export const Accordion = Object.assign(AccordionRoot, { Item: AccordionItem, Header: AccordionHeader, Trigger: AccordionTrigger, Content: AccordionContent, useContext: useAccordionRootContext });

View File

@ -0,0 +1,14 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const AlertDialogRootPropsSchema = z.object({
open: z.boolean().optional().describe("Controlled open state"),
defaultOpen: z.boolean().optional().describe("Initial open state when uncontrolled"),
});
export interface AlertDialogRootProps extends z.infer<typeof AlertDialogRootPropsSchema> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
export const AlertDialogMeta: ComponentMeta = {
name: "AlertDialog",
description: "Modal dialog for critical confirmations that requires explicit user action to dismiss",
parts: ["Root", "Trigger", "Portal", "Overlay", "Content", "Title", "Description", "Cancel", "Action"] as const,
requiredParts: ["Root", "Content", "Title", "Action"] as const,
} as const;

View File

@ -8,26 +8,7 @@ import { AlertDialogPortal } from "./alert-dialog-portal";
import { AlertDialogRoot } from "./alert-dialog-root"; import { AlertDialogRoot } from "./alert-dialog-root";
import { AlertDialogTitle } from "./alert-dialog-title"; import { AlertDialogTitle } from "./alert-dialog-title";
import { AlertDialogTrigger } from "./alert-dialog-trigger"; import { AlertDialogTrigger } from "./alert-dialog-trigger";
export { AlertDialogRootPropsSchema, AlertDialogMeta } from "./alert-dialog.props";
export const AlertDialog = Object.assign(AlertDialogRoot, { export type { AlertDialogRootProps } from "./alert-dialog.props";
Content: AlertDialogContent, export type { AlertDialogContentProps, AlertDialogTitleProps, AlertDialogDescriptionProps, AlertDialogTriggerProps, AlertDialogCancelProps, AlertDialogActionProps, AlertDialogPortalProps, AlertDialogOverlayProps, AlertDialogContextValue } from "./alert-dialog-content";
Title: AlertDialogTitle, export const AlertDialog = Object.assign(AlertDialogRoot, { Content: AlertDialogContent, Title: AlertDialogTitle, Description: AlertDialogDescription, Trigger: AlertDialogTrigger, Cancel: AlertDialogCancel, Action: AlertDialogAction, Portal: AlertDialogPortal, Overlay: AlertDialogOverlay, useContext: useAlertDialogContext });
Description: AlertDialogDescription,
Trigger: AlertDialogTrigger,
Cancel: AlertDialogCancel,
Action: AlertDialogAction,
Portal: AlertDialogPortal,
Overlay: AlertDialogOverlay,
useContext: useAlertDialogContext,
});
export type { AlertDialogRootProps } from "./alert-dialog-root";
export type { AlertDialogContentProps } from "./alert-dialog-content";
export type { AlertDialogTitleProps } from "./alert-dialog-title";
export type { AlertDialogDescriptionProps } from "./alert-dialog-description";
export type { AlertDialogTriggerProps } from "./alert-dialog-trigger";
export type { AlertDialogCancelProps } from "./alert-dialog-cancel";
export type { AlertDialogActionProps } from "./alert-dialog-action";
export type { AlertDialogPortalProps } from "./alert-dialog-portal";
export type { AlertDialogOverlayProps } from "./alert-dialog-overlay";
export type { AlertDialogContextValue } from "./alert-dialog-context";

View File

@ -0,0 +1,13 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const BreadcrumbsRootPropsSchema = z.object({
separator: z.string().optional().describe("Custom separator string rendered between breadcrumb items"),
});
export interface BreadcrumbsRootProps extends z.infer<typeof BreadcrumbsRootPropsSchema>, Omit<JSX.HTMLAttributes<HTMLElement>, keyof z.infer<typeof BreadcrumbsRootPropsSchema>> { children: JSX.Element; }
export const BreadcrumbsMeta: ComponentMeta = {
name: "Breadcrumbs",
description: "Navigation trail showing the current page location within a hierarchy",
parts: ["Root", "Item", "Link", "Separator"] as const,
requiredParts: ["Root", "Item"] as const,
} as const;

View File

@ -2,15 +2,7 @@ import { BreadcrumbsItem } from "./breadcrumbs-item";
import { BreadcrumbsLink } from "./breadcrumbs-link"; import { BreadcrumbsLink } from "./breadcrumbs-link";
import { BreadcrumbsRoot } from "./breadcrumbs-root"; import { BreadcrumbsRoot } from "./breadcrumbs-root";
import { BreadcrumbsSeparator } from "./breadcrumbs-separator"; import { BreadcrumbsSeparator } from "./breadcrumbs-separator";
export { BreadcrumbsRootPropsSchema, BreadcrumbsMeta } from "./breadcrumbs.props";
/** Compound breadcrumbs component with Item, Link, and Separator sub-components. */ export type { BreadcrumbsRootProps } from "./breadcrumbs.props";
export const Breadcrumbs = Object.assign(BreadcrumbsRoot, { export type { BreadcrumbsItemProps, BreadcrumbsLinkProps, BreadcrumbsSeparatorProps } from "./breadcrumbs-item";
Item: BreadcrumbsItem, export const Breadcrumbs = Object.assign(BreadcrumbsRoot, { Item: BreadcrumbsItem, Link: BreadcrumbsLink, Separator: BreadcrumbsSeparator });
Link: BreadcrumbsLink,
Separator: BreadcrumbsSeparator,
});
export type { BreadcrumbsRootProps } from "./breadcrumbs-root";
export type { BreadcrumbsItemProps } from "./breadcrumbs-item";
export type { BreadcrumbsLinkProps } from "./breadcrumbs-link";
export type { BreadcrumbsSeparatorProps } from "./breadcrumbs-separator";

View File

@ -0,0 +1,15 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const CollapsibleRootPropsSchema = z.object({
open: z.boolean().optional().describe("Controlled open state"),
defaultOpen: z.boolean().optional().describe("Initial open state when uncontrolled"),
disabled: z.boolean().optional().describe("Prevent the collapsible from being toggled"),
});
export interface CollapsibleRootProps extends z.infer<typeof CollapsibleRootPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof CollapsibleRootPropsSchema>> { onOpenChange?: (open: boolean) => void; children: JSX.Element; }
export const CollapsibleMeta: ComponentMeta = {
name: "Collapsible",
description: "Content section that can be expanded or collapsed with a trigger",
parts: ["Root", "Trigger", "Content"] as const,
requiredParts: ["Root", "Trigger", "Content"] as const,
} as const;

View File

@ -2,14 +2,9 @@ import { CollapsibleContent } from "./collapsible-content";
import { useCollapsibleContext } from "./collapsible-context"; import { useCollapsibleContext } from "./collapsible-context";
import { CollapsibleRoot } from "./collapsible-root"; import { CollapsibleRoot } from "./collapsible-root";
import { CollapsibleTrigger } from "./collapsible-trigger"; import { CollapsibleTrigger } from "./collapsible-trigger";
export { CollapsibleRootPropsSchema, CollapsibleMeta } from "./collapsible.props";
export const Collapsible = Object.assign(CollapsibleRoot, { export type { CollapsibleRootProps } from "./collapsible.props";
Trigger: CollapsibleTrigger,
Content: CollapsibleContent,
useContext: useCollapsibleContext,
});
export type { CollapsibleRootProps } from "./collapsible-root";
export type { CollapsibleTriggerProps } from "./collapsible-trigger"; export type { CollapsibleTriggerProps } from "./collapsible-trigger";
export type { CollapsibleContentProps } from "./collapsible-content"; export type { CollapsibleContentProps } from "./collapsible-content";
export type { CollapsibleContextValue } from "./collapsible-context"; export type { CollapsibleContextValue } from "./collapsible-context";
export const Collapsible = Object.assign(CollapsibleRoot, { Trigger: CollapsibleTrigger, Content: CollapsibleContent, useContext: useCollapsibleContext });

View File

@ -3,16 +3,7 @@ import { TabsList } from "./tabs-list";
import { TabsPanel } from "./tabs-panel"; import { TabsPanel } from "./tabs-panel";
import { TabsRoot } from "./tabs-root"; import { TabsRoot } from "./tabs-root";
import { TabsTab } from "./tabs-tab"; import { TabsTab } from "./tabs-tab";
export { TabsRootPropsSchema, TabsMeta } from "./tabs.props";
export const Tabs = Object.assign(TabsRoot, { export type { TabsRootProps } from "./tabs.props";
List: TabsList, export type { TabsListProps, TabsTabProps, TabsPanelProps, TabsContextValue } from "./tabs-list";
Tab: TabsTab, export const Tabs = Object.assign(TabsRoot, { List: TabsList, Tab: TabsTab, Panel: TabsPanel, useContext: useTabsContext });
Panel: TabsPanel,
useContext: useTabsContext,
});
export type { TabsRootProps } from "./tabs-root";
export type { TabsListProps } from "./tabs-list";
export type { TabsTabProps } from "./tabs-tab";
export type { TabsPanelProps } from "./tabs-panel";
export type { TabsContextValue } from "./tabs-context";

View File

@ -0,0 +1,17 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const TabsRootPropsSchema = z.object({
value: z.string().optional().describe("Controlled active tab value"),
defaultValue: z.string().optional().describe("Initial active tab value when uncontrolled"),
orientation: z.enum(["horizontal", "vertical"]).optional().describe("Layout orientation of the tab list. Defaults to horizontal"),
activationMode: z.enum(["automatic", "manual"]).optional().describe("automatic: activates on focus; manual: activates on click or Enter/Space. Defaults to automatic"),
disabled: z.boolean().optional().describe("Disable all tabs"),
});
export interface TabsRootProps extends z.infer<typeof TabsRootPropsSchema>, Omit<JSX.HTMLAttributes<HTMLDivElement>, keyof z.infer<typeof TabsRootPropsSchema>> { onValueChange?: (value: string) => void; children: JSX.Element; }
export const TabsMeta: ComponentMeta = {
name: "Tabs",
description: "Tabbed interface for switching between different views or sections of content",
parts: ["Root", "List", "Trigger", "Content"] as const,
requiredParts: ["Root", "List", "Trigger", "Content"] as const,
} as const;

View File

@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { AccordionRootPropsSchema, AccordionMeta } from "../../src/components/accordion/accordion.props";
import { CollapsibleRootPropsSchema, CollapsibleMeta } from "../../src/components/collapsible/collapsible.props";
import { AlertDialogRootPropsSchema, AlertDialogMeta } from "../../src/components/alert-dialog/alert-dialog.props";
import { BreadcrumbsRootPropsSchema, BreadcrumbsMeta } from "../../src/components/breadcrumbs/breadcrumbs.props";
import { TabsRootPropsSchema, TabsMeta } from "../../src/components/tabs/tabs.props";
describe("Disclosure component schemas", () => {
it("Accordion validates", () => {
expect(AccordionRootPropsSchema.safeParse({ value: "item-1" }).success).toBe(true);
expect(AccordionRootPropsSchema.safeParse({ value: ["item-1", "item-2"], multiple: true }).success).toBe(true);
});
it("Collapsible validates", () => { expect(CollapsibleRootPropsSchema.safeParse({ open: true, disabled: false }).success).toBe(true); });
it("AlertDialog validates", () => { expect(AlertDialogRootPropsSchema.safeParse({ open: false }).success).toBe(true); });
it("Breadcrumbs validates", () => { expect(BreadcrumbsRootPropsSchema.safeParse({ separator: ">" }).success).toBe(true); });
it("Tabs validates", () => {
expect(TabsRootPropsSchema.safeParse({ value: "tab1", orientation: "vertical", activationMode: "manual" }).success).toBe(true);
expect(TabsRootPropsSchema.safeParse({ orientation: "diagonal" }).success).toBe(false);
});
it("all Meta objects valid", () => {
const metas = [AccordionMeta, CollapsibleMeta, AlertDialogMeta, BreadcrumbsMeta, TabsMeta];
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,41 @@
import { describe, expect, it } from "vitest";
import { TextFieldRootPropsSchema, TextFieldMeta } from "../../src/components/text-field/text-field.props";
import { CheckboxRootPropsSchema, CheckboxMeta } from "../../src/components/checkbox/checkbox.props";
import { SwitchRootPropsSchema, SwitchMeta } from "../../src/components/switch/switch.props";
import { RadioGroupRootPropsSchema, RadioGroupMeta } from "../../src/components/radio-group/radio-group.props";
import { SliderRootPropsSchema, SliderMeta } from "../../src/components/slider/slider.props";
import { NumberFieldRootPropsSchema, NumberFieldMeta } from "../../src/components/number-field/number-field.props";
import { ToggleGroupRootPropsSchema, ToggleGroupMeta } from "../../src/components/toggle-group/toggle-group.props";
describe("Form component schemas", () => {
it("TextField validates", () => {
expect(TextFieldRootPropsSchema.safeParse({ value: "hello", disabled: false }).success).toBe(true);
});
it("Checkbox validates", () => {
expect(CheckboxRootPropsSchema.safeParse({ checked: true, name: "agree" }).success).toBe(true);
});
it("Switch validates", () => {
expect(SwitchRootPropsSchema.safeParse({ checked: false }).success).toBe(true);
});
it("RadioGroup validates", () => {
expect(RadioGroupRootPropsSchema.safeParse({ value: "opt1", orientation: "horizontal" }).success).toBe(true);
expect(RadioGroupRootPropsSchema.safeParse({ orientation: "invalid" }).success).toBe(false);
});
it("Slider validates", () => {
expect(SliderRootPropsSchema.safeParse({ value: [25, 75], min: 0, max: 100, step: 5 }).success).toBe(true);
});
it("NumberField validates", () => {
expect(NumberFieldRootPropsSchema.safeParse({ value: 42, min: 0, max: 100 }).success).toBe(true);
});
it("ToggleGroup validates", () => {
expect(ToggleGroupRootPropsSchema.safeParse({ value: "a" }).success).toBe(true);
expect(ToggleGroupRootPropsSchema.safeParse({ value: ["a", "b"], multiple: true }).success).toBe(true);
});
it("all Meta objects valid", () => {
const metas = [TextFieldMeta, CheckboxMeta, SwitchMeta, RadioGroupMeta, SliderMeta, NumberFieldMeta, ToggleGroupMeta];
for (const meta of metas) {
expect(meta.name).toBeTruthy();
expect(meta.description).toBeTruthy();
expect(meta.parts.length).toBeGreaterThan(0);
}
});
});