379 lines
14 KiB
Markdown
379 lines
14 KiB
Markdown
# 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):
|
|
|
|
```ts
|
|
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()`.
|
|
|
|
```html
|
|
<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-orientation` — `horizontal` or `vertical`
|
|
|
|
### Event Pattern
|
|
|
|
Components dispatch standard `CustomEvent`s with the `petty-` prefix:
|
|
|
|
```ts
|
|
this.dispatchEvent(new CustomEvent("petty-close", {
|
|
bubbles: true,
|
|
detail: { value: "confirm" },
|
|
}));
|
|
```
|
|
|
|
Consumers listen with standard DOM:
|
|
```js
|
|
dialog.addEventListener("petty-close", (e) => {
|
|
if (e.detail.value === "confirm") deleteItem();
|
|
});
|
|
```
|
|
|
|
### Property/Attribute Reflection
|
|
|
|
Components expose both property and attribute APIs:
|
|
|
|
```ts
|
|
// 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.
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```html
|
|
<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:
|
|
```ts
|
|
import "pettyui/dialog"; // registers <petty-dialog> etc.
|
|
```
|
|
|
|
Or consumers include a script tag:
|
|
```html
|
|
<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)
|