Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/main/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
- [User profile](user_profile.md) — French-speaking, shadcn/Tailwind v4 practitioner fighting agent-generated "slop UI"
- [Concrete over theory](feedback_concrete_over_theory.md) — anchor design talk in current, verifiable tooling; label opinion as opinion
- [Design learnings repo](project_design_learnings.md) — knowledge base + working monorepo for deessejs/ui registry at ui.deessejs.com (hosted on Vercel)
- [Phase 4 validated](project_phase4_validated.md) — external install end-to-end confirmed 2026-07-29; Phase 6 (official shadcn index submission) gate is now lifted
82 changes: 71 additions & 11 deletions .claude/agent-memory/main/project_design_learnings.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,88 @@
---
name: project-design-learnings
description: Purpose of this repo — a knowledge base + working monorepo for the deessejs UI registry at ui.deessejs.com
description: Purpose of this repo — knowledge base + working monorepo for the deessejs UI registry at ui.deessejs.com
metadata:
type: project
---

This repo (`design/`) is both a **knowledge base** and the **working monorepo** for the deessejs UI registry. Started as research notes under `learnings/<topic>/` (anti-slop design system thesis), now also contains the actual implementation: registry components, the showcase site, and shared config packages.

**Public destination:** the monorepo will be migrated to **https://github.com/deessejs/ui** and deployed via **Vercel** at **ui.deessejs.com**. The repo will be self-contained: `packages/registry/` (the component library) + `apps/web/` (the showcase site) + `packages/ui/` (shadcn primitives).
**Public destination:** deployed via **Vercel** at **https://ui.deessejs.com**. Repo: **https://github.com/deessejs/ui**. Self-contained: `packages/registry/` (component library) + `apps/web/` (showcase site) + `packages/ui/` (shadcn primitives).

**Why:** the user builds UI with agents and the output was coherent per-screen but drifted across screens. The working thesis: prompting cannot fix this because it decays across context, so the fix must be *enforced* (theme namespaces deleted at the compiler level) and *fetchable* (system distributed as a registry, not re-explained).
**Stack (locked in):**
- npm 10 workspaces (not pnpm)
- Next.js 16 App Router + React 19 + Turbopack
- Tailwind v4, semantic tokens only (no raw palette)
- shadcn/ui on **Base UI** (NOT Radix) — `packages/ui/` only
- TypeScript strict, Node 22+

**Why:** the user builds UI with agents and the output was coherent per-screen but drifted across screens. The thesis: prompting cannot fix this because it decays across context, so the fix must be *enforced* (theme namespaces deleted at the compiler level) and *fetchable* (system distributed as a registry, not re-explained).

## Architecture (current)

**Architecture (current):**
- `learnings/` — research notes (Tailwind, shadcn, layout, page-content, marketing-ui, agent-system). Source URL + verification date convention.
- `apps/web/` — Next.js 16 showcase site. Header, footer, nav, cards, code-block (Shiki), pager (previous/next), all on shadcn/Base UI + Tailwind v4.
- `packages/ui/` — shadcn primitives (Base UI, not Radix), tokens, globals.css. Don't touch — this is the foundation.
- `packages/registry/` — the deessejs registry components. Currently has Button (re-export) + IconButton (real impl). Each component has `index.tsx` + `meta.ts`. Source extraction via `fs.readFileSync` at module load.
- `packages/registry/` — deessejs registry components. Currently has Button (re-export) + IconButton + ColoredBadge (real impl). Each component has `index.tsx` (component + Demo export) and `meta.ts` (ComponentMeta).
- `apps/web/lib/registry/` — types, sources, aggregator. The seam for future DB-backed registry.
- `docs/product/README.md` — product-facing README for the registry site.
- `apps/web/scripts/build-sources.mjs` — build-time codegen that reads `packages/registry/src/**/*.tsx` and emits `apps/web/lib/registry/sources.generated.ts`.

## Timeless patterns (apply to all future work on this project)

### Encodeability at the type level

**Required fields, no fallback.** Every `ComponentEntry` has `Demo: React.ComponentType` (required). Every card takes `preview: ReactNode` (required). Every category has `Preview: React.ComponentType`. There is no "placeholder if missing" — TS errors at compile time if anyone adds a component without a Demo. This is the project's *anti-slop* principle applied: don't let the system degrade to a default.

**Add a component** = create `index.tsx` + `meta.ts` + register in `apps/web/lib/registry/index.tsx`. See `.claude/skills/add-component/SKILL.md`.

### Build pipeline (do not break this chain)

1. `prebuild`: `node scripts/build-sources.mjs` — reads sources, writes `sources.generated.ts`
2. `npm run build -w @workspace/registry` — produces `packages/registry/dist/`
3. `next build` — bundles everything

All three steps are chained in `apps/web/package.json`'s `build` script. **Do not split them** — Vercel's auto-detected turbo scope is `web` only, so the registry needs to be built by npm-workspace before next runs. The generated `sources.generated.ts` is **checked into git** so cold builds work without re-running prebuild.

### Vercel-specific gotchas

- **No `fs.readFileSync` at runtime.** `packages/registry/src/**` is not in the deployed bundle. Source extraction happens at build time via the codegen script.
- **`?raw` imports don't work in this Next.js + Turbopack + workspaces combo.** Tested and confirmed.
- **`@workspace/registry/*` must resolve via `dist/`** — the registry needs to be built before the web build.

### Card system (visual language)

- Flat: no `rounded-lg`, no per-card borders
- `bg-background` on cards (blends with page)
- Dividers via Tailwind: `divide-y divide-border sm:divide-y-0 sm:divide-x` on the grid container, `border-b border-border` on the inner preview/body separator
- Preview area: `h-60 sm:aspect-square` with `previewClassName="sm:h-60"` override when the card spans 2 cols
- Last item in odd-count grids gets `sm:col-span-2 sm:border-t sm:border-border` for the row separator

### Layout principles (from `docs/learnings/layout/`)

- **Modulation**: sparse → dense → sparse. Not uniform `gap-X` everywhere.
- **One focal anchor** per page (the H1 on the homepage, the Preview tab on detail pages). Rest is subordinate.
- **Named relationships**: H1 → subtitle (`gap-12`), subtitle → CTA (`gap-8`), section → section (`pt-24 pb-24`). Not all the same gap.
- **Macro frame**: `border-t border-border` to delimit page-level sections.

### Brand

- Display name: **DeesseJS** (capital D, S)
- URLs and repo paths: lowercase `deessejs.com`, `github.com/deessejs/ui`
- Repo package: `@workspace/registry`

### Conventions

- Components organized in folders with `index.ts` barrel: `headers/`, `footers/`, `nav/`, `cards/`, `pager/`
- JSX needs `.tsx` files. **Never put JSX in `.ts` files** — rename to `.tsx`
- Server components by default. `"use client"` only when needed
- `cn()` helper for conditional classes (from `@workspace/ui/lib/utils`)
- Semantic tokens only. No raw palette utilities. No `dark:` variants (tokens handle both modes)
- Typecheck from `apps/web/`, not repo root: `cd apps/web && npx tsc --noEmit`

### Workflow

**How to apply:**
- Doc convention: source URL + verification date at the top, vendor-documented facts separated from our own judgment, applied synthesis docs explicitly labelled as opinion.
- Components: each `packages/registry/src/components/<id>/` has `index.tsx` (component + Demo export) and `meta.ts` (ComponentMeta). New components are added to `apps/web/lib/registry/{index,sources}.ts`.
- Conventions: semantic tokens only (no raw palette), `flex + gap-*` (no `space-*`), `font-mono text-xs` for technical labels, no `dark:` variants (tokens handle both modes), `cn()` helper for conditional classes.
- Typecheck must stay green at every step.
- **Don't commit/push without explicit confirmation** from the user
- Memory updates without code changes are fine
- Skill files live in `.claude/skills/<name>/SKILL.md`

See [[user-profile]] and [[feedback-concrete-over-theory]].
30 changes: 30 additions & 0 deletions .claude/agent-memory/main/project_phase4_validated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: project-phase4-validated
description: External install end-to-end validation completed 2026-07-29 — Phase 6 (submit to official shadcn registry index) gate is now satisfied
metadata:
type: project
---

Phase 4 of [[project-design-learnings]] validated on 2026-07-29: a fresh Next.js 16 + Tailwind v4 + shadcn consumer sandbox installed all 3 ds-* components from `https://ui.deessejs.com/r/*.json`, built clean, and rendered SSR with correct tokens.

**Sandbox:** `temp/sandbox-validate/` (Next 16.2.12, React 19.2.4, Tailwind v4, shadcn init `--defaults` → preset `base-nova`).

**Evidence:**
- `npx shadcn@latest add https://ui.deessejs.com/r/ds-button.json` → `components/ui/ds-button.tsx`, peer deps (`@base-ui/react`, `cva`, `clsx`, `tailwind-merge`) installed
- Same for `ds-icon-button.json`, `ds-colored-badge.json`
- `npm run build` → "Compiled successfully in 3.4s", TypeScript clean, 4 static pages
- `npx next start -p 3939` + `curl` → HTTP 200, 18.7 KB HTML containing `data-slot="button"`, `bg-primary`, `bg-blue-600/10`, `aria-label`, "variant helper OK" (proves `dsButtonVariants` runtime helper works)

**Drift fixes confirmed shipped in prod:**
- `d90b7d9` — `ds-colored-badge` consumer inlined workspace Badge class strings
- `e2e47a4` — blue shade bumped from `bg-blue-500` to `bg-blue-600` across both trees
Both fixes visible in the deployed JSON at `https://ui.deessejs.com/r/ds-colored-badge.json`.

**Why:** the Phase 6 gate in `docs/plans/2026-07-29-shadcn-registry-adoption.md` explicitly defers submission to `https://ui.shadcn.com/r/registries.json` until *"at least one external user has confirmed an install end-to-end."* That gate is now formally satisfied — independent verification against the deployed registry, not just the contract test shim.

**How to apply:**
- Phase 6 PR is now legitimate to open. The submission entry is locked in the plan: `{ "name": "deessejs", "url": "https://ui.deessejs.com/r/{name}.json", "homepage": "https://ui.deessejs.com", "description": "DeesseJS components — Base UI on shadcn base-nova tokens." }`
- Reuse the `temp/sandbox-validate/` pattern for any future regression check (refresh `node_modules`, re-add, re-curl) — non-disruptive, lives outside the workspace tree
- The Turbopack multi-lockfile warning during build was sandbox-specific (parent repo + sandbox both have lockfiles), not a registry issue — do not chase it

Related: [[project-design-learnings]]
124 changes: 124 additions & 0 deletions .claude/skills/add-component/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
name: add-component
description: Add a new component to the @deessejs/ui registry — file scaffolding, registry wiring, codegen, deploy
---

Add a new component to the registry at ui.deessejs.com. Walks through the four-file pattern: `index.tsx`, `meta.ts`, registry entry, and source codegen.

## When to use

- User asks to add a new component to the registry
- User asks to migrate a component from mock data to a real implementation
- After adding a new component to `packages/ui/` that should be showcased

## Workflow

### 1. Create the component files

```
packages/registry/src/components/<id>/
├── index.tsx ← React component + Demo export
└── meta.ts ← ComponentMeta
```

**`index.tsx`** — real implementation OR re-export from `@workspace/ui`:

```tsx
"use client"

import { Button as ShadcnButton } from "@workspace/ui/components/button"

export type ButtonProps = React.ComponentProps<typeof ShadcnButton>

export { ShadcnButton as Button }

export function ButtonDemo() {
return (
<div className="flex flex-wrap items-center gap-2">
<ShadcnButton variant="default">Default</ShadcnButton>
{/* ...other variants */}
</div>
)
}
```

The `Demo` export renders in the preview tab. Keep it self-contained — no providers, no external state.

**`meta.ts`** — metadata for registry indexing:

```ts
import type { ComponentMeta } from "../../types.ts"

export const meta: ComponentMeta = {
id: "button",
name: "Button",
description: "Default button with variants.",
category: "buttons",
variants: ["default", "secondary", "outline", "ghost", "destructive", "link"],
}
```

Categories are typed in `packages/registry/src/types.ts`. If the category doesn't exist yet, add it to `COMPONENT_CATEGORIES`.

### 2. Register in `apps/web`

**`apps/web/lib/registry/index.ts`** — add the entry to `COMPONENT_REGISTRY`:

```ts
import { Button, ButtonDemo } from "@workspace/registry/components/button"
import { meta as buttonMeta } from "@workspace/registry/components/button/meta"

const COMPONENT_REGISTRY: ComponentEntry[] = [
{ ...buttonMeta, Component: Button, Demo: ButtonDemo, source: SOURCES.components.button },
// ... existing entries
]
```

**`apps/web/lib/registry/sources.ts`** — usually no change needed. The `prebuild` codegen script auto-discovers source files in `packages/registry/src/components/<id>/index.tsx`.

### 3. Verify locally

```bash
npm run typecheck # must stay green
npm run build # runs prebuild → registry build → next build
```

`prebuild` regenerates `apps/web/lib/registry/sources.generated.ts` with the new component's source. Commit this generated file (it's checked into git so cold builds work without re-running prebuild).

### 4. Commit and push

```bash
git add \
packages/registry/src/components/<id>/ \
apps/web/lib/registry/index.ts \
apps/web/lib/registry/sources.generated.ts
git commit -m "feat(registry): add <id>"
git push origin main
```

Vercel deploys automatically. Production URL: `https://ui.deessejs.com/components/<category>/<id>`.

## What gets generated

The `prebuild` step (`apps/web/scripts/build-sources.mjs`):
1. Scans `packages/registry/src/components/*/index.tsx` and `packages/registry/src/blocks/*/index.tsx`
2. Emits `apps/web/lib/registry/sources.generated.ts` with each source as a string literal

The generated file gets bundled into the Next.js output. **No `fs.readFileSync` at runtime** — that's why the deployment works on Vercel (where the source files aren't in the bundle).

## Common pitfalls

- **ComponentMeta not exported**: `meta.ts` must have `export const meta` (named export, not default)
- **Category not in the typed list**: add it to `COMPONENT_CATEGORIES` in `packages/registry/src/types.ts`
- **Demo breaks SSR**: avoid hooks needing providers (React Query, theme); keep `Demo` stateless
- **Source code wrong in Code tab**: re-run `npm run build` to regenerate `sources.generated.ts`
- **Build fails with `Module not found @workspace/registry/...`**: confirm `packages/registry/dist/` exists. If not, run `npm run build -w @workspace/registry`

## Reference

- Registry aggregator: `apps/web/lib/registry/index.ts`
- Types: `packages/registry/src/types.ts`
- Source extraction: `apps/web/scripts/build-sources.mjs`
- Generated sources: `apps/web/lib/registry/sources.generated.ts`
- Page consumer: `apps/web/app/components/[category_id]/[component_id]/page.tsx`
- Live site: https://ui.deessejs.com
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ node_modules
# testing
coverage
.contract-test/ # ephemeral shim project created by apps/web/scripts/contract-test.mjs
temp/sandbox-validate

# next.js
.next/
Expand Down
55 changes: 55 additions & 0 deletions apps/web/lib/registry/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@
IconButtonDemo,
} from "@workspace/registry/components/icon-button"
import { meta as iconButtonMeta } from "@workspace/registry/components/icon-button/meta"
import {
Breadcrumb,
BreadcrumbDemo,
} from "@workspace/registry/components/breadcrumb"
import { meta as breadcrumbMeta } from "@workspace/registry/components/breadcrumb/meta"
import {
Empty,
EmptyDemo,
} from "@workspace/registry/components/empty"
import { meta as emptyMeta } from "@workspace/registry/components/empty/meta"
import {
Tabs,
TabsDemo,
} from "@workspace/registry/components/tabs"
import { meta as tabsMeta } from "@workspace/registry/components/tabs/meta"
import {
Input,
InputDemo,
} from "@workspace/registry/components/input"
import { meta as inputMeta } from "@workspace/registry/components/input/meta"
import {
Textarea,
TextareaDemo,
} from "@workspace/registry/components/textarea"
import { meta as textareaMeta } from "@workspace/registry/components/textarea/meta"

import { SOURCES } from "./sources"
import type { ComponentMeta, BlockMeta } from "./types"
Expand Down Expand Up @@ -50,6 +75,36 @@
Demo: IconButtonDemo,
source: SOURCES.components["icon-button"],
},
{
...breadcrumbMeta,
Component: Breadcrumb,
Demo: BreadcrumbDemo,
source: SOURCES.components.breadcrumb,
},
{
...emptyMeta,
Component: Empty,
Demo: EmptyDemo,
source: SOURCES.components.empty,
},
{
...tabsMeta,
Component: Tabs,
Demo: TabsDemo,
source: SOURCES.components.tabs,
},
{
...inputMeta,
Component: Input,
Demo: InputDemo,
source: SOURCES.components.input,
},
{
...textareaMeta,
Component: Textarea,
Demo: TextareaDemo,
source: SOURCES.components.textarea,
},
]

const BLOCK_REGISTRY: BlockEntry[] = []
Expand Down Expand Up @@ -136,13 +191,13 @@
function deriveComponentPreview(items: ComponentEntry[]): React.ComponentType {
const First = items[0]!
const Demo = First.Demo
return () => <Demo />

Check warning on line 194 in apps/web/lib/registry/index.tsx

View workflow job for this annotation

GitHub Actions / Lint

Component definition is missing display name
}

function deriveBlockPreview(items: BlockEntry[]): React.ComponentType {
const First = items[0]!
const Block = First.Block
return () => <Block />

Check warning on line 200 in apps/web/lib/registry/index.tsx

View workflow job for this annotation

GitHub Actions / Lint

Component definition is missing display name
}

export function getEnrichedComponentCategories(): EnrichedComponentCategory[] {
Expand Down
Loading
Loading