patties.config.ts — optional `ui` block
shadcn centralizes UI scaffolding config in components.json. patties replaces
Purpose
shadcn centralizes UI scaffolding config in components.json. patties replaces that with fixed conventions (app/components/ui/, _internal/, app/styles/tokens.css, CSS-vars always on, lucide-react, no RSC toggle). The parity matrix records that stance as a deliberate divergence — less surface, no config drift — but flags one real cost: shadcn's --path flexibility, the one hard-coded convention users miss immediately (matrix §3 "Takeaway", §2 --path).
This spec adds a narrow, optional ui block to patties.config.ts so that the two paths that genuinely vary between projects — where component source is stamped and where tokens live — can be redirected, without reintroducing a full components.json. Everything not listed here stays convention, not config.
Non-goals (kept as convention, not config)
These remain fixed; we are not porting components.json wholesale:
aliases— import paths are derived fromcomponentsDir, not separately configurable. (matrix §3: divergent)cssVariableson/off,style,baseColor,rsc,tsx— patties is always CSS-vars-on, single-style, SSR-island,.tsx. (matrix §3: divergent)iconLibrary—lucide-reactis fixed; userland swaps icons by editing stamped source. (matrix §3: n/a)
If a future RFC wants any of these configurable, it extends this block — but the default of "no config needed" must survive.
Schema
The ui block is fully optional. Absent config behaves exactly as today.
// src/config/schema.ts (Zod)
const UiConfig = z
.object({
// Where `patties add` stamps component source. Default: "app/components/ui".
componentsDir: z.string().default("app/components/ui"),
// Where shared helpers (_internal/cn, slot, variants) are stamped.
// Default: "<componentsDir>/_internal".
internalDir: z.string().optional(),
// Where the token CSS is merged. Default: "app/styles/tokens.css".
tokensFile: z.string().default("app/styles/tokens.css"),
})
.strict()
.optional();
- All paths are resolved relative to the project root (the dir holding
patties.config.ts), never absolute, never..-escaping the project — the loader rejects paths that resolve outside the project root (EXIT.USAGE). internalDirdefaults to<componentsDir>/_internalso redirectingcomponentsDirkeeps helpers co-located without a second setting.
Resolution & precedence
patties add resolves effective paths in this order (first wins):
--path <dir>CLI flag (per [[cli/14-add-path]]) — affectscomponentsDironly, for one invocation.config.ui.componentsDir/internalDir/tokensFile.- The hard-coded convention defaults above.
The add pipeline (stamper.ts, internal.ts, tokens.ts) currently hard-codes these destinations; this spec threads a resolved UiPaths object through them instead of string literals.
Manifest / agent-md
No manifest change. The ui block is read by the CLI add pipeline only; it is not part of route/island discovery and does not appear in the generated CLAUDE.md manifest section.
Acceptance criteria
- With no
uiblock,patties add buttonstamps toapp/components/ui/button.tsxandapp/components/ui/_internal/cn.ts, tokens toapp/styles/tokens.css— byte-identical to today. - With
ui: { componentsDir: "src/ui" },patties add buttonstamps tosrc/ui/button.tsxand helpers tosrc/ui/_internal/. - With
ui: { tokensFile: "styles/theme.css" }, the token merge targets that file and is idempotent on re-run. - A
componentsDirthat resolves outside the project root exitsEXIT.USAGEwith a one-line message; no files are written. config.uiis.strict()— an unknown key (e.g.aliases) is a config error, pointing the user at the convention it replaces.bun run typecheckpasses; the config Zod schema infers the optionaluishape.