PettyUI/docs/superpowers/specs/2026-03-30-pettyui-v2-web-components-design.md
2026-03-30 12:08:51 +07:00

14 KiB

PettyUI v2: Web Components Architecture

Overview

PettyUI v2 is a complete rewrite from SolidJS to vanilla Web Components. Zero framework dependencies. Zero external runtime. Built on modern web platform APIs (Popover, <dialog>, Invoker Commands, Navigation API, View Transitions). AI-native with Zod schemas and MCP tools.

Goal: The smallest, fastest headless UI component library that exists, because it builds on the browser instead of replacing it. Capable of powering full SPA experiences with zero client-side framework.

Non-goals: Styled components. Shadow DOM. Framework-specific bindings. Build step requirements.

Architecture

Core Principles

  1. Browser-first — use native APIs before writing JS. Popover API for overlays, <dialog> for modals, Invoker Commands for triggers, Navigation API for routing, View Transitions for animations, <details> for disclosure.
  2. Progressive enhancement — components that can work before JS loads DO work before JS loads. JS upgrades add ARIA, keyboard navigation, and programmatic APIs.
  3. Zero dependencies — no Lit, no Floating UI, no framework. The only runtime is a ~30-line signals utility (~500 bytes) bundled inline.
  4. AI-native — Zod schemas describe every component's API. MCP tools (discover, inspect, compose, validate, add) let AI agents generate correct HTML without reading docs.
  5. Full SPA capability — Navigation API + View Transitions replace client-side routers. PettyUI apps feel like SPAs with zero framework overhead.

Tech Stack

  • Language: TypeScript, compiled to ES modules
  • Reactivity: Homegrown signals (~30 lines, ~500 bytes), aligned with TC39 Signals proposal
  • Components: Custom Elements v1 (no Shadow DOM)
  • Positioning: Simple CSS (relative/absolute). Consumer adds Floating UI if they need viewport-edge flipping — that's a styling concern, not a library concern.
  • Overlays: Popover API + <dialog> element
  • Triggers: Invoker Commands API (commandfor/command attributes)
  • Routing: Navigation API (browser-native SPA router, Baseline Jan 2026)
  • Transitions: View Transitions API (smooth page/component animations, Baseline 2025)
  • Build: tsdown (same as v1)
  • Testing: Vitest + @web/test-runner (real browser tests for Custom Elements)
  • Validation: Zod v4 schemas (carried over from v1)

Signals Utility

The entire reactivity system, bundled with the library (not an external dep):

let current: (() => void) | null = null;

interface Signal<T> {
  get(): T;
  set(value: T): void;
}

function signal<T>(value: T): Signal<T> {
  const subs = new Set<() => void>();
  return {
    get() { if (current) subs.add(current); return value; },
    set(v: T) { value = v; for (const fn of subs) fn(); },
  };
}

function effect(fn: () => void): void {
  const prev = current;
  current = fn;
  fn();
  current = prev;
}

Components use signal() for internal state and effect() to sync state to DOM attributes/properties.

Component Anatomy

Three Tiers

Tier 1 — HTML-native (works without JS):

  • Dialog, Popover, Tooltip, Collapsible/Accordion (via <details>), Alert, Separator, Link
  • JS upgrade adds: ARIA linking, keyboard nav, custom events, programmatic API

Tier 2 — Popover-enhanced (partially works without JS):

  • Select, DropdownMenu, ContextMenu, HoverCard, DatePicker, CommandPalette
  • Popover API handles open/close. JS adds: keyboard nav, typeahead, ARIA, selection state

Tier 3 — JS-required (inert until upgrade):

  • Tabs, Slider, Calendar, DataTable, VirtualList, Combobox, Wizard, Form, NumberField, Toast, Pagination
  • These components need JS for core functionality. They render as plain HTML until the custom element upgrades.

Composition: Nested Custom Elements

Complex components use nested custom elements. Each part is a registered element with its own lifecycle. Parts find their parent via this.closest().

<petty-dialog>
  <button commandfor="my-dlg" command="show-modal">Open</button>
  <dialog id="my-dlg">
    <petty-dialog-title>Confirm</petty-dialog-title>
    <petty-dialog-description>Are you sure?</petty-dialog-description>
    <button commandfor="my-dlg" command="close">Cancel</button>
  </dialog>
</petty-dialog>

Rule: If a part needs its own behavior (keyboard handling, ARIA management, event emission), it's a custom element. If it's pure structure, it's a plain element with data-part for styling hooks.

Data Attributes

Following the established convention (Radix, Shoelace):

  • data-state — component state: open, closed, active, inactive, checked, unchecked, on, off
  • data-part — structural identification for CSS targeting
  • data-disabled — disabled state flag
  • data-orientationhorizontal or vertical

Event Pattern

Components dispatch standard CustomEvents with the petty- prefix:

this.dispatchEvent(new CustomEvent("petty-close", {
  bubbles: true,
  detail: { value: "confirm" },
}));

Consumers listen with standard DOM:

dialog.addEventListener("petty-close", (e) => {
  if (e.detail.value === "confirm") deleteItem();
});

Property/Attribute Reflection

Components expose both property and attribute APIs:

// Attribute API (HTML)
<petty-select value="apple">

// Property API (JS)
document.querySelector("petty-select").value = "apple";

Properties are the source of truth. Attributes reflect via attributeChangedCallback → property setter. This avoids the string-only limitation of attributes for complex values.

Component List (44 components, same as v1)

Layout & Display (6)

  • petty-avatar — image with fallback
  • petty-badge — status indicator (simple element, no sub-parts)
  • petty-card — container with petty-card-header, petty-card-content, petty-card-footer
  • petty-image — image with fallback
  • petty-separator<hr> with role and orientation
  • petty-skeleton — loading placeholder

Inputs & Forms (14)

  • petty-button<button> wrapper with loading/disabled state
  • petty-text-field — label + input + description + error
  • petty-number-field — input with increment/decrement
  • petty-checkbox — tri-state checkbox
  • petty-switch — on/off toggle
  • petty-radio-group + petty-radio-item — mutually exclusive options
  • petty-slider — range input with track/thumb
  • petty-toggle — pressed/unpressed button
  • petty-toggle-group + items — single/multi toggle selection
  • petty-select + petty-select-trigger, petty-select-content, petty-select-option — dropdown select using Popover API
  • petty-combobox + parts — searchable select
  • petty-listbox + petty-listbox-option — inline selectable list
  • petty-form + petty-form-field — Zod-validated form
  • petty-date-picker + parts — date input with calendar popover

Navigation (8)

  • petty-link — anchor with external/disabled support
  • petty-breadcrumbs + petty-breadcrumb-item — navigation trail
  • petty-tabs + petty-tab, petty-tab-panel — tabbed interface
  • petty-accordion + petty-accordion-item — collapsible sections (uses <details> internally)
  • petty-collapsible — single disclosure (uses <details> internally)
  • petty-pagination + parts — page navigation
  • petty-nav-menu + parts — horizontal nav with dropdowns
  • petty-wizard + parts — multi-step flow

Overlays (9)

  • petty-dialog — wraps native <dialog> with ARIA + events
  • petty-alert-dialog — confirmation dialog
  • petty-drawer — slide-in panel (uses <dialog>)
  • petty-popover — floating content (uses Popover API)
  • petty-tooltip — hover label (uses Popover API + CSS Anchor)
  • petty-hover-card — rich hover preview
  • petty-dropdown-menu + items — action menu (uses Popover API)
  • petty-context-menu + items — right-click menu
  • petty-command-palette + parts — search-driven command menu

Feedback & Status (4)

  • petty-alert — inline status message
  • petty-toast + petty-toast-region — temporary notification
  • petty-progress — progress bar
  • petty-meter — value gauge

Data (3)

  • petty-calendar + parts — month grid date picker
  • petty-data-table + parts — sortable/filterable table
  • petty-virtual-list — windowed scroll

SPA Router (petty-router)

PettyUI includes an optional ~15-line SPA router built entirely on browser standards. No external dependency.

How it works

The Navigation API (Baseline January 2026) provides browser-native SPA routing. View Transitions API (Baseline 2025) animates page swaps. Together they replace React Router, Vue Router, and every other client-side routing library.

// petty-router: the entire implementation
navigation.addEventListener("navigate", (event) => {
  const url = new URL(event.destination.url);
  if (url.origin !== location.origin) return;

  event.intercept({
    async handler() {
      const res = await fetch(url.pathname, {
        headers: { "X-Partial": "true" }
      });
      const html = await res.text();

      document.startViewTransition(() => {
        document.querySelector("[data-petty-outlet]").innerHTML = html;
      });
    }
  });
});

Consumer usage

<script type="module">
  import "pettyui/router";
</script>

<nav>
  <a href="/dashboard">Dashboard</a>
  <a href="/settings">Settings</a>
</nav>

<main data-petty-outlet>
  <!-- Content swapped here on navigation -->
</main>

Clicking a link: Navigation API intercepts → fetches HTML from server → View Transitions animates the swap → PettyUI components in the new HTML upgrade instantly. Back/forward buttons work. URL bar updates. No full page reload.

What the server does

The server checks for the X-Partial header. If present, it returns just the page fragment (no <html>, <head>, etc.). If absent, it returns the full page (for direct URL visits, bookmarks, SEO crawlers).

Browser support

API Chrome Firefox Safari Edge
Navigation API 102+ 136+ 18.2+ 102+
View Transitions (same-doc) 111+ 140+ 18+ 111+

Both are Baseline. For the rare old browser without support, links just work as normal full-page navigations — progressive enhancement.

Platform APIs Usage Map

Browser API Replaces (v1) Used by
Popover API createDismiss(), z-index management Select, DropdownMenu, ContextMenu, Tooltip, Popover, HoverCard, CommandPalette, DatePicker
<dialog> createFocusTrap(), scroll lock, backdrop Dialog, AlertDialog, Drawer
Invoker Commands JS click handlers for open/close Dialog, AlertDialog, Popover triggers
Navigation API React Router / client-side routers petty-router (SPA navigation)
View Transitions Animation JS / Framer Motion petty-router (page transitions), Accordion, Dialog enter/exit
<details>/<summary> Disclosure state management Accordion, Collapsible
inert attribute Manual aria-hidden + tabindex management Dialog background freeze

File Structure

packages/core/
  src/
    signals.ts              — signal() + effect() (~30 lines)
    router.ts               — petty-router SPA navigation (~15 lines)
    shared/
      keyboard.ts           — shared keyboard navigation utilities
      aria.ts               — shared ARIA helpers
    components/
      dialog/
        dialog.ts           — PettyDialog custom element
        dialog-title.ts     — PettyDialogTitle
        dialog-description.ts
        dialog.schema.ts    — Zod schema (carried from v1)
        dialog.test.ts
        index.ts            — registration + exports
      select/
        select.ts
        select-trigger.ts
        select-content.ts
        select-option.ts
        select.schema.ts
        select.test.ts
        index.ts
      ... (same pattern for all 44)
    index.ts                — registers all elements, exports all
  package.json
  tsconfig.json

Each component is independently importable:

import "pettyui/dialog";  // registers <petty-dialog> etc.

Or consumers include a script tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/pettyui/dialog"></script>

Migration from v1

What carries over unchanged

  • Zod schemas (props definitions, validation)
  • MCP tools architecture (discover, inspect, compose, validate, add)
  • Component registry metadata
  • Test scenarios (same behaviors, different implementation)
  • NagLint rules

What gets rewritten

  • All 44 component implementations (SolidJS JSX → Custom Elements)
  • Utility primitives (createFloating → CSS anchor, createDismiss → Popover API, createFocusTrap → <dialog>)
  • Test harness (SolidJS testing library → DOM-based testing)

What gets deleted

  • solid-js peer dependency
  • @floating-ui/dom dependency
  • vite-plugin-solid
  • All .tsx files (replaced by .ts)
  • Compound component Object.assign pattern

Phased Rollout

Phase 1: Foundation

  • Signals utility
  • Shared utilities (keyboard, ARIA, anchor fallback)
  • Base component patterns established

Phase 2: Simple Components (Tier 1 + easy Tier 3)

  • Dialog, Alert, AlertDialog, Collapsible, Accordion
  • Button, Badge, Separator, Link, Toggle, Skeleton, Avatar, Image
  • Progress, Meter

Phase 3: Selection & Input Components

  • Select, Combobox, Listbox
  • TextField, NumberField, Checkbox, Switch, RadioGroup, Slider
  • ToggleGroup, Form

Phase 4: Complex Components

  • Tabs, Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu
  • Drawer, CommandPalette, DatePicker, Calendar
  • Pagination, NavigationMenu, Wizard, Breadcrumbs

Phase 5: Data Components + MCP Update

  • DataTable, VirtualList, Toast
  • Update MCP tools for new architecture
  • Update registry

Size Budget

Item v1 (SolidJS) v2 (Web Components)
Framework runtime solid-js ~23KB 0KB
Floating UI @floating-ui/dom ~15KB 0KB (simple CSS)
Router N/A (framework-specific) ~400 bytes (Navigation API wrapper)
Signals utility (included in solid-js) ~500 bytes
Per-component avg ~2-4KB ~0.5-2KB
Total (all 44 + router) ~80-100KB ~20-35KB

Success Criteria

  1. Zero external runtime dependencies
  2. Every component passes existing accessibility test scenarios
  3. Tier 1 components function without JavaScript
  4. Total gzipped bundle (all 44 components) under 15KB
  5. INP < 100ms on component interactions (no hydration)
  6. MCP tools work with the new architecture
  7. Works in Chrome, Firefox, Safari, Edge (latest 2 versions)