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
@@ -0,0 +1,105 @@
1
+ <!--
2
+ Canonical source for the /patties pattern catalog (cli spec 19).
3
+ Both the Claude skill (.claude/skills/patties/SKILL.md) and the Codex rule
4
+ (.codex/rules/patties-patterns.md) are generated from this body by
5
+ scripts/gen-patties-skill.ts. Edit THIS file, then run
6
+ `bun run generate:patties-skill`. Do not edit the generated files by hand.
7
+ -->
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).
@@ -3,80 +3,86 @@
3
3
  Built with [Patties](https://github.com/bihaviour-ai/bun-patties-framework) — a
4
4
  Bun-native full-stack meta-framework.
5
5
 
6
- <!-- if:scaffold=demo -->
6
+ <!-- if:type=fullstack -->
7
7
  ## What you got
8
8
 
9
- An interactive todo demo:
9
+ A full-stack Patties app:
10
10
 
11
- - `app/routes/index.tsx` — server-rendered page that mounts the island.
12
- - `app/islands/TodoApp.tsx` — `useState`-based todo list, hydrated in the
13
- browser.
11
+ - `app/routes/index.tsx` — server-rendered page that mounts an island.
12
+ - `app/islands/TodoApp.tsx` — `useState`-based todo list, hydrated in the browser.
13
+ - `app/routes/api/health.ts` — a JSON API route (`GET /api/health` → `{ ok: true }`).
14
14
  - `app/server.ts` — dev entry that wires the router into `Bun.serve`.
15
+ <!-- /if -->
16
+ <!-- if:type=frontend -->
17
+ ## What you got
15
18
 
16
- ## Run it
17
-
18
- ```sh
19
- bun install # if you used --no-install
20
- bunx patties dev # → http://localhost:3000
21
- ```
22
-
23
- Dev mode SSRs the page and hydrates the island, so the todo buttons work
24
- immediately. HMR reloads the browser when you edit a route or island.
19
+ A frontend Patties app:
25
20
 
26
- ## Try editing
21
+ - `app/routes/index.tsx` — server-rendered page that mounts an island.
22
+ - `app/islands/TodoApp.tsx` — `useState`-based todo list, hydrated in the browser.
23
+ - `app/server.ts` — dev entry that wires the router into `Bun.serve`.
24
+ <!-- /if -->
25
+ <!-- if:type=backend -->
26
+ ## What you got
27
27
 
28
- 1. Open `app/routes/index.tsx`, change the heading text, save. The browser
29
- reloads (HMR).
30
- 2. Open `app/islands/TodoApp.tsx`, change the initial todo list or input
31
- placeholder, save, and try it — the bundle rebuilds on the next request.
28
+ A backend Patties app API routes, no React UI:
32
29
 
33
- ## Remove the demo when you're ready
30
+ - `app/routes/api/health.ts` `GET /api/health` `{ ok: true }`.
31
+ - `app/routes/api/todos.ts` — a sample in-memory `GET` / `POST` resource.
32
+ - `app/server.ts` — dev entry that wires the router into `Bun.serve`.
33
+ <!-- /if -->
34
34
 
35
- The todo demo exists to show you state hydration. When you start your real
36
- app, delete it:
35
+ ## Run it
37
36
 
38
37
  ```sh
39
- rm app/islands/TodoApp.tsx
38
+ bun install # if you used --no-install
39
+ <!-- if:monorepo=yes -->bun --filter {{app_name}} dev # → http://localhost:3000<!-- /if -->
40
+ <!-- if:monorepo=no -->bunx patties dev # → http://localhost:3000<!-- /if -->
40
41
  ```
41
42
 
42
- Then replace `app/routes/index.tsx` with whatever your landing page should
43
- be a minimal version:
43
+ <!-- if:ui=yes -->
44
+ ## Patties UI
44
45
 
45
- ```tsx
46
- export default function Index(): JSX.Element {
47
- return <main><h1>Hello from {{name}}</h1></main>;
48
- }
49
- ```
46
+ Styled components are stamped into `app/components/ui/` (`button`, `card`,
47
+ `input`, `label`) with shared helpers in `app/components/ui/_internal/` and
48
+ design tokens in `app/styles/tokens.css`.
50
49
 
51
- If you don't need any islands at all, you can also delete the empty
52
- `app/islands/` directory. You can re-scaffold without the demo at any time
53
- using `bunx create-patties@latest <new-name> --blank`.
54
- <!-- /if -->
55
- <!-- if:scaffold=blank -->
56
- ## What you got
50
+ Add more components with `patties add <component>` (e.g. `patties add dialog`),
51
+ preview first with `patties view <component>`, or ask your coding agent see
52
+ the agent section below.
57
53
 
58
- A minimal hello-world Patties app:
54
+ ### Styling
59
55
 
60
- - `app/routes/index.tsx` server-rendered landing page.
61
- - `app/server.ts` dev entry that wires the router into `Bun.serve`.
62
-
63
- ## Run it
56
+ Patties does not bundle Tailwind for you. `app/styles/app.css` is pre-wired
57
+ (`@import "tailwindcss"` + the token mapping). Compile it with the Tailwind v4
58
+ CLI and include the output from your page `<head>`:
64
59
 
65
60
  ```sh
66
- bun install # if you used --no-install
67
- bunx patties dev # → http://localhost:3000
61
+ bunx @tailwindcss/cli -i app/styles/app.css -o app/styles/out.css --watch
68
62
  ```
69
-
70
- ## Add your first interactive feature
71
-
72
- Create `app/islands/` and drop in a component that uses `useState` or
73
- `useEffect`. Import it from a route file under `app/routes/` and wrap the
74
- use site in `<Island name="MyIsland">…</Island>` (from `patties/render`)
75
- so the SSR markers are emitted and the client runtime hydrates it.
76
63
  <!-- /if -->
77
64
 
78
65
  ## Project layout
79
66
 
67
+ <!-- if:monorepo=yes -->
68
+ ```
69
+ apps/
70
+ {{app_name}}/
71
+ app/
72
+ routes/ # filesystem-routed pages and API handlers
73
+ islands/ # interactive client components
74
+ server.ts # dev entry — wires the router into Bun.serve
75
+ patties.config.ts
76
+ package.json
77
+ packages/ # shared workspace packages
78
+ package.json # Bun workspace root
79
+ ```
80
+
81
+ This is a Bun workspace. Run app scripts with `bun --filter <app> <script>`
82
+ (e.g. `bun --filter {{app_name}} build`) or from inside `apps/{{app_name}}/`.
83
+ Add more apps under `apps/` and shared code under `packages/`.
84
+ <!-- /if -->
85
+ <!-- if:monorepo=no -->
80
86
  ```
81
87
  app/
82
88
  routes/ # filesystem-routed pages and API handlers
@@ -86,19 +92,28 @@ patties.config.ts
86
92
  package.json
87
93
  tsconfig.json
88
94
  ```
95
+ <!-- /if -->
89
96
 
90
97
  ## Build for production
91
98
 
92
99
  ```sh
93
- bun run build
100
+ <!-- if:monorepo=yes -->bun --filter {{app_name}} run build<!-- /if -->
101
+ <!-- if:monorepo=no -->bun run build<!-- /if -->
94
102
  ```
95
103
 
96
- Build artifacts land in `.patties/`. Run the server bundle with:
104
+ Build artifacts land in `.patties/`. Run the server bundle with
105
+ `bun .patties/server/server-entry.js`.
106
+
107
+ <!-- if:target=container -->
108
+ ## Deploy — container
109
+
110
+ A `Dockerfile` (+ `.dockerignore`) is included for the `bun` runtime:
97
111
 
98
112
  ```sh
99
- bun .patties/server/server-entry.js
113
+ docker build -t {{name}} .
114
+ docker run -p 3000:3000 {{name}}
100
115
  ```
101
-
116
+ <!-- /if -->
102
117
  <!-- if:deploy=cloudflare -->
103
118
  ## Deploy
104
119
 
@@ -123,33 +138,32 @@ This project is configured for **Deno Deploy**. See the
123
138
  This project is configured for **Netlify Edge**. See the
124
139
  `@patties/deploy-netlify` plugin docs for the next steps.
125
140
  <!-- /if -->
126
- <!-- if:deploy=bun -->
127
- ## Deploy
128
-
129
- This project ships as a standalone Bun bundle. Run `bun run build` then
130
- deploy the contents of `.patties/` to any host that runs Bun.
131
- <!-- /if -->
132
141
 
133
142
  <!-- if:agent=claude -->
134
143
  ## Claude Code is set up
135
144
 
136
- `CLAUDE.md` at the project root describes the framework conventions for Claude
137
- Code. `.claude/settings.json` allow-lists the commands you'll typically need
138
- (`bun`, `bunx`, `patties`). `.claude/hooks/biome-check.sh` runs Biome after
139
- every edit if you install it (`bun add -d @biomejs/biome`).
145
+ `CLAUDE.md` describes the framework conventions. The `/patties` skill
146
+ (`.claude/skills/patties/`) adds components and scaffolds feature patterns
147
+ (auth, CRM, task board, dashboard, …). For a guided first scaffold, open a new
148
+ terminal and run:
140
149
 
141
- Launch a session with `claude` in this directory.
150
+ ```sh
151
+ claude --permission-mode plan "/patties-init"
152
+ ```
142
153
  <!-- /if -->
143
154
  <!-- if:agent=codex -->
144
155
  ## Codex is set up
145
156
 
146
- `AGENTS.md` at the project root describes the framework conventions for
147
- Codex CLI and other AGENTS.md-aware tools. Patties regenerates the
148
- inventory section between `<!-- patties:manifest-start -->` and
149
- `<!-- patties:manifest-end -->` on every `patties dev/build` — everything
150
- outside those markers is yours to edit.
157
+ `AGENTS.md` describes the framework conventions and links
158
+ `.codex/rules/patties-patterns.md`, which tells Codex how to add components and
159
+ scaffold feature patterns. Open Codex and ask it to scaffold a pattern.
160
+ <!-- /if -->
161
+ <!-- if:agent=none -->
162
+ ## No agent
151
163
 
152
- Launch a session with `codex` in this directory.
164
+ Add UI components with `patties add <component>`. Feature patterns
165
+ (auth, CRM, dashboard, …) are agent-driven — scaffold a project with
166
+ `--agent claude` or `--agent codex` to get the `/patties` skill.
153
167
  <!-- /if -->
154
168
 
155
169
  ## Learn more
@@ -0,0 +1,8 @@
1
+ import type { PattiesContext } from "patties/middleware";
2
+
3
+ // API routes live under app/routes/api/ and export named HTTP methods
4
+ // (GET/POST/…) that return a standard Response. `ctx.json()` is the thin
5
+ // PattiesContext helper for JSON replies.
6
+ export function GET(_req: Request, ctx: PattiesContext): Response {
7
+ return ctx.json({ ok: true });
8
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ isValidElement,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ } from "react";
8
+
9
+ interface SlotProps {
10
+ children?: ReactNode;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export function Slot({
15
+ children,
16
+ ...slotProps
17
+ }: SlotProps): ReactElement | null {
18
+ const child = Children.only(children);
19
+ if (!isValidElement(child)) return null;
20
+ const childEl = child as ReactElement<Record<string, unknown>>;
21
+ return cloneElement(childEl, mergeProps(slotProps, childEl.props));
22
+ }
23
+
24
+ function mergeProps(
25
+ parent: Record<string, unknown>,
26
+ child: Record<string, unknown>,
27
+ ): Record<string, unknown> {
28
+ const merged: Record<string, unknown> = { ...parent };
29
+ for (const key of Object.keys(child)) {
30
+ const parentVal = parent[key];
31
+ const childVal = child[key];
32
+ if (
33
+ key.startsWith("on") &&
34
+ typeof parentVal === "function" &&
35
+ typeof childVal === "function"
36
+ ) {
37
+ merged[key] = (...args: unknown[]) => {
38
+ (childVal as (...a: unknown[]) => unknown)(...args);
39
+ (parentVal as (...a: unknown[]) => unknown)(...args);
40
+ };
41
+ } else if (key === "className") {
42
+ merged[key] = [parentVal, childVal].filter(Boolean).join(" ");
43
+ } else if (key === "style") {
44
+ merged[key] = { ...(parentVal as object), ...(childVal as object) };
45
+ } else {
46
+ merged[key] = childVal;
47
+ }
48
+ }
49
+ return merged;
50
+ }
@@ -0,0 +1 @@
1
+ export { cva, type VariantProps } from "class-variance-authority";
@@ -0,0 +1,60 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cn } from "./_internal/cn.ts";
3
+ import { Slot } from "./_internal/slot.ts";
4
+ import { cva, type VariantProps } from "./_internal/variants.ts";
5
+
6
+ export const island = "subtree" as const;
7
+
8
+ export const buttonVariants = cva(
9
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default:
14
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
15
+ destructive:
16
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20",
17
+ outline:
18
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
19
+ secondary:
20
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
21
+ ghost: "hover:bg-accent hover:text-accent-foreground",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: { variant: "default", size: "default" },
32
+ },
33
+ );
34
+
35
+ export type ButtonVariant = NonNullable<
36
+ VariantProps<typeof buttonVariants>["variant"]
37
+ >;
38
+ export type ButtonSize = NonNullable<
39
+ VariantProps<typeof buttonVariants>["size"]
40
+ >;
41
+
42
+ type ButtonProps = ComponentProps<"button"> &
43
+ VariantProps<typeof buttonVariants> & { asChild?: boolean };
44
+
45
+ export function Button({
46
+ className,
47
+ variant,
48
+ size,
49
+ asChild = false,
50
+ ...props
51
+ }: ButtonProps) {
52
+ const Comp = asChild ? Slot : "button";
53
+ return (
54
+ <Comp
55
+ data-slot="button"
56
+ className={cn(buttonVariants({ variant, size }), className)}
57
+ {...props}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,92 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cn } from "./_internal/cn.ts";
3
+ import { Slot } from "./_internal/slot.ts";
4
+
5
+ export const island = false as const;
6
+
7
+ export function Card({ className, ...props }: ComponentProps<"div">) {
8
+ return (
9
+ <div
10
+ data-slot="card"
11
+ className={cn(
12
+ "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ export function CardHeader({ className, ...props }: ComponentProps<"div">) {
21
+ return (
22
+ <div
23
+ data-slot="card-header"
24
+ className={cn(
25
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ export function CardTitle({
34
+ className,
35
+ asChild = false,
36
+ ...props
37
+ }: ComponentProps<"div"> & { asChild?: boolean }) {
38
+ const Comp = asChild ? Slot : "div";
39
+ return (
40
+ <Comp
41
+ data-slot="card-title"
42
+ className={cn("font-semibold leading-none", className)}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ export function CardDescription({
49
+ className,
50
+ ...props
51
+ }: ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-description"
55
+ className={cn("text-muted-foreground text-sm", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ export function CardAction({ className, ...props }: ComponentProps<"div">) {
62
+ return (
63
+ <div
64
+ data-slot="card-action"
65
+ className={cn(
66
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ export function CardContent({ className, ...props }: ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-content"
78
+ className={cn("px-6", className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export function CardFooter({ className, ...props }: ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="card-footer"
88
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "../components/ui/button.tsx";
5
+ import { Input } from "../components/ui/input.tsx";
6
+
7
+ interface Todo {
8
+ id: number;
9
+ text: string;
10
+ done: boolean;
11
+ }
12
+
13
+ export default function TodoApp(): JSX.Element {
14
+ const [todos, setTodos] = useState<Todo[]>([
15
+ { id: 1, text: "edit app/routes/index.tsx", done: false },
16
+ { id: 2, text: "edit app/islands/TodoApp.tsx", done: false },
17
+ { id: 3, text: "add a component with `patties add`", done: false },
18
+ ]);
19
+ const [draft, setDraft] = useState("");
20
+
21
+ function add(): void {
22
+ const text = draft.trim();
23
+ if (!text) return;
24
+ setTodos([...todos, { id: Date.now(), text, done: false }]);
25
+ setDraft("");
26
+ }
27
+
28
+ function toggle(id: number): void {
29
+ setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
30
+ }
31
+
32
+ function remove(id: number): void {
33
+ setTodos(todos.filter((t) => t.id !== id));
34
+ }
35
+
36
+ return (
37
+ <section aria-label="Todo demo" className="flex flex-col gap-3">
38
+ <form
39
+ className="flex gap-2"
40
+ onSubmit={(e) => {
41
+ e.preventDefault();
42
+ add();
43
+ }}
44
+ >
45
+ <Input
46
+ type="text"
47
+ value={draft}
48
+ onChange={(e) => setDraft(e.target.value)}
49
+ placeholder="What needs doing?"
50
+ aria-label="New todo"
51
+ />
52
+ <Button type="submit">Add</Button>
53
+ </form>
54
+ <ul className="flex flex-col gap-2">
55
+ {todos.map((t) => (
56
+ <li key={t.id} className="flex items-center gap-2">
57
+ <label className="flex flex-1 items-center gap-2">
58
+ <input
59
+ type="checkbox"
60
+ checked={t.done}
61
+ onChange={() => toggle(t.id)}
62
+ />
63
+ <span
64
+ className={t.done ? "text-muted-foreground line-through" : ""}
65
+ >
66
+ {t.text}
67
+ </span>
68
+ </label>
69
+ <Button
70
+ type="button"
71
+ variant="ghost"
72
+ size="sm"
73
+ onClick={() => remove(t.id)}
74
+ aria-label="Remove"
75
+ >
76
+ ×
77
+ </Button>
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ <p className="text-muted-foreground text-sm">
82
+ {todos.filter((t) => !t.done).length} of {todos.length} remaining
83
+ </p>
84
+ </section>
85
+ );
86
+ }
@@ -0,0 +1,41 @@
1
+ import { Island } from "patties/render";
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "../components/ui/card.tsx";
9
+ import TodoApp from "../islands/TodoApp.tsx";
10
+
11
+ export const meta = {
12
+ title: "Welcome to Patties",
13
+ description: "A Bun-native full-stack meta-framework.",
14
+ };
15
+
16
+ export default function Index(): JSX.Element {
17
+ return (
18
+ <main className="mx-auto max-w-2xl p-8">
19
+ <Card>
20
+ <CardHeader>
21
+ <CardTitle>Welcome to Patties</CardTitle>
22
+ <CardDescription>
23
+ This page is server-rendered with Patties UI components. The list
24
+ below is a client island — <code>app/islands/TodoApp.tsx</code> —
25
+ hydrated in the browser.
26
+ </CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <Island name="TodoApp">
30
+ <TodoApp />
31
+ </Island>
32
+ </CardContent>
33
+ </Card>
34
+ <p className="mt-4 text-muted-foreground text-sm">
35
+ Components live in <code>app/components/ui/</code>. Add more with
36
+ <code>patties add</code>, or ask your coding agent via{" "}
37
+ <code>/patties</code>.
38
+ </p>
39
+ </main>
40
+ );
41
+ }