@designtools/blocks 0.0.0-stage → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +121 -2
  4. package/dist/cli.js +385 -0
  5. package/package.json +61 -3
  6. package/reference/README.md +20 -0
  7. package/reference/app/(design)/design/system/(docs)/brand/imagery/page.tsx +15 -0
  8. package/reference/app/(design)/design/system/(docs)/brand/logo/page.tsx +15 -0
  9. package/reference/app/(design)/design/system/(docs)/brand/page.tsx +16 -0
  10. package/reference/app/(design)/design/system/(docs)/brand/voice/page.tsx +15 -0
  11. package/reference/app/(design)/design/system/(docs)/colour/page.tsx +26 -0
  12. package/reference/app/(design)/design/system/(docs)/components/[slug]/page.tsx +86 -0
  13. package/reference/app/(design)/design/system/(docs)/layout.tsx +35 -0
  14. package/reference/app/(design)/design/system/(docs)/page.tsx +48 -0
  15. package/reference/app/(design)/design/system/(docs)/patterns/[id]/page.tsx +23 -0
  16. package/reference/app/(design)/design/system/(docs)/rules/page.tsx +42 -0
  17. package/reference/app/(design)/design/system/(docs)/scale/page.tsx +15 -0
  18. package/reference/app/(design)/design/system/(docs)/standards/page.tsx +21 -0
  19. package/reference/app/(design)/design/system/(docs)/type/page.tsx +15 -0
  20. package/reference/app/(design)/design/system/(docs)/vocabulary/page.tsx +15 -0
  21. package/reference/app/(design)/design/system/_components/copy-page.tsx +32 -0
  22. package/reference/app/(design)/design/system/_components/prose.tsx +26 -0
  23. package/reference/app/(design)/design/system/_data.ts +53 -0
  24. package/reference/app/(design)/design/system/manifest/[file]/route.ts +15 -0
  25. package/reference/app/(design)/design/system/md/[[...path]]/route.ts +94 -0
  26. package/reference/app/(design)/design/system/preview/[slug]/[example]/page.tsx +23 -0
  27. package/reference/app/(design)/layout.tsx +16 -0
  28. package/reference/app/globals.css +14 -0
  29. package/reference/app/layout.tsx +16 -0
  30. package/reference/designtools.json +14 -0
  31. package/reference/package.json +38 -0
  32. package/reference/proxy.ts +55 -0
  33. package/registry/a11y-panel.tsx +99 -0
  34. package/registry/adherence-summary.tsx +110 -0
  35. package/registry/agent-view.tsx +58 -0
  36. package/registry/anatomy.tsx +89 -0
  37. package/registry/ask-claude.tsx +73 -0
  38. package/registry/code-view.tsx +77 -0
  39. package/registry/examples.tsx +97 -0
  40. package/registry/glossary.tsx +59 -0
  41. package/registry/imagery.tsx +39 -0
  42. package/registry/lib/adherence-types.ts +181 -0
  43. package/registry/lib/boundary.tsx +21 -0
  44. package/registry/lib/contrast.ts +44 -0
  45. package/registry/lib/cx.ts +12 -0
  46. package/registry/lib/jsx.ts +29 -0
  47. package/registry/lib/lookup.tsx +46 -0
  48. package/registry/lib/manifest-types.ts +284 -0
  49. package/registry/lib/manifest.ts +228 -0
  50. package/registry/lib/markdown.ts +288 -0
  51. package/registry/lib/search.ts +99 -0
  52. package/registry/lib/standards.ts +82 -0
  53. package/registry/lib/status.tsx +32 -0
  54. package/registry/lib/text.tsx +27 -0
  55. package/registry/logo-usage.tsx +112 -0
  56. package/registry/pattern.tsx +69 -0
  57. package/registry/playground.tsx +181 -0
  58. package/registry/preview-frame.tsx +106 -0
  59. package/registry/props-table.tsx +110 -0
  60. package/registry/registry.generated.tsx +9 -0
  61. package/registry/rule.tsx +99 -0
  62. package/registry/scale.tsx +115 -0
  63. package/registry/search.tsx +134 -0
  64. package/registry/shell.tsx +247 -0
  65. package/registry/standards.tsx +41 -0
  66. package/registry/swatches.tsx +225 -0
  67. package/registry/tsconfig.json +13 -0
  68. package/registry/type-ramp.tsx +105 -0
  69. package/registry/usage.tsx +73 -0
  70. package/registry/variant-matrix.tsx +105 -0
  71. package/registry/voice-terms.tsx +45 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # @designtools/blocks
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3da728f: From the first end-to-end run:
8
+
9
+ - Component markdown imports through the alias the manifest found (`@ds/button/button`), so the line an agent copies resolves; `importOf` gives the same specifier to the blocks.
10
+ - `componentMarkdown` and `patternMarkdown` take a heading level: `1` for a page of its own, so it no longer opens with the same heading twice. Do and don't examples come out in pairs, as the page shows them.
11
+ - Every pressable thing in the blocks is at least 44 × 44 CSS px, and a project sets `--docs-target` for its own rule (`3rem` for 48px).
12
+ - A component's parts fold under it in the navigation (`groupFamilies`), instead of each taking a line.
13
+ - The standards block titles its sections and rows, shows notes as sentences, leaves out values nobody has set, and titles thresholds beyond the schema from their keys; its markdown matches.
14
+ - The logo's minimum-size sample is painted on the logo's own surface, so a reversed logo is visible.
15
+ - The demo's docs app ships in `reference/`: `proxy.ts`, the data module, the markdown, manifest and preview routes, and every page.
16
+ - `add` defaults to `app/(design)/design/system/_blocks`, beside the docs in the `(design)` route group, unless the docs are already in `app/design/system`.
17
+ - `search` (and the shell, by default): find a component, part, rule, pattern or page by name, fuzzily, or by a wrong name from the taxonomy, on ⌘K, Ctrl+K or `/`. Base UI's Autocomplete in a Dialog, ranked by `fuzzysort`; the manifest is the index. Pass `terms` to the shell for the wrong names.
18
+ - The shell takes the client's `logo`. Editorial headers take a `reviewer` as well as an author.
19
+ - Accessibility, from the second run: scrolling tables and code are keyboard-reachable regions; no text below `text-xs` (the props table's "required" and the ramp labels were 10px); do and don't labels and a failing contrast grade use measured subdued pairs, not fill colours as text; rule anchors and "instead" links meet the target size.
20
+ - The logo page paints surfaces in their light-mode colours whatever the docs' mode, lists rules for every logo once above them, and drops the faded surface label.
21
+ - `reference/` gates the docs in production in every format (the `(design)` layout and `proxy.ts`), shows parts on their component's overview tile, puts the logo in the shell, and pins its packages exactly.
22
+ - `PageContrast` in the swatches block: each colour on each background (canvas, surface, overlay and every named `--surface-*`), per mode, measured on what the browser paints and marked as fine for text, as a shape, or neither.
23
+ - The status badge shows `measured` (a rule generated from the tokens) apart from confirmed and assumed ones.
24
+
25
+ ## 0.1.0
26
+
27
+ ### Minor Changes
28
+
29
+ - First release. Twenty-three accessible blocks for one branded site holding a brand and its design system, copied into the project with `designtools-blocks add`: shell, logo usage, imagery, voice and terms, swatches, type ramp, scale, rule, pattern, usage, examples, variant matrix, props table, anatomy, playground, code view, accessibility panel, preview frame, standards, glossary, adherence summary, Ask Claude and agent view. Each reads the `@designtools/manifest` JSON, renders the project's real components and assets, and is styled only with the semantic tokens `@designtools/tokens` generates. Every block has a markdown form, with typed fences for rules, examples, do and don't.
30
+
31
+ `designtools-blocks map` writes the registry of components, examples, patterns and assets the client blocks render from, and `status` and `show` compare a project's copies with the current version.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Made by Many Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @designtools/blocks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Accessible React blocks for one branded site that holds a client's brand and design system together, for the people who make the product and the agents that build it with them: logo usage, imagery, voice and terms, swatches, type, scale, rules, patterns, usage, examples, a variant matrix, props, anatomy, a playground, code, accessibility, a preview frame, standards, a glossary and more. They are copied into your repo, not installed, so they are yours to edit, and they carry no look of their own: they are styled with your tokens, so the site looks like the brand.
4
+
5
+ Each block reads the JSON [`@designtools/manifest`](../manifest) writes and renders your real components and assets. Nothing is restated or drawn by hand, so the site cannot drift from the code. Every block also has a markdown form, so an agent gets the same page as text.
6
+
7
+ ```bash
8
+ npx @designtools/blocks add swatches type-ramp scale shell # the token pages
9
+ npx @designtools/blocks add --all # everything
10
+ npx @designtools/blocks map # after each manifest build
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ The blocks expect a Next app on the suite stack: React, Tailwind v4 over tokens from `@designtools/tokens`, and Base UI. In `designtools.json`:
16
+
17
+ ```json
18
+ {
19
+ "manifest": { "system": "src/ds", "tokens": ["app/styles/tokens.css", "app/styles/scale.css"] },
20
+ "blocks": { "dir": "app/(design)/design/system/_blocks" }
21
+ }
22
+ ```
23
+
24
+ `add` records `blocks.dir` the first time if you have not, defaulting to `app/(design)/design/system/_blocks`, beside the docs in the `(design)` route group (or under `src/app` when the project has one; `app/design/system/_blocks` when the docs already live outside the group). The leading underscore keeps the folder out of Next's routing.
25
+
26
+ 1. `designtools-manifest build` writes `components.json` and `tokens.json`.
27
+ 2. `designtools-blocks add <block…>` copies the blocks, the shared files they import, and nothing else. It says which packages they need that the project lacks: `@base-ui/react` for the shell, search and playground, `fuzzysort` (MIT) for search, `shiki` (MIT) for the code view, `axe-core` for the accessibility panel. Pin them exactly. `axe-core` is MPL-2.0: check it against the project's licence list before adding `a11y-panel`, and leave the panel out if the list will not take it; nothing else needs it.
28
+ 3. `designtools-blocks map` writes `registry.generated.tsx`: every component, examples module, pattern and brand asset in the manifest, imported by path. Run it after each manifest build; `map --check` exits 1 when it is stale, for a pre-commit hook.
29
+
30
+ Blocks that render components are client components, and a server page cannot pass a component to one. So a page passes the manifest entry, which is plain data, and the block finds the component in the generated map.
31
+
32
+ ## Pages
33
+
34
+ A component page is a server component that reads the manifest and composes blocks:
35
+
36
+ ```tsx
37
+ import components from "@ds/manifest/components.json";
38
+ import { findBySlug, readComponents } from "../../_blocks/lib/manifest";
39
+ import { DocsHeader, DocsSection } from "../../_blocks/shell";
40
+ import { Playground } from "../../_blocks/playground";
41
+ import { PropsTable } from "../../_blocks/props-table";
42
+ import { Examples } from "../../_blocks/examples";
43
+ import { Usage } from "../../_blocks/usage";
44
+ import { VariantMatrix } from "../../_blocks/variant-matrix";
45
+
46
+ const manifest = readComponents(components);
47
+
48
+ export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
49
+ const entry = findBySlug(manifest, (await params).slug)!;
50
+ return (
51
+ <>
52
+ <DocsHeader title={entry.name} lead={entry.description} source={entry.source} />
53
+ <DocsSection title="Playground"><Playground entry={entry} /></DocsSection>
54
+ <DocsSection title="Variants"><VariantMatrix entry={entry} /></DocsSection>
55
+ <DocsSection title="Usage"><Usage entry={entry} manifest={manifest} /></DocsSection>
56
+ <DocsSection title="Examples"><Examples entry={entry} previewHref={`/design/system/preview/${entry.name.toLowerCase()}`} /></DocsSection>
57
+ <DocsSection title="Props"><PropsTable entry={entry} /></DocsSection>
58
+ </>
59
+ );
60
+ }
61
+ ```
62
+
63
+ `readComponents` and `readTokens` type a JSON import and check its schema version. The full site ships with the package, in `reference/` (`node_modules/@designtools/blocks/reference/`): brand, logo, imagery, voice, colour, type, scale, rules, a pattern, a component page using every component block, standards and vocabulary, the preview route, the markdown and manifest routes, and `proxy.ts`. It is a copy of the designtools repo's `examples/demo`, taken at build, so it matches this version of the blocks. Read it; do not import from it.
64
+
65
+ ## Blocks
66
+
67
+ | Block | Shows | Reads |
68
+ | --- | --- | --- |
69
+ | `shell` | `DocsShell`: the client's `logo` and title, search, and navigation in the site's order (brand, foundations, rules, patterns, components by `@category` with their parts folded under them, standards). `DocsHeader` with the provenance of a generated page, or the author, reviewer and review date of an editorial one. `DocsSection` | `components.json`, `rules.json`, `patterns.json`, `taxonomy.json` |
70
+ | `search` | `DocsSearch`: find a component, part, rule, pattern or page by name, fuzzily (`buton` finds Button), or by a wrong name from the taxonomy (`dropdown` finds Select, and says so). ⌘K, Ctrl+K or `/` opens it; arrows and Enter go. Navigation only, not page text: the manifest is the index, so there is nothing to build. The shell includes it; `search={false}` leaves it out | the same, through `searchIndex` in `lib/search` |
71
+ | `logo-usage` | Each logo on its surfaces, at minimum size, with clear space drawn. Surfaces are painted in their light-mode colours whatever the docs' mode, because a brand book's surfaces are fixed. Rules for every logo once, above them; a rule naming one logo, under it | `assets.json`, `rules.json`, the map |
72
+ | `imagery` | The approved icons and imagery with their usage notes | `assets.json`, the map |
73
+ | `voice-terms` | Preferred terms beside their wrong forms | `taxonomy.json` |
74
+ | `swatches` | Each semantic colour in light and dark side by side, contrast measured on the painted colours; every colour on every background (`PageContrast`); the ramps | `tokens.json` |
75
+ | `type-ramp` | Each `--text-*` size with its leading and tracking, then families, weights, leading, tracking | `tokens.json` |
76
+ | `scale` | Spacing, size, radius, shadow, duration and easing, drawn the way each is used | `tokens.json` |
77
+ | `rule` | A rule for people, and as an agent reads it, with its check and a slot for a demonstration | `rules.json` |
78
+ | `pattern` | Each composition live, with its code and the components it uses | `patterns.json`, the map |
79
+ | `usage` | Status, when to use it, when not, and what instead | an entry |
80
+ | `examples` | The canonical example, then each do beside its don't, with code | an entry, the map |
81
+ | `variant-matrix` | The first two axes as a grid, each further axis as a row | an entry, the map |
82
+ | `props-table` | Own props first, a wrapped primitive's folded away, deprecations struck through | an entry |
83
+ | `anatomy` | The `data-slot` parts; pointing at one outlines it in the canonical example | an entry, the map |
84
+ | `playground` | The component live, controls from its axes and boolean props, and the JSX for what is showing | an entry, the map |
85
+ | `code-view` | Highlighted code with copy; Shiki loads on first use | code |
86
+ | `a11y-panel` | axe-core over what it wraps, on demand | the DOM |
87
+ | `preview-frame` | `PreviewFrame`: a preview route at 375, 768 and 1280 wide, light or dark. `PreviewSurface`, `PreviewExample` and `PreviewComposition` for the route itself | the map |
88
+ | `standards` | Performance budgets, accessibility target and support baseline | `.mxa/stack.json` baselines |
89
+ | `glossary` | Every term, searchable by its wrong forms, so a wrong name finds the right one | `taxonomy.json` |
90
+ | `ask-claude` | Copies a prompt naming the component, its file, the variant and the example | an entry |
91
+ | `agent-view` | Any block's markdown form, copyable, in a tab beside the rendered one | an entry, or markdown |
92
+
93
+
94
+ Every pressable thing in the blocks (navigation, tabs, buttons, summaries, the playground's controls) is at least 44 by 44 CSS pixels, WCAG 2.5.5's size. A client whose own rule is larger sets `--docs-target` on the docs layout (`style={{ "--docs-target": "3rem" }}` for 48px), so the site that states the rule meets it. A component's parts (`CardHeader`, `CardTitle`) are folded under it in the navigation and open when you are on one of them.
95
+
96
+ ## Markdown
97
+
98
+ `lib/markdown.ts` holds the markdown form of every block, generated from the manifest rather than converted from HTML: `componentMarkdown`, `coloursMarkdown`, `typeMarkdown`, `scaleMarkdown`, `ruleMarkdown`, `patternMarkdown`, `glossaryMarkdown`, `assetsMarkdown`, `standardsMarkdown`, and `systemMarkdown` for everything in one file. Structure survives as typed fences (```` ```rule ````, ```` ```example ````, ```` ```do ````, ```` ```dont ````), and status travels with everything. `generatedHeader` opens a generated page with its version and commit; `editorialHeader` opens a written one with its author and review date. Pass `1` as the second argument to `componentMarkdown` or `patternMarkdown` when it is the whole page, so the page has one heading; inside `systemMarkdown` they sit at level 2. Do and don't examples come out in pairs, as the page shows them, and the import line uses the alias the manifest found (`@ds/button/button`).
99
+
100
+ Serving it is per-app wiring, so it is copied rather than imported: `reference/proxy.ts` answers `Accept: text/markdown` and `.md` with these, sets `Vary: Accept` and the canonical `Link`, and returns a markdown 404 for a page that does not exist.
101
+
102
+ ## Styling
103
+
104
+ Blocks use only the semantic utilities every `@designtools/tokens` palette defines (`bg-surface`, `text-muted-foreground`, `border-border`, `bg-primary`, …) and the scale's sizes. To restyle a block, edit your copy. There is no theming API and no plugin system.
105
+
106
+ A mode panel switches mode with `data-theme` on the element that paints, because the generated stylesheet redefines the `--color-*` bridge per mode: a whole cell in `data-theme="dark"` would put dark-mode text on a light page.
107
+
108
+ If the blocks folder is gitignored, Tailwind skips it when scanning for classes. Commit the blocks (they are your code), or add `@source "./design/system/_blocks";` to the stylesheet.
109
+
110
+ ## Upgrading
111
+
112
+ Copies never update themselves. Each starts with a header naming the version it came from:
113
+
114
+ ```
115
+ // @designtools/blocks 0.1.0 · swatches · yours to edit; designtools-blocks status compares it with the registry
116
+ ```
117
+
118
+ `designtools-blocks status` reports each copy as the same as this version, edited here, or from an older version. `designtools-blocks show <file>` prints this version, to compare with your copy and carry your edits across. `add --force` replaces a copy outright.
119
+
120
+ ## Licence
121
+
122
+ [MIT](LICENSE), © Made by Many Ltd.
package/dist/cli.js ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "fs";
5
+ import { basename, dirname as dirname2, join as join2, relative, resolve, sep } from "path";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/registry.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { dirname, join, normalize } from "path";
11
+ import { fileURLToPath } from "url";
12
+ var REGISTRY_DIR = join(dirname(fileURLToPath(import.meta.url)), "../registry");
13
+ var GENERATED = "registry.generated.tsx";
14
+ var BLOCKS = [
15
+ { name: "shell", file: "shell.tsx", description: "Docs frame: brand, foundations, rules, patterns, components, standards; page header and sections" },
16
+ { name: "search", file: "search.tsx", description: "Fuzzy search over components, parts, rules, patterns and pages, wrong names included; \u2318K, Ctrl+K or /" },
17
+ { name: "logo-usage", file: "logo-usage.tsx", description: "Each logo on its surfaces, at minimum size, with clear space and its brand rules" },
18
+ { name: "imagery", file: "imagery.tsx", description: "The approved icons and imagery with their usage notes" },
19
+ { name: "voice-terms", file: "voice-terms.tsx", description: "Preferred terms beside their wrong forms" },
20
+ { name: "swatches", file: "swatches.tsx", description: "Semantic colour pairs per mode with measured contrast, and the primitive ramps" },
21
+ { name: "type-ramp", file: "type-ramp.tsx", description: "Type scale with leading and tracking pairs, families, weights" },
22
+ { name: "scale", file: "scale.tsx", description: "Space, size, radius, elevation and motion tiers" },
23
+ { name: "rule", file: "rule.tsx", description: "A rule for people, and as an agent reads it, with its check and room for a demonstration" },
24
+ { name: "pattern", file: "pattern.tsx", description: "A screen composition, live, with the components it uses" },
25
+ { name: "usage", file: "usage.tsx", description: "When to use a component, when not, what instead, and its status" },
26
+ { name: "examples", file: "examples.tsx", description: "The canonical example, then do and don't pairs side by side" },
27
+ { name: "variant-matrix", file: "variant-matrix.tsx", description: "Every variant combination, rendered live" },
28
+ { name: "props-table", file: "props-table.tsx", description: "Props, types, defaults, required; a wrapped primitive's folded away" },
29
+ { name: "anatomy", file: "anatomy.tsx", description: "The component's data-slot parts, outlined on hover" },
30
+ { name: "playground", file: "playground.tsx", description: "The component live, with controls from its variants and flags, and its JSX" },
31
+ { name: "code-view", file: "code-view.tsx", description: "Highlighted code with copy; Shiki loads on first use" },
32
+ { name: "a11y-panel", file: "a11y-panel.tsx", description: "axe-core over a preview, on demand" },
33
+ { name: "preview-frame", file: "preview-frame.tsx", description: "A preview route in an iframe at set widths and either mode" },
34
+ { name: "standards", file: "standards.tsx", description: "Performance budgets, accessibility target and support baseline" },
35
+ { name: "glossary", file: "glossary.tsx", description: "The taxonomy, searchable, so a wrong name finds the right one" },
36
+ { name: "adherence-summary", file: "adherence-summary.tsx", description: "The latest @designtools/adherence report: share, off-system values, overrides, by route" },
37
+ { name: "ask-claude", file: "ask-claude.tsx", description: "Copies a prompt naming the component, file, variant and example" },
38
+ { name: "agent-view", file: "agent-view.tsx", description: "Any block's markdown form, copyable, in a tab beside the rendered one" }
39
+ ];
40
+ var ASSUMED = /* @__PURE__ */ new Set(["react", "react-dom"]);
41
+ var IMPORT = /^[ \t]*(?:import|export)\s[^"'`;]*?from\s+["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']\s*\)/gm;
42
+ function importsOf(source) {
43
+ return [...source.matchAll(IMPORT)].map((m) => m[1] ?? m[2]);
44
+ }
45
+ function resolveBlocks(names) {
46
+ const files = /* @__PURE__ */ new Set();
47
+ const packages = /* @__PURE__ */ new Set();
48
+ let needsMap = false;
49
+ const queue = names.map((n) => {
50
+ const block = BLOCKS.find((b) => b.name === n);
51
+ if (!block) throw new Error(`unknown block "${n}". Run designtools-blocks list to see them.`);
52
+ return block.file;
53
+ });
54
+ while (queue.length) {
55
+ const file = queue.shift();
56
+ if (files.has(file)) continue;
57
+ files.add(file);
58
+ for (const spec of importsOf(readFileSync(join(REGISTRY_DIR, file), "utf8"))) {
59
+ if (!spec.startsWith(".")) {
60
+ const pkg = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
61
+ if (!ASSUMED.has(pkg)) packages.add(pkg);
62
+ continue;
63
+ }
64
+ const target = registryFile(normalize(join(dirname(file), spec)));
65
+ if (target === GENERATED) needsMap = true;
66
+ else queue.push(target);
67
+ }
68
+ }
69
+ return { files: [...files].sort(), packages: [...packages].sort(), needsMap };
70
+ }
71
+ function registryFile(path) {
72
+ const clean = path.split("\\").join("/");
73
+ for (const ext of ["", ".tsx", ".ts"]) {
74
+ if (existsSync(join(REGISTRY_DIR, clean + ext)) && /\.(tsx?)$/.test(clean + ext)) return clean + ext;
75
+ }
76
+ throw new Error(`registry import not found: ${clean}`);
77
+ }
78
+
79
+ // src/map.ts
80
+ function generateMap(manifest, toRoot, manifestPath2) {
81
+ const spec = (source, keepExtension = false) => {
82
+ const path = `${toRoot || "."}/${keepExtension ? source : source.replace(/\.tsx?$/, "")}`;
83
+ return path.startsWith(".") ? path : `./${path}`;
84
+ };
85
+ const lines = [
86
+ `"use client";`,
87
+ `// Generated by designtools-blocks map from ${manifestPath2}. Do not edit: run designtools-blocks map again.`,
88
+ `import type { ComponentType } from "react";`
89
+ ];
90
+ const components = [];
91
+ const examples = [];
92
+ const patterns = [];
93
+ const assets = [];
94
+ const seen = /* @__PURE__ */ new Set();
95
+ manifest.components.components.forEach((c, i) => {
96
+ lines.push(`import { ${c.export} as C${i} } from ${JSON.stringify(spec(c.source))};`);
97
+ components.push(` ${JSON.stringify(`${c.source}#${c.export}`)}: C${i},`);
98
+ if (c.examples && !seen.has(c.examples.source)) {
99
+ seen.add(c.examples.source);
100
+ const name = `E${examples.length}`;
101
+ lines.push(`import * as ${name} from ${JSON.stringify(spec(c.examples.source))};`);
102
+ examples.push(` ${JSON.stringify(c.examples.source)}: ${name},`);
103
+ }
104
+ });
105
+ (manifest.patterns?.patterns ?? []).forEach((p, i) => {
106
+ lines.push(`import * as P${i} from ${JSON.stringify(spec(p.source))};`);
107
+ patterns.push(` ${JSON.stringify(p.source)}: P${i},`);
108
+ });
109
+ (manifest.assets?.assets ?? []).forEach((a, i) => {
110
+ lines.push(`import A${i} from ${JSON.stringify(spec(a.file, true))};`);
111
+ assets.push(` ${JSON.stringify(a.file)}: A${i},`);
112
+ });
113
+ const block = (head, rows) => [head, ...rows, "};", ""];
114
+ return [
115
+ ...lines,
116
+ "",
117
+ "// eslint-disable-next-line @typescript-eslint/no-explicit-any",
118
+ ...block("export const components: Record<string, ComponentType<any>> = {", components),
119
+ ...block("export const examples: Record<string, Record<string, unknown>> = {", examples),
120
+ ...block("export const patterns: Record<string, Record<string, unknown>> = {", patterns),
121
+ ...block("export const assets: Record<string, string | { src: string }> = {", assets)
122
+ ].join("\n");
123
+ }
124
+
125
+ // src/cli.ts
126
+ var VERSION = JSON.parse(readFileSync2(join2(REGISTRY_DIR, "../package.json"), "utf8")).version;
127
+ var CONFIG_FILE = "designtools.json";
128
+ var HEADER = /^\/\/ @designtools\/blocks (\S+) · .*\r?\n/;
129
+ var HELP = `designtools-blocks ${VERSION}
130
+
131
+ Unstyled, accessible docs blocks for a React design system, copied into your
132
+ repo to own and edit. Each reads the @designtools/manifest JSON and renders
133
+ your real components.
134
+
135
+ Usage
136
+ designtools-blocks list List the blocks
137
+ designtools-blocks add <block\u2026> Copy blocks, and the files they need, into the project
138
+ designtools-blocks add --all Copy every block
139
+ designtools-blocks map Write registry.generated.tsx from the manifest
140
+ designtools-blocks map --check Exit 1 if registry.generated.tsx is out of date
141
+ designtools-blocks status Compare the project's copies with this version
142
+ designtools-blocks show <file> Print this version of a block file, e.g. swatches.tsx
143
+
144
+ Options
145
+ --dir <dir> Where blocks live (default: blocks.dir in designtools.json,
146
+ else app/(design)/design/system/_blocks, or src/app/\u2026 when
147
+ src/app exists; app/design/system/_blocks if the docs are there)
148
+ --manifest <file> components.json (default: from manifest.system in designtools.json)
149
+ --root <dir> Project root (default: the current directory)
150
+ --force add: overwrite files that already exist
151
+ -h, --help Show this help
152
+ -v, --version Show the version
153
+ `;
154
+ function fail(message, code = 2) {
155
+ process.stderr.write(`designtools-blocks: ${message}
156
+ `);
157
+ process.exit(code);
158
+ }
159
+ var parsed;
160
+ function parse() {
161
+ return parseArgs({
162
+ allowPositionals: true,
163
+ options: {
164
+ dir: { type: "string" },
165
+ manifest: { type: "string" },
166
+ root: { type: "string" },
167
+ force: { type: "boolean" },
168
+ all: { type: "boolean" },
169
+ check: { type: "boolean" },
170
+ help: { type: "boolean", short: "h" },
171
+ version: { type: "boolean", short: "v" }
172
+ }
173
+ });
174
+ }
175
+ try {
176
+ parsed = parse();
177
+ } catch (e) {
178
+ fail(`${e.message}
179
+
180
+ ${HELP}`);
181
+ }
182
+ var { values, positionals } = parsed;
183
+ if (values.help) {
184
+ process.stdout.write(HELP);
185
+ process.exit(0);
186
+ }
187
+ if (values.version) {
188
+ process.stdout.write(`${VERSION}
189
+ `);
190
+ process.exit(0);
191
+ }
192
+ var [command, ...rest] = positionals;
193
+ function real(path) {
194
+ if (existsSync2(path)) return realpathSync(path);
195
+ const parent = dirname2(path);
196
+ return parent === path ? path : join2(real(parent), basename(path));
197
+ }
198
+ var root = real(resolve(values.root ?? "."));
199
+ function readConfig() {
200
+ const path = join2(root, CONFIG_FILE);
201
+ if (!existsSync2(path)) return {};
202
+ try {
203
+ return JSON.parse(readFileSync2(path, "utf8"));
204
+ } catch (e) {
205
+ fail(`${CONFIG_FILE} is not valid JSON: ${e.message}`);
206
+ }
207
+ }
208
+ function blocksDir(config) {
209
+ if (values.dir) return values.dir;
210
+ if (config.blocks?.dir) return config.blocks.dir;
211
+ const app = existsSync2(join2(root, "src/app")) ? "src/app" : "app";
212
+ if (existsSync2(join2(root, app, "design/system")) && !existsSync2(join2(root, app, "(design)"))) return `${app}/design/system/_blocks`;
213
+ return `${app}/(design)/design/system/_blocks`;
214
+ }
215
+ function manifestPath(config) {
216
+ if (values.manifest) return values.manifest;
217
+ const m = config.manifest;
218
+ if (!m?.system && !m?.out) {
219
+ fail(`no manifest. Pass --manifest <components.json>, or set manifest.system in ${CONFIG_FILE}.`);
220
+ }
221
+ return `${(m.out ?? `${m.system.replace(/\/+$/, "")}/manifest`).replace(/\/+$/, "")}/components.json`;
222
+ }
223
+ var posix = (p) => p.split(sep).join("/");
224
+ var abs = (p) => real(resolve(root, p));
225
+ var shown = (p) => posix(relative(root, abs(p))) || ".";
226
+ var lf = (text) => text.replace(/\r\n/g, "\n");
227
+ switch (command) {
228
+ case "list":
229
+ list();
230
+ break;
231
+ case "add":
232
+ add();
233
+ break;
234
+ case "map":
235
+ map();
236
+ break;
237
+ case "status":
238
+ status();
239
+ break;
240
+ case "show":
241
+ show();
242
+ break;
243
+ default:
244
+ fail(command ? `unknown command "${command}"
245
+
246
+ ${HELP}` : `a command is required
247
+
248
+ ${HELP}`);
249
+ }
250
+ function list() {
251
+ const width = Math.max(...BLOCKS.map((b) => b.name.length));
252
+ for (const b of BLOCKS) process.stdout.write(`${b.name.padEnd(width)} ${b.description}
253
+ `);
254
+ }
255
+ function add() {
256
+ const names = values.all ? BLOCKS.map((b) => b.name) : rest;
257
+ if (names.length === 0) fail("name the blocks to add, or pass --all. Run designtools-blocks list to see them.");
258
+ let resolved;
259
+ try {
260
+ resolved = resolveBlocks(names);
261
+ } catch (e) {
262
+ fail(e.message);
263
+ }
264
+ const config = readConfig();
265
+ const dir = blocksDir(config);
266
+ const written = [];
267
+ const kept = [];
268
+ for (const file of resolved.files) {
269
+ const target = join2(abs(dir), file);
270
+ if (existsSync2(target) && !values.force) {
271
+ kept.push(file);
272
+ continue;
273
+ }
274
+ mkdirSync(dirname2(target), { recursive: true });
275
+ writeFileSync(target, withHeader(file, readFileSync2(join2(REGISTRY_DIR, file), "utf8")));
276
+ written.push(file);
277
+ }
278
+ if (!config.blocks?.dir) {
279
+ const next = { ...config, blocks: { ...config.blocks, dir: shown(dir) } };
280
+ writeFileSync(join2(root, CONFIG_FILE), JSON.stringify(next, null, 2) + "\n");
281
+ process.stderr.write(`recorded blocks.dir = ${shown(dir)} in ${CONFIG_FILE}
282
+ `);
283
+ }
284
+ for (const f of written) process.stderr.write(`added ${dir}/${f}
285
+ `);
286
+ for (const f of kept) process.stderr.write(`kept ${dir}/${f} (exists; --force to replace, or designtools-blocks status to compare)
287
+ `);
288
+ const pkg = existsSync2(join2(root, "package.json")) ? JSON.parse(readFileSync2(join2(root, "package.json"), "utf8")) : {};
289
+ const installed = { ...pkg.dependencies, ...pkg.devDependencies };
290
+ const missing = resolved.packages.filter((p) => !(p in installed));
291
+ if (missing.length) process.stderr.write(`
292
+ these blocks need: ${missing.join(" ")}
293
+ pnpm add ${missing.join(" ")}
294
+ `);
295
+ if (resolved.needsMap) {
296
+ const path = config.manifest?.system || config.manifest?.out || values.manifest ? manifestPath(config) : void 0;
297
+ if (path && existsSync2(abs(path))) writeMap(dir, path);
298
+ else process.stderr.write(`
299
+ then build the manifest and run designtools-blocks map, which writes ${dir}/${GENERATED}
300
+ `);
301
+ }
302
+ }
303
+ function withHeader(file, source) {
304
+ const block = BLOCKS.find((b) => b.file === file)?.name ?? file;
305
+ return `// @designtools/blocks ${VERSION} \xB7 ${block} \xB7 yours to edit; designtools-blocks status compares it with the registry
306
+ ${source}`;
307
+ }
308
+ function map() {
309
+ const config = readConfig();
310
+ const dir = blocksDir(config);
311
+ const path = manifestPath(config);
312
+ if (!existsSync2(abs(path))) fail(`${path} not found. Run designtools-manifest build first.`);
313
+ if (values.check) {
314
+ const target = join2(abs(dir), GENERATED);
315
+ const expected = generate(dir, path);
316
+ const ok = existsSync2(target) && lf(readFileSync2(target, "utf8")) === expected;
317
+ process.stderr.write(ok ? `map: ${dir}/${GENERATED} up to date
318
+ ` : `map: ${dir}/${GENERATED} is out of date. Run designtools-blocks map.
319
+ `);
320
+ process.exit(ok ? 0 : 1);
321
+ }
322
+ writeMap(dir, path);
323
+ }
324
+ function generate(dir, manifest) {
325
+ const json = JSON.parse(readFileSync2(abs(manifest), "utf8"));
326
+ const sibling = (name) => {
327
+ const path = join2(dirname2(abs(manifest)), name);
328
+ return existsSync2(path) ? JSON.parse(readFileSync2(path, "utf8")) : void 0;
329
+ };
330
+ return generateMap(
331
+ { components: json, patterns: sibling("patterns.json"), assets: sibling("assets.json") },
332
+ posix(relative(abs(dir), sourceRoot(abs(manifest), json.system))),
333
+ shown(manifest)
334
+ );
335
+ }
336
+ function sourceRoot(manifest, system) {
337
+ const path = posix(manifest);
338
+ const tail = system ? `/${system.replace(/^\.?\/+|\/+$/g, "")}/manifest/components.json` : void 0;
339
+ return tail && path.endsWith(tail) ? path.slice(0, -tail.length) || "/" : root;
340
+ }
341
+ function writeMap(dir, manifest) {
342
+ const target = join2(abs(dir), GENERATED);
343
+ mkdirSync(dirname2(target), { recursive: true });
344
+ writeFileSync(target, generate(dir, manifest));
345
+ process.stderr.write(`wrote ${shown(dir)}/${GENERATED} from ${shown(manifest)}
346
+ `);
347
+ }
348
+ function status() {
349
+ const dir = blocksDir(readConfig());
350
+ let found = 0;
351
+ const all = resolveBlocks(BLOCKS.map((b) => b.name)).files;
352
+ for (const file of all) {
353
+ const target = join2(abs(dir), file);
354
+ if (!existsSync2(target)) continue;
355
+ found++;
356
+ const copy = readFileSync2(target, "utf8");
357
+ const header = HEADER.exec(copy);
358
+ const body = lf(header ? copy.slice(header[0].length) : copy);
359
+ const current = readFileSync2(join2(REGISTRY_DIR, file), "utf8");
360
+ const from = header?.[1] ?? "unknown";
361
+ const state = body === current ? "same as the registry" : from === VERSION ? "edited here" : `differs: copied from ${from}, registry is ${VERSION} (may include your edits; designtools-blocks show ${file})`;
362
+ process.stdout.write(`${file.padEnd(24)} ${state}
363
+ `);
364
+ }
365
+ const known = /* @__PURE__ */ new Set([...all, GENERATED]);
366
+ const walk = (folder, prefix) => existsSync2(folder) ? readdirSync(folder, { withFileTypes: true }).flatMap(
367
+ (d) => d.isDirectory() ? walk(join2(folder, d.name), `${prefix}${d.name}/`) : [`${prefix}${d.name}`]
368
+ ) : [];
369
+ for (const file of walk(abs(dir), "").sort()) {
370
+ if (known.has(file) || !/\.tsx?$/.test(file)) continue;
371
+ if (!HEADER.test(readFileSync2(join2(abs(dir), file), "utf8"))) continue;
372
+ found++;
373
+ process.stdout.write(`${file.padEnd(24)} not in ${VERSION}: removed or renamed in the registry; delete it once nothing imports it
374
+ `);
375
+ }
376
+ if (!found) process.stdout.write(`no blocks in ${dir}
377
+ `);
378
+ }
379
+ function show() {
380
+ const [file] = rest;
381
+ if (!file) fail("name a registry file, e.g. designtools-blocks show swatches.tsx");
382
+ const path = join2(REGISTRY_DIR, file);
383
+ if (!path.startsWith(REGISTRY_DIR) || !existsSync2(path) || file === GENERATED) fail(`no registry file ${file}`);
384
+ process.stdout.write(readFileSync2(path, "utf8"));
385
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@designtools/blocks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Unstyled, accessible React blocks for design system docs, copied into your repo: swatches, type ramp, scale, variant matrix, state grid, props table, anatomy, playground, code view, accessibility panel, preview frame. Each reads the @designtools/manifest JSON and renders your real components.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Made by Many",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/madebymany/designtools.git",
11
+ "directory": "packages/blocks"
12
+ },
13
+ "homepage": "https://github.com/madebymany/designtools/tree/main/packages/blocks#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/madebymany/designtools/issues"
16
+ },
17
+ "bin": {
18
+ "designtools-blocks": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "registry",
23
+ "reference",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "keywords": [
32
+ "design-system",
33
+ "documentation",
34
+ "react",
35
+ "base-ui",
36
+ "tailwind",
37
+ "docs",
38
+ "swatches",
39
+ "props-table",
40
+ "playground"
41
+ ],
42
+ "devDependencies": {
43
+ "@base-ui/react": "1.6.0",
44
+ "@types/node": "^24.19.1",
45
+ "@types/react": "^19.3.0",
46
+ "@types/react-dom": "^19.3.0",
47
+ "axe-core": "^4.14.0",
48
+ "fuzzysort": "4.0.2",
49
+ "next": "16.3.0",
50
+ "react": "19.2.7",
51
+ "react-dom": "19.2.7",
52
+ "shiki": "^4.5.0",
53
+ "tsup": "^8.5.1",
54
+ "tsx": "^4.23.15",
55
+ "typescript": "~6.0.3",
56
+ "vitest": "^4.1.11"
57
+ },
58
+ "scripts": {
59
+ "build": "node scripts/reference.mjs && tsup src/cli.ts --format esm --clean",
60
+ "typecheck": "tsc --noEmit && tsc --noEmit -p registry",
61
+ "test": "vitest run",
62
+ "cli": "tsx src/cli.ts"
63
+ }
6
64
  }
@@ -0,0 +1,20 @@
1
+ # Reference: a docs site wired end to end
2
+
3
+ A copy of `examples/demo` in the designtools repo, taken when this version of
4
+ `@designtools/blocks` was built: everything a project writes per app, around the
5
+ blocks it copies. Read it; do not import from it.
6
+
7
+ | File | What it shows |
8
+ | --- | --- |
9
+ | `proxy.ts` | Markdown on `Accept: text/markdown` or `.md`, `Vary: Accept`, the alternate `Link`, and a 404 for every docs URL on production, route handlers included |
10
+ | `app/(design)/layout.tsx` | The 404 and `noindex` for every design page on production |
11
+ | `app/(design)/design/system/_data.ts` | One data module that the pages and the markdown route both read |
12
+ | `app/(design)/design/system/md/[[...path]]/route.ts` | Every page as markdown, with the canonical `Link` and a markdown 404 |
13
+ | `app/(design)/design/system/manifest/[file]/route.ts` | The manifest served as JSON |
14
+ | `app/(design)/design/system/preview/[slug]/[example]/page.tsx` | One example alone, for the preview frame and visual regression |
15
+ | `app/(design)/design/system/(docs)/` | The pages, composing the blocks: brand, foundations, rules, patterns, components, standards, vocabulary |
16
+ | `package.json`, `designtools.json`, `app/globals.css` | The setup scripts, the config, and the `@source` line Tailwind needs for the blocks |
17
+
18
+ Paths match a project that keeps its system in `src/ds` and its docs in the
19
+ `(design)` route group. Blocks are imported relatively from `_blocks`, where
20
+ `designtools-blocks add` puts them.