diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 94da16b..e202bc0 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,7 +1,7 @@
import type { Preview } from "@storybook/react-vite";
import React from "react";
import { ToastProvider } from "../src/components/ui/ToastProvider";
-import "../src/styles/global.css";
+import "../src/styles.css";
const preview: Preview = {
parameters: {
diff --git a/AGENTS.md b/AGENTS.md
index 0cefddc..ca16dd1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,7 +36,7 @@ chore: update panda css to v1.9
- [ ] Uses `cva()` for variants, `css()` for static styles
- [ ] Wraps BaseUI primitive if interactive (Dialog, Select, etc.)
- [ ] Accepts `className` prop for composition
-- [ ] Uses `forwardRef` for DOM elements
+- [ ] Accepts `ref` as a regular prop (React 19 — no `forwardRef`)
- [ ] Exported from `src/components/ui/index.ts`
- [ ] Types exported (`ComponentNameProps`)
- [ ] No `styled()` — only `css()`/`cva()`
diff --git a/AUDIT.md b/AUDIT.md
deleted file mode 100644
index f2a1c38..0000000
--- a/AUDIT.md
+++ /dev/null
@@ -1,174 +0,0 @@
-# 🔍 Technical Audit Report — React Starter Kit
-
-**Auditor:** Skeptical Technical Lead (subagent)
-**Date:** 2026-03-06
-**Scope:** Full codebase read-through + dependency verification + claim cross-referencing
-
----
-
-## 🔴 Verified Issues
-
-### 1. `@storybook/test` version mismatch with Storybook 10
-**File:** `package.json`
-**Details:** All Storybook packages are `^10.1.11` except `@storybook/test` which is `^8.6.15`. As of this audit, `@storybook/test` on npm has a **latest version of 8.6.15** — there is no v10 release of this package. This means either:
-- Storybook 10 folded `@storybook/test` into another package and this dep is dead weight
-- Or it's genuinely incompatible and will cause version resolution warnings or runtime issues
-
-**Impact:** Tests in stories may break or silently use the wrong testing utilities. The `@storybook/addon-vitest` (v10) likely supersedes `@storybook/test` (v8) anyway.
-
-**Recommendation:** Remove `@storybook/test` or verify Storybook 10's migration guide for the replacement.
-
-### 2. Both `bun.lock` and `package-lock.json` exist
-**File:** Project root
-**Details:** Two lockfiles coexist. The project claims to use Bun exclusively, but having both lockfiles is a recipe for install confusion. CI uses `bun install --frozen-lockfile` which reads `bun.lock`, so `package-lock.json` is dead weight at best and misleading at worst.
-
-**Recommendation:** Delete `package-lock.json` and add it to `.gitignore`.
-
-### 3. `@slideIn` animation used but not declared in Panda CSS config
-**File:** `src/components/ui/ToastProvider.tsx` line: `animation: "slideIn 200ms ease-out"`
-**Details:** The `slideIn` keyframes are defined in `src/styles/global.css`, not in `panda.config.ts`. This works because the CSS is loaded globally, but it bypasses Panda's animation system. If someone removes or refactors `global.css`, toast animations silently break with no build error. Panda CSS supports `keyframes` in config — this should live there for consistency.
-
-**Impact:** Low — it works. But it's inconsistent with the "everything through Panda" philosophy.
-
----
-
-## 🟡 Dubious Claims
-
-### 1. "13 Core Components" (README)
-**Claim:** README says "13 Core Components" but never enumerates all 13. The barrel export would need to be checked to verify the count. The README shows examples of: Button, Input, TextArea, Select, Checkbox, Modal, ConfirmDialog, LoadingSpinner, Skeleton, Badge, EmptyState, ToastProvider/useToast. That's 12 distinct components (counting Toast as one). Could be 13 if you count the hook separately, but that's a stretch.
-
-**Verdict:** Minor marketing claim, not a real problem. But "13" is likely wrong.
-
-### 2. "Zero-runtime" claim for Panda CSS
-**Claim:** CLAUDE.md and README both claim Panda CSS is "zero-runtime — CSS generated at build time, no JS shipped for styles."
-**Reality:** This is *mostly* true but nuanced. The `css()` and `cva()` calls still ship JS — they're function calls that return class name strings at runtime. The *styles* are pre-generated, but the class name resolution logic is in the bundle. This is different from truly zero-runtime solutions like vanilla-extract where even the class names are statically resolved. Panda CSS's own docs call it "zero-runtime" too, so this is industry-standard marketing, but it's technically misleading.
-
-### 3. varlock integration is placeholder-level
-**Claim:** README and CLAUDE.md present varlock as a key part of the stack with `.env.schema` validation.
-**Reality:** The `.env.schema` has almost nothing in it — just `APP_ENV`, `VITE_APP_NAME`, and `VITE_APP_URL` with everything else commented out as examples. The `serverEnv.ts` just re-exports `ENV` from `varlock/env`. There's no evidence this has been tested with actual Cloudflare Worker bindings. For a starter kit, this is fine, but the docs oversell it as if it's a battle-tested setup.
-
-### 4. "forwardRef for broad compatibility" (CLAUDE.md)
-**Claim:** "The existing components use `forwardRef` for broad compatibility."
-**Reality:** React 19 (which this project uses — `^19.1.0`) supports ref as a regular prop. `forwardRef` still works but is officially legacy. The "broad compatibility" claim implies supporting React 18 consumers, but the `package.json` pins `react: ^19.1.0`, so there's no backward compatibility to preserve. This is cargo cult from the React 18 era.
-
----
-
-## 🟢 Verified Good
-
-### 1. All dependency versions are real and resolve correctly
-Every pinned version in `package.json` resolves to real, published packages:
-- `zod@^4.3.5` → installed `4.3.6` ✅ (Zod 4 is real, released 2025)
-- `vite@^7.2.6` → installed `7.3.1` ✅
-- `vitest@^4.0.16` → installed `4.0.18` ✅
-- `typescript@^5.9.3` → installed `5.9.3` ✅
-- `storybook@^10.1.11` → installed `10.2.15` ✅
-- `@base-ui-components/react@^1.0.0-rc.0` → installed `1.0.0-rc.0` ✅
-- `@tanstack/react-start@^1.145.5` → installed `1.166.2` ✅
-
-### 2. BaseUI sub-path imports are correct
-The imports like `@base-ui-components/react/dialog`, `@base-ui-components/react/button`, `@base-ui-components/react/input` all exist and export the expected components. Verified in node_modules.
-
-### 3. `typeof className === "string"` guard in Button.tsx is correct
-I was initially suspicious of this, but BaseUI's `BaseUIComponentProps` type allows `className` to be `string | ((state: State) => string | undefined)`. The guard correctly prevents passing a function to `cx()`, which expects strings.
-
-### 4. z-index strategy is consistently applied
-Checked `dialogStyles.ts` — both backdrop and popup use `zIndex: 1`, relying on DOM order for stacking. The comment explaining this is accurate. ToastProvider also uses `zIndex: 1`. No arbitrary z-index values found anywhere.
-
-### 5. `server.handlers` pattern for API routes is real
-Verified via Elysia integration docs and the actual TanStack Start codebase — `createFileRoute` with `server: { handlers: { GET, POST } }` is a legitimate pattern for API routes in TanStack Start.
-
-### 6. `createAPIFileRoute` correctly noted as not-yet-available
-The TODO in `health.ts` says to migrate to `createAPIFileRoute` "when available." Grep of entire `node_modules/@tanstack/` confirms this export doesn't exist yet. The TODO is accurate.
-
-### 7. Theme system dark-mode-flash prevention
-The inline script in `__root.tsx` that reads `localStorage` and sets `data-theme` before first paint is a well-known pattern to prevent FOUC (Flash of Unstyled Content) for dark mode. Correctly implemented.
-
-### 8. Router creates per-request instances for SSR safety
-`router.ts` creates a new `QueryClient` and router per call to `createAppRouter()`. This is correct for SSR on Cloudflare Workers where you must not share state across requests.
-
-### 9. `use-sync-external-store` shim aliases
-The vite `resolve.alias` entries redirect the shim packages to the real `use-sync-external-store`. This is a legitimate workaround for libraries that still import the React 18 shim when running on React 19.
-
----
-
-## 📋 Cargo Cult Watch
-
-### 1. `forwardRef` everywhere
-As noted above, every component uses `forwardRef` despite React 19 supporting ref as a prop. The CLAUDE.md even acknowledges this ("forwardRef is technically legacy in React 19") but keeps it anyway for "broad compatibility" that doesn't exist given the `react: ^19.1.0` pin. New components should just accept `ref` as a prop.
-
-### 2. `@types/bun` pinned to exact version
-**File:** `package.json` — `"@types/bun": "1.3.5"` (no caret)
-Every other dependency uses `^` ranges. This one is pinned exactly. There's no comment explaining why. This looks like someone copied a lockfile version into package.json.
-
-### 3. Both `jsdom` and `happy-dom` as dev dependencies
-The project has both `jsdom@^28.1.0` and `happy-dom@^20.0.11`. The CLAUDE.md recommends `happy-dom` for tests. Having both is confusing — pick one.
-
-### 4. `optimizeDeps.exclude` for TanStack packages
-**File:** `vite.config.ts`
-```ts
-optimizeDeps: {
- exclude: ["@tanstack/react-start", "@tanstack/start-server-core"],
-}
-```
-This is likely copied from a GitHub issue or Discord recommendation. It may have been needed for an earlier version of TanStack Start but could be unnecessary now. No comment explains why it's there or when it can be removed.
-
-### 5. `exclude` in tsconfig.json excludes test files
-**File:** `tsconfig.json` — test files (`**/*.test.ts`, `**/*.test.tsx`) are excluded from type checking.
-This means `bun run typecheck` will NOT catch type errors in tests. This is a common pattern to "speed up" type checking, but it means your tests could have type errors that nobody notices until they fail at runtime. Tests should be type-checked.
-
----
-
-## 💀 Silent Bugs
-
-### 1. Toast auto-dismiss uses `setTimeout` — not cleared on unmount
-**File:** `src/components/ui/ToastProvider.tsx`
-```tsx
-setTimeout(() => {
- setToasts((prev) => prev.filter((t) => t.id !== id));
-}, durationRef.current);
-```
-If the `ToastProvider` unmounts before the timeout fires (e.g., route change that unmounts the root), this will call `setToasts` on an unmounted component. In React 18 this would warn; in React 19 it's silently ignored. But more importantly, the timeout is never cleaned up — if you show 100 toasts rapidly, you have 100 pending timeouts. No `clearTimeout` on dismiss either, so dismissing a toast manually still leaves the timeout running (it will try to remove an already-removed toast, which is harmless but wasteful).
-
-**Fix:** Store timeout IDs and clear them on dismiss and unmount.
-
-### 2. `env.ts` falls back to empty string in production
-**File:** `src/lib/env.ts`
-```tsx
-VITE_APP_URL: import.meta.env.VITE_APP_URL || (import.meta.env.DEV ? "http://localhost:3000" : ""),
-```
-In production, if `VITE_APP_URL` isn't set, this silently falls back to `""`. The `head.ts` helper uses this to build OG image URLs: `${appUrl}${options.image}`. So your OG images would get paths like `/og-image.png` instead of `https://mysite.com/og-image.png`. No error, just broken social previews.
-
-### 3. `panda.config.ts` excludes `src/lib/**` from style scanning
-**File:** `panda.config.ts` — `exclude: ["./src/lib/**"]`
-If anyone adds Panda CSS utilities (`css()`, `cva()`) in a file under `src/lib/`, the styles will silently not be generated. There's no warning. This is probably intentional (lib files shouldn't have styles), but it's undocumented and will bite someone who creates a `src/lib/someHelper.ts` that returns class names.
-
-### 4. Head metadata `{ title }` is not a valid meta tag
-**File:** `src/lib/head.ts`
-```tsx
-const meta = [
- { title }, // ← This is { title: "My Title" }
- ...
-]
-```
-The `meta` array includes `{ title }` as the first entry. This is a TanStack-specific convention where `{ title }` is a special meta entry that sets the `
` tag. This works with TanStack's `HeadContent` component, so it's not actually a bug — but it looks wrong if you're used to standard HTML meta tags. Verified it's intentional TanStack Start behavior.
-
-### 5. CI caches Playwright by `bun.lock` hash, not Playwright version
-**File:** `.github/workflows/ci.yml`
-```yaml
-key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}
-```
-If you update Playwright's version in `package.json` but `bun.lock` doesn't change (unlikely but possible with range deps), you'd get stale browser binaries. More practically: if ANY dependency changes in `bun.lock`, the entire Playwright browser cache is invalidated and re-downloaded, even if Playwright itself didn't change. The key should include the Playwright version specifically, e.g., from `node_modules/@playwright/test/package.json`.
-
----
-
-## Summary
-
-The starter kit is **well-structured and mostly correct**. The dependency versions are real (not hallucinated), the architecture claims are largely accurate, and the patterns are sound. The main concerns are:
-
-1. **`@storybook/test` version mismatch** — needs attention
-2. **`forwardRef` cargo cult** — unnecessary with React 19
-3. **Toast timer leak** — minor but real
-4. **Empty `VITE_APP_URL` in production** — will cause silent OG metadata issues
-5. **Test files excluded from type checking** — type errors in tests go unnoticed
-
-Nothing is catastrophically broken, but items #3, #4, and #5 are the kind of silent bugs that surface months later in production.
diff --git a/CLAUDE.md b/CLAUDE.md
index 8b2ed41..c306bb2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -313,7 +313,8 @@ The `recipes/` directory contains drop-in patterns:
- **authoring/** — Markdown rendering + TipTap rich text editor
- **convex/** — Convex real-time database integration
- **analytics/** — PostHog scaffolding
-- **storybook/** — Storybook configuration
+- **pickers/** — Color + Icon pickers (react-colorful, lucide-react)
+- **storybook-deploy/** — Storybook deployment to CF Pages
Each recipe has its own README with setup instructions and required dependencies.
diff --git a/README.md b/README.md
index d102b2d..228955a 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,24 @@ bun install
bun run dev
```
+### Post-Clone Checklist
+
+After cloning, replace these placeholders with your project's details:
+
+| File | What to change |
+|------|---------------|
+| `package.json` | `"name"` — your package name |
+| `wrangler.jsonc` | `"name"` — your Cloudflare Worker name |
+| `.env.schema` | Add `VITE_APP_NAME` with your app name (used in page titles via `makeHead()`) |
+| `src/routes/_app/index.tsx` | Replace landing page heading and description |
+| `README.md` | Replace this README with your own |
+| `CLAUDE.md` | Update project-specific conventions as you go |
+
+For local dev, create a `.dev.vars` file:
+```
+VITE_APP_NAME=My App
+```
+
## Stack
| Layer | Choice | Why |
@@ -33,10 +51,11 @@ bun run dev
| **Components** | [BaseUI](https://base-ui.com) | Headless accessible primitives |
| **Deployment** | [Cloudflare Workers](https://workers.cloudflare.com) | Edge SSR via `@cloudflare/vite-plugin` |
| **Testing** | [Vitest](https://vitest.dev) | Vite-native, fast |
+| **Storybook** | [Storybook 10](https://storybook.js.org) | Component explorer with a11y addon |
## What's Included
-### 14 Core Components
+### 24 Core Components
All follow the same pattern: BaseUI primitive → Panda CSS `cva()` recipe → typed props → `ref` as a regular prop (React 19).
@@ -175,6 +194,36 @@ export const getItems = createServerFn({ method: "GET" })
});
```
+### Storybook
+
+Storybook 10 is pre-configured with Panda CSS support and the a11y addon. The config filters out TanStack Start and Cloudflare plugins that don't apply in the Storybook context.
+
+```bash
+bun run storybook # Opens on http://localhost:6006
+```
+
+Add stories next to your components:
+
+```tsx
+// src/components/ui/Button.stories.tsx
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Button } from "./Button";
+
+const meta = {
+ component: Button,
+ args: { children: "Click me" },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = { args: { variant: "primary" } };
+export const Ghost: Story = { args: { variant: "ghost" } };
+export const Loading: Story = { args: { loading: true } };
+```
+
+For deployment options (CF Pages, subdirectory), see `recipes/storybook-deploy/`.
+
### Environment Variables
Environment validation is handled by [varlock](https://varlock.dev) via `.env.schema`. The included schema is scaffolded — expand for your project. Add `@required`, `@type`, `@sensitive` decorators to define your schema. Varlock validates on load and fails fast with clear errors.
@@ -291,27 +340,17 @@ function CheckoutButton() {
**Extra deps:** `posthog-js`, `posthog-node`
-### `recipes/storybook/` -- Component Stories
-
-Storybook config pre-wired for Panda CSS + BaseUI components. Includes a11y addon.
-
-```bash
-# After copying .storybook/ config:
-bun run storybook
-# Opens on http://localhost:6006
-```
-
-**Extra deps:** `storybook`, `@storybook/react-vite`, `@storybook/addon-a11y`
-
## Commands
```bash
-bun run dev # Start dev server (port 3000)
-bun run build # Production build
-bun run preview # Preview production build locally
-bun run test # Run tests (Vitest)
-bun run typecheck # TypeScript check
-bun run deploy # Deploy to Cloudflare Workers
+bun run dev # Start dev server (port 3000)
+bun run build # Production build
+bun run preview # Preview production build locally
+bun run test # Run tests (Vitest)
+bun run typecheck # TypeScript check
+bun run storybook # Storybook dev server (port 6006)
+bun run build:storybook # Build static Storybook
+bun run deploy # Deploy to Cloudflare Workers
```
## Project Structure
@@ -321,17 +360,17 @@ bun run deploy # Deploy to Cloudflare Workers
│ ├── components/
│ │ ├── ui/ # Design system (Button, Input, Modal, etc.)
│ │ ├── layout/ # Flex, Grid, HStack, VStack, Box, Center
-│ │ └── icons/ # Minimal icon set (7 icons)
+│ │ └── icons/ # 7 icons from Untitled UI (thin React wrappers)
│ ├── lib/
│ │ ├── env.ts # Client env flags (isProduction, isDevelopment)
│ │ └── serverEnv.ts # Server env (re-exports varlock ENV)
│ ├── routes/ # File-based routing (TanStack Start)
-│ ├── styles/
-│ │ └── global.css # Global styles + Panda CSS layers
+│ ├── styles.css # Global styles + Panda CSS layers
│ ├── router.ts # Router config + context type
│ ├── start.ts # SSR entry
│ └── client.tsx # Client entry
-├── recipes/ # Opt-in patterns (auth, markdown, convex, etc.)
+├── .storybook/ # Storybook config (pre-wired for Panda CSS + BaseUI)
+├── recipes/ # Opt-in patterns (auth, authoring, convex, analytics, pickers, storybook-deploy)
├── docs/ # Architecture decisions, component API, deployment
├── styled-system/ # Generated by Panda CSS (gitignored)
├── panda.config.ts # Design tokens + theme
diff --git a/recipes/forms/README.md b/docs/forms.md
similarity index 100%
rename from recipes/forms/README.md
rename to docs/forms.md
diff --git a/recipes/README.md b/recipes/README.md
index faa6265..3103f6f 100644
--- a/recipes/README.md
+++ b/recipes/README.md
@@ -10,7 +10,8 @@ Drop-in patterns you can copy into your project. Each recipe is self-contained w
| **authoring/** | Markdown rendering + rich text editing | `react-markdown`, `remark-gfm`, TipTap |
| **convex/** | Convex real-time database integration | `convex`, `@convex-dev/react-query` |
| **analytics/** | PostHog analytics scaffolding | `posthog-js`, `posthog-node` |
-| **storybook/** | Storybook configuration for components | `storybook`, `@storybook/react-vite` |
+| **pickers/** | Color + Icon pickers | `react-colorful`, `lucide-react` |
+| **storybook-deploy/** | Storybook deployment to CF Pages | None (Storybook is a core dependency) |
## How to Use
diff --git a/recipes/markdown/README.md b/recipes/markdown/README.md
deleted file mode 100644
index eac2d4a..0000000
--- a/recipes/markdown/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Markdown Recipe
-
-Rich text editing with TipTap + collaborative sync + Markdown rendering.
-
-## What's Included
-
-- `components/MarkdownEditor.tsx` — TipTap-based rich text editor
-- `components/Markdown.tsx` — Markdown renderer (react-markdown + remark-gfm)
-- Collaborative sync pattern via Convex ProseMirror (optional)
-
-## Additional Dependencies
-
-```bash
-bun add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder react-markdown remark-gfm
-# For collaborative sync:
-bun add @convex-dev/prosemirror-sync
-```
-
-## Key Patterns
-
-- **Client-only rendering**: MarkdownEditor skips SSR (avoids sessionStorage issues)
-- **Document ID convention**: `{entity}:{id}:{field}` (e.g., `zone:abc123:description`)
-- **Auto-create on first edit**: Documents are lazily created
-- **Markdown subset**: Headings, lists, code, links, emphasis
-
-## Integration
-
-1. Copy components into your preferred location (e.g., `src/components/markdown/`)
-2. Install dependencies
-3. Import and use — no provider setup needed for standalone mode
-4. For collaborative sync, set up Convex ProseMirror (see your project's Convex sync setup)
diff --git a/recipes/posthog/README.md b/recipes/posthog/README.md
deleted file mode 100644
index 94e1bcf..0000000
--- a/recipes/posthog/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# PostHog Recipe
-
-Product analytics with PostHog (client + server).
-
-## Additional Dependencies
-
-```bash
-bun add posthog-js posthog-node
-```
-
-## Setup
-
-1. Add `VITE_POSTHOG_KEY` to `src/lib/env.ts`
-2. Add `POSTHOG_API_KEY` to `src/lib/serverEnv.ts`
-3. Initialize client-side in `__root.tsx`:
-
-```typescript
-import posthog from "posthog-js";
-
-if (typeof window !== "undefined") {
- posthog.init(env.VITE_POSTHOG_KEY, {
- api_host: "https://us.i.posthog.com",
- });
-}
-```
-
-4. Server-side (in server functions):
-
-```typescript
-import { PostHog } from "posthog-node";
-
-const posthog = new PostHog(serverEnv.POSTHOG_API_KEY);
-posthog.capture({ distinctId: userId, event: "action_name" });
-await posthog.shutdown();
-```
-
-## Key Patterns
-
-- Client: auto-capture enabled by default, feature flags via `posthog.isFeatureEnabled()`
-- Server: explicit capture only, always `shutdown()` in Workers (flush before response ends)
-- Use feature flags for gradual rollouts
diff --git a/skills/component-scaffold/SKILL.md b/skills/component-scaffold/SKILL.md
index 6ad9527..e4e40ec 100644
--- a/skills/component-scaffold/SKILL.md
+++ b/skills/component-scaffold/SKILL.md
@@ -21,7 +21,6 @@ Ask for (if not provided):
Create `src/components/ui/{Name}.tsx` following this exact pattern:
```tsx
-import { forwardRef, type ComponentPropsWithoutRef } from "react";
// Import BaseUI primitive if wrapping one:
// import { SomePrimitive } from "@base-ui-components/react/some-primitive";
import { cva, type RecipeVariantProps } from "styled-system/css";
@@ -47,22 +46,21 @@ const {name}Recipe = cva({
type {Name}Variants = RecipeVariantProps;
-export type {Name}Props = ComponentPropsWithoutRef<"div"> & // or BaseUI type
+export type {Name}Props = React.ComponentPropsWithoutRef<"div"> & // or BaseUI type
{Name}Variants & {
+ ref?: React.Ref;
// Custom props here
};
-export const {Name} = forwardRef(
- function {Name}({ variant, size, className, ...props }, ref) {
- return (
-
- );
- }
-);
+export function {Name}({ variant, size, className, ref, ...props }: {Name}Props) {
+ return (
+
+ );
+}
```
### 3. Update the Barrel Export
@@ -79,7 +77,7 @@ Create `src/components/ui/{Name}.stories.tsx` (see storybook-gen skill).
## Rules
- **`cva()` only** -- never use `styled()`
-- **`forwardRef` with named function** -- `forwardRef(function MyComponent(...))`
+- **No `forwardRef`** -- React 19 passes `ref` as a regular prop. Accept `ref?: React.Ref` in your props type and pass it through directly. `forwardRef` is legacy and will be removed in a future React version.
- **Export both component and Props type** from the barrel
- **No `any` types** -- use proper generics or `unknown`
- **z-index: only -1, 0, 1** -- if the component needs stacking, justify it
diff --git a/skills/recipe-install/SKILL.md b/skills/recipe-install/SKILL.md
index a6e29c0..dd4504a 100644
--- a/skills/recipe-install/SKILL.md
+++ b/skills/recipe-install/SKILL.md
@@ -4,18 +4,18 @@ Installs a recipe from the `recipes/` directory into the active project.
## When to Use
-When asked to add auth, forms, authoring/markdown, Convex, PostHog analytics, or Storybook to the project.
+When asked to add auth, authoring/markdown, Convex, PostHog analytics, or pickers to the project.
## Available Recipes
| Recipe | Directory | Extra Dependencies |
|--------|-----------|-------------------|
| Auth (OTP + sessions) | `recipes/auth/` | `twilio` (or your SMS/email provider) |
-| Forms | `recipes/forms/` | None (zod + @tanstack/react-form already included) |
| Authoring (Markdown + rich text) | `recipes/authoring/` | `react-markdown`, `remark-gfm`, `@tiptap/react`, `@tiptap/starter-kit`, `@tiptap/extension-placeholder` |
+| Pickers (Color + Icon) | `recipes/pickers/` | `react-colorful`, `lucide-react` |
| Convex | `recipes/convex/` | `convex`, `@convex-dev/react-query` |
| Analytics (PostHog) | `recipes/analytics/` | `posthog-js`, `posthog-node` |
-| Storybook | `recipes/storybook/` | `storybook`, `@storybook/react-vite`, `@storybook/addon-a11y`, `@storybook/test` |
+| Storybook Deploy | `recipes/storybook-deploy/` | None (Storybook is already a core dependency) |
## Process
@@ -41,11 +41,11 @@ Copy recipe files into the appropriate location in `src/`. Recommended structure
| Recipe | Target Location |
|--------|----------------|
| Auth | `src/features/auth/` |
-| Forms | Follow pattern in README (no files to copy, just a pattern) |
| Authoring | `src/features/authoring/` |
| Convex | `convex/` (project root) + `src/lib/convex.ts` |
| Analytics | `src/lib/analytics.ts` + provider in `__root.tsx` |
-| Storybook | `.storybook/` (project root) |
+| Pickers | `src/components/ui/` or `src/features/pickers/` |
+| Storybook Deploy | CI config (`.github/workflows/`) |
### 4. Wire Up
diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts
index e8a4d00..7e06d49 100644
--- a/src/components/icons/index.ts
+++ b/src/components/icons/index.ts
@@ -1,3 +1,7 @@
+/**
+ * Icons from Untitled UI (https://untitledui.com/icons).
+ * Thin React components wrapping their SVG paths.
+ */
export { CheckIcon } from "./CheckIcon";
export { CloseIcon } from "./CloseIcon";
export { ChevronDownIcon } from "./ChevronDownIcon";
diff --git a/recipes/data-display/components/Avatar.stories.tsx b/src/components/ui/Avatar.stories.tsx
similarity index 100%
rename from recipes/data-display/components/Avatar.stories.tsx
rename to src/components/ui/Avatar.stories.tsx
diff --git a/recipes/data-display/components/Avatar.tsx b/src/components/ui/Avatar.tsx
similarity index 100%
rename from recipes/data-display/components/Avatar.tsx
rename to src/components/ui/Avatar.tsx
diff --git a/recipes/layout/components/Card.stories.tsx b/src/components/ui/Card.stories.tsx
similarity index 100%
rename from recipes/layout/components/Card.stories.tsx
rename to src/components/ui/Card.stories.tsx
diff --git a/recipes/layout/components/Card.tsx b/src/components/ui/Card.tsx
similarity index 100%
rename from recipes/layout/components/Card.tsx
rename to src/components/ui/Card.tsx
diff --git a/recipes/pickers/components/DangerZone.stories.tsx b/src/components/ui/DangerZone.stories.tsx
similarity index 98%
rename from recipes/pickers/components/DangerZone.stories.tsx
rename to src/components/ui/DangerZone.stories.tsx
index 69c588b..814de1e 100644
--- a/recipes/pickers/components/DangerZone.stories.tsx
+++ b/src/components/ui/DangerZone.stories.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { userEvent, within, expect, waitFor } from "@storybook/test";
-import { fn } from "@storybook/test";
+import { userEvent, within, expect, waitFor } from "storybook/test";
+import { fn } from "storybook/test";
import { DangerZone } from "./DangerZone";
const meta = {
diff --git a/recipes/pickers/components/DangerZone.tsx b/src/components/ui/DangerZone.tsx
similarity index 100%
rename from recipes/pickers/components/DangerZone.tsx
rename to src/components/ui/DangerZone.tsx
diff --git a/recipes/layout/components/Header.stories.tsx b/src/components/ui/Header.stories.tsx
similarity index 100%
rename from recipes/layout/components/Header.stories.tsx
rename to src/components/ui/Header.stories.tsx
diff --git a/recipes/layout/components/Header.tsx b/src/components/ui/Header.tsx
similarity index 100%
rename from recipes/layout/components/Header.tsx
rename to src/components/ui/Header.tsx
diff --git a/recipes/data-display/components/List.stories.tsx b/src/components/ui/List.stories.tsx
similarity index 100%
rename from recipes/data-display/components/List.stories.tsx
rename to src/components/ui/List.stories.tsx
diff --git a/recipes/data-display/components/List.tsx b/src/components/ui/List.tsx
similarity index 100%
rename from recipes/data-display/components/List.tsx
rename to src/components/ui/List.tsx
diff --git a/recipes/layout/components/Main.stories.tsx b/src/components/ui/Main.stories.tsx
similarity index 100%
rename from recipes/layout/components/Main.stories.tsx
rename to src/components/ui/Main.stories.tsx
diff --git a/recipes/layout/components/Main.tsx b/src/components/ui/Main.tsx
similarity index 100%
rename from recipes/layout/components/Main.tsx
rename to src/components/ui/Main.tsx
diff --git a/recipes/layout/components/Section.stories.tsx b/src/components/ui/Section.stories.tsx
similarity index 100%
rename from recipes/layout/components/Section.stories.tsx
rename to src/components/ui/Section.stories.tsx
diff --git a/recipes/layout/components/Section.tsx b/src/components/ui/Section.tsx
similarity index 100%
rename from recipes/layout/components/Section.tsx
rename to src/components/ui/Section.tsx
diff --git a/recipes/layout/components/Sidebar.stories.tsx b/src/components/ui/Sidebar.stories.tsx
similarity index 100%
rename from recipes/layout/components/Sidebar.stories.tsx
rename to src/components/ui/Sidebar.stories.tsx
diff --git a/recipes/layout/components/Sidebar.tsx b/src/components/ui/Sidebar.tsx
similarity index 100%
rename from recipes/layout/components/Sidebar.tsx
rename to src/components/ui/Sidebar.tsx
diff --git a/recipes/data-display/components/Table.stories.tsx b/src/components/ui/Table.stories.tsx
similarity index 100%
rename from recipes/data-display/components/Table.stories.tsx
rename to src/components/ui/Table.stories.tsx
diff --git a/recipes/data-display/components/Table.tsx b/src/components/ui/Table.tsx
similarity index 100%
rename from recipes/data-display/components/Table.tsx
rename to src/components/ui/Table.tsx
diff --git a/recipes/data-display/components/Timestamp.stories.tsx b/src/components/ui/Timestamp.stories.tsx
similarity index 100%
rename from recipes/data-display/components/Timestamp.stories.tsx
rename to src/components/ui/Timestamp.stories.tsx
diff --git a/recipes/data-display/components/Timestamp.tsx b/src/components/ui/Timestamp.tsx
similarity index 100%
rename from recipes/data-display/components/Timestamp.tsx
rename to src/components/ui/Timestamp.tsx
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index 360172a..2993a02 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -12,3 +12,13 @@ export { LoadingSpinner, type LoadingSpinnerProps } from "./LoadingSpinner";
export { Skeleton, type SkeletonProps } from "./Skeleton";
export { Badge, type BadgeProps } from "./Badge";
export { EmptyState, type EmptyStateProps } from "./EmptyState";
+export { Avatar, type AvatarProps } from "./Avatar";
+export { Card, type CardProps } from "./Card";
+export { Header, type HeaderProps } from "./Header";
+export { List, type ListProps } from "./List";
+export { Main, type MainProps } from "./Main";
+export { Section, type SectionProps } from "./Section";
+export { Sidebar, type SidebarProps } from "./Sidebar";
+export { Table, type TableProps, type Column } from "./Table";
+export { Timestamp, type TimestampProps } from "./Timestamp";
+export { DangerZone, type DangerZoneProps } from "./DangerZone";
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index e502bc8..44b6193 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -3,7 +3,7 @@ import { type ReactNode, useEffect } from "react";
import * as tsr from "@tanstack/react-router";
import { QueryClientProvider } from "@tanstack/react-query";
import type { RouterContext } from "@/router";
-import globalStyles from "@/styles/global.css?url";
+import globalStyles from "@/styles.css?url";
import { ToastProvider } from "@/components/ui";
import {
DefaultErrorComponent,
diff --git a/src/routes/api/health.ts b/src/routes/api/health.ts
index b8d8fcc..f4bdf24 100644
--- a/src/routes/api/health.ts
+++ b/src/routes/api/health.ts
@@ -3,8 +3,6 @@
*
* Returns 200 with status. Add your own dependency checks here.
*/
-// TODO: Migrate to createAPIFileRoute from "@tanstack/react-start/api" when available.
-// See: https://tanstack.com/router/latest/docs/framework/react/start/api-routes
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/health")({
diff --git a/src/routes/api/liveness.ts b/src/routes/api/liveness.ts
index f31fcf5..d94dd06 100644
--- a/src/routes/api/liveness.ts
+++ b/src/routes/api/liveness.ts
@@ -1,7 +1,6 @@
/**
* Liveness Probe — returns 204 No Content for uptime monitoring.
*/
-// TODO: Migrate to createAPIFileRoute from "@tanstack/react-start/api" when available.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/liveness")({
diff --git a/src/styles/global.css b/src/styles.css
similarity index 100%
rename from src/styles/global.css
rename to src/styles.css
diff --git a/src/test/setup.ts b/test/setup.ts
similarity index 100%
rename from src/test/setup.ts
rename to test/setup.ts
diff --git a/tsconfig.test.json b/tsconfig.test.json
index 1d9adac..a205d41 100644
--- a/tsconfig.test.json
+++ b/tsconfig.test.json
@@ -1,6 +1,6 @@
{
"extends": "./tsconfig.json",
- "include": ["src/**/*.ts", "src/**/*.tsx", "e2e/**/*.ts"],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "e2e/**/*.ts", "test/**/*.ts"],
"exclude": [
"node_modules",
"dist",
diff --git a/vitest.config.ts b/vitest.config.ts
index 94907cb..4e84a20 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -8,7 +8,7 @@ export default defineConfig({
test: {
globals: true,
environment: "happy-dom",
- setupFiles: ["./src/test/setup.ts"],
+ setupFiles: ["./test/setup.ts"],
include: ["./src/**/*.test.ts", "./src/**/*.test.tsx"],
exclude: ["node_modules", "dist", "**/*.stories.*"],
alias: {