division-ds 0.0.0-stage → 0.1.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/AGENTS.md +190 -0
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/package.json +20 -4
- package/public/manifest/components.json +10550 -0
- package/public/manifest/tokens.json +932 -0
- package/scripts/division-ds.mjs +188 -0
- package/scripts/lib/agent-docs.mjs +510 -0
- package/scripts/lib/button-spacing.mjs +416 -0
- package/scripts/lib/component-props.mjs +181 -0
- package/scripts/lib/props-validator.mjs +320 -0
- package/scripts/lib/rules-installer.mjs +132 -0
- package/scripts/lib/token-discipline.mjs +125 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// Renders the agent-readable outputs from component metadata. Pure functions: text in, text out.
|
|
2
|
+
// The runner (scripts/build-agent-docs.mjs) reads the repository and writes the files.
|
|
3
|
+
|
|
4
|
+
const TIER_NOUN = { atoms: 'Atom', molecules: 'Molecule', organisms: 'Organism' }
|
|
5
|
+
|
|
6
|
+
/** Where the docs site is hosted. The skill names it so an assistant outside this repository can
|
|
7
|
+
* still read the component pages; src/links.ts holds the same address for the site itself. */
|
|
8
|
+
export const DOCS_SITE = 'https://victor-ilioiu.vercel.app/systems/division-ds/'
|
|
9
|
+
/** Where shadcn downloads the components from: public/r/ in the public GitHub repository. */
|
|
10
|
+
export const REGISTRY = 'https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r'
|
|
11
|
+
|
|
12
|
+
/** A table cell: one line, with the pipes that would end the cell escaped. */
|
|
13
|
+
const cell = text => String(text ?? '').replace(/\s*\n\s*/g, ' ').replace(/\|/g, '\\|')
|
|
14
|
+
const code = text => '`' + String(text).replace(/\|/g, '\\|') + '`'
|
|
15
|
+
const fence = (source, language = 'tsx') => '```' + language + '\n' + source + '\n```'
|
|
16
|
+
|
|
17
|
+
/** `import { Card, CardBody } from 'src/components/molecules/card/Card'`, one line per file. */
|
|
18
|
+
export function importLines(entry) {
|
|
19
|
+
return entry.imports.map(({ names, from }) => `import { ${names.join(', ')} } from '${from}'`).join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One component as a markdown page. `entry` is a manifest entry (see `manifestEntry`). */
|
|
23
|
+
export function componentMarkdown(entry) {
|
|
24
|
+
const out = [`# ${entry.name}`, '', `> ${entry.summary}`, '']
|
|
25
|
+
out.push(`- **Best for:** ${entry.useWhen}`, `- **Not for:** ${entry.notFor}`)
|
|
26
|
+
out.push(`- ${TIER_NOUN[entry.tier]}. Source: \`${entry.source}\``)
|
|
27
|
+
out.push(`- Rendered docs page: \`/components/${entry.slug}\``, '')
|
|
28
|
+
|
|
29
|
+
out.push('## Import', '', fence(importLines(entry)), '')
|
|
30
|
+
|
|
31
|
+
if (entry.requires?.length) {
|
|
32
|
+
out.push('## Requires', '', ...entry.requires.map(item => `- ${item}`), '')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
out.push('## Props', '', '| Prop | Type | Default | Description |', '|---|---|---|---|')
|
|
36
|
+
for (const row of entry.props) {
|
|
37
|
+
out.push(`| ${code(row.name)} | ${code(row.type)} | ${row.default ? code(row.default) : ''} | ${cell(row.description)} |`)
|
|
38
|
+
}
|
|
39
|
+
out.push('')
|
|
40
|
+
|
|
41
|
+
if (entry.api?.length) {
|
|
42
|
+
out.push(
|
|
43
|
+
'## Types and helpers',
|
|
44
|
+
'',
|
|
45
|
+
'Exported beside the component, from the same file. Import a type with `import type { ... }`.',
|
|
46
|
+
''
|
|
47
|
+
)
|
|
48
|
+
for (const item of entry.api) out.push(fence(item.code, 'ts'), '')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (entry.examples.length) {
|
|
52
|
+
out.push('## Examples', '', 'Each is compiled and rendered on the docs page. Imports are omitted; see above.', '')
|
|
53
|
+
for (const example of entry.examples) out.push(`### ${example.caption}`, '', fence(example.source), '')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
out.push('## Keyboard', '')
|
|
57
|
+
if (entry.keyboard.length === 0) out.push('Not focusable. No keyboard behaviour of its own.', '')
|
|
58
|
+
else {
|
|
59
|
+
out.push('| Keys | Result |', '|---|---|')
|
|
60
|
+
for (const row of entry.keyboard) out.push(`| ${row.keys.map(code).join(' ')} | ${cell(row.does)} |`)
|
|
61
|
+
out.push('')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
out.push('## Do', '', ...entry.dos.map(item => `- ${item}`), '')
|
|
65
|
+
out.push("## Don't", '', ...entry.donts.map(item => `- ${item}`), '')
|
|
66
|
+
|
|
67
|
+
if (entry.pitfalls?.length) {
|
|
68
|
+
out.push('## Common mistakes', '')
|
|
69
|
+
for (const pitfall of entry.pitfalls) {
|
|
70
|
+
out.push('Wrong:', '', fence(pitfall.wrong), '', 'Right:', '', fence(pitfall.right), '', pitfall.why, '')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
out.push('## Anatomy', '', ...entry.anatomy.map(row => `- **${row.part}**: ${row.description}`), '')
|
|
75
|
+
return out.join('\n')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What the manifest holds for one component: the metadata plus what was read from the code. */
|
|
79
|
+
export function manifestEntry(meta, { imports, api = [], examples, source }) {
|
|
80
|
+
return {
|
|
81
|
+
name: meta.name,
|
|
82
|
+
title: meta.title,
|
|
83
|
+
tier: meta.tier,
|
|
84
|
+
slug: meta.slug,
|
|
85
|
+
summary: meta.summary,
|
|
86
|
+
useWhen: meta.useWhen,
|
|
87
|
+
notFor: meta.notFor,
|
|
88
|
+
source,
|
|
89
|
+
docs: `/docs/components/${meta.slug}.md`,
|
|
90
|
+
imports,
|
|
91
|
+
...(meta.requires ? { requires: meta.requires } : {}),
|
|
92
|
+
props: meta.props,
|
|
93
|
+
...(api.length ? { api } : {}),
|
|
94
|
+
keyboard: meta.keyboard,
|
|
95
|
+
dos: meta.dos,
|
|
96
|
+
donts: meta.donts,
|
|
97
|
+
...(meta.pitfalls ? { pitfalls: meta.pitfalls } : {}),
|
|
98
|
+
anatomy: meta.anatomy,
|
|
99
|
+
examples,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const byTier = entries =>
|
|
104
|
+
Object.keys(TIER_NOUN).map(tier => [tier, entries.filter(entry => entry.tier === tier)])
|
|
105
|
+
|
|
106
|
+
/** The first sentence of a summary: the index line keeps that much and spends the rest of its
|
|
107
|
+
* length on choosing, which is what an agent reads the index for. */
|
|
108
|
+
export const firstSentence = text => text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text
|
|
109
|
+
|
|
110
|
+
/** One component's line in llms.txt: what it is, then best for and not for. */
|
|
111
|
+
export const indexLine = entry =>
|
|
112
|
+
`- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)} Best for: ${entry.useWhen} Not for: ${entry.notFor}`
|
|
113
|
+
|
|
114
|
+
/** llms.txt, in the llmstxt.org shape: a title, a summary, then sections of links. */
|
|
115
|
+
export function llmsIndex(entries, { rules }) {
|
|
116
|
+
const out = [
|
|
117
|
+
'# Division DS',
|
|
118
|
+
'',
|
|
119
|
+
`> A React 19, Tailwind 4 and Radix design system: ${entries.length} components, design tokens in three layers, light and dark themes, WCAG 2.2 AA. Components are source you read and copy, not a package.`,
|
|
120
|
+
'',
|
|
121
|
+
'Links below are relative to this file, so they work wherever the site is served. In the repository the same files are under `public/`.',
|
|
122
|
+
'Read a component page before using the component: it has the props, working examples and the rules.',
|
|
123
|
+
'',
|
|
124
|
+
'## Start here',
|
|
125
|
+
'',
|
|
126
|
+
'- [Rules](llms-guidelines.txt): imports, colour, providers, forms. Short. Read it first.',
|
|
127
|
+
'- [Tokens](llms-tokens.txt): every semantic and component token with when to use it.',
|
|
128
|
+
'- [Helpers](docs/helpers.md): the shared functions and types under src/lib (form errors, money, links, theme, streamed text).',
|
|
129
|
+
'- [All components, compact](llms-components.txt): every prop signature in one file.',
|
|
130
|
+
'- [Component manifest](manifest/components.json) and [token manifest](manifest/tokens.json): the same as data.',
|
|
131
|
+
'',
|
|
132
|
+
]
|
|
133
|
+
for (const [tier, group] of byTier(entries)) {
|
|
134
|
+
out.push(`## ${tier[0].toUpperCase()}${tier.slice(1)}`, '')
|
|
135
|
+
for (const entry of group) out.push(indexLine(entry))
|
|
136
|
+
out.push('')
|
|
137
|
+
}
|
|
138
|
+
out.push('## The rules in brief', '', ...ruleHeadlines(rules).map(line => `- ${line}`), '')
|
|
139
|
+
return out.join('\n')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The first sentence of each rule paragraph (`**Imports.** Import a component from...`). */
|
|
143
|
+
export function ruleHeadlines(rules) {
|
|
144
|
+
return rules
|
|
145
|
+
.split(/\n\s*\n/)
|
|
146
|
+
.map(block => block.replace(/\s*\n\s*/g, ' ').trim())
|
|
147
|
+
.filter(block => block.startsWith('**'))
|
|
148
|
+
.map(block => block.match(/^\*\*[^*]+\*\*\s+.*?[.:](?=\s|$)/)?.[0] ?? block)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Every component's contract in one compact file: no examples, no prose beyond the summary. */
|
|
152
|
+
export function componentsBundle(entries) {
|
|
153
|
+
const out = [
|
|
154
|
+
'# Division DS: components, compact',
|
|
155
|
+
'',
|
|
156
|
+
'> One block per component: what it is, how to import it, what it needs, and every prop as `name: type = default`. Full pages with examples: /docs/components/<slug>.md.',
|
|
157
|
+
'',
|
|
158
|
+
]
|
|
159
|
+
for (const [tier, group] of byTier(entries)) {
|
|
160
|
+
out.push(`## ${tier[0].toUpperCase()}${tier.slice(1)}`, '')
|
|
161
|
+
for (const entry of group) {
|
|
162
|
+
out.push(`### ${entry.name}`, '', entry.summary, '', fence(importLines(entry)), '')
|
|
163
|
+
for (const item of entry.requires ?? []) out.push(`Requires: ${item}`, '')
|
|
164
|
+
for (const row of entry.props) {
|
|
165
|
+
out.push(`- ${code(`${row.name}: ${row.type}${row.default ? ` = ${row.default}` : ''}`)} ${cell(row.description)}`)
|
|
166
|
+
}
|
|
167
|
+
out.push('', `Page: ${entry.docs}`, '')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out.join('\n')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The tokens an author chooses from, grouped by layer, each with its use. Primitive colour ramps
|
|
174
|
+
* are named as a family and not listed: they are not for choosing. */
|
|
175
|
+
export function tokensBundle(manifest) {
|
|
176
|
+
const group = layer => manifest.tokens.filter(token => token.layer === layer)
|
|
177
|
+
const value = token => (typeof token.value === 'string' ? token.value : `dark ${token.value.dark}, light ${token.value.light}`)
|
|
178
|
+
const section = (title, layer, note) => [
|
|
179
|
+
`## ${title}`,
|
|
180
|
+
'',
|
|
181
|
+
...(note ? [note, ''] : []),
|
|
182
|
+
'| Token | Use | Value |',
|
|
183
|
+
'|---|---|---|',
|
|
184
|
+
...group(layer).map(token => `| ${code(token.name)} | ${cell(token.use)} | ${code(value(token))} |`),
|
|
185
|
+
'',
|
|
186
|
+
]
|
|
187
|
+
const ramps = [...new Set(group('primitive').map(token => token.name.replace(/-\d+$/, '')))]
|
|
188
|
+
return [
|
|
189
|
+
'# Division DS: tokens',
|
|
190
|
+
'',
|
|
191
|
+
`> ${manifest.rule}`,
|
|
192
|
+
'',
|
|
193
|
+
'Write a token as a CSS variable inside a Tailwind arbitrary value: `bg-[var(--color-surface-base)]`, `text-[var(--color-text-primary)]`, `gap-[var(--spacing-4)]`. You must use only the names listed here. Do not make one up: an undefined variable is not an error in the browser, it just paints nothing.',
|
|
194
|
+
'',
|
|
195
|
+
...section('Semantic colour', 'semantic', 'Choose by what the colour is doing, not by how it looks. Values differ by theme and switch on their own.'),
|
|
196
|
+
...section('Component', 'component', 'What the controls are built from. Use the component; reach for these only when building a new control.'),
|
|
197
|
+
...section('Spacing', 'spacing', 'Tailwind\'s own spacing utilities sit on the same 4px grid (`gap-3` is 12px, `p-4` is 16px) and are fine to use.'),
|
|
198
|
+
...section('Motion', 'motion'),
|
|
199
|
+
'## Primitive ramps',
|
|
200
|
+
'',
|
|
201
|
+
`Not for use in components or screens: ${ramps.map(code).join(', ')}, each with stops from 50 to 950. The semantic layer is built from them.`,
|
|
202
|
+
'',
|
|
203
|
+
].join('\n')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The shared helpers under src/lib, one section per file: exported types whole, functions as
|
|
207
|
+
* signatures, each with its doc comment. `helpers` is `[{ from, api }]`. */
|
|
208
|
+
export function helpersPage(helpers) {
|
|
209
|
+
const out = [
|
|
210
|
+
'# Division DS: helpers',
|
|
211
|
+
'',
|
|
212
|
+
'> The functions and types under `src/lib` that components and screens share. Read from the source: types are whole, functions are signatures. Import each from its own file.',
|
|
213
|
+
'',
|
|
214
|
+
]
|
|
215
|
+
for (const { from, api } of helpers) {
|
|
216
|
+
if (api.length === 0) continue
|
|
217
|
+
const values = api.filter(item => item.kind !== 'type').map(item => item.name)
|
|
218
|
+
const types = api.filter(item => item.kind === 'type').map(item => item.name)
|
|
219
|
+
const lines = [
|
|
220
|
+
...(values.length ? [`import { ${values.join(', ')} } from '${from}'`] : []),
|
|
221
|
+
...(types.length ? [`import type { ${types.join(', ')} } from '${from}'`] : []),
|
|
222
|
+
]
|
|
223
|
+
out.push(`## ${from}`, '', fence(lines.join('\n')), '')
|
|
224
|
+
for (const item of api) out.push(fence(item.code, 'ts'), '')
|
|
225
|
+
}
|
|
226
|
+
return out.join('\n')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The rules block of AGENTS.md as written, topic markers included. */
|
|
230
|
+
function rulesBlock(agentsMd) {
|
|
231
|
+
const match = agentsMd.replace(/\r\n/g, '\n').match(/<!-- rules:start -->\n([\s\S]*?)\n<!-- rules:end -->/)
|
|
232
|
+
if (!match) throw new Error('AGENTS.md has no <!-- rules:start --> ... <!-- rules:end --> section.')
|
|
233
|
+
return match[1].trim()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A `<!-- topic: forms -->` line inside the rules block. It says which skill topic file the rules
|
|
237
|
+
* under it belong to, until the next marker. It is for the generator only: no output shows it. */
|
|
238
|
+
const TOPIC_MARKER = /^<!-- topic: ([a-z-]+) -->$/
|
|
239
|
+
|
|
240
|
+
/** The rules section of AGENTS.md, between its markers, with the topic markers taken out. Every
|
|
241
|
+
* file that carries the rules whole (llms-guidelines.txt, the rules installer's block) reads this. */
|
|
242
|
+
export function extractRules(agentsMd) {
|
|
243
|
+
return rulesBlock(agentsMd)
|
|
244
|
+
.split('\n')
|
|
245
|
+
.filter(line => !TOPIC_MARKER.test(line))
|
|
246
|
+
.join('\n')
|
|
247
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The skill's topics. `core` is not a file: it is the rules SKILL.md carries itself.
|
|
252
|
+
*
|
|
253
|
+
* What goes in core, and why: only the rules that apply to every screen whatever it holds, so the
|
|
254
|
+
* one file an agent always loads stays short. That is imports, colour basics (a token, never a
|
|
255
|
+
* literal), the providers every screen assumes, the focus ring, the page title and the empty and
|
|
256
|
+
* loading states every screen plans (the `build` command starts there), what the system does not
|
|
257
|
+
* have (it decides the first component choice), and vocabulary (it applies to any copy written in
|
|
258
|
+
* this repository). Everything that only matters once a screen has a form, an overlay, an accent,
|
|
259
|
+
* a status, a layout decision or an animation is a topic, loaded when the work reaches it.
|
|
260
|
+
*/
|
|
261
|
+
export const SKILL_TOPICS = {
|
|
262
|
+
colour: { title: 'Colour roles', when: 'choosing an accent, the text on a fill, or a status colour' },
|
|
263
|
+
layout: { title: 'Layout and type', when: 'laying out a page: the shell, background, width, type sizes, cards, links' },
|
|
264
|
+
forms: { title: 'Forms and actions', when: 'a screen has a field, a form or more than one button' },
|
|
265
|
+
overlays: { title: 'Overlays', when: 'adding a Dialog, an AlertDialog or a Sheet' },
|
|
266
|
+
feedback: { title: 'Feedback', when: 'a screen reports a result or a wait: a toast, an alert, progress, a spinner' },
|
|
267
|
+
motion: { title: 'Motion', when: 'animating anything yourself' },
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The rules split by their topic markers: `{ intro, core, topics: { colour, forms, ... } }`, each a
|
|
272
|
+
* markdown string. Fails when a rule sits before the first marker, a marker names a topic that is
|
|
273
|
+
* not in SKILL_TOPICS, or a topic has no rules, so every rule lands in exactly one file.
|
|
274
|
+
*/
|
|
275
|
+
export function rulesByTopic(agentsMd) {
|
|
276
|
+
const intro = []
|
|
277
|
+
const groups = {}
|
|
278
|
+
let topic = null
|
|
279
|
+
for (const block of rulesBlock(agentsMd).split(/\n\s*\n/)) {
|
|
280
|
+
const marker = block.trim().match(TOPIC_MARKER)
|
|
281
|
+
if (marker) {
|
|
282
|
+
topic = marker[1]
|
|
283
|
+
if (topic !== 'core' && !(topic in SKILL_TOPICS)) throw new Error(`AGENTS.md: unknown rules topic "${topic}".`)
|
|
284
|
+
groups[topic] ??= []
|
|
285
|
+
} else if (topic) groups[topic].push(block)
|
|
286
|
+
else if (block.startsWith('**')) throw new Error(`AGENTS.md: a rule before the first <!-- topic: ... --> marker: ${block.slice(0, 40)}`)
|
|
287
|
+
else intro.push(block)
|
|
288
|
+
}
|
|
289
|
+
for (const name of ['core', ...Object.keys(SKILL_TOPICS)]) {
|
|
290
|
+
if (!groups[name]?.length) throw new Error(`AGENTS.md: no rules under <!-- topic: ${name} -->.`)
|
|
291
|
+
}
|
|
292
|
+
const { core, ...topics } = Object.fromEntries(Object.entries(groups).map(([name, blocks]) => [name, blocks.join('\n\n')]))
|
|
293
|
+
return { intro: intro.join('\n\n'), core, topics }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function guidelinesFile(rules) {
|
|
297
|
+
return [
|
|
298
|
+
'# Division DS: rules',
|
|
299
|
+
'',
|
|
300
|
+
'> What to know before writing a screen with Division DS. Generated from the Rules section of AGENTS.md.',
|
|
301
|
+
'',
|
|
302
|
+
rules.replace(/^## Rules\n+/, ''),
|
|
303
|
+
'',
|
|
304
|
+
].join('\n')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const GENERATED = 'Generated from AGENTS.md by `npm run agent-docs`. Edit the rules there, not here.'
|
|
308
|
+
|
|
309
|
+
const CHECK_COMMANDS = [
|
|
310
|
+
'- In a project that uses the components: `npx division-ds check <files or dirs>` (the checker is on npm; Node 22 or newer). It flags a raw colour, an invented token, touching buttons, an unknown prop and a value outside a prop\'s union.',
|
|
311
|
+
'- In the Division DS repository itself: `npm run verify` (type-check, every gate, the unit tests).',
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
/** The commands: procedures an agent runs, one file each, loaded by name. */
|
|
315
|
+
const SKILL_COMMANDS = {
|
|
316
|
+
build: {
|
|
317
|
+
when: 'building a new screen or page. Plans the regions and their states before any code',
|
|
318
|
+
body: [
|
|
319
|
+
'Plan before code. The states a screen gets wrong are the ones nobody planned: an empty list',
|
|
320
|
+
'with no message, a load that fails into a blank panel, a button that is disabled with no reason.',
|
|
321
|
+
'',
|
|
322
|
+
'1. **Regions.** List the screen top to bottom: the page header, each block of content (a table,',
|
|
323
|
+
' a form, a row of stats, a list), then anything that opens on top (a dialog, a sheet).',
|
|
324
|
+
'2. **States.** Under each region, write one line per state, even when the answer is "none":',
|
|
325
|
+
' - **empty**: nothing yet, or a filter emptied it;',
|
|
326
|
+
' - **loading**: the first load, and a refresh;',
|
|
327
|
+
' - **error**: the load failed, or a save was rejected;',
|
|
328
|
+
' - **success**: the content, and what confirms an action;',
|
|
329
|
+
' - **disabled or no permission**: what someone who cannot act sees, and the reason they are told.',
|
|
330
|
+
'3. **Components.** Pick one component per region and state from `public/llms.txt`. Each line there',
|
|
331
|
+
' has "Best for" and "Not for"; when "Not for" names your case, take the component it names.',
|
|
332
|
+
' The usual picks: page title `PageHeader`; empty `EmptyState`; loading `Skeleton` shapes in a',
|
|
333
|
+
' `SkeletonGroup`; failed load `Alert` in the `hostile` tone with a retry; a done action `Toast`;',
|
|
334
|
+
' a destructive confirm `AlertDialog`.',
|
|
335
|
+
'4. **Read.** Open `public/docs/components/<slug>.md` for every component in the plan (Requires,',
|
|
336
|
+
' props, examples, Common mistakes). Load the topic files the plan touches: a field means',
|
|
337
|
+
' `topics/forms.md`, an overlay `topics/overlays.md`, a status colour `topics/colour.md`.',
|
|
338
|
+
'5. **Write.** Each component imported from its own file; every colour a token from',
|
|
339
|
+
' `public/llms-tokens.txt`.',
|
|
340
|
+
'6. **Check.** Run the check below and fix every finding (`commands/fix.md`), then go through',
|
|
341
|
+
' `commands/review.md`.',
|
|
342
|
+
'',
|
|
343
|
+
'A plan looks like this:',
|
|
344
|
+
'',
|
|
345
|
+
'```',
|
|
346
|
+
'PageHeader: "Orders", action "New order"',
|
|
347
|
+
'Region: orders table (DataTable, Pagination below)',
|
|
348
|
+
' empty: EmptyState "No orders yet", action "Browse replicas", as emptyMessage',
|
|
349
|
+
' loading: SkeletonGroup "Loading orders", one Skeleton line per row',
|
|
350
|
+
' error: Alert hostile "Orders did not load", Retry button',
|
|
351
|
+
' success: rows; a bulk action confirms with a Toast',
|
|
352
|
+
' no access: the "New order" button is absent, a sentence says who can create orders',
|
|
353
|
+
'```',
|
|
354
|
+
'',
|
|
355
|
+
'## Check',
|
|
356
|
+
'',
|
|
357
|
+
...CHECK_COMMANDS,
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
add: {
|
|
361
|
+
when: 'adding one component to a screen that already exists',
|
|
362
|
+
body: [
|
|
363
|
+
'1. Find it in `public/llms.txt`. Its "Best for" should describe your case and its "Not for"',
|
|
364
|
+
' should not; if "Not for" names your case, use the component it names instead.',
|
|
365
|
+
'2. Open `public/docs/components/<slug>.md` and read, in order: **Requires** (a provider, a',
|
|
366
|
+
' wrapper such as `FormField`, a landmark), **Props**, **Examples**, **Common mistakes** and',
|
|
367
|
+
' **Don\'t**.',
|
|
368
|
+
'3. Take the import from the page\'s Import block and rewrite it as a relative path from your',
|
|
369
|
+
' file. Import a type with `import type`.',
|
|
370
|
+
'4. Start from the closest example on the page and change only the content. Keep what it sets',
|
|
371
|
+
' for access: `label`, `id`, `aria-*`, the heading level.',
|
|
372
|
+
'5. Supply what Requires names: a field inside a `FormField`; `Switch`, `Checkbox` and `Radio`',
|
|
373
|
+
' with an `id` and a `<label htmlFor>`; a button beside another inside a parent with a gap.',
|
|
374
|
+
'6. Add the states it brings with it: why a control is disabled, what a list shows when empty,',
|
|
375
|
+
' a button\'s `loading`.',
|
|
376
|
+
'7. Run the check below on the file.',
|
|
377
|
+
'',
|
|
378
|
+
'## Check',
|
|
379
|
+
'',
|
|
380
|
+
...CHECK_COMMANDS,
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
fix: {
|
|
384
|
+
when: 'a gate, `division-ds check` or a test has failed and named a rule. What each message means and the fix',
|
|
385
|
+
body: [
|
|
386
|
+
'Every finding prints `path:line [rule] what was found` and a line saying what to write instead.',
|
|
387
|
+
'Do what that line says. Never silence a gate: no new EXEMPT entry, no `token-discipline: allow`',
|
|
388
|
+
'comment and no ALLOWED_IDIOMS entry to get past one.',
|
|
389
|
+
'',
|
|
390
|
+
'| Message | Fix |',
|
|
391
|
+
'|---|---|',
|
|
392
|
+
'| `[raw-colour]` a hex, `rgb()` or `hsl()` | Replace it with a semantic token by intent, `text-[var(--color-text-primary)]`. Pick from `public/llms-tokens.txt`. |',
|
|
393
|
+
'| `[palette-class]` `bg-red-500`, `text-white` | Same: an arbitrary value on a token, `bg-[var(--color-surface-raised)]`. |',
|
|
394
|
+
'| `[unknown-token]` | The name is not a token. Take the "Did you mean" suggestion, or look it up in `public/llms-tokens.txt`. Never define it yourself. |',
|
|
395
|
+
'| `[primitive-in-component]` | A ramp step (`--color-signal-500`). Use the semantic token for what it does: `accent.<state>.text`, `.fill` or `.ink`. |',
|
|
396
|
+
'| `touching-buttons`, "Button-spacing gate FAILED" | Wrap the buttons in one parent with a gap (`flex flex-wrap items-center gap-3`) and move any margin onto it. `DialogFooter` and `CardFooter` already are one. |',
|
|
397
|
+
'| `[unknown-prop]` "has no prop" | The prop does not exist. Open the page the message names and use a prop from its table. |',
|
|
398
|
+
'| `[unknown-prop]` "types X away on purpose" | The component sets that attribute itself. Use the prop it names (`label` for a name, `invalid` for an error state). |',
|
|
399
|
+
'| `[invalid-value]` | The value is not in the prop\'s union. The message lists the allowed values. |',
|
|
400
|
+
'| "Vocabulary gate FAILED" (this repository only) | Reword the line. If it is an ordinary idiom, ask before adding it to ALLOWED_IDIOMS. |',
|
|
401
|
+
'| "Agent docs are stale" (this repository only) | Run `npm run agent-docs`, then `npm run verify`. |',
|
|
402
|
+
'| A props-table test fails in `meta.test.ts` (this repository only) | The `<Name>.meta.ts` table and the real props disagree. Change the table to match the code, both ways. |',
|
|
403
|
+
'| A type error on a component prop | Read the prop\'s type on its page; for a link that is disabled or busy, render the state directly instead of fighting the type. |',
|
|
404
|
+
'',
|
|
405
|
+
'After the fix, run the same command again until it passes.',
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
review: {
|
|
409
|
+
when: 'a screen is written and you are about to say it is done. The checklist to run over it',
|
|
410
|
+
body: [
|
|
411
|
+
'Go through every line. A line that does not hold is a change to make, not a note to leave.',
|
|
412
|
+
'',
|
|
413
|
+
'- [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned in `commands/build.md`. An empty list says what is empty and offers the action that fills it.',
|
|
414
|
+
'- [ ] **Headings.** One `PageHeader`, so one `h1`. Section headings continue the outline: `h2` under the `h1`, `h3` inside an `h2`, no level skipped. `SectionHeading` is given the level.',
|
|
415
|
+
'- [ ] **Tokens only.** No hex, `rgb()`, palette class or invented token name. `division-ds check` passes.',
|
|
416
|
+
'- [ ] **Buttons.** Two or more side by side share a parent with a gap. One primary button per view. A busy button takes `loading`; a disabled one says why nearby. Icon-only actions are `IconButton` with a `label`.',
|
|
417
|
+
'- [ ] **Forms.** Every `Input`, `Textarea`, `Select`, `MultiSelect` and `QuantityStepper` is inside a `FormField`; `Switch`, `Checkbox` and `Radio` have an `id` and a `<label htmlFor>`. A failed submit shows `ErrorSummary` and focuses it.',
|
|
418
|
+
'- [ ] **Status.** Every status has a word, not only a colour.',
|
|
419
|
+
'- [ ] **Focus.** Everything interactive is reached by Tab in reading order and shows the focus ring; anything styled by hand has the `focus-visible:outline-*` classes. An overlay hands focus back to what opened it.',
|
|
420
|
+
'- [ ] **Text size.** Secondary text is 14px or larger; 11 to 12px only for uppercase mono labels.',
|
|
421
|
+
'- [ ] **320px.** At 320px wide nothing scrolls sideways (`document.documentElement.scrollWidth <= 320`) and button rows wrap.',
|
|
422
|
+
'- [ ] **Both themes.** It reads in dark and in light (`data-theme="light"` on `<html>`).',
|
|
423
|
+
'- [ ] **Motion.** Anything animated by hand stops under reduced motion; nothing moves on its own.',
|
|
424
|
+
'',
|
|
425
|
+
'## Check',
|
|
426
|
+
'',
|
|
427
|
+
...CHECK_COMMANDS,
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The skill an agent installs, as files under `skills/division-ds/` keyed by their path inside it:
|
|
434
|
+
* `SKILL.md` (the core, always loaded), `topics/<name>.md` (the rest of the rules, by subject) and
|
|
435
|
+
* `commands/<name>.md` (procedures). The split is what keeps the always-loaded part short: an agent
|
|
436
|
+
* reads the core every time and a topic only when the work reaches it. `split` is `rulesByTopic`.
|
|
437
|
+
*/
|
|
438
|
+
export function skillFiles(split, componentCount) {
|
|
439
|
+
const files = {}
|
|
440
|
+
files['SKILL.md'] = [
|
|
441
|
+
'---',
|
|
442
|
+
'name: division-ds',
|
|
443
|
+
`description: Use when building or changing UI with Division DS (React 19, Tailwind 4, Radix; ${componentCount} components). Covers which component and token to use, the composition rules prop types cannot express (providers, FormField, button gaps, card borders), planning a screen's states, and how to check the result. Load it before writing any screen or component in a project that has Division DS components.`,
|
|
444
|
+
'---',
|
|
445
|
+
'',
|
|
446
|
+
'# Division DS',
|
|
447
|
+
'',
|
|
448
|
+
GENERATED,
|
|
449
|
+
'',
|
|
450
|
+
'This file is the core: what every screen needs. The topic and command files below hold the rest; load one when the work reaches it, not before.',
|
|
451
|
+
'',
|
|
452
|
+
'## Plan first',
|
|
453
|
+
'',
|
|
454
|
+
'Before writing a screen, list its regions and, for each, its states: empty, loading, error, success, and disabled or no permission. Pick a component for each from `public/llms.txt` by its "Best for" and "Not for". `commands/build.md` is the whole procedure.',
|
|
455
|
+
'',
|
|
456
|
+
'## Where things are',
|
|
457
|
+
'',
|
|
458
|
+
'| You want | Read |',
|
|
459
|
+
'|---|---|',
|
|
460
|
+
'| Every component, one line each with best for and not for | `public/llms.txt` |',
|
|
461
|
+
'| One component: props, examples, keyboard, mistakes | `public/docs/components/<slug>.md` |',
|
|
462
|
+
'| Which token to use for what | `public/llms-tokens.txt` |',
|
|
463
|
+
'| Shared helpers: form errors, money, links, theme, streamed text | `public/docs/helpers.md` |',
|
|
464
|
+
'| The same as data | `public/manifest/components.json`, `public/manifest/tokens.json` |',
|
|
465
|
+
'',
|
|
466
|
+
`Outside this repository, read the same files online: \`${DOCS_SITE}llms.txt\`, \`${DOCS_SITE}docs/components/<slug>.md\`, and so on. Add a component to a project with \`npx shadcn@latest add ${REGISTRY}/<slug>.json\`, or every component with \`division-ds-all\` as the slug. The setup steps, in plain text: \`${DOCS_SITE}install.md\`.`,
|
|
467
|
+
'',
|
|
468
|
+
'## Check your work',
|
|
469
|
+
'',
|
|
470
|
+
...CHECK_COMMANDS,
|
|
471
|
+
'',
|
|
472
|
+
'A finding says what to write instead. `commands/fix.md` explains each one.',
|
|
473
|
+
'',
|
|
474
|
+
'## Topics: load when',
|
|
475
|
+
'',
|
|
476
|
+
...Object.entries(SKILL_TOPICS).map(([name, topic]) => `- \`topics/${name}.md\`: ${topic.when}.`),
|
|
477
|
+
'',
|
|
478
|
+
'## Commands: load when',
|
|
479
|
+
'',
|
|
480
|
+
...Object.entries(SKILL_COMMANDS).map(([name, command]) => `- \`commands/${name}.md\`: ${command.when}.`),
|
|
481
|
+
'',
|
|
482
|
+
'## Core rules',
|
|
483
|
+
'',
|
|
484
|
+
split.intro.replace(/^## Rules\n*/, '').trim(),
|
|
485
|
+
'',
|
|
486
|
+
split.core,
|
|
487
|
+
'',
|
|
488
|
+
].join('\n')
|
|
489
|
+
for (const [name, topic] of Object.entries(SKILL_TOPICS)) {
|
|
490
|
+
files[`topics/${name}.md`] = [
|
|
491
|
+
`# Division DS: ${topic.title.toLowerCase()}`,
|
|
492
|
+
'',
|
|
493
|
+
`> Load when ${topic.when}. ${GENERATED}`,
|
|
494
|
+
'',
|
|
495
|
+
split.topics[name],
|
|
496
|
+
'',
|
|
497
|
+
].join('\n')
|
|
498
|
+
}
|
|
499
|
+
for (const [name, command] of Object.entries(SKILL_COMMANDS)) {
|
|
500
|
+
files[`commands/${name}.md`] = [
|
|
501
|
+
`# Division DS: ${name}`,
|
|
502
|
+
'',
|
|
503
|
+
`> Load when ${command.when}. Generated by \`npm run agent-docs\`; the procedure lives in scripts/lib/agent-docs.mjs.`,
|
|
504
|
+
'',
|
|
505
|
+
...command.body,
|
|
506
|
+
'',
|
|
507
|
+
].join('\n')
|
|
508
|
+
}
|
|
509
|
+
return files
|
|
510
|
+
}
|