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.
- package/CHANGELOG.md +30 -0
- package/package.json +4 -2
- package/src/index.ts +345 -167
- package/src/prompts.ts +62 -17
- package/src/readme.ts +12 -5
- package/src/ui.ts +93 -0
- package/templates/_backend/app/routes/api/todos.ts +30 -0
- package/templates/_claude/.claude/commands/patties-init.md +33 -0
- package/templates/_claude/.claude/skills/patties/SKILL.md +104 -0
- package/templates/_codex/.codex/rules/patties-patterns.md +105 -0
- package/templates/_codex/AGENTS.md +1 -0
- package/templates/_container/.dockerignore +7 -0
- package/templates/_container/Dockerfile +27 -0
- package/templates/_monorepo/packages/README.md +18 -0
- package/templates/_shared/patties-patterns.md +105 -0
- package/templates/default/README-template.md +85 -71
- package/templates/default/app/routes/api/health.ts +8 -0
- package/templates/ui-starter/_internal/cn.ts +6 -0
- package/templates/ui-starter/_internal/slot.ts +50 -0
- package/templates/ui-starter/_internal/variants.ts +1 -0
- package/templates/ui-starter/button.tsx +60 -0
- package/templates/ui-starter/card.tsx +92 -0
- package/templates/ui-starter/demo/TodoApp.tsx +86 -0
- package/templates/ui-starter/demo/index.tsx +41 -0
- package/templates/ui-starter/input.tsx +20 -0
- package/templates/ui-starter/label.tsx +18 -0
- package/templates/ui-starter/themes/neutral/tokens.css +46 -0
- package/templates/ui-starter/themes/slate/tokens.css +46 -0
- package/templates/ui-starter/themes/stone/tokens.css +46 -0
- package/templates/ui-starter/themes/zinc/tokens.css +46 -0
- 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:
|
|
6
|
+
<!-- if:type=fullstack -->
|
|
7
7
|
## What you got
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A full-stack Patties app:
|
|
10
10
|
|
|
11
|
-
- `app/routes/index.tsx` — server-rendered page that mounts
|
|
12
|
-
- `app/islands/TodoApp.tsx` — `useState`-based todo list, hydrated in the
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
app, delete it:
|
|
35
|
+
## Run it
|
|
37
36
|
|
|
38
37
|
```sh
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
43
|
+
<!-- if:ui=yes -->
|
|
44
|
+
## Patties UI
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
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
|
-
|
|
54
|
+
### Styling
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
137
|
-
|
|
138
|
-
(
|
|
139
|
-
|
|
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
|
-
|
|
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`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|