@dowel-ui/registry 0.4.0 → 0.7.0
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/dist/build.d.ts +32 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +528 -78
- package/dist/build.js.map +1 -1
- package/dist/generate.d.ts +71 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +324 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +184 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +648 -2
- package/dist/index.js.map +1 -0
- package/dist/{schema-ByqIpT47.js → schema-BbyNjH6Q.js} +25 -5
- package/dist/schema-BbyNjH6Q.js.map +1 -0
- package/dist/{schema-Cc32v3R-.d.ts → schema-BmYeKe9e.d.ts} +36 -2
- package/dist/schema-BmYeKe9e.d.ts.map +1 -0
- package/package.json +12 -4
- package/src/agent-docs.ts +544 -0
- package/src/build.ts +66 -4
- package/src/custom.ts +312 -0
- package/src/generate.ts +500 -0
- package/src/index.ts +41 -0
- package/src/schema.ts +23 -0
- package/dist/schema-ByqIpT47.js.map +0 -1
- package/dist/schema-Cc32v3R-.d.ts.map +0 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import type { RegistryIndex, RegistryIndexEntry, RegistryItem } from "./schema";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Documentation written for coding agents rather than for people.
|
|
5
|
+
*
|
|
6
|
+
* This lives beside the registry rather than in the CLI or the docs site
|
|
7
|
+
* because all three need to emit the same text: the CLI writes it into a
|
|
8
|
+
* consumer's repository, the site serves it at /llms.txt, and the MCP server
|
|
9
|
+
* answers with it. Three hand-maintained copies would disagree within a
|
|
10
|
+
* release, and an agent acting on a stale catalogue writes code that does not
|
|
11
|
+
* compile.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is derived from the registry index. Nothing is a hardcoded
|
|
14
|
+
* list of component names — that is the failure mode this replaces.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface AgentDocsContext {
|
|
18
|
+
index: RegistryIndex;
|
|
19
|
+
/**
|
|
20
|
+
* Full registry items, when the caller has them.
|
|
21
|
+
*
|
|
22
|
+
* The index carries no accessibility notes, so callers that can afford to
|
|
23
|
+
* fetch every item (the docs build, the MCP server) get richer output than
|
|
24
|
+
* ones that cannot (the CLI, which would otherwise make 81 requests).
|
|
25
|
+
*/
|
|
26
|
+
items?: RegistryItem[];
|
|
27
|
+
/** Base URL the CLI installs from. */
|
|
28
|
+
registryUrl: string;
|
|
29
|
+
/** Base URL of the documentation site, no trailing slash. */
|
|
30
|
+
docsUrl: string;
|
|
31
|
+
/** npm package name of the CLI, e.g. `@dowel-ui/cli`. */
|
|
32
|
+
cliPackage: string;
|
|
33
|
+
libraryName: string;
|
|
34
|
+
/** Registry names already present in the project, if known. */
|
|
35
|
+
installed?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* What components are imported from in this project.
|
|
38
|
+
*
|
|
39
|
+
* Source-first installs resolve to the project's own alias; the published
|
|
40
|
+
* package is a separate, supported way to consume the same components. An
|
|
41
|
+
* agent told the wrong one writes imports that do not resolve.
|
|
42
|
+
*/
|
|
43
|
+
importFrom: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
47
|
+
foundation: "Foundation",
|
|
48
|
+
form: "Forms",
|
|
49
|
+
overlay: "Overlays",
|
|
50
|
+
navigation: "Navigation",
|
|
51
|
+
display: "Display",
|
|
52
|
+
data: "Data",
|
|
53
|
+
feedback: "Feedback",
|
|
54
|
+
layout: "Layout",
|
|
55
|
+
ai: "AI",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const CATEGORY_ORDER = [
|
|
59
|
+
"foundation",
|
|
60
|
+
"form",
|
|
61
|
+
"overlay",
|
|
62
|
+
"navigation",
|
|
63
|
+
"display",
|
|
64
|
+
"data",
|
|
65
|
+
"feedback",
|
|
66
|
+
"layout",
|
|
67
|
+
"ai",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function label(category: string): string {
|
|
71
|
+
return CATEGORY_LABELS[category] ?? category;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Ordered by curation where curated, alphabetical for anything new. */
|
|
75
|
+
function categoriesOf(entries: RegistryIndexEntry[]): string[] {
|
|
76
|
+
const present = new Set(entries.map((entry) => entry.category));
|
|
77
|
+
const known = CATEGORY_ORDER.filter((category) => present.has(category));
|
|
78
|
+
const rest = [...present].filter((category) => !CATEGORY_ORDER.includes(category)).sort();
|
|
79
|
+
return [...known, ...rest];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function byType(index: RegistryIndex, type: RegistryIndexEntry["type"]): RegistryIndexEntry[] {
|
|
83
|
+
return index.items
|
|
84
|
+
.filter((entry) => entry.type === type)
|
|
85
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function components(index: RegistryIndex) {
|
|
89
|
+
return byType(index, "registry:ui");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function blocks(index: RegistryIndex) {
|
|
93
|
+
return byType(index, "registry:block");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Accessibility rules that differ from what a model has seen elsewhere.
|
|
98
|
+
*
|
|
99
|
+
* An agent trained on every other React library will reach for `disabled` on a
|
|
100
|
+
* loading button and a live region on every alert. Stating only the deltas is
|
|
101
|
+
* deliberate: a general accessibility lecture is ignored, a short list of
|
|
102
|
+
* "here this is different" is followed.
|
|
103
|
+
*/
|
|
104
|
+
const ACCESSIBILITY_DELTAS = [
|
|
105
|
+
"A loading `Button` uses `aria-disabled` + `aria-busy` and guards its own click handler. Never add `disabled` to it — disabling a control mid-action strands keyboard focus.",
|
|
106
|
+
'`Alert` is not a live region by default. One that exists on first paint announces for no reason. Opt in with `live="polite"` or `live="assertive"` only when the alert appears in response to something.',
|
|
107
|
+
"`Separator`, `Skeleton` and `Spinner` are decorative and stay out of the accessibility tree unless given a label. Do not add `role` or `aria-label` to them by reflex.",
|
|
108
|
+
'`PopoverContent` carries `role="dialog"` and warns in development without an accessible name. Always give it `aria-label` or `aria-labelledby`.',
|
|
109
|
+
"Never use colour as the only signal. The `monochrome` preset exists as a standing check on exactly this — if a state is unreadable under it, the component is wrong.",
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const TOKEN_RULES = [
|
|
113
|
+
"Use semantic tokens (`bg-background`, `text-foreground`, `border-border`, `ring-ring`, `bg-primary`, `text-muted-foreground`). Never raw hex, and never Tailwind's own palette (`bg-slate-900`, `text-gray-500`) — those do not follow the theme and break every preset and dark mode.",
|
|
114
|
+
"Spacing, radius and type come from the scale. `rounded-md` and `rounded-lg` re-proportion with `--radius-scale`; an arbitrary `rounded-[7px]` does not.",
|
|
115
|
+
"Durations derive from `--motion-scale`. Do not hardcode transition timings.",
|
|
116
|
+
"Compose class names with `cn()` from the project's utils, so consumer overrides win over defaults.",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export function conventionsDoc(context: AgentDocsContext): string {
|
|
120
|
+
const { libraryName, cliPackage, importFrom, docsUrl } = context;
|
|
121
|
+
|
|
122
|
+
return `# ${libraryName} — conventions
|
|
123
|
+
|
|
124
|
+
Rules for writing code in this project. ${libraryName} is **source-first**: its
|
|
125
|
+
components are files in this repository, not a dependency you can reason about
|
|
126
|
+
from its README. They are yours to edit, and edits are preserved across updates.
|
|
127
|
+
|
|
128
|
+
## The rule that matters most
|
|
129
|
+
|
|
130
|
+
**Do not hand-write a component that ${libraryName} already has.** Check the
|
|
131
|
+
catalogue in \`components.md\` first. Writing a second Button — with different
|
|
132
|
+
focus rings, different disabled semantics, different tokens — is the single
|
|
133
|
+
most common and most damaging thing to do here.
|
|
134
|
+
|
|
135
|
+
## Adding a component
|
|
136
|
+
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
npx ${cliPackage} add <name>
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
This writes the source into the project and installs whatever it depends on.
|
|
142
|
+
\`add\` is safe to re-run: an untouched file is left alone, an edited one is
|
|
143
|
+
never overwritten without \`--overwrite\`.
|
|
144
|
+
|
|
145
|
+
Do not \`npm install\` a component. Do not copy source out of the documentation
|
|
146
|
+
by hand — the CLI resolves the dependency graph and rewrites imports to this
|
|
147
|
+
project's path alias, and doing it manually gets both wrong.
|
|
148
|
+
|
|
149
|
+
## Importing
|
|
150
|
+
|
|
151
|
+
\`\`\`tsx
|
|
152
|
+
import { Button, Card, CardContent } from "${importFrom}";
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
## Styling
|
|
156
|
+
|
|
157
|
+
${TOKEN_RULES.map((rule) => `- ${rule}`).join("\n")}
|
|
158
|
+
|
|
159
|
+
## Accessibility
|
|
160
|
+
|
|
161
|
+
Targeted at WCAG 2.2 AA, verified with axe per component. Where ${libraryName}
|
|
162
|
+
differs from what you have seen in other libraries:
|
|
163
|
+
|
|
164
|
+
${ACCESSIBILITY_DELTAS.map((rule) => `- ${rule}`).join("\n")}
|
|
165
|
+
|
|
166
|
+
## Before you build a page
|
|
167
|
+
|
|
168
|
+
Check \`components.md\` for a **block** that already covers it. A block is a
|
|
169
|
+
whole section — a login form, a settings page, a chat surface — and installing
|
|
170
|
+
one brings every component it is assembled from. Building a dashboard out of
|
|
171
|
+
individual primitives when \`add dashboard\` exists is wasted work.
|
|
172
|
+
|
|
173
|
+
## Reference
|
|
174
|
+
|
|
175
|
+
- Documentation: ${docsUrl}
|
|
176
|
+
- Full text for models: ${docsUrl}/llms-full.txt
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function componentsDoc(context: AgentDocsContext): string {
|
|
181
|
+
const { index, libraryName, cliPackage, installed } = context;
|
|
182
|
+
const have = new Set(installed ?? []);
|
|
183
|
+
const known = installed !== undefined;
|
|
184
|
+
const ui = components(index);
|
|
185
|
+
const blk = blocks(index);
|
|
186
|
+
|
|
187
|
+
const lines: string[] = [
|
|
188
|
+
`# ${libraryName} — catalogue`,
|
|
189
|
+
"",
|
|
190
|
+
`${String(ui.length)} components and ${String(blk.length)} blocks, generated from ` +
|
|
191
|
+
`\`${index.generatedFrom}\`. This is the complete list — anything not here does not exist.`,
|
|
192
|
+
"",
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
if (known) {
|
|
196
|
+
lines.push(
|
|
197
|
+
"`✓` marks what is already installed in this project. Everything else needs",
|
|
198
|
+
`\`npx ${cliPackage} add <name>\` before it can be imported.`,
|
|
199
|
+
"",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const mark = (entry: RegistryIndexEntry) =>
|
|
204
|
+
known ? (have.has(entry.name) ? "✓ " : " ") : "";
|
|
205
|
+
|
|
206
|
+
for (const category of categoriesOf(ui)) {
|
|
207
|
+
lines.push(`## ${label(category)}`, "");
|
|
208
|
+
for (const entry of ui.filter((item) => item.category === category)) {
|
|
209
|
+
const status = entry.status === "stable" ? "" : ` _(${entry.status})_`;
|
|
210
|
+
lines.push(`- ${mark(entry)}**${entry.name}** — ${entry.description}${status}`);
|
|
211
|
+
}
|
|
212
|
+
lines.push("");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lines.push(
|
|
216
|
+
"## Blocks",
|
|
217
|
+
"",
|
|
218
|
+
"Whole sections. Installing one installs every component it is built from.",
|
|
219
|
+
"",
|
|
220
|
+
);
|
|
221
|
+
for (const entry of blk) {
|
|
222
|
+
const deps = entry.registryDependencies.length;
|
|
223
|
+
const resolves = deps > 0 ? ` _(resolves ${String(deps)} components)_` : "";
|
|
224
|
+
lines.push(`- ${mark(entry)}**${entry.name}** — ${entry.description}${resolves}`);
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function aiDoc(context: AgentDocsContext): string {
|
|
232
|
+
const { index, libraryName, cliPackage } = context;
|
|
233
|
+
const ai = components(index).filter((entry) => entry.category === "ai");
|
|
234
|
+
|
|
235
|
+
return `# ${libraryName} — AI components
|
|
236
|
+
|
|
237
|
+
${String(ai.length)} surfaces for AI features. Reach for these before building
|
|
238
|
+
anything custom for a model-facing interface.
|
|
239
|
+
|
|
240
|
+
Most component sets ship a chat transcript and stop. Real AI features are
|
|
241
|
+
extraction, enrichment, autofill and agents that *change things* — so the parts
|
|
242
|
+
that matter are the ones around the transcript, not the transcript itself.
|
|
243
|
+
|
|
244
|
+
${ai.map((entry) => `- **${entry.name}** — ${entry.description}`).join("\n")}
|
|
245
|
+
|
|
246
|
+
## Choosing between them
|
|
247
|
+
|
|
248
|
+
- Rendering a conversation → \`ai-conversation\` with \`ai-message\` and \`ai-response\`.
|
|
249
|
+
- The composer → \`ai-prompt-input\`, with \`ai-model-selector\` if the model is switchable.
|
|
250
|
+
- A tool the model called → \`ai-tool\`. Its arguments and result belong there, not in prose.
|
|
251
|
+
- Asking permission *before* a tool runs → \`ai-approval-request\`.
|
|
252
|
+
- Reporting what it did *after* → \`ai-action-ledger\`, which is also where reversibility belongs. A deletion can be undone, a refund can only be offset, a sent email cannot be taken back — the ledger is where that distinction is shown.
|
|
253
|
+
- An object streaming in field by field → \`ai-structured-output\`, which reserves layout up front so nothing jumps.
|
|
254
|
+
- Ghost text in a real textarea → \`ai-inline-completion\`. Escape always returns Tab to focus management, so a keyboard user is never trapped.
|
|
255
|
+
- A value the model proposes for a form field → \`ai-suggested-value\`.
|
|
256
|
+
- Reviewing what was pulled out of a document → \`ai-extraction-review\`.
|
|
257
|
+
- Long-running work → \`ai-agent-status\` and \`ai-agent-plan\`.
|
|
258
|
+
- Where an answer came from → \`ai-sources\`. Cost → \`ai-token-usage\`. Chain of thought → \`ai-reasoning\`.
|
|
259
|
+
|
|
260
|
+
## Whole surface at once
|
|
261
|
+
|
|
262
|
+
\`\`\`bash
|
|
263
|
+
npx ${cliPackage} add ai-chat
|
|
264
|
+
\`\`\`
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function themesDoc(context: AgentDocsContext): string {
|
|
269
|
+
const { libraryName } = context;
|
|
270
|
+
|
|
271
|
+
return `# ${libraryName} — theming
|
|
272
|
+
|
|
273
|
+
Tokens are two-tier. **Tier 1** is raw scales: an OKLCH neutral ramp, a radius
|
|
274
|
+
ladder, a 15px-base type scale, elevation, motion. **Tier 2** is semantic
|
|
275
|
+
aliases — \`--primary\`, \`--background\`, \`--border\`, \`--ring\` — and components
|
|
276
|
+
consume Tier 2 *exclusively*.
|
|
277
|
+
|
|
278
|
+
Re-skinning the system means reassigning Tier 2. It never means editing a
|
|
279
|
+
component file. If you find yourself changing a colour inside a component, the
|
|
280
|
+
change belongs in the token layer instead.
|
|
281
|
+
|
|
282
|
+
## Presets
|
|
283
|
+
|
|
284
|
+
\`default\`, \`ocean\`, \`emerald\`, \`violet\`, \`rose\`, \`amber\`, \`monochrome\`.
|
|
285
|
+
|
|
286
|
+
\`\`\`html
|
|
287
|
+
<html data-theme="ocean" class="dark">
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
\`data-theme\` selects the preset; the \`dark\` class selects the mode. They are
|
|
291
|
+
independent — every preset works in both.
|
|
292
|
+
|
|
293
|
+
\`monochrome\` is not only a style. It is a standing check that no component uses
|
|
294
|
+
colour as its only signal, so verify new work under it.
|
|
295
|
+
|
|
296
|
+
## Two properties that re-proportion everything
|
|
297
|
+
|
|
298
|
+
- \`--radius-scale\` — one multiplier behind every corner in the system. \`1\` is the designed default, \`0\` is fully square.
|
|
299
|
+
- \`--motion-scale\` — one multiplier every duration derives from. Under \`prefers-reduced-motion\` it collapses, but indicators that report ongoing state are *slowed* rather than stopped via \`--motion-scale-indicator\`, because a frozen spinner reads as a hung application.
|
|
300
|
+
|
|
301
|
+
## Contrast
|
|
302
|
+
|
|
303
|
+
All semantic pairs are verified against WCAG 2.2 AA across both modes and every
|
|
304
|
+
preset, in CI. A new token pair has to pass the same check — do not introduce
|
|
305
|
+
one without running \`audit:contrast\`.
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Frontmatter-carrying skill file for Claude Code. */
|
|
310
|
+
export function skillDoc(context: AgentDocsContext): string {
|
|
311
|
+
const { index, libraryName, cliPackage, importFrom } = context;
|
|
312
|
+
const ui = components(index);
|
|
313
|
+
const blk = blocks(index);
|
|
314
|
+
const names = ui.map((entry) => entry.name).join(", ");
|
|
315
|
+
|
|
316
|
+
return `---
|
|
317
|
+
name: ${libraryName.toLowerCase()}-ui
|
|
318
|
+
description: >-
|
|
319
|
+
Build React interfaces with ${libraryName}, the source-first component system
|
|
320
|
+
installed in this project. Use whenever writing or editing React UI here —
|
|
321
|
+
any button, form, dialog, table, dashboard or AI surface. Covers the
|
|
322
|
+
${String(ui.length)}-component catalogue, the ${String(blk.length)} blocks, design tokens, theming
|
|
323
|
+
and the accessibility rules that differ from other libraries.
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
# ${libraryName}
|
|
327
|
+
|
|
328
|
+
Source-first React components. They are **files in this repository**, not a
|
|
329
|
+
dependency — installed with a CLI, then owned and edited like any other code.
|
|
330
|
+
|
|
331
|
+
## Do this first
|
|
332
|
+
|
|
333
|
+
Never hand-write a component ${libraryName} already has. The catalogue:
|
|
334
|
+
|
|
335
|
+
${names}
|
|
336
|
+
|
|
337
|
+
Blocks (whole sections, each resolving its own components):
|
|
338
|
+
${blk.map((entry) => entry.name).join(", ")}
|
|
339
|
+
|
|
340
|
+
## Adding one
|
|
341
|
+
|
|
342
|
+
\`\`\`bash
|
|
343
|
+
npx ${cliPackage} add button card dialog
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
Resolves dependencies, installs npm packages, rewrites imports to this
|
|
347
|
+
project's alias. Safe to re-run — it will not overwrite a file you have edited
|
|
348
|
+
without \`--overwrite\`.
|
|
349
|
+
|
|
350
|
+
## Importing
|
|
351
|
+
|
|
352
|
+
\`\`\`tsx
|
|
353
|
+
import { Button, Card, CardContent } from "${importFrom}";
|
|
354
|
+
\`\`\`
|
|
355
|
+
|
|
356
|
+
## Styling rules
|
|
357
|
+
|
|
358
|
+
${TOKEN_RULES.map((rule) => `- ${rule}`).join("\n")}
|
|
359
|
+
|
|
360
|
+
## Accessibility rules that differ here
|
|
361
|
+
|
|
362
|
+
${ACCESSIBILITY_DELTAS.map((rule) => `- ${rule}`).join("\n")}
|
|
363
|
+
|
|
364
|
+
## Reference files
|
|
365
|
+
|
|
366
|
+
- \`.dowel/components.md\` — the full catalogue with descriptions
|
|
367
|
+
- \`.dowel/ai.md\` — the AI components and when to use each
|
|
368
|
+
- \`.dowel/themes.md\` — tokens, presets, radius and motion scales
|
|
369
|
+
- \`.dowel/conventions.md\` — the rules above, in full
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Cursor project rule (`.cursor/rules/*.mdc`). */
|
|
374
|
+
export function cursorRule(context: AgentDocsContext): string {
|
|
375
|
+
const { index, libraryName, cliPackage, importFrom } = context;
|
|
376
|
+
const ui = components(index);
|
|
377
|
+
|
|
378
|
+
return `---
|
|
379
|
+
description: ${libraryName} component system — use for all React UI in this project
|
|
380
|
+
globs: ["**/*.tsx", "**/*.jsx"]
|
|
381
|
+
alwaysApply: false
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
${libraryName} is source-first: its ${String(ui.length)} components are files in this
|
|
385
|
+
repository. Never hand-write one that already exists.
|
|
386
|
+
|
|
387
|
+
Add: \`npx ${cliPackage} add <name>\`
|
|
388
|
+
Import: \`import { Button } from "${importFrom}"\`
|
|
389
|
+
|
|
390
|
+
Available: ${ui.map((entry) => entry.name).join(", ")}
|
|
391
|
+
|
|
392
|
+
${TOKEN_RULES.map((rule) => `- ${rule}`).join("\n")}
|
|
393
|
+
${ACCESSIBILITY_DELTAS.map((rule) => `- ${rule}`).join("\n")}
|
|
394
|
+
|
|
395
|
+
Full catalogue and reasoning: \`.dowel/\`
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export const AGENTS_MARKER_START = "<!-- dowel:start -->";
|
|
400
|
+
export const AGENTS_MARKER_END = "<!-- dowel:end -->";
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The block written into a project's AGENTS.md.
|
|
404
|
+
*
|
|
405
|
+
* Marker-wrapped rather than written as a whole file: AGENTS.md belongs to the
|
|
406
|
+
* project and usually already says things about the project. Replacing it would
|
|
407
|
+
* destroy that; appending without markers would duplicate the section on every
|
|
408
|
+
* regeneration.
|
|
409
|
+
*/
|
|
410
|
+
export function agentsSection(context: AgentDocsContext): string {
|
|
411
|
+
const { index, libraryName, cliPackage, importFrom } = context;
|
|
412
|
+
const ui = components(index);
|
|
413
|
+
|
|
414
|
+
return `${AGENTS_MARKER_START}
|
|
415
|
+
|
|
416
|
+
## UI components — ${libraryName}
|
|
417
|
+
|
|
418
|
+
This project uses ${libraryName}, a **source-first** component system: its
|
|
419
|
+
${String(ui.length)} components live in this repository as editable files.
|
|
420
|
+
|
|
421
|
+
- **Never hand-write a component that already exists.** The full catalogue is in \`.dowel/components.md\`.
|
|
422
|
+
- Add one with \`npx ${cliPackage} add <name>\` — never \`npm install\`, never copy source by hand.
|
|
423
|
+
- Import from \`${importFrom}\`.
|
|
424
|
+
- Style with semantic tokens only (\`bg-background\`, \`text-muted-foreground\`), never raw hex and never Tailwind's own palette.
|
|
425
|
+
- Building a page? Check \`.dowel/components.md\` for a **block** first.
|
|
426
|
+
- Building an AI feature? \`.dowel/ai.md\` lists surfaces you will not find elsewhere.
|
|
427
|
+
- Accessibility deltas from other libraries are in \`.dowel/conventions.md\`. Read them before adding ARIA by reflex.
|
|
428
|
+
|
|
429
|
+
${AGENTS_MARKER_END}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Replaces the marked block, or appends it if there is none. */
|
|
433
|
+
export function upsertAgentsSection(existing: string, section: string): string {
|
|
434
|
+
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
435
|
+
const end = existing.indexOf(AGENTS_MARKER_END);
|
|
436
|
+
|
|
437
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
438
|
+
return existing.slice(0, start) + section + existing.slice(end + AGENTS_MARKER_END.length);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const base = existing.trimEnd();
|
|
442
|
+
return base.length > 0 ? `${base}\n\n${section}\n` : `${section}\n`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The llms.txt index.
|
|
447
|
+
*
|
|
448
|
+
* Deliberately a map rather than a dump: it names every component and points at
|
|
449
|
+
* the one URL that carries everything, so a model with a small budget can find
|
|
450
|
+
* the right page and one with a large budget can take the lot.
|
|
451
|
+
*/
|
|
452
|
+
export function llmsTxt(context: AgentDocsContext): string {
|
|
453
|
+
const { index, libraryName, docsUrl, cliPackage } = context;
|
|
454
|
+
const ui = components(index);
|
|
455
|
+
const blk = blocks(index);
|
|
456
|
+
|
|
457
|
+
const lines = [
|
|
458
|
+
`# ${libraryName}`,
|
|
459
|
+
"",
|
|
460
|
+
`> Source-first React components for SaaS and AI products. ${String(ui.length)} components ` +
|
|
461
|
+
`and ${String(blk.length)} blocks, installed as code you own rather than imported from a ` +
|
|
462
|
+
`dependency. Built on Tailwind v4 and OKLCH design tokens, targeted at WCAG 2.2 AA.`,
|
|
463
|
+
"",
|
|
464
|
+
`Install: \`npx ${cliPackage} add <name>\` writes the component's source into your project.`,
|
|
465
|
+
"Re-running is safe — files you have edited are never overwritten without `--overwrite`.",
|
|
466
|
+
"",
|
|
467
|
+
`Generated from ${index.generatedFrom}.`,
|
|
468
|
+
"",
|
|
469
|
+
"## Start here",
|
|
470
|
+
"",
|
|
471
|
+
`- [Everything, in one file](${docsUrl}/llms-full.txt): the complete catalogue with descriptions, accessibility notes and conventions`,
|
|
472
|
+
`- [Installation](${docsUrl}/docs/installation)`,
|
|
473
|
+
`- [CLI](${docsUrl}/docs/cli)`,
|
|
474
|
+
`- [Theming](${docsUrl}/docs/themes)`,
|
|
475
|
+
`- [Accessibility](${docsUrl}/docs/accessibility)`,
|
|
476
|
+
"",
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
for (const category of categoriesOf(ui)) {
|
|
480
|
+
lines.push(`## ${label(category)}`, "");
|
|
481
|
+
for (const entry of ui.filter((item) => item.category === category)) {
|
|
482
|
+
lines.push(
|
|
483
|
+
`- [${entry.name}](${docsUrl}/docs/components/${entry.name}): ${entry.description}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
lines.push("");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
lines.push("## Blocks", "");
|
|
490
|
+
for (const entry of blk) {
|
|
491
|
+
lines.push(`- [${entry.name}](${docsUrl}/docs/blocks/${entry.name}): ${entry.description}`);
|
|
492
|
+
}
|
|
493
|
+
lines.push("");
|
|
494
|
+
|
|
495
|
+
return lines.join("\n");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Everything an agent needs, in one request. */
|
|
499
|
+
export function llmsFullTxt(context: AgentDocsContext): string {
|
|
500
|
+
const { index, items, libraryName, docsUrl } = context;
|
|
501
|
+
const detail = new Map((items ?? []).map((item) => [item.name, item]));
|
|
502
|
+
|
|
503
|
+
const parts = [
|
|
504
|
+
conventionsDoc(context),
|
|
505
|
+
componentsDoc({ ...context, installed: undefined }),
|
|
506
|
+
aiDoc(context),
|
|
507
|
+
themesDoc(context),
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
const lines = [
|
|
511
|
+
`# ${libraryName} — full reference`,
|
|
512
|
+
"",
|
|
513
|
+
`Generated from ${index.generatedFrom}. Canonical source: ${docsUrl}`,
|
|
514
|
+
"",
|
|
515
|
+
"---",
|
|
516
|
+
"",
|
|
517
|
+
parts.join("\n---\n\n"),
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
if (detail.size > 0) {
|
|
521
|
+
lines.push("---", "", "# Per-component detail", "");
|
|
522
|
+
|
|
523
|
+
for (const entry of components(index)) {
|
|
524
|
+
const item = detail.get(entry.name);
|
|
525
|
+
lines.push(`## ${entry.title} \`${entry.name}\``, "", entry.description, "");
|
|
526
|
+
lines.push(
|
|
527
|
+
`- Category: ${label(entry.category)} · Status: ${entry.status}`,
|
|
528
|
+
`- Install: \`add ${entry.name}\``,
|
|
529
|
+
);
|
|
530
|
+
if (entry.registryDependencies.length > 0) {
|
|
531
|
+
lines.push(`- Also installs: ${entry.registryDependencies.join(", ")}`);
|
|
532
|
+
}
|
|
533
|
+
if (entry.dependencies.length > 0) {
|
|
534
|
+
lines.push(`- npm: ${entry.dependencies.join(", ")}`);
|
|
535
|
+
}
|
|
536
|
+
if (item?.a11y) {
|
|
537
|
+
lines.push(`- Accessibility: ${item.a11y}`);
|
|
538
|
+
}
|
|
539
|
+
lines.push("");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return lines.join("\n");
|
|
544
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -68,6 +68,7 @@ function buildSourceItem(meta: ComponentMeta): RegistryItem {
|
|
|
68
68
|
registryDependencies: meta.registryDependencies,
|
|
69
69
|
files,
|
|
70
70
|
a11y: meta.a11y,
|
|
71
|
+
access: meta.access ?? "free",
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -147,6 +148,16 @@ export function buildRegistry(): RegistryItem[] {
|
|
|
147
148
|
];
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
/** Items whose source anyone may fetch. */
|
|
152
|
+
export function freeItems(items: RegistryItem[]): RegistryItem[] {
|
|
153
|
+
return items.filter((item) => item.access !== "pro");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Items whose source requires a licence. */
|
|
157
|
+
export function proItems(items: RegistryItem[]): RegistryItem[] {
|
|
158
|
+
return items.filter((item) => item.access === "pro");
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
export function buildIndex(items: RegistryItem[]) {
|
|
151
162
|
const entries: RegistryIndexEntry[] = items.map((item) => ({
|
|
152
163
|
name: item.name,
|
|
@@ -157,6 +168,7 @@ export function buildIndex(items: RegistryItem[]) {
|
|
|
157
168
|
status: item.status,
|
|
158
169
|
dependencies: item.dependencies,
|
|
159
170
|
registryDependencies: item.registryDependencies,
|
|
171
|
+
access: item.access,
|
|
160
172
|
fileCount: item.files.length,
|
|
161
173
|
}));
|
|
162
174
|
|
|
@@ -170,24 +182,73 @@ export function buildIndex(items: RegistryItem[]) {
|
|
|
170
182
|
});
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
export
|
|
185
|
+
export interface WriteResult {
|
|
186
|
+
items: number;
|
|
187
|
+
files: number;
|
|
188
|
+
/** Items whose body was withheld from the public directory. */
|
|
189
|
+
licensed: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Writes the public registry.
|
|
194
|
+
*
|
|
195
|
+
* The index lists everything, including licensed items — that is the catalogue,
|
|
196
|
+
* and an item nobody can see is an item nobody buys. What it does *not* write is
|
|
197
|
+
* a licensed item's body: those files never touch the directory a CDN serves,
|
|
198
|
+
* because a paywall that can be stepped around by fetching the JSON directly is
|
|
199
|
+
* not a paywall. Their bodies go to `writeLicensedModule` instead, behind a
|
|
200
|
+
* request the server can refuse.
|
|
201
|
+
*/
|
|
202
|
+
export function writeRegistry(outDir: string): WriteResult {
|
|
174
203
|
const items = buildRegistry();
|
|
175
204
|
const index = buildIndex(items);
|
|
205
|
+
const free = freeItems(items);
|
|
176
206
|
|
|
177
207
|
rmSync(outDir, { recursive: true, force: true });
|
|
178
208
|
mkdirSync(outDir, { recursive: true });
|
|
179
209
|
|
|
180
210
|
writeFileSync(join(outDir, "index.json"), `${JSON.stringify(index, null, 2)}\n`);
|
|
181
|
-
for (const item of
|
|
211
|
+
for (const item of free) {
|
|
182
212
|
writeFileSync(join(outDir, `${item.name}.json`), `${JSON.stringify(item, null, 2)}\n`);
|
|
183
213
|
}
|
|
184
214
|
|
|
185
215
|
return {
|
|
186
216
|
items: items.length,
|
|
187
|
-
files:
|
|
217
|
+
files: free.reduce((total, item) => total + item.files.length, 0),
|
|
218
|
+
licensed: items.length - free.length,
|
|
188
219
|
};
|
|
189
220
|
}
|
|
190
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Writes the licensed item bodies as a TypeScript module.
|
|
224
|
+
*
|
|
225
|
+
* A module rather than a directory of JSON, because the server that serves
|
|
226
|
+
* these runs on a platform that traces imports to decide what to deploy. A file
|
|
227
|
+
* read at runtime from a path the bundler never saw is a file that is not there
|
|
228
|
+
* in production — and the failure appears only once someone with a valid
|
|
229
|
+
* licence tries to install something.
|
|
230
|
+
*/
|
|
231
|
+
export function writeLicensedModule(outFile: string): number {
|
|
232
|
+
const licensed = proItems(buildRegistry());
|
|
233
|
+
const byName = Object.fromEntries(licensed.map((item) => [item.name, item]));
|
|
234
|
+
|
|
235
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
236
|
+
writeFileSync(
|
|
237
|
+
outFile,
|
|
238
|
+
`// Generated by @dowel-ui/registry. Do not edit.
|
|
239
|
+
//
|
|
240
|
+
// The bodies of licensed registry items. Imported by the route that serves them
|
|
241
|
+
// so the platform's dependency tracing includes them in the deployment; they are
|
|
242
|
+
// deliberately absent from the public registry directory.
|
|
243
|
+
import type { RegistryItem } from "@dowel-ui/registry";
|
|
244
|
+
|
|
245
|
+
export const licensedItems: Record<string, RegistryItem> = ${JSON.stringify(byName, null, 2)};
|
|
246
|
+
`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return licensed.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
// Run directly: `tsx src/build.ts [--out <dir>]`
|
|
192
253
|
const invokedDirectly =
|
|
193
254
|
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -201,6 +262,7 @@ if (invokedDirectly) {
|
|
|
201
262
|
|
|
202
263
|
const result = writeRegistry(outDir);
|
|
203
264
|
console.log(
|
|
204
|
-
`Registry written to ${outDir}: ${String(result.items)} items, ${String(result.files)} files
|
|
265
|
+
`Registry written to ${outDir}: ${String(result.items)} items, ${String(result.files)} files` +
|
|
266
|
+
(result.licensed > 0 ? `, ${String(result.licensed)} licensed item(s) withheld.` : "."),
|
|
205
267
|
);
|
|
206
268
|
}
|