@erclx/canon 4.35.0 → 4.36.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.35.0",
4
+ "version": "4.36.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.35.0",
4
+ "version": "4.36.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -30,6 +30,11 @@ export const LAYOUTS: LayoutInfo[] = [
30
30
  { name: 'stat-callout', description: 'Row of large stats with captions' },
31
31
  { name: 'grid', description: 'Two-by-two cards, each a term and detail' },
32
32
  { name: 'quote', description: 'Large pull quote with attribution' },
33
+ {
34
+ name: 'freeform',
35
+ description:
36
+ 'Explicit shapes at a declared position and size, for a figure the other layouts cannot draw',
37
+ },
33
38
  ]
34
39
 
35
40
  const LAYOUT_NAMES = new Set(LAYOUTS.map((layout) => layout.name))
@@ -55,6 +60,7 @@ const RENDERERS: Record<
55
60
  'stat-callout': renderStatCallout,
56
61
  grid: renderGrid,
57
62
  quote: renderQuote,
63
+ freeform: renderFreeform,
58
64
  }
59
65
 
60
66
  export function renderDeckSlide(
@@ -346,6 +352,81 @@ function renderQuote(s: PptxSlide, slide: Slide, theme: Theme): void {
346
352
  }
347
353
  }
348
354
 
355
+ const SHAPE_KEYS = new Set(['x', 'y', 'w', 'h', 'color'])
356
+ const COLOR_ROLES: (keyof Theme)[] = [
357
+ 'background',
358
+ 'surface',
359
+ 'ink',
360
+ 'muted',
361
+ 'accent',
362
+ ]
363
+
364
+ function renderFreeform(s: PptxSlide, slide: Slide, theme: Theme): void {
365
+ addTitle(s, slide.title, theme)
366
+ for (const line of slide.content) {
367
+ if (!line.trim().startsWith('-')) throw malformedShape(slide.title, line)
368
+ renderShapeLine(s, line, slide.title, theme)
369
+ }
370
+ }
371
+
372
+ function malformedShape(title: string, line: string): Error {
373
+ return new Error(
374
+ `Malformed freeform shape on slide "${title}": ${line.trim()}`,
375
+ )
376
+ }
377
+
378
+ function renderShapeLine(
379
+ s: PptxSlide,
380
+ line: string,
381
+ title: string,
382
+ theme: Theme,
383
+ ): void {
384
+ const stripped = line.replace(/^\s*-\s*/, '')
385
+ const colonIndex = stripped.indexOf(':')
386
+ const head = (
387
+ colonIndex === -1 ? stripped : stripped.slice(0, colonIndex)
388
+ ).trim()
389
+ const text = colonIndex === -1 ? '' : stripped.slice(colonIndex + 1).trim()
390
+
391
+ const tokens = head.split(/\s+/).filter(Boolean)
392
+ const kind = tokens.shift()
393
+ if (kind !== 'text' && kind !== 'rect') throw malformedShape(title, line)
394
+ if (kind === 'text' && colonIndex === -1) throw malformedShape(title, line)
395
+
396
+ const attrs: Record<string, string> = {}
397
+ for (const token of tokens) {
398
+ const match = token.match(/^(\w+)=(.+)$/)
399
+ if (!match) throw malformedShape(title, line)
400
+ if (!SHAPE_KEYS.has(match[1])) throw malformedShape(title, line)
401
+ attrs[match[1]] = match[2]
402
+ }
403
+
404
+ const x = Number(attrs.x)
405
+ const y = Number(attrs.y)
406
+ const w = Number(attrs.w)
407
+ const h = Number(attrs.h)
408
+ if (![x, y, w, h].every(Number.isFinite)) throw malformedShape(title, line)
409
+
410
+ const role = attrs.color as keyof Theme
411
+ if (!COLOR_ROLES.includes(role)) throw malformedShape(title, line)
412
+ const color = theme[role]
413
+
414
+ if (kind === 'text') {
415
+ s.addText(clean(text), {
416
+ x,
417
+ y,
418
+ w,
419
+ h,
420
+ fontFace: FONTS.body,
421
+ fontSize: TYPE.body,
422
+ color,
423
+ valign: 'top',
424
+ })
425
+ } else {
426
+ s.addShape('rect', { x, y, w, h, fill: { color } })
427
+ }
428
+ }
429
+
349
430
  function addTitle(s: PptxSlide, title: string, theme: Theme): void {
350
431
  if (!title) return
351
432
  s.addText(clean(title), {