Compare commits
No commits in common. "e8e811f711acffdc7fb5fc8ef5e639802d946bbc" and "bf576905a7cdf6a9308cf5f003840bb4df6ed81a" have entirely different histories.
e8e811f711
...
bf576905a7
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Biome check
|
||||
run: pnpm biome check packages/
|
||||
|
||||
- name: ESLint (Solid rules)
|
||||
run: pnpm eslint packages --ext .ts,.tsx
|
||||
|
||||
- name: Type check
|
||||
run: pnpm -r typecheck
|
||||
|
||||
- name: Tests
|
||||
run: pnpm -r test
|
||||
|
||||
- name: Build
|
||||
run: pnpm -r build
|
||||
36
biome.json
Normal file
36
biome.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"useTemplate": "error"
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["node_modules", "dist", ".tsdown", "coverage"]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,323 @@
|
||||
# PettyUI AI-First Architecture Design
|
||||
|
||||
**Date:** 2026-03-29
|
||||
**Status:** Approved
|
||||
**Supersedes:** All prior specs and plans in this directory
|
||||
|
||||
## Vision
|
||||
|
||||
PettyUI is the first component library where **the AI is the intended user, not an afterthought**. The primary consumer is LLMs generating code. Human developers benefit as a side effect of good AI ergonomics. Distribution follows the proven Radix → shadcn two-layer model: headless core + copy-paste styled registry.
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
core/ → Headless primitives with Zod-first props + meta
|
||||
mcp/ → MCP server: discovery, validation, code generation
|
||||
registry/ → Styled copy-paste components (shadcn layer for Solid)
|
||||
```
|
||||
|
||||
No separate `schemas/` package. The Zod schema lives WITH the component because it IS the component's type system.
|
||||
|
||||
## Component Inventory
|
||||
|
||||
Organized by **AI task** — what the agent is trying to build — not UI taxonomy.
|
||||
|
||||
### BUILD A FORM (AI's #1 task)
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| TextField | Done | |
|
||||
| Checkbox | Done | |
|
||||
| Switch | Done | |
|
||||
| RadioGroup | Done | |
|
||||
| Select | Done | |
|
||||
| Combobox | Done | |
|
||||
| Slider | Done | |
|
||||
| NumberField | Done | |
|
||||
| ToggleGroup | Done | |
|
||||
| **Form** | New | Validation integration (Zod v4), field grouping, error display, aria-describedby linking |
|
||||
| **DatePicker** | New | Calendar + input, range selection, locale-aware |
|
||||
| **Calendar** | New | Standalone month/year grid, keyboard nav |
|
||||
|
||||
### BUILD NAVIGATION
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Tabs | Done | |
|
||||
| Breadcrumbs | Done | |
|
||||
| Link | Done | |
|
||||
| DropdownMenu | Done | |
|
||||
| **CommandPalette** | New | cmdk pattern — search, keyboard nav, grouped actions |
|
||||
| **NavigationMenu** | New | Horizontal nav with dropdown submenus, hover intent |
|
||||
|
||||
### BUILD AN OVERLAY
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Dialog | Done | |
|
||||
| AlertDialog | Done | |
|
||||
| Drawer | Done | |
|
||||
| Popover | Done | |
|
||||
| Tooltip | Done | |
|
||||
| HoverCard | Done | |
|
||||
| Toast | Done | |
|
||||
|
||||
### BUILD A DASHBOARD / DATA VIEW
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Progress | Done | |
|
||||
| Badge | Done | |
|
||||
| Skeleton | Done | |
|
||||
| Alert | Done | |
|
||||
| **DataTable** | New | Sorting, filtering, pagination, column resize, row selection |
|
||||
| **VirtualList** | New | Virtualized rendering for large datasets, variable row heights |
|
||||
| **Avatar** | New | Image + fallback initials, status indicator |
|
||||
|
||||
### BUILD LAYOUT & STRUCTURE
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Accordion | Done | |
|
||||
| Collapsible | Done | |
|
||||
| **Card** | New | Header/Content/Footer compound, token contract |
|
||||
| **Wizard/Stepper** | New | Multi-step flows, step validation, linear/non-linear |
|
||||
|
||||
### BUILD INTERACTIONS
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Button | Done | |
|
||||
| Toggle | Done | |
|
||||
|
||||
### CUT (remove from exports, keep internally if needed)
|
||||
|
||||
| Component | Reason |
|
||||
|-----------|--------|
|
||||
| ContextMenu | Niche desktop pattern |
|
||||
| Image | `<img loading="lazy">` covers it |
|
||||
| Meter | Nobody uses it vs Progress |
|
||||
| Separator | Keep as menu sub-component only, drop standalone export |
|
||||
| Pagination | Keep as DataTable internal, drop standalone export |
|
||||
| ColorPicker suite | Design-tool-only, <5% of projects |
|
||||
| Menubar | Desktop OS pattern |
|
||||
| TimeField | Extremely specialized |
|
||||
| Rating | Trivial, single-purpose |
|
||||
| FileField | Browser native + dropzone libs |
|
||||
| SegmentedControl | ToggleGroup covers this |
|
||||
|
||||
### Totals
|
||||
|
||||
| Category | Done | Cut | New | Final Total |
|
||||
|----------|------|-----|-----|-------------|
|
||||
| Components | 28 | -3 | +10 | 35 |
|
||||
| Standalone exports | 28 | -5 | +10 | 33 |
|
||||
| Primitives | 5 | 0 | +1 (createVirtualizer) | 6 |
|
||||
| Utilities | 6 | 0 | 0 | 6 |
|
||||
|
||||
## Schema Architecture: Zod-First, Single Source of Truth
|
||||
|
||||
Every component's props are defined AS Zod schemas. TypeScript types are derived FROM the schemas. No duplication, no drift.
|
||||
|
||||
### Per-Component Pattern
|
||||
|
||||
```typescript
|
||||
// components/dialog/dialog.props.ts
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const DialogRootProps = z.object({
|
||||
open: z.boolean().optional()
|
||||
.describe("Controlled open state"),
|
||||
defaultOpen: z.boolean().optional()
|
||||
.describe("Initial open state (uncontrolled)"),
|
||||
onOpenChange: z.function().args(z.boolean()).returns(z.void()).optional()
|
||||
.describe("Called when open state changes"),
|
||||
modal: z.boolean().default(true)
|
||||
.describe("Whether to trap focus and add backdrop"),
|
||||
});
|
||||
|
||||
// TypeScript types DERIVED from schema
|
||||
export type DialogRootProps = z.infer<typeof DialogRootProps>;
|
||||
|
||||
// Metadata — just enough for MCP discovery
|
||||
export const DialogMeta = {
|
||||
name: "Dialog",
|
||||
description: "Modal overlay requiring user acknowledgment",
|
||||
parts: ["Root", "Trigger", "Portal", "Overlay", "Content", "Title", "Description", "Close"] as const,
|
||||
requiredParts: ["Root", "Content", "Title"] as const,
|
||||
} as const;
|
||||
```
|
||||
|
||||
### What This Enables
|
||||
|
||||
| AI asks... | Schema provides... |
|
||||
|---|---|
|
||||
| "What components can build a form?" | `.describe()` strings + MCP semantic search |
|
||||
| "What props does Select accept?" | Full typed contract with descriptions |
|
||||
| "Is this Dialog usage valid?" | Runtime validation against schema |
|
||||
| "Which parts are required for a11y?" | `requiredParts` in meta |
|
||||
|
||||
## MCP Server Architecture
|
||||
|
||||
The MCP server is the AI's interface to PettyUI. It reads Zod schemas and meta directly from `core/`.
|
||||
|
||||
### Tools
|
||||
|
||||
```
|
||||
pettyui.discover → "I need to collect user input" → returns matching components with schemas
|
||||
pettyui.inspect → "Tell me about Dialog" → returns full props schema + parts + required
|
||||
pettyui.validate → "Is this Dialog usage correct?" → validates JSX/props against schema
|
||||
pettyui.add → "Add Dialog to my project" → copies styled component from registry
|
||||
pettyui.compose → "Build a settings form" → returns composed multi-component JSX
|
||||
```
|
||||
|
||||
### pettyui.discover
|
||||
|
||||
```
|
||||
Input: { intent: "I need a searchable dropdown" }
|
||||
Output: [
|
||||
{ name: "Combobox", match: 0.95, description: "..." },
|
||||
{ name: "CommandPalette", match: 0.72, description: "..." },
|
||||
{ name: "Select", match: 0.6, description: "..." }
|
||||
]
|
||||
```
|
||||
|
||||
Uses `.describe()` strings for semantic matching. No rigid taxonomy — descriptions ARE the search index.
|
||||
|
||||
### pettyui.inspect
|
||||
|
||||
```
|
||||
Input: { component: "Dialog" }
|
||||
Output: {
|
||||
props: { /* Zod schema as JSON Schema */ },
|
||||
parts: ["Root", "Trigger", "Portal", "Overlay", "Content", "Title", "Description", "Close"],
|
||||
requiredParts: ["Root", "Content", "Title"],
|
||||
example: "/* minimal valid usage */",
|
||||
source: "/* full component source if requested */"
|
||||
}
|
||||
```
|
||||
|
||||
### pettyui.validate
|
||||
|
||||
```
|
||||
Input: { component: "Dialog", jsx: "<Dialog.Root><Dialog.Content>hello</Dialog.Content></Dialog.Root>" }
|
||||
Output: {
|
||||
valid: false,
|
||||
errors: ["Missing required part: Title. Dialog.Content must contain Dialog.Title for accessibility"],
|
||||
fix: "<Dialog.Root><Dialog.Content><Dialog.Title>...</Dialog.Title>hello</Dialog.Content></Dialog.Root>"
|
||||
}
|
||||
```
|
||||
|
||||
### pettyui.add
|
||||
|
||||
```
|
||||
Input: { component: "Dialog", style: "default" }
|
||||
Output: {
|
||||
files: [{ path: "src/components/ui/dialog.tsx", content: "/* styled component */" }],
|
||||
dependencies: ["pettyui/dialog"]
|
||||
}
|
||||
```
|
||||
|
||||
### pettyui.compose
|
||||
|
||||
```
|
||||
Input: { intent: "login form with email and password", components: ["Form", "TextField", "Button"] }
|
||||
Output: { jsx: "/* complete composed component */", imports: ["pettyui/form", "pettyui/text-field", "pettyui/button"] }
|
||||
```
|
||||
|
||||
### MCP Server Does NOT Do
|
||||
|
||||
- No styling opinions — registry's job
|
||||
- No state management — components handle their own
|
||||
- No routing — out of scope
|
||||
- No build tooling — tsdown/vite
|
||||
|
||||
## Registry Layer (shadcn Model for Solid)
|
||||
|
||||
### What Gets Copied
|
||||
|
||||
`pettyui add dialog` creates `src/components/ui/dialog.tsx`:
|
||||
|
||||
```typescript
|
||||
import { Dialog as DialogPrimitive } from "pettyui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DialogContent = (props) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay class={cn("fixed inset-0 bg-black/50 ...")} />
|
||||
<DialogPrimitive.Content
|
||||
class={cn("fixed left-1/2 top-1/2 ... bg-background rounded-lg shadow-lg", props.class)}
|
||||
>
|
||||
{props.children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
|
||||
export { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogClose };
|
||||
```
|
||||
|
||||
### Registry Principles
|
||||
|
||||
1. **Tailwind + CSS variables** — styles co-located, tokens via `--` variables
|
||||
2. **Imports headless from core** — registry depends on `pettyui/*`, not copy-pasted behavior
|
||||
3. **Pre-composed for 90% case** — DialogContent includes Portal + Overlay automatically
|
||||
4. **Fully editable** — your file after copy
|
||||
5. **One file per component** — AI reads one file, gets everything
|
||||
|
||||
### Theme Contract
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--border: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
pettyui init # Sets up tokens, utils, tsconfig paths
|
||||
pettyui add dialog # Copies styled dialog component
|
||||
pettyui add form # Copies styled form with Zod integration
|
||||
pettyui diff dialog # Shows what you've changed vs upstream
|
||||
```
|
||||
|
||||
## Build Order
|
||||
|
||||
```
|
||||
Phase 1: Foundation Phase 2: Advanced Phase 3: AI Layer
|
||||
───────────────── ────────────────── ─────────────────
|
||||
Zod-first props refactor DataTable MCP server
|
||||
(migrate 28 components) CommandPalette pettyui.discover
|
||||
Meta objects on all DatePicker + Calendar pettyui.inspect
|
||||
components Wizard/Stepper pettyui.validate
|
||||
Card + Avatar (simple) Form system pettyui.add
|
||||
NavigationMenu VirtualList + primitive pettyui.compose
|
||||
Cut ContextMenu/Image/
|
||||
Meter/Separator/Pagination
|
||||
|
||||
Registry scaffolding Registry: styled versions CLI wrapper
|
||||
(init, tokens, utils) of all components pettyui init/add/diff
|
||||
```
|
||||
|
||||
Phase 1 is mostly refactoring what exists. Phase 2 is the hard new work. Phase 3 is where PettyUI becomes unique.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **AI is the primary user** — every API decision optimizes for LLM code generation
|
||||
2. **Zod-first** — schemas ARE the type system, not a parallel description
|
||||
3. **Sub-path exports** — `pettyui/dialog` not `pettyui` barrel. Prevents hallucination
|
||||
4. **Compound components** — `Dialog.Root` + `Dialog.Content` over prop explosion
|
||||
5. **Sensible defaults** — components work with zero props, accept controlled props when needed
|
||||
6. **Union types over booleans** — `variant: 'primary' | 'secondary'` not `isPrimary?: boolean`
|
||||
7. **Consistent naming** — same patterns everywhere, reduces AI search space
|
||||
8. **CSS variables for theming** — AI handles CSS vars naturally vs JS theme providers
|
||||
9. **`.describe()` on every prop** — this IS the documentation
|
||||
10. **Runtime validation feedback** — AI generates, validates, fixes. Feedback loop
|
||||
22
eslint.config.mjs
Normal file
22
eslint.config.mjs
Normal file
@ -0,0 +1,22 @@
|
||||
import solid from "eslint-plugin-solid";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["packages/**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
plugins: { solid },
|
||||
rules: {
|
||||
"solid/reactivity": "error",
|
||||
"solid/no-destructure": "error",
|
||||
"solid/prefer-for": "warn",
|
||||
"solid/no-react-deps": "error",
|
||||
"solid/no-react-specific-props": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["node_modules/**", "dist/**", "coverage/**"],
|
||||
},
|
||||
];
|
||||
@ -4,9 +4,16 @@
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"test": "pnpm -r test",
|
||||
"lint": "biome check . && eslint packages --ext .ts,.tsx",
|
||||
"format": "biome format --write .",
|
||||
"typecheck": "pnpm -r typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^6.0.2"
|
||||
"@biomejs/biome": "^1.9.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-solid": "^0.14.0",
|
||||
"typescript": "^6.0.2",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user