create-patties 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +4 -2
  3. package/src/index.ts +345 -167
  4. package/src/prompts.ts +62 -17
  5. package/src/readme.ts +12 -5
  6. package/src/ui.ts +93 -0
  7. package/templates/_backend/app/routes/api/todos.ts +30 -0
  8. package/templates/_claude/.claude/commands/patties-init.md +33 -0
  9. package/templates/_claude/.claude/skills/patties/SKILL.md +104 -0
  10. package/templates/_codex/.codex/rules/patties-patterns.md +105 -0
  11. package/templates/_codex/AGENTS.md +1 -0
  12. package/templates/_container/.dockerignore +7 -0
  13. package/templates/_container/Dockerfile +27 -0
  14. package/templates/_monorepo/packages/README.md +18 -0
  15. package/templates/_shared/patties-patterns.md +105 -0
  16. package/templates/default/README-template.md +85 -71
  17. package/templates/default/app/routes/api/health.ts +8 -0
  18. package/templates/ui-starter/_internal/cn.ts +6 -0
  19. package/templates/ui-starter/_internal/slot.ts +50 -0
  20. package/templates/ui-starter/_internal/variants.ts +1 -0
  21. package/templates/ui-starter/button.tsx +60 -0
  22. package/templates/ui-starter/card.tsx +92 -0
  23. package/templates/ui-starter/demo/TodoApp.tsx +86 -0
  24. package/templates/ui-starter/demo/index.tsx +41 -0
  25. package/templates/ui-starter/input.tsx +20 -0
  26. package/templates/ui-starter/label.tsx +18 -0
  27. package/templates/ui-starter/themes/neutral/tokens.css +46 -0
  28. package/templates/ui-starter/themes/slate/tokens.css +46 -0
  29. package/templates/ui-starter/themes/stone/tokens.css +46 -0
  30. package/templates/ui-starter/themes/zinc/tokens.css +46 -0
  31. package/templates/ui-starter/tokens.css +46 -0
package/src/prompts.ts CHANGED
@@ -1,12 +1,15 @@
1
- // Interactive prompts for create-patties. See spec cli/09-create-patties-dx
2
- // items 3 + 5. Uses Bun's built-in prompt() — no new dependency.
1
+ // Interactive prompts for create-patties. See cli specs 09 + 18.
2
+ // Uses Bun's built-in prompt() — no new dependency.
3
3
  //
4
4
  // Non-TTY callers must supply the equivalent flags; the run loop only invokes
5
- // these helpers when stdin is a TTY and --yes was not passed.
5
+ // these helpers when stdin is a TTY and --yes was not passed. The prompt order
6
+ // (spec 18) is gated by project type: UI is asked only for frontend/fullstack,
7
+ // monorepo only for fullstack, and the deploy target option set depends on type.
6
8
 
7
9
  export type AgentTemplate = "claude" | "codex" | "none";
8
- export type Target = "bun" | "edge";
9
- export type Scaffold = "demo" | "blank";
10
+ export type ProjectType = "frontend" | "backend" | "fullstack";
11
+ export type Target = "bun" | "edge" | "container";
12
+ export type Theme = "neutral" | "slate" | "stone" | "zinc";
10
13
  export type Deploy =
11
14
  | "cloudflare"
12
15
  | "vercel"
@@ -48,41 +51,83 @@ export function promptName(
48
51
  return undefined;
49
52
  }
50
53
 
54
+ // "old school" is the TTY label for the no-agent choice; the flag value stays
55
+ // `none` (spec 18). `--template` remains an alias for `--agent`.
51
56
  export function promptAgent(io: PromptIO = {}): AgentTemplate {
52
57
  const ask = io.prompt ?? prompt;
53
58
  const answer = (
54
- ask("Which AI coding agent will you use? [claude/codex/none] (claude) ") ??
55
- ""
59
+ ask(
60
+ "Which coding agent are you using? [Claude/Codex/old school] (Claude) ",
61
+ ) ?? ""
56
62
  )
57
63
  .trim()
58
64
  .toLowerCase();
59
65
  if (answer === "codex") return "codex";
60
- if (answer === "none") return "none";
66
+ if (answer === "none" || answer === "old school" || answer === "old-school") {
67
+ return "none";
68
+ }
61
69
  return "claude";
62
70
  }
63
71
 
64
- export function promptScaffold(io: PromptIO = {}): Scaffold {
72
+ export function promptType(io: PromptIO = {}): ProjectType {
65
73
  const ask = io.prompt ?? prompt;
66
- const answer = (ask("Include the interactive todo demo? [Y/n] (Y) ") ?? "")
74
+ const answer = (
75
+ ask(
76
+ "What kind of project are you building? [frontend/backend/fullstack] (fullstack) ",
77
+ ) ?? ""
78
+ )
67
79
  .trim()
68
80
  .toLowerCase();
69
- if (answer === "n" || answer === "no" || answer === "blank") return "blank";
70
- return "demo";
81
+ if (answer === "frontend") return "frontend";
82
+ if (answer === "backend") return "backend";
83
+ return "fullstack";
71
84
  }
72
85
 
73
- export function promptTarget(io: PromptIO = {}): Target {
86
+ // Asked only for frontend/fullstack backend has no UI surface.
87
+ export function promptUi(io: PromptIO = {}): boolean {
74
88
  const ask = io.prompt ?? prompt;
75
- const answer = (ask("Runtime target? [bun/edge] (bun) ") ?? "")
89
+ const answer = (
90
+ ask("Do you want a styled UI template (Patties UI)? [Y/n] (Y) ") ?? ""
91
+ )
76
92
  .trim()
77
93
  .toLowerCase();
78
- return answer === "edge" ? "edge" : "bun";
94
+ return !(answer === "n" || answer === "no");
95
+ }
96
+
97
+ // Asked only for fullstack.
98
+ export function promptMonorepo(io: PromptIO = {}): boolean {
99
+ const ask = io.prompt ?? prompt;
100
+ const answer = (ask("Do you want a monorepo structure? [y/N] (N) ") ?? "")
101
+ .trim()
102
+ .toLowerCase();
103
+ return answer === "y" || answer === "yes";
104
+ }
105
+
106
+ // The option set depends on project type: container is fullstack-only.
107
+ export function promptTarget(type: ProjectType, io: PromptIO = {}): Target {
108
+ const ask = io.prompt ?? prompt;
109
+ const options = type === "fullstack" ? "bun/edge/container" : "bun/edge";
110
+ const answer = (ask(`Where will you deploy? [${options}] (bun) `) ?? "")
111
+ .trim()
112
+ .toLowerCase();
113
+ if (answer === "edge" || answer === "worker" || answer === "worker/edge") {
114
+ return "edge";
115
+ }
116
+ if (
117
+ type === "fullstack" &&
118
+ (answer === "container" ||
119
+ answer === "docker" ||
120
+ answer === "container/docker")
121
+ ) {
122
+ return "container";
123
+ }
124
+ return "bun";
79
125
  }
80
126
 
81
127
  export function promptDeploy(io: PromptIO = {}): Deploy {
82
128
  const ask = io.prompt ?? prompt;
83
129
  const answer = (
84
- ask("Deploy plugin? [cloudflare/vercel/deno/netlify/bun/none] (none) ") ??
85
- ""
130
+ ask("Deploy plugin? [cloudflare/vercel/deno/netlify/none] (none) ") ?? ""
86
131
  )
87
132
  .trim()
88
133
  .toLowerCase();
package/src/readme.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  // 1. Strip/keep conditional blocks delimited by:
5
5
  // <!-- if:KEY=VALUE -->...<!-- /if -->
6
6
  // The block is kept iff `vars[KEY] === VALUE`.
7
- // 2. Replace `{{name}}`, `{{agent}}`, `{{target}}`, `{{deploy}}` placeholders
8
- // with the corresponding `vars` entry.
7
+ // 2. Replace `{{name}}`, `{{agent}}`, `{{type}}`, `{{ui}}`, `{{monorepo}}`,
8
+ // `{{app_name}}`, `{{target}}`, `{{deploy}}` placeholders with the
9
+ // corresponding `vars` entry.
9
10
  //
10
11
  // `renderTemplatesInTree` walks a scaffolded project and applies the template
11
12
  // to every text file we care about (README.md, *.md, *.json, *.ts, *.tsx).
@@ -14,9 +15,12 @@
14
15
  export interface TemplateVars {
15
16
  name: string;
16
17
  agent: "claude" | "codex" | "none";
17
- target: "bun" | "edge";
18
+ type: "frontend" | "backend" | "fullstack";
19
+ ui: "yes" | "no";
20
+ monorepo: "yes" | "no";
21
+ target: "bun" | "edge" | "container";
18
22
  deploy: "cloudflare" | "vercel" | "deno" | "netlify" | "bun" | "none";
19
- scaffold: "demo" | "blank";
23
+ app_name: string;
20
24
  }
21
25
 
22
26
  const CONDITIONAL_RE =
@@ -37,9 +41,12 @@ export function applyTemplate(source: string, vars: TemplateVars): string {
37
41
  stripped
38
42
  .replaceAll("{{name}}", vars.name)
39
43
  .replaceAll("{{agent}}", vars.agent)
44
+ .replaceAll("{{type}}", vars.type)
45
+ .replaceAll("{{ui}}", vars.ui)
46
+ .replaceAll("{{monorepo}}", vars.monorepo)
47
+ .replaceAll("{{app_name}}", vars.app_name)
40
48
  .replaceAll("{{target}}", vars.target)
41
49
  .replaceAll("{{deploy}}", vars.deploy)
42
- .replaceAll("{{scaffold}}", vars.scaffold)
43
50
  // Legacy placeholders from earlier overlay revisions — kept so existing
44
51
  // CLAUDE.md / AGENTS.md template text keeps interpolating.
45
52
  .replaceAll("{{PROJECT_NAME}}", vars.name)
package/src/ui.ts ADDED
@@ -0,0 +1,93 @@
1
+ // Patties UI starter integration (spec 18 §Patties UI).
2
+ //
3
+ // create-patties stays zero-dependency and offline, so instead of shelling out
4
+ // to `patties ui init` + `patties add` (which need the project's installed
5
+ // binary) we vendor the starter set under templates/ui-starter/ and copy it
6
+ // directly. The vendored copies are byte-for-byte identical to
7
+ // packages/patties-ui/templates/ — a drift test enforces that — so this is
8
+ // equivalent to what `patties add button card input label` would stamp.
9
+
10
+ import { existsSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import type { Theme } from "./prompts.ts";
13
+
14
+ // Runtime peer deps the starter components + _internal helpers import.
15
+ // Derived from packages/patties-ui/src/registry.ts entries for the starter set:
16
+ // button → cn + cva, card → cn, input → cn, label → cn + @radix-ui/react-label
17
+ export const UI_DEPS: Record<string, string> = {
18
+ "@radix-ui/react-label": "^2.1.0",
19
+ "class-variance-authority": "^0.7.0",
20
+ clsx: "^2.1.0",
21
+ "tailwind-merge": "^2.5.0",
22
+ };
23
+
24
+ // Dev-only: the catalog the user runs `patties add` / `patties ui` against.
25
+ export const UI_DEV_DEPS: Record<string, string> = {
26
+ "patties-ui": "latest",
27
+ };
28
+
29
+ const STARTER_COMPONENTS = ["button", "card", "input", "label"] as const;
30
+
31
+ // Stamp the vendored starter set into an app directory and rewrite the demo to
32
+ // import the stamped components. `uiStarterDir` is templates/ui-starter.
33
+ export async function applyUiStarter(
34
+ appRoot: string,
35
+ theme: Theme,
36
+ uiStarterDir: string,
37
+ ): Promise<void> {
38
+ const uiDir = `${appRoot}/app/components/ui`;
39
+ const internalDir = `${uiDir}/_internal`;
40
+ const stylesDir = `${appRoot}/app/styles`;
41
+ await Bun.$`mkdir -p ${internalDir} ${stylesDir}`.quiet();
42
+
43
+ for (const name of STARTER_COMPONENTS) {
44
+ await Bun.$`cp ${uiStarterDir}/${name}.tsx ${uiDir}/${name}.tsx`.quiet();
45
+ }
46
+ await Bun.$`cp -R ${uiStarterDir}/_internal/. ${internalDir}`.quiet();
47
+
48
+ const themeTokens = resolve(uiStarterDir, "themes", theme, "tokens.css");
49
+ const tokens = existsSync(themeTokens)
50
+ ? themeTokens
51
+ : resolve(uiStarterDir, "tokens.css");
52
+ await Bun.$`cp ${tokens} ${stylesDir}/tokens.css`.quiet();
53
+ // app.css is written (not vendored) because a committed `@theme` stylesheet
54
+ // would trip this repo's Biome CSS parser, which has Tailwind directives off.
55
+ await Bun.write(`${stylesDir}/app.css`, APP_CSS);
56
+
57
+ // Demo page + island that import the stamped components. Only applies to
58
+ // frontend / fullstack, which always have app/routes + app/islands.
59
+ await Bun.$`cp ${uiStarterDir}/demo/index.tsx ${appRoot}/app/routes/index.tsx`.quiet();
60
+ await Bun.$`cp ${uiStarterDir}/demo/TodoApp.tsx ${appRoot}/app/islands/TodoApp.tsx`.quiet();
61
+ }
62
+
63
+ // Pre-wired Tailwind v4 + tokens stylesheet (the wiring `patties ui init`
64
+ // prints). Patties does not bundle Tailwind — compile this with the Tailwind
65
+ // CLI; see the generated README "Styling" section.
66
+ const APP_CSS = `@import "tailwindcss";
67
+ @import "./tokens.css";
68
+
69
+ @theme inline {
70
+ --color-background: var(--background);
71
+ --color-foreground: var(--foreground);
72
+ --color-card: var(--card);
73
+ --color-card-foreground: var(--card-foreground);
74
+ --color-popover: var(--popover);
75
+ --color-popover-foreground: var(--popover-foreground);
76
+ --color-primary: var(--primary);
77
+ --color-primary-foreground: var(--primary-foreground);
78
+ --color-secondary: var(--secondary);
79
+ --color-secondary-foreground: var(--secondary-foreground);
80
+ --color-muted: var(--muted);
81
+ --color-muted-foreground: var(--muted-foreground);
82
+ --color-accent: var(--accent);
83
+ --color-accent-foreground: var(--accent-foreground);
84
+ --color-destructive: var(--destructive);
85
+ --color-destructive-foreground: var(--destructive-foreground);
86
+ --color-border: var(--border);
87
+ --color-input: var(--input);
88
+ --color-ring: var(--ring);
89
+ --radius-lg: var(--radius);
90
+ --radius-md: calc(var(--radius) - 2px);
91
+ --radius-sm: calc(var(--radius) - 4px);
92
+ }
93
+ `;
@@ -0,0 +1,30 @@
1
+ import type { PattiesContext } from "patties/middleware";
2
+
3
+ // A small in-memory sample resource so a backend project boots with a working
4
+ // endpoint. Swap the array for a real data source when you're ready.
5
+ interface Todo {
6
+ id: number;
7
+ text: string;
8
+ done: boolean;
9
+ }
10
+
11
+ const todos: Todo[] = [
12
+ { id: 1, text: "wire up a database", done: false },
13
+ { id: 2, text: "add authentication", done: false },
14
+ ];
15
+
16
+ export function GET(_req: Request, ctx: PattiesContext): Response {
17
+ return ctx.json({ todos });
18
+ }
19
+
20
+ export async function POST(
21
+ req: Request,
22
+ ctx: PattiesContext,
23
+ ): Promise<Response> {
24
+ const body = (await req.json()) as { text?: unknown };
25
+ const text = typeof body.text === "string" ? body.text.trim() : "";
26
+ if (!text) return ctx.json({ error: "text is required" }, { status: 400 });
27
+ const todo: Todo = { id: todos.length + 1, text, done: false };
28
+ todos.push(todo);
29
+ return ctx.json({ todo }, { status: 201 });
30
+ }
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: Guided, plan-mode first scaffold for a Patties project — pick feature patterns, see a plan, then scaffold after approval.
3
+ ---
4
+
5
+ # /patties-init
6
+
7
+ Guide the user through their first feature scaffold. This command is a thin
8
+ driver — all the recipe detail lives in the `patties` skill. Do **not**
9
+ duplicate the pattern catalog here; defer to `/patties`.
10
+
11
+ You are (or should be) in **plan mode**: explore and propose, but write nothing
12
+ until the user approves the plan.
13
+
14
+ Run these four steps:
15
+
16
+ 1. **Discover** — read the project to fit the plan to what was scaffolded:
17
+ - project type (frontend / backend / fullstack) and deploy target, from
18
+ `patties.config.ts` and `package.json`,
19
+ - whether Patties UI is initialized (does `app/components/ui/_internal/`
20
+ exist?),
21
+ - flat vs. monorepo layout (is there an `apps/` workspace?).
22
+ 2. **Ask** — which feature pattern(s) from the `patties` skill catalog the user
23
+ wants (auth-rbac, crm, task, pivot, dashboard), plus the domain specifics the
24
+ recipes adapt to: entity names, fields, and roles.
25
+ 3. **Plan** — present the file list, the `patties add` components to stamp, and
26
+ the mock-data shape, honoring the skill's **UI + mock data** depth contract
27
+ (no `bun:sqlite`, migrations, or real backend). Write nothing yet.
28
+ 4. **Scaffold** — once the user approves and exits plan mode, generate the files
29
+ per the `patties` skill recipes. If the catalog isn't initialized, run
30
+ `patties ui init` first.
31
+
32
+ For ongoing work after this first scaffold (adding one component or one more
33
+ pattern later), the user invokes the `/patties` skill directly.
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: patties
3
+ description: Use when adding a Patties UI component or scaffolding a feature pattern (auth + RBAC, CRM, task board, pivot table, dashboard). Trigger phrases include "add a component", "scaffold a CRM / dashboard / auth", "patties pattern", "/patties".
4
+ ---
5
+
6
+ # /patties — components & feature patterns
7
+
8
+ ## When to use
9
+
10
+ Use this when the user wants to **add a Patties UI component** or **scaffold a
11
+ feature pattern** (auth + RBAC, a CRM, a task board, a pivot table, a
12
+ dashboard). It covers scaffolding only — for running, building, deploying, or
13
+ managing secrets, use the `patties-cli` skill instead.
14
+
15
+ Two capabilities:
16
+
17
+ 1. **Add a UI component** — a thin wrapper over the deterministic catalog.
18
+ 2. **Scaffold a feature pattern** — instruction-driven: you generate the files,
19
+ adapting names/fields/copy to the user's domain.
20
+
21
+ ## Add a component
22
+
23
+ The component catalog is deterministic — never hand-author component source; the
24
+ registry is the source of truth.
25
+
26
+ 1. If `app/components/ui/_internal/` does not exist, the catalog isn't
27
+ initialized — run `patties ui init` first (pass `--theme <neutral|slate|stone|zinc>`
28
+ to match the project).
29
+ 2. Run `patties add <name>` (preview first with `patties view <name>` or
30
+ `patties add --view <name>` if the user wants to see the source).
31
+ 3. Import the stamped component from `app/components/ui/<name>.tsx` and use it.
32
+
33
+ `patties add` edits `package.json` (it never runs an install) and stamps source
34
+ into `app/components/ui/`. After adding, remind the user to run `bun install` if
35
+ new peer deps were added.
36
+
37
+ ## Scaffold a pattern
38
+
39
+ Pick the pattern from the catalog, `patties add` its components first, then
40
+ generate the listed files — adapting entity names, fields, columns, and copy to
41
+ the user's stated domain (e.g. "a CRM for veterinary clinics").
42
+
43
+ | Pattern | Goal | `patties add` components | Generated files (under `app/`) |
44
+ |---|---|---|---|
45
+ | **auth-rbac** | Login / logout + role-gated route | `form`, `input`, `label`, `button`, `card` | `routes/login.tsx`, `routes/logout.ts`, `routes/admin.tsx` (role-gated), `lib/auth.ts` (cookie session over mock users), `lib/rbac.ts` (role guard), `lib/mock-users.ts` |
46
+ | **crm** | Contacts list + detail / edit | `data-table`, `dialog`, `form`, `input`, `button`, `badge` | `routes/contacts/index.tsx` (table), `routes/contacts/[id].tsx` (detail), `islands/ContactForm.tsx`, `lib/mock-contacts.ts` |
47
+ | **task** | Task board / list | `card`, `checkbox`, `badge`, `dialog`, `button` | `routes/tasks.tsx`, `islands/TaskBoard.tsx` (columns + toggle), `lib/mock-tasks.ts` |
48
+ | **pivot** | Group-by / pivot over rows | `table`, `select`, `card` | `routes/pivot.tsx`, `islands/PivotTable.tsx` (pick row/col/measure), `lib/mock-rows.ts` |
49
+ | **dashboard** | Metrics overview | `card`, `chart`, `separator`, `sidebar` | `routes/dashboard.tsx` (cards + chart + sidebar shell), `lib/mock-metrics.ts` |
50
+
51
+ Per-pattern recipes:
52
+
53
+ - **auth-rbac** — sessions are a signed cookie over the mock user list; the RBAC
54
+ guard is a small helper the protected route calls in its handler. Make the
55
+ mock-only nature loud: a banner on the login page and a TODO at the top of the
56
+ auth module. Real auth needs persistence (a future DB spec).
57
+ - **crm** — the list uses `data-table` over `mock-contacts`; create / edit
58
+ happens in a `dialog` driven by a `form` island. The detail route reads the
59
+ same mock store by id.
60
+ - **task** — the board is an island holding `useState` columns; toggling a
61
+ `checkbox` moves a task between done / not-done. No persistence — state resets
62
+ on reload (call this out).
63
+ - **pivot** — an island lets the user pick a row dimension, a column dimension,
64
+ and a numeric measure from `select`s, then renders the aggregated `table`.
65
+ Aggregation runs client-side over the mock rows.
66
+ - **dashboard** — a static SSR shell (`sidebar` is `subtree`, `chart` hydrates)
67
+ with metric `card`s fed by `mock-metrics`; the chart is the one interactive
68
+ island.
69
+
70
+ Always `patties add` the listed components (initializing the catalog first if
71
+ needed) before generating files that import them. Only reference components that
72
+ exist in the shipped registry with `status: "completed"`.
73
+
74
+ ## Depth contract: UI + mock data
75
+
76
+ Every pattern is scaffolded at **"UI + mock data"** depth:
77
+
78
+ - **In scope:** SSR routes / pages, islands where interactivity is needed,
79
+ stamped Patties UI components, and a typed in-memory seed in
80
+ `app/lib/mock-<entity>.ts` that the pages read from.
81
+ - **Out of scope:** `bun:sqlite`, migrations, a real persistence or auth
82
+ backend, network calls. The pattern *shows the shape*; the developer swaps the
83
+ mock data for a real source later.
84
+ - Every generated `mock-*` module starts with `// TODO: replace mock data with a
85
+ real source`, and the route / page carries a short note marking the mock
86
+ boundary.
87
+
88
+ Do not generate a database layer, migrations, or a real auth/network backend
89
+ from this skill — that is deferred to a future spec.
90
+
91
+ ## Conventions
92
+
93
+ - Pages live under `app/routes/*.tsx` (default-export a React component); API
94
+ handlers under `app/routes/api/*.ts` (named `GET`/`POST`/… returning a
95
+ `Response`, e.g. via `ctx.json()`). Dynamic segments use `[id]`.
96
+ - Islands (interactive client components) live under `app/islands/` and are
97
+ wrapped at the use site in `<Island name="…">…</Island>` from `patties/render`.
98
+ - Mock data lives in `app/lib/mock-<entity>.ts`, typed, with the TODO marker.
99
+ - Never re-author component source — stamp from the catalog and import.
100
+
101
+ ## See also
102
+
103
+ - The `patties-cli` skill — running, building, deploying the app.
104
+ - cli specs 11–15 (Patties UI: `ui init`, `add`, `view`, `update`, registries).
@@ -0,0 +1,105 @@
1
+ # Patties patterns — components & feature scaffolding
2
+
3
+ > Codex rule. The catalog and recipes below are shared verbatim with the
4
+ > Claude `/patties` skill — both are generated from one source
5
+ > (`templates/_shared/patties-patterns.md` in create-patties), so they
6
+ > cannot drift. Read this when the user asks to add a component or scaffold a
7
+ > feature pattern.
8
+
9
+ ## When to use
10
+
11
+ Use this when the user wants to **add a Patties UI component** or **scaffold a
12
+ feature pattern** (auth + RBAC, a CRM, a task board, a pivot table, a
13
+ dashboard). It covers scaffolding only — for running, building, deploying, or
14
+ managing secrets, use the `patties-cli` skill instead.
15
+
16
+ Two capabilities:
17
+
18
+ 1. **Add a UI component** — a thin wrapper over the deterministic catalog.
19
+ 2. **Scaffold a feature pattern** — instruction-driven: you generate the files,
20
+ adapting names/fields/copy to the user's domain.
21
+
22
+ ## Add a component
23
+
24
+ The component catalog is deterministic — never hand-author component source; the
25
+ registry is the source of truth.
26
+
27
+ 1. If `app/components/ui/_internal/` does not exist, the catalog isn't
28
+ initialized — run `patties ui init` first (pass `--theme <neutral|slate|stone|zinc>`
29
+ to match the project).
30
+ 2. Run `patties add <name>` (preview first with `patties view <name>` or
31
+ `patties add --view <name>` if the user wants to see the source).
32
+ 3. Import the stamped component from `app/components/ui/<name>.tsx` and use it.
33
+
34
+ `patties add` edits `package.json` (it never runs an install) and stamps source
35
+ into `app/components/ui/`. After adding, remind the user to run `bun install` if
36
+ new peer deps were added.
37
+
38
+ ## Scaffold a pattern
39
+
40
+ Pick the pattern from the catalog, `patties add` its components first, then
41
+ generate the listed files — adapting entity names, fields, columns, and copy to
42
+ the user's stated domain (e.g. "a CRM for veterinary clinics").
43
+
44
+ | Pattern | Goal | `patties add` components | Generated files (under `app/`) |
45
+ |---|---|---|---|
46
+ | **auth-rbac** | Login / logout + role-gated route | `form`, `input`, `label`, `button`, `card` | `routes/login.tsx`, `routes/logout.ts`, `routes/admin.tsx` (role-gated), `lib/auth.ts` (cookie session over mock users), `lib/rbac.ts` (role guard), `lib/mock-users.ts` |
47
+ | **crm** | Contacts list + detail / edit | `data-table`, `dialog`, `form`, `input`, `button`, `badge` | `routes/contacts/index.tsx` (table), `routes/contacts/[id].tsx` (detail), `islands/ContactForm.tsx`, `lib/mock-contacts.ts` |
48
+ | **task** | Task board / list | `card`, `checkbox`, `badge`, `dialog`, `button` | `routes/tasks.tsx`, `islands/TaskBoard.tsx` (columns + toggle), `lib/mock-tasks.ts` |
49
+ | **pivot** | Group-by / pivot over rows | `table`, `select`, `card` | `routes/pivot.tsx`, `islands/PivotTable.tsx` (pick row/col/measure), `lib/mock-rows.ts` |
50
+ | **dashboard** | Metrics overview | `card`, `chart`, `separator`, `sidebar` | `routes/dashboard.tsx` (cards + chart + sidebar shell), `lib/mock-metrics.ts` |
51
+
52
+ Per-pattern recipes:
53
+
54
+ - **auth-rbac** — sessions are a signed cookie over the mock user list; the RBAC
55
+ guard is a small helper the protected route calls in its handler. Make the
56
+ mock-only nature loud: a banner on the login page and a TODO at the top of the
57
+ auth module. Real auth needs persistence (a future DB spec).
58
+ - **crm** — the list uses `data-table` over `mock-contacts`; create / edit
59
+ happens in a `dialog` driven by a `form` island. The detail route reads the
60
+ same mock store by id.
61
+ - **task** — the board is an island holding `useState` columns; toggling a
62
+ `checkbox` moves a task between done / not-done. No persistence — state resets
63
+ on reload (call this out).
64
+ - **pivot** — an island lets the user pick a row dimension, a column dimension,
65
+ and a numeric measure from `select`s, then renders the aggregated `table`.
66
+ Aggregation runs client-side over the mock rows.
67
+ - **dashboard** — a static SSR shell (`sidebar` is `subtree`, `chart` hydrates)
68
+ with metric `card`s fed by `mock-metrics`; the chart is the one interactive
69
+ island.
70
+
71
+ Always `patties add` the listed components (initializing the catalog first if
72
+ needed) before generating files that import them. Only reference components that
73
+ exist in the shipped registry with `status: "completed"`.
74
+
75
+ ## Depth contract: UI + mock data
76
+
77
+ Every pattern is scaffolded at **"UI + mock data"** depth:
78
+
79
+ - **In scope:** SSR routes / pages, islands where interactivity is needed,
80
+ stamped Patties UI components, and a typed in-memory seed in
81
+ `app/lib/mock-<entity>.ts` that the pages read from.
82
+ - **Out of scope:** `bun:sqlite`, migrations, a real persistence or auth
83
+ backend, network calls. The pattern *shows the shape*; the developer swaps the
84
+ mock data for a real source later.
85
+ - Every generated `mock-*` module starts with `// TODO: replace mock data with a
86
+ real source`, and the route / page carries a short note marking the mock
87
+ boundary.
88
+
89
+ Do not generate a database layer, migrations, or a real auth/network backend
90
+ from this skill — that is deferred to a future spec.
91
+
92
+ ## Conventions
93
+
94
+ - Pages live under `app/routes/*.tsx` (default-export a React component); API
95
+ handlers under `app/routes/api/*.ts` (named `GET`/`POST`/… returning a
96
+ `Response`, e.g. via `ctx.json()`). Dynamic segments use `[id]`.
97
+ - Islands (interactive client components) live under `app/islands/` and are
98
+ wrapped at the use site in `<Island name="…">…</Island>` from `patties/render`.
99
+ - Mock data lives in `app/lib/mock-<entity>.ts`, typed, with the TODO marker.
100
+ - Never re-author component source — stamp from the catalog and import.
101
+
102
+ ## See also
103
+
104
+ - The `patties-cli` skill — running, building, deploying the app.
105
+ - cli specs 11–15 (Patties UI: `ui init`, `add`, `view`, `update`, registries).
@@ -15,6 +15,7 @@ Built with Patties — a Bun-native full-stack meta-framework.
15
15
  - [Build-time discovery](.codex/rules/build-time-discovery.md) — discovery happens at build time, not at request time.
16
16
  - [Optional AI dependency](.codex/rules/optional-ai.md) — keep `@anthropic-ai/sdk` import-disjoint from non-AI code paths.
17
17
  - [Patties CLI](.codex/rules/patties-cli.md) — `patties dev / build / start / deploy / secret`.
18
+ - [Patties patterns](.codex/rules/patties-patterns.md) — add UI components and scaffold feature patterns (auth, CRM, task board, pivot, dashboard).
18
19
 
19
20
  ## Live inventory
20
21
 
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .patties
3
+ .git
4
+ *.log
5
+ Dockerfile
6
+ .dockerignore
7
+ README.md
@@ -0,0 +1,27 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Multi-stage build for a Patties app on the `bun` adapter.
3
+ # Build: docker build -t {{name}} .
4
+ # Run: docker run -p 3000:3000 {{name}}
5
+
6
+ FROM oven/bun:1.3 AS base
7
+ WORKDIR /app
8
+
9
+ # --- deps: install once, cached on package.json changes ---
10
+ FROM base AS deps
11
+ COPY package.json bun.lock* ./
12
+ RUN bun install
13
+
14
+ # --- build: produce the production server bundle in .patties/ ---
15
+ FROM base AS build
16
+ COPY --from=deps /app/node_modules ./node_modules
17
+ COPY . .
18
+ RUN bunx patties build --target bun
19
+
20
+ # --- release: minimal runtime image ---
21
+ FROM base AS release
22
+ ENV NODE_ENV=production
23
+ COPY --from=deps /app/node_modules ./node_modules
24
+ COPY --from=build /app/.patties ./.patties
25
+ COPY package.json ./
26
+ EXPOSE 3000
27
+ CMD ["bun", ".patties/server/server-entry.js"]
@@ -0,0 +1,18 @@
1
+ # packages/
2
+
3
+ Shared code for this Bun workspace. Create a package per shared concern
4
+ (e.g. `packages/ui`, `packages/db`) with its own `package.json`, then depend
5
+ on it from an app under `apps/*` using the workspace protocol:
6
+
7
+ ```jsonc
8
+ // apps/<app>/package.json
9
+ {
10
+ "dependencies": {
11
+ "@{{name}}/ui": "workspace:*"
12
+ }
13
+ }
14
+ ```
15
+
16
+ Bun resolves `workspace:*` to the local package and runs scripts
17
+ topologically with `bun --filter`. This monorepo is intentionally
18
+ Bun-workspaces-only — no Turborepo / Nx.