canon-ds 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/bin/canon.js +6 -0
- package/lib/build-manifest.mjs +54 -0
- package/lib/build.mjs +69 -0
- package/lib/cli.mjs +273 -0
- package/lib/color.js +160 -0
- package/lib/components/_shared.mjs +93 -0
- package/lib/components/accordion.mjs +136 -0
- package/lib/components/account-card.mjs +166 -0
- package/lib/components/activity-feed.mjs +176 -0
- package/lib/components/activity-gauge.mjs +98 -0
- package/lib/components/agent-presence.mjs +156 -0
- package/lib/components/alert.mjs +154 -0
- package/lib/components/app-store-button.mjs +108 -0
- package/lib/components/avatar-group.mjs +128 -0
- package/lib/components/avatar.mjs +225 -0
- package/lib/components/badge-group.mjs +55 -0
- package/lib/components/badge.mjs +174 -0
- package/lib/components/banner.mjs +116 -0
- package/lib/components/breadcrumb.mjs +170 -0
- package/lib/components/button-group.mjs +114 -0
- package/lib/components/button.mjs +197 -0
- package/lib/components/card-header.mjs +115 -0
- package/lib/components/card.mjs +106 -0
- package/lib/components/carousel.mjs +182 -0
- package/lib/components/chart-frame.mjs +101 -0
- package/lib/components/checkbox.mjs +163 -0
- package/lib/components/close-button.mjs +49 -0
- package/lib/components/code.mjs +123 -0
- package/lib/components/color-picker.mjs +122 -0
- package/lib/components/combobox.mjs +210 -0
- package/lib/components/command-palette.mjs +226 -0
- package/lib/components/content-divider.mjs +104 -0
- package/lib/components/counter.mjs +86 -0
- package/lib/components/credit-card.mjs +126 -0
- package/lib/components/date-picker.mjs +202 -0
- package/lib/components/description-list.mjs +88 -0
- package/lib/components/dialog.mjs +140 -0
- package/lib/components/divider.mjs +115 -0
- package/lib/components/drawer.mjs +150 -0
- package/lib/components/empty-state.mjs +112 -0
- package/lib/components/featured-card.mjs +124 -0
- package/lib/components/featured-icon.mjs +67 -0
- package/lib/components/field.mjs +122 -0
- package/lib/components/file-dropzone.mjs +166 -0
- package/lib/components/filter-bar.mjs +179 -0
- package/lib/components/header-navigation.mjs +94 -0
- package/lib/components/icon-button.mjs +179 -0
- package/lib/components/index.mjs +52 -0
- package/lib/components/inline-cta.mjs +139 -0
- package/lib/components/input-group.mjs +164 -0
- package/lib/components/input.mjs +197 -0
- package/lib/components/kanban.mjs +179 -0
- package/lib/components/kbd.mjs +70 -0
- package/lib/components/kicker.mjs +79 -0
- package/lib/components/link.mjs +108 -0
- package/lib/components/list.mjs +117 -0
- package/lib/components/media-frame.mjs +119 -0
- package/lib/components/menu.mjs +190 -0
- package/lib/components/message.mjs +249 -0
- package/lib/components/metric-group.mjs +210 -0
- package/lib/components/mobile-header.mjs +173 -0
- package/lib/components/multi-select.mjs +218 -0
- package/lib/components/notification.mjs +175 -0
- package/lib/components/number-input.mjs +149 -0
- package/lib/components/page-header.mjs +68 -0
- package/lib/components/pagination.mjs +236 -0
- package/lib/components/pin-input.mjs +120 -0
- package/lib/components/popover.mjs +126 -0
- package/lib/components/progress-circle.mjs +115 -0
- package/lib/components/progress.mjs +93 -0
- package/lib/components/prose.mjs +96 -0
- package/lib/components/radio.mjs +160 -0
- package/lib/components/rating.mjs +93 -0
- package/lib/components/rich-text-editor.mjs +178 -0
- package/lib/components/section-header.mjs +62 -0
- package/lib/components/segmented-control.mjs +121 -0
- package/lib/components/select.mjs +163 -0
- package/lib/components/sidebar-nav.mjs +172 -0
- package/lib/components/sidebar.mjs +124 -0
- package/lib/components/skeleton.mjs +66 -0
- package/lib/components/slider.mjs +122 -0
- package/lib/components/social-button.mjs +119 -0
- package/lib/components/spinner.mjs +69 -0
- package/lib/components/stat.mjs +120 -0
- package/lib/components/stepper.mjs +228 -0
- package/lib/components/switch.mjs +159 -0
- package/lib/components/table.mjs +212 -0
- package/lib/components/tabs.mjs +207 -0
- package/lib/components/tag.mjs +196 -0
- package/lib/components/tags-input.mjs +146 -0
- package/lib/components/textarea.mjs +117 -0
- package/lib/components/timeline.mjs +135 -0
- package/lib/components/toast.mjs +115 -0
- package/lib/components/tooltip.mjs +86 -0
- package/lib/components/topbar.mjs +207 -0
- package/lib/components/tree-view.mjs +156 -0
- package/lib/components/video-player.mjs +189 -0
- package/lib/connect.mjs +110 -0
- package/lib/design-files.mjs +175 -0
- package/lib/distribution.mjs +109 -0
- package/lib/editor.js +940 -0
- package/lib/engine.js +339 -0
- package/lib/generators/agents.mjs +68 -0
- package/lib/generators/connection.md +58 -0
- package/lib/generators/css.mjs +2 -0
- package/lib/generators/designmd.mjs +324 -0
- package/lib/generators/documentation.css +82 -0
- package/lib/generators/documentation.html +81 -0
- package/lib/generators/documentation.js +96 -0
- package/lib/generators/documentation.mjs +35 -0
- package/lib/generators/dtcg.mjs +60 -0
- package/lib/generators/preview.css +279 -0
- package/lib/generators/preview.mjs +225 -0
- package/lib/generators/react.mjs +162 -0
- package/lib/generators/tailwind.mjs +114 -0
- package/lib/install.mjs +108 -0
- package/lib/lint.mjs +538 -0
- package/lib/mcp.mjs +268 -0
- package/lib/open.mjs +7 -0
- package/lib/patterns/_app.mjs +56 -0
- package/lib/patterns/app-shell.mjs +25 -0
- package/lib/patterns/audit-log.mjs +37 -0
- package/lib/patterns/auth-pages.mjs +44 -0
- package/lib/patterns/billing-page.mjs +39 -0
- package/lib/patterns/calendar-page.mjs +55 -0
- package/lib/patterns/dashboard-page.mjs +28 -0
- package/lib/patterns/files-page.mjs +30 -0
- package/lib/patterns/first-run.mjs +31 -0
- package/lib/patterns/form-layout.mjs +24 -0
- package/lib/patterns/inbox-page.mjs +58 -0
- package/lib/patterns/index.mjs +29 -0
- package/lib/patterns/list-detail-page.mjs +24 -0
- package/lib/patterns/mobile-app.mjs +46 -0
- package/lib/patterns/modal-flows.mjs +32 -0
- package/lib/patterns/onboarding-page.mjs +42 -0
- package/lib/patterns/permissions-page.mjs +38 -0
- package/lib/patterns/profile-page.mjs +32 -0
- package/lib/patterns/settings-page.mjs +24 -0
- package/lib/project.mjs +28 -0
- package/lib/serve.mjs +279 -0
- package/lib/system.mjs +210 -0
- package/lib/tokens/base.js +251 -0
- package/lib/tokens/canon-preset.mjs +67 -0
- package/lib/tokens/index.mjs +1 -0
- package/lib/tokens/legacy-canon.json +559 -0
- package/lib/tokens/presets.mjs +161 -0
- package/lib/tokens/resolve.mjs +1 -0
- package/lib/types.mjs +4 -0
- package/lib/version.mjs +2 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Canon contributors
|
|
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
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Canon
|
|
2
|
+
|
|
3
|
+
**Connect Canon to your agent. Shape the design in Studio.**
|
|
4
|
+
|
|
5
|
+
Canon is an open-source design system for apps built with coding agents. It provides shared tokens, components, page layouts, a visual Studio and focused instructions that keep interface work consistent.
|
|
6
|
+
|
|
7
|
+
[Documentation and connection prompt](https://marcosdocanto.github.io/canon/) · [Explore the Studio](https://marcosdocanto.github.io/canon/preview.html) · [Contribute](CONTRIBUTING.md)
|
|
8
|
+
|
|
9
|
+
## Connect your agent
|
|
10
|
+
|
|
11
|
+
Paste this into the Claude or Codex conversation for a new or existing project:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Use Canon as this project's design system.
|
|
15
|
+
Read and follow: https://marcosdocanto.github.io/canon/CONNECT.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The agent installs Canon, imports the design, connects its generated CSS to the existing app and opens the project's Studio. You can customize the design before building the first screen or at any point later. Reconnecting preserves existing Canon definitions and their configured path. You do not need to describe the app again.
|
|
19
|
+
|
|
20
|
+
The public site is a catalog. Your editable Studio runs locally, and your design files stay in the project. The application uses generated CSS through its own frontend and deployment stack; Canon requires no hosted service at runtime. Node.js **22.18.0 or newer** is needed for Canon's build tools and Studio.
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary>npm installation reference</summary>
|
|
24
|
+
|
|
25
|
+
For an npm release, the agent can install the package directly:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install --save-dev canon-ds
|
|
29
|
+
npx canon connect https://marcosdocanto.github.io/canon/CONNECT.md
|
|
30
|
+
npx canon studio --port 0 --open
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use the project's existing package manager. The connection procedure also supports the site's downloadable package when a registry release is unavailable, retaining that archive in the project for future installs.
|
|
34
|
+
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
## Use Studio
|
|
38
|
+
|
|
39
|
+
Explore the public component catalog and page layouts. In the Studio opened by your agent, use **Customize → Style** to change the visual direction, or click a component with the editor open to adjust its parts in **Inspect**. Choose an example and compare desktop/mobile layouts at Fit or 100%.
|
|
40
|
+
|
|
41
|
+
In a bound Studio, **Save** atomically updates the design source, generated CSS and installed agent references. MCP reads the new build on its next data request, so there is no sync or reload step to ask for. The app reflects CSS changes through its framework's normal reload or build. Use **Undo** to revert an edit and save again when needed.
|
|
42
|
+
|
|
43
|
+
Screen layout, routes and application behavior remain agent work; Save does not rewrite or deploy the app. For design-source edits made outside Studio, run `canon sync`.
|
|
44
|
+
|
|
45
|
+
## Contribute
|
|
46
|
+
|
|
47
|
+
Contributions to components, accessibility, performance, Studio, agent integrations and documentation are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), [component authoring](docs/AUTHORING.md) or [pattern authoring](docs/PATTERNS.md). Use an issue to report a reproducible problem or discuss a larger change, then open a focused pull request.
|
|
48
|
+
|
|
49
|
+
Canon is distributed under the [MIT license](LICENSE). You can use it in personal, commercial and closed-source applications, subject to the license terms. [Third-party notices](THIRD_PARTY_NOTICES.md) cover the included icon references and external assets. Maintainers follow the [release guide](docs/RELEASING.md).
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary>Technical reference for agents and contributors</summary>
|
|
53
|
+
|
|
54
|
+
## How it stays consistent
|
|
55
|
+
|
|
56
|
+
1. **Shared definitions.** `design/tokens.json`, component specs and patterns generate CSS, docs, React and the gallery from the same source. `src/engine.js` runs in Node and in the browser editor.
|
|
57
|
+
2. **Focused agent context.** Start with `DESIGN.compact.md` and MCP's `design_rules`. Discover pieces with `search`, `list_components` or `list_patterns`; fetch only the required specs with `get_component` and `get_pattern`. Without MCP, read the relevant source JSON and sections of `DESIGN.md`.
|
|
58
|
+
3. **Installed references.** `canon connect` builds and installs managed blocks in `AGENTS.md` and `CLAUDE.md`, the Claude Code skill, Cursor rule, MCP configuration and optional lint hook, then records the design path in `.canon/project.json`. `--no-hooks` skips adding the hook; it does not remove an existing one.
|
|
59
|
+
4. **Code checks.** `canon lint` detects supported violations such as raw style values, Tailwind palette classes, unknown Canon classes and invalid prop values. `canon check` verifies a complete build, compiler version, source content and generated file hashes, then runs lint. Application tests and browser review cover the remaining delivery requirements.
|
|
60
|
+
|
|
61
|
+
## Generated files
|
|
62
|
+
|
|
63
|
+
With the default output directory, these files live in `design/dist/`:
|
|
64
|
+
|
|
65
|
+
| File | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `<prefix>.css` | Complete token, reset, component and pattern styles, ready for a prototype. |
|
|
68
|
+
| `tokens.css`, `base.css`, `components.css` | The same styles as separate layers. |
|
|
69
|
+
| `css/components/<slug>.css`, `css/patterns/<slug>.css` | Individual styles for explicit imports of the pieces used by a product. |
|
|
70
|
+
| `tailwind.theme.css`, `tailwind.preset.cjs` | Tailwind v4 and v3 themes mapped to the tokens. |
|
|
71
|
+
| `react/` | Typed React wrappers for components and their parts. |
|
|
72
|
+
| `tokens.dtcg.json` | Tokens in DTCG format. |
|
|
73
|
+
| `preview.html`, `docs.html` | Studio and the user guide. |
|
|
74
|
+
| `CONNECT.md` | Canon's setup procedure for agents connecting to a new or existing project. |
|
|
75
|
+
| `DESIGN.compact.md`, `DESIGN.md` | Focused agent reference and the complete specification. |
|
|
76
|
+
| `agents/` | Instructions, skill, rules and MCP configuration for installation. |
|
|
77
|
+
| `canon.lock.json` | The build manifest verified by `canon check`. |
|
|
78
|
+
|
|
79
|
+
For a product, the agent can import `tokens.css`, `base.css` and the individual component and pattern styles actually used, including components referenced by each pattern. This is an explicit integration choice; the agent should measure the resulting route rather than assume unused styles are automatically removed.
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
82
|
+
|
|
83
|
+
These are reference commands for the agent. Run them in the application repository and use `--design` consistently if the source lives outside `design/`.
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
canon connect <Studio URL|/full/snapshot.json> --root <project-root>
|
|
87
|
+
[--design <existing-custom-path>] [--no-hooks]
|
|
88
|
+
canon init <name> [--preset canon|editorial|vera|clean|dark] [--prefix app]
|
|
89
|
+
[--brand '#B4309F'] [--font 'DM Sans'] [--design design]
|
|
90
|
+
canon build [--design design] [--only react,preview]
|
|
91
|
+
canon install [--design design] [--root .] [--no-hooks]
|
|
92
|
+
canon sync [--design design] [--root .] [--no-hooks]
|
|
93
|
+
canon studio --root <project-root> [--design design] [--port 0] [--open]
|
|
94
|
+
canon serve # alias for studio
|
|
95
|
+
canon lint [paths…] [--design design] [--json] [--changed]
|
|
96
|
+
canon check [--design design] [--root .]
|
|
97
|
+
canon mcp [--design design]
|
|
98
|
+
canon add <component-slug> [--design design]
|
|
99
|
+
canon doctor [--design design]
|
|
100
|
+
canon presets
|
|
101
|
+
canon --help
|
|
102
|
+
canon --version
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`connect` imports the saved snapshot from `./api/system` beside the connection document, or from a full local JSON path, only when the project has no configured design. Public sites hosted under a path such as `/canon/` retain that path. Otherwise the command reuses the source recorded in `.canon/project.json` without fetching or resetting it. For a legacy installation, discover its source and pass `--design`; never use `init` or a preset as fallback for a failed import.
|
|
106
|
+
|
|
107
|
+
`init` requires a name and builds automatically. Use `sync` after editing an existing source outside Studio; `init --force` intentionally resets its definitions. `add` imports a missing catalogue component. `hook` accepts the Claude Code edit event on stdin and is normally configured by `install`.
|
|
108
|
+
|
|
109
|
+
`canon mcp` runs over stdio and is started by the agent client. It refreshes data on the request after a successful build. Agents read the current installed references at the beginning of UI work. `canon lint src app` checks those paths; use the application's actual directories and confirm the report covers the changed files.
|
|
110
|
+
|
|
111
|
+
A build with `--only` selects optional generators while still generating CSS. Run a complete `canon build` before `canon check`. In CI, check versioned outputs directly; if outputs are generated in CI, build before checking.
|
|
112
|
+
|
|
113
|
+
## Local CLI reference
|
|
114
|
+
|
|
115
|
+
The compiler and CLI have zero runtime dependencies and require Node.js **22.18.0 or newer**. A source checkout uses Node's native TypeScript support; packaged versions run compiled JavaScript.
|
|
116
|
+
|
|
117
|
+
The Studio serves the connection procedure at `./CONNECT.md`, the saved snapshot at `./api/system` and an installable package at `./canon-package.tgz`. The public website exposes those files too. Registry releases use `canon-ds`; a retained archive also supports installations without registry access. A localhost URL works for an agent on the same computer. If the agent runs elsewhere, use the public URL or an address it can reach.
|
|
118
|
+
|
|
119
|
+
From a source checkout, invoke `node bin/canon.js` directly, or create a short command once:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm link
|
|
123
|
+
canon --version
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Then continue in the application's repository. Replace the checkout path when needed.
|
|
127
|
+
|
|
128
|
+
Presets are `canon` (default), `editorial`, `vera`, `clean` and `dark`. `canon presets` describes their visual directions. Seed options include `--brand`, `--action`, `--font`, `--radius`, `--base`, `--control`, `--canvas`, `--ink`, `--mono`, `--display`, `--shadow-tint`, `--neutral-hue`, `--neutral-chroma` and `--theme`.
|
|
129
|
+
|
|
130
|
+
Explicit seeds replace corresponding preset defaults. Deliberate token overrides live in `seeds.overrides`; defaults live in `seeds.presetOverrides`. Studio preserves hand-edited token values when changing unrelated seeds.
|
|
131
|
+
|
|
132
|
+
## What is in the catalog
|
|
133
|
+
|
|
134
|
+
- **Components** (`src/components/*.ts`, 90): actions (buttons, icon buttons, groups, close, social/app-store), forms (fields, inputs and groups, textarea, select, combobox, multi-select, checkbox, radio, switch, slider, segmented control, pin/number/tags inputs, date picker, color picker, file dropzone, rich text editor), navigation (sidebar family, topbar, header navigation, mobile header, tabs, breadcrumb, pagination, menu, command palette, stepper, page and section headers), data display (badge, badge group, tag, counter, avatar and groups, table, list, description list, stat/metrics, accordion, timeline, kanban, tree view, chart frame, rating, activity feed, messages, account card), feedback (alert, banner, toast, notification, progress bar and circle, spinner, skeleton, empty state, inline CTA, featured card), overlays (tooltip, popover, dialog, drawer), media (featured icon, media frame, carousel, video player, credit card), typography (kicker, prose, link, code, kbd).
|
|
135
|
+
- **Patterns** (`src/patterns/*.ts`): application layouts and pages only — app shell, form layout, dashboard, settings, list with detail drawer, inbox / chat / notification center, onboarding wizard, profile, files, calendar, dialog and drawer flows, phone-width screens, first-run and empty states, billing and checkout, audit log, roles and permissions, auth and error pages — each with 2–6 layout variants, rendered at desktop and 375px in the gallery.
|
|
136
|
+
- **Presets**: `canon` (default), `editorial`, `vera`, `clean`, `dark`.
|
|
137
|
+
|
|
138
|
+
## Authoring
|
|
139
|
+
|
|
140
|
+
Components live in `src/components/*.ts` as data (see `docs/AUTHORING.md`). Patterns (layouts, sections, pages) in `src/patterns/*.ts`. `canon init` copies them into a project's `design/` as JSON where they can be edited by hand or in the editor; `canon add <slug>` pulls a new catalog component into an existing project.
|
|
141
|
+
|
|
142
|
+
## Tests
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm ci
|
|
146
|
+
npm test
|
|
147
|
+
npx playwright install chromium
|
|
148
|
+
npm run test:browser
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`npm run test:ci` typechecks the source and runs both suites. `npm run build` prepares the compiled `lib/` runtime; `npm pack` runs it automatically. The packaging tests install the tarball in a temporary consumer and exercise the CLI without runtime dependencies.
|
|
152
|
+
|
|
153
|
+
</details>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Canon includes inline outline icons in its examples and Studio. Lucide is the recommended product icon set; the applicable Lucide and Feather notices are retained below.
|
|
4
|
+
|
|
5
|
+
Source: https://github.com/lucide-icons/lucide/blob/main/LICENSE
|
|
6
|
+
|
|
7
|
+
ISC License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
12
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
13
|
+
copyright notice and this permission notice appear in all copies.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
16
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
18
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
19
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
20
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
21
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
The following Lucide icons are derived from the Feather project:
|
|
26
|
+
|
|
27
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
28
|
+
|
|
29
|
+
The MIT License (MIT) (for the icons listed above)
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2013-present Cole Bemis
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
|
50
|
+
|
|
51
|
+
## Fonts and brand examples
|
|
52
|
+
|
|
53
|
+
The Studio requests selected fonts from Google Fonts. Font binaries are not bundled in the npm package; self-hosted copies retain their respective font licenses.
|
|
54
|
+
|
|
55
|
+
Company names and logo representations in example components identify those companies. They are not covered by a grant of trademark rights from Canon and do not imply endorsement.
|
package/bin/canon.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// A checkout runs current source; npm ships only the compiled runtime.
|
|
5
|
+
const source = new URL('../src/cli.ts', import.meta.url);
|
|
6
|
+
await import(existsSync(source) ? source.href : new URL('../lib/cli.mjs', import.meta.url).href);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { VERSION } from "./version.mjs";
|
|
5
|
+
export const BUILD_GENERATORS = ['css', 'tailwind', 'dtcg', 'react', 'preview', 'designmd', 'agents'];
|
|
6
|
+
export const contentHash = (content) => createHash('sha256').update(content).digest('hex');
|
|
7
|
+
export const sourceHash = (system) => contentHash(JSON.stringify(system));
|
|
8
|
+
/** Manifest paths are portable, output-relative file names, never filesystem paths. */
|
|
9
|
+
export function outputPath(outDir, rel) {
|
|
10
|
+
const path = resolve(outDir, rel);
|
|
11
|
+
const within = relative(resolve(outDir), path);
|
|
12
|
+
if (!rel || rel.includes('\\') || isAbsolute(rel) || within === '' || within === '..' || within.startsWith(`..${sep}`) || isAbsolute(within)) {
|
|
13
|
+
throw new Error(`Invalid generated file path: ${rel}`);
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
export function checkBuild(system, designDir) {
|
|
18
|
+
const outDir = join(designDir, system.meta.out || 'dist');
|
|
19
|
+
let manifest;
|
|
20
|
+
try {
|
|
21
|
+
manifest = JSON.parse(readFileSync(join(outDir, 'canon.lock.json'), 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const missing = error.code === 'ENOENT';
|
|
25
|
+
return { ok: false, issues: [missing ? 'dist not built (missing build manifest).' : 'Build manifest cannot be read.'] };
|
|
26
|
+
}
|
|
27
|
+
if (!manifest || manifest.manifestVersion !== 1 || !Array.isArray(manifest.generators)
|
|
28
|
+
|| !manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)
|
|
29
|
+
|| Object.keys(manifest.files).length === 0) {
|
|
30
|
+
return { ok: false, issues: ['Build manifest is incomplete or uses an older format.'] };
|
|
31
|
+
}
|
|
32
|
+
const issues = [];
|
|
33
|
+
if (manifest.canon !== VERSION)
|
|
34
|
+
issues.push(`Compiler version changed (built with ${manifest.canon}, current ${VERSION}).`);
|
|
35
|
+
if (manifest.sourceHash !== sourceHash(system))
|
|
36
|
+
issues.push('dist is stale (design source changed since last build).');
|
|
37
|
+
const missing = BUILD_GENERATORS.filter((name) => !manifest.generators.includes(name));
|
|
38
|
+
if (missing.length)
|
|
39
|
+
issues.push(`Build is incomplete; missing generators: ${missing.join(', ')}.`);
|
|
40
|
+
for (const [rel, expected] of Object.entries(manifest.files)) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof expected !== 'string' || !/^[a-f0-9]{64}$/.test(expected))
|
|
43
|
+
throw new Error('invalid content hash');
|
|
44
|
+
const actual = contentHash(readFileSync(outputPath(outDir, rel)));
|
|
45
|
+
if (actual !== expected)
|
|
46
|
+
issues.push(`Generated file changed: ${rel}.`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const code = error.code;
|
|
50
|
+
issues.push(code === 'ENOENT' ? `Generated file missing: ${rel}.` : `Generated file cannot be verified: ${rel}.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { ok: issues.length === 0, issues };
|
|
54
|
+
}
|
package/lib/build.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { indexTokens } from "./tokens/resolve.mjs";
|
|
4
|
+
import { componentCss, fullCss, patternsCss } from "./generators/css.mjs";
|
|
5
|
+
import { validateSystem } from "./system.mjs";
|
|
6
|
+
import { VERSION } from "./version.mjs";
|
|
7
|
+
import { contentHash, outputPath, sourceHash } from "./build-manifest.mjs";
|
|
8
|
+
const optional = [
|
|
9
|
+
{ name: 'tailwind', load: () => import("./generators/tailwind.mjs") },
|
|
10
|
+
{ name: 'dtcg', load: () => import("./generators/dtcg.mjs") },
|
|
11
|
+
{ name: 'react', load: () => import("./generators/react.mjs") },
|
|
12
|
+
{ name: 'preview', load: () => import("./generators/preview.mjs") },
|
|
13
|
+
{ name: 'designmd', load: () => import("./generators/designmd.mjs") },
|
|
14
|
+
{ name: 'agents', load: () => import("./generators/agents.mjs") },
|
|
15
|
+
];
|
|
16
|
+
export async function buildSystem(system, designDir, opts = {}) {
|
|
17
|
+
const v = validateSystem(system);
|
|
18
|
+
if (v.errors.length)
|
|
19
|
+
throw new Error(`Design system is invalid:\n - ${v.errors.join('\n - ')}`);
|
|
20
|
+
const idx = indexTokens(system.tokens, system.meta.prefix);
|
|
21
|
+
const outDir = join(designDir, system.meta.out || 'dist');
|
|
22
|
+
mkdirSync(outDir, { recursive: true });
|
|
23
|
+
const files = [];
|
|
24
|
+
const hashes = {};
|
|
25
|
+
const generators = [];
|
|
26
|
+
const warnings = [...v.warnings];
|
|
27
|
+
const write = (rel, content) => {
|
|
28
|
+
const p = outputPath(outDir, rel);
|
|
29
|
+
mkdirSync(join(p, '..'), { recursive: true });
|
|
30
|
+
writeFileSync(p, content);
|
|
31
|
+
files.push(p);
|
|
32
|
+
hashes[rel] = contentHash(content);
|
|
33
|
+
};
|
|
34
|
+
const css = fullCss(system, idx);
|
|
35
|
+
write('tokens.css', css.tokens);
|
|
36
|
+
write('base.css', css.base);
|
|
37
|
+
write('components.css', css.components);
|
|
38
|
+
write(`${system.meta.prefix}.css`, css.all);
|
|
39
|
+
// Selective imports keep dependencies explicit: tokens, base, components, then patterns.
|
|
40
|
+
for (const component of system.components) {
|
|
41
|
+
write(`css/components/${component.slug}.css`, componentCss(component, idx, system.meta.prefix));
|
|
42
|
+
}
|
|
43
|
+
for (const pattern of system.patterns) {
|
|
44
|
+
write(`css/patterns/${pattern.slug}.css`, patternsCss({ ...system, patterns: [pattern] }, idx));
|
|
45
|
+
}
|
|
46
|
+
generators.push('css');
|
|
47
|
+
for (const g of optional) {
|
|
48
|
+
if (opts.only && !opts.only.includes(g.name))
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const mod = await g.load();
|
|
52
|
+
await mod.generate(system, idx, write);
|
|
53
|
+
generators.push(g.name);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
const err = e;
|
|
57
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
58
|
+
warnings.push(`generator "${g.name}" not available yet`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`generator "${g.name}" failed: ${err.stack ?? err.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const hash = sourceHash(system);
|
|
65
|
+
// Certify only this invocation's generators, even if older files remain in dist.
|
|
66
|
+
const manifest = { manifestVersion: 1, canon: VERSION, system: system.meta.name, version: system.meta.version, sourceHash: hash, builtAt: new Date().toISOString(), components: system.components.length, tokens: idx.size, generators, files: { ...hashes } };
|
|
67
|
+
write('canon.lock.json', JSON.stringify(manifest, null, 2) + '\n');
|
|
68
|
+
return { outDir, files, hash, warnings };
|
|
69
|
+
}
|
package/lib/cli.mjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { VERSION } from "./version.mjs";
|
|
5
|
+
import { PRESETS } from "./tokens/presets.mjs";
|
|
6
|
+
import { findProject } from "./project.mjs";
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const cmd = args[0];
|
|
9
|
+
function flag(name, def) {
|
|
10
|
+
const i = args.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
11
|
+
if (i === -1)
|
|
12
|
+
return def;
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a.includes('='))
|
|
15
|
+
return a.slice(a.indexOf('=') + 1);
|
|
16
|
+
const next = args[i + 1];
|
|
17
|
+
if (next && !next.startsWith('--'))
|
|
18
|
+
return next;
|
|
19
|
+
return 'true';
|
|
20
|
+
}
|
|
21
|
+
const has = (name) => args.includes(`--${name}`) || args.some((a) => a.startsWith(`--${name}=`));
|
|
22
|
+
const positional = args.slice(1).filter((a, i, arr) => !a.startsWith('--') && !(arr[i - 1]?.startsWith('--') && !arr[i - 1].includes('=') && !['true', 'false'].includes(a) && isFlagWithValue(arr[i - 1])));
|
|
23
|
+
function isFlagWithValue(f) { return !['--force', '--no-install', '--json', '--quiet', '--fix', '--changed', '--annotate', '--hooks', '--no-hooks', '--open'].includes(f); }
|
|
24
|
+
const rootDir = () => resolve(flag('root') ?? findProject(process.cwd())?.root ?? '.');
|
|
25
|
+
const designDir = () => {
|
|
26
|
+
const from = flag('root') ? resolve(flag('root')) : process.cwd();
|
|
27
|
+
if (flag('design'))
|
|
28
|
+
return resolve(from, flag('design'));
|
|
29
|
+
const project = findProject(from);
|
|
30
|
+
if (project)
|
|
31
|
+
return project.design;
|
|
32
|
+
return existsSync(join(from, 'system.json')) ? from : join(from, 'design');
|
|
33
|
+
};
|
|
34
|
+
const HELP = `canon ${VERSION} — design systems that AI agents cannot get wrong.
|
|
35
|
+
|
|
36
|
+
Usage
|
|
37
|
+
canon connect <studio-url|snapshot.json> [--root .] [--design design] [--no-hooks]
|
|
38
|
+
Import the saved design or reuse this project's existing Canon
|
|
39
|
+
canon init <name> [--preset canon|editorial|vera|clean|dark] [--prefix cn] [--brand #hex] [--action #hex]
|
|
40
|
+
[--font "Geist"] [--radius 1] [--base 14] [--control 36] [--design design] [--force]
|
|
41
|
+
canon build [--design design] Compile tokens → css, tailwind, react, gallery, DESIGN.md, agent files
|
|
42
|
+
canon install [--design design] [--root .] [--no-hooks]
|
|
43
|
+
Put DESIGN.md, AGENTS.md/CLAUDE.md blocks, skill, cursor rules, MCP config and lint hook in a project
|
|
44
|
+
canon sync build + install
|
|
45
|
+
canon lint [paths…] [--design design] [--json] [--changed]
|
|
46
|
+
Find raw colors/sizes/fonts/tailwind palette classes outside the system
|
|
47
|
+
canon check Fail if dist is stale or lint fails (CI / pre-commit)
|
|
48
|
+
canon mcp [--design design] MCP server (stdio) exposing tokens, components, rules and lint to any agent
|
|
49
|
+
canon add <component-slug> Add a catalog component missing from the design dir
|
|
50
|
+
canon presets List presets
|
|
51
|
+
canon studio [--root .] [--port 4600] [--open]
|
|
52
|
+
Edit this project's design and save its styles and agent references
|
|
53
|
+
canon serve Alias for studio
|
|
54
|
+
canon doctor Sanity-check a design dir
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
canon init "Vera" --preset vera --prefix vera && canon install
|
|
58
|
+
canon lint src app
|
|
59
|
+
canon mcp --design ./design (register in .mcp.json; canon install does it)
|
|
60
|
+
`;
|
|
61
|
+
async function main() {
|
|
62
|
+
switch (cmd) {
|
|
63
|
+
case 'connect': {
|
|
64
|
+
const { connect } = await import("./connect.mjs");
|
|
65
|
+
const result = await connect(positional[0], { root: rootDir(), design: flag('design'), hooks: !has('no-hooks') });
|
|
66
|
+
console.log(`✓ ${result.reused ? 'Reused existing design' : 'Imported saved Studio design'} → ${result.design}`);
|
|
67
|
+
for (const line of result.log)
|
|
68
|
+
console.log(line);
|
|
69
|
+
console.log('Start this project’s Studio: canon studio --port 0 --open');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case undefined:
|
|
73
|
+
case 'help':
|
|
74
|
+
case '--help':
|
|
75
|
+
case '-h':
|
|
76
|
+
console.log(HELP);
|
|
77
|
+
return;
|
|
78
|
+
case '--version':
|
|
79
|
+
case '-v':
|
|
80
|
+
case 'version':
|
|
81
|
+
console.log(VERSION);
|
|
82
|
+
return;
|
|
83
|
+
case 'presets': {
|
|
84
|
+
for (const p of Object.values(PRESETS))
|
|
85
|
+
console.log(`${p.id.padEnd(9)} ${p.label}\n${''.padEnd(10)}${p.direction.summary}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
case 'init': {
|
|
89
|
+
const { createSystem, writeDesignDir } = await import("./system.mjs");
|
|
90
|
+
const { buildSystem } = await import("./build.mjs");
|
|
91
|
+
const name = positional[0] ?? flag('name');
|
|
92
|
+
if (!name) {
|
|
93
|
+
console.error('canon init <name> — a name is required.');
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
const dir = resolve(flag('design', 'design'));
|
|
97
|
+
if (existsSync(join(dir, 'system.json')) && !has('force')) {
|
|
98
|
+
console.error(`${dir}/system.json already exists. Use --force to overwrite (components you customized will be reset).`);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
const num = (n) => (flag(n) !== undefined ? Number(flag(n)) : undefined);
|
|
102
|
+
const system = await createSystem({
|
|
103
|
+
name,
|
|
104
|
+
prefix: flag('prefix'),
|
|
105
|
+
preset: flag('preset', 'canon'),
|
|
106
|
+
description: flag('description'),
|
|
107
|
+
seeds: {
|
|
108
|
+
brand: flag('brand'), action: flag('action'),
|
|
109
|
+
fontSans: flag('font'), fontMono: flag('mono'), fontDisplay: flag('display'),
|
|
110
|
+
radiusScale: num('radius'), baseFontSize: num('base'), controlHeight: num('control'),
|
|
111
|
+
canvasLight: flag('canvas'), inkDark: flag('ink'), shadowTint: flag('shadow-tint'),
|
|
112
|
+
neutralHue: num('neutral-hue'), neutralChroma: num('neutral-chroma'),
|
|
113
|
+
defaultTheme: flag('theme'),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const written = writeDesignDir(system, dir);
|
|
117
|
+
console.log(`✓ ${system.meta.name} (prefix "${system.meta.prefix}", preset ${flag('preset', 'canon')}) → ${dir}`);
|
|
118
|
+
console.log(` ${written.length} source files: system.json, tokens.json, ${system.components.length} components, ${system.patterns.length} patterns`);
|
|
119
|
+
const r = await buildSystem(system, dir);
|
|
120
|
+
report(r);
|
|
121
|
+
console.log(`\nNext: open ${join(r.outDir, 'preview.html')} to see everything, then \`canon install\` to wire agents.`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
case 'build': {
|
|
125
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
126
|
+
const { buildSystem } = await import("./build.mjs");
|
|
127
|
+
const dir = designDir();
|
|
128
|
+
const system = loadDesignDir(dir);
|
|
129
|
+
const r = await buildSystem(system, dir, { only: flag('only')?.split(',') });
|
|
130
|
+
report(r);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'install':
|
|
134
|
+
case 'sync': {
|
|
135
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
136
|
+
const { buildSystem } = await import("./build.mjs");
|
|
137
|
+
const { install } = await import("./install.mjs");
|
|
138
|
+
const dir = designDir();
|
|
139
|
+
const system = loadDesignDir(dir);
|
|
140
|
+
if (cmd === 'sync')
|
|
141
|
+
report(await buildSystem(system, dir));
|
|
142
|
+
const res = install(system, dir, { root: rootDir(), hooks: !has('no-hooks') });
|
|
143
|
+
for (const l of res.log)
|
|
144
|
+
console.log(l);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'lint': {
|
|
148
|
+
const { runLint, formatReport } = await import("./lint.mjs");
|
|
149
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
150
|
+
const dir = designDir();
|
|
151
|
+
const system = loadDesignDir(dir);
|
|
152
|
+
const paths = positional.length ? positional : undefined;
|
|
153
|
+
const res = await runLint(system, dir, { paths, root: rootDir(), changed: has('changed') });
|
|
154
|
+
if (has('json'))
|
|
155
|
+
console.log(JSON.stringify(res, null, 2));
|
|
156
|
+
else
|
|
157
|
+
console.log(formatReport(res));
|
|
158
|
+
process.exit(res.violations.filter((v) => v.severity === 'error').length ? 1 : 0);
|
|
159
|
+
}
|
|
160
|
+
case 'check': {
|
|
161
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
162
|
+
const { runLint, formatReport } = await import("./lint.mjs");
|
|
163
|
+
const { checkBuild } = await import("./build-manifest.mjs");
|
|
164
|
+
const dir = designDir();
|
|
165
|
+
const system = loadDesignDir(dir);
|
|
166
|
+
const build = checkBuild(system, dir);
|
|
167
|
+
let ok = build.ok;
|
|
168
|
+
if (build.ok)
|
|
169
|
+
console.log('✓ dist is up to date (all generators and file hashes verified)');
|
|
170
|
+
else {
|
|
171
|
+
for (const issue of build.issues)
|
|
172
|
+
console.log(`✗ ${issue}`);
|
|
173
|
+
console.log(' Run `canon build` to rebuild all artifacts.');
|
|
174
|
+
}
|
|
175
|
+
const res = await runLint(system, dir, { root: rootDir() });
|
|
176
|
+
console.log(formatReport(res));
|
|
177
|
+
if (res.violations.some((v) => v.severity === 'error'))
|
|
178
|
+
ok = false;
|
|
179
|
+
process.exit(ok ? 0 : 1);
|
|
180
|
+
}
|
|
181
|
+
case 'hook': {
|
|
182
|
+
// Claude Code PostToolUse hook: lint the edited file; exit 2 + stderr feeds violations back to the agent.
|
|
183
|
+
const { runLint, formatReport } = await import("./lint.mjs");
|
|
184
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
185
|
+
const chunks = [];
|
|
186
|
+
for await (const c of process.stdin)
|
|
187
|
+
chunks.push(c);
|
|
188
|
+
let file;
|
|
189
|
+
try {
|
|
190
|
+
const j = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
191
|
+
file = j.tool_input?.file_path ?? j.tool_input?.path;
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
194
|
+
if (!file || !existsSync(file) || !/\.(css|scss|less|tsx|jsx|ts|js|mjs|html|vue|svelte|astro|mdx)$/.test(file))
|
|
195
|
+
return;
|
|
196
|
+
const dir = designDir();
|
|
197
|
+
const system = loadDesignDir(dir);
|
|
198
|
+
const res = await runLint(system, dir, { paths: [file], root: process.cwd() });
|
|
199
|
+
if (res.summary.errors) {
|
|
200
|
+
console.error(`canon lint found ${res.summary.errors} design-system violation(s) in ${file}. Fix them using tokens/components from DESIGN.md:\n` + formatReport(res));
|
|
201
|
+
process.exit(2);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
case 'mcp': {
|
|
206
|
+
const { startMcp } = await import("./mcp.mjs");
|
|
207
|
+
await startMcp(designDir());
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
case 'add': {
|
|
211
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
212
|
+
const { loadCatalog } = await import("./components/index.mjs");
|
|
213
|
+
const CATALOG = await loadCatalog();
|
|
214
|
+
const { writeFileSync } = await import('node:fs');
|
|
215
|
+
const dir = designDir();
|
|
216
|
+
const system = loadDesignDir(dir);
|
|
217
|
+
const slug = positional[0];
|
|
218
|
+
const spec = CATALOG.find((c) => c.slug === slug);
|
|
219
|
+
if (!spec) {
|
|
220
|
+
console.error(`Unknown component "${slug}". Available: ${CATALOG.map((c) => c.slug).join(', ')}`);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
|
223
|
+
if (system.components.some((c) => c.slug === slug) && !has('force')) {
|
|
224
|
+
console.error(`${slug} already exists in ${dir}/components. Use --force to reset it.`);
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
writeFileSync(join(dir, 'components', `${slug}.json`), JSON.stringify(spec, null, 2) + '\n');
|
|
228
|
+
console.log(`✓ added components/${slug}.json — run canon build`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
case 'serve':
|
|
232
|
+
case 'studio': {
|
|
233
|
+
const { serve } = await import("./serve.mjs");
|
|
234
|
+
const { loadDesignDir } = await import("./system.mjs");
|
|
235
|
+
const dir = designDir();
|
|
236
|
+
const system = loadDesignDir(dir);
|
|
237
|
+
const project = findProject(rootDir()) ?? findProject(dir);
|
|
238
|
+
const { realpathSync } = await import('node:fs');
|
|
239
|
+
const projectRoot = project && realpathSync(project.design) === realpathSync(dir) ? project.root : undefined;
|
|
240
|
+
await serve(join(dir, system.meta.out || 'dist'), Number(flag('port', '4600')), dir, { projectRoot, open: has('open') });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case 'doctor': {
|
|
244
|
+
const { loadDesignDir, validateSystem } = await import("./system.mjs");
|
|
245
|
+
const dir = designDir();
|
|
246
|
+
const system = loadDesignDir(dir);
|
|
247
|
+
const v = validateSystem(system);
|
|
248
|
+
for (const w of v.warnings)
|
|
249
|
+
console.log(` ! ${w}`);
|
|
250
|
+
if (v.errors.length) {
|
|
251
|
+
console.log(`✗ ${v.errors.length} problem(s):\n - ${v.errors.join('\n - ')}`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
console.log(`✓ ${system.meta.name}: ${system.components.length} components, ${system.patterns.length} patterns, tokens ok`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
default:
|
|
258
|
+
console.error(`Unknown command "${cmd}".\n`);
|
|
259
|
+
console.log(HELP);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function report(r) {
|
|
264
|
+
console.log(`✓ built ${r.files.length} files → ${r.outDir}`);
|
|
265
|
+
for (const w of r.warnings)
|
|
266
|
+
console.log(` ! ${w}`);
|
|
267
|
+
}
|
|
268
|
+
main().catch((e) => {
|
|
269
|
+
console.error(`✗ ${e.message}`);
|
|
270
|
+
if (process.env.CANON_DEBUG)
|
|
271
|
+
console.error(e.stack);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
});
|