division-ds 0.2.1 → 0.3.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.
- package/AGENTS.md +4 -4
- package/package.json +1 -1
- package/public/docs/components/accordion.md +1 -1
- package/public/docs/components/ai-label.md +1 -1
- package/public/docs/components/alert-dialog.md +1 -1
- package/public/docs/components/alert.md +1 -1
- package/public/docs/components/approval-request.md +1 -1
- package/public/docs/components/auth-form.md +1 -1
- package/public/docs/components/avatar-group.md +1 -1
- package/public/docs/components/avatar.md +1 -1
- package/public/docs/components/badge.md +1 -1
- package/public/docs/components/breadcrumbs.md +1 -1
- package/public/docs/components/button.md +1 -1
- package/public/docs/components/card.md +1 -1
- package/public/docs/components/carousel.md +1 -1
- package/public/docs/components/cart.md +1 -1
- package/public/docs/components/chart-panel.md +1 -1
- package/public/docs/components/chat-message.md +1 -1
- package/public/docs/components/chat-thread.md +1 -1
- package/public/docs/components/checkbox.md +1 -1
- package/public/docs/components/checkout-form.md +1 -1
- package/public/docs/components/command-palette.md +5 -2
- package/public/docs/components/data-table.md +1 -1
- package/public/docs/components/date-picker.md +166 -0
- package/public/docs/components/date-presets.md +111 -0
- package/public/docs/components/date-range-picker.md +135 -0
- package/public/docs/components/description-list.md +1 -1
- package/public/docs/components/dialog.md +1 -1
- package/public/docs/components/divider.md +1 -1
- package/public/docs/components/dropdown-menu.md +1 -1
- package/public/docs/components/empty-state.md +1 -1
- package/public/docs/components/footer.md +1 -1
- package/public/docs/components/form-field.md +2 -2
- package/public/docs/components/icon-button.md +1 -1
- package/public/docs/components/input.md +1 -1
- package/public/docs/components/kanban-board.md +1 -1
- package/public/docs/components/kbd.md +1 -1
- package/public/docs/components/link.md +1 -1
- package/public/docs/components/list-item.md +1 -1
- package/public/docs/components/loading-animation.md +1 -1
- package/public/docs/components/multi-select.md +5 -5
- package/public/docs/components/navbar.md +1 -1
- package/public/docs/components/newsletter-form.md +1 -1
- package/public/docs/components/page-header.md +1 -1
- package/public/docs/components/pagination.md +1 -1
- package/public/docs/components/product-card.md +1 -1
- package/public/docs/components/progress.md +1 -1
- package/public/docs/components/prompt-input.md +1 -1
- package/public/docs/components/quantity-stepper.md +2 -2
- package/public/docs/components/radio.md +1 -1
- package/public/docs/components/rating.md +1 -1
- package/public/docs/components/score-ring.md +1 -1
- package/public/docs/components/search.md +1 -1
- package/public/docs/components/section-heading.md +1 -1
- package/public/docs/components/segmented-control.md +1 -1
- package/public/docs/components/select.md +2 -2
- package/public/docs/components/sheet.md +1 -1
- package/public/docs/components/sidebar.md +1 -1
- package/public/docs/components/skeleton.md +1 -1
- package/public/docs/components/slider.md +1 -1
- package/public/docs/components/sparkline.md +1 -1
- package/public/docs/components/spinner.md +1 -1
- package/public/docs/components/stat.md +1 -1
- package/public/docs/components/status-tracker.md +1 -1
- package/public/docs/components/stepper.md +1 -1
- package/public/docs/components/switch.md +1 -1
- package/public/docs/components/tabs.md +1 -1
- package/public/docs/components/tag.md +1 -1
- package/public/docs/components/textarea.md +1 -1
- package/public/docs/components/time-field.md +127 -0
- package/public/docs/components/timeline.md +1 -1
- package/public/docs/components/toast.md +1 -1
- package/public/docs/components/tool-call.md +1 -1
- package/public/docs/components/tooltip.md +1 -1
- package/public/manifest/components.json +1045 -86
- package/scripts/lib/agent-docs.mjs +22 -1
- package/scripts/lib/mcp-tools.mjs +2 -14
- package/scripts/lib/props-validator.mjs +36 -2
- package/scripts/lib/search-score.mjs +24 -0
- package/skills/division-ds/SKILL.md +2 -2
- package/skills/division-ds/topics/forms.md +1 -1
- package/division-ds-0.2.1.tgz +0 -0
|
@@ -31,8 +31,29 @@ export function alsoExports(entry) {
|
|
|
31
31
|
.filter(name => /^[A-Z][a-z]/.test(name) && !name.startsWith(entry.name) && !name.endsWith('Provider'))
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** The docs site's top-bar search reads this, fetched the first time search opens. Slim on purpose:
|
|
35
|
+
* the names, the one-line summaries and the prop names and descriptions the shared scorer
|
|
36
|
+
* (search-score.mjs) ranks on, and the screen-facing tokens. Primitives are left out, as in the
|
|
37
|
+
* MCP search: they are never the answer for a screen. */
|
|
38
|
+
export function searchIndex(entries, tokens) {
|
|
39
|
+
const index = {
|
|
40
|
+
generatedBy: 'scripts/build-agent-docs.mjs',
|
|
41
|
+
components: entries.map(entry => ({
|
|
42
|
+
name: entry.name,
|
|
43
|
+
slug: entry.slug,
|
|
44
|
+
tier: entry.tier,
|
|
45
|
+
summary: entry.summary,
|
|
46
|
+
useWhen: entry.useWhen,
|
|
47
|
+
also: alsoExports(entry),
|
|
48
|
+
props: entry.props.map(prop => ({ name: prop.name, description: prop.description ?? '' })),
|
|
49
|
+
})),
|
|
50
|
+
tokens: tokens.tokens.filter(token => token.layer !== 'primitive').map(token => ({ name: token.name, layer: token.layer, use: token.use ?? '' })),
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(index) + '\n'
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
/** Said under every Import block: the paths are the repository's, not an installed project's. */
|
|
35
|
-
export const INSTALLED_PATHS = 'Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.'
|
|
56
|
+
export const INSTALLED_PATHS = 'Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file. An install has no index barrels: import each component from its own file, never from `components/molecules`.'
|
|
36
57
|
|
|
37
58
|
/** One component as a markdown page. `entry` is a manifest entry (see `manifestEntry`). */
|
|
38
59
|
export function componentMarkdown(entry) {
|
|
@@ -9,6 +9,7 @@ import { alsoExports, INSTALLED_PATHS } from './agent-docs.mjs'
|
|
|
9
9
|
import { checkText } from './check-text.mjs'
|
|
10
10
|
import { contrastRatio } from './contrast.mjs'
|
|
11
11
|
import { buildIndex, closest } from './props-validator.mjs'
|
|
12
|
+
import { scorer, words } from './search-score.mjs'
|
|
12
13
|
|
|
13
14
|
export const REGISTRY_BASE = 'https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r'
|
|
14
15
|
const TIERS = ['atoms', 'molecules', 'organisms']
|
|
@@ -16,7 +17,6 @@ const LAYERS = ['primitive', 'spacing', 'motion', 'semantic', 'component']
|
|
|
16
17
|
// WCAG 2.2 minimums: SC 1.4.3 for text, SC 1.4.11 for the boundary of a control or a graphic.
|
|
17
18
|
const MINIMUMS = { normal: 4.5, large: 3, ui: 3 }
|
|
18
19
|
|
|
19
|
-
const words = text => String(text).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
20
20
|
|
|
21
21
|
/** `context`: { manifest, tokens, version, readDoc(slug) -> string|undefined, readChecklist() -> string|undefined } */
|
|
22
22
|
export function createTools(context) {
|
|
@@ -122,19 +122,7 @@ export function createTools(context) {
|
|
|
122
122
|
run({ query, limit = 10 }) {
|
|
123
123
|
const terms = words(query)
|
|
124
124
|
if (terms.length === 0) throw new ToolError('query has no words to search for.')
|
|
125
|
-
|
|
126
|
-
// prop table does not outrank a component or token whose own summary is about the query.
|
|
127
|
-
const score = (title, body, props = '', propNames = '') => {
|
|
128
|
-
const t = words(title).join(' ')
|
|
129
|
-
const b = words(body)
|
|
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)
|
|
137
|
-
}
|
|
125
|
+
const score = scorer(query)
|
|
138
126
|
const hits = [
|
|
139
127
|
// The title holds every name on the page (ChatMessage and ResponseFeedback); the body adds
|
|
140
128
|
// the prop descriptions, where what a part does is written ("helpful", "thumbs up").
|
|
@@ -156,7 +156,14 @@ export function buildIndex(manifest) {
|
|
|
156
156
|
const single = exportNames.length === 1
|
|
157
157
|
for (const name of exportNames) {
|
|
158
158
|
const spec = specs[name]
|
|
159
|
-
|
|
159
|
+
// On a page that documents several components, the rows with no part tag are the main
|
|
160
|
+
// component's own. The main one is checked too when nothing passes through undocumented:
|
|
161
|
+
// its type is closed (manifest `closed`), or its only pass-through is native attributes,
|
|
162
|
+
// which the check already allows. Its parts stay unchecked: their rows list only what
|
|
163
|
+
// differs from the main component.
|
|
164
|
+
const main = name === entry.name && (entry.closed === true || spec.rest?.open === 'native')
|
|
165
|
+
spec.checkUnknown = (single || main) && !unresolved && spec.rest?.open !== 'any'
|
|
166
|
+
spec.namedByFormField = (entry.namedByFormField ?? []).includes(name)
|
|
160
167
|
index.set(name, spec)
|
|
161
168
|
}
|
|
162
169
|
}
|
|
@@ -228,7 +235,7 @@ export function scriptKind(path) {
|
|
|
228
235
|
|
|
229
236
|
/**
|
|
230
237
|
* Every props violation in one file: `{ path, line, column, rule, component, prop, found, fix }`.
|
|
231
|
-
* `rule` is 'unknown-prop'
|
|
238
|
+
* `rule` is 'unknown-prop', 'invalid-value' or 'missing-label' (a control FormField names, with no label and no FormField around it). `index` comes from `buildIndex`.
|
|
232
239
|
*/
|
|
233
240
|
export function findPropViolations(text, path, index) {
|
|
234
241
|
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path))
|
|
@@ -254,11 +261,38 @@ export function findPropViolations(text, path, index) {
|
|
|
254
261
|
return undefined
|
|
255
262
|
}
|
|
256
263
|
|
|
264
|
+
// Inside a Division FormField, which names the control it holds.
|
|
265
|
+
const insideFormField = element => {
|
|
266
|
+
for (let node = element.parent; node; node = node.parent) {
|
|
267
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : ts.isJsxSelfClosingElement(node) ? node : null
|
|
268
|
+
if (opening && opening !== element && resolve(opening.tagName) === 'FormField') return true
|
|
269
|
+
}
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
|
|
257
273
|
const check = element => {
|
|
258
274
|
const exportName = resolve(element.tagName)
|
|
259
275
|
const spec = exportName && index.get(exportName)
|
|
260
276
|
if (!spec) return
|
|
261
277
|
const tag = element.tagName.getText(source)
|
|
278
|
+
const attributes = element.attributes.properties
|
|
279
|
+
if (
|
|
280
|
+
spec.namedByFormField &&
|
|
281
|
+
!attributes.some(a => ts.isJsxSpreadAttribute(a) || a.name.getText(source) === 'label') &&
|
|
282
|
+
!insideFormField(element)
|
|
283
|
+
) {
|
|
284
|
+
const at = source.getLineAndCharacterOfPosition(element.getStart(source))
|
|
285
|
+
violations.push({
|
|
286
|
+
path,
|
|
287
|
+
line: at.line + 1,
|
|
288
|
+
column: at.character + 1,
|
|
289
|
+
rule: 'missing-label',
|
|
290
|
+
component: tag,
|
|
291
|
+
prop: 'label',
|
|
292
|
+
found: `<${tag}>`,
|
|
293
|
+
fix: `${tag} has no name: give it a label, or put it in a FormField, which names it. See ${spec.docs}.`,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
262
296
|
const known = [...spec.props.keys()]
|
|
263
297
|
for (const attribute of element.attributes.properties) {
|
|
264
298
|
if (!ts.isJsxAttribute(attribute)) continue
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The one ranking for "find me the thing for X". The MCP server's `search` tool and the docs site's
|
|
2
|
+
// top-bar search both call it, so an agent and a person typing the same words get the same order.
|
|
3
|
+
// Pure and dependency-free: it runs in Node and in the browser.
|
|
4
|
+
|
|
5
|
+
export const words = text => String(text).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
6
|
+
|
|
7
|
+
/** A scorer for one query. `title` is the names, `body` the summary; `props` (descriptions) and
|
|
8
|
+
* `propNames` count once each, as hints, so a long prop table does not outrank a component or
|
|
9
|
+
* token whose own summary is about the query. 0 means no match. */
|
|
10
|
+
export function scorer(query) {
|
|
11
|
+
const terms = words(query)
|
|
12
|
+
return (title, body, props = '', propNames = '') => {
|
|
13
|
+
if (terms.length === 0) return 0
|
|
14
|
+
const t = words(title).join(' ')
|
|
15
|
+
const b = words(body)
|
|
16
|
+
const p = words(props)
|
|
17
|
+
const n = words(propNames)
|
|
18
|
+
const inMain = term => t.includes(term) || b.some(w => w.startsWith(term))
|
|
19
|
+
// Every word of the query in the name or summary beats one word found often: "page
|
|
20
|
+
// background" is the surface token, not PageHeader, whose name matches "page" alone.
|
|
21
|
+
const all = terms.length > 1 && terms.every(inMain) ? 5 : 0
|
|
22
|
+
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)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: division-ds
|
|
3
|
-
description: Use when building or changing UI with Division DS (React 19, Tailwind 4, Radix;
|
|
3
|
+
description: Use when building or changing UI with Division DS (React 19, Tailwind 4, Radix; 72 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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Division DS
|
|
@@ -82,7 +82,7 @@ provider. A project that uses the components mounts both itself (step 4 of `inst
|
|
|
82
82
|
`src/demo/states/` files are not part of the system.
|
|
83
83
|
|
|
84
84
|
**What the system does not have.** No Popover, no Combobox (single value: use `Select`; several:
|
|
85
|
-
`MultiSelect`)
|
|
85
|
+
`MultiSelect`). Do not reach into `src/demo/` from a component.
|
|
86
86
|
|
|
87
87
|
**Vocabulary.** Inside this repository, `npm run check:vocabulary` bans a list of words in `src/`,
|
|
88
88
|
`scripts/` and `e2e/` (the list is `scripts/lib/vocabulary.mjs`; it is not published with the
|
|
@@ -11,7 +11,7 @@ renders `ErrorSummary` at the top and focuses it; `useFieldErrors` holds the sta
|
|
|
11
11
|
`public/docs/helpers.md`).
|
|
12
12
|
|
|
13
13
|
A `RadioGroup` in a form sits in a `<fieldset>` whose `<legend>` names it (code on the Radio
|
|
14
|
-
page). A date is
|
|
14
|
+
page). A date is `DatePicker` in a `FormField`; its value is 'YYYY-MM-DD'.
|
|
15
15
|
|
|
16
16
|
**Buttons.** Two or more buttons side by side need a parent with a gap
|
|
17
17
|
(`flex flex-wrap gap-3`); `DialogFooter` and `CardFooter` already are one. A `Button` is
|
package/division-ds-0.2.1.tgz
DELETED
|
Binary file
|