division-ds 0.1.2 → 0.2.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 +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +7 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// A Model Context Protocol server over stdio, for `division-ds mcp`. Hand-written rather than
|
|
2
|
+
// built on @modelcontextprotocol/sdk: the npm package has one dependency (typescript) and a tools-
|
|
3
|
+
// only server needs four methods. The transport is one JSON-RPC message per line on stdin and
|
|
4
|
+
// stdout; anything written to stdout that is not a message breaks the client, so logs go to stderr.
|
|
5
|
+
//
|
|
6
|
+
// `handle` is pure (a message in, a reply or nothing out) so the tests drive it without a process.
|
|
7
|
+
import { ToolError } from './mcp-tools.mjs'
|
|
8
|
+
|
|
9
|
+
// Newest first. A client asking for one of these gets it back; any other gets the newest, and the
|
|
10
|
+
// client decides whether it can work with that (the spec's negotiation).
|
|
11
|
+
export const PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']
|
|
12
|
+
|
|
13
|
+
const INSTRUCTIONS = `Division DS: a React 19, Tailwind 4 and Radix design system.
|
|
14
|
+
Before writing a component, call get_component for its props and examples. Use only tokens from get_tokens or search, as var(--name).
|
|
15
|
+
Before saying a screen is done, call validate_code on each file you wrote and fix every finding, then go through get_review_checklist.`
|
|
16
|
+
|
|
17
|
+
/** A JSON-RPC error reply. */
|
|
18
|
+
const failure = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } })
|
|
19
|
+
|
|
20
|
+
/** The first problem with `args` against a tool's input schema, or undefined. Only what the tools
|
|
21
|
+
* declare: required keys, unknown keys, and the JSON type of each value. */
|
|
22
|
+
export function argumentProblem(schema, args) {
|
|
23
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) return 'arguments must be an object.'
|
|
24
|
+
const properties = schema.properties ?? {}
|
|
25
|
+
for (const key of schema.required ?? []) if (args[key] === undefined) return `${key} is required.`
|
|
26
|
+
for (const [key, value] of Object.entries(args)) {
|
|
27
|
+
const property = properties[key]
|
|
28
|
+
if (!property) return `Unknown argument ${key}. This tool takes: ${Object.keys(properties).join(', ') || 'nothing'}.`
|
|
29
|
+
const type = Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value
|
|
30
|
+
const ok = property.type === type || (property.type === 'number' && type === 'integer')
|
|
31
|
+
if (!ok) return `${key} must be ${/^[aeiou]/.test(property.type) ? 'an' : 'a'} ${property.type}.`
|
|
32
|
+
if (property.enum && !property.enum.includes(value)) return `${key} is one of ${property.enum.join(', ')}.`
|
|
33
|
+
if (property.type === 'array' && property.items?.type && value.some(item => typeof item !== property.items.type)) return `${key} holds ${property.items.type}s.`
|
|
34
|
+
}
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createServer({ tools, version }) {
|
|
39
|
+
const byName = new Map(tools.map(tool => [tool.name, tool]))
|
|
40
|
+
|
|
41
|
+
function call(params) {
|
|
42
|
+
const tool = byName.get(params?.name)
|
|
43
|
+
if (!tool) return { content: [{ type: 'text', text: `No tool "${params?.name}". Tools: ${[...byName.keys()].join(', ')}.` }], isError: true }
|
|
44
|
+
const args = params.arguments ?? {}
|
|
45
|
+
const problem = argumentProblem(tool.inputSchema, args)
|
|
46
|
+
if (problem) return { content: [{ type: 'text', text: `${tool.name}: ${problem}` }], isError: true }
|
|
47
|
+
try {
|
|
48
|
+
return { content: [{ type: 'text', text: tool.run(args) }] }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// A ToolError is the agent's mistake and says how to fix it. Anything else is ours; it is
|
|
51
|
+
// still reported as a tool result, so one bad call does not take the session down.
|
|
52
|
+
const text = error instanceof ToolError ? error.message : `division-ds ${version} failed on ${tool.name}: ${error.message}`
|
|
53
|
+
return { content: [{ type: 'text', text }], isError: true }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One parsed message in; the reply to send, or undefined for a notification. */
|
|
58
|
+
function handle(message) {
|
|
59
|
+
if (message === null || typeof message !== 'object' || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
|
|
60
|
+
return failure(message?.id ?? null, -32600, 'Invalid request.')
|
|
61
|
+
}
|
|
62
|
+
const { id, method, params } = message
|
|
63
|
+
// A notification (no id) never gets a reply, even one we do not understand.
|
|
64
|
+
if (id === undefined) return undefined
|
|
65
|
+
switch (method) {
|
|
66
|
+
case 'initialize': {
|
|
67
|
+
const asked = params?.protocolVersion
|
|
68
|
+
return {
|
|
69
|
+
jsonrpc: '2.0',
|
|
70
|
+
id,
|
|
71
|
+
result: {
|
|
72
|
+
protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
73
|
+
capabilities: { tools: {} },
|
|
74
|
+
serverInfo: { name: 'division-ds', version },
|
|
75
|
+
instructions: INSTRUCTIONS,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
case 'ping':
|
|
80
|
+
return { jsonrpc: '2.0', id, result: {} }
|
|
81
|
+
case 'tools/list':
|
|
82
|
+
return { jsonrpc: '2.0', id, result: { tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) } }
|
|
83
|
+
case 'tools/call':
|
|
84
|
+
return { jsonrpc: '2.0', id, result: call(params) }
|
|
85
|
+
default:
|
|
86
|
+
return failure(id, -32601, `Method not found: ${method}`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Reads messages from `input` line by line and writes replies to `output`. */
|
|
91
|
+
function listen(input, output) {
|
|
92
|
+
let buffer = ''
|
|
93
|
+
const send = reply => reply && output.write(JSON.stringify(reply) + '\n')
|
|
94
|
+
input.setEncoding('utf8')
|
|
95
|
+
input.on('data', chunk => {
|
|
96
|
+
buffer += chunk
|
|
97
|
+
let newline
|
|
98
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
99
|
+
const line = buffer.slice(0, newline).trim()
|
|
100
|
+
buffer = buffer.slice(newline + 1)
|
|
101
|
+
if (!line) continue
|
|
102
|
+
let message
|
|
103
|
+
try {
|
|
104
|
+
message = JSON.parse(line)
|
|
105
|
+
} catch {
|
|
106
|
+
send(failure(null, -32700, 'Parse error: each line must be one JSON-RPC message.'))
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
send(handle(message))
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { handle, listen }
|
|
115
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// The tools `division-ds mcp` serves, as plain functions over the two manifests. The protocol
|
|
2
|
+
// (JSON-RPC over stdio) lives in mcp-server.mjs; nothing here knows it exists, so every tool is
|
|
3
|
+
// unit-tested by calling it.
|
|
4
|
+
//
|
|
5
|
+
// Most design-system MCP servers stop at list / search / get docs (shadcn, Chakra, Dialtone,
|
|
6
|
+
// Blade, Vibe; surveyed 2026-10-25). `validate_code` is the one they lack: it runs the same gates
|
|
7
|
+
// as `division-ds check` on the code the agent has just written, before the agent says it is done.
|
|
8
|
+
import { checkText } from './check-text.mjs'
|
|
9
|
+
import { contrastRatio } from './contrast.mjs'
|
|
10
|
+
import { buildIndex, closest } from './props-validator.mjs'
|
|
11
|
+
|
|
12
|
+
export const REGISTRY_BASE = 'https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r'
|
|
13
|
+
const TIERS = ['atoms', 'molecules', 'organisms']
|
|
14
|
+
const LAYERS = ['primitive', 'spacing', 'motion', 'semantic', 'component']
|
|
15
|
+
// WCAG 2.2 minimums: SC 1.4.3 for text, SC 1.4.11 for the boundary of a control or a graphic.
|
|
16
|
+
const MINIMUMS = { normal: 4.5, large: 3, ui: 3 }
|
|
17
|
+
|
|
18
|
+
const words = text => String(text).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
19
|
+
|
|
20
|
+
/** `context`: { manifest, tokens, version, readDoc(slug) -> string|undefined, readChecklist() -> string|undefined } */
|
|
21
|
+
export function createTools(context) {
|
|
22
|
+
const { manifest, tokens, version } = context
|
|
23
|
+
const components = manifest.components
|
|
24
|
+
const index = buildIndex(manifest)
|
|
25
|
+
const tokenNames = tokens.tokens.map(token => token.name)
|
|
26
|
+
const tokenByName = new Map(tokens.tokens.map(token => [token.name, token]))
|
|
27
|
+
// Every reply says which data it came from, because the agent's project may have installed an
|
|
28
|
+
// older copy of the components than the manifest this server reads (Dialtone does the same).
|
|
29
|
+
const stamp = `\n\n(division-ds ${version}; data: ${components.length} components, ${tokenNames.length} tokens. It describes the current release; a project that installed the components earlier may differ.)`
|
|
30
|
+
|
|
31
|
+
function findComponent(name) {
|
|
32
|
+
const wanted = String(name ?? '').trim().toLowerCase()
|
|
33
|
+
return components.find(c => c.name.toLowerCase() === wanted || c.slug === wanted)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function unknownComponent(name) {
|
|
37
|
+
const guess = closest(String(name), components.map(c => c.name))
|
|
38
|
+
throw new ToolError(`No component "${name}".${guess ? ` Did you mean ${guess}?` : ''} list_components gives every name.`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A token's value in one theme, following var() references down to a colour. */
|
|
42
|
+
function tokenValue(name, theme, seen = new Set()) {
|
|
43
|
+
if (seen.has(name)) throw new ToolError(`Token ${name} refers to itself.`)
|
|
44
|
+
seen.add(name)
|
|
45
|
+
const token = tokenByName.get(name)
|
|
46
|
+
if (!token) {
|
|
47
|
+
const guess = closest(name, tokenNames)
|
|
48
|
+
throw new ToolError(`No token ${name}.${guess ? ` Did you mean ${guess}?` : ''} get_tokens lists them.`)
|
|
49
|
+
}
|
|
50
|
+
const value = typeof token.value === 'object' ? token.value[theme] : token.value
|
|
51
|
+
const ref = /^var\((--[a-z0-9-]+)\)$/.exec(value)
|
|
52
|
+
return ref ? tokenValue(ref[1], theme, seen) : value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const colour = (input, theme) => (String(input).startsWith('--') ? tokenValue(String(input), theme) : String(input))
|
|
56
|
+
|
|
57
|
+
const tools = [
|
|
58
|
+
{
|
|
59
|
+
name: 'list_components',
|
|
60
|
+
description: 'Every Division DS component with its tier and one line on when to use it. Start here to pick a component; then call get_component for its props and examples.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: { tier: { type: 'string', enum: TIERS, description: 'Only this tier.' } },
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
},
|
|
66
|
+
run({ tier } = {}) {
|
|
67
|
+
if (tier !== undefined && !TIERS.includes(tier)) throw new ToolError(`tier is one of ${TIERS.join(', ')}.`)
|
|
68
|
+
const rows = components.filter(c => !tier || c.tier === tier)
|
|
69
|
+
return rows.map(c => `- ${c.name} (${c.tier}): ${c.useWhen}${c.notFor ? ` Not for: ${c.notFor}` : ''}`).join('\n')
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'get_component',
|
|
74
|
+
description: 'One component: import path, props with types and defaults, keyboard behaviour, do and don\'t, and working examples. Read it before writing the component, so props and values are real.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
name: { type: 'string', description: 'Component name or slug, e.g. "Button" or "icon-button".' },
|
|
79
|
+
section: { type: 'string', enum: ['all', 'props'], description: '"props" returns only the import and the prop table. Default "all".' },
|
|
80
|
+
},
|
|
81
|
+
required: ['name'],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
run({ name, section = 'all' }) {
|
|
85
|
+
const c = findComponent(name) ?? unknownComponent(name)
|
|
86
|
+
const imports = c.imports.map(i => `import { ${i.names.join(', ')} } from '${i.from}'`).join('\n')
|
|
87
|
+
const props = c.props.map(p => `- ${p.name}: ${p.type}${p.default ? ` = ${p.default}` : ''}. ${p.description ?? ''}`.trimEnd()).join('\n')
|
|
88
|
+
const head = `# ${c.name} (${c.tier})\n\n${c.summary}\n\n${imports}\n\nPaths are from the Division DS repository root; installed with shadcn they start src/division-ds/.\n\n## Props\n\n${props}`
|
|
89
|
+
if (section === 'props') return head
|
|
90
|
+
const page = context.readDoc?.(c.slug)
|
|
91
|
+
// The page is the full contract (examples, keyboard, do and don't, written for agents).
|
|
92
|
+
// Without it, fall back to the same facts from the manifest.
|
|
93
|
+
if (page) return page
|
|
94
|
+
const list = (title, items) => (items?.length ? `\n\n## ${title}\n\n${items.map(item => `- ${item}`).join('\n')}` : '')
|
|
95
|
+
const keyboard = (c.keyboard ?? []).map(k => `${k.keys.join(' + ')}: ${k.does}`)
|
|
96
|
+
const examples = (c.examples ?? []).map(e => `### ${e.caption}\n\n\`\`\`tsx\n${e.source}\n\`\`\``).join('\n\n')
|
|
97
|
+
return head + list('Keyboard', keyboard) + list('Do', c.dos) + list('Don\'t', c.donts) + (examples ? `\n\n## Examples\n\n${examples}` : '')
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'search',
|
|
102
|
+
description: 'Find components and tokens by what they are for, e.g. "confirm delete", "page background", "loading". Returns exact names to pass to get_component or to use as var(--name).',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
query: { type: 'string' },
|
|
107
|
+
limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Default 10.' },
|
|
108
|
+
},
|
|
109
|
+
required: ['query'],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
},
|
|
112
|
+
run({ query, limit = 10 }) {
|
|
113
|
+
const terms = words(query)
|
|
114
|
+
if (terms.length === 0) throw new ToolError('query has no words to search for.')
|
|
115
|
+
const score = (title, body) => {
|
|
116
|
+
const t = words(title).join(' ')
|
|
117
|
+
const b = words(body)
|
|
118
|
+
return terms.reduce((sum, term) => sum + (t.includes(term) ? 3 : 0) + b.filter(w => w.startsWith(term)).length, 0)
|
|
119
|
+
}
|
|
120
|
+
const hits = [
|
|
121
|
+
...components.map(c => ({ kind: 'component', name: c.name, line: c.useWhen, score: score(c.name, `${c.summary} ${c.useWhen}`) })),
|
|
122
|
+
// Primitives are not for screens (tokens.json's rule), so they never come up as an answer.
|
|
123
|
+
...tokens.tokens.filter(t => t.layer !== 'primitive').map(t => ({ kind: `${t.layer} token`, name: t.name, line: t.use ?? '', score: score(t.name, t.use ?? '') })),
|
|
124
|
+
]
|
|
125
|
+
.filter(hit => hit.score > 0)
|
|
126
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
127
|
+
.slice(0, limit)
|
|
128
|
+
if (hits.length === 0) return `Nothing matches "${query}". Try a broader word, or list_components / get_tokens.`
|
|
129
|
+
return hits.map(hit => `- ${hit.name} (${hit.kind}): ${hit.line}`).join('\n')
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'get_tokens',
|
|
134
|
+
description: 'Design tokens with their values (light and dark for themed colours) and when to use each. Screens use semantic and component tokens only, as var(--name) inside a Tailwind arbitrary value: text-[var(--color-text-primary)].',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
layer: { type: 'string', enum: LAYERS, description: 'Only this layer. Default: every layer except primitive.' },
|
|
139
|
+
query: { type: 'string', description: 'Only tokens whose name or use contains this text.' },
|
|
140
|
+
},
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
run({ layer, query } = {}) {
|
|
144
|
+
if (layer !== undefined && !LAYERS.includes(layer)) throw new ToolError(`layer is one of ${LAYERS.join(', ')}.`)
|
|
145
|
+
const needle = query?.toLowerCase()
|
|
146
|
+
const rows = tokens.tokens
|
|
147
|
+
.filter(t => (layer ? t.layer === layer : t.layer !== 'primitive'))
|
|
148
|
+
.filter(t => !needle || `${t.name} ${t.use ?? ''}`.toLowerCase().includes(needle))
|
|
149
|
+
if (rows.length === 0) return 'No tokens match.'
|
|
150
|
+
const value = v => (typeof v === 'object' ? `dark ${v.dark}, light ${v.light}` : v)
|
|
151
|
+
return `${tokens.rule}\n\n${rows.map(t => `- ${t.name} (${t.layer}) = ${value(t.value)}${t.use ? `. ${t.use}` : ''}`).join('\n')}`
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'validate_code',
|
|
156
|
+
description: 'Checks code you wrote with Division DS components, before you say it is done: raw colours, Tailwind palette classes, token names that do not exist, two buttons with no gap, unknown props, prop values outside their union, "AI" in a button label and "answer"/"please"/"!" in assistant component labels. Props are checked only on elements imported from Division DS paths. Each finding says what to write instead. Same gates as `npx division-ds check`.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
code: { type: 'string', description: 'The whole file, imports included: props are matched through the imports.' },
|
|
161
|
+
filename: { type: 'string', description: 'Its name, e.g. "src/pages/Settings.tsx". The extension decides which gates run. Default "Screen.tsx".' },
|
|
162
|
+
},
|
|
163
|
+
required: ['code'],
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
run({ code, filename = 'Screen.tsx' }) {
|
|
167
|
+
if (!/\.(tsx|jsx|ts)$/.test(filename)) throw new ToolError('filename ends in .tsx, .jsx or .ts.')
|
|
168
|
+
const findings = checkText(String(code), filename, index, tokenNames)
|
|
169
|
+
if (findings.length === 0) {
|
|
170
|
+
return `No findings in ${filename}: colours, tokens, button spacing, props and AI copy are clean.\nNot checked: how the screen behaves. Run get_review_checklist, in a browser.`
|
|
171
|
+
}
|
|
172
|
+
return `${findings.length} finding(s) in ${filename}. Fix each, then validate again.\n\n${findings.map(f => `- [${f.gate}] line ${f.line}: ${f.found}\n Fix: ${f.fix}`).join('\n')}`
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'check_contrast',
|
|
177
|
+
description: 'The WCAG 2.2 AA contrast of a foreground on a background, in both themes. Each colour is a token name (--color-text-secondary) or a colour (#a1a1aa, rgb()). Translucent colours are composited over --color-surface-base. Use it before pairing two tokens the system does not already pair.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
foreground: { type: 'string' },
|
|
182
|
+
background: { type: 'string' },
|
|
183
|
+
use: { type: 'string', enum: ['normal', 'large', 'ui'], description: 'normal text (4.5:1), large text (3:1: 24px, or 18.66px bold), or a control boundary / graphic (3:1). Default normal.' },
|
|
184
|
+
},
|
|
185
|
+
required: ['foreground', 'background'],
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
},
|
|
188
|
+
run({ foreground, background, use = 'normal' }) {
|
|
189
|
+
const min = MINIMUMS[use]
|
|
190
|
+
if (min === undefined) throw new ToolError('use is one of normal, large, ui.')
|
|
191
|
+
const lines = ['dark', 'light'].map(theme => {
|
|
192
|
+
const fg = colour(foreground, theme)
|
|
193
|
+
const bg = colour(background, theme)
|
|
194
|
+
let ratio
|
|
195
|
+
try {
|
|
196
|
+
ratio = contrastRatio(fg, bg, tokenValue('--color-surface-base', theme))
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new ToolError(error.message)
|
|
199
|
+
}
|
|
200
|
+
return `- ${theme}: ${ratio.toFixed(2)}:1 ${ratio >= min ? 'PASS' : 'FAIL'} (${fg} on ${bg})`
|
|
201
|
+
})
|
|
202
|
+
const failed = lines.some(line => line.includes('FAIL'))
|
|
203
|
+
return `${foreground} on ${background}, ${use} (needs ${min}:1):\n${lines.join('\n')}${failed ? '\n\nPick another pair: search for a text or border token meant for this surface.' : ''}`
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'get_add_command',
|
|
208
|
+
description: 'The shadcn command that installs components into a project, with their dependencies. Use when the project does not have the components yet.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
components: { type: 'array', items: { type: 'string' }, description: 'Names or slugs. Empty or "all" gives the command for every component.' },
|
|
213
|
+
},
|
|
214
|
+
additionalProperties: false,
|
|
215
|
+
},
|
|
216
|
+
run({ components: wanted = [] } = {}) {
|
|
217
|
+
if (wanted.length === 0 || (wanted.length === 1 && wanted[0] === 'all')) {
|
|
218
|
+
return `npx shadcn@latest add ${REGISTRY_BASE}/division-ds-all.json\n\nIt installs under src/division-ds/. Then mount one <Toaster> and one <TooltipProvider> at the root of the app.`
|
|
219
|
+
}
|
|
220
|
+
const urls = wanted.map(name => `${REGISTRY_BASE}/${(findComponent(name) ?? unknownComponent(name)).slug}.json`)
|
|
221
|
+
return `npx shadcn@latest add ${urls.join(' ')}\n\nIt installs under src/division-ds/, with the components these depend on.`
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'get_review_checklist',
|
|
226
|
+
description: 'The checklist to run over a screen before saying it is done: states, headings, focus, forms, 320px, both themes, motion. validate_code covers the code; this covers behaviour.',
|
|
227
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
228
|
+
run() {
|
|
229
|
+
const checklist = context.readChecklist?.()
|
|
230
|
+
if (!checklist) throw new ToolError('The review checklist is not in this package. Read it at https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/commands/review.md')
|
|
231
|
+
return checklist
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
return tools.map(tool => ({ ...tool, run: args => tool.run(args ?? {}) + stamp }))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A mistake in the agent's arguments: reported to it as a tool error it can act on, not as a
|
|
240
|
+
* protocol failure. */
|
|
241
|
+
export class ToolError extends Error {}
|
|
@@ -218,7 +218,7 @@ function literalValues(initializer) {
|
|
|
218
218
|
return collect(initializer.expression)
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function scriptKind(path) {
|
|
221
|
+
export function scriptKind(path) {
|
|
222
222
|
if (/\.tsx$/.test(path)) return ts.ScriptKind.TSX
|
|
223
223
|
if (/\.jsx$/.test(path)) return ts.ScriptKind.JSX
|
|
224
224
|
if (/\.[cm]?js$/.test(path)) return ts.ScriptKind.JSX
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// agents AGENTS.md Codex, Claude Code (via AGENTS.md), Copilot, Jules, ...
|
|
13
13
|
// cursor .cursor/rules/division-ds.mdc Cursor project rules; the whole file is ours
|
|
14
14
|
// gemini GEMINI.md Gemini CLI
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
16
|
-
import { dirname, join } from 'node:path'
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { dirname, join, relative } from 'node:path'
|
|
17
17
|
|
|
18
18
|
export const START = '<!-- division-ds:start -->'
|
|
19
19
|
export const END = '<!-- division-ds:end -->'
|
|
@@ -102,14 +102,52 @@ export function planRules({ targetDir, rules, only = TARGETS, remove = false })
|
|
|
102
102
|
})
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
// The skill installer (`division-ds skill`): the generated skills/division-ds/ folder, copied whole
|
|
106
|
+
// into the folders agents scan for skills. Checked 2026-10-26 against each agent's docs:
|
|
107
|
+
// agents .agents/skills/division-ds/ Codex (repo scope), Cursor (also reads .claude/skills)
|
|
108
|
+
// claude .claude/skills/division-ds/ Claude Code project skills
|
|
109
|
+
// The folder is ours whole, like the .mdc: a file in it that the skill no longer ships is deleted.
|
|
110
|
+
export const SKILL_TARGETS = ['agents', 'claude']
|
|
111
|
+
const SKILL_DIRS = { agents: '.agents/skills/division-ds', claude: '.claude/skills/division-ds' }
|
|
112
|
+
|
|
113
|
+
const filesUnder = dir =>
|
|
114
|
+
!existsSync(dir)
|
|
115
|
+
? []
|
|
116
|
+
: readdirSync(dir, { recursive: true }).map(String).filter(name => statSync(join(dir, name)).isFile()).map(name => name.split('\\').join('/')).sort()
|
|
117
|
+
|
|
118
|
+
/** Every file change to install (or `remove`) the skill in `sourceDir` into each target, in the
|
|
119
|
+
* same shape as planRules: `[{ target, path, action, before, after }]`. */
|
|
120
|
+
export function planSkill({ targetDir, sourceDir, only = SKILL_TARGETS, remove = false }) {
|
|
121
|
+
const source = filesUnder(sourceDir)
|
|
122
|
+
return only.flatMap(target => {
|
|
123
|
+
const dir = join(targetDir, SKILL_DIRS[target])
|
|
124
|
+
const names = [...new Set([...source, ...filesUnder(dir)])].sort()
|
|
125
|
+
return names.map(name => {
|
|
126
|
+
const path = join(dir, name)
|
|
127
|
+
const before = read(path)
|
|
128
|
+
const after = remove || !source.includes(name) ? null : readFileSync(join(sourceDir, name), 'utf8')
|
|
129
|
+
let action
|
|
130
|
+
if (before === null) action = after === null ? 'absent' : 'create'
|
|
131
|
+
else if (after === null) action = 'delete'
|
|
132
|
+
else action = after === before ? 'unchanged' : 'update'
|
|
133
|
+
return { target, path, action, before, after }
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Writes a plan to disk. A deletion also removes the folders it left empty, up to `stopAt`. */
|
|
139
|
+
export function applyPlan(plan, stopAt) {
|
|
107
140
|
for (const change of plan) {
|
|
108
141
|
if (change.action === 'create' || change.action === 'update') {
|
|
109
142
|
mkdirSync(dirname(change.path), { recursive: true })
|
|
110
143
|
writeFileSync(change.path, change.after)
|
|
111
144
|
} else if (change.action === 'delete') {
|
|
112
145
|
rmSync(change.path)
|
|
146
|
+
if (!stopAt) continue
|
|
147
|
+
for (let dir = dirname(change.path); relative(stopAt, dir) && !relative(stopAt, dir).startsWith('..'); dir = dirname(dir)) {
|
|
148
|
+
if (readdirSync(dir).length > 0) break
|
|
149
|
+
rmdirSync(dir)
|
|
150
|
+
}
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: division-ds
|
|
3
|
+
description: Use when building or changing UI with Division DS (React 19, Tailwind 4, Radix; 68 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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Division DS
|
|
7
|
+
|
|
8
|
+
Generated from AGENTS.md by `npm run agent-docs`. Edit the rules there, not here.
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
## Plan first
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
## Where things are
|
|
17
|
+
|
|
18
|
+
| You want | Read |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Every component, one line each with best for and not for | `public/llms.txt` |
|
|
21
|
+
| One component: props, examples, keyboard, mistakes | `public/docs/components/<slug>.md` |
|
|
22
|
+
| Which token to use for what | `public/llms-tokens.txt` |
|
|
23
|
+
| Shared helpers: form errors, money, links, theme, streamed text | `public/docs/helpers.md` |
|
|
24
|
+
| The same as data | `public/manifest/components.json`, `public/manifest/tokens.json` |
|
|
25
|
+
|
|
26
|
+
Outside this repository, read the same files online: `https://victor-ilioiu.vercel.app/systems/division-ds/llms.txt`, `https://victor-ilioiu.vercel.app/systems/division-ds/docs/components/<slug>.md`, and so on. Add a component to a project with `npx shadcn@latest add https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r/<slug>.json`, or every component with `division-ds-all` as the slug. The setup steps, in plain text: `https://victor-ilioiu.vercel.app/systems/division-ds/install.md`.
|
|
27
|
+
|
|
28
|
+
## Check your work
|
|
29
|
+
|
|
30
|
+
- 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.
|
|
31
|
+
- In the Division DS repository itself: `npm run verify` (type-check, every gate, the unit tests).
|
|
32
|
+
|
|
33
|
+
A finding says what to write instead. `commands/fix.md` explains each one.
|
|
34
|
+
|
|
35
|
+
## Topics: load when
|
|
36
|
+
|
|
37
|
+
- `topics/colour.md`: choosing an accent, the text on a fill, or a status colour.
|
|
38
|
+
- `topics/layout.md`: laying out a page: the shell, background, width, type sizes, cards, links.
|
|
39
|
+
- `topics/forms.md`: a screen has a field, a form or more than one button.
|
|
40
|
+
- `topics/overlays.md`: adding a Dialog, an AlertDialog or a Sheet.
|
|
41
|
+
- `topics/feedback.md`: a screen reports a result or a wait: a toast, an alert, progress, a spinner.
|
|
42
|
+
- `topics/motion.md`: animating anything yourself.
|
|
43
|
+
- `topics/ai.md`: wording a chat.
|
|
44
|
+
|
|
45
|
+
## Commands: load when
|
|
46
|
+
|
|
47
|
+
- `commands/build.md`: building a new screen or page. Plans the regions and their states before any code.
|
|
48
|
+
- `commands/add.md`: adding one component to a screen that already exists.
|
|
49
|
+
- `commands/fix.md`: a gate, `division-ds check` or a test has failed and named a rule. What each message means and the fix.
|
|
50
|
+
- `commands/review.md`: a screen is written and you are about to say it is done. The checklist to run over it.
|
|
51
|
+
|
|
52
|
+
## Core rules
|
|
53
|
+
|
|
54
|
+
Each of these is something an agent got wrong or had to guess when it first used the system.
|
|
55
|
+
|
|
56
|
+
**Imports.** Import a component from its own file, by relative path. There is no path alias: the
|
|
57
|
+
docs write `src/components/atoms/button/Button`, and from `src/pages/Screen.tsx` that is
|
|
58
|
+
`'../components/atoms/button/Button'`. Helpers work the same way: `src/lib/money` is
|
|
59
|
+
`'../lib/money'` from there. The tier barrels (`components/atoms`, `/molecules`,
|
|
60
|
+
`/organisms`) are for the system's own composition; the organisms barrel pulls the chart library
|
|
61
|
+
into whatever imports it. Icons come from `lucide-react`, with `aria-hidden="true"` unless the
|
|
62
|
+
icon is the only content. Installed with shadcn, paths start `src/division-ds/` instead of `src/`.
|
|
63
|
+
|
|
64
|
+
**Colour.** Every colour is a semantic token inside a Tailwind arbitrary value:
|
|
65
|
+
`text-[var(--color-text-primary)]`, `bg-[var(--color-surface-base)]`. Never a hex, never
|
|
66
|
+
`rgb()`, never a Tailwind palette class (`bg-red-500`, `text-white`), never a token name you
|
|
67
|
+
have not seen in `llms-tokens.txt`. An invented name fails silently in the browser and loudly in
|
|
68
|
+
`npm run verify`.
|
|
69
|
+
|
|
70
|
+
**Providers.** One `<Toaster>` and one `<TooltipProvider>` sit at the root of the app (`AppShell`,
|
|
71
|
+
`DemoLayout` in this repository). Assume both, including for a screen you are writing that is not
|
|
72
|
+
mounted anywhere yet: it will be mounted under one of the two shells. Call `useToast()` freely; do
|
|
73
|
+
not mount a second `Toaster`, and do not drop a toast or a tooltip because you cannot see the
|
|
74
|
+
provider. A project that uses the components mounts both itself (step 4 of `install.md`).
|
|
75
|
+
|
|
76
|
+
**Focus.** Anything focusable that you style yourself gets
|
|
77
|
+
`focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]`.
|
|
78
|
+
|
|
79
|
+
**Page title, empty, loading.** Plan these before writing a screen. A page title is `PageHeader`
|
|
80
|
+
(it renders the `h1`). An empty list or result is `EmptyState`; inside a `DataTable`, pass it as
|
|
81
|
+
`emptyMessage`. A loading region is `Skeleton` shapes inside a `SkeletonGroup`. The demo's
|
|
82
|
+
`src/demo/states/` files are not part of the system.
|
|
83
|
+
|
|
84
|
+
**What the system does not have.** No Popover, no Combobox (single value: use `Select`; several:
|
|
85
|
+
`MultiSelect`), no date picker. Do not reach into `src/demo/` from a component.
|
|
86
|
+
|
|
87
|
+
**Vocabulary.** Inside this repository, `npm run check:vocabulary` bans a list of words in `src/`,
|
|
88
|
+
`scripts/` and `e2e/` (the list is `scripts/lib/vocabulary.mjs`; it is not published with the
|
|
89
|
+
docs). Run it on sample copy too: an ordinary phrase can match. Products in sample data are
|
|
90
|
+
"replicas". The gate does not apply to a project that only uses the components.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Division DS: add
|
|
2
|
+
|
|
3
|
+
> Load when adding one component to a screen that already exists. Generated by `npm run agent-docs`; the procedure lives in scripts/lib/agent-docs.mjs.
|
|
4
|
+
|
|
5
|
+
Paths starting `public/` are in the Division DS repository. In a project that uses the components, read them online: `public/llms.txt` is `https://victor-ilioiu.vercel.app/systems/division-ds/llms.txt`, and so on.
|
|
6
|
+
|
|
7
|
+
1. Find it in `public/llms.txt`. Its "Best for" should describe your case and its "Not for"
|
|
8
|
+
should not; if "Not for" names your case, use the component it names instead.
|
|
9
|
+
2. Open `public/docs/components/<slug>.md` and read, in order: **Requires** (a provider, a
|
|
10
|
+
wrapper such as `FormField`, a landmark), **Props**, **Examples**, **Common mistakes** and
|
|
11
|
+
**Don't**.
|
|
12
|
+
3. Take the import from the page's Import block and rewrite it as a relative path from your
|
|
13
|
+
file. Import a type with `import type`.
|
|
14
|
+
4. Start from the closest example on the page and change only the content. Keep what it sets
|
|
15
|
+
for access: `label`, `id`, `aria-*`, the heading level.
|
|
16
|
+
5. Supply what Requires names: a field inside a `FormField`; `Switch`, `Checkbox` and `Radio`
|
|
17
|
+
with an `id` and a `<label htmlFor>`; a button beside another inside a parent with a gap.
|
|
18
|
+
6. Add the states it brings with it: why a control is disabled, what a list shows when empty,
|
|
19
|
+
a button's `loading`.
|
|
20
|
+
7. Run the check below on the file.
|
|
21
|
+
|
|
22
|
+
## Check
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
- In the Division DS repository itself: `npm run verify` (type-check, every gate, the unit tests).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Division DS: build
|
|
2
|
+
|
|
3
|
+
> Load when building a new screen or page. Plans the regions and their states before any code. Generated by `npm run agent-docs`; the procedure lives in scripts/lib/agent-docs.mjs.
|
|
4
|
+
|
|
5
|
+
Paths starting `public/` are in the Division DS repository. In a project that uses the components, read them online: `public/llms.txt` is `https://victor-ilioiu.vercel.app/systems/division-ds/llms.txt`, and so on.
|
|
6
|
+
|
|
7
|
+
Plan before code. The states a screen gets wrong are the ones nobody planned: an empty list
|
|
8
|
+
with no message, a load that fails into a blank panel, a button that is disabled with no reason.
|
|
9
|
+
|
|
10
|
+
1. **Regions.** List the screen top to bottom: the page header, each block of content (a table,
|
|
11
|
+
a form, a row of stats, a list), then anything that opens on top (a dialog, a sheet).
|
|
12
|
+
2. **States.** Under each region, write one line per state, even when the answer is "none":
|
|
13
|
+
- **empty**: nothing yet, or a filter emptied it;
|
|
14
|
+
- **loading**: the first load, and a refresh;
|
|
15
|
+
- **error**: the load failed, or a save was rejected;
|
|
16
|
+
- **success**: the content, and what confirms an action;
|
|
17
|
+
- **disabled or no permission**: what someone who cannot act sees, and the reason they are told.
|
|
18
|
+
3. **Components.** Pick one component per region and state from `public/llms.txt`. Each line there
|
|
19
|
+
has "Best for" and "Not for"; when "Not for" names your case, take the component it names.
|
|
20
|
+
The usual picks: page title `PageHeader`; empty `EmptyState`; loading `Skeleton` shapes in a
|
|
21
|
+
`SkeletonGroup`; failed load `Alert` in the `hostile` tone with a retry; a done action `Toast`;
|
|
22
|
+
a destructive confirm `AlertDialog`.
|
|
23
|
+
4. **Read.** Open `public/docs/components/<slug>.md` for every component in the plan (Requires,
|
|
24
|
+
props, examples, Common mistakes). Load the topic files the plan touches: a field means
|
|
25
|
+
`topics/forms.md`, an overlay `topics/overlays.md`, a status colour `topics/colour.md`.
|
|
26
|
+
5. **Write.** Each component imported from its own file; every colour a token from
|
|
27
|
+
`public/llms-tokens.txt`.
|
|
28
|
+
6. **Check.** Run the check below and fix every finding (`commands/fix.md`), then go through
|
|
29
|
+
`commands/review.md`.
|
|
30
|
+
|
|
31
|
+
A plan looks like this:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
PageHeader: "Orders", action "New order"
|
|
35
|
+
Region: orders table (DataTable, Pagination below)
|
|
36
|
+
empty: EmptyState "No orders yet", action "Browse replicas", as emptyMessage
|
|
37
|
+
loading: SkeletonGroup "Loading orders", one Skeleton line per row
|
|
38
|
+
error: Alert hostile "Orders did not load", Retry button
|
|
39
|
+
success: rows; a bulk action confirms with a Toast
|
|
40
|
+
no access: the "New order" button is absent, a sentence says who can create orders
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Check
|
|
44
|
+
|
|
45
|
+
- 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.
|
|
46
|
+
- In the Division DS repository itself: `npm run verify` (type-check, every gate, the unit tests).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Division DS: fix
|
|
2
|
+
|
|
3
|
+
> Load when a gate, `division-ds check` or a test has failed and named a rule. What each message means and the fix. Generated by `npm run agent-docs`; the procedure lives in scripts/lib/agent-docs.mjs.
|
|
4
|
+
|
|
5
|
+
Paths starting `public/` are in the Division DS repository. In a project that uses the components, read them online: `public/llms.txt` is `https://victor-ilioiu.vercel.app/systems/division-ds/llms.txt`, and so on.
|
|
6
|
+
|
|
7
|
+
Every finding prints `path:line [rule] what was found` and a line saying what to write instead.
|
|
8
|
+
Do what that line says. Never silence a gate: no new EXEMPT entry, no `token-discipline: allow`
|
|
9
|
+
comment and no ALLOWED_IDIOMS entry to get past one.
|
|
10
|
+
|
|
11
|
+
| Message | Fix |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `[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`. |
|
|
14
|
+
| `[palette-class]` `bg-red-500`, `text-white` | Same: an arbitrary value on a token, `bg-[var(--color-surface-raised)]`. |
|
|
15
|
+
| `[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. |
|
|
16
|
+
| `[primitive-in-component]` | A ramp step (`--color-signal-500`). Use the semantic token for what it does: `accent.<state>.text`, `.fill` or `.ink`. |
|
|
17
|
+
| `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. |
|
|
18
|
+
| `[unknown-prop]` "has no prop" | The prop does not exist. Open the page the message names and use a prop from its table. |
|
|
19
|
+
| `[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). |
|
|
20
|
+
| `[invalid-value]` | The value is not in the prop's union. The message lists the allowed values. |
|
|
21
|
+
| `[ai-in-cta]` | A button says "AI". Name what it does instead: "Summarize", "Draft a reply". |
|
|
22
|
+
| `[assistant-copy]` | An assistant component's label says "answer", "please" or "!". Write "response", drop "please", drop the exclamation point. |
|
|
23
|
+
| "Vocabulary gate FAILED" (this repository only) | Reword the line. If it is an ordinary idiom, ask before adding it to ALLOWED_IDIOMS. |
|
|
24
|
+
| "Agent docs are stale" (this repository only) | Run `npm run agent-docs`, then `npm run verify`. |
|
|
25
|
+
| 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. |
|
|
26
|
+
| 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. |
|
|
27
|
+
|
|
28
|
+
After the fix, run the same command again until it passes.
|