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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { tokensCss, baseCss, componentCss, patternsCss, rePrefix } from "./css.mjs";
|
|
3
|
+
import { CATEGORY_ORDER, CATEGORY_LABEL } from "../components/index.mjs";
|
|
4
|
+
import { legacyPresetDefaults } from "../tokens/canon-preset.mjs";
|
|
5
|
+
import { documentationHtml } from "./documentation.mjs";
|
|
6
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
7
|
+
const inline = (rel) => readFileSync(new URL(rel, import.meta.url), 'utf8').replace(/^export /gm, '').replace(/^import .*$/gm, '');
|
|
8
|
+
const PATTERN_CATS = { 'app-layout': 'Application layouts', 'app-section': 'Application sections', 'app-page': 'Application pages', 'marketing-section': 'Marketing sections', 'marketing-page': 'Marketing pages', 'shared-page': 'Shared pages', email: 'Email' };
|
|
9
|
+
// These examples change their anatomy, not just a root attribute. Keep their
|
|
10
|
+
// authored markup intact instead of manufacturing a misleading variant matrix.
|
|
11
|
+
const STRUCTURAL_COMPONENTS = new Set(['sidebar', 'sidebar-nav', 'topbar', 'header-navigation', 'mobile-header', 'filter-bar', 'table', 'kanban', 'metric-group', 'rich-text-editor']);
|
|
12
|
+
const HEADER_COMPONENTS = new Set(['topbar', 'header-navigation', 'page-header', 'section-header', 'card-header']);
|
|
13
|
+
const isStructural = (spec) => spec.category === 'layout' || STRUCTURAL_COMPONENTS.has(spec.slug);
|
|
14
|
+
function examplePicker(examples, id, label = 'Example') {
|
|
15
|
+
return examples.length > 1 ? `<label class="pv-example-picker">${label}<select data-example-picker aria-label="${label}" aria-controls="${id}">${examples.map((example, i) => `<option value="${i}">${esc(example.title)}</option>`).join('')}</select><span class="pv-muted" aria-hidden="true">${examples.length} examples</span></label>` : '';
|
|
16
|
+
}
|
|
17
|
+
function viewportControls(viewport, height = 760) {
|
|
18
|
+
return `<div class="pv-pattern__controls"><output class="pv-pattern__metrics" data-preview-metrics>${viewport === 'mobile' ? '375' : '1280'} × ${height}</output><button type="button" class="pv-btn pv-btn--sm" data-preview-zoom="actual" aria-label="Preview at 100%" title="Preview at 100%">100%</button><span class="pv-pattern__vp" role="group" aria-label="Preview viewport"><button type="button" class="pv-btn pv-btn--sm" data-viewport="desktop" aria-pressed="${viewport === 'desktop'}">Desktop</button><button type="button" class="pv-btn pv-btn--sm" data-viewport="mobile" aria-pressed="${viewport === 'mobile'}">Mobile · 375</button></span></div>`;
|
|
19
|
+
}
|
|
20
|
+
function previewCanvas(html, label, viewport, height = 760, component = false) {
|
|
21
|
+
return `<div class="pv-canvas" data-preview-canvas><div class="pv-canvas__bounds"><div class="pv-frame${component ? ' pv-example__stage' : ''}" data-vp="${viewport}" style="--pv-viewport-height:${height}px" tabindex="0" role="region" aria-label="${esc(label)}">${html}</div></div></div>`;
|
|
22
|
+
}
|
|
23
|
+
function matrix(spec, prefix) {
|
|
24
|
+
if (isStructural(spec))
|
|
25
|
+
return '';
|
|
26
|
+
const axes = Object.keys(spec.variants ?? {}).filter((a) => spec.props[a]);
|
|
27
|
+
if (!axes.length || !spec.examples.length)
|
|
28
|
+
return '';
|
|
29
|
+
const rowAxis = axes.includes('variant') ? 'variant' : axes.includes('tone') ? 'tone' : axes[0];
|
|
30
|
+
const colAxis = axes.includes('size') && rowAxis !== 'size' ? 'size' : axes.find((a) => a !== rowAxis);
|
|
31
|
+
const base = rePrefix(spec.examples[0].html, prefix);
|
|
32
|
+
const swap = (html, axis, value) => {
|
|
33
|
+
const re = new RegExp(`data-${axis}="[^"]*"`);
|
|
34
|
+
return re.test(html) ? html.replace(re, `data-${axis}="${value}"`) : html.replace(/^(<[a-z0-9-]+)/i, `$1 data-${axis}="${value}"`);
|
|
35
|
+
};
|
|
36
|
+
const rows = spec.props[rowAxis].values;
|
|
37
|
+
const cols = colAxis ? spec.props[colAxis].values : [''];
|
|
38
|
+
if (rows.length * cols.length > 60)
|
|
39
|
+
return '';
|
|
40
|
+
let out = `<div class="pv-matrix-wrap" data-matrix><table class="pv-matrix"><thead><tr><th>${rowAxis}${colAxis ? ` ↓ / ${colAxis} →` : ''}</th>${cols.map((c) => `<th>${c}</th>`).join('')}</tr></thead><tbody>`;
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
out += `<tr><th>${r}</th>`;
|
|
43
|
+
for (const c of cols) {
|
|
44
|
+
let html = swap(base, rowAxis, r);
|
|
45
|
+
if (colAxis)
|
|
46
|
+
html = swap(html, colAxis, c);
|
|
47
|
+
out += `<td>${html}</td>`;
|
|
48
|
+
}
|
|
49
|
+
out += '</tr>';
|
|
50
|
+
}
|
|
51
|
+
return out + '</tbody></table></div>';
|
|
52
|
+
}
|
|
53
|
+
function exampleFig(e, prefix, kind, i, block = false, option, framed = false) {
|
|
54
|
+
const html = rePrefix(e.html, prefix);
|
|
55
|
+
const stage = framed ? previewCanvas(html, e.title + ' component preview', 'desktop', 384, true) : `<div class="pv-example__stage"${block ? ' data-block' : ''}>${html}</div>`;
|
|
56
|
+
return `<figure class="pv-example" data-${kind}="${i}"${option === undefined ? '' : ` data-example-option="${option}"${option ? ' hidden' : ''}`}><figcaption class="pv-example__title">${esc(e.title)}${option === undefined ? '' : '<span class="pv-pattern__context">Component preview</span>'}${e.description ? `<span class="pv-example__desc">${esc(e.description)}</span>` : ''}${framed ? viewportControls('desktop', 384) : ''}</figcaption>${stage}<details class="pv-code"><summary>HTML</summary><pre><code>${esc(html)}</code></pre></details></figure>`;
|
|
57
|
+
}
|
|
58
|
+
function componentSection(spec, prefix) {
|
|
59
|
+
const props = Object.entries(spec.props).map(([k, p]) => `<tr><td><code>${k}</code></td><td>${p.values.map((v) => `<code${v === p.default ? ' class="pv-default"' : ''}>${v}</code>`).join(' ')}</td><td>${esc(p.description)}</td></tr>`).join('');
|
|
60
|
+
const states = Object.entries(spec.states).map(([k, s]) => `<tr><td><code>${k}</code></td><td><code>${esc(s.selector)}</code></td><td>${esc(s.description)}${s.markup ? ` <span class="pv-muted">(${esc(s.markup)})</span>` : ''}</td></tr>`).join('');
|
|
61
|
+
const anatomy = spec.anatomy.map((a) => `<li><code>.${prefix}-${spec.slug}${a.part === 'root' ? '' : '__' + a.part}</code> <span class="pv-muted"><${a.element}>${a.optional ? ' · optional' : ''}</span> — ${esc(a.description)}</li>`).join('');
|
|
62
|
+
const m = matrix(spec, prefix);
|
|
63
|
+
const structural = isStructural(spec);
|
|
64
|
+
const examplesId = `c-${spec.slug}-examples`;
|
|
65
|
+
const recipesId = `c-${spec.slug}-recipes`;
|
|
66
|
+
const html = `<section class="pv-section" id="c-${spec.slug}" data-slug="${spec.slug}" data-category="${spec.category}">
|
|
67
|
+
<div class="pv-section__head"><div class="pv-kicker">${CATEGORY_LABEL[spec.category]}</div><h2 class="pv-h2" title="Open in editor">${esc(spec.name)} <code class="pv-slug">.${prefix}-${spec.slug}</code><span class="pv-editlink">edit</span></h2><p class="pv-lead">${esc(spec.description)}</p><p class="pv-usage"><strong>Use:</strong> ${esc(spec.usage)}</p></div>
|
|
68
|
+
${structural ? examplePicker(spec.examples, examplesId) : ''}<div class="pv-examples" id="${examplesId}"${structural ? ' data-example-set data-structural' : ''}>${spec.examples.map((e, i) => exampleFig(e, prefix, 'example', i, false, structural ? i : undefined, HEADER_COMPONENTS.has(spec.slug))).join('')}</div>
|
|
69
|
+
${spec.recipes?.length ? `<h3 class="pv-h3">Recipes</h3>${structural ? examplePicker(spec.recipes, recipesId, 'Recipe') : ''}<div class="pv-examples" id="${recipesId}"${structural ? ' data-example-set data-structural' : ''}>${spec.recipes.map((e, i) => exampleFig(e, prefix, 'recipe', i, false, structural ? i : undefined, HEADER_COMPONENTS.has(spec.slug))).join('')}</div>` : ''}
|
|
70
|
+
${m ? `<h3 class="pv-h3">Matrix</h3>${m}` : ''}
|
|
71
|
+
<details class="pv-details"><summary>Anatomy, props, states, rules</summary>
|
|
72
|
+
<h4 class="pv-h4">Anatomy</h4><ul class="pv-list">${anatomy}</ul>
|
|
73
|
+
${props ? `<h4 class="pv-h4">Props</h4><table class="pv-table"><thead><tr><th>prop</th><th>values (default outlined)</th><th>meaning</th></tr></thead><tbody>${props}</tbody></table>` : ''}
|
|
74
|
+
${states ? `<h4 class="pv-h4">States</h4><table class="pv-table"><thead><tr><th>state</th><th>selector</th><th>meaning</th></tr></thead><tbody>${states}</tbody></table>` : ''}
|
|
75
|
+
<h4 class="pv-h4">Rules</h4><ul class="pv-list">${spec.rules.map((r) => `<li>${esc(r)}</li>`).join('')}</ul>
|
|
76
|
+
<h4 class="pv-h4">Accessibility</h4><ul class="pv-list">${spec.a11y.map((r) => `<li>${esc(r)}</li>`).join('')}</ul>
|
|
77
|
+
</details>
|
|
78
|
+
</section>`;
|
|
79
|
+
return rePrefix(html, prefix);
|
|
80
|
+
}
|
|
81
|
+
function patternSection(pt, prefix) {
|
|
82
|
+
const viewport = pt.viewport === 'mobile' ? 'mobile' : 'desktop';
|
|
83
|
+
const examplesId = `p-${pt.slug}-examples`;
|
|
84
|
+
const examples = [{ title: 'Primary layout', html: pt.html }, ...(pt.variants ?? [])];
|
|
85
|
+
const frame = (html, title, description, i = -1) => `<div class="pv-pattern" data-variant="${i}" data-example-option="${i + 1}"${i >= 0 ? ' hidden' : ''}><div class="pv-pattern__bar"><div class="pv-pattern__title"><span class="pv-pattern__context">Product preview</span><span>${esc(title)}</span>${description ? `<span class="pv-example__desc">${esc(description)}</span>` : ''}</div>${viewportControls(viewport)}</div>${previewCanvas(rePrefix(html, prefix), pt.name + ' — ' + title + ' preview', viewport)}<details class="pv-code"><summary>HTML</summary><pre><code>${esc(rePrefix(html, prefix))}</code></pre></details></div>`;
|
|
86
|
+
return `<section class="pv-section" id="p-${pt.slug}" data-pattern="${pt.slug}" data-category="${pt.category}"><div class="pv-kicker">${PATTERN_CATS[pt.category] ?? pt.category}</div><h2 class="pv-h2">${esc(pt.name)}</h2><p class="pv-lead">${esc(pt.description)}</p>${examplePicker(examples, examplesId)}<div id="${examplesId}" data-example-set>${frame(pt.html, 'Primary layout')}${(pt.variants ?? []).map((v, i) => frame(v.html, v.title, v.description, i)).join('')}</div><details class="pv-details"><summary>Layout guidelines</summary><ul class="pv-list">${pt.rules.map((r) => `<li>${esc(r)}</li>`).join('')}</ul></details></section>`;
|
|
87
|
+
}
|
|
88
|
+
function overview(system) {
|
|
89
|
+
const p = system.meta.prefix;
|
|
90
|
+
const sample = (slug) => {
|
|
91
|
+
const spec = system.components.find((c) => c.slug === slug);
|
|
92
|
+
return spec?.examples[0] ? rePrefix(spec.examples[0].html, p) : '';
|
|
93
|
+
};
|
|
94
|
+
const badge = system.components.some((c) => c.slug === 'badge') ? `<span class="${p}-badge" data-tone="success" data-variant="soft" data-size="sm" data-shape="square">Active</span>` : '';
|
|
95
|
+
return `<section class="pv-section pv-overview" id="f-overview">
|
|
96
|
+
<div class="pv-kicker">Canon / Build with your agent</div>
|
|
97
|
+
<h2 class="pv-h2">${esc(system.meta.name)}</h2>
|
|
98
|
+
<p class="pv-lead">Conecte este design system ao Claude ou Codex no seu projeto. O Canon entrega as referências e instruções para o agente seguir nas próximas alterações de interface.</p>
|
|
99
|
+
<a class="pv-start" href="./docs.html#primeiro-projeto">Conectar ao meu projeto <span aria-hidden="true">→</span></a>
|
|
100
|
+
<div class="pv-overview__tokens">
|
|
101
|
+
<a href="#f-color"><span class="pv-overview__color" aria-hidden="true"></span><span>Brand<span class="pv-overview__value" data-token="brand.600">${esc(system.tokens.color.primitive.brand['600'])}</span></span></a>
|
|
102
|
+
<a href="#f-type"><span class="pv-overview__letter" aria-hidden="true">Aa</span><span>Typography<span class="pv-overview__value" data-font-label>${esc(system.tokens.font.family.sans.split(',')[0].replace(/"/g, ''))}</span></span></a>
|
|
103
|
+
<a href="#f-radius"><span class="pv-overview__shape" aria-hidden="true"></span><span>Shape<span class="pv-overview__value" data-token="radius.control">${esc(system.tokens.radius.control)}</span></span></a>
|
|
104
|
+
<a href="#f-size"><span class="pv-overview__density" aria-hidden="true"></span><span>Density<span class="pv-overview__value" data-token="size.control.md">${esc(system.tokens.size.control.md)}</span></span></a>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="pv-overview__surface pv-frame">
|
|
107
|
+
<div class="pv-overview__heading"><div><div class="${p}-text-heading-md">Workspace overview</div><p class="${p}-text-body-md pv-muted">A few everyday elements, with your design decisions applied.</p></div>${badge}</div>
|
|
108
|
+
<div class="pv-overview__controls">${sample('input')}${sample('button').replace('Save changes', 'Invite member')}</div>
|
|
109
|
+
<div class="pv-overview__examples">${sample('card')}${sample('stat')}</div>
|
|
110
|
+
</div>
|
|
111
|
+
<p class="pv-small pv-muted pv-overview__caption">Live component samples · Use the theme toggle to compare light and dark.</p>
|
|
112
|
+
</section>`;
|
|
113
|
+
}
|
|
114
|
+
function foundations(system, idx) {
|
|
115
|
+
const p = system.meta.prefix;
|
|
116
|
+
const t = system.tokens;
|
|
117
|
+
const prim = Object.entries(t.color.primitive).filter(([, v]) => typeof v !== 'string');
|
|
118
|
+
const scales = prim.map(([name, scale]) => `<div class="pv-scale"><div class="pv-scale__name">${name}</div><div class="pv-scale__row">${Object.entries(scale).map(([step, hex]) => `<div class="pv-swatch" style="background:var(--${p}-${name}-${step})" title="${name}.${step}"><span class="pv-swatch__step">${step}</span><span class="pv-swatch__hex" data-token="${name}.${step}">${hex}</span></div>`).join('')}</div></div>`).join('');
|
|
119
|
+
const sem = Object.entries(t.color.semantic).map(([name, v]) => {
|
|
120
|
+
const r = idx.get(`color.${name}`);
|
|
121
|
+
return `<div class="pv-sem"><div class="pv-sem__chip" style="background:var(--${p}-color-${name})"></div><div><code>--${p}-color-${name}</code><div class="pv-muted pv-small"><span data-token="color.${name}">${r.light}</span> · dark ${r.dark}</div><div class="pv-small">${esc(v.description ?? '')}</div></div></div>`;
|
|
122
|
+
}).join('');
|
|
123
|
+
const type = Object.entries(t.type).map(([name, ts]) => {
|
|
124
|
+
const size = idx.get(`type.${name}.size`).light, lh = idx.get(`type.${name}.lineHeight`).light, w = idx.get(`type.${name}.weight`).light, ls = idx.get(`type.${name}.letterSpacing`).light;
|
|
125
|
+
const specimen = name.startsWith('display') || name.startsWith('heading') ? 'A place for work to take shape' : name === 'kicker' ? 'Field notes / 01' : name.startsWith('code') ? 'const total = items.reduce((a, b) => a + b, 0);' : name.startsWith('numeric') ? '1,234,567.89 · 42% · $12,400' : name.startsWith('label') ? 'Save changes' : 'Every decision leaves a trace. Bring the next task, the people involved and the context you need into one clear view.';
|
|
126
|
+
return `<div class="pv-type"><div class="pv-type__meta"><code>.${p}-text-${name}</code><span class="pv-muted pv-small"><span data-token="type.${name}.size">${size}</span> / ${lh} · ${w} · ${ls}${ts.transform ? ' · ' + ts.transform : ''}</span><span class="pv-small">${esc(ts.description ?? '')}</span></div><div class="${p}-text-${name} pv-type__specimen">${specimen}</div></div>`;
|
|
127
|
+
}).join('');
|
|
128
|
+
const space = Object.entries(t.space).map(([k, v]) => `<div class="pv-space"><code>space.${k}</code><div class="pv-space__bar" style="width:var(--${p}-space-${k.replace('.', '-')})"></div><span class="pv-muted pv-small" data-token="space.${k}">${v}</span></div>`).join('');
|
|
129
|
+
const radius = Object.entries(t.radius).map(([k, v]) => `<div class="pv-radius"><div class="pv-radius__box" style="border-radius:var(--${p}-radius-${k})"></div><code>radius.${k}</code><span class="pv-muted pv-small" data-token="radius.${k}">${v}</span></div>`).join('');
|
|
130
|
+
const shadow = Object.entries(t.shadow).map(([k, v]) => `<div class="pv-shadow"><div class="pv-shadow__box" style="box-shadow:var(--${p}-shadow-${k})"></div><code>shadow.${k}</code><span class="pv-small">${esc(v.description ?? '')}</span></div>`).join('');
|
|
131
|
+
const sizes = Object.entries(t.size.control).map(([k, v]) => `<div class="pv-ctrl"><div class="pv-ctrl__box" style="height:var(--${p}-size-control-${k})"></div><code>control.${k}</code><span class="pv-muted pv-small" data-token="size.control.${k}">${v}</span></div>`).join('');
|
|
132
|
+
const motion = Object.entries(t.motion.duration).map(([k, v]) => `<code>duration.${k}</code> ${v}`).join(' · ') + '<br>' + Object.entries(t.motion.easing).map(([k, v]) => `<code>easing.${k}</code> ${v}`).join(' · ');
|
|
133
|
+
return `
|
|
134
|
+
<section class="pv-section" id="f-direction"><div class="pv-kicker">Direction</div><h2 class="pv-h2">${esc(system.meta.name)}</h2><p class="pv-lead">${esc(system.meta.direction.summary)}</p><ul class="pv-list">${system.meta.direction.principles.map((x) => `<li>${esc(x)}</li>`).join('')}</ul><h4 class="pv-h4">Never</h4><ul class="pv-list pv-never">${system.meta.direction.never.map((x) => `<li>${esc(x)}</li>`).join('')}</ul></section>
|
|
135
|
+
<section class="pv-section" id="f-color"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Color</h2><h3 class="pv-h3">Primitives</h3>${scales}<h3 class="pv-h3">Semantic (use these, not primitives)</h3><div class="pv-sem-grid">${sem}</div></section>
|
|
136
|
+
<section class="pv-section" id="f-type"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Typography</h2><p class="pv-lead">Sans: <code>${esc(t.font.family.sans)}</code><br>Mono: <code>${esc(t.font.family.mono)}</code></p>${type}</section>
|
|
137
|
+
<section class="pv-section" id="f-space"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Spacing</h2><div class="pv-space-grid">${space}</div></section>
|
|
138
|
+
<section class="pv-section" id="f-radius"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Radius</h2><div class="pv-row">${radius}</div></section>
|
|
139
|
+
<section class="pv-section" id="f-shadow"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Shadow</h2><div class="pv-row">${shadow}</div></section>
|
|
140
|
+
<section class="pv-section" id="f-size"><div class="pv-kicker">Foundations</div><h2 class="pv-h2">Control sizes</h2><div class="pv-row">${sizes}</div><h3 class="pv-h3">Motion</h3><p class="pv-small">${motion}</p></section>`;
|
|
141
|
+
}
|
|
142
|
+
const PREVIEW_CSS = readFileSync(new URL('./preview.css', import.meta.url), 'utf8');
|
|
143
|
+
export function previewHtml(system, idx, options = {}) {
|
|
144
|
+
const p = system.meta.prefix;
|
|
145
|
+
const byCat = CATEGORY_ORDER.map((cat) => ({ cat, items: system.components.filter((c) => c.category === cat) })).filter((g) => g.items.length);
|
|
146
|
+
const nav = byCat.map((g) => `<details class="pv-nav-group"><summary>${CATEGORY_LABEL[g.cat]}</summary>${g.items.map((c) => `<a href="#c-${c.slug}">${esc(c.name)}</a>`).join('')}</details>`).join('');
|
|
147
|
+
const pcats = Object.keys(PATTERN_CATS).map((cat) => ({ cat, items: system.patterns.filter((pt) => pt.category === cat) })).filter((g) => g.items.length);
|
|
148
|
+
const pnav = pcats.map((g) => `<details class="pv-nav-group"><summary>${PATTERN_CATS[g.cat]}</summary>${g.items.map((pt) => `<a href="#p-${pt.slug}">${esc(pt.name)}</a>`).join('')}</details>`).join('');
|
|
149
|
+
const comps = system.components.map((c) => componentSection(c, p)).join('\n');
|
|
150
|
+
const patterns = pcats.map((g) => g.items.map((pt) => patternSection(pt, p)).join('\n')).join('\n');
|
|
151
|
+
const fontLink = fontsLink(system.tokens.font.family.sans + ' ' + system.tokens.font.family.mono + ' ' + system.tokens.font.family.display);
|
|
152
|
+
const styles = [
|
|
153
|
+
`<style id="cn-css-tokens">${tokensCss(system, idx)}</style>`,
|
|
154
|
+
`<style id="cn-css-base">${baseCss(system, idx)}</style>`,
|
|
155
|
+
...system.components.map((c) => `<style data-component="${c.slug}">${componentCss(c, idx, p)}</style>`),
|
|
156
|
+
`<style id="cn-css-patterns">${patternsCss(system, idx)}</style>`,
|
|
157
|
+
`<style id="pv-css">${PREVIEW_CSS.replace(/--P-/g, `--${p}-`)}</style>`,
|
|
158
|
+
].join('\n');
|
|
159
|
+
const systemJson = JSON.stringify(system).replace(/<\/script/g, '<\\/script');
|
|
160
|
+
const legacyDefaults = JSON.stringify(Object.hasOwn(system.meta.seeds, 'presetOverrides') ? {} : legacyPresetDefaults(system.meta.seeds)).replace(/<\/script/g, '<\\/script');
|
|
161
|
+
return `<!doctype html>
|
|
162
|
+
<html lang="en" data-theme="${system.meta.defaultTheme}"${options.publicSite ? ' data-public-studio' : ''}>
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="utf-8">
|
|
165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
166
|
+
<title>${esc(system.meta.name)} — design system</title>
|
|
167
|
+
${fontLink}
|
|
168
|
+
${styles}
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<div class="pv-app" id="pv-app">
|
|
172
|
+
<nav class="pv-nav">
|
|
173
|
+
<div class="pv-nav__brand"><span class="pv-nav__mark" aria-hidden="true"></span><span>Canon</span><span class="pv-nav__studio">Studio</span></div>
|
|
174
|
+
<a class="pv-nav__start" href="./docs.html#primeiro-projeto">Get started <span aria-hidden="true">→</span></a>
|
|
175
|
+
<a class="pv-nav__docs" href="./docs.html">Documentação<span aria-hidden="true">↗</span></a>
|
|
176
|
+
<button class="pv-btn pv-nav__toggle" id="pv-nav-toggle" aria-expanded="false" aria-controls="pv-nav-content">Browse catalog</button>
|
|
177
|
+
<div id="pv-nav-content">
|
|
178
|
+
<div class="pv-nav__sub"><strong>${esc(system.meta.name)}</strong><br>${system.components.length} components · ${system.patterns.length} patterns · ${idx.size} tokens</div>
|
|
179
|
+
<label class="pv-nav__search"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg><input type="search" id="pv-search" placeholder="Search catalog…" aria-label="Search"></label>
|
|
180
|
+
<p class="pv-nav-empty" id="pv-nav-empty" role="status" hidden>No matching components or pages.</p>
|
|
181
|
+
<details class="pv-nav-group" open><summary>Foundations</summary>
|
|
182
|
+
<a href="#f-overview">Overview</a><a href="#f-direction">Direction</a><a href="#f-color">Color</a><a href="#f-type">Typography</a><a href="#f-space">Spacing</a><a href="#f-radius">Radius</a><a href="#f-shadow">Shadow</a><a href="#f-size">Sizes & motion</a>
|
|
183
|
+
</details>
|
|
184
|
+
<div class="pv-nav__group">Components</div>
|
|
185
|
+
${nav}
|
|
186
|
+
<div class="pv-nav__group">Layouts & pages</div>
|
|
187
|
+
${pnav}
|
|
188
|
+
</div>
|
|
189
|
+
</nav>
|
|
190
|
+
<main class="pv-main">
|
|
191
|
+
<div class="pv-top"><div class="pv-top__title"><span class="pv-top__crumb" id="pv-crumb">Foundations</span><span id="pv-project" class="pv-project" hidden></span><span id="pv-status" class="pv-status"></span></div><div class="pv-top__actions"><button class="pv-btn pv-btn--icon" id="pv-theme" type="button" aria-label="Switch theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="pv-theme-moon" d="M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z"/><g class="pv-theme-sun"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4m-14.2 0 1.4-1.4M17.7 6.3l1.4-1.4"/></g></svg><span id="pv-theme-label" class="pv-sr">Switch theme</span></button>${options.publicSite ? '<a class="pv-btn pv-btn--primary" id="pv-edit" href="./docs.html#primeiro-projeto">Use in my project</a>' : '<button class="pv-btn pv-btn--primary" id="pv-edit">Customize</button>'}</div></div>
|
|
192
|
+
<p class="pv-sync-hint pv-small" id="pv-sync-hint" hidden></p>
|
|
193
|
+
${overview(system)}
|
|
194
|
+
${foundations(system, idx)}
|
|
195
|
+
${comps}
|
|
196
|
+
${patterns}
|
|
197
|
+
</main>
|
|
198
|
+
<aside class="pv-editor" id="pv-editor" hidden aria-label="Design system editor"></aside>
|
|
199
|
+
</div>
|
|
200
|
+
<script>window.__CANON__ = ${systemJson}; window.__CANON_PRESET_DEFAULTS__ = ${legacyDefaults};</script>
|
|
201
|
+
<script>
|
|
202
|
+
${inline('../color.js')}
|
|
203
|
+
${inline('../engine.js')}
|
|
204
|
+
${inline('../tokens/base.js')}
|
|
205
|
+
</script>
|
|
206
|
+
<script>
|
|
207
|
+
${readFileSync(new URL('../editor.js', import.meta.url), 'utf8')}
|
|
208
|
+
</script>
|
|
209
|
+
</body>
|
|
210
|
+
</html>
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
export function fontsLink(families) {
|
|
214
|
+
const known = { Geist: 'Geist:wght@400..700', 'Geist Mono': 'Geist+Mono:wght@400..700', Inter: 'Inter:wght@400..700', 'Inter Tight': 'Inter+Tight:wght@400..700', 'JetBrains Mono': 'JetBrains+Mono:wght@400..700', Fraunces: 'Fraunces:opsz,wght@9..144,300..700', 'IBM Plex Sans': 'IBM+Plex+Sans:wght@400..700', 'IBM Plex Mono': 'IBM+Plex+Mono:wght@400..700', Manrope: 'Manrope:wght@400..700', 'DM Sans': 'DM+Sans:wght@400..700', 'Space Grotesk': 'Space+Grotesk:wght@400..700', 'Roboto Mono': 'Roboto+Mono:wght@400..700' };
|
|
215
|
+
const wanted = Object.entries(known).filter(([name]) => new RegExp(`"${name}"`).test(families)).map(([, q]) => `family=${q}`);
|
|
216
|
+
if (!wanted.length)
|
|
217
|
+
return '';
|
|
218
|
+
return `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?${wanted.join('&')}&display=swap">`;
|
|
219
|
+
}
|
|
220
|
+
export function generate(system, idx, write) {
|
|
221
|
+
write('preview.html', previewHtml(system, idx));
|
|
222
|
+
const fonts = fontsLink(system.tokens.font.family.sans + ' ' + system.tokens.font.family.mono + ' ' + system.tokens.font.family.display);
|
|
223
|
+
write('docs.html', documentationHtml(system, idx, fonts));
|
|
224
|
+
write('CONNECT.md', readFileSync(new URL('./connection.md', import.meta.url), 'utf8'));
|
|
225
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const pascal = (s) => s.replace(/(^|[-_])(\w)/g, (_m, _p, c) => c.toUpperCase());
|
|
2
|
+
const camel = (s) => { const p = pascal(s); return p[0].toLowerCase() + p.slice(1); };
|
|
3
|
+
const VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
4
|
+
const DISABLED_ELEMENTS = new Set(['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea']);
|
|
5
|
+
/** Selectors sharing a root data attribute describe values of one state axis. */
|
|
6
|
+
function stateProps(spec) {
|
|
7
|
+
const axes = new Map();
|
|
8
|
+
for (const [name, st] of Object.entries(spec.states)) {
|
|
9
|
+
const m = st.selector.match(/^\[data-([a-z-]+)(?:=["']([^"']+)["'])?\]/);
|
|
10
|
+
if (!m || spec.props[m[1]])
|
|
11
|
+
continue; // Explicit variant props already generate this attribute.
|
|
12
|
+
const axis = axes.get(m[1]) ?? { name: camel(m[1]), attr: `data-${m[1]}`, values: [], flags: [] };
|
|
13
|
+
if (m[2] && !axis.values.includes(m[2]))
|
|
14
|
+
axis.values.push(m[2]);
|
|
15
|
+
if (!m[2] && name === 'loading')
|
|
16
|
+
axis.aria = 'aria-busy';
|
|
17
|
+
axes.set(m[1], axis);
|
|
18
|
+
}
|
|
19
|
+
const used = new Set([...Object.keys(spec.props), ...[...axes.values()].map((s) => s.name)]);
|
|
20
|
+
for (const axis of axes.values()) {
|
|
21
|
+
if (axis.attr !== 'data-state')
|
|
22
|
+
continue;
|
|
23
|
+
if (axis.values.includes('open') && !axis.values.includes('closed'))
|
|
24
|
+
axis.values.push('closed');
|
|
25
|
+
// Keep the existing boolean conveniences; the explicit axis prop takes precedence.
|
|
26
|
+
for (const value of axis.values) {
|
|
27
|
+
const name = camel(value);
|
|
28
|
+
if (used.has(name))
|
|
29
|
+
continue;
|
|
30
|
+
axis.flags.push({ name, value });
|
|
31
|
+
used.add(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...axes.values()];
|
|
35
|
+
}
|
|
36
|
+
function stateValue(axis) {
|
|
37
|
+
if (!axis.values.length)
|
|
38
|
+
return `${axis.name} ? '' : undefined`;
|
|
39
|
+
if (!axis.flags.length)
|
|
40
|
+
return axis.name;
|
|
41
|
+
const fallback = axis.attr === 'data-state' && axis.values.includes('open') ? "'closed'" : 'undefined';
|
|
42
|
+
return `${axis.name} ?? (${axis.flags.map((s) => `${s.name} ? ${JSON.stringify(s.value)} : `).join('')}${fallback})`;
|
|
43
|
+
}
|
|
44
|
+
export function componentTsx(spec, prefix) {
|
|
45
|
+
const Name = pascal(spec.slug);
|
|
46
|
+
const rootEl = spec.anatomy.find((a) => a.part === 'root')?.element ?? 'div';
|
|
47
|
+
const el = /^[a-z][a-z0-9]*$/.test(rootEl) ? rootEl : 'div';
|
|
48
|
+
const props = Object.entries(spec.props);
|
|
49
|
+
const sprops = stateProps(spec);
|
|
50
|
+
const isVoid = VOID_ELEMENTS.has(el);
|
|
51
|
+
const stateNames = sprops.flatMap((s) => [s.name, ...s.flags.map((f) => f.name)]);
|
|
52
|
+
const omitted = [...props.map(([k]) => k), ...stateNames, ...(isVoid ? ['children', 'dangerouslySetInnerHTML'] : [])];
|
|
53
|
+
const intrinsicProps = `React.ComponentPropsWithoutRef<'${el}'>`;
|
|
54
|
+
const baseProps = omitted.length ? `Omit<${intrinsicProps}, ${omitted.map((name) => JSON.stringify(name)).join(' | ')}>` : intrinsicProps;
|
|
55
|
+
const typeLines = [
|
|
56
|
+
...props.map(([k, p]) => `export type ${Name}${pascal(k)} = ${p.values.map((v) => JSON.stringify(v)).join(' | ')};`),
|
|
57
|
+
...sprops.filter((s) => s.values.length).map((s) => `export type ${Name}${pascal(s.name)} = ${s.values.map((v) => JSON.stringify(v)).join(' | ')};`),
|
|
58
|
+
].join('\n');
|
|
59
|
+
const propDecls = [
|
|
60
|
+
...props.map(([k, p]) => ` /** ${p.description.replace(/\*\//g, '* /')} */\n ${k}?: ${Name}${pascal(k)};`),
|
|
61
|
+
...sprops.flatMap((s) => [
|
|
62
|
+
` ${s.name}?: ${s.values.length ? `${Name}${pascal(s.name)}` : 'boolean'};`,
|
|
63
|
+
...s.flags.map((f) => ` ${f.name}?: boolean;`),
|
|
64
|
+
]),
|
|
65
|
+
...(isVoid ? [' children?: never;', ' dangerouslySetInnerHTML?: never;'] : []),
|
|
66
|
+
].join('\n');
|
|
67
|
+
const destructure = [
|
|
68
|
+
...props.map(([k, p]) => `${k} = ${JSON.stringify(p.default)}`),
|
|
69
|
+
...stateNames,
|
|
70
|
+
'className', ...(isVoid ? [] : ['children']), '...rest',
|
|
71
|
+
].join(', ');
|
|
72
|
+
const openAxis = sprops.find((s) => s.attr === 'data-state' && s.values.includes('open'));
|
|
73
|
+
const disabledAxis = sprops.find((s) => s.name === 'disabled' && !s.values.length);
|
|
74
|
+
const attrs = [
|
|
75
|
+
...props.map(([k]) => `data-${k}={${k}}`),
|
|
76
|
+
...sprops.map((s) => `${s.attr}={${stateValue(s)}}${s.aria ? ` ${s.aria}={${s.name} || undefined}` : ''}`),
|
|
77
|
+
...(disabledAxis && DISABLED_ELEMENTS.has(el) ? ['disabled={disabled}'] : []),
|
|
78
|
+
...(openAxis && ['details', 'dialog'].includes(el) ? [`open={(${stateValue(openAxis)}) === 'open'}`] : []),
|
|
79
|
+
].join(' ');
|
|
80
|
+
const parts = spec.anatomy.filter((a) => a.part !== 'root').map((a) => {
|
|
81
|
+
const pel = /^[a-z][a-z0-9]*$/.test(a.element) ? a.element : 'div';
|
|
82
|
+
const PName = `${Name}${pascal(a.part)}`;
|
|
83
|
+
const partProps = VOID_ELEMENTS.has(pel)
|
|
84
|
+
? `Omit<React.ComponentPropsWithoutRef<'${pel}'>, 'children' | 'dangerouslySetInnerHTML'> & { children?: never; dangerouslySetInnerHTML?: never }`
|
|
85
|
+
: `React.ComponentPropsWithoutRef<'${pel}'>`;
|
|
86
|
+
return `/** ${a.description.replace(/\*\//g, '* /')} */\nexport const ${PName} = React.forwardRef<React.ComponentRef<'${pel}'>, ${partProps}>(function ${PName}({ className, ...props }, ref) {\n return <${pel} ref={ref}${pel === 'svg' ? ' aria-hidden="true"' : ''} className={cx('${prefix}-${spec.slug}__${a.part}', className)} {...props} />;\n});`;
|
|
87
|
+
}).join('\n\n');
|
|
88
|
+
const example = spec.examples[0]?.html.replace(/\bcn-/g, `${prefix}-`) ?? '';
|
|
89
|
+
return `// ${spec.name} — generated by canon from design/components/${spec.slug}.json. Do not edit; the CSS is the truth.
|
|
90
|
+
// ${spec.description}
|
|
91
|
+
import * as React from 'react';
|
|
92
|
+
import { cx } from './cx';
|
|
93
|
+
|
|
94
|
+
${typeLines}
|
|
95
|
+
|
|
96
|
+
export interface ${Name}Props extends ${baseProps} {
|
|
97
|
+
${propDecls}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ${spec.usage.replace(/\*\//g, '* /')}
|
|
102
|
+
*
|
|
103
|
+
* HTML equivalent:
|
|
104
|
+
* ${example.replace(/\n/g, '\n * ')}
|
|
105
|
+
*/
|
|
106
|
+
export const ${Name} = React.forwardRef<React.ComponentRef<'${el}'>, ${Name}Props>(function ${Name}({ ${destructure} }, ref) {
|
|
107
|
+
return (
|
|
108
|
+
<${el} ref={ref} className={cx('${prefix}-${spec.slug}', className)} ${attrs} {...rest}${isVoid ? ' />' : `>
|
|
109
|
+
{children}
|
|
110
|
+
</${el}>`}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
${parts}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
export function generate(system, _idx, write) {
|
|
118
|
+
const p = system.meta.prefix;
|
|
119
|
+
write('react/cx.ts', `export function cx(...parts: (string | false | null | undefined)[]): string {\n return parts.filter(Boolean).join(' ');\n}\n`);
|
|
120
|
+
const exports = [];
|
|
121
|
+
const symbols = new Map();
|
|
122
|
+
for (const c of system.components) {
|
|
123
|
+
write(`react/${c.slug}.tsx`, componentTsx(c, p));
|
|
124
|
+
exports.push(`export * from './${c.slug}';`);
|
|
125
|
+
const Name = pascal(c.slug);
|
|
126
|
+
const add = (name, kind) => {
|
|
127
|
+
symbols.set(name, [...(symbols.get(name) ?? []), { slug: c.slug, kind }]);
|
|
128
|
+
};
|
|
129
|
+
add(Name, 'root');
|
|
130
|
+
add(`${Name}Props`, 'type');
|
|
131
|
+
for (const prop of Object.keys(c.props))
|
|
132
|
+
add(`${Name}${pascal(prop)}`, 'type');
|
|
133
|
+
for (const axis of stateProps(c))
|
|
134
|
+
if (axis.values.length)
|
|
135
|
+
add(`${Name}${pascal(axis.name)}`, 'type');
|
|
136
|
+
for (const a of c.anatomy)
|
|
137
|
+
if (a.part !== 'root')
|
|
138
|
+
add(`${Name}${pascal(a.part)}`, 'part');
|
|
139
|
+
}
|
|
140
|
+
const used = new Set(symbols.keys());
|
|
141
|
+
const aliases = [];
|
|
142
|
+
for (const [name, owners] of symbols) {
|
|
143
|
+
if (owners.length < 2)
|
|
144
|
+
continue;
|
|
145
|
+
const primary = owners.find((o) => o.kind === 'root') ?? owners[0];
|
|
146
|
+
// An explicit export resolves export-star ambiguity without changing direct module imports.
|
|
147
|
+
exports.push(`export { ${primary.kind === 'type' ? 'type ' : ''}${name} } from './${primary.slug}';`);
|
|
148
|
+
for (const owner of owners) {
|
|
149
|
+
if (owner === primary)
|
|
150
|
+
continue;
|
|
151
|
+
const base = `${name}${owner.kind === 'part' ? 'Part' : owner.kind === 'type' ? 'Type' : 'Component'}`;
|
|
152
|
+
let alias = base;
|
|
153
|
+
for (let n = 2; used.has(alias); n++)
|
|
154
|
+
alias = `${base}${n}`;
|
|
155
|
+
used.add(alias);
|
|
156
|
+
exports.push(`export { ${owner.kind === 'type' ? 'type ' : ''}${name} as ${alias} } from './${owner.slug}';`);
|
|
157
|
+
aliases.push(`\`${alias}\` is \`${name}\` from \`./${owner.slug}\``);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
write('react/index.ts', `// ${system.meta.name} React bindings — thin wrappers over the generated CSS classes.\n// Import '${p}.css' once in your app root. Props map 1:1 to data-* attributes.\n${exports.join('\n')}\n`);
|
|
161
|
+
write('react/README.md', `# ${system.meta.name} — React\n\nEvery component is a thin wrapper: \`<Button variant="primary" size="md">\` renders \`<button class="${p}-button" data-variant="primary" data-size="md">\`. The CSS in \`${p}.css\` is the single source of truth; these files only make the data attributes type-safe.\n\nParts are exported as \`<ButtonIcon>\`, \`<ButtonLabel>\` … (one per anatomy part). Component roots keep their names in the catalog index. Colliding parts get a \`Part\` suffix; direct module imports keep their original names.${aliases.length ? `\n\n${aliases.join('; ')}.` : ''}\n\nBoolean state props (\`loading\`, \`open\`, \`invalid\`, \`selected\` …) set the matching \`data-*\` attribute. Select one value with \`state="open"\` or a boolean convenience such as \`open\`. An explicit \`state\` takes precedence; otherwise the first true flag in the spec wins. Open/closed axes default to closed; other inactive axes omit the attribute. Native states (hover, focus, disabled, checked) are native.\n\nCopy this folder into your app (e.g. \`src/ui/\`) or import from \`design/dist/react\`.\n`);
|
|
162
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { indexTokens, toCssVars } from "../tokens/resolve.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Tailwind v4 `@theme` file and a v3 preset. Both map utilities onto the CSS
|
|
4
|
+
* variables, so `bg-action` compiles to `background-color: var(--cn-color-bg-action)`
|
|
5
|
+
* and follows the theme automatically. The default Tailwind palette is removed so
|
|
6
|
+
* `bg-blue-500` does not exist — the lint rule and the LLM spec rely on that.
|
|
7
|
+
*/
|
|
8
|
+
export function tailwindV4(system, idx) {
|
|
9
|
+
const p = system.meta.prefix;
|
|
10
|
+
const t = system.tokens;
|
|
11
|
+
const variable = (ref) => toCssVars(`{${ref}}`, idx);
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push(`/* ${system.meta.name} — Tailwind v4 theme. Generated by canon. Import AFTER tokens.css:`);
|
|
14
|
+
lines.push(` @import "tailwindcss";`);
|
|
15
|
+
lines.push(` @import "./${p}.css"; (tokens + base + components)`);
|
|
16
|
+
lines.push(` @import "./tailwind.theme.css"; */`);
|
|
17
|
+
lines.push(`@theme {`);
|
|
18
|
+
lines.push(` /* wipe the default palette so only system colors exist */`);
|
|
19
|
+
lines.push(` --color-*: initial;`);
|
|
20
|
+
lines.push(` --font-*: initial;`);
|
|
21
|
+
lines.push(` --shadow-*: initial;`);
|
|
22
|
+
lines.push(` --radius-*: initial;`);
|
|
23
|
+
lines.push(` --text-*: initial;`);
|
|
24
|
+
lines.push(` --breakpoint-*: initial;`);
|
|
25
|
+
for (const [name, val] of Object.entries(t.color.primitive)) {
|
|
26
|
+
if (typeof val === 'string')
|
|
27
|
+
lines.push(` --color-${name}: ${variable(name)};`);
|
|
28
|
+
else
|
|
29
|
+
for (const step of Object.keys(val))
|
|
30
|
+
lines.push(` --color-${name}-${step}: ${variable(`${name}.${step}`)};`);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(t.color.semantic))
|
|
33
|
+
lines.push(` --color-${name}: ${variable(`color.${name}`)};`);
|
|
34
|
+
for (const k of Object.keys(t.font.family))
|
|
35
|
+
lines.push(` --font-${k}: ${variable(`font.family.${k}`)};`);
|
|
36
|
+
for (const k of Object.keys(t.font.size))
|
|
37
|
+
lines.push(` --text-${k}: ${variable(`font.size.${k}`)};`);
|
|
38
|
+
for (const k of Object.keys(t.type)) {
|
|
39
|
+
lines.push(` --text-${k}: ${variable(`type.${k}.size`)};`);
|
|
40
|
+
lines.push(` --text-${k}--line-height: ${variable(`type.${k}.lineHeight`)};`);
|
|
41
|
+
lines.push(` --text-${k}--letter-spacing: ${variable(`type.${k}.letterSpacing`)};`);
|
|
42
|
+
lines.push(` --text-${k}--font-weight: ${variable(`type.${k}.weight`)};`);
|
|
43
|
+
}
|
|
44
|
+
for (const k of Object.keys(t.font.weight))
|
|
45
|
+
lines.push(` --font-weight-${k}: ${variable(`font.weight.${k}`)};`);
|
|
46
|
+
for (const k of Object.keys(t.font.lineHeight))
|
|
47
|
+
lines.push(` --leading-${k}: ${variable(`font.lineHeight.${k}`)};`);
|
|
48
|
+
for (const k of Object.keys(t.font.letterSpacing))
|
|
49
|
+
lines.push(` --tracking-${k}: ${variable(`font.letterSpacing.${k}`)};`);
|
|
50
|
+
lines.push(` --spacing: 4px;`);
|
|
51
|
+
for (const k of Object.keys(t.radius))
|
|
52
|
+
lines.push(` --radius-${k}: ${variable(`radius.${k}`)};`);
|
|
53
|
+
for (const k of Object.keys(t.shadow))
|
|
54
|
+
lines.push(` --shadow-${k}: ${variable(`shadow.${k}`)};`);
|
|
55
|
+
for (const [k, v] of Object.entries(t.breakpoint))
|
|
56
|
+
lines.push(` --breakpoint-${k}: ${v};`);
|
|
57
|
+
for (const k of Object.keys(t.size.container))
|
|
58
|
+
lines.push(` --container-${k}: ${variable(`size.container.${k}`)};`);
|
|
59
|
+
for (const k of Object.keys(t.size.control))
|
|
60
|
+
lines.push(` --spacing-control-${k}: ${variable(`size.control.${k}`)};`);
|
|
61
|
+
for (const k of Object.keys(t.size.icon))
|
|
62
|
+
lines.push(` --spacing-icon-${k}: ${variable(`size.icon.${k}`)};`);
|
|
63
|
+
for (const k of Object.keys(t.z))
|
|
64
|
+
lines.push(` --z-index-${k}: ${variable(`z.${k}`)};`);
|
|
65
|
+
for (const k of Object.keys(t.motion.duration))
|
|
66
|
+
lines.push(` --duration-${k}: ${variable(`motion.duration.${k}`)};`);
|
|
67
|
+
for (const k of Object.keys(t.motion.easing))
|
|
68
|
+
lines.push(` --ease-${k}: ${variable(`motion.easing.${k}`)};`);
|
|
69
|
+
lines.push(`}`);
|
|
70
|
+
lines.push(`@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`);
|
|
71
|
+
return lines.join('\n') + '\n';
|
|
72
|
+
}
|
|
73
|
+
export function tailwindV3(system, idx = indexTokens(system.tokens, system.meta.prefix)) {
|
|
74
|
+
const t = system.tokens;
|
|
75
|
+
const variable = (ref) => toCssVars(`{${ref}}`, idx);
|
|
76
|
+
const colors = {};
|
|
77
|
+
for (const [name, val] of Object.entries(t.color.primitive)) {
|
|
78
|
+
if (typeof val === 'string')
|
|
79
|
+
colors[name] = variable(name);
|
|
80
|
+
else
|
|
81
|
+
colors[name] = Object.fromEntries(Object.keys(val).map((s) => [s, variable(`${name}.${s}`)]));
|
|
82
|
+
}
|
|
83
|
+
for (const name of Object.keys(t.color.semantic))
|
|
84
|
+
colors[name] = variable(`color.${name}`);
|
|
85
|
+
const fontSize = {};
|
|
86
|
+
for (const k of Object.keys(t.font.size))
|
|
87
|
+
fontSize[k] = variable(`font.size.${k}`);
|
|
88
|
+
for (const k of Object.keys(t.type))
|
|
89
|
+
fontSize[k] = [variable(`type.${k}.size`), { lineHeight: variable(`type.${k}.lineHeight`), letterSpacing: variable(`type.${k}.letterSpacing`), fontWeight: variable(`type.${k}.weight`) }];
|
|
90
|
+
const preset = {
|
|
91
|
+
darkMode: ['selector', '[data-theme="dark"]'],
|
|
92
|
+
theme: {
|
|
93
|
+
colors,
|
|
94
|
+
fontFamily: Object.fromEntries(Object.keys(t.font.family).map((k) => [k, variable(`font.family.${k}`)])),
|
|
95
|
+
fontSize,
|
|
96
|
+
fontWeight: Object.fromEntries(Object.keys(t.font.weight).map((k) => [k, variable(`font.weight.${k}`)])),
|
|
97
|
+
lineHeight: Object.fromEntries(Object.keys(t.font.lineHeight).map((k) => [k, variable(`font.lineHeight.${k}`)])),
|
|
98
|
+
letterSpacing: Object.fromEntries(Object.keys(t.font.letterSpacing).map((k) => [k, variable(`font.letterSpacing.${k}`)])),
|
|
99
|
+
spacing: Object.fromEntries(Object.keys(t.space).map((k) => [k, variable(`space.${k}`)])),
|
|
100
|
+
borderRadius: Object.fromEntries(Object.keys(t.radius).map((k) => [k, variable(`radius.${k}`)])),
|
|
101
|
+
boxShadow: Object.fromEntries(Object.keys(t.shadow).map((k) => [k, variable(`shadow.${k}`)])),
|
|
102
|
+
screens: t.breakpoint,
|
|
103
|
+
zIndex: Object.fromEntries(Object.keys(t.z).map((k) => [k, variable(`z.${k}`)])),
|
|
104
|
+
transitionDuration: Object.fromEntries(Object.keys(t.motion.duration).map((k) => [k, variable(`motion.duration.${k}`)])),
|
|
105
|
+
transitionTimingFunction: Object.fromEntries(Object.keys(t.motion.easing).map((k) => [k, variable(`motion.easing.${k}`)])),
|
|
106
|
+
extend: { maxWidth: Object.fromEntries(Object.keys(t.size.container).map((k) => [`container-${k}`, variable(`size.container.${k}`)])) },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
return `// ${system.meta.name} — Tailwind v3 preset. Generated by canon.\n// tailwind.config.js: module.exports = { presets: [require('./design/dist/tailwind.preset.cjs')], content: [...] }\n// The default palette is replaced on purpose: only system colors exist.\nmodule.exports = ${JSON.stringify(preset, null, 2)};\n`;
|
|
110
|
+
}
|
|
111
|
+
export function generate(system, idx, write) {
|
|
112
|
+
write('tailwind.theme.css', tailwindV4(system, idx));
|
|
113
|
+
write('tailwind.preset.cjs', tailwindV3(system, idx));
|
|
114
|
+
}
|