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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/THIRD_PARTY_NOTICES.md +55 -0
  4. package/bin/canon.js +6 -0
  5. package/lib/build-manifest.mjs +54 -0
  6. package/lib/build.mjs +69 -0
  7. package/lib/cli.mjs +273 -0
  8. package/lib/color.js +160 -0
  9. package/lib/components/_shared.mjs +93 -0
  10. package/lib/components/accordion.mjs +136 -0
  11. package/lib/components/account-card.mjs +166 -0
  12. package/lib/components/activity-feed.mjs +176 -0
  13. package/lib/components/activity-gauge.mjs +98 -0
  14. package/lib/components/agent-presence.mjs +156 -0
  15. package/lib/components/alert.mjs +154 -0
  16. package/lib/components/app-store-button.mjs +108 -0
  17. package/lib/components/avatar-group.mjs +128 -0
  18. package/lib/components/avatar.mjs +225 -0
  19. package/lib/components/badge-group.mjs +55 -0
  20. package/lib/components/badge.mjs +174 -0
  21. package/lib/components/banner.mjs +116 -0
  22. package/lib/components/breadcrumb.mjs +170 -0
  23. package/lib/components/button-group.mjs +114 -0
  24. package/lib/components/button.mjs +197 -0
  25. package/lib/components/card-header.mjs +115 -0
  26. package/lib/components/card.mjs +106 -0
  27. package/lib/components/carousel.mjs +182 -0
  28. package/lib/components/chart-frame.mjs +101 -0
  29. package/lib/components/checkbox.mjs +163 -0
  30. package/lib/components/close-button.mjs +49 -0
  31. package/lib/components/code.mjs +123 -0
  32. package/lib/components/color-picker.mjs +122 -0
  33. package/lib/components/combobox.mjs +210 -0
  34. package/lib/components/command-palette.mjs +226 -0
  35. package/lib/components/content-divider.mjs +104 -0
  36. package/lib/components/counter.mjs +86 -0
  37. package/lib/components/credit-card.mjs +126 -0
  38. package/lib/components/date-picker.mjs +202 -0
  39. package/lib/components/description-list.mjs +88 -0
  40. package/lib/components/dialog.mjs +140 -0
  41. package/lib/components/divider.mjs +115 -0
  42. package/lib/components/drawer.mjs +150 -0
  43. package/lib/components/empty-state.mjs +112 -0
  44. package/lib/components/featured-card.mjs +124 -0
  45. package/lib/components/featured-icon.mjs +67 -0
  46. package/lib/components/field.mjs +122 -0
  47. package/lib/components/file-dropzone.mjs +166 -0
  48. package/lib/components/filter-bar.mjs +179 -0
  49. package/lib/components/header-navigation.mjs +94 -0
  50. package/lib/components/icon-button.mjs +179 -0
  51. package/lib/components/index.mjs +52 -0
  52. package/lib/components/inline-cta.mjs +139 -0
  53. package/lib/components/input-group.mjs +164 -0
  54. package/lib/components/input.mjs +197 -0
  55. package/lib/components/kanban.mjs +179 -0
  56. package/lib/components/kbd.mjs +70 -0
  57. package/lib/components/kicker.mjs +79 -0
  58. package/lib/components/link.mjs +108 -0
  59. package/lib/components/list.mjs +117 -0
  60. package/lib/components/media-frame.mjs +119 -0
  61. package/lib/components/menu.mjs +190 -0
  62. package/lib/components/message.mjs +249 -0
  63. package/lib/components/metric-group.mjs +210 -0
  64. package/lib/components/mobile-header.mjs +173 -0
  65. package/lib/components/multi-select.mjs +218 -0
  66. package/lib/components/notification.mjs +175 -0
  67. package/lib/components/number-input.mjs +149 -0
  68. package/lib/components/page-header.mjs +68 -0
  69. package/lib/components/pagination.mjs +236 -0
  70. package/lib/components/pin-input.mjs +120 -0
  71. package/lib/components/popover.mjs +126 -0
  72. package/lib/components/progress-circle.mjs +115 -0
  73. package/lib/components/progress.mjs +93 -0
  74. package/lib/components/prose.mjs +96 -0
  75. package/lib/components/radio.mjs +160 -0
  76. package/lib/components/rating.mjs +93 -0
  77. package/lib/components/rich-text-editor.mjs +178 -0
  78. package/lib/components/section-header.mjs +62 -0
  79. package/lib/components/segmented-control.mjs +121 -0
  80. package/lib/components/select.mjs +163 -0
  81. package/lib/components/sidebar-nav.mjs +172 -0
  82. package/lib/components/sidebar.mjs +124 -0
  83. package/lib/components/skeleton.mjs +66 -0
  84. package/lib/components/slider.mjs +122 -0
  85. package/lib/components/social-button.mjs +119 -0
  86. package/lib/components/spinner.mjs +69 -0
  87. package/lib/components/stat.mjs +120 -0
  88. package/lib/components/stepper.mjs +228 -0
  89. package/lib/components/switch.mjs +159 -0
  90. package/lib/components/table.mjs +212 -0
  91. package/lib/components/tabs.mjs +207 -0
  92. package/lib/components/tag.mjs +196 -0
  93. package/lib/components/tags-input.mjs +146 -0
  94. package/lib/components/textarea.mjs +117 -0
  95. package/lib/components/timeline.mjs +135 -0
  96. package/lib/components/toast.mjs +115 -0
  97. package/lib/components/tooltip.mjs +86 -0
  98. package/lib/components/topbar.mjs +207 -0
  99. package/lib/components/tree-view.mjs +156 -0
  100. package/lib/components/video-player.mjs +189 -0
  101. package/lib/connect.mjs +110 -0
  102. package/lib/design-files.mjs +175 -0
  103. package/lib/distribution.mjs +109 -0
  104. package/lib/editor.js +940 -0
  105. package/lib/engine.js +339 -0
  106. package/lib/generators/agents.mjs +68 -0
  107. package/lib/generators/connection.md +58 -0
  108. package/lib/generators/css.mjs +2 -0
  109. package/lib/generators/designmd.mjs +324 -0
  110. package/lib/generators/documentation.css +82 -0
  111. package/lib/generators/documentation.html +81 -0
  112. package/lib/generators/documentation.js +96 -0
  113. package/lib/generators/documentation.mjs +35 -0
  114. package/lib/generators/dtcg.mjs +60 -0
  115. package/lib/generators/preview.css +279 -0
  116. package/lib/generators/preview.mjs +225 -0
  117. package/lib/generators/react.mjs +162 -0
  118. package/lib/generators/tailwind.mjs +114 -0
  119. package/lib/install.mjs +108 -0
  120. package/lib/lint.mjs +538 -0
  121. package/lib/mcp.mjs +268 -0
  122. package/lib/open.mjs +7 -0
  123. package/lib/patterns/_app.mjs +56 -0
  124. package/lib/patterns/app-shell.mjs +25 -0
  125. package/lib/patterns/audit-log.mjs +37 -0
  126. package/lib/patterns/auth-pages.mjs +44 -0
  127. package/lib/patterns/billing-page.mjs +39 -0
  128. package/lib/patterns/calendar-page.mjs +55 -0
  129. package/lib/patterns/dashboard-page.mjs +28 -0
  130. package/lib/patterns/files-page.mjs +30 -0
  131. package/lib/patterns/first-run.mjs +31 -0
  132. package/lib/patterns/form-layout.mjs +24 -0
  133. package/lib/patterns/inbox-page.mjs +58 -0
  134. package/lib/patterns/index.mjs +29 -0
  135. package/lib/patterns/list-detail-page.mjs +24 -0
  136. package/lib/patterns/mobile-app.mjs +46 -0
  137. package/lib/patterns/modal-flows.mjs +32 -0
  138. package/lib/patterns/onboarding-page.mjs +42 -0
  139. package/lib/patterns/permissions-page.mjs +38 -0
  140. package/lib/patterns/profile-page.mjs +32 -0
  141. package/lib/patterns/settings-page.mjs +24 -0
  142. package/lib/project.mjs +28 -0
  143. package/lib/serve.mjs +279 -0
  144. package/lib/system.mjs +210 -0
  145. package/lib/tokens/base.js +251 -0
  146. package/lib/tokens/canon-preset.mjs +67 -0
  147. package/lib/tokens/index.mjs +1 -0
  148. package/lib/tokens/legacy-canon.json +559 -0
  149. package/lib/tokens/presets.mjs +161 -0
  150. package/lib/tokens/resolve.mjs +1 -0
  151. package/lib/types.mjs +4 -0
  152. package/lib/version.mjs +2 -0
  153. package/package.json +54 -0
package/lib/mcp.mjs ADDED
@@ -0,0 +1,268 @@
1
+ // Minimal MCP server over stdio (JSON-RPC 2.0, newline-delimited). Zero deps.
2
+ import { createInterface } from 'node:readline';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { loadDesignDir } from "./system.mjs";
6
+ import { indexTokens } from "./tokens/resolve.mjs";
7
+ import { componentCss } from "./generators/css.mjs";
8
+ import { lintSource, knownFromSystem } from "./lint.mjs";
9
+ import { parseCssColor, distance } from './color.js';
10
+ import { VERSION } from "./version.mjs";
11
+ import { CATEGORY_LABEL } from "./components/index.mjs";
12
+ import { sourceHash } from "./build-manifest.mjs";
13
+ const sameRevision = (a, b) => a.path === b.path && a.content === b.content;
14
+ function readRevision(designDir, out) {
15
+ const path = join(designDir, out || 'dist', 'canon.lock.json');
16
+ try {
17
+ return { path, content: readFileSync(path, 'utf8') };
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return { path, content: null };
22
+ throw error;
23
+ }
24
+ }
25
+ function currentRevision(designDir) {
26
+ const meta = JSON.parse(readFileSync(join(designDir, 'system.json'), 'utf8'));
27
+ return readRevision(designDir, typeof meta.out === 'string' ? meta.out : 'dist');
28
+ }
29
+ export async function startMcp(designDir) {
30
+ let system = loadDesignDir(designDir);
31
+ let idx = indexTokens(system.tokens, system.meta.prefix);
32
+ let known = knownFromSystem(system, idx);
33
+ let revision = readRevision(designDir, system.meta.out || 'dist');
34
+ const reload = () => {
35
+ const nextSystem = loadDesignDir(designDir);
36
+ const nextIdx = indexTokens(nextSystem.tokens, nextSystem.meta.prefix);
37
+ const nextKnown = knownFromSystem(nextSystem, nextIdx);
38
+ const nextRevision = readRevision(designDir, nextSystem.meta.out || 'dist');
39
+ system = nextSystem;
40
+ idx = nextIdx;
41
+ known = nextKnown;
42
+ revision = nextRevision;
43
+ };
44
+ const refreshIfBuilt = () => {
45
+ let observed;
46
+ try {
47
+ observed = currentRevision(designDir);
48
+ }
49
+ catch (error) {
50
+ throw new Error(`Cannot refresh Canon MCP: ${error.message}`);
51
+ }
52
+ if (sameRevision(observed, revision))
53
+ return;
54
+ if (observed.content === null)
55
+ throw new Error(`Cannot refresh Canon MCP: build manifest is missing at ${observed.path}.`);
56
+ try {
57
+ const manifest = JSON.parse(observed.content);
58
+ if (manifest.manifestVersion !== 1 || typeof manifest.sourceHash !== 'string') {
59
+ throw new Error('build manifest is incomplete or uses an older format');
60
+ }
61
+ const nextSystem = loadDesignDir(designDir);
62
+ if (readRevision(designDir, nextSystem.meta.out || 'dist').path !== observed.path) {
63
+ throw new Error('design output path changed while refreshing');
64
+ }
65
+ if (sourceHash(nextSystem) !== manifest.sourceHash) {
66
+ throw new Error('design source does not match the latest build manifest');
67
+ }
68
+ const nextIdx = indexTokens(nextSystem.tokens, nextSystem.meta.prefix);
69
+ const nextKnown = knownFromSystem(nextSystem, nextIdx);
70
+ const confirmed = currentRevision(designDir);
71
+ if (!sameRevision(confirmed, observed))
72
+ throw new Error('build manifest changed while refreshing');
73
+ system = nextSystem;
74
+ idx = nextIdx;
75
+ known = nextKnown;
76
+ revision = confirmed;
77
+ }
78
+ catch (error) {
79
+ throw new Error(`Cannot refresh Canon MCP: ${error.message}`);
80
+ }
81
+ };
82
+ const distDir = () => join(designDir, system.meta.out || 'dist');
83
+ const p = () => system.meta.prefix;
84
+ const tools = [
85
+ { name: 'design_rules', description: 'The mandatory rules, art direction and workflow for producing UI in this project. Call this first.', inputSchema: { type: 'object', properties: {} } },
86
+ { name: 'list_components', description: 'List every component with class, category, props and when to use it.', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Optional category filter: actions, forms, navigation, data-display, feedback, overlays, layout, typography, media' } } } },
87
+ { name: 'get_component', description: 'Full specification of one component: anatomy, props, states, exact markup examples, every CSS value, rules and accessibility. Use the slug (e.g. "button", "dialog").', inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] } },
88
+ { name: 'get_css', description: 'The generated CSS for one component (or "tokens" / "all").', inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] } },
89
+ { name: 'list_tokens', description: 'List tokens by group (color, font, type, space, radius, shadow, size, z, motion, breakpoint, opacity) with CSS variables and light/dark values.', inputSchema: { type: 'object', properties: { group: { type: 'string' } } } },
90
+ { name: 'get_token', description: 'Details for one token by reference ("color.bg-action", "space.4", "type.body-md.size") or CSS variable name.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
91
+ { name: 'suggest_token', description: 'Given a raw value (hex/rgb color, px size, shadow) and optionally the CSS property, return the tokens to use instead.', inputSchema: { type: 'object', properties: { value: { type: 'string' }, property: { type: 'string' } }, required: ['value'] } },
92
+ { name: 'search', description: 'Free-text search across components, patterns and tokens (names, descriptions, rules).', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
93
+ { name: 'list_patterns', description: 'List layout/section/page patterns with their category and description.', inputSchema: { type: 'object', properties: { category: { type: 'string' } } } },
94
+ { name: 'get_pattern', description: 'Full HTML (primary + variants), rules and CSS for one pattern by slug.', inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] } },
95
+ { name: 'lint_code', description: 'Lint a code snippet (CSS/TSX/HTML…) against the system: raw values, unknown classes, invalid prop values. Returns violations with suggested tokens.', inputSchema: { type: 'object', properties: { code: { type: 'string' }, filename: { type: 'string', description: 'e.g. Card.tsx or styles.css (drives the parser)' } }, required: ['code'] } },
96
+ { name: 'reload', description: 'Reload the design system from disk after `canon build`.', inputSchema: { type: 'object', properties: {} } },
97
+ ];
98
+ const text = (t) => ({ content: [{ type: 'text', text: t }] });
99
+ const tokenLine = (t) => `${t.ref} ${t.cssVar} = ${t.light}${t.themed ? ` (dark ${t.dark})` : ''}${t.description ? ` — ${t.description}` : ''}`;
100
+ const call = async (name, args) => {
101
+ if (name !== 'reload')
102
+ refreshIfBuilt();
103
+ switch (name) {
104
+ case 'design_rules': {
105
+ const { agentsMd } = await import("./generators/agents.mjs");
106
+ return text(agentsMd(system) + `\nPrinciples:\n${system.meta.direction.principles.map((x) => `- ${x}`).join('\n')}\n\nNever:\n${system.meta.direction.never.map((x) => `- ${x}`).join('\n')}`);
107
+ }
108
+ case 'list_components': {
109
+ const cat = args.category;
110
+ const list = system.components.filter((c) => !cat || c.category === cat);
111
+ return text(list.map((c) => `- ${c.name} (.${p()}-${c.slug}) [${CATEGORY_LABEL[c.category]}] props: ${Object.entries(c.props).map(([k, v]) => `${k}=${v.values.join('|')}`).join(' ') || '—'}\n ${c.usage}`).join('\n'));
112
+ }
113
+ case 'get_component': {
114
+ const c = system.components.find((x) => x.slug === args.slug || x.name.toLowerCase() === String(args.slug).toLowerCase());
115
+ if (!c)
116
+ return text(`Unknown component "${args.slug}". Known: ${system.components.map((x) => x.slug).join(', ')}`);
117
+ const { designMd } = await import("./generators/designmd.mjs");
118
+ const full = designMd({ ...system, components: [c], patterns: [] }, idx);
119
+ const start = full.indexOf(`<a id="${c.slug}"></a>`);
120
+ const end = full.indexOf('## 4. Patterns');
121
+ return text(full.slice(start, end));
122
+ }
123
+ case 'get_css': {
124
+ if (args.slug === 'tokens')
125
+ return text(readFileSync(join(distDir(), 'tokens.css'), 'utf8'));
126
+ if (args.slug === 'all')
127
+ return text(readFileSync(join(distDir(), `${p()}.css`), 'utf8'));
128
+ const c = system.components.find((x) => x.slug === args.slug);
129
+ if (!c)
130
+ return text(`Unknown component "${args.slug}"`);
131
+ return text(componentCss(c, idx, p(), true));
132
+ }
133
+ case 'list_tokens': {
134
+ const g = args.group;
135
+ return text([...idx.values()].filter((t) => !g || t.group === g || t.ref.startsWith(`{${g}`)).map(tokenLine).join('\n'));
136
+ }
137
+ case 'get_token': {
138
+ const n = String(args.name).replace(/^\{|\}$/g, '');
139
+ const t = idx.get(n) ?? [...idx.values()].find((x) => x.cssVar === n || x.cssVar === `--${n}`);
140
+ return text(t ? tokenLine(t) : `Unknown token ${n}`);
141
+ }
142
+ case 'suggest_token': {
143
+ const value = String(args.value).trim();
144
+ const prop = String(args.property ?? '');
145
+ const hex = parseCssColor(value);
146
+ if (hex) {
147
+ const ranked = known.colors.map((c) => ({ c, d: distance(hex, c.hex) })).sort((a, b) => a.d - b.d);
148
+ const sem = ranked.filter((r) => r.c.ref.includes('-color-')).slice(0, 5);
149
+ const prim = ranked.filter((r) => !r.c.ref.includes('-color-')).slice(0, 3);
150
+ return text(`For ${value} (${hex}):\nSemantic (prefer):\n${sem.map((r) => `- var(${r.c.ref}) ${r.c.hex} (Δ ${r.d.toFixed(3)})`).join('\n')}\nPrimitive:\n${prim.map((r) => `- var(${r.c.ref}) ${r.c.hex} (Δ ${r.d.toFixed(3)})`).join('\n')}`);
151
+ }
152
+ const m = value.match(/^(-?[\d.]+)(px|rem|em)?$/);
153
+ if (m) {
154
+ const px = m[2] === 'rem' || m[2] === 'em' ? +m[1] * 16 : +m[1];
155
+ const group = /radius/.test(prop) ? 'radius' : /font/.test(prop) ? 'font' : /height|width/.test(prop) ? 'size' : 'space';
156
+ const ranked = known.dims.filter((d) => d.group === group).sort((a, b) => Math.abs(a.px - px) - Math.abs(b.px - px)).slice(0, 4);
157
+ return text(`For ${value} on ${prop || 'spacing'}:\n${ranked.map((d) => `- var(${d.ref}) = ${d.px}px${d.px === px ? ' (exact)' : ''}`).join('\n')}`);
158
+ }
159
+ if (/shadow/.test(prop) || /px.*rgba?/.test(value))
160
+ return text(`Shadows: ${Object.keys(system.tokens.shadow).map((k) => `var(--${p()}-shadow-${k})`).join(', ')} — xs on controls, sm resting cards (only on same-color canvas), md menus/popovers, lg dialogs/drawers, focus for rings.`);
161
+ return text(`No suggestion for "${value}". Provide a color (hex/rgb) or a length (px/rem) and the CSS property.`);
162
+ }
163
+ case 'search': {
164
+ const q = String(args.query).toLowerCase();
165
+ const hits = [];
166
+ for (const c of system.components) {
167
+ const hay = [c.name, c.slug, c.description, c.usage, ...c.rules, ...Object.keys(c.props)].join(' ').toLowerCase();
168
+ if (hay.includes(q))
169
+ hits.push(`component ${c.slug}: ${c.description}`);
170
+ }
171
+ for (const pt of system.patterns)
172
+ if ([pt.name, pt.slug, pt.description, ...pt.rules].join(' ').toLowerCase().includes(q))
173
+ hits.push(`pattern ${pt.slug}: ${pt.description}`);
174
+ for (const t of idx.values())
175
+ if ((t.ref + ' ' + (t.description ?? '')).toLowerCase().includes(q))
176
+ hits.push(`token ${tokenLine(t)}`);
177
+ return text(hits.length ? hits.slice(0, 40).join('\n') : 'No matches.');
178
+ }
179
+ case 'list_patterns': {
180
+ const cat = args.category;
181
+ return text(system.patterns.filter((pt) => !cat || pt.category === cat).map((pt) => `- ${pt.slug} [${pt.category}] ${pt.name}: ${pt.description}${pt.variants?.length ? ` (${pt.variants.length} more layouts)` : ''}`).join('\n'));
182
+ }
183
+ case 'get_pattern': {
184
+ const pt = system.patterns.find((x) => x.slug === args.slug);
185
+ if (!pt)
186
+ return text(`Unknown pattern "${args.slug}". Known: ${system.patterns.map((x) => x.slug).join(', ')}`);
187
+ const re = (s) => s.replace(/\bcn-/g, `${p()}-`);
188
+ return text(`# ${pt.name} [${pt.category}]\n${pt.description}\n\nRules:\n${pt.rules.map((r) => `- ${r}`).join('\n')}\n\n## Primary\n\`\`\`html\n${re(pt.html)}\n\`\`\`\n${(pt.variants ?? []).map((v) => `## ${v.title}\n${v.description ?? ''}\n\`\`\`html\n${re(v.html)}\n\`\`\``).join('\n')}${pt.css ? `\n## CSS (already in ${p()}.css)\n\`\`\`css\n${re(pt.css)}\n\`\`\`` : ''}`);
189
+ }
190
+ case 'lint_code': {
191
+ const vs = lintSource(known, String(args.filename ?? 'snippet.tsx'), String(args.code), { tailwind: system.meta.lint.tailwind });
192
+ return text(vs.length ? vs.map((v) => `${v.line}:${v.col} ${v.severity} ${v.rule}: ${v.message}${v.suggestion ? ` → ${v.suggestion}` : ''}\n ${v.snippet}`).join('\n') : '✓ no violations');
193
+ }
194
+ case 'reload':
195
+ reload();
196
+ return text(`Reloaded ${system.meta.name}: ${system.components.length} components, ${idx.size} tokens.`);
197
+ default:
198
+ throw Object.assign(new Error(`Unknown tool ${name}`), { code: -32602 });
199
+ }
200
+ };
201
+ const resources = () => [
202
+ { uri: 'canon://DESIGN.md', name: 'DESIGN.md', mimeType: 'text/markdown', description: 'Full design system specification' },
203
+ { uri: 'canon://DESIGN.compact.md', name: 'DESIGN.compact.md', mimeType: 'text/markdown', description: 'Compact quick reference' },
204
+ { uri: 'canon://tokens.css', name: 'tokens.css', mimeType: 'text/css', description: 'All CSS custom properties' },
205
+ ];
206
+ const readResource = (uri) => {
207
+ refreshIfBuilt();
208
+ const file = uri.replace('canon://', '');
209
+ const path = join(distDir(), file);
210
+ if (!existsSync(path))
211
+ throw Object.assign(new Error(`Resource not built: run canon build (${path})`), { code: -32002 });
212
+ return { contents: [{ uri, mimeType: file.endsWith('.css') ? 'text/css' : 'text/markdown', text: readFileSync(path, 'utf8') }] };
213
+ };
214
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
215
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
216
+ rl.on('line', async (line) => {
217
+ if (!line.trim())
218
+ return;
219
+ let req;
220
+ try {
221
+ req = JSON.parse(line);
222
+ }
223
+ catch {
224
+ return;
225
+ }
226
+ const id = req.id;
227
+ const method = String(req.method);
228
+ const params = (req.params ?? {});
229
+ const reply = (result) => { if (id !== undefined)
230
+ send({ jsonrpc: '2.0', id, result }); };
231
+ const fail = (code, message) => { if (id !== undefined)
232
+ send({ jsonrpc: '2.0', id, error: { code, message } }); };
233
+ try {
234
+ switch (method) {
235
+ case 'initialize':
236
+ reply({ protocolVersion: params.protocolVersion ?? '2025-06-18', capabilities: { tools: { listChanged: false }, resources: { listChanged: false } }, serverInfo: { name: 'canon', version: VERSION }, instructions: `Design system "${system.meta.name}". Call design_rules first, then get_component / get_pattern for exact markup, suggest_token for any raw value, lint_code before finishing.` });
237
+ break;
238
+ case 'notifications/initialized':
239
+ case 'notifications/cancelled':
240
+ break;
241
+ case 'ping':
242
+ reply({});
243
+ break;
244
+ case 'tools/list':
245
+ reply({ tools });
246
+ break;
247
+ case 'tools/call':
248
+ reply(await call(String(params.name), (params.arguments ?? {})));
249
+ break;
250
+ case 'resources/list':
251
+ reply({ resources: resources() });
252
+ break;
253
+ case 'resources/read':
254
+ reply(readResource(String(params.uri)));
255
+ break;
256
+ default:
257
+ fail(-32601, `Method not found: ${method}`);
258
+ }
259
+ }
260
+ catch (e) {
261
+ const err = e;
262
+ fail(err.code ?? -32000, err.message);
263
+ }
264
+ });
265
+ rl.on('close', () => process.exit(0));
266
+ process.stderr.write(`canon mcp: ${system.meta.name} (${system.components.length} components, ${idx.size} tokens) on stdio\n`);
267
+ await new Promise(() => { });
268
+ }
package/lib/open.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ export async function openBrowser(url) {
4
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32.exe' : 'xdg-open';
5
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
6
+ await promisify(execFile)(command, args, { timeout: 10_000, windowsHide: true });
7
+ }
@@ -0,0 +1,56 @@
1
+ // Shared snippets for application page patterns.
2
+ import { ICON } from "../components/_shared.mjs";
3
+ export { ICON };
4
+ export const IMG = (ratio = '16 / 9', attrs = '') => `<span class="cn-placeholder" style="aspect-ratio:${ratio}" role="img" aria-label="Placeholder image" ${attrs}><svg viewBox="0 0 160 90" preserveAspectRatio="none" aria-hidden="true"><rect width="160" height="90" fill="var(--cn-color-bg-muted)"/><path d="M0 90L160 0" stroke="var(--cn-color-border-strong)" stroke-width="1"/></svg></span>`;
5
+ export const BTN = (variant, label, size = 'md', icon = '') => `<button type="button" class="cn-button" data-variant="${variant}" data-size="${size}">${icon ? icon.replace('cn-icon', 'cn-button__icon') : ''}<span class="cn-button__label">${label}</span></button>`;
6
+ export const AVATAR = (i, n, size = 'md', tone = 'neutral', status = 'none') => `<span class="cn-avatar" data-size="${size}" data-shape="circle" data-tone="${tone}" data-status="${status}" role="img" aria-label="${n}"><span class="cn-avatar__fallback" aria-hidden="true">${i}</span>${status !== 'none' ? '<span class="cn-avatar__status"></span>' : ''}</span>`;
7
+ export const BADGE = (text, tone = 'neutral', variant = 'soft', dot = false) => `<span class="cn-badge" data-tone="${tone}" data-variant="${variant}" data-size="sm">${dot ? '<span class="cn-badge__dot"></span>' : ''}${text}</span>`;
8
+ const navItem = (label, icon, current = false, badge = '') => `<a href="#" class="cn-sidebar__item"${current ? ' aria-current="page"' : ''}>${icon.replace('cn-icon', 'cn-sidebar__icon')}<span class="cn-sidebar__label">${label}</span>${badge ? `<span class="cn-sidebar__badge">${badge}</span>` : ''}</a>`;
9
+ export const SIDEBAR = (current = 'Dashboard', variant = 'simple', theme = 'light') => `<nav class="cn-sidebar" data-variant="${variant}" data-theme="${theme}" aria-label="Main"><div class="cn-sidebar__brand"><span class="cn-sidebar__logo" aria-hidden="true"></span><span class="cn-sidebar__brand-name">Lumen</span></div>${variant === 'slim' ? '' : `<div class="cn-sidebar__search cn-input" data-variant="default" data-size="md">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search" aria-label="Search"></div>`}<div class="cn-sidebar__nav">${navItem('Home', ICON.home, current === 'Home')}${navItem('Dashboard', ICON.calendar, current === 'Dashboard')}${navItem('Projects', ICON.copy, current === 'Projects')}${navItem('Tasks', ICON.check, current === 'Tasks', '10')}${navItem('Reporting', ICON.inbox, current === 'Reporting')}${navItem('Users', ICON.user, current === 'Users')}</div><div class="cn-sidebar__footer">${navItem('Support', ICON.info)}${navItem('Settings', ICON.settings, current === 'Settings')}<div class="cn-sidebar__account"><span class="cn-sidebar__avatar" aria-hidden="true">MC</span><div class="cn-sidebar__account-text"><span class="cn-sidebar__name">Maya Chen</span><span class="cn-sidebar__email">maya@lumen.co</span></div><button type="button" class="cn-sidebar__account-action" aria-label="Log out">${ICON.external}</button></div></div></nav>`;
10
+ export const PAGE_HEADER = (title, desc, actions, variant = 'simple', extra = '') => `<header class="cn-page-header" data-variant="${variant}" data-divider="yes">${extra}<div class="cn-page-header__content"><h1 class="cn-page-header__title">${title}</h1><p class="cn-page-header__description">${desc}</p></div><div class="cn-page-header__actions">${actions}</div></header>`;
11
+ export const TABS = (items, active = 0, variant = 'underline') => `<div class="cn-tabs" data-variant="${variant}" data-size="md" role="tablist">${items.map((t, i) => `<button type="button" class="cn-tabs__tab" role="tab" aria-selected="${i === active}">${t}</button>`).join('')}</div>`;
12
+ export const STAT = (label, value, delta, trend = 'up', desc = 'vs last month') => `<div class="cn-stat" data-size="md" data-variant="card" data-trend="${trend}"><div class="cn-stat__label">${label}</div><div class="cn-stat__value">${value}</div><div class="cn-stat__delta"><svg class="cn-stat__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${trend === 'down' ? 'M8 3v10M4 9l4 4 4-4' : trend === 'flat' ? 'M3 8h10' : 'M8 13V3M4 7l4-4 4 4'}"/></svg><span>${delta}</span></div><div class="cn-stat__description">${desc}</div></div>`;
13
+ export const CHART = (h = 240) => `<svg class="cn-app__chart" viewBox="0 0 800 ${h}" preserveAspectRatio="none" role="img" aria-label="Line chart placeholder"><g class="cn-app__grid">${[0.2, 0.4, 0.6, 0.8].map((f) => `<line x1="0" x2="800" y1="${h * f}" y2="${h * f}"/>`).join('')}</g><path class="cn-app__area" d="M0 ${h * 0.75} C100 ${h * 0.7} 150 ${h * 0.5} 240 ${h * 0.55} S400 ${h * 0.3} 480 ${h * 0.35} S640 ${h * 0.15} 800 ${h * 0.2} V${h} H0 Z"/><path class="cn-app__line" d="M0 ${h * 0.75} C100 ${h * 0.7} 150 ${h * 0.5} 240 ${h * 0.55} S400 ${h * 0.3} 480 ${h * 0.35} S640 ${h * 0.15} 800 ${h * 0.2}"/></svg>`;
14
+ export const BARS = (h = 240) => `<svg class="cn-app__chart" viewBox="0 0 800 ${h}" preserveAspectRatio="none" role="img" aria-label="Bar chart placeholder"><g class="cn-app__grid">${[0.25, 0.5, 0.75].map((f) => `<line x1="0" x2="800" y1="${h * f}" y2="${h * f}"/>`).join('')}</g>${[0.4, 0.55, 0.35, 0.7, 0.6, 0.8, 0.5, 0.9, 0.65, 0.75, 0.55, 0.85].map((v, i) => `<rect class="cn-app__bar" x="${i * 66 + 14}" width="40" y="${h - h * v}" height="${h * v}" rx="4"/>`).join('')}</svg>`;
15
+ export const CARD = (header, body, footer = '') => `<section class="cn-card" data-variant="default" data-padding="none">${header ? `<div class="cn-section-header" data-variant="card" data-divider="yes">${header}</div>` : ''}<div class="cn-card__body cn-app__card-body">${body}</div>${footer ? `<footer class="cn-card__footer">${footer}</footer>` : ''}</section>`;
16
+ export const CARD_HEAD = (title, desc = '', actions = '', badge = '') => `<div class="cn-section-header__content"><h2 class="cn-section-header__title">${title}${badge ? ` <span class="cn-section-header__badge">${badge}</span>` : ''}</h2>${desc ? `<p class="cn-section-header__description">${desc}</p>` : ''}</div>${actions ? `<div class="cn-section-header__actions">${actions}</div>` : ''}`;
17
+ export const PEOPLE = [['MC', 'Maya Chen', 'maya@lumen.co', 'Product Manager', 'accent'], ['DC', 'Daniel Costa', 'daniel@lumen.co', 'Engineering', 'success'], ['SA', 'Sofia Almeida', 'sofia@lumen.co', 'Design', 'warning'], ['LF', 'Lucas Ferreira', 'lucas@lumen.co', 'Frontend', 'info'], ['AK', 'Aisha Khan', 'aisha@lumen.co', 'Backend', 'accent'], ['NB', 'Noah Berg', 'noah@lumen.co', 'Product Design', 'success']];
18
+ export const CHECKBOX = (label = '', checked = false) => `<label class="cn-checkbox" data-size="sm"><input class="cn-checkbox__input" type="checkbox"${checked ? ' checked' : ''} aria-label="${label || 'Select row'}"><span class="cn-checkbox__control" aria-hidden="true"><svg class="cn-checkbox__indicator" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8.5l3 3 7-7"/></svg></span>${label ? `<span class="cn-checkbox__label">${label}</span>` : ''}</label>`;
19
+ export const TEAM_TABLE = (rows = PEOPLE, selectable = true) => `<table class="cn-table" data-density="default" data-variant="default" data-interactive><thead class="cn-table__head"><tr class="cn-table__row">${selectable ? `<th class="cn-table__header-cell" scope="col" style="width:48px">${CHECKBOX()}</th>` : ''}<th class="cn-table__header-cell" scope="col">Name</th><th class="cn-table__header-cell" scope="col">Status</th><th class="cn-table__header-cell" scope="col">Role</th><th class="cn-table__header-cell" scope="col">Email address</th><th class="cn-table__header-cell" scope="col">Teams</th><th class="cn-table__header-cell" scope="col"><span class="cn-sr-only">Actions</span></th></tr></thead><tbody class="cn-table__body">${rows.map(([i, n, e, r, t], k) => `<tr class="cn-table__row"${k === 1 ? ' data-selected' : ''}>${selectable ? `<td class="cn-table__cell">${CHECKBOX('', k === 1)}</td>` : ''}<td class="cn-table__cell"><div class="cn-row" data-gap="3">${AVATAR(i, n, 'sm', t)}<div class="cn-stack" data-gap="0"><span class="cn-text-label-sm">${n}</span><span class="cn-text-body-sm cn-app__muted">@${n.split(' ')[0].toLowerCase()}</span></div></div></td><td class="cn-table__cell">${BADGE(k % 3 === 2 ? 'Offline' : 'Active', k % 3 === 2 ? 'neutral' : 'success', 'soft', true)}</td><td class="cn-table__cell">${r}</td><td class="cn-table__cell">${e}</td><td class="cn-table__cell"><div class="cn-row" data-gap="1">${BADGE('Design', 'accent')}${BADGE('Product', 'info')}${k % 2 ? BADGE('+2') : ''}</div></td><td class="cn-table__cell cn-table__actions"><div class="cn-row" data-gap="1" data-justify="end">${BTN('ghost', 'Delete', 'sm')}${BTN('link-color', 'Edit', 'sm')}</div></td></tr>`).join('')}</tbody></table>`;
20
+ export const PAGINATION = `<div class="cn-row" data-justify="between" data-gap="3"><span class="cn-text-body-sm cn-app__muted">Page 1 of 10</span><div class="cn-row" data-gap="2">${BTN('outline', 'Previous', 'sm')}${BTN('outline', 'Next', 'sm')}</div></div>`;
21
+ export const FIELD = (id, label, control, helper = '', layout = 'vertical') => `<div class="cn-field" data-layout="${layout}" data-size="md"><label class="cn-field__label" for="${id}">${label}</label><div class="cn-field__control">${control}</div>${helper ? `<p class="cn-field__helper">${helper}</p>` : ''}</div>`;
22
+ export const INPUT = (id, value = '', type = 'text', placeholder = '', icon = '') => `<div class="cn-input" data-variant="default" data-size="md">${icon ? icon.replace('cn-icon', 'cn-input__icon') : ''}<input class="cn-input__field" id="${id}" type="${type}"${value ? ` value="${value}"` : ''}${placeholder ? ` placeholder="${placeholder}"` : ''}></div>`;
23
+ export const APP_CSS = `
24
+ .cn-app { display: flex; min-height: 100vh; background-color: {color.bg-canvas}; }
25
+ .cn-app__main { flex: 1 1 auto; min-width: 0; padding: {space.8}; display: flex; flex-direction: column; gap: {space.8}; }
26
+ .cn-app__muted { color: {color.fg-muted}; }
27
+ .cn-card[data-padding="none"] > .cn-app__card-body { padding: {space.6}; }
28
+ .cn-card[data-padding="none"] > .cn-app__card-body--flush { padding: 0; overflow-x: auto; }
29
+ .cn-app__chart { width: 100%; height: auto; display: block; }
30
+ .cn-app__grid line { stroke: {color.border-subtle}; stroke-width: 1; }
31
+ .cn-app__line { fill: none; stroke: {color.bg-action}; stroke-width: 2; }
32
+ .cn-app__area { fill: {color.bg-action-subtle}; }
33
+ .cn-app__bar { fill: {color.bg-action}; }
34
+ .cn-app__bar:nth-child(even) { fill: {brand.300}; }
35
+ .cn-app__table-card .cn-table { width: 100%; }
36
+ .cn-app__table-card[data-padding="none"] > .cn-card__footer { padding: {space.3} {space.6}; }
37
+ .cn-app__table-card > .cn-card__footer > .cn-row { flex-wrap: wrap; }
38
+ .cn-app__legend { display: flex; gap: {space.4}; font-size: {font.size.sm}; color: {color.fg-muted}; }
39
+ .cn-app__legend span::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 2px; background-color: {color.bg-action}; margin-inline-end: {space.2}; }
40
+ .cn-app__legend span:nth-child(2)::before { background-color: {brand.300}; }
41
+ .cn-app__drawer { width: 440px; flex-shrink: 0; border-inline-start: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; display: flex; flex-direction: column; }
42
+ .cn-app__drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: {space.4}; padding: {space.6}; border-bottom: {border.width.thin} solid {color.border-default}; }
43
+ .cn-app__drawer-body { padding: {space.6}; display: flex; flex-direction: column; gap: {space.6}; flex: 1 1 auto; }
44
+ .cn-app__drawer-foot { display: flex; justify-content: flex-end; gap: {space.3}; padding: {space.4} {space.6}; border-top: {border.width.thin} solid {color.border-default}; }
45
+ .cn-app__dl { display: grid; grid-template-columns: 1fr; gap: {space.4}; }
46
+ .cn-app__dl dt { font-size: {font.size.sm}; font-weight: {font.weight.medium}; color: {color.fg-muted}; }
47
+ .cn-app__dl dd { font-size: {font.size.sm}; color: {color.fg-default}; margin-top: {space.1}; }
48
+ .cn-app__form-row { display: grid; grid-template-columns: 280px 1fr; gap: {space.8}; padding-block: {space.5}; border-bottom: {border.width.thin} solid {color.border-default}; }
49
+ .cn-app__form-row > .cn-stack { max-width: 512px; }
50
+ .cn-app__form-label { display: flex; flex-direction: column; gap: {space.1}; }
51
+ .cn-app__form-label span:first-child { font-size: {font.size.sm}; font-weight: {font.weight.semibold}; color: {color.fg-default}; }
52
+ .cn-app__form-label span:last-child { font-size: {font.size.sm}; color: {color.fg-muted}; }
53
+ .cn-app__form-actions { display: flex; justify-content: flex-end; gap: {space.3}; padding-top: {space.5}; }
54
+ .cn-app__plan { display: flex; flex-direction: column; gap: {space.4}; }
55
+ @media (max-width: {breakpoint.lg}) { .cn-app .cn-sidebar { display: none; } .cn-app__drawer { display: none; } .cn-app__form-row { grid-template-columns: 1fr; gap: {space.3}; } }
56
+ @media (max-width: {breakpoint.md}) { .cn-app__main { padding: {space.4}; gap: {space.6}; } }`;
@@ -0,0 +1,25 @@
1
+ export const appShell = {
2
+ name: 'App shell',
3
+ slug: 'app-shell',
4
+ category: 'app-layout',
5
+ description: 'Top navigation bar with 4–5 tabs phrased as user questions, a page header (kicker + title + one primary action), and a content area with a max width. No left sidebar by default.',
6
+ rules: [
7
+ 'Structure: <header class="cn-topbar"> → <main class="cn-page"> → <div class="cn-page__header"> + <div class="cn-page__body">.',
8
+ 'Page header: kicker (mono uppercase) above the h1 (heading-xl). Actions on the right: at most one primary button plus ghost/outline buttons.',
9
+ 'Content max width 1152px (size.container.xl), 24px side padding, 32px between page header and body.',
10
+ 'Sections inside the body are separated by 32px (space.8); groups inside a section by 16px (space.4); rows inside a group by 8px (space.2).',
11
+ 'Detail views open in a Drawer over the current list; do not navigate to a separate page for one item.',
12
+ ],
13
+ html: `<div class="cn-shell"><header class="cn-topbar"><div class="cn-topbar__inner"><a href="#" class="cn-topbar__brand">Vera</a><nav class="cn-tabs" data-variant="underline" data-size="md" aria-label="Main"><a class="cn-tabs__tab" href="#" aria-current="page">O que precisa de mim?</a><a class="cn-tabs__tab" href="#">A equipe está trabalhando?</a><a class="cn-tabs__tab" href="#">Onde cada pessoa está?</a><a class="cn-tabs__tab" href="#">Configurações</a></nav><div class="cn-topbar__actions"><span class="cn-avatar" data-size="sm" data-shape="circle"><span class="cn-avatar__fallback">MD</span></span></div></div></header><main class="cn-page"><div class="cn-page__header"><div><div class="cn-kicker" data-mark="square"><span class="cn-kicker__mark"></span>Hoje</div><h1 class="cn-page__title">O que precisa de mim?</h1><p class="cn-page__description">3 aprovações e 2 respostas aguardando.</p></div><div class="cn-page__actions"><button type="button" class="cn-button" data-variant="outline" data-size="md"><span class="cn-button__label">Exportar</span></button><button type="button" class="cn-button" data-variant="primary" data-size="md"><span class="cn-button__label">Nova busca</span></button></div></div><div class="cn-page__body"><section class="cn-card" data-variant="default" data-padding="md"><div class="cn-card__body cn-text-body-md">Content</div></section></div></main></div>`,
14
+ css: `
15
+ .cn-shell { min-height: 100vh; background-color: {color.bg-canvas}; }
16
+ .cn-page { width: 100%; max-width: {size.container.xl}; margin-inline: auto; padding: {space.8} {space.6} {space.16}; }
17
+ .cn-page__header { display: flex; align-items: flex-end; justify-content: space-between; gap: {space.6}; margin-bottom: {space.8}; }
18
+ .cn-page__title { font-family: {type.heading-xl.family}; font-size: {type.heading-xl.size}; font-weight: {type.heading-xl.weight}; line-height: {type.heading-xl.lineHeight}; letter-spacing: {type.heading-xl.letterSpacing}; margin-top: {space.2}; }
19
+ .cn-page__description { font-size: {font.size.md}; color: {color.fg-muted}; margin-top: {space.2}; }
20
+ .cn-page__actions { display: flex; align-items: center; gap: {space.2}; flex-shrink: 0; }
21
+ .cn-page__body { display: flex; flex-direction: column; gap: {space.8}; }
22
+ .cn-section { display: flex; flex-direction: column; gap: {space.4}; }
23
+ .cn-section__header { display: flex; align-items: baseline; justify-content: space-between; gap: {space.4}; }
24
+ @media (max-width: {breakpoint.md}) { .cn-page { padding-inline: {space.4}; } .cn-page__header { flex-direction: column; align-items: stretch; } }`,
25
+ };
@@ -0,0 +1,37 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, AVATAR, BADGE, ICON, APP_CSS, PAGINATION, CARD_HEAD } from "./_app.mjs";
2
+ const ROWS = [['MC', 'Maya Chen', 'accent', 'updated', 'Billing settings', 'Changed payment method to Visa •••• 4242', 'Today 09:41', 'settings', true], ['DC', 'Daniel Costa', 'success', 'deployed', 'Billing webhook v2', 'Production · 2 migrations', 'Today 08:20', 'deploy', false], ['System', 'System', 'neutral', 'rotated', 'API key lum_live_…8f2a', 'Scheduled rotation (90 days)', 'Today 03:00', 'security', false], ['SA', 'Sofia Almeida', 'warning', 'shared', 'Brand assets v3', 'With 8 people · view only', 'Yesterday 18:40', 'files', false], ['AK', 'Aisha Khan', 'accent', 'changed role', 'Elena Rossi', 'Member → Admin', 'Yesterday 16:12', 'members', true], ['LF', 'Lucas Ferreira', 'info', 'exported', 'Customers (2,420 rows)', 'CSV · 1.2 MB', 'Mon 11:05', 'data', false], ['NB', 'Noah Berg', 'success', 'signed in', 'New device', 'MacBook Pro · São Paulo', 'Mon 09:02', 'security', false]];
3
+ const CAT = { settings: 'neutral', deploy: 'info', security: 'warning', files: 'accent', members: 'accent', data: 'neutral' };
4
+ const FILTERS = `<div class="cn-row" data-justify="between" data-gap="3" data-wrap><div class="cn-row" data-gap="3" data-wrap><div class="cn-input" data-variant="default" data-size="md" style="width:280px">${ICON.search.replace('cn-icon', 'cn-input__icon')}<input class="cn-input__field" type="search" placeholder="Search events" aria-label="Search events"></div>${BTN('outline', 'Actor', 'md', ICON.chevronDown)}${BTN('outline', 'Category', 'md', ICON.chevronDown)}${BTN('outline', 'Last 7 days', 'md', ICON.calendar)}</div><div class="cn-row" data-gap="2"><span class="cn-text-body-sm cn-app__muted">1,204 events</span>${BTN('outline', 'Export', 'md', ICON.external)}</div></div>`;
5
+ export const auditLog = {
6
+ name: 'Audit log and activity',
7
+ slug: 'audit-log',
8
+ category: 'app-page',
9
+ description: 'A chronological record of who did what: a dense table with actor, action, target and metadata and expandable detail rows; a timeline view; and a security-events variant with severity.',
10
+ rules: [
11
+ 'Filters: search, actor, category and a date range on the left; count and Export on the right. Filters never hide the table.',
12
+ 'Rows are compact (48px): actor (avatar xs + name), action in plain words, target in label-sm, a category badge, timestamp right-aligned in body-sm muted; important rows can expand to a detail block (key/value list) below the row.',
13
+ 'Timeline variant groups by day with a vertical line and dots; the same fields, more room for descriptions.',
14
+ 'Security events use severity badges (danger/warning/neutral) and a “Review” action; never color entire rows.',
15
+ ],
16
+ html: `<div class="cn-app">${SIDEBAR('Reporting')}<main class="cn-app__main">${PAGE_HEADER('Audit log', 'Every change in your workspace, kept for 400 days.', BTN('outline', 'Retention settings', 'md', ICON.settings))}${FILTERS}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table"><table class="cn-table" data-density="compact" data-variant="default" data-interactive><thead class="cn-table__head"><tr class="cn-table__row"><th class="cn-table__header-cell" scope="col" style="width:32px"></th><th class="cn-table__header-cell" scope="col">Actor</th><th class="cn-table__header-cell" scope="col">Event</th><th class="cn-table__header-cell" scope="col">Category</th><th class="cn-table__header-cell" scope="col" data-align="right">When</th></tr></thead><tbody class="cn-table__body">${ROWS.map(([i, n, tone, a, t, d, w, cat, open]) => `<tr class="cn-table__row"${open ? ' data-selected' : ''}><td class="cn-table__cell"><button type="button" class="cn-audit__toggle" aria-expanded="${open}" aria-label="Details">${ICON.chevronRight}</button></td><td class="cn-table__cell"><div class="cn-row" data-gap="2">${n === 'System' ? `<span class="cn-audit__sys" aria-hidden="true">${ICON.settings}</span>` : AVATAR(i, n, 'xs', tone)}<span class="cn-text-label-sm">${n}</span></div></td><td class="cn-table__cell"><span class="cn-app__muted">${a}</span> <span class="cn-text-label-sm">${t}</span> <span class="cn-app__muted">· ${d}</span></td><td class="cn-table__cell">${BADGE(cat, CAT[cat])}</td><td class="cn-table__cell" data-align="right">${w}</td></tr>${open ? `<tr class="cn-table__row cn-audit__detail"><td class="cn-table__cell" colspan="5"><dl class="cn-audit__dl"><div><dt>Event ID</dt><dd><code class="cn-code" data-variant="inline" data-size="sm">evt_01J8Z3K2QWERTY</code></dd></div><div><dt>IP address</dt><dd>187.34.12.9 · Florianópolis, BR</dd></div><div><dt>User agent</dt><dd>Chrome 129 · macOS</dd></div><div><dt>Before → After</dt><dd>Mastercard •••• 0913 → Visa •••• 4242</dd></div></dl></td></tr>` : ''}`).join('')}</tbody></table></div><footer class="cn-card__footer">${PAGINATION}</footer></section></main></div>`,
17
+ variants: [
18
+ { title: 'Timeline view', html: `<div class="cn-app">${SIDEBAR('Reporting')}<main class="cn-app__main">${PAGE_HEADER('Activity', 'What your team did this week.', BTN('outline', 'Export', 'md', ICON.external))}${FILTERS}<div class="cn-stack" data-gap="8" style="max-width:800px">${[['Today', ROWS.slice(0, 3)], ['Yesterday', ROWS.slice(3, 5)], ['Monday', ROWS.slice(5)]].map(([day, rows]) => `<section class="cn-stack" data-gap="3"><h2 class="cn-text-label-sm cn-app__muted">${day}</h2><ol class="cn-audit__timeline">${rows.map(([i, n, tone, a, t, d, w, cat]) => `<li class="cn-audit__event"><span class="cn-audit__dot" data-tone="${CAT[cat]}" aria-hidden="true"></span><div class="cn-stack" data-gap="1"><div class="cn-row" data-gap="2" data-wrap>${n === 'System' ? '' : AVATAR(i, n, 'xs', tone)}<span class="cn-text-body-sm"><strong>${n}</strong> ${a} <strong>${t}</strong></span>${BADGE(cat, CAT[cat])}<span class="cn-text-body-xs cn-app__muted">${w.replace('Today ', '').replace('Yesterday ', '').replace('Mon ', '')}</span></div><span class="cn-text-body-sm cn-app__muted">${d}</span></div></li>`).join('')}</ol></section>`).join('')}</div></main></div>` },
19
+ { title: 'Security events with severity', html: `<div class="cn-app">${SIDEBAR('Settings')}<main class="cn-app__main">${PAGE_HEADER('Security events', 'Sign-ins, key rotations and permission changes.', `${BTN('outline', 'Configure alerts', 'md', ICON.settings)}${BTN('primary', 'Review all', 'md')}`)}<div class="cn-grid" data-cols="3" data-gap="6" data-keep>${[['Needs review', '3', 'danger'], ['New devices (7d)', '12', 'warning'], ['Key rotations (30d)', '4', 'neutral']].map(([l, v, tone]) => `<div class="cn-stat" data-size="md" data-variant="card" data-trend="flat"><div class="cn-stat__label">${l}</div><div class="cn-row" data-gap="3" data-align="end"><div class="cn-stat__value">${v}</div>${BADGE(tone === 'danger' ? 'Action needed' : tone === 'warning' ? 'Monitor' : 'Normal', tone)}</div></div>`).join('')}</div>${FILTERS.replace('Search events', 'Search security events')}<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Events', '', '', BADGE('3 need review', 'danger'))}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table"><table class="cn-table" data-density="compact" data-variant="default"><thead class="cn-table__head"><tr class="cn-table__row"><th class="cn-table__header-cell" scope="col">Severity</th><th class="cn-table__header-cell" scope="col">Event</th><th class="cn-table__header-cell" scope="col">Actor</th><th class="cn-table__header-cell" scope="col" data-align="right">When</th><th class="cn-table__header-cell" scope="col"><span class="cn-sr-only">Actions</span></th></tr></thead><tbody class="cn-table__body">${[['High', 'danger', 'Sign-in from new country', 'Noah Berg · Lagos, NG', 'NB', 'success', '09:02'], ['High', 'danger', 'Role changed to Admin', 'Elena Rossi by Aisha Khan', 'AK', 'accent', 'Yesterday'], ['Medium', 'warning', 'API key created', 'lum_live_…c91d by Daniel Costa', 'DC', 'success', 'Mon'], ['Low', 'neutral', 'Scheduled key rotation', 'lum_live_…8f2a', 'S', 'neutral', 'Mon'], ['Low', 'neutral', 'Password changed', 'Sofia Almeida', 'SA', 'warning', 'Sun']].map(([sev, tone, ev, who, i, atone, w]) => `<tr class="cn-table__row"><td class="cn-table__cell">${BADGE(sev, tone, 'soft', true)}</td><td class="cn-table__cell"><span class="cn-text-label-sm">${ev}</span></td><td class="cn-table__cell"><div class="cn-row" data-gap="2">${AVATAR(i, who, 'xs', atone)}<span>${who}</span></div></td><td class="cn-table__cell" data-align="right">${w}</td><td class="cn-table__cell cn-table__actions">${tone === 'danger' ? BTN('outline', 'Review', 'sm') : BTN('ghost', 'Details', 'sm')}</td></tr>`).join('')}</tbody></table></div></section></main></div>` },
20
+ ],
21
+ css: `${APP_CSS}
22
+ .cn-audit__toggle { appearance: none; border: 0; background: none; padding: 0; width: 24px; height: 24px; border-radius: {radius.sm}; color: {color.fg-subtle}; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
23
+ .cn-audit__toggle[aria-expanded="true"] { transform: rotate(90deg); }
24
+ .cn-audit__toggle .cn-icon { width: 16px; height: 16px; }
25
+ .cn-audit__sys { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: {radius.full}; background-color: {color.bg-subtle}; color: {color.fg-subtle}; }
26
+ .cn-audit__sys .cn-icon { width: 14px; height: 14px; }
27
+ .cn-audit__detail .cn-table__cell { background-color: {color.bg-subtle}; padding-inline-start: 56px; }
28
+ .cn-audit__dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: {space.4}; margin: 0; }
29
+ .cn-audit__dl dt { font-size: {font.size.xs}; font-weight: {font.weight.medium}; color: {color.fg-subtle}; }
30
+ .cn-audit__dl dd { font-size: {font.size.sm}; color: {color.fg-default}; margin-top: {space.1}; }
31
+ .cn-audit__timeline { list-style: none; margin: 0; padding: 0 0 0 {space.6}; position: relative; display: flex; flex-direction: column; gap: {space.5}; border-inline-start: {border.width.thin} solid {color.border-default}; margin-inline-start: 6px; }
32
+ .cn-audit__event { position: relative; }
33
+ .cn-audit__dot { position: absolute; left: calc({space.6} * -1 - 6px); top: 4px; width: 11px; height: 11px; border-radius: {radius.full}; background-color: {color.border-strong}; box-shadow: 0 0 0 2px {color.bg-canvas}; }
34
+ .cn-audit__dot[data-tone="accent"] { background-color: {color.bg-action}; } .cn-audit__dot[data-tone="info"] { background-color: {color.bg-info}; } .cn-audit__dot[data-tone="warning"] { background-color: {color.bg-warning}; }
35
+ @media (max-width: {breakpoint.md}) { .cn-audit__dl { grid-template-columns: 1fr 1fr; } }`,
36
+ viewport: 'desktop',
37
+ };
@@ -0,0 +1,44 @@
1
+ import { BTN, FIELD, INPUT, CHECKBOX, ICON, IMG } from "./_app.mjs";
2
+ const LOGO = `<span class="cn-auth__logo" aria-hidden="true"></span>`;
3
+ const GOOGLE = `<button type="button" class="cn-button" data-variant="outline" data-size="lg" style="width:100%"><svg class="cn-button__icon" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v6M5 8h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg><span class="cn-button__label">Sign in with Google</span></button>`;
4
+ const LOGIN_FORM = `<form class="cn-stack" data-gap="5">${FIELD('l-email', 'Email', INPUT('l-email', '', 'email', 'Enter your email'))}${FIELD('l-pass', 'Password', INPUT('l-pass', '', 'password', '••••••••'))}<div class="cn-row" data-justify="between">${CHECKBOX('Remember for 30 days')}<a href="#" class="cn-link" data-variant="standalone">Forgot password</a></div><div class="cn-stack" data-gap="3">${BTN('primary', 'Sign in', 'lg').replace('class="cn-button"', 'class="cn-button" style="width:100%"')}${GOOGLE}</div></form><p class="cn-text-body-sm cn-auth__muted cn-center">Don’t have an account? <a href="#" class="cn-link" data-variant="standalone">Sign up</a></p>`;
5
+ const SIGNUP_FORM = `<form class="cn-stack" data-gap="5">${FIELD('s-name', 'Name', INPUT('s-name', '', 'text', 'Enter your name'))}${FIELD('s-email', 'Email', INPUT('s-email', '', 'email', 'Enter your email'))}${FIELD('s-pass', 'Password', INPUT('s-pass', '', 'password', 'Create a password'), 'Must be at least 8 characters.')}<div class="cn-stack" data-gap="3">${BTN('primary', 'Get started', 'lg').replace('class="cn-button"', 'class="cn-button" style="width:100%"')}${GOOGLE.replace('Sign in with Google', 'Sign up with Google')}</div></form><p class="cn-text-body-sm cn-auth__muted cn-center">Already have an account? <a href="#" class="cn-link" data-variant="standalone">Log in</a></p>`;
6
+ const head = (title, text) => `<div class="cn-stack cn-center" data-gap="3" data-align="center">${LOGO}<h1 class="cn-text-heading-lg">${title}</h1><p class="cn-text-body-lg cn-auth__muted">${text}</p></div>`;
7
+ const FOOT = `<footer class="cn-auth__foot"><span class="cn-text-body-sm cn-auth__muted">© Lumen 2026</span><span class="cn-text-body-sm cn-auth__muted">help@lumen.co</span></footer>`;
8
+ export const authPages = {
9
+ name: 'Auth and error pages',
10
+ slug: 'auth-pages',
11
+ category: 'shared-page',
12
+ description: 'Log in, sign up, forgot password, verification code and 404 — centered single-column cards (360px) or split with an image. Shared across every product.',
13
+ rules: [
14
+ 'One column, 360px wide, centered vertically and horizontally; logo, heading-lg title, body-lg supporting text, then the form.',
15
+ 'Form: fields 20px apart, full-width lg primary button, optional social button below it, then the switch link ("Don’t have an account?").',
16
+ 'Split variant: form on the left inside a 1:1 split, image bleeding to the right edge, footer with copyright and support email.',
17
+ 'Verification: 4 (or 6) 64px code inputs, 8px apart, with a resend link.',
18
+ '404: a brand-colored "404 error" kicker, display-lg title, body-xl text, two buttons (outline "Go back", primary "Take me home").',
19
+ ],
20
+ html: `<div class="cn-auth"><div class="cn-auth__card">${head('Log in to your account', 'Welcome back! Please enter your details.')}${LOGIN_FORM}</div></div>`,
21
+ variants: [
22
+ { title: 'Sign up', html: `<div class="cn-auth"><div class="cn-auth__card">${head('Create an account', 'Start your 30-day free trial.')}${SIGNUP_FORM}</div></div>` },
23
+ { title: 'Log in, split with image', html: `<div class="cn-auth cn-auth--split"><div class="cn-auth__side"><div class="cn-auth__card">${head('Welcome back', 'Please enter your details.').replace('cn-center', '').replace('data-align="center"', 'data-align="start"')}${LOGIN_FORM}</div>${FOOT}</div>${IMG('1 / 1', 'data-radius="none"')}</div>` },
24
+ { title: 'Forgot password', html: `<div class="cn-auth"><div class="cn-auth__card">${head('Forgot password?', 'No worries, we’ll send you reset instructions.')}<form class="cn-stack" data-gap="5">${FIELD('f-email', 'Email', INPUT('f-email', '', 'email', 'Enter your email'))}${BTN('primary', 'Reset password', 'lg').replace('class="cn-button"', 'class="cn-button" style="width:100%"')}</form><a href="#" class="cn-link cn-auth__back" data-variant="standalone">${ICON.arrow.replace('cn-icon', 'cn-link__icon cn-auth__back-icon')}Back to log in</a></div></div>` },
25
+ { title: 'Check your email (verification code)', html: `<div class="cn-auth"><div class="cn-auth__card">${head('Check your email', 'We sent a verification code to maya@lumen.co')}<form class="cn-stack" data-gap="6"><div class="cn-auth__code" role="group" aria-label="Verification code">${[0, 1, 2, 3].map((i) => `<input class="cn-auth__digit" type="text" inputmode="numeric" maxlength="1" aria-label="Digit ${i + 1}"${i < 2 ? ` value="${[7, 2][i]}"` : ''}>`).join('')}</div>${BTN('primary', 'Verify email', 'lg').replace('class="cn-button"', 'class="cn-button" style="width:100%"')}</form><p class="cn-text-body-sm cn-auth__muted cn-center">Didn’t receive the email? <a href="#" class="cn-link" data-variant="standalone">Click to resend</a></p><a href="#" class="cn-link cn-auth__back" data-variant="standalone">${ICON.arrow.replace('cn-icon', 'cn-link__icon cn-auth__back-icon')}Back to log in</a></div></div>` },
26
+ { title: '404', html: `<div class="cn-auth"><div class="cn-auth__card cn-auth__card--wide"><div class="cn-stack cn-center" data-gap="6" data-align="center"><div class="cn-kicker" data-tone="accent" data-size="md" data-mark="none"><span class="cn-kicker__mark"></span>404 error</div><h1 class="cn-text-display-lg">We can’t find that page</h1><p class="cn-text-body-xl cn-auth__muted">Sorry, the page you are looking for doesn’t exist or has been moved.</p><div class="cn-row" data-gap="3">${BTN('outline', 'Go back', 'xl')}${BTN('primary', 'Take me home', 'xl')}</div></div></div></div>` },
27
+ ],
28
+ css: `
29
+ .cn-auth { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: {space.12} {space.4}; background-color: {color.bg-surface}; }
30
+ .cn-auth__card { width: 100%; max-width: 360px; display: flex; flex-direction: column; gap: {space.8}; }
31
+ .cn-auth__card--wide { max-width: 640px; }
32
+ .cn-auth__logo { display: inline-block; width: 48px; height: 48px; border-radius: {radius.xl}; background-color: {color.bg-action}; margin-bottom: {space.3}; }
33
+ .cn-auth__muted { color: {color.fg-muted}; }
34
+ .cn-auth__back { align-self: center; display: inline-flex; align-items: center; gap: {space.1.5}; }
35
+ .cn-auth__back-icon { transform: rotate(180deg); margin: 0; }
36
+ .cn-auth__code { display: flex; gap: {space.2}; justify-content: center; }
37
+ .cn-auth__digit { width: 64px; height: 64px; text-align: center; font-family: {font.family.sans}; font-size: {font.size.5xl}; font-weight: {font.weight.medium}; color: {color.fg-action}; border: {border.width.thin} solid {color.border-control}; border-radius: {radius.lg}; background-color: {color.bg-surface}; box-shadow: {shadow.xs}; }
38
+ .cn-auth__digit:focus { outline: none; border-color: {color.border-action}; box-shadow: {shadow.focus}; }
39
+ .cn-auth--split { display: grid; grid-template-columns: 1fr 1fr; padding: 0; min-height: 720px; }
40
+ .cn-auth__side { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: {space.12} {space.8}; position: relative; }
41
+ .cn-auth__foot { position: absolute; bottom: {space.8}; left: {space.8}; right: {space.8}; display: flex; flex-wrap: wrap; gap: {space.2} {space.6}; justify-content: space-between; }
42
+ @media (max-width: {breakpoint.md}) { .cn-auth--split { grid-template-columns: 1fr; } .cn-auth--split > .cn-placeholder { display: none; } .cn-auth__foot { position: static; margin-top: {space.8}; } }`,
43
+ viewport: 'both',
44
+ };
@@ -0,0 +1,39 @@
1
+ import { SIDEBAR, PAGE_HEADER, BTN, TABS, BADGE, ICON, APP_CSS, CARD, CARD_HEAD, FIELD, INPUT } from "./_app.mjs";
2
+ const PLAN = `<section class="cn-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Business plan', 'Renews on 12 October 2026', `${BTN('outline', 'Change plan', 'sm')}`, BADGE('Monthly', 'accent'))}</div><div class="cn-card__body cn-app__card-body cn-stack" data-gap="4"><div class="cn-row" data-justify="between" data-align="end"><span class="cn-text-heading-lg">$20<span class="cn-text-body-md cn-app__muted"> per user / month</span></span><span class="cn-text-body-sm cn-app__muted">14 of 20 seats used</span></div><div class="cn-progress" data-size="sm" data-tone="action" role="progressbar" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100" aria-label="Seats used"><div class="cn-progress__track"><div class="cn-progress__indicator" style="width:70%"></div></div></div></div></section>`;
3
+ const PAYMENT = `<section class="cn-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Payment method', 'Change how you pay for your plan.', BTN('outline', 'Edit', 'sm'))}</div><div class="cn-card__body cn-app__card-body"><div class="cn-billing__card"><span class="cn-billing__brand" aria-hidden="true">VISA</span><div class="cn-stack cn-flex-1" data-gap="0"><span class="cn-text-label-sm">Visa ending in 4242</span><span class="cn-text-body-sm cn-app__muted">Expiry 06/2028 · billing@lumen.co</span></div>${BADGE('Default', 'neutral')}</div></div></section>`;
4
+ const INVOICES = `<section class="cn-card cn-app__table-card" data-variant="default" data-padding="none"><div class="cn-section-header" data-variant="card" data-divider="yes">${CARD_HEAD('Billing history', 'Download invoices for your records.', BTN('outline', 'Download all', 'sm', ICON.external))}</div><div class="cn-card__body cn-app__card-body cn-app__card-body--flush" role="region" tabindex="0" aria-label="Scrollable table"><table class="cn-table" data-density="default" data-variant="default"><thead class="cn-table__head"><tr class="cn-table__row"><th class="cn-table__header-cell" scope="col">Invoice</th><th class="cn-table__header-cell" scope="col">Date</th><th class="cn-table__header-cell" scope="col">Status</th><th class="cn-table__header-cell" scope="col" data-align="right">Amount</th><th class="cn-table__header-cell" scope="col" data-align="right">Seats</th><th class="cn-table__header-cell" scope="col"><span class="cn-sr-only">Actions</span></th></tr></thead><tbody class="cn-table__body">${[['Sep 2026', 'Paid', 'success', '$280.00', 14], ['Aug 2026', 'Paid', 'success', '$260.00', 13], ['Jul 2026', 'Paid', 'success', '$240.00', 12], ['Jun 2026', 'Refunded', 'neutral', '$220.00', 11], ['May 2026', 'Failed', 'danger', '$220.00', 11]].map(([m, s, tone, a, n], i) => `<tr class="cn-table__row"><td class="cn-table__cell"><span class="cn-text-label-sm">Invoice #${2091 - i}</span></td><td class="cn-table__cell">${m}</td><td class="cn-table__cell">${BADGE(s, tone, 'soft', true)}</td><td class="cn-table__cell" data-align="right">${a}</td><td class="cn-table__cell" data-align="right">${n}</td><td class="cn-table__cell cn-table__actions">${BTN('link-color', 'Download', 'sm')}</td></tr>`).join('')}</tbody></table></div></section>`;
5
+ export const billingPage = {
6
+ name: 'Billing and checkout',
7
+ slug: 'billing-page',
8
+ category: 'app-page',
9
+ description: 'Plan and usage, payment method, billing history; a plan change with a comparison; and a checkout form with an order summary.',
10
+ rules: [
11
+ 'Billing page: plan card (name, renewal, price per seat, seat usage bar), payment method card (brand mark, last 4, expiry, Default badge), invoices table (number, date, status badge, amount right-aligned, download link).',
12
+ 'Failed payments show a danger Alert at the top with a single "Update payment method" action.',
13
+ 'Checkout: two columns — form (contact, card number with brand icon, expiry + CVC in a row, country) on the left, an order summary card on the right (line items, subtotal, tax, total in heading-md); the primary button names the amount ("Pay $280.00").',
14
+ 'Never mask everything: show the last 4 digits and the brand; never ask for the CVC outside the payment step.',
15
+ ],
16
+ html: `<div class="cn-app">${SIDEBAR('Settings')}<main class="cn-app__main">${PAGE_HEADER('Billing', 'Manage your plan, payment method and invoices.', BTN('outline', 'Contact billing support', 'md'))}${TABS(['Plan', 'Usage', 'Invoices', 'Tax'], 0)}<div class="cn-alert" data-tone="danger" data-variant="soft" data-layout="inline" role="alert"><span class="cn-alert__icon" aria-hidden="true">${ICON.warning}</span><div class="cn-alert__content"><div class="cn-alert__title">Your last payment failed</div><p class="cn-alert__description">We couldn’t charge the Visa ending in 4242 on 12 September. Update your payment method to keep your plan active.</p><div class="cn-alert__actions">${BTN('link-color', 'Update payment method', 'sm')}</div></div></div><div class="cn-grid" data-cols="2" data-gap="6">${PLAN}${PAYMENT}</div>${INVOICES}</main></div>`,
17
+ variants: [
18
+ { title: 'Change plan', html: `<div class="cn-app">${SIDEBAR('Settings')}<main class="cn-app__main">${PAGE_HEADER('Change plan', 'Switch plans any time. Changes are prorated on your next invoice.', BTN('ghost', 'Back to billing', 'md'))}<div class="cn-segmented-control" data-size="md" data-variant="default" role="radiogroup" aria-label="Billing period" style="align-self:flex-start"><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="true">Monthly</button><button type="button" class="cn-segmented-control__item" role="radio" aria-checked="false">Annual · save 20%</button></div><div class="cn-grid" data-cols="3" data-gap="6">${[['Basic', '$10', 'For small teams getting started.', ['Up to 10 users', 'Basic reporting', 'Email support'], false, false], ['Business', '$20', 'Your current plan.', ['Up to 20 users', 'Advanced reporting', 'Priority support', '200+ integrations'], true, false], ['Enterprise', '$40', 'Security, control and scale.', ['Unlimited users', 'Audit log', 'SSO / SAML', 'Dedicated manager'], false, true]].map(([n, p, d, items, cur, best]) => `<section class="cn-card cn-billing__plan" data-variant="default" data-padding="lg"${best ? ' data-selected' : ''}><div class="cn-card__body cn-stack" data-gap="5"><div class="cn-row" data-justify="between"><span class="cn-text-heading-sm">${n}</span>${cur ? BADGE('Current', 'accent') : best ? BADGE('Recommended', 'success') : ''}</div><span class="cn-text-heading-lg">${p}<span class="cn-text-body-sm cn-app__muted"> per user / month</span></span><span class="cn-text-body-sm cn-app__muted">${d}</span><ul class="cn-stack" data-gap="2">${items.map((it) => `<li class="cn-row" data-gap="2"><span class="cn-billing__tick">${ICON.check}</span><span class="cn-text-body-sm">${it}</span></li>`).join('')}</ul>${cur ? BTN('outline', 'Current plan', 'md') : BTN(best ? 'primary' : 'outline', 'Switch to ' + n, 'md')}</div></section>`).join('')}</div></main></div>` },
19
+ { title: 'Checkout', html: `<div class="cn-app">${SIDEBAR('Settings', 'slim')}<main class="cn-app__main">${PAGE_HEADER('Checkout', 'Upgrade to Enterprise · billed monthly', '')}<div class="cn-grid cn-billing__checkout" data-cols="3" data-gap="8"><form class="cn-app__span2 cn-stack" data-gap="8"><section class="cn-stack" data-gap="5"><h2 class="cn-text-heading-sm">Contact</h2>${FIELD('b-email', 'Email', INPUT('b-email', 'billing@lumen.co', 'email'))}</section><section class="cn-stack" data-gap="5"><h2 class="cn-text-heading-sm">Payment</h2>${FIELD('b-card', 'Card number', `<div class="cn-input" data-variant="default" data-size="md"><span class="cn-billing__brand cn-billing__brand--sm" aria-hidden="true">VISA</span><input class="cn-input__field" id="b-card" type="text" inputmode="numeric" placeholder="1234 1234 1234 1234"></div>`)}<div class="cn-grid" data-cols="2" data-gap="5" data-keep>${FIELD('b-exp', 'Expiry', INPUT('b-exp', '', 'text', 'MM / YY'))}${FIELD('b-cvc', 'CVC', INPUT('b-cvc', '', 'text', '123'))}</div>${FIELD('b-name', 'Name on card', INPUT('b-name', 'Maya Chen'))}${FIELD('b-country', 'Country', `<div class="cn-select" data-variant="default" data-size="md"><select class="cn-select__field" id="b-country" aria-label="Country"><option>Brazil</option><option selected>United States</option><option>Portugal</option></select><svg class="cn-select__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></div>`)}</section></form><aside>${CARD(CARD_HEAD('Order summary'), `<div class="cn-stack" data-gap="4"><dl class="cn-billing__lines"><div><dt>Enterprise plan × 14 seats</dt><dd>$560.00</dd></div><div><dt>Credit from current plan</dt><dd>−$186.67</dd></div><div><dt>Tax (0%)</dt><dd>$0.00</dd></div><div class="cn-billing__total"><dt>Due today</dt><dd>$373.33</dd></div></dl><span class="cn-text-body-sm cn-app__muted">Then $560.00 on the 12th of each month. Cancel any time.</span>${BTN('primary', 'Pay $373.33', 'lg')}<span class="cn-text-body-xs cn-app__muted cn-center">Payments are processed securely. By paying you agree to the <a href="#" class="cn-link" data-variant="inline">terms</a>.</span></div>`)}</aside></div></main></div>` },
20
+ ],
21
+ css: `${APP_CSS}
22
+ .cn-app__span2 { grid-column: span 2; min-width: 0; }
23
+ .cn-billing__card { display: flex; align-items: center; gap: {space.4}; padding: {space.4}; border: {border.width.thin} solid {color.border-default}; border-radius: {radius.card}; }
24
+ .cn-billing__brand { display: inline-flex; align-items: center; justify-content: center; width: 46px; height: 32px; border-radius: {radius.sm}; border: {border.width.thin} solid {color.border-default}; background-color: {color.bg-surface}; font-family: {font.family.sans}; font-size: 10px; font-weight: {font.weight.bold}; letter-spacing: 0.04em; color: {brand.700}; flex-shrink: 0; }
25
+ .cn-billing__brand--sm { width: 34px; height: 22px; margin-inline-start: {space.3}; font-size: 8px; }
26
+ .cn-billing__plan[data-selected] { border-color: {color.border-action}; box-shadow: 0 0 0 1px {color.border-action}; }
27
+ .cn-billing__plan .cn-button { width: 100%; }
28
+ .cn-billing__tick { display: inline-flex; color: {color.fg-success}; }
29
+ .cn-billing__tick .cn-icon { width: 16px; height: 16px; }
30
+ .cn-billing__lines { margin: 0; display: flex; flex-direction: column; gap: {space.3}; }
31
+ .cn-billing__lines > div { display: flex; justify-content: space-between; gap: {space.4}; font-size: {font.size.sm}; color: {color.fg-muted}; }
32
+ .cn-billing__lines dd { margin: 0; color: {color.fg-default}; font-variant-numeric: tabular-nums; }
33
+ .cn-billing__total { padding-top: {space.3}; border-top: {border.width.thin} solid {color.border-default}; font-weight: {font.weight.semibold}; }
34
+ .cn-billing__total dt, .cn-billing__total dd { color: {color.fg-default}; font-size: {font.size.lg}; }
35
+ .cn-billing__checkout aside { position: sticky; top: {space.8}; }
36
+ .cn-billing__checkout .cn-button { width: 100%; }
37
+ @media (max-width: {breakpoint.md}) { .cn-app__span2 { grid-column: auto; } }`,
38
+ viewport: 'desktop',
39
+ };