division-ds 0.2.0 → 0.2.1

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.
Files changed (89) hide show
  1. package/AGENTS.md +9 -3
  2. package/division-ds-0.2.1.tgz +0 -0
  3. package/package.json +1 -1
  4. package/public/docs/components/accordion.md +2 -0
  5. package/public/docs/components/ai-label.md +2 -0
  6. package/public/docs/components/alert-dialog.md +2 -0
  7. package/public/docs/components/alert.md +2 -0
  8. package/public/docs/components/approval-request.md +2 -0
  9. package/public/docs/components/auth-form.md +2 -0
  10. package/public/docs/components/avatar-group.md +2 -0
  11. package/public/docs/components/avatar.md +2 -0
  12. package/public/docs/components/badge.md +2 -0
  13. package/public/docs/components/breadcrumbs.md +2 -0
  14. package/public/docs/components/button.md +2 -0
  15. package/public/docs/components/card.md +2 -0
  16. package/public/docs/components/carousel.md +2 -0
  17. package/public/docs/components/cart.md +2 -0
  18. package/public/docs/components/chart-panel.md +2 -0
  19. package/public/docs/components/chat-message.md +3 -0
  20. package/public/docs/components/chat-thread.md +2 -0
  21. package/public/docs/components/checkbox.md +2 -0
  22. package/public/docs/components/checkout-form.md +2 -0
  23. package/public/docs/components/command-palette.md +2 -0
  24. package/public/docs/components/data-table.md +2 -0
  25. package/public/docs/components/description-list.md +2 -0
  26. package/public/docs/components/dialog.md +2 -0
  27. package/public/docs/components/divider.md +2 -0
  28. package/public/docs/components/dropdown-menu.md +2 -0
  29. package/public/docs/components/empty-state.md +2 -0
  30. package/public/docs/components/footer.md +2 -0
  31. package/public/docs/components/form-field.md +4 -1
  32. package/public/docs/components/icon-button.md +2 -0
  33. package/public/docs/components/input.md +2 -0
  34. package/public/docs/components/kanban-board.md +2 -0
  35. package/public/docs/components/kbd.md +2 -0
  36. package/public/docs/components/link.md +2 -0
  37. package/public/docs/components/list-item.md +3 -0
  38. package/public/docs/components/loading-animation.md +2 -0
  39. package/public/docs/components/multi-select.md +2 -0
  40. package/public/docs/components/navbar.md +2 -0
  41. package/public/docs/components/newsletter-form.md +2 -0
  42. package/public/docs/components/page-header.md +2 -0
  43. package/public/docs/components/pagination.md +2 -0
  44. package/public/docs/components/product-card.md +3 -0
  45. package/public/docs/components/progress.md +3 -1
  46. package/public/docs/components/prompt-input.md +2 -0
  47. package/public/docs/components/quantity-stepper.md +2 -0
  48. package/public/docs/components/radio.md +38 -1
  49. package/public/docs/components/rating.md +2 -0
  50. package/public/docs/components/score-ring.md +2 -0
  51. package/public/docs/components/search.md +2 -0
  52. package/public/docs/components/section-heading.md +2 -0
  53. package/public/docs/components/segmented-control.md +2 -0
  54. package/public/docs/components/select.md +3 -1
  55. package/public/docs/components/sheet.md +2 -0
  56. package/public/docs/components/sidebar.md +2 -0
  57. package/public/docs/components/skeleton.md +2 -0
  58. package/public/docs/components/slider.md +3 -1
  59. package/public/docs/components/sparkline.md +2 -0
  60. package/public/docs/components/spinner.md +4 -2
  61. package/public/docs/components/stat.md +2 -0
  62. package/public/docs/components/status-tracker.md +2 -0
  63. package/public/docs/components/stepper.md +2 -0
  64. package/public/docs/components/switch.md +4 -2
  65. package/public/docs/components/tabs.md +2 -0
  66. package/public/docs/components/tag.md +2 -0
  67. package/public/docs/components/textarea.md +2 -0
  68. package/public/docs/components/timeline.md +2 -0
  69. package/public/docs/components/toast.md +2 -0
  70. package/public/docs/components/tool-call.md +2 -0
  71. package/public/docs/components/tooltip.md +2 -0
  72. package/public/manifest/components.json +16 -9
  73. package/public/manifest/tokens.json +36 -5
  74. package/scripts/division-ds.mjs +5 -5
  75. package/scripts/lib/agent-docs.mjs +28 -6
  76. package/scripts/lib/ai-copy.mjs +15 -1
  77. package/scripts/lib/check-text.mjs +13 -0
  78. package/scripts/lib/mcp-server.mjs +2 -1
  79. package/scripts/lib/mcp-tools.mjs +33 -8
  80. package/scripts/lib/props-validator.mjs +2 -1
  81. package/scripts/lib/token-discipline.mjs +10 -3
  82. package/skills/division-ds/commands/add.md +1 -1
  83. package/skills/division-ds/commands/build.md +1 -1
  84. package/skills/division-ds/commands/fix.md +1 -1
  85. package/skills/division-ds/commands/review.md +2 -2
  86. package/skills/division-ds/topics/ai.md +2 -1
  87. package/skills/division-ds/topics/forms.md +3 -0
  88. package/skills/division-ds/topics/layout.md +3 -1
  89. package/division-ds-0.2.0.tgz +0 -0
@@ -5,6 +5,7 @@
5
5
  // Most design-system MCP servers stop at list / search / get docs (shadcn, Chakra, Dialtone,
6
6
  // Blade, Vibe; surveyed 2026-10-25). `validate_code` is the one they lack: it runs the same gates
7
7
  // as `division-ds check` on the code the agent has just written, before the agent says it is done.
8
+ import { alsoExports, INSTALLED_PATHS } from './agent-docs.mjs'
8
9
  import { checkText } from './check-text.mjs'
9
10
  import { contrastRatio } from './contrast.mjs'
10
11
  import { buildIndex, closest } from './props-validator.mjs'
@@ -30,7 +31,11 @@ export function createTools(context) {
30
31
 
31
32
  function findComponent(name) {
32
33
  const wanted = String(name ?? '').trim().toLowerCase()
33
- return components.find(c => c.name.toLowerCase() === wanted || c.slug === wanted)
34
+ return (
35
+ components.find(c => c.name.toLowerCase() === wanted || c.slug === wanted) ??
36
+ // A component documented on another's page (ResponseFeedback on ChatMessage's) opens that page.
37
+ components.find(c => alsoExports(c).some(other => other.toLowerCase() === wanted))
38
+ )
34
39
  }
35
40
 
36
41
  function unknownComponent(name) {
@@ -66,7 +71,12 @@ export function createTools(context) {
66
71
  run({ tier } = {}) {
67
72
  if (tier !== undefined && !TIERS.includes(tier)) throw new ToolError(`tier is one of ${TIERS.join(', ')}.`)
68
73
  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')
74
+ return rows
75
+ .map(c => {
76
+ const also = alsoExports(c)
77
+ return `- ${c.name} (${c.tier}): ${c.useWhen}${c.notFor ? ` Not for: ${c.notFor}` : ''}${also.length ? ` Also on its page: ${also.join(', ')}.` : ''}`
78
+ })
79
+ .join('\n')
70
80
  },
71
81
  },
72
82
  {
@@ -85,7 +95,7 @@ export function createTools(context) {
85
95
  const c = findComponent(name) ?? unknownComponent(name)
86
96
  const imports = c.imports.map(i => `import { ${i.names.join(', ')} } from '${i.from}'`).join('\n')
87
97
  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}`
98
+ const head = `# ${c.name} (${c.tier})\n\n${c.summary}\n\n${imports}\n\n${INSTALLED_PATHS}\n\n## Props\n\n${props}`
89
99
  if (section === 'props') return head
90
100
  const page = context.readDoc?.(c.slug)
91
101
  // The page is the full contract (examples, keyboard, do and don't, written for agents).
@@ -112,13 +122,28 @@ export function createTools(context) {
112
122
  run({ query, limit = 10 }) {
113
123
  const terms = words(query)
114
124
  if (terms.length === 0) throw new ToolError('query has no words to search for.')
115
- const score = (title, body) => {
125
+ // `props` is the prop descriptions: a word found there counts once, as a hint, so a long
126
+ // prop table does not outrank a component or token whose own summary is about the query.
127
+ const score = (title, body, props = '', propNames = '') => {
116
128
  const t = words(title).join(' ')
117
129
  const b = words(body)
118
- return terms.reduce((sum, term) => sum + (t.includes(term) ? 3 : 0) + b.filter(w => w.startsWith(term)).length, 0)
130
+ const p = words(props)
131
+ const n = words(propNames)
132
+ const inMain = term => t.includes(term) || b.some(w => w.startsWith(term))
133
+ // Every word of the query in the name or summary beats one word found often: "page
134
+ // background" is the surface token, not PageHeader, whose name matches "page" alone.
135
+ const all = terms.length > 1 && terms.every(inMain) ? 5 : 0
136
+ return all + terms.reduce((sum, term) => sum + (t.includes(term) ? 3 : 0) + Math.min(b.filter(w => w.startsWith(term)).length, 3) + (inMain(term) ? 0 : n.some(w => w.startsWith(term)) ? 2 : p.some(w => w.startsWith(term)) ? 1 : 0), 0)
119
137
  }
120
138
  const hits = [
121
- ...components.map(c => ({ kind: 'component', name: c.name, line: c.useWhen, score: score(c.name, `${c.summary} ${c.useWhen}`) })),
139
+ // The title holds every name on the page (ChatMessage and ResponseFeedback); the body adds
140
+ // the prop descriptions, where what a part does is written ("helpful", "thumbs up").
141
+ ...components.map(c => {
142
+ const also = alsoExports(c)
143
+ const props = c.props.map(p => `${p.name} ${p.description ?? ''}`).join(' ')
144
+ const line = `${c.useWhen}${also.length ? ` Also on its page: ${also.join(', ')}.` : ''}`
145
+ return { kind: 'component', name: c.name, line, score: score(`${c.name} ${also.join(' ')}`, `${c.summary} ${c.useWhen}`, props, c.props.map(p => p.name).join(' ')) }
146
+ }),
122
147
  // Primitives are not for screens (tokens.json's rule), so they never come up as an answer.
123
148
  ...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
149
  ]
@@ -158,13 +183,13 @@ export function createTools(context) {
158
183
  type: 'object',
159
184
  properties: {
160
185
  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".' },
186
+ filename: { type: 'string', description: 'Its name, e.g. "src/pages/Settings.tsx". The extension decides which gates run: a .css file gets the colour and token checks, and setting a real token (--color-surface-base: ...) is allowed there, since that is how a project themes the system. Default "Screen.tsx".' },
162
187
  },
163
188
  required: ['code'],
164
189
  additionalProperties: false,
165
190
  },
166
191
  run({ code, filename = 'Screen.tsx' }) {
167
- if (!/\.(tsx|jsx|ts)$/.test(filename)) throw new ToolError('filename ends in .tsx, .jsx or .ts.')
192
+ if (!/\.(tsx|jsx|ts|css)$/.test(filename)) throw new ToolError('filename ends in .tsx, .jsx, .ts or .css.')
168
193
  const findings = checkText(String(code), filename, index, tokenNames)
169
194
  if (findings.length === 0) {
170
195
  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.`
@@ -28,6 +28,7 @@
28
28
  // accepted; one the rest type names in its Omit<> is not. With a `...rest` row of a Radix
29
29
  // component's props, any other prop is accepted (the Radix surface is not in the manifest).
30
30
  import ts from 'typescript'
31
+ import { DOCS_SITE } from './agent-docs.mjs'
31
32
  import { parseRowName, resolvePart } from './component-props.mjs'
32
33
 
33
34
  /** Always accepted on any component: React's own props, and attributes every DOM wrapper takes. */
@@ -125,7 +126,7 @@ export function buildIndex(manifest) {
125
126
  if (exportNames.length === 0) continue
126
127
  const components = Object.fromEntries(exportNames.map(name => [name, true]))
127
128
  const specs = Object.fromEntries(
128
- exportNames.map(name => [name, { entry: entry.name, docs: entry.docs, props: new Map(), rest: null }])
129
+ exportNames.map(name => [name, { entry: entry.name, docs: entry.docs && `${DOCS_SITE}${entry.docs.replace(/^\//, '')}`, props: new Map(), rest: null }])
129
130
  )
130
131
  let unresolved = false
131
132
  for (const row of entry.props ?? []) {
@@ -9,6 +9,12 @@
9
9
  // Colour only, on purpose. Spacing is not gated: enforcing it before the scale is complete makes
10
10
  // an author (or an agent) invent a token name to satisfy the rule, which is the worse failure.
11
11
 
12
+ import { DOCS_SITE } from './agent-docs.mjs'
13
+
14
+ /** Where a token list lives for whoever reads the finding: the MCP tool, or the docs site. A
15
+ * repository path meant nothing in a project that installed the components (2026-10-29 test). */
16
+ const TOKEN_LIST = `get_tokens over MCP, or ${DOCS_SITE}llms-tokens.txt`
17
+
12
18
  export const SCANNED_ROOTS = ['src']
13
19
  export const SCANNED_EXTENSIONS = /\.(tsx?|css)$/
14
20
 
@@ -16,6 +22,7 @@ export const SCANNED_EXTENSIONS = /\.(tsx?|css)$/
16
22
  * A new entry needs a reason as good as these; "it was easier" is the case the gate exists for. */
17
23
  export const EXEMPT = [
18
24
  { pattern: /^src\/styles\/tokens\.css$/, why: 'generated from tokens/: this is where the literals are meant to be' },
25
+ { pattern: /(^|\/)division-ds\/styles\/tokens\.css$/, why: 'the same file, installed into a project by shadcn; it also names the MCP Apps host\'s own variables' },
19
26
  { pattern: /\.test\.tsx?$/, why: 'tests assert on literal values and on deliberately wrong input' },
20
27
  { pattern: /^src\/demo\/(scene|blob|terrain)\//, why: 'WebGL materials: three.js takes colours as numbers, not CSS variables' },
21
28
  { pattern: /^src\/components\/atoms\/loading-animation\/(palette|scene|kit|variants)\.ts$/, why: 'WebGL materials, read from the tokens at runtime where they can be' },
@@ -98,9 +105,9 @@ export function findViolations(text, path, tokenNames) {
98
105
  if (name.endsWith('-')) continue
99
106
  if (!known.has(name)) {
100
107
  const near = suggest(name, tokenNames)
101
- add('unknown-token', `var(${name})`, near.length ? `No such token. Did you mean ${near.join(', ')}?` : 'No such token. The full list is public/manifest/tokens.json.')
108
+ add('unknown-token', `var(${name})`, near.length ? `No such token. Did you mean ${near.join(', ')}?` : `No such token. The full list: ${TOKEN_LIST}.`)
102
109
  } else if (PRIMITIVE_RAMP.test(name) && inComponents(path)) {
103
- add('primitive-in-component', `var(${name})`, 'A component reads semantic tokens, not a ramp step. Pick one by intent from public/manifest/tokens.json (accent.<state>.text, .fill or .ink).')
110
+ add('primitive-in-component', `var(${name})`, `A component reads semantic tokens, not a ramp step. Pick one by intent (accent.<state>.text, .fill or .ink): ${TOKEN_LIST}.`)
104
111
  }
105
112
  }
106
113
 
@@ -108,7 +115,7 @@ export function findViolations(text, path, tokenNames) {
108
115
  // where a literal colour would be.
109
116
  const rest = line.replace(/var\([^)]*\)/g, '')
110
117
  for (const match of rest.matchAll(HEX)) {
111
- add('raw-colour', match[0], 'Use a semantic token: text-[var(--color-text-primary)], bg-[var(--color-surface-base)]. Tokens and what each is for: public/manifest/tokens.json.')
118
+ add('raw-colour', match[0], `Use a semantic token: text-[var(--color-text-primary)], bg-[var(--color-surface-base)]. Tokens and what each is for: ${TOKEN_LIST}.`)
112
119
  }
113
120
  for (const match of rest.matchAll(COLOR_FUNCTION)) {
114
121
  add('raw-colour', match[0] + '…)', 'Use a semantic token. If no token fits, the colour is a change to the system: add it under tokens/ with a contrast pair.')
@@ -2,7 +2,7 @@
2
2
 
3
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
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.
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. The other procedures named here (`commands/build.md` and the rest) are in the skill: `https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/commands/<name>.md`, or `npx division-ds skill` to install them.
6
6
 
7
7
  1. Find it in `public/llms.txt`. Its "Best for" should describe your case and its "Not for"
8
8
  should not; if "Not for" names your case, use the component it names instead.
@@ -2,7 +2,7 @@
2
2
 
3
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
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.
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. The other procedures named here (`commands/build.md` and the rest) are in the skill: `https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/commands/<name>.md`, or `npx division-ds skill` to install them.
6
6
 
7
7
  Plan before code. The states a screen gets wrong are the ones nobody planned: an empty list
8
8
  with no message, a load that fails into a blank panel, a button that is disabled with no reason.
@@ -2,7 +2,7 @@
2
2
 
3
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
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.
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. The other procedures named here (`commands/build.md` and the rest) are in the skill: `https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/commands/<name>.md`, or `npx division-ds skill` to install them.
6
6
 
7
7
  Every finding prints `path:line [rule] what was found` and a line saying what to write instead.
8
8
  Do what that line says. Never silence a gate: no new EXEMPT entry, no `token-discipline: allow`
@@ -2,11 +2,11 @@
2
2
 
3
3
  > Load when a screen is written and you are about to say it is done. The checklist to run over it. Generated by `npm run agent-docs`; the procedure lives in scripts/lib/agent-docs.mjs.
4
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.
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. The other procedures named here (`commands/build.md` and the rest) are in the skill: `https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/commands/<name>.md`, or `npx division-ds skill` to install them.
6
6
 
7
7
  Go through every line. A line that does not hold is a change to make, not a note to leave.
8
8
 
9
- - [ ] **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.
9
+ - [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned before the code (the regions-and-states plan of `commands/build.md`). An empty list says what is empty and offers the action that fills it.
10
10
  - [ ] **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.
11
11
  - [ ] **Tokens only.** No hex, `rgb()`, palette class or invented token name. `division-ds check` passes.
12
12
  - [ ] **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`.
@@ -4,4 +4,5 @@
4
4
 
5
5
  **Assistant copy.** A button names the action ("Summarize"), never "AI". Say "response", not
6
6
  "answer". No "please" or "!" in assistant labels. The interface says "Assistant"; only its own
7
- messages say "I".
7
+ messages say "I". Response feedback: `ResponseFeedback` in `actions`.
8
+ A value the assistant filled is `aiValue` on that `Input` or `Textarea` only; leaving it accepts it.
@@ -10,6 +10,9 @@ bare and name it through its own `label` prop ("Role for Ilse Varga"). `Switch`,
10
10
  renders `ErrorSummary` at the top and focuses it; `useFieldErrors` holds the state (signature in
11
11
  `public/docs/helpers.md`).
12
12
 
13
+ A `RadioGroup` in a form sits in a `<fieldset>` whose `<legend>` names it (code on the Radio
14
+ page). A date is `<Input type="date">` in a `FormField`.
15
+
13
16
  **Buttons.** Two or more buttons side by side need a parent with a gap
14
17
  (`flex flex-wrap gap-3`); `DialogFooter` and `CardFooter` already are one. A `Button` is
15
18
  `type="button"` unless you say `type="submit"`. A busy button takes `loading`, not `disabled`.
@@ -8,7 +8,9 @@ The shell paints the page background (`surface.base`) and supplies the gutter; a
8
8
  own `max-w-*` and vertical spacing, and no page background. A form or an overlay is the
9
9
  exception: it paints `surface.base` on itself, so its field colours never depend on what is
10
10
  behind it. In a project that uses the components, your own root does this
11
- (step 4 of `install.md`).
11
+ (step 4 of `install.md`). Toggle it with `setTheme()` from `src/lib/theme.ts`.
12
+
13
+ **App navigation.** A few views: `Navbar` (`current: true` on the open one). Many: `Sidebar`.
12
14
 
13
15
  **Type.** Three families, as utilities: `font-display` (headings, figures), `font-body` (the
14
16
  default, set by the shell), `font-mono` (labels, code, tabular numbers). A page title is
Binary file