@nusoft/nuos-build-catalogue 0.26.0 → 0.28.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/dist/cli.js CHANGED
@@ -427,6 +427,9 @@ Usage:
427
427
  nuos-catalogue mode print the current operator mode
428
428
  nuos-catalogue mode <name> set operator mode: coaching | standard | developer
429
429
 
430
+ nuos-catalogue render regenerate HTML companion views for visual registers
431
+ nuos-catalogue render <register> just one register: surfaces | design-system | maps | architecture
432
+
430
433
  nuos-catalogue swarm status [--limit=N] list recent /build-wu runs
431
434
  nuos-catalogue swarm cost aggregate cost across swarm runs
432
435
 
@@ -567,6 +570,17 @@ async function main() {
567
570
  process.exit(code);
568
571
  break;
569
572
  }
573
+ case 'render': {
574
+ const { cmdRender } = await import('./commands/render.js');
575
+ const code = await cmdRender({
576
+ cwd: process.cwd(),
577
+ positional: args.positional[0],
578
+ buildRootFlag: args.flags['build-root'],
579
+ });
580
+ if (code !== 0)
581
+ process.exit(code);
582
+ break;
583
+ }
570
584
  case 'swarm': {
571
585
  const sub = args.positional[0];
572
586
  const { cmdSwarmStatus, cmdSwarmCost } = await import('./commands/swarm.js');
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `nuos-catalogue render [register]` — regenerate the HTML companion views
3
+ * for the visual registers (ui-ux, design-system, maps, architecture).
4
+ *
5
+ * nuos-catalogue render # render all four
6
+ * nuos-catalogue render surfaces # render just ui-ux/_view.html
7
+ * nuos-catalogue render design-system # render just design-system/_view.html
8
+ * nuos-catalogue render maps # render just maps/_view.html
9
+ * nuos-catalogue render architecture # render just architecture/_view.html
10
+ *
11
+ * Companion HTML files are *generated artefacts*. The canonical source for
12
+ * every register stays markdown — every agent (architect, coder, reviewer)
13
+ * reads markdown. The HTML exists so the operator can review inherently visual
14
+ * artefacts in their natural medium.
15
+ */
16
+ export interface RenderCommandOptions {
17
+ cwd?: string;
18
+ /** Optional register name (`surfaces`, `design-system`, `maps`, `architecture`). */
19
+ positional?: string;
20
+ /** Override for --build-root flag. */
21
+ buildRootFlag?: string | boolean;
22
+ /** Override "now" for deterministic test output. */
23
+ now?: () => Date;
24
+ }
25
+ export declare function cmdRender(options?: RenderCommandOptions): Promise<number>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `nuos-catalogue render [register]` — regenerate the HTML companion views
3
+ * for the visual registers (ui-ux, design-system, maps, architecture).
4
+ *
5
+ * nuos-catalogue render # render all four
6
+ * nuos-catalogue render surfaces # render just ui-ux/_view.html
7
+ * nuos-catalogue render design-system # render just design-system/_view.html
8
+ * nuos-catalogue render maps # render just maps/_view.html
9
+ * nuos-catalogue render architecture # render just architecture/_view.html
10
+ *
11
+ * Companion HTML files are *generated artefacts*. The canonical source for
12
+ * every register stays markdown — every agent (architect, coder, reviewer)
13
+ * reads markdown. The HTML exists so the operator can review inherently visual
14
+ * artefacts in their natural medium.
15
+ */
16
+ import path from 'node:path';
17
+ import { resolveBuildRoot } from '../path-resolution.js';
18
+ import { RENDERABLE_REGISTERS, runRender } from '../render/run.js';
19
+ export async function cmdRender(options = {}) {
20
+ const buildRoot = resolveBuildRoot(options.buildRootFlag, { cwd: options.cwd ?? process.cwd() });
21
+ let only;
22
+ if (options.positional && options.positional !== 'all') {
23
+ if (!RENDERABLE_REGISTERS.includes(options.positional)) {
24
+ console.error(`unknown register: ${options.positional}`);
25
+ console.error(`available: ${RENDERABLE_REGISTERS.join(', ')}`);
26
+ return 1;
27
+ }
28
+ only = [options.positional];
29
+ }
30
+ const report = await runRender({ buildRoot, only, now: options.now });
31
+ for (const r of report.results) {
32
+ const status = r.written ? '✓' : '·';
33
+ const rel = path.relative(process.cwd(), r.outPath);
34
+ console.log(` ${status} ${r.register.padEnd(16)} ${r.detail.padEnd(20)} ${r.written ? rel : ''}`);
35
+ }
36
+ const writtenCount = report.results.filter((r) => r.written).length;
37
+ console.log('');
38
+ console.log(`${writtenCount}/${report.results.length} register${report.results.length === 1 ? '' : 's'} rendered.`);
39
+ return 0;
40
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Render the `architecture/` register as a module gallery with cross-links.
3
+ *
4
+ * Each module becomes a card showing its responsibility (one paragraph), the
5
+ * personas/modules it depends on, and the contracts it owns. The cards link
6
+ * to each other where dependencies are named, so the operator can navigate the
7
+ * module graph visually instead of by grep.
8
+ */
9
+ export interface ArchitectureRenderResult {
10
+ written: boolean;
11
+ outPath: string;
12
+ moduleCount: number;
13
+ }
14
+ export declare function renderArchitecture(opts: {
15
+ buildRoot: string;
16
+ projectName: string;
17
+ generatedAt: string;
18
+ }): Promise<ArchitectureRenderResult>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Render the `architecture/` register as a module gallery with cross-links.
3
+ *
4
+ * Each module becomes a card showing its responsibility (one paragraph), the
5
+ * personas/modules it depends on, and the contracts it owns. The cards link
6
+ * to each other where dependencies are named, so the operator can navigate the
7
+ * module graph visually instead of by grep.
8
+ */
9
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { parseSections, isPlaceholder } from './parser.js';
13
+ import { pageWrapper, card, emptyState, escapeHtml, renderProse, } from './html.js';
14
+ const SKIP_FILES = new Set(['_index.md', 'module-template.md']);
15
+ export async function renderArchitecture(opts) {
16
+ const dir = path.join(opts.buildRoot, 'architecture');
17
+ const outPath = path.join(dir, '_view.html');
18
+ if (!existsSync(dir)) {
19
+ return { written: false, outPath, moduleCount: 0 };
20
+ }
21
+ const entries = await readdir(dir, { withFileTypes: true });
22
+ const modules = [];
23
+ for (const entry of entries) {
24
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
25
+ continue;
26
+ if (SKIP_FILES.has(entry.name))
27
+ continue;
28
+ const md = await readFile(path.join(dir, entry.name), 'utf8');
29
+ if (isPlaceholder(md))
30
+ continue;
31
+ const titleMatch = md.match(/^#\s+(.+)$/m);
32
+ modules.push({
33
+ file: entry.name,
34
+ slug: entry.name.replace(/\.md$/, ''),
35
+ title: titleMatch ? titleMatch[1].trim() : entry.name,
36
+ sections: parseSections(md),
37
+ });
38
+ }
39
+ modules.sort((a, b) => a.title.localeCompare(b.title));
40
+ const body = modules.length === 0
41
+ ? card('No modules filed yet', emptyState('Phase B of planning produces this content.'))
42
+ : [renderModuleIndex(modules), ...modules.map(renderModuleCard)].join('');
43
+ const html = pageWrapper({
44
+ title: 'Architecture',
45
+ projectName: opts.projectName,
46
+ generatedAt: opts.generatedAt,
47
+ sourceNote: '<code>docs/build/architecture/</code>',
48
+ body,
49
+ });
50
+ await mkdir(dir, { recursive: true });
51
+ await writeFile(outPath, html, 'utf8');
52
+ return { written: true, outPath, moduleCount: modules.length };
53
+ }
54
+ function renderModuleIndex(modules) {
55
+ const items = modules
56
+ .map((m) => `<li><a href="#${escapeHtml(m.slug)}">${escapeHtml(m.title)}</a></li>`)
57
+ .join('');
58
+ return card('Modules', `<ul class="sitemap-list">${items}</ul>`);
59
+ }
60
+ function renderModuleCard(m) {
61
+ const sectionBlocks = [];
62
+ for (const [heading, body] of m.sections) {
63
+ if (heading === '' || heading.toLowerCase() === 'notes')
64
+ continue;
65
+ const rendered = body.trim() ? renderProse(body) : '';
66
+ if (!rendered)
67
+ continue;
68
+ sectionBlocks.push(`<h4>${escapeHtml(heading)}</h4>${rendered}`);
69
+ }
70
+ return `<section id="${escapeHtml(m.slug)}" class="card">
71
+ <h3>${escapeHtml(m.title)}</h3>
72
+ <div class="card__body">${sectionBlocks.join('')}</div>
73
+ </section>`;
74
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Render the `design-system/` register as an interactive companion gallery.
3
+ *
4
+ * What gets rendered:
5
+ * - Colour tokens — as visual swatches with the hex value beside the name.
6
+ * - Type scale — each step rendered at its declared px size with its line-height
7
+ * and weight applied so the operator sees the real shape, not a description.
8
+ * - Spacing scale — rendered as horizontal bars at the actual pixel widths.
9
+ * - Radius & elevation — small boxes showing the corner radius applied.
10
+ * - Motion — table only (no animation; would push complexity past MVP scope).
11
+ * - Components + patterns — name list with first-paragraph summary.
12
+ * - Voice + accessibility — first-paragraph summary blocks.
13
+ *
14
+ * Token files use GFM tables with a `Token` column and a value column. The parser
15
+ * looks for either a `Hex` / `Value` / `Size` / `Used for` column shape and falls
16
+ * back to "list the table verbatim" when the shape isn't recognised.
17
+ */
18
+ export interface DesignSystemRenderResult {
19
+ written: boolean;
20
+ outPath: string;
21
+ }
22
+ export declare function renderDesignSystem(opts: {
23
+ buildRoot: string;
24
+ projectName: string;
25
+ generatedAt: string;
26
+ }): Promise<DesignSystemRenderResult>;
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Render the `design-system/` register as an interactive companion gallery.
3
+ *
4
+ * What gets rendered:
5
+ * - Colour tokens — as visual swatches with the hex value beside the name.
6
+ * - Type scale — each step rendered at its declared px size with its line-height
7
+ * and weight applied so the operator sees the real shape, not a description.
8
+ * - Spacing scale — rendered as horizontal bars at the actual pixel widths.
9
+ * - Radius & elevation — small boxes showing the corner radius applied.
10
+ * - Motion — table only (no animation; would push complexity past MVP scope).
11
+ * - Components + patterns — name list with first-paragraph summary.
12
+ * - Voice + accessibility — first-paragraph summary blocks.
13
+ *
14
+ * Token files use GFM tables with a `Token` column and a value column. The parser
15
+ * looks for either a `Hex` / `Value` / `Size` / `Used for` column shape and falls
16
+ * back to "list the table verbatim" when the shape isn't recognised.
17
+ */
18
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
19
+ import { existsSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { parseSections, parseTable, firstParagraph, isPlaceholder, extractHexColours, } from './parser.js';
22
+ import { pageWrapper, card, emptyState, escapeHtml, renderInlineMarkdown, renderProse, } from './html.js';
23
+ export async function renderDesignSystem(opts) {
24
+ const dir = path.join(opts.buildRoot, 'design-system');
25
+ const outPath = path.join(dir, '_view.html');
26
+ if (!existsSync(dir)) {
27
+ return { written: false, outPath };
28
+ }
29
+ const blocks = [];
30
+ blocks.push(await renderTokenFile(dir, 'tokens-colour.md', renderColourTokens));
31
+ blocks.push(await renderTokenFile(dir, 'tokens-typography.md', renderTypeTokens));
32
+ blocks.push(await renderTokenFile(dir, 'tokens-spacing.md', renderSpacingTokens));
33
+ blocks.push(await renderTokenFile(dir, 'tokens-radius-elevation.md', renderRadiusTokens));
34
+ blocks.push(await renderTokenFile(dir, 'tokens-motion.md', renderMotionTokens));
35
+ blocks.push(await renderListing(path.join(dir, 'components'), 'Components'));
36
+ blocks.push(await renderListing(path.join(dir, 'patterns'), 'Patterns'));
37
+ blocks.push(await renderPlainFile(dir, 'voice.md', 'Voice'));
38
+ blocks.push(await renderPlainFile(dir, 'accessibility.md', 'Accessibility'));
39
+ const body = blocks.filter(Boolean).join('\n');
40
+ const html = pageWrapper({
41
+ title: 'Design system',
42
+ projectName: opts.projectName,
43
+ generatedAt: opts.generatedAt,
44
+ sourceNote: '<code>docs/build/design-system/</code>',
45
+ body: body || card('Design system not yet populated', emptyState('Phase C of planning populates this register.')),
46
+ });
47
+ await mkdir(dir, { recursive: true });
48
+ await writeFile(outPath, html, 'utf8');
49
+ return { written: true, outPath };
50
+ }
51
+ async function renderTokenFile(dir, filename, renderer) {
52
+ const filePath = path.join(dir, filename);
53
+ if (!existsSync(filePath))
54
+ return '';
55
+ const md = await readFile(filePath, 'utf8');
56
+ if (isPlaceholder(md)) {
57
+ return card(prettyTitle(filename), emptyState(`Not yet filled in (${filename}).`), { tone: 'muted' });
58
+ }
59
+ const sections = parseSections(md);
60
+ return renderer(sections, md);
61
+ }
62
+ function renderColourTokens(sections, md) {
63
+ const swatchBlocks = [];
64
+ for (const [heading, body] of sections) {
65
+ if (heading === '')
66
+ continue;
67
+ const table = parseTable(body);
68
+ if (!table)
69
+ continue;
70
+ const tokenColumn = findColumn(table, ['Token', 'Name']);
71
+ const hexColumn = findColumn(table, ['Hex', 'Value']);
72
+ const useColumn = findColumn(table, ['Used for', 'Role', 'Description']);
73
+ if (tokenColumn === -1)
74
+ continue;
75
+ const swatches = table.rows
76
+ .map((row) => {
77
+ const token = stripInline(row[tokenColumn] ?? '');
78
+ const value = stripInline(row[hexColumn === -1 ? 1 : hexColumn] ?? '');
79
+ const purpose = useColumn === -1 ? '' : row[useColumn] ?? '';
80
+ const hexes = extractHexColours(value);
81
+ const hex = hexes[0];
82
+ if (!hex)
83
+ return '';
84
+ return `<div class="swatch">
85
+ <div class="swatch__chip" style="background:${escapeHtml(hex)}"></div>
86
+ <div class="swatch__meta">
87
+ <code>${escapeHtml(token)}</code>
88
+ <small>${escapeHtml(hex)}${purpose ? ` · ${escapeHtml(stripInline(purpose))}` : ''}</small>
89
+ </div>
90
+ </div>`;
91
+ })
92
+ .filter(Boolean)
93
+ .join('');
94
+ if (swatches) {
95
+ swatchBlocks.push(`<h4>${escapeHtml(heading)}</h4>${swatches}`);
96
+ }
97
+ }
98
+ if (swatchBlocks.length === 0) {
99
+ return card('Colour tokens', renderProse(md));
100
+ }
101
+ return card('Colour tokens', swatchBlocks.join(''));
102
+ }
103
+ function renderTypeTokens(sections, md) {
104
+ const blocks = [];
105
+ for (const [heading, body] of sections) {
106
+ if (heading === '')
107
+ continue;
108
+ const table = parseTable(body);
109
+ if (!table)
110
+ continue;
111
+ const tokenCol = findColumn(table, ['Token', 'Name']);
112
+ const sizeCol = findColumn(table, ['Size']);
113
+ const lineCol = findColumn(table, ['Line height']);
114
+ const weightCol = findColumn(table, ['Weight']);
115
+ const useCol = findColumn(table, ['Used for']);
116
+ if (tokenCol === -1)
117
+ continue;
118
+ if (sizeCol !== -1) {
119
+ const samples = table.rows
120
+ .map((row) => {
121
+ const token = stripInline(row[tokenCol] ?? '');
122
+ const size = stripInline(row[sizeCol] ?? '');
123
+ const line = lineCol === -1 ? '' : stripInline(row[lineCol] ?? '');
124
+ const weight = weightCol === -1 ? '' : stripInline(row[weightCol] ?? '');
125
+ const use = useCol === -1 ? '' : stripInline(row[useCol] ?? '');
126
+ const cssSize = parsePxOrRem(size) ?? '16px';
127
+ const cssLine = parseUnitless(line) ?? '1.4';
128
+ const cssWeight = parseWeight(weight) ?? '400';
129
+ return `<div class="type-row">
130
+ <span class="type-sample" style="font-size:${cssSize};line-height:${cssLine};font-weight:${cssWeight}">The quick brown fox</span>
131
+ <small><code>${escapeHtml(token)}</code> · ${escapeHtml([size, weight, use].filter(Boolean).join(' · '))}</small>
132
+ </div>`;
133
+ })
134
+ .join('');
135
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${samples}`);
136
+ }
137
+ else {
138
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${renderTableAsTokens(table)}`);
139
+ }
140
+ }
141
+ if (blocks.length === 0)
142
+ return card('Typography', renderProse(md));
143
+ return card('Typography', blocks.join(''));
144
+ }
145
+ function renderSpacingTokens(sections, md) {
146
+ const blocks = [];
147
+ for (const [heading, body] of sections) {
148
+ if (heading === '')
149
+ continue;
150
+ const table = parseTable(body);
151
+ if (!table)
152
+ continue;
153
+ const tokenCol = findColumn(table, ['Token', 'Name', 'Step']);
154
+ const valCol = findColumn(table, ['Value', 'Size', 'px']);
155
+ if (tokenCol === -1 || valCol === -1) {
156
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${renderTableAsTokens(table)}`);
157
+ continue;
158
+ }
159
+ const bars = table.rows
160
+ .map((row) => {
161
+ const token = stripInline(row[tokenCol] ?? '');
162
+ const value = stripInline(row[valCol] ?? '');
163
+ const px = parsePx(value);
164
+ if (!px)
165
+ return '';
166
+ return `<div class="spacing-row">
167
+ <small><code>${escapeHtml(token)}</code> · ${escapeHtml(value)}</small>
168
+ <div class="spacing-bar" style="width:${px}px;max-width:100%"></div>
169
+ </div>`;
170
+ })
171
+ .filter(Boolean)
172
+ .join('');
173
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${bars || renderTableAsTokens(table)}`);
174
+ }
175
+ if (blocks.length === 0)
176
+ return card('Spacing', renderProse(md));
177
+ return card('Spacing', blocks.join(''));
178
+ }
179
+ function renderRadiusTokens(sections, md) {
180
+ const blocks = [];
181
+ for (const [heading, body] of sections) {
182
+ if (heading === '')
183
+ continue;
184
+ const table = parseTable(body);
185
+ if (!table)
186
+ continue;
187
+ const tokenCol = findColumn(table, ['Token', 'Name']);
188
+ const valCol = findColumn(table, ['Value', 'Radius', 'Size']);
189
+ if (tokenCol === -1 || valCol === -1) {
190
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${renderTableAsTokens(table)}`);
191
+ continue;
192
+ }
193
+ const examples = table.rows
194
+ .map((row) => {
195
+ const token = stripInline(row[tokenCol] ?? '');
196
+ const value = stripInline(row[valCol] ?? '');
197
+ const px = parsePx(value);
198
+ return `<div class="radius-row">
199
+ <div class="radius-chip" style="border-radius:${px ?? 0}px"></div>
200
+ <small><code>${escapeHtml(token)}</code> · ${escapeHtml(value)}</small>
201
+ </div>`;
202
+ })
203
+ .join('');
204
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${examples}`);
205
+ }
206
+ if (blocks.length === 0)
207
+ return card('Radius & elevation', renderProse(md));
208
+ return card('Radius & elevation', blocks.join(''));
209
+ }
210
+ function renderMotionTokens(sections, md) {
211
+ const blocks = [];
212
+ for (const [heading, body] of sections) {
213
+ if (heading === '')
214
+ continue;
215
+ const table = parseTable(body);
216
+ if (table)
217
+ blocks.push(`<h4>${escapeHtml(heading)}</h4>${renderTableAsTokens(table)}`);
218
+ }
219
+ if (blocks.length === 0)
220
+ return card('Motion', renderProse(md));
221
+ return card('Motion', blocks.join(''));
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Components / patterns listing
225
+ // ---------------------------------------------------------------------------
226
+ async function renderListing(dir, title) {
227
+ if (!existsSync(dir))
228
+ return '';
229
+ const entries = await readdir(dir, { withFileTypes: true });
230
+ const items = [];
231
+ for (const entry of entries) {
232
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
233
+ continue;
234
+ if (entry.name === '_index.md' || entry.name === '_template.md')
235
+ continue;
236
+ const md = await readFile(path.join(dir, entry.name), 'utf8');
237
+ if (isPlaceholder(md))
238
+ continue;
239
+ const titleMatch = md.match(/^#\s+(.+)$/m);
240
+ const name = titleMatch ? titleMatch[1].trim() : entry.name;
241
+ const summary = firstParagraph(md);
242
+ items.push(`<div class="listing-item">
243
+ <strong>${escapeHtml(name)}</strong>
244
+ ${summary ? `<p>${renderInlineMarkdown(summary)}</p>` : ''}
245
+ <small><code>${escapeHtml(entry.name)}</code></small>
246
+ </div>`);
247
+ }
248
+ if (items.length === 0)
249
+ return card(title, emptyState('None filed yet.'), { tone: 'muted' });
250
+ return card(title, items.join(''));
251
+ }
252
+ async function renderPlainFile(dir, filename, title) {
253
+ const filePath = path.join(dir, filename);
254
+ if (!existsSync(filePath))
255
+ return '';
256
+ const md = await readFile(filePath, 'utf8');
257
+ if (isPlaceholder(md)) {
258
+ return card(title, emptyState(`Not yet filled in (${filename}).`), { tone: 'muted' });
259
+ }
260
+ const summary = firstParagraph(md);
261
+ return card(title, summary ? `<p>${renderInlineMarkdown(summary)}</p>` : renderProse(md));
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // Helpers
265
+ // ---------------------------------------------------------------------------
266
+ function prettyTitle(filename) {
267
+ return filename
268
+ .replace(/\.md$/, '')
269
+ .replace(/^tokens-/, '')
270
+ .replace(/-/g, ' ')
271
+ .replace(/^./, (c) => c.toUpperCase());
272
+ }
273
+ function findColumn(table, candidates) {
274
+ const lower = table.headers.map((h) => h.toLowerCase());
275
+ for (const c of candidates) {
276
+ const idx = lower.indexOf(c.toLowerCase());
277
+ if (idx >= 0)
278
+ return idx;
279
+ }
280
+ return -1;
281
+ }
282
+ function stripInline(s) {
283
+ return s.replace(/^`(.+)`$/, '$1').replace(/\\\|/g, '|').trim();
284
+ }
285
+ function renderTableAsTokens(table) {
286
+ const head = table.headers.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
287
+ const rows = table.rows
288
+ .map((row) => row.map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`).join(''))
289
+ .map((r) => `<tr>${r}</tr>`)
290
+ .join('');
291
+ return `<table class="tokens-table"><thead><tr>${head}</tr></thead><tbody>${rows}</tbody></table>`;
292
+ }
293
+ function parsePxOrRem(s) {
294
+ const m = s.match(/(\d+(?:\.\d+)?)(px|rem|em)/);
295
+ return m ? `${m[1]}${m[2]}` : null;
296
+ }
297
+ function parsePx(s) {
298
+ const m = s.match(/(\d+(?:\.\d+)?)\s*px/i);
299
+ return m ? Number(m[1]) : null;
300
+ }
301
+ function parseUnitless(s) {
302
+ const m = s.match(/^(\d+(?:\.\d+)?)$/);
303
+ return m ? m[1] : null;
304
+ }
305
+ function parseWeight(s) {
306
+ const m = s.match(/\b(100|200|300|400|500|600|700|800|900)\b/);
307
+ return m ? m[1] : null;
308
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * HTML primitives shared across the per-register renderers.
3
+ *
4
+ * Design notes:
5
+ * - Pure static HTML. No JS, no framework, no external CSS or fonts.
6
+ * - One self-contained file per register so the operator can open it directly
7
+ * in a browser or share it without a build step.
8
+ * - Inline styles in a single `<style>` block; CSS variables for the few values
9
+ * the renderers care about (token swatches, type previews) so they can be
10
+ * overridden per-block without restating the rule set.
11
+ * - The visual language is deliberately neutral — cards, restrained spacing,
12
+ * system fonts. Consumer projects with strong design opinions can post-process
13
+ * or skip companion rendering; the default isn't trying to be a brand.
14
+ */
15
+ export declare function escapeHtml(s: string): string;
16
+ /**
17
+ * Convert a single line of markdown to inline HTML — `code`, **bold**, *italic*,
18
+ * and [text](url) links. Block-level constructs (headings, lists, tables) are not
19
+ * processed; callers parse those structurally and use this helper on the cells.
20
+ */
21
+ export declare function renderInlineMarkdown(s: string): string;
22
+ /**
23
+ * Render a multi-line markdown block as paragraphs. Lists become `<ul>`/`<ol>`.
24
+ * Sub-headings (### / ####) become `<h4>`. Tables are skipped — the renderer should
25
+ * have parsed those structurally before getting here.
26
+ */
27
+ export declare function renderProse(md: string): string;
28
+ export declare function card(title: string, body: string, opts?: {
29
+ tone?: 'default' | 'muted';
30
+ }): string;
31
+ export declare function grid(items: string[], opts?: {
32
+ columns?: 'auto' | 'wide';
33
+ }): string;
34
+ export declare function emptyState(message: string): string;
35
+ /**
36
+ * Wrap rendered fragments into a self-contained HTML document.
37
+ *
38
+ * The doc carries an explicit "generated, do not edit" banner so the operator
39
+ * doesn't try to hand-edit the file — the source of truth is the markdown.
40
+ */
41
+ export declare function pageWrapper(opts: {
42
+ title: string;
43
+ projectName: string;
44
+ generatedAt: string;
45
+ sourceNote: string;
46
+ body: string;
47
+ }): string;