# Phase 2: Advanced Components — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build 7 advanced components (Form, Calendar, DatePicker, CommandPalette, DataTable, VirtualList, Wizard) and the createVirtualizer primitive. **Architecture:** All components follow the Zod-first pattern from Phase 1: `{name}.props.ts` with schema + Meta, compound API via `Object.assign`, context for cross-part communication, `createControllableSignal` for controlled/uncontrolled state. Complex list components reuse `createListNavigation`. DataTable and VirtualList share a new `createVirtualizer` primitive. **Tech Stack:** SolidJS, Zod v4, TypeScript 6, Vitest, @solidjs/testing-library **Spec:** `docs/superpowers/specs/2026-03-29-pettyui-ai-first-architecture.md` **Parallelism:** Tasks 1, 3, 5, 6, 7 can run in parallel (no dependencies). Task 2 depends on Task 1. Task 4 depends on Task 3. Task 8 depends on Task 1. Task 9 runs last. ``` Wave 1 (parallel): Task 1 (createVirtualizer) Task 3 (Calendar) Task 5 (CommandPalette) Task 6 (Form) Task 7 (Wizard) Wave 2 (parallel): Task 2 (VirtualList) — after Task 1 Task 4 (DatePicker) — after Task 3 Task 8 (DataTable) — after Task 1 Wave 3: Task 9 (build config + verification) ``` --- ## Conventions (from Phase 1) All components MUST follow these exact patterns: **Props file** (`{name}.props.ts`): ```typescript import { z } from "zod/v4"; import type { JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; export const {Name}RootPropsSchema = z.object({ /* .describe() on every field */ }); export interface {Name}RootProps extends z.infer { children: JSX.Element; } export const {Name}Meta: ComponentMeta = { name, description, parts, requiredParts } as const; ``` **Index file** (`index.ts`): ```typescript export const {Name} = Object.assign({Name}Root, { SubPart: {Name}SubPart, useContext: use{Name}Context }); export type { ... } from "./{name}.props"; export { {Name}RootPropsSchema, {Name}Meta } from "./{name}.props"; ``` **Context error messages**: `"[PettyUI] {Name} parts must be used within <{Name}>. Fix: Wrap ... inside <{Name}>."` **Zod v4 function syntax**: `z.function(z.tuple([z.string()]), z.void())` NOT `z.function().args().returns()` **NagLint**: Keep files under 500 lines. Keep `.props.ts` compact (single-line interfaces where possible). Index files need ≥30% logic density. --- ### Task 1: createVirtualizer Primitive **Files:** - Create: `packages/core/src/primitives/create-virtualizer.ts` - Create: `packages/core/tests/primitives/create-virtualizer.test.ts` This primitive powers both VirtualList and DataTable. It manages which items are visible in a scrollable container and provides offset calculations. - [ ] **Step 1: Write failing test** Create `packages/core/tests/primitives/create-virtualizer.test.ts`: ```typescript import { describe, expect, it } from "vitest"; import { createRoot } from "solid-js"; import { createVirtualizer } from "../../src/primitives/create-virtualizer"; describe("createVirtualizer", () => { it("calculates visible range for fixed-size items", () => { createRoot((dispose) => { const virtualizer = createVirtualizer({ count: () => 1000, getScrollElement: () => null, estimateSize: () => 40, }); expect(virtualizer.totalSize()).toBe(40000); dispose(); }); }); it("returns virtual items with correct offsets", () => { createRoot((dispose) => { const virtualizer = createVirtualizer({ count: () => 100, getScrollElement: () => null, estimateSize: () => 50, overscan: 0, }); const items = virtualizer.virtualItems(); expect(items.length).toBeGreaterThanOrEqual(0); dispose(); }); }); it("handles zero count", () => { createRoot((dispose) => { const virtualizer = createVirtualizer({ count: () => 0, getScrollElement: () => null, estimateSize: () => 40, }); expect(virtualizer.totalSize()).toBe(0); expect(virtualizer.virtualItems()).toEqual([]); dispose(); }); }); it("supports horizontal orientation", () => { createRoot((dispose) => { const virtualizer = createVirtualizer({ count: () => 50, getScrollElement: () => null, estimateSize: () => 100, horizontal: true, }); expect(virtualizer.totalSize()).toBe(5000); dispose(); }); }); }); ``` - [ ] **Step 2: Run test to verify it fails** ```bash cd packages/core && pnpm vitest run tests/primitives/create-virtualizer.test.ts ``` - [ ] **Step 3: Implement createVirtualizer** Create `packages/core/src/primitives/create-virtualizer.ts`: ```typescript import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js"; /** A single virtual item with its position and index. */ export interface VirtualItem { index: number; start: number; end: number; size: number; key: string | number; } /** Options for createVirtualizer. */ export interface CreateVirtualizerOptions { /** Total number of items. Reactive. */ count: Accessor; /** The scrollable container element. */ getScrollElement: Accessor; /** Estimated size in pixels for each item. Used for initial layout. */ estimateSize: (index: number) => number; /** Number of items to render outside the visible area. @default 5 */ overscan?: number; /** Whether scrolling is horizontal. @default false */ horizontal?: boolean; /** Custom key extractor. @default index */ getItemKey?: (index: number) => string | number; } /** State returned by createVirtualizer. */ export interface VirtualizerState { /** Virtual items currently in the visible range (plus overscan). */ virtualItems: Accessor; /** Total scrollable size in pixels. */ totalSize: Accessor; /** Imperatively scroll to an item by index. */ scrollToIndex: (index: number, options?: { align?: "start" | "center" | "end" }) => void; /** Current scroll offset. */ scrollOffset: Accessor; /** Measure a specific item's actual size after render. */ measureElement: (element: HTMLElement | null) => void; } /** * Virtualizes a list of items, only rendering those visible in the scroll viewport. * Supports fixed and variable row heights, horizontal/vertical, and overscan. */ export function createVirtualizer(options: CreateVirtualizerOptions): VirtualizerState { const overscan = options.overscan ?? 5; const horizontal = options.horizontal ?? false; const getKey = options.getItemKey ?? ((i: number) => i); const [scrollOffset, setScrollOffset] = createSignal(0); const [containerSize, setContainerSize] = createSignal(0); const measurements = new Map(); function getSize(index: number): number { return measurements.get(index) ?? options.estimateSize(index); } function getItemOffset(index: number): number { let offset = 0; for (let i = 0; i < index; i++) { offset += getSize(i); } return offset; } const totalSize: Accessor = () => { const count = options.count(); let total = 0; for (let i = 0; i < count; i++) { total += getSize(i); } return total; }; const virtualItems: Accessor = () => { const count = options.count(); if (count === 0) return []; const offset = scrollOffset(); const size = containerSize(); if (size === 0) return []; let startIndex = 0; let accumulated = 0; while (startIndex < count && accumulated + getSize(startIndex) <= offset) { accumulated += getSize(startIndex); startIndex++; } let endIndex = startIndex; let visibleAccumulated = 0; while (endIndex < count && visibleAccumulated < size) { visibleAccumulated += getSize(endIndex); endIndex++; } const overscanStart = Math.max(0, startIndex - overscan); const overscanEnd = Math.min(count - 1, endIndex + overscan); const items: VirtualItem[] = []; for (let i = overscanStart; i <= overscanEnd; i++) { const itemSize = getSize(i); const start = getItemOffset(i); items.push({ index: i, start, end: start + itemSize, size: itemSize, key: getKey(i), }); } return items; }; createEffect(() => { const el = options.getScrollElement(); if (!el) return; const sizeKey = horizontal ? "clientWidth" : "clientHeight"; const scrollKey = horizontal ? "scrollLeft" : "scrollTop"; setContainerSize(el[sizeKey]); setScrollOffset(el[scrollKey]); const onScroll = () => { setScrollOffset(el[scrollKey]); }; const resizeObserver = new ResizeObserver(() => { setContainerSize(el[sizeKey]); }); el.addEventListener("scroll", onScroll, { passive: true }); resizeObserver.observe(el); onCleanup(() => { el.removeEventListener("scroll", onScroll); resizeObserver.disconnect(); }); }); function scrollToIndex(index: number, opts?: { align?: "start" | "center" | "end" }): void { const el = options.getScrollElement(); if (!el) return; const itemOffset = getItemOffset(index); const itemSize = getSize(index); const viewSize = containerSize(); const scrollKey = horizontal ? "scrollLeft" : "scrollTop"; let target = itemOffset; if (opts?.align === "center") { target = itemOffset - viewSize / 2 + itemSize / 2; } else if (opts?.align === "end") { target = itemOffset - viewSize + itemSize; } el[scrollKey] = Math.max(0, target); } function measureElement(element: HTMLElement | null): void { if (!element) return; const index = Number(element.dataset.index); if (Number.isNaN(index)) return; const size = horizontal ? element.offsetWidth : element.offsetHeight; measurements.set(index, size); } return { virtualItems, totalSize, scrollToIndex, scrollOffset, measureElement }; } ``` - [ ] **Step 4: Run tests** ```bash cd packages/core && pnpm vitest run tests/primitives/create-virtualizer.test.ts ``` Expected: 4 tests pass. - [ ] **Step 5: Commit** ```bash git add packages/core/src/primitives/create-virtualizer.ts packages/core/tests/primitives/create-virtualizer.test.ts git commit -m "feat: add createVirtualizer primitive for virtual scrolling" ``` --- ### Task 2: VirtualList Component **Depends on:** Task 1 (createVirtualizer) **Files:** - Create: `packages/core/src/components/virtual-list/virtual-list.props.ts` - Create: `packages/core/src/components/virtual-list/virtual-list-root.tsx` - Create: `packages/core/src/components/virtual-list/index.ts` - Create: `packages/core/tests/components/virtual-list/virtual-list.test.tsx` - [ ] **Step 1: Create virtual-list.props.ts** ```typescript import { z } from "zod/v4"; import type { Accessor, JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; import type { VirtualItem } from "../../primitives/create-virtualizer"; export const VirtualListRootPropsSchema = z.object({ count: z.number().describe("Total number of items in the list"), estimateSize: z.number().optional().describe("Estimated item height in pixels. Defaults to 40"), overscan: z.number().optional().describe("Number of items rendered outside visible area. Defaults to 5"), horizontal: z.boolean().optional().describe("Whether the list scrolls horizontally"), }); export interface VirtualListRootProps extends Omit, "count" | "estimateSize">, Omit, "children"> { count: number; estimateSize?: number | ((index: number) => number); children: (item: VirtualItem) => JSX.Element; } export const VirtualListMeta: ComponentMeta = { name: "VirtualList", description: "Virtualized scrollable list that only renders visible items, for large datasets", parts: ["Root"] as const, requiredParts: ["Root"] as const, } as const; ``` - [ ] **Step 2: Create virtual-list-root.tsx** ```typescript import type { JSX } from "solid-js"; import { For, createSignal, splitProps } from "solid-js"; import { createVirtualizer } from "../../primitives/create-virtualizer"; import type { VirtualListRootProps } from "./virtual-list.props"; export function VirtualListRoot(props: VirtualListRootProps): JSX.Element { const [local, rest] = splitProps(props, ["count", "estimateSize", "overscan", "horizontal", "children"]); const [scrollRef, setScrollRef] = createSignal(null); const estimateFn = () => { const est = local.estimateSize ?? 40; return typeof est === "function" ? est : () => est; }; const virtualizer = createVirtualizer({ count: () => local.count, getScrollElement: scrollRef, estimateSize: (i) => estimateFn()(i), overscan: local.overscan, horizontal: local.horizontal, }); return (
{(item) => (
virtualizer.measureElement(el)} style={{ position: "absolute", top: local.horizontal ? "0" : `${item.start}px`, left: local.horizontal ? `${item.start}px` : "0", width: local.horizontal ? `${item.size}px` : "100%", height: local.horizontal ? "100%" : `${item.size}px`, }} > {local.children(item)}
)}
); } ``` - [ ] **Step 3: Create index.ts** ```typescript import { VirtualListRoot } from "./virtual-list-root"; export const VirtualList = VirtualListRoot; export type { VirtualListRootProps } from "./virtual-list.props"; export { VirtualListRootPropsSchema, VirtualListMeta } from "./virtual-list.props"; ``` - [ ] **Step 4: Write test** Create `packages/core/tests/components/virtual-list/virtual-list.test.tsx`: ```typescript import { render, screen } from "@solidjs/testing-library"; import { describe, expect, it } from "vitest"; import { VirtualList } from "../../../src/components/virtual-list/index"; import { VirtualListRootPropsSchema, VirtualListMeta } from "../../../src/components/virtual-list/virtual-list.props"; describe("VirtualList", () => { it("renders with render prop children", () => { render(() => ( {(item) =>
Item {item.index}
}
)); expect(screen.getByTestId("list")).toBeTruthy(); }); it("schema validates props", () => { expect(VirtualListRootPropsSchema.safeParse({ count: 100, estimateSize: 40 }).success).toBe(true); expect(VirtualListRootPropsSchema.safeParse({ count: 50, horizontal: true, overscan: 10 }).success).toBe(true); }); it("meta has required fields", () => { expect(VirtualListMeta.name).toBe("VirtualList"); expect(VirtualListMeta.description).toBeTruthy(); expect(VirtualListMeta.parts).toContain("Root"); }); }); ``` - [ ] **Step 5: Run tests and commit** ```bash cd packages/core && pnpm vitest run tests/components/virtual-list/ git add packages/core/src/components/virtual-list/ packages/core/tests/components/virtual-list/ git commit -m "feat(virtual-list): add VirtualList component with createVirtualizer" ``` --- ### Task 3: Calendar Component **Files:** - Create: `packages/core/src/components/calendar/calendar.props.ts` - Create: `packages/core/src/components/calendar/calendar-context.ts` - Create: `packages/core/src/components/calendar/calendar-root.tsx` - Create: `packages/core/src/components/calendar/calendar-header.tsx` - Create: `packages/core/src/components/calendar/calendar-grid.tsx` - Create: `packages/core/src/components/calendar/calendar-grid-head.tsx` - Create: `packages/core/src/components/calendar/calendar-grid-body.tsx` - Create: `packages/core/src/components/calendar/calendar-cell.tsx` - Create: `packages/core/src/components/calendar/calendar-heading.tsx` - Create: `packages/core/src/components/calendar/calendar-nav.tsx` - Create: `packages/core/src/components/calendar/index.ts` - Create: `packages/core/tests/components/calendar/calendar.test.tsx` - [ ] **Step 1: Create calendar.props.ts** ```typescript import { z } from "zod/v4"; import type { JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; export const CalendarRootPropsSchema = z.object({ value: z.string().optional().describe("Controlled selected date as ISO string (YYYY-MM-DD)"), defaultValue: z.string().optional().describe("Initial selected date (uncontrolled)"), month: z.number().optional().describe("Controlled displayed month (0-11)"), year: z.number().optional().describe("Controlled displayed year"), minDate: z.string().optional().describe("Earliest selectable date as ISO string"), maxDate: z.string().optional().describe("Latest selectable date as ISO string"), disabled: z.boolean().optional().describe("Whether the calendar is disabled"), locale: z.string().optional().describe("BCP 47 locale string for day/month names. Defaults to 'en-US'"), weekStartsOn: z.number().optional().describe("Day the week starts on (0=Sunday, 1=Monday). Defaults to 0"), }); export interface CalendarRootProps extends z.infer { onValueChange?: (date: string) => void; onMonthChange?: (month: number, year: number) => void; children: JSX.Element; } export interface CalendarHeaderProps extends JSX.HTMLAttributes { children: JSX.Element; } export interface CalendarHeadingProps extends JSX.HTMLAttributes {} export interface CalendarNavProps extends JSX.HTMLAttributes { children: JSX.Element; } export interface CalendarPrevProps extends JSX.ButtonHTMLAttributes { children?: JSX.Element; } export interface CalendarNextProps extends JSX.ButtonHTMLAttributes { children?: JSX.Element; } export interface CalendarGridProps extends JSX.HTMLAttributes { children: JSX.Element; } export interface CalendarGridHeadProps extends JSX.HTMLAttributes {} export interface CalendarGridBodyProps extends JSX.HTMLAttributes {} export interface CalendarCellProps extends JSX.HTMLAttributes { date: string; children?: JSX.Element; } export const CalendarMeta: ComponentMeta = { name: "Calendar", description: "Month grid for date selection with keyboard navigation, locale support, and min/max constraints", parts: ["Root", "Header", "Heading", "Nav", "PrevButton", "NextButton", "Grid", "GridHead", "GridBody", "Cell"] as const, requiredParts: ["Root", "Grid", "GridBody", "Cell"] as const, } as const; ``` - [ ] **Step 2: Create calendar-context.ts** The context provides the current month/year state, locale helpers, selected date, and navigation methods. Read existing context patterns (e.g., `dialog-context.ts`) and follow the same structure with `[PettyUI]` error prefix. Key context fields: ```typescript interface CalendarContextValue { focusedDate: Accessor; selectedDate: Accessor; displayMonth: Accessor; displayYear: Accessor; locale: Accessor; weekStartsOn: Accessor; minDate: Accessor; maxDate: Accessor; disabled: Accessor; selectDate: (date: string) => void; focusDate: (date: Date) => void; goToPrevMonth: () => void; goToNextMonth: () => void; isDateDisabled: (date: string) => boolean; isDateSelected: (date: string) => boolean; isDateToday: (date: string) => boolean; getDaysInMonth: () => Date[]; getWeekDayNames: () => string[]; } ``` - [ ] **Step 3: Create calendar-root.tsx** Uses `createControllableSignal` for selected date. Manages month/year display state. Provides context. Renders no DOM — just context providers around children. Key implementation: - Parse ISO date strings to Date objects for internal calculations - `getDaysInMonth()` returns array of Date objects for the current display month - `getWeekDayNames()` uses `Intl.DateTimeFormat` with the configured locale - Keyboard navigation: Arrow keys move focus, Enter/Space selects - Month navigation: goToPrevMonth/goToNextMonth update display state - [ ] **Step 4: Create sub-components** **calendar-header.tsx:** Simple `
` wrapper. **calendar-heading.tsx:** Renders current month/year from context using `Intl.DateTimeFormat`: ```typescript const heading = () => { const date = new Date(ctx.displayYear(), ctx.displayMonth()); return new Intl.DateTimeFormat(ctx.locale(), { month: "long", year: "numeric" }).format(date); }; return

{heading()}

; ``` **calendar-nav.tsx:** Container for prev/next buttons. Contains CalendarPrev and CalendarNext sub-components that call `ctx.goToPrevMonth()` / `ctx.goToNextMonth()`. **calendar-grid.tsx:** ``. **calendar-grid-head.tsx:** Renders weekday headers using `ctx.getWeekDayNames()` as `` with `` rows with slots for CalendarCell. Computes the grid layout from `ctx.getDaysInMonth()`. **calendar-cell.tsx:** Individual day cell. Sets data attributes: `data-selected`, `data-today`, `data-disabled`, `data-outside-month`. Calls `ctx.selectDate()` on click. - [ ] **Step 5: Create index.ts** ```typescript import { CalendarRoot } from "./calendar-root"; import { CalendarHeader } from "./calendar-header"; import { CalendarHeading } from "./calendar-heading"; import { CalendarNav } from "./calendar-nav"; import { CalendarGrid } from "./calendar-grid"; import { CalendarGridHead } from "./calendar-grid-head"; import { CalendarGridBody } from "./calendar-grid-body"; import { CalendarCell } from "./calendar-cell"; export const Calendar = Object.assign(CalendarRoot, { Header: CalendarHeader, Heading: CalendarHeading, Nav: CalendarNav, Grid: CalendarGrid, GridHead: CalendarGridHead, GridBody: CalendarGridBody, Cell: CalendarCell, }); export type { CalendarRootProps, CalendarCellProps } from "./calendar.props"; export { CalendarRootPropsSchema, CalendarMeta } from "./calendar.props"; ``` - [ ] **Step 6: Write test** Create `packages/core/tests/components/calendar/calendar.test.tsx`: ```typescript import { render, screen, fireEvent } from "@solidjs/testing-library"; import { createSignal } from "solid-js"; import { describe, expect, it } from "vitest"; import { Calendar } from "../../../src/components/calendar/index"; import { CalendarRootPropsSchema, CalendarMeta } from "../../../src/components/calendar/calendar.props"; describe("Calendar", () => { it("renders month grid with day cells", () => { render(() => ( )); expect(screen.getByText("March 2026")).toBeTruthy(); }); it("calls onValueChange when date selected", () => { let selected = ""; render(() => ( { selected = d; }}> )); const cells = screen.getAllByRole("gridcell"); if (cells.length > 0) fireEvent.click(cells[10]); expect(selected).toBeTruthy(); }); it("schema validates date constraints", () => { expect(CalendarRootPropsSchema.safeParse({ value: "2026-03-15", minDate: "2026-01-01", maxDate: "2026-12-31", locale: "en-US", weekStartsOn: 1, }).success).toBe(true); }); it("meta has required fields", () => { expect(CalendarMeta.name).toBe("Calendar"); expect(CalendarMeta.parts).toContain("Root"); expect(CalendarMeta.parts).toContain("Grid"); expect(CalendarMeta.parts).toContain("Cell"); }); }); ``` - [ ] **Step 7: Run tests and commit** ```bash cd packages/core && pnpm vitest run tests/components/calendar/ git add packages/core/src/components/calendar/ packages/core/tests/components/calendar/ git commit -m "feat(calendar): add Calendar component with locale support and keyboard nav" ``` --- ### Task 4: DatePicker Component **Depends on:** Task 3 (Calendar) **Files:** - Create: `packages/core/src/components/date-picker/date-picker.props.ts` - Create: `packages/core/src/components/date-picker/date-picker-context.ts` - Create: `packages/core/src/components/date-picker/date-picker-root.tsx` - Create: `packages/core/src/components/date-picker/date-picker-input.tsx` - Create: `packages/core/src/components/date-picker/date-picker-trigger.tsx` - Create: `packages/core/src/components/date-picker/date-picker-content.tsx` - Create: `packages/core/src/components/date-picker/index.ts` - Create: `packages/core/tests/components/date-picker/date-picker.test.tsx` Composes Calendar inside a Popover-like dropdown anchored to an input. - [ ] **Step 1: Create date-picker.props.ts** ```typescript import { z } from "zod/v4"; import type { JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; export const DatePickerRootPropsSchema = z.object({ value: z.string().optional().describe("Controlled selected date as ISO string (YYYY-MM-DD)"), defaultValue: z.string().optional().describe("Initial selected date (uncontrolled)"), open: z.boolean().optional().describe("Controlled popover open state"), defaultOpen: z.boolean().optional().describe("Initial popover open state"), minDate: z.string().optional().describe("Earliest selectable date"), maxDate: z.string().optional().describe("Latest selectable date"), disabled: z.boolean().optional().describe("Whether the date picker is disabled"), required: z.boolean().optional().describe("Whether a date selection is required"), name: z.string().optional().describe("Form field name for submission"), placeholder: z.string().optional().describe("Placeholder for the input. Defaults to 'Select date'"), locale: z.string().optional().describe("BCP 47 locale for formatting. Defaults to 'en-US'"), format: z.string().optional().describe("Display format string. Defaults to locale default"), }); export interface DatePickerRootProps extends z.infer { onValueChange?: (date: string) => void; onOpenChange?: (open: boolean) => void; children: JSX.Element; } export interface DatePickerInputProps extends Omit, "value"> {} export interface DatePickerTriggerProps extends JSX.ButtonHTMLAttributes { children?: JSX.Element; } export interface DatePickerContentProps extends JSX.HTMLAttributes { children: JSX.Element; } export const DatePickerMeta: ComponentMeta = { name: "DatePicker", description: "Date input with dropdown calendar for selecting dates, with locale formatting and constraints", parts: ["Root", "Input", "Trigger", "Content", "Calendar"] as const, requiredParts: ["Root", "Input", "Content"] as const, } as const; ``` - [ ] **Step 2: Create context, root, and sub-components** The DatePicker wraps a disclosure state (open/close) + a Calendar value. The root provides context containing: - `value`/`setValue` for the selected date - `open`/`setOpen` for the popover - `formatDate()` helper using `Intl.DateTimeFormat` **date-picker-root.tsx:** Uses `createDisclosureState` for open, `createControllableSignal` for value. When a date is selected in the Calendar, it closes the popover and updates the value. **date-picker-input.tsx:** Read-only input showing the formatted date. Clicking opens the calendar. A hidden `` for form submission. **date-picker-trigger.tsx:** Button that toggles the popover open state. **date-picker-content.tsx:** Floating content panel using `createFloating` for positioning. Contains the Calendar inside. - [ ] **Step 3: Create index.ts** ```typescript import { DatePickerRoot } from "./date-picker-root"; import { DatePickerInput } from "./date-picker-input"; import { DatePickerTrigger } from "./date-picker-trigger"; import { DatePickerContent } from "./date-picker-content"; export const DatePicker = Object.assign(DatePickerRoot, { Input: DatePickerInput, Trigger: DatePickerTrigger, Content: DatePickerContent, }); export type { DatePickerRootProps, DatePickerInputProps, DatePickerTriggerProps, DatePickerContentProps } from "./date-picker.props"; export { DatePickerRootPropsSchema, DatePickerMeta } from "./date-picker.props"; ``` - [ ] **Step 4: Write test** Create `packages/core/tests/components/date-picker/date-picker.test.tsx`: ```typescript import { render, screen } from "@solidjs/testing-library"; import { describe, expect, it } from "vitest"; import { DatePicker } from "../../../src/components/date-picker/index"; import { DatePickerRootPropsSchema, DatePickerMeta } from "../../../src/components/date-picker/date-picker.props"; describe("DatePicker", () => { it("renders input with placeholder", () => { render(() => ( Open
Calendar goes here
)); const input = screen.getByTestId("input"); expect(input).toBeTruthy(); }); it("shows formatted date when value set", () => { render(() => (
Calendar
)); expect(screen.getByTestId("input")).toBeTruthy(); }); it("schema validates", () => { expect(DatePickerRootPropsSchema.safeParse({ value: "2026-03-15", minDate: "2026-01-01", locale: "en-US", }).success).toBe(true); }); it("meta has required fields", () => { expect(DatePickerMeta.name).toBe("DatePicker"); expect(DatePickerMeta.parts).toContain("Root"); expect(DatePickerMeta.parts).toContain("Input"); expect(DatePickerMeta.parts).toContain("Content"); }); }); ``` - [ ] **Step 5: Run tests and commit** ```bash cd packages/core && pnpm vitest run tests/components/date-picker/ git add packages/core/src/components/date-picker/ packages/core/tests/components/date-picker/ git commit -m "feat(date-picker): add DatePicker with Calendar integration and locale formatting" ``` --- ### Task 5: CommandPalette Component **Files:** - Create: `packages/core/src/components/command-palette/command-palette.props.ts` - Create: `packages/core/src/components/command-palette/command-palette-context.ts` - Create: `packages/core/src/components/command-palette/command-palette-root.tsx` - Create: `packages/core/src/components/command-palette/command-palette-input.tsx` - Create: `packages/core/src/components/command-palette/command-palette-list.tsx` - Create: `packages/core/src/components/command-palette/command-palette-item.tsx` - Create: `packages/core/src/components/command-palette/command-palette-group.tsx` - Create: `packages/core/src/components/command-palette/command-palette-empty.tsx` - Create: `packages/core/src/components/command-palette/command-palette-separator.tsx` - Create: `packages/core/src/components/command-palette/index.ts` - Create: `packages/core/tests/components/command-palette/command-palette.test.tsx` Follows the cmdk pattern: search input filters a list of actions with keyboard navigation. - [ ] **Step 1: Create command-palette.props.ts** ```typescript import { z } from "zod/v4"; import type { JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; export const CommandPaletteRootPropsSchema = z.object({ value: z.string().optional().describe("Controlled selected item value"), defaultValue: z.string().optional().describe("Initial selected item (uncontrolled)"), search: z.string().optional().describe("Controlled search input value"), defaultSearch: z.string().optional().describe("Initial search value (uncontrolled)"), loop: z.boolean().optional().describe("Whether keyboard navigation wraps. Defaults to true"), filter: z.boolean().optional().describe("Whether built-in filtering is enabled. Defaults to true"), }); export interface CommandPaletteRootProps extends z.infer { onValueChange?: (value: string) => void; onSearchChange?: (search: string) => void; onSelect?: (value: string) => void; children: JSX.Element; } export interface CommandPaletteInputProps extends Omit, "value" | "onInput"> { placeholder?: string; } export interface CommandPaletteListProps extends JSX.HTMLAttributes { children: JSX.Element; } export interface CommandPaletteItemProps extends JSX.HTMLAttributes { value: string; keywords?: string[]; disabled?: boolean; onSelect?: () => void; children?: JSX.Element; } export interface CommandPaletteGroupProps extends JSX.HTMLAttributes { heading?: string; children: JSX.Element; } export interface CommandPaletteEmptyProps extends JSX.HTMLAttributes { children?: JSX.Element; } export interface CommandPaletteSeparatorProps extends JSX.HTMLAttributes {} export const CommandPaletteMeta: ComponentMeta = { name: "CommandPalette", description: "Search-driven command menu for finding and executing actions, with keyboard navigation and grouping", parts: ["Root", "Input", "List", "Item", "Group", "Empty", "Separator"] as const, requiredParts: ["Root", "Input", "List", "Item"] as const, } as const; ``` - [ ] **Step 2: Create context** Context provides: search string, filtered items, highlighted value, select handler, filter function. Uses `createListNavigation` internally in activation mode. - [ ] **Step 3: Create root component** **command-palette-root.tsx:** Manages search state via `createControllableSignal`. Collects registered items and filters them based on search. Uses `createListNavigation` with `mode: "activation"`. Provides context to children. Key filtering logic: ```typescript const filteredItems = () => { if (!filterEnabled) return allItems(); const query = searchValue().toLowerCase(); if (!query) return allItems(); return allItems().filter((item) => { const text = item.value.toLowerCase(); const keywords = item.keywords?.join(" ").toLowerCase() ?? ""; return text.includes(query) || keywords.includes(query); }); }; ``` - [ ] **Step 4: Create sub-components** **command-palette-input.tsx:** `` that updates search state on input. Delegates keyDown to list navigation. `role="combobox"`, `aria-expanded`, `aria-activedescendant`. **command-palette-list.tsx:** Container for items. Spreads `containerProps` from `createListNavigation`. Shows `CommandPaletteEmpty` when no filtered items. **command-palette-item.tsx:** Individual action. Spreads `getItemProps(value)` from list navigation. `data-highlighted`, `data-disabled`. Calls `onSelect` on activation. **command-palette-group.tsx:** Groups items with optional heading. `role="group"`, `aria-label`. **command-palette-empty.tsx:** Shown when search yields no results. ``. **command-palette-separator.tsx:** Visual divider between groups. `role="separator"`. - [ ] **Step 5: Create index.ts** ```typescript import { CommandPaletteRoot } from "./command-palette-root"; import { CommandPaletteInput } from "./command-palette-input"; import { CommandPaletteList } from "./command-palette-list"; import { CommandPaletteItem } from "./command-palette-item"; import { CommandPaletteGroup } from "./command-palette-group"; import { CommandPaletteEmpty } from "./command-palette-empty"; import { CommandPaletteSeparator } from "./command-palette-separator"; export const CommandPalette = Object.assign(CommandPaletteRoot, { Input: CommandPaletteInput, List: CommandPaletteList, Item: CommandPaletteItem, Group: CommandPaletteGroup, Empty: CommandPaletteEmpty, Separator: CommandPaletteSeparator, }); export type { CommandPaletteRootProps, CommandPaletteInputProps, CommandPaletteItemProps, CommandPaletteGroupProps } from "./command-palette.props"; export { CommandPaletteRootPropsSchema, CommandPaletteMeta } from "./command-palette.props"; ``` - [ ] **Step 6: Write test** Create `packages/core/tests/components/command-palette/command-palette.test.tsx`: ```typescript import { render, screen, fireEvent } from "@solidjs/testing-library"; import { describe, expect, it } from "vitest"; import { CommandPalette } from "../../../src/components/command-palette/index"; import { CommandPaletteRootPropsSchema, CommandPaletteMeta } from "../../../src/components/command-palette/command-palette.props"; describe("CommandPalette", () => { it("renders input and items", () => { render(() => ( Copy Paste )); expect(screen.getByPlaceholderText("Search commands...")).toBeTruthy(); expect(screen.getByText("Copy")).toBeTruthy(); expect(screen.getByText("Paste")).toBeTruthy(); }); it("renders groups with headings", () => { render(() => ( Save )); expect(screen.getByText("Actions")).toBeTruthy(); expect(screen.getByText("Save")).toBeTruthy(); }); it("calls onSelect when item activated", () => { let selected = ""; render(() => ( { selected = v; }}> Run )); fireEvent.click(screen.getByText("Run")); expect(selected).toBe("run"); }); it("schema validates", () => { expect(CommandPaletteRootPropsSchema.safeParse({ search: "test", loop: true, filter: false }).success).toBe(true); }); it("meta has fields", () => { expect(CommandPaletteMeta.name).toBe("CommandPalette"); expect(CommandPaletteMeta.parts).toContain("Root"); expect(CommandPaletteMeta.parts).toContain("Input"); expect(CommandPaletteMeta.parts).toContain("Item"); }); }); ``` - [ ] **Step 7: Run tests and commit** ```bash cd packages/core && pnpm vitest run tests/components/command-palette/ git add packages/core/src/components/command-palette/ packages/core/tests/components/command-palette/ git commit -m "feat(command-palette): add CommandPalette with search, keyboard nav, and grouping" ``` --- ### Task 6: Form Component **Files:** - Create: `packages/core/src/components/form/form.props.ts` - Create: `packages/core/src/components/form/form-context.ts` - Create: `packages/core/src/components/form/form-root.tsx` - Create: `packages/core/src/components/form/form-field.tsx` - Create: `packages/core/src/components/form/form-label.tsx` - Create: `packages/core/src/components/form/form-control.tsx` - Create: `packages/core/src/components/form/form-description.tsx` - Create: `packages/core/src/components/form/form-error-message.tsx` - Create: `packages/core/src/components/form/form-submit.tsx` - Create: `packages/core/src/components/form/index.ts` - Create: `packages/core/tests/components/form/form.test.tsx` The Form integrates with Zod v4 for validation — AI generates a schema, the form validates against it. - [ ] **Step 1: Create form.props.ts** ```typescript import { z } from "zod/v4"; import type { JSX } from "solid-js"; import type { ComponentMeta } from "../../meta"; export const FormRootPropsSchema = z.object({ disabled: z.boolean().optional().describe("Disable all fields in the form"), validateOn: z.enum(["submit", "blur", "change"]).optional().describe("When to validate. Defaults to 'submit'"), }); export interface FormRootProps extends z.infer, Omit, "onSubmit"> { schema?: z.ZodType; onSubmit?: (values: Record, event: SubmitEvent) => void; onValidationError?: (errors: Record) => void; children: JSX.Element; } export const FormFieldPropsSchema = z.object({ name: z.string().describe("Field name matching the schema key"), }); export interface FormFieldProps extends z.infer { children: JSX.Element; } export interface FormLabelProps extends JSX.LabelHTMLAttributes { children?: JSX.Element; } export interface FormControlProps { children: (props: { id: string; name: string; "aria-describedby"?: string; "aria-invalid"?: boolean }) => JSX.Element; } export interface FormDescriptionProps extends JSX.HTMLAttributes { children?: JSX.Element; } export interface FormErrorMessageProps extends JSX.HTMLAttributes { children?: JSX.Element | ((errors: string[]) => JSX.Element); } export interface FormSubmitProps extends JSX.ButtonHTMLAttributes { children?: JSX.Element; } export const FormMeta: ComponentMeta = { name: "Form", description: "Form with Zod v4 schema validation, field-level error display, and accessible aria linking", parts: ["Root", "Field", "Label", "Control", "Description", "ErrorMessage", "Submit"] as const, requiredParts: ["Root", "Field"] as const, } as const; ``` - [ ] **Step 2: Create form context** Two contexts: FormContext (root-level — schema, errors store, submit handler, validateOn) and FormFieldContext (per-field — name, fieldId, errors, touched). Key form context: ```typescript interface FormContextValue { errors: Accessor>; getFieldErrors: (name: string) => string[]; setFieldValue: (name: string, value: unknown) => void; getFieldValue: (name: string) => unknown; validateField: (name: string) => void; validateAll: () => boolean; disabled: Accessor; validateOn: Accessor<"submit" | "blur" | "change">; } ``` - [ ] **Step 3: Create form-root.tsx** Uses a reactive store (`createStore`) for field values and errors. On submit: 1. Collect all field values 2. If `schema` provided, validate with `schema.safeParse(values)` 3. If valid, call `onSubmit(values, event)` 4. If invalid, map Zod errors to field-level error arrays, call `onValidationError` ```typescript const handleSubmit: JSX.EventHandlerUnion = (e) => { e.preventDefault(); if (local.schema) { const result = local.schema.safeParse(formValues); if (!result.success) { const fieldErrors: Record = {}; for (const issue of result.error.issues) { const path = issue.path.join("."); if (!fieldErrors[path]) fieldErrors[path] = []; fieldErrors[path].push(issue.message); } setErrors(fieldErrors); local.onValidationError?.(fieldErrors); return; } setErrors({}); local.onSubmit?.(result.data, e); } else { local.onSubmit?.(formValues, e); } }; ``` - [ ] **Step 4: Create form sub-components** **form-field.tsx:** Provides per-field context. Generates unique IDs for label/description/error linking. Calls `validateField` on blur/change based on `validateOn`. **form-label.tsx:** `
` for each day. **calendar-grid-body.tsx:** Renders weeks as `
` element with context provider. - [ ] **Step 4: Create sub-components** **data-table-header.tsx:** `` with auto-generated header row from columns if no children. **data-table-header-cell.tsx:** `` that renders rows. If render prop `children` is provided, maps over `pageData`. Otherwise auto-renders cells from column definitions. **data-table-row.tsx:** `` with optional selection checkbox. `data-selected`, `aria-selected`. **data-table-cell.tsx:** `
` for a column. Shows sort indicator. Calls `toggleSort` on click for sortable columns. `aria-sort`. **data-table-body.tsx:** `
` wrapper. **data-table-pagination.tsx:** Pagination controls. Shows page info, prev/next buttons, page size selector. - [ ] **Step 5: Create index.ts, write test, run and commit** Test covers: rendering table with columns and data, sorting on header click, filtering, pagination, row selection, schema validation, meta fields. ```bash cd packages/core && pnpm vitest run tests/components/data-table/ git add packages/core/src/components/data-table/ packages/core/tests/components/data-table/ git commit -m "feat(data-table): add DataTable with sorting, filtering, pagination, and row selection" ``` --- ### Task 9: Add All New Components to Build Config + Full Verification **Depends on:** All previous tasks **Files:** - Modify: `packages/core/tsdown.config.ts` - Modify: `packages/core/package.json` - [ ] **Step 1: Add components to tsdown.config.ts** Add to the `components` array: ``` "virtual-list", "calendar", "date-picker", "command-palette", "form", "wizard", "data-table" ``` - [ ] **Step 2: Add exports to package.json** Add 7 new export entries following the existing pattern: ```json "./virtual-list": { "solid": "...", "import": "...", "require": "..." }, "./calendar": { ... }, "./date-picker": { ... }, "./command-palette": { ... }, "./form": { ... }, "./wizard": { ... }, "./data-table": { ... } ``` - [ ] **Step 3: Add createVirtualizer to primitives exports (optional)** If primitives should be importable directly, add to tsdown config: ```typescript entry["primitives/create-virtualizer"] = "src/primitives/create-virtualizer.ts"; ``` - [ ] **Step 4: Run full test suite** ```bash cd packages/core && pnpm vitest run ``` Expected: All tests pass (Phase 1 tests + Phase 2 tests). - [ ] **Step 5: Run full build** ```bash cd packages/core && pnpm build ``` Expected: Build succeeds with all new component entries. - [ ] **Step 6: Verify export count** ```bash node -e 'console.log(Object.keys(require("./packages/core/package.json").exports).length)' ``` Expected: 45 (38 from Phase 1 + 7 new). - [ ] **Step 7: Commit** ```bash git add packages/core/tsdown.config.ts packages/core/package.json git commit -m "feat: add Phase 2 components to build config — 39 component exports total" ``` --- ## Verification Checklist After all tasks complete: | Component | Schema | Meta | Tests | Export | |-----------|--------|------|-------|--------| | createVirtualizer | N/A | N/A | ✅ | primitive | | VirtualList | ✅ | ✅ | ✅ | `./virtual-list` | | Calendar | ✅ | ✅ | ✅ | `./calendar` | | DatePicker | ✅ | ✅ | ✅ | `./date-picker` | | CommandPalette | ✅ | ✅ | ✅ | `./command-palette` | | Form | ✅ | ✅ | ✅ | `./form` | | Wizard | ✅ | ✅ | ✅ | `./wizard` | | DataTable | ✅ | ✅ | ✅ | `./data-table` |