@bendyline/squisq 1.5.0 → 1.5.2

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 (135) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BsMkmzH8.d.ts → Doc-BBVGq1_9.d.ts} +94 -15
  4. package/dist/{ImageEditDoc-DvoeHMP9.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +1 -1
  5. package/dist/{chunk-4ZFRZBU2.js → chunk-5TYS5V3G.js} +866 -192
  6. package/dist/chunk-5TYS5V3G.js.map +1 -0
  7. package/dist/{chunk-7MQGNR76.js → chunk-A4O7GIWE.js} +2 -27
  8. package/dist/chunk-A4O7GIWE.js.map +1 -0
  9. package/dist/{chunk-OH43RPTP.js → chunk-BEQODJWV.js} +20 -3
  10. package/dist/chunk-BEQODJWV.js.map +1 -0
  11. package/dist/chunk-ET53IIEP.js +1 -0
  12. package/dist/chunk-ET53IIEP.js.map +1 -0
  13. package/dist/{chunk-KZNXZ5SM.js → chunk-GPLTCSXO.js} +18 -7
  14. package/dist/chunk-GPLTCSXO.js.map +1 -0
  15. package/dist/{chunk-OKJOQXK4.js → chunk-HWVFJAAH.js} +38 -2
  16. package/dist/chunk-HWVFJAAH.js.map +1 -0
  17. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  18. package/dist/chunk-IMSCRLLV.js.map +1 -0
  19. package/dist/{chunk-4AKUQPEE.js → chunk-KKNUBQ6Y.js} +1 -1
  20. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  21. package/dist/{chunk-TVSGLUID.js → chunk-LDQ2HJIX.js} +3 -3
  22. package/dist/{chunk-RUDYOTA6.js → chunk-LTN6I7QW.js} +56 -1
  23. package/dist/chunk-LTN6I7QW.js.map +1 -0
  24. package/dist/{chunk-2OIBZYKP.js → chunk-P3K7NLHF.js} +2 -2
  25. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  26. package/dist/chunk-T5UK6YOB.js.map +1 -0
  27. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  28. package/dist/chunk-TQWLI6S2.js.map +1 -0
  29. package/dist/{chunk-LRCV7N7F.js → chunk-UY7KGQ5R.js} +2 -2
  30. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  31. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  32. package/dist/doc/index.d.ts +4 -4
  33. package/dist/doc/index.js +24 -9
  34. package/dist/generate/index.d.ts +2 -80
  35. package/dist/generate/index.js +2 -7
  36. package/dist/imageEdit/index.d.ts +4 -4
  37. package/dist/imageEdit/index.js +2 -2
  38. package/dist/index.d.ts +8 -8
  39. package/dist/index.js +41 -26
  40. package/dist/jsonForm/index.d.ts +42 -1
  41. package/dist/jsonForm/index.js +9 -1
  42. package/dist/markdown/index.d.ts +18 -2
  43. package/dist/markdown/index.js +5 -3
  44. package/dist/recommend/index.js +2 -2
  45. package/dist/schemas/index.d.ts +4 -4
  46. package/dist/schemas/index.js +3 -5
  47. package/dist/storage/index.d.ts +2 -2
  48. package/dist/storage/index.js +1 -1
  49. package/dist/story/index.d.ts +151 -13
  50. package/dist/story/index.js +24 -9
  51. package/dist/{themeLibrary-DQQKuRMx.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  52. package/dist/timing/index.js +3 -5
  53. package/dist/transform/index.d.ts +2 -2
  54. package/dist/transform/index.js +3 -3
  55. package/dist/versions/index.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/src/__tests__/contentExtractor.test.ts +60 -0
  58. package/src/__tests__/coverBlock.test.ts +79 -3
  59. package/src/__tests__/customTemplateSample.test.ts +63 -2
  60. package/src/__tests__/customTemplatesFrontmatter.test.ts +31 -0
  61. package/src/__tests__/customThemesFrontmatter.test.ts +27 -0
  62. package/src/__tests__/inputDescriptors.test.ts +162 -0
  63. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  64. package/src/__tests__/markdown.test.ts +64 -0
  65. package/src/__tests__/markdownSanitize.test.ts +15 -0
  66. package/src/__tests__/markdownToDoc.test.ts +173 -2
  67. package/src/__tests__/mediaAnnotations.test.ts +27 -0
  68. package/src/__tests__/shapeGeometry.test.ts +37 -0
  69. package/src/__tests__/structuredData.test.ts +73 -3
  70. package/src/__tests__/templateMetadata.test.ts +11 -1
  71. package/src/__tests__/templates.test.ts +112 -0
  72. package/src/__tests__/validateDoc.test.ts +160 -0
  73. package/src/doc/annotationBlocks.ts +117 -0
  74. package/src/doc/customTemplatesFrontmatter.ts +7 -1
  75. package/src/doc/customThemesFrontmatter.ts +6 -1
  76. package/src/doc/docToMarkdown.ts +78 -29
  77. package/src/doc/getLayers.ts +11 -2
  78. package/src/doc/index.ts +12 -0
  79. package/src/doc/markdownToDoc.ts +80 -3
  80. package/src/doc/mediaAnnotations.ts +15 -33
  81. package/src/doc/standaloneAnnotation.ts +58 -0
  82. package/src/doc/structuredData.ts +92 -21
  83. package/src/doc/templates/__tests__/customTemplate.test.ts +21 -2
  84. package/src/doc/templates/comparisonBar.ts +8 -1
  85. package/src/doc/templates/coverBlock.ts +78 -15
  86. package/src/doc/templates/customTemplate.ts +10 -4
  87. package/src/doc/templates/diagramBlock.ts +2 -2
  88. package/src/doc/templates/drawingBlock.ts +2 -2
  89. package/src/doc/templates/factCard.ts +4 -2
  90. package/src/doc/templates/imageWithCaption.ts +10 -8
  91. package/src/doc/templates/index.ts +12 -2
  92. package/src/doc/templates/inputDescriptors.ts +432 -0
  93. package/src/doc/templates/photoGrid.ts +16 -39
  94. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +267 -1
  95. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  96. package/src/doc/templates/videoWithCaption.ts +15 -14
  97. package/src/doc/utils/nearest.ts +59 -0
  98. package/src/doc/utils/shapeGeometry.ts +90 -2
  99. package/src/doc/validate.ts +153 -54
  100. package/src/generate/contentExtractor.ts +148 -35
  101. package/src/generate/index.ts +0 -3
  102. package/src/jsonForm/index.ts +2 -0
  103. package/src/jsonForm/tokens.ts +78 -0
  104. package/src/markdown/annotationCoercion.ts +1 -1
  105. package/src/markdown/attrTokens.ts +19 -0
  106. package/src/markdown/convert.ts +11 -1
  107. package/src/markdown/index.ts +2 -1
  108. package/src/markdown/sanitize.ts +22 -2
  109. package/src/markdown/stringify.ts +12 -3
  110. package/src/markdown/utils.ts +62 -1
  111. package/src/schemas/CustomTemplates.ts +56 -4
  112. package/src/schemas/Doc.ts +39 -41
  113. package/src/schemas/themeCompile.ts +1 -1
  114. package/src/storage/ContentContainer.ts +6 -2
  115. package/src/transform/types.ts +1 -1
  116. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  117. package/dist/chunk-3E5F2XMR.js.map +0 -1
  118. package/dist/chunk-4AKUQPEE.js.map +0 -1
  119. package/dist/chunk-4X3JQXNM.js +0 -35
  120. package/dist/chunk-4X3JQXNM.js.map +0 -1
  121. package/dist/chunk-4ZFRZBU2.js.map +0 -1
  122. package/dist/chunk-7MQGNR76.js.map +0 -1
  123. package/dist/chunk-FBKP5CAO.js +0 -145
  124. package/dist/chunk-FBKP5CAO.js.map +0 -1
  125. package/dist/chunk-FR2RBTKO.js.map +0 -1
  126. package/dist/chunk-K32VJONL.js.map +0 -1
  127. package/dist/chunk-KZNXZ5SM.js.map +0 -1
  128. package/dist/chunk-OH43RPTP.js.map +0 -1
  129. package/dist/chunk-OKJOQXK4.js.map +0 -1
  130. package/dist/chunk-RUDYOTA6.js.map +0 -1
  131. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  132. package/src/generate/slideshowGenerator.ts +0 -280
  133. /package/dist/{chunk-TVSGLUID.js.map → chunk-LDQ2HJIX.js.map} +0 -0
  134. /package/dist/{chunk-2OIBZYKP.js.map → chunk-P3K7NLHF.js.map} +0 -0
  135. /package/dist/{chunk-LRCV7N7F.js.map → chunk-UY7KGQ5R.js.map} +0 -0
@@ -1,8 +1,8 @@
1
- import { a2 as PersistentLayer, ax as Theme, R as Layer, a3 as PersistentLayerConfig, aE as TitleBlockInput, as as TemplateContext, ag as SectionHeaderInput, am as StatHighlightInput, ab as QuoteBlockInput, H as FactCardInput, aG as TwoColumnInput, t as DateEventInput, P as ImageWithCaptionInput, T as LeftFeatureInput, ae as RightFeatureInput, W as MapBlockInput, al as StartBlockConfig, K as FullBleedQuoteInput, V as ListBlockInput, a7 as PhotoGridInput, v as DefinitionCardInput, n as ComparisonBarInput, aa as PullQuoteInput, aK as VideoWithCaptionInput, aJ as VideoPullQuoteInput, s as DataTableInput, w as DiagramBlockInput, B as Block, E as DrawingBlockInput, _ as MarkerStyle, ar as TemplateBlock, A as AccentImage, O as ImageTreatment, a as AccentPosition, aL as ViewportConfig, p as CustomTemplateDefinition, y as DocBlock, au as TemplateRegistry, c as Animation, d as AnimationType, az as ThemeColorScheme, x as Doc, z as DocDiagnostic } from '../Doc-BsMkmzH8.js';
2
- export { F as FRONTMATTER_CUSTOM_TEMPLATES_KEY, G as FRONTMATTER_CUSTOM_THEMES_KEY, S as LayoutHints, ad as RenderStyle, ay as ThemeColorPalette, aC as ThemeStyle, aD as ThemeTypography, aH as VIEWPORT_PRESETS, aM as ViewportOrientation, aN as ViewportPreset, aS as createTemplateContext, aY as getLayoutHints, b0 as getTwoColumnPositions, b1 as getViewport, b2 as getViewportOrientation, b4 as isTemplateBlock, b9 as scaledFontSize } from '../Doc-BsMkmzH8.js';
1
+ import { a4 as PersistentLayer, az as Theme, S as Layer, a5 as PersistentLayerConfig, aG as TitleBlockInput, au as TemplateContext, ai as SectionHeaderInput, ao as StatHighlightInput, ad as QuoteBlockInput, I as FactCardInput, aI as TwoColumnInput, u as DateEventInput, Q as ImageWithCaptionInput, V as LeftFeatureInput, ag as RightFeatureInput, Y as MapBlockInput, an as StartBlockConfig, L as FullBleedQuoteInput, X as ListBlockInput, a9 as PhotoGridInput, w as DefinitionCardInput, n as ComparisonBarInput, ac as PullQuoteInput, aM as VideoWithCaptionInput, aL as VideoPullQuoteInput, t as DataTableInput, x as DiagramBlockInput, B as Block, F as DrawingBlockInput, a0 as MarkerStyle, at as TemplateBlock, A as AccentImage, P as ImageTreatment, a as AccentPosition, aN as ViewportConfig, p as CustomTemplateDefinition, z as DocBlock, aw as TemplateRegistry, c as Animation, d as AnimationType, aB as ThemeColorScheme, y as Doc, E as DocDiagnostic } from '../Doc-BBVGq1_9.js';
2
+ export { G as FRONTMATTER_CUSTOM_TEMPLATES_KEY, H as FRONTMATTER_CUSTOM_THEMES_KEY, U as LayoutHints, af as RenderStyle, aA as ThemeColorPalette, aE as ThemeStyle, aF as ThemeTypography, aJ as VIEWPORT_PRESETS, aO as ViewportOrientation, aP as ViewportPreset, aU as createTemplateContext, a_ as getLayoutHints, b2 as getTwoColumnPositions, b3 as getViewport, b4 as getViewportOrientation, b6 as isTemplateBlock, bb as scaledFontSize } from '../Doc-BBVGq1_9.js';
3
3
  import { I as MarkdownNode, $ as TransitionType, _ as TransitionDirection, M as MarkdownBlockNode, q as MarkdownHeading, m as MarkdownDocument, P as MarkdownTable, h as MarkdownCodeBlock } from '../types-DlAZ7MW4.js';
4
- import { C as ContentContainer } from '../ContentContainer-DNx460y8.js';
5
- export { D as DEFAULT_THEME, g as getAvailableThemes, b as getThemeSummaries, r as resolveTheme } from '../themeLibrary-DQQKuRMx.js';
4
+ import { C as ContentContainer } from '../ContentContainer-BXUlIf18.js';
5
+ export { D as DEFAULT_THEME, g as getAvailableThemes, b as getThemeSummaries, r as resolveTheme } from '../themeLibrary-CehcJhzz.js';
6
6
 
7
7
  /**
8
8
  * Canonical, framework-free UI metadata for every built-in block template.
@@ -516,9 +516,11 @@ declare function drawingBlock(input: DrawingBlockInput, context: TemplateContext
516
516
  * - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the
517
517
  * standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the
518
518
  * editor's edge renderer).
519
- * - `connectorPath(routing, start, end)` + `clipEndpoints(a, b)` → the line/edge
519
+ * - `connectorPath(routing, start, end)` + `snapEndpoints(a, b)` → the line/edge
520
520
  * geometry between two shapes (straight / orthogonal / curved), lifted from the
521
- * diagram template so drawings and diagrams draw identical connectors.
521
+ * diagram template so drawings and diagrams draw identical connectors. The older
522
+ * `clipEndpoints(a, b)` helper remains for free-angle clipping, but rendered
523
+ * connectors use snapped ports so authored lines land on stable attachment points.
522
524
  */
523
525
 
524
526
  /** Connector routing styles. */
@@ -549,6 +551,12 @@ interface ClipBox {
549
551
  rx: number;
550
552
  ry: number;
551
553
  }
554
+ type ConnectorPort = 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left';
555
+ interface ConnectorSnapPoint {
556
+ port: ConnectorPort;
557
+ x: number;
558
+ y: number;
559
+ }
552
560
  /** Intersection of the center-to-center line with `from`'s bounding box. */
553
561
  declare function clipPoint(from: ClipBox, to: ClipBox): {
554
562
  x: number;
@@ -565,6 +573,22 @@ declare function clipEndpoints(a: ClipBox, b: ClipBox): {
565
573
  y: number;
566
574
  };
567
575
  };
576
+ /** The stable connector ports exposed around a shape/card box. */
577
+ declare function snapPoints(box: ClipBox): ConnectorSnapPoint[];
578
+ /** Nearest stable connector port on `box` to an arbitrary point. */
579
+ declare function nearestSnapPoint(box: ClipBox, point: {
580
+ x: number;
581
+ y: number;
582
+ }): ConnectorSnapPoint;
583
+ /**
584
+ * Pick the pair of connector ports, one on each box, with the shortest
585
+ * distance between them. This gives stable attachments to side/corner handles
586
+ * instead of arbitrary center-ray intersections.
587
+ */
588
+ declare function snapEndpoints(a: ClipBox, b: ClipBox): {
589
+ start: ConnectorSnapPoint;
590
+ end: ConnectorSnapPoint;
591
+ };
568
592
  /** Build the SVG `d` for a connector between two clipped endpoints. */
569
593
  declare function connectorPath(routing: ConnectorRouting, start: {
570
594
  x: number;
@@ -1336,7 +1360,9 @@ declare function readCustomTemplatesFromFrontmatter(frontmatter: Record<string,
1336
1360
  * object described in the module header. Returns undefined when the
1337
1361
  * input list is empty so callers can leave the key off the output.
1338
1362
  */
1339
- declare function writeCustomTemplatesToFrontmatter(templates: readonly CustomTemplateDefinition[] | undefined): string | undefined;
1363
+ declare function writeCustomTemplatesToFrontmatter(templates: readonly CustomTemplateDefinition[] | undefined, options?: {
1364
+ pretty?: boolean;
1365
+ }): string | undefined;
1340
1366
 
1341
1367
  /**
1342
1368
  * Frontmatter serialization for user-defined custom themes.
@@ -1375,7 +1401,9 @@ declare function readCustomThemesFromFrontmatter(frontmatter: Record<string, unk
1375
1401
  * the module header (keyed by theme id). Returns undefined when the input is
1376
1402
  * empty so callers can leave the key off the output.
1377
1403
  */
1378
- declare function writeCustomThemesToFrontmatter(themes: readonly Theme[] | undefined): string | undefined;
1404
+ declare function writeCustomThemesToFrontmatter(themes: readonly Theme[] | undefined, options?: {
1405
+ pretty?: boolean;
1406
+ }): string | undefined;
1379
1407
 
1380
1408
  /**
1381
1409
  * Doc-scoped theme resolution.
@@ -1542,10 +1570,12 @@ declare function resolveAudioMapping(doc: Doc, container: ContentContainer): Pro
1542
1570
  * didn't provide them explicitly.
1543
1571
  *
1544
1572
  * YAML support is a documented subset (this package has no YAML
1545
- * dependency): top-level `key: scalar`, `key: [inline, array]`, and
1546
- * `key:` followed by an indented `- item` block sequence whose items are
1547
- * scalars or inline arrays. Nested maps are rejected with a clear error —
1548
- * use a `json data` fence for deeply nested input.
1573
+ * dependency): top-level `key: scalar`, `key: [inline, array]`, `key:`
1574
+ * followed by an indented `- item` block sequence (scalars or inline
1575
+ * arrays), and `key:` followed by *one level* of indented `subkey: scalar`
1576
+ * lines (a nested map, e.g. `center:` + indented `lat:` / `lng:`). Deeper
1577
+ * nesting, and mixing list items with mapping keys under one key, are
1578
+ * rejected with a clear error — use a `json data` fence for those.
1549
1579
  */
1550
1580
 
1551
1581
  /**
@@ -1565,6 +1595,17 @@ interface DataFenceParseResult {
1565
1595
  * fences are parsed with {@link parseYamlSubset}.
1566
1596
  */
1567
1597
  declare function parseDataFence(node: MarkdownCodeBlock): DataFenceParseResult;
1598
+ /**
1599
+ * Return a new contents array with the first data fence's code content
1600
+ * replaced by `data` (pretty-printed JSON). The fence node keeps its
1601
+ * language and `data` marker; only `value` changes. When no data fence
1602
+ * exists, a new ` ```json data ` fence is appended instead.
1603
+ *
1604
+ * Pure: neither `contents` nor its nodes are mutated. This is the write
1605
+ * half of the data-fence channel — editors update `block.templateData`
1606
+ * and call this to reflect the change back into `block.contents`.
1607
+ */
1608
+ declare function replaceDataFence(contents: MarkdownBlockNode[], data: Record<string, unknown>): MarkdownBlockNode[];
1568
1609
  /**
1569
1610
  * Parse the supported YAML subset (see module doc) into a key→value map.
1570
1611
  * Throws an Error with a line-anchored message on anything outside the
@@ -1624,6 +1665,7 @@ interface MarkdownValidationResult {
1624
1665
  diagnostics: DocDiagnostic[];
1625
1666
  errorCount: number;
1626
1667
  warningCount: number;
1668
+ infoCount: number;
1627
1669
  /** The converted Doc (conversion diagnostics are included above). */
1628
1670
  doc: Doc;
1629
1671
  }
@@ -1660,4 +1702,100 @@ declare function validateMarkdownDoc(markdownDoc: MarkdownDocument, options?: Va
1660
1702
  */
1661
1703
  declare function fallbackBlockLayers(block: DocBlock, context: TemplateContext, notice: string): Layer[];
1662
1704
 
1663
- export { type AccentLayout, type AudioSegmentTiming, CONTAINER_TEMPLATES, type ClipBox, type ConnectorRouting, type CoverBlockInput, DEFAULT_LAYOUT, type DataFenceParseResult, type DeriveTemplateInputsOptions, type DiagramEdge, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodePosition, DocBlock, type DocStylePreset, type DrawingConnector, type DrawingLayout, type DrawingLayoutOptions, type DrawingShape, type DrawingShapeKind, type ExpandDocBlocksOptions, type ExtractedTableData, type FirstImage, type LayoutLayerDefaults, type LayoutLayersResult, type MarkdownToDocOptions, type MarkdownValidationResult, PATH_SHAPE_KINDS, PersistentLayerConfig, type RenderContext, type RuntimeTemplateRegistry, SHAPE_NAMES, TEMPLATE_ALIASES, TEMPLATE_METADATA, TemplateBlock, TemplateContext, type TemplateMetadata, Theme, ThemeColorScheme, type ValidateOptions, ViewportConfig, adjustY, applyRenderStyleToLayers, buildRegistry, clipEndpoints, clipPoint, comparisonBar, computeDiagramLayout, computeDrawingLayout, computeLayoutLayers, connectorPath, countBlocks, coverBlock, createAccentLayers, cssFilterForTreatment, dataTable, dateEvent, decodeLayersFromFrontmatter, definitionCard, deriveTemplateInputs, diagramBlock, docToMarkdown, drawingBlock, encodeLayersForFrontmatter, expandCoverBlock, expandDocBlocks, expandPersistentLayers, expandTemplateBlock, extractBlockquoteText, extractBodyPlainText, extractFirstImage, extractImages, extractListItems, extractTableData, extractTableFromContents, factCard, fallbackBlockLayers, findFirstTable, flattenBlocks, flattenRenderableBlocks, fullBleedQuote, getAccentLayout, getAnimationProgress, getAnimationStyle, getAvailableTemplates, getBlockDepth, getDefaultAnimation, getDefaultAnimationDuration, getDocStyleConfig, getLayers, getOverlayOpacity, getPersistentLayersFromTheme, getTemplateHint, getThemeFont, getTransitionClass, hasTemplate, imageWithCaption, isContainerTemplate, isDataFence, isShapeName, layoutBlock, leftFeature, lineStyleDasharray, listBlock, mapBlock, markdownToDoc, markerPath, normalizeShapeKind, parseDataFence, parseYamlSubset, photoGrid, pullQuote, quoteBlock, readCustomTemplatesFromFrontmatter, readCustomThemesFromFrontmatter, resolveAudioMapping, resolveColorScheme, resolvePersistentLayers, resolveTemplateName, resolveThemeForDoc, rightFeature, scaleAnimationDuration, scoreTextSimilarity, sectionHeader, shapePath, shouldUseShadow, statHighlight, templateRegistry, themeWantsAmbientMotion, themedEntrance, themedFontSize, themedImageTreatment, themedScrim, themedSurfaceGradient, titleBlock, twoColumn, validateMarkdownDoc, validateMarkdownSource, videoPullQuote, videoWithCaption, wrapWithPersistentLayers, writeCustomTemplatesToFrontmatter, writeCustomThemesToFrontmatter };
1705
+ /**
1706
+ * Template input descriptors — a typed catalog of the inline `{[…]}` params
1707
+ * each built-in template understands, plus the pure coercion + lint logic
1708
+ * that consumes it.
1709
+ *
1710
+ * Two jobs, one source of truth:
1711
+ *
1712
+ * - **Coercion (`coerceTemplateParams`)** turns a flat `key → raw string`
1713
+ * param map (from a `{[name key=value]}` annotation, always strings) into a
1714
+ * partial typed input the template functions can consume directly — e.g.
1715
+ * `center="47.6,-122.3"` → `{ lat: 47.6, lng: -122.3 }`, `zoom=9` → `9`.
1716
+ * This is what makes `{[map center="47.6,-122.3" zoom=9]}` render with no
1717
+ * changes to any template function. Unknown keys ALWAYS pass through
1718
+ * untouched (as strings), so coercion is never lossy; coercion failures are
1719
+ * returned as `warnings` and the raw string is preserved.
1720
+ *
1721
+ * - **Linting (`lintTemplateParams`)** checks a param map against the
1722
+ * descriptors and reports unknown keys (with a did-you-mean suggestion),
1723
+ * values outside a closed enum / that fail coercion, and missing required
1724
+ * inputs. The document validator surfaces these as diagnostics.
1725
+ *
1726
+ * This mirrors the proven block-meta descriptor pattern in
1727
+ * `markdown/annotationCoercion.ts` (`BLOCK_META_KEY_DESCRIPTORS`), extended
1728
+ * from block geometry/timing to per-template content inputs.
1729
+ *
1730
+ * The descriptors are the `{[…]}` counterpart of the template input
1731
+ * interfaces in `schemas/BlockTemplates.ts` — only keys/enums that provably
1732
+ * exist on those interfaces are declared. Keys that can't be meaningfully
1733
+ * expressed as a single inline string (e.g. a `dataTable`'s `rows`) are still
1734
+ * declared with `coerce: 'string'` so they aren't mis-flagged as unknown.
1735
+ */
1736
+ /** How a raw `{[…]}` param string is coerced into a typed input value. */
1737
+ type InputCoercion = 'string' | 'number' | 'boolean' | 'latLng' | 'labeledPair' | 'stringList';
1738
+ /**
1739
+ * Editor/validator-facing description of a single template input key: what it
1740
+ * does, how to coerce its raw string, and either its closed set of valid
1741
+ * values or a free-form format hint. `values`/`valueHint` are mutually
1742
+ * exclusive (a key has a closed enum OR a hint, not both).
1743
+ */
1744
+ interface TemplateInputDescriptor {
1745
+ /** The `{[name key=…]}` param key, matching the template input interface. */
1746
+ key: string;
1747
+ /** One-line description of the input. */
1748
+ description: string;
1749
+ /** Coercion strategy for the raw string (default `'string'`). */
1750
+ coerce?: InputCoercion;
1751
+ /** Closed set of valid values, for keys backed by an enum. */
1752
+ values?: readonly string[];
1753
+ /** Format hint shown when the value is free-form (no closed set). */
1754
+ valueHint?: string;
1755
+ /** When true, the validator warns if the key is absent and can't be derived. */
1756
+ required?: boolean;
1757
+ }
1758
+ /** A single lint finding produced by {@link lintTemplateParams}. */
1759
+ interface TemplateParamFinding {
1760
+ kind: 'unknown-input' | 'invalid-input-value' | 'missing-input';
1761
+ key: string;
1762
+ message: string;
1763
+ /** Did-you-mean candidate (for `unknown-input`). */
1764
+ suggestion?: string;
1765
+ }
1766
+ /**
1767
+ * Inputs shared by every template via `BaseTemplateBlock`. Merged into each
1768
+ * template's known-key set for coercion + lint. Block-level timing/geometry
1769
+ * keys (`duration`, `x`, `y`, …) are NOT here — those stay owned by the
1770
+ * block-meta registry (`KNOWN_BLOCK_META_KEYS`) and are folded into the lint
1771
+ * known-key set separately.
1772
+ */
1773
+ declare const BASE_INPUT_DESCRIPTORS: readonly TemplateInputDescriptor[];
1774
+ /**
1775
+ * Per-template input descriptors, keyed by canonical template id. A template
1776
+ * absent from this map has no descriptors and is exempt from lint (custom
1777
+ * templates and not-yet-described built-ins).
1778
+ */
1779
+ declare const TEMPLATE_INPUT_DESCRIPTORS: Readonly<Record<string, readonly TemplateInputDescriptor[]>>;
1780
+ /**
1781
+ * Coerce a flat `{[…]}` param map into a partial typed template input.
1782
+ *
1783
+ * Known descriptor keys are coerced per their `coerce` strategy; unknown keys
1784
+ * (and `string`-typed keys) pass through unchanged. A coercion failure leaves
1785
+ * the raw string in place and adds a human-readable `warnings` entry — the
1786
+ * output is never lossy. Returns a fresh object; the caller must NOT feed
1787
+ * `templateOverrides` back in — only the ephemeral merged input is coerced.
1788
+ */
1789
+ declare function coerceTemplateParams(template: string | undefined, params: Record<string, string>): {
1790
+ input: Record<string, unknown>;
1791
+ warnings: string[];
1792
+ };
1793
+ /**
1794
+ * Lint a template's `{[…]}` params against its descriptors. Returns findings
1795
+ * for unknown keys, values outside a closed enum / that fail coercion, and
1796
+ * missing required inputs. Templates without descriptors (custom or
1797
+ * not-yet-described built-ins) return `[]`.
1798
+ */
1799
+ declare function lintTemplateParams(template: string, params: Record<string, string>): TemplateParamFinding[];
1800
+
1801
+ export { type AccentLayout, type AudioSegmentTiming, BASE_INPUT_DESCRIPTORS, CONTAINER_TEMPLATES, type ClipBox, type ConnectorPort, type ConnectorRouting, type ConnectorSnapPoint, type CoverBlockInput, DEFAULT_LAYOUT, type DataFenceParseResult, type DeriveTemplateInputsOptions, type DiagramEdge, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodePosition, DocBlock, type DocStylePreset, type DrawingConnector, type DrawingLayout, type DrawingLayoutOptions, type DrawingShape, type DrawingShapeKind, type ExpandDocBlocksOptions, type ExtractedTableData, type FirstImage, type InputCoercion, type LayoutLayerDefaults, type LayoutLayersResult, type MarkdownToDocOptions, type MarkdownValidationResult, PATH_SHAPE_KINDS, PersistentLayerConfig, type RenderContext, type RuntimeTemplateRegistry, SHAPE_NAMES, TEMPLATE_ALIASES, TEMPLATE_INPUT_DESCRIPTORS, TEMPLATE_METADATA, TemplateBlock, TemplateContext, type TemplateInputDescriptor, type TemplateMetadata, type TemplateParamFinding, Theme, ThemeColorScheme, type ValidateOptions, ViewportConfig, adjustY, applyRenderStyleToLayers, buildRegistry, clipEndpoints, clipPoint, coerceTemplateParams, comparisonBar, computeDiagramLayout, computeDrawingLayout, computeLayoutLayers, connectorPath, countBlocks, coverBlock, createAccentLayers, cssFilterForTreatment, dataTable, dateEvent, decodeLayersFromFrontmatter, definitionCard, deriveTemplateInputs, diagramBlock, docToMarkdown, drawingBlock, encodeLayersForFrontmatter, expandCoverBlock, expandDocBlocks, expandPersistentLayers, expandTemplateBlock, extractBlockquoteText, extractBodyPlainText, extractFirstImage, extractImages, extractListItems, extractTableData, extractTableFromContents, factCard, fallbackBlockLayers, findFirstTable, flattenBlocks, flattenRenderableBlocks, fullBleedQuote, getAccentLayout, getAnimationProgress, getAnimationStyle, getAvailableTemplates, getBlockDepth, getDefaultAnimation, getDefaultAnimationDuration, getDocStyleConfig, getLayers, getOverlayOpacity, getPersistentLayersFromTheme, getTemplateHint, getThemeFont, getTransitionClass, hasTemplate, imageWithCaption, isContainerTemplate, isDataFence, isShapeName, layoutBlock, leftFeature, lineStyleDasharray, lintTemplateParams, listBlock, mapBlock, markdownToDoc, markerPath, nearestSnapPoint, normalizeShapeKind, parseDataFence, parseYamlSubset, photoGrid, pullQuote, quoteBlock, readCustomTemplatesFromFrontmatter, readCustomThemesFromFrontmatter, replaceDataFence, resolveAudioMapping, resolveColorScheme, resolvePersistentLayers, resolveTemplateName, resolveThemeForDoc, rightFeature, scaleAnimationDuration, scoreTextSimilarity, sectionHeader, shapePath, shouldUseShadow, snapEndpoints, snapPoints, statHighlight, templateRegistry, themeWantsAmbientMotion, themedEntrance, themedFontSize, themedImageTreatment, themedScrim, themedSurfaceGradient, titleBlock, twoColumn, validateMarkdownDoc, validateMarkdownSource, videoPullQuote, videoWithCaption, wrapWithPersistentLayers, writeCustomTemplatesToFrontmatter, writeCustomThemesToFrontmatter };
@@ -1,12 +1,15 @@
1
1
  import {
2
+ BASE_INPUT_DESCRIPTORS,
2
3
  CONTAINER_TEMPLATES,
3
4
  DEFAULT_LAYOUT,
4
5
  SHAPE_NAMES,
5
6
  TEMPLATE_ALIASES,
7
+ TEMPLATE_INPUT_DESCRIPTORS,
6
8
  TEMPLATE_METADATA,
7
9
  adjustY,
8
10
  applyRenderStyleToLayers,
9
11
  buildRegistry,
12
+ coerceTemplateParams,
10
13
  comparisonBar,
11
14
  computeDiagramLayout,
12
15
  computeDrawingLayout,
@@ -62,6 +65,7 @@ import {
62
65
  isShapeName,
63
66
  layoutBlock,
64
67
  leftFeature,
68
+ lintTemplateParams,
65
69
  listBlock,
66
70
  mapBlock,
67
71
  markdownToDoc,
@@ -73,6 +77,7 @@ import {
73
77
  quoteBlock,
74
78
  readCustomTemplatesFromFrontmatter,
75
79
  readCustomThemesFromFrontmatter,
80
+ replaceDataFence,
76
81
  resolveAudioMapping,
77
82
  resolveColorScheme,
78
83
  resolvePersistentLayers,
@@ -100,8 +105,8 @@ import {
100
105
  wrapWithPersistentLayers,
101
106
  writeCustomTemplatesToFrontmatter,
102
107
  writeCustomThemesToFrontmatter
103
- } from "../chunk-4ZFRZBU2.js";
104
- import "../chunk-LRCV7N7F.js";
108
+ } from "../chunk-5TYS5V3G.js";
109
+ import "../chunk-UY7KGQ5R.js";
105
110
  import "../chunk-V543AI2F.js";
106
111
  import "../chunk-FVTQSNR7.js";
107
112
  import {
@@ -111,8 +116,11 @@ import {
111
116
  connectorPath,
112
117
  lineStyleDasharray,
113
118
  markerPath,
114
- shapePath
115
- } from "../chunk-RUDYOTA6.js";
119
+ nearestSnapPoint,
120
+ shapePath,
121
+ snapEndpoints,
122
+ snapPoints
123
+ } from "../chunk-LTN6I7QW.js";
116
124
  import {
117
125
  DEFAULT_THEME,
118
126
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -120,7 +128,7 @@ import {
120
128
  getAvailableThemes,
121
129
  getThemeSummaries,
122
130
  resolveTheme
123
- } from "../chunk-4AKUQPEE.js";
131
+ } from "../chunk-KKNUBQ6Y.js";
124
132
  import {
125
133
  VIEWPORT_PRESETS,
126
134
  createTemplateContext,
@@ -131,13 +139,13 @@ import {
131
139
  isTemplateBlock,
132
140
  scaledFontSize2 as scaledFontSize
133
141
  } from "../chunk-DLXZMS5K.js";
134
- import "../chunk-OH43RPTP.js";
135
- import "../chunk-OKJOQXK4.js";
142
+ import "../chunk-BEQODJWV.js";
143
+ import "../chunk-HWVFJAAH.js";
136
144
  import "../chunk-DBS6RIFL.js";
137
145
  import "../chunk-QE7TWGVT.js";
138
- import "../chunk-4X3JQXNM.js";
139
- import "../chunk-3E5F2XMR.js";
146
+ import "../chunk-VWUFZ6ZG.js";
140
147
  export {
148
+ BASE_INPUT_DESCRIPTORS,
141
149
  CONTAINER_TEMPLATES,
142
150
  DEFAULT_LAYOUT,
143
151
  DEFAULT_THEME,
@@ -146,6 +154,7 @@ export {
146
154
  PATH_SHAPE_KINDS,
147
155
  SHAPE_NAMES,
148
156
  TEMPLATE_ALIASES,
157
+ TEMPLATE_INPUT_DESCRIPTORS,
149
158
  TEMPLATE_METADATA,
150
159
  VIEWPORT_PRESETS,
151
160
  adjustY,
@@ -153,6 +162,7 @@ export {
153
162
  buildRegistry,
154
163
  clipEndpoints,
155
164
  clipPoint,
165
+ coerceTemplateParams,
156
166
  comparisonBar,
157
167
  computeDiagramLayout,
158
168
  computeDrawingLayout,
@@ -218,10 +228,12 @@ export {
218
228
  layoutBlock,
219
229
  leftFeature,
220
230
  lineStyleDasharray,
231
+ lintTemplateParams,
221
232
  listBlock,
222
233
  mapBlock,
223
234
  markdownToDoc,
224
235
  markerPath,
236
+ nearestSnapPoint,
225
237
  normalizeShapeKind,
226
238
  parseDataFence,
227
239
  parseYamlSubset,
@@ -230,6 +242,7 @@ export {
230
242
  quoteBlock,
231
243
  readCustomTemplatesFromFrontmatter,
232
244
  readCustomThemesFromFrontmatter,
245
+ replaceDataFence,
233
246
  resolveAudioMapping,
234
247
  resolveColorScheme,
235
248
  resolvePersistentLayers,
@@ -243,6 +256,8 @@ export {
243
256
  sectionHeader,
244
257
  shapePath,
245
258
  shouldUseShadow,
259
+ snapEndpoints,
260
+ snapPoints,
246
261
  statHighlight,
247
262
  templateRegistry,
248
263
  themeWantsAmbientMotion,
@@ -1,4 +1,4 @@
1
- import { ax as Theme } from './Doc-BsMkmzH8.js';
1
+ import { az as Theme } from './Doc-BBVGq1_9.js';
2
2
 
3
3
  /**
4
4
  * Built-in Theme Library
@@ -1,16 +1,14 @@
1
1
  import "../chunk-DNJKBAI7.js";
2
- import {
3
- estimateNarrationTime,
4
- estimateReadingTime
5
- } from "../chunk-4X3JQXNM.js";
6
2
  import {
7
3
  DEFAULT_WORDS_PER_SECOND,
8
4
  calculatePrefixDuration,
9
5
  countSpokenWords,
10
6
  estimateNarrationDuration,
7
+ estimateNarrationTime,
8
+ estimateReadingTime,
11
9
  estimateSpokenWordCount,
12
10
  estimateTimeFromText
13
- } from "../chunk-3E5F2XMR.js";
11
+ } from "../chunk-VWUFZ6ZG.js";
14
12
  export {
15
13
  DEFAULT_WORDS_PER_SECOND,
16
14
  calculatePrefixDuration,
@@ -1,4 +1,4 @@
1
- import { m as ColorScheme, x as Doc, B as Block } from '../Doc-BsMkmzH8.js';
1
+ import { m as ColorScheme, y as Doc, B as Block } from '../Doc-BBVGq1_9.js';
2
2
  import { d as ExtractionType, E as ExtractedElement, b as ExtractionOptions } from '../contentExtractor-BNfVJV2U.js';
3
3
  import '../types-DlAZ7MW4.js';
4
4
 
@@ -70,7 +70,7 @@ interface TransformStyleConfig {
70
70
  outro?: boolean;
71
71
  };
72
72
  /**
73
- * Duration-based slide budget (folded in from the deprecated
73
+ * Duration-based slide budget (folded in from the since-removed
74
74
  * `generateSlideshow`): caps promoted template blocks at roughly
75
75
  * `slidesPerMinute` of doc duration (default: uncapped — only
76
76
  * `transformRatio` applies).
@@ -8,11 +8,11 @@ import {
8
8
  registerTransformStyle,
9
9
  resolveTransformStyle,
10
10
  unregisterTransformStyle
11
- } from "../chunk-TVSGLUID.js";
12
- import "../chunk-FR2RBTKO.js";
11
+ } from "../chunk-LDQ2HJIX.js";
12
+ import "../chunk-TQWLI6S2.js";
13
13
  import "../chunk-FVTQSNR7.js";
14
14
  import "../chunk-DLXZMS5K.js";
15
- import "../chunk-OKJOQXK4.js";
15
+ import "../chunk-HWVFJAAH.js";
16
16
  export {
17
17
  DEFAULT_TRANSFORM_STYLE_ID,
18
18
  analyzeBlocks,
@@ -1,5 +1,5 @@
1
1
  import { C as CoalesceOptions, V as Version, P as PrunePolicy, R as RevertOptions, S as SaveVersionOptions, a as SaveVersionResult } from '../types-D98li1gV.js';
2
- import { C as ContentContainer } from '../ContentContainer-DNx460y8.js';
2
+ import { C as ContentContainer } from '../ContentContainer-BXUlIf18.js';
3
3
 
4
4
  /**
5
5
  * Pure-ish version operations against a {@link ContentContainer}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -116,6 +116,36 @@ describe('extractContent — comparisons', () => {
116
116
  expect(comp.right.label).toBeTruthy();
117
117
  }
118
118
  });
119
+
120
+ it('does not let a comparison capture cross a sentence boundary', () => {
121
+ // Regression: greedy `.{2,30}` used to capture right="witness. Robertson"
122
+ // from the "from witness to witness." idiom bleeding into the next sentence.
123
+ const text =
124
+ 'Testimony about who carried guns and how many shots were fired ' +
125
+ 'contradicted from witness to witness. Robertson, who initially ' +
126
+ 'testified against the others, was later named as the ringleader.';
127
+ const result = extractContent(text, { types: ['comparison'] });
128
+ for (const el of result.elements) {
129
+ if (el.data.type === 'comparison') {
130
+ expect(el.data.left.label).not.toContain('.');
131
+ expect(el.data.right.label).not.toContain('.');
132
+ expect(el.data.right.label.toLowerCase()).not.toContain('robertson');
133
+ }
134
+ }
135
+ });
136
+
137
+ it('rejects idiomatic "from X to X" non-comparisons', () => {
138
+ // "from witness to witness", "from day to day" mean "it varied", not "A vs B".
139
+ for (const text of [
140
+ 'The testimony varied from witness to witness across the trial.',
141
+ 'Conditions changed from day to day during the expedition.',
142
+ 'The practice was handed down from generation to generation.',
143
+ ]) {
144
+ const result = extractContent(text, { types: ['comparison'] });
145
+ const comparisons = result.elements.filter((e) => e.data.type === 'comparison');
146
+ expect(comparisons.length).toBe(0);
147
+ }
148
+ });
119
149
  });
120
150
 
121
151
  describe('extractContent — impact lines', () => {
@@ -125,6 +155,36 @@ describe('extractContent — impact lines', () => {
125
155
  const result = extractContent(text, { types: ['impactLine'] });
126
156
  expect(result.elements.length).toBeGreaterThan(0);
127
157
  });
158
+
159
+ it('does not split sentences on abbreviations or initials', () => {
160
+ // Regression: "…at George W. Tibbetts' store…" was split at "W." into the
161
+ // fragment "They were met at George W" and shown as a standalone slide.
162
+ const text =
163
+ "They were met at George W. Tibbetts' store by a crowd of white and Native men " +
164
+ 'who intimidated them into turning back.';
165
+ const result = extractContent(text, { types: ['impactLine', 'fact'] });
166
+ for (const el of result.elements) {
167
+ if (el.data.type === 'impactLine') {
168
+ expect(el.data.text).not.toMatch(/George W$/);
169
+ }
170
+ if (el.data.type === 'fact') {
171
+ expect(el.data.fact).not.toMatch(/George W$/);
172
+ }
173
+ // No fragment should end on a bare single-letter initial.
174
+ expect(el.text).not.toMatch(/\b[A-Z]$/);
175
+ }
176
+ });
177
+
178
+ it('keeps initials-heavy name lists intact as one sentence', () => {
179
+ const text =
180
+ 'A jury named M. DeWitt Rumsey, Joseph Day, and David Hughes among the killers. ' +
181
+ 'The verdict shocked the town.';
182
+ const result = extractContent(text, { types: ['fact', 'impactLine'] });
183
+ // The "M." initial must not spawn a "A jury named M" fragment.
184
+ for (const el of result.elements) {
185
+ expect(el.text).not.toMatch(/named M$/);
186
+ }
187
+ });
128
188
  });
129
189
 
130
190
  describe('extractContent — lists', () => {
@@ -1,5 +1,10 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { coverBlock, expandCoverBlock, startBlockToCoverInput } from '../doc/templates/coverBlock';
2
+ import {
3
+ coverBlock,
4
+ expandCoverBlock,
5
+ startBlockToCoverInput,
6
+ fitCoverTitleSize,
7
+ } from '../doc/templates/coverBlock';
3
8
  import { createTemplateContext, DEFAULT_THEME, VIEWPORT_PRESETS } from '../doc/templates/index';
4
9
  import type { StartBlockConfig } from '../schemas/Doc';
5
10
  import type { CoverBlockInput } from '../doc/templates/coverBlock';
@@ -80,6 +85,7 @@ describe('coverBlock with hero image', () => {
80
85
  it('does not produce theme background layers', () => {
81
86
  const layers = coverBlock(input, landscapeContext);
82
87
  expect(findLayer(layers, 'cover-bg')).toBeUndefined();
88
+ expect(findLayer(layers, 'cover-bg-tint')).toBeUndefined();
83
89
  expect(findLayer(layers, 'cover-accent')).toBeUndefined();
84
90
  });
85
91
 
@@ -105,12 +111,16 @@ describe('coverBlock without hero image', () => {
105
111
  subtitle: 'Subtitle text',
106
112
  };
107
113
 
108
- it('produces theme background instead of hero', () => {
114
+ it('produces theme background and soft tint instead of hero', () => {
109
115
  const layers = coverBlock(input, landscapeContext);
110
116
  const bg = findShape(layers, 'cover-bg');
117
+ const tint = findShape(layers, 'cover-bg-tint');
111
118
  expect(bg).toBeDefined();
112
119
  expect(bg!.type).toBe('shape');
113
- expect(bg!.content.fill).toContain('radial-gradient');
120
+ expect(bg!.content.fill).toBe(DEFAULT_THEME.colors.background);
121
+ expect(tint).toBeDefined();
122
+ expect(tint!.content.fill).toContain('radial-gradient');
123
+ expect(tint!.content.fill).toContain('rgba(37, 99, 235, 0.16)');
114
124
  });
115
125
 
116
126
  it('produces decorative accent line', () => {
@@ -139,6 +149,31 @@ describe('coverBlock without hero image', () => {
139
149
  const subtitle = findLayer(layers, 'cover-subtitle');
140
150
  expect(subtitle!.position.y).not.toBe('82%');
141
151
  });
152
+
153
+ it('uses high-contrast Standard Light cover text', () => {
154
+ const layers = coverBlock(input, landscapeContext);
155
+ const title = findText(layers, 'cover-title');
156
+ const subtitle = findText(layers, 'cover-subtitle');
157
+ expect(title!.content.style.shadow).toBe(false);
158
+ expect(subtitle!.content.style.fontWeight).toBe('bold');
159
+ expect(subtitle!.content.style.color).toBe('rgba(15, 23, 42, 0.78)');
160
+ });
161
+
162
+ it('keeps non-Standard themes on the existing dramatic cover background', () => {
163
+ const nonStandardContext = createTemplateContext(
164
+ { ...DEFAULT_THEME, id: 'custom-light' },
165
+ 0,
166
+ 1,
167
+ VIEWPORT_PRESETS.landscape,
168
+ );
169
+ const layers = coverBlock(input, nonStandardContext);
170
+ const bg = findShape(layers, 'cover-bg');
171
+ const title = findText(layers, 'cover-title');
172
+ expect(findShape(layers, 'cover-bg-tint')).toBeUndefined();
173
+ expect(bg!.content.fill).toContain('radial-gradient');
174
+ expect(bg!.content.fill).toContain(`${DEFAULT_THEME.colors.primary} 0%`);
175
+ expect(title!.content.style.shadow).toBe(true);
176
+ });
142
177
  });
143
178
 
144
179
  // ── coverBlock title-only (no subtitle) ─────────────────────────────
@@ -179,6 +214,46 @@ describe('coverBlock viewport scaling', () => {
179
214
  });
180
215
  });
181
216
 
217
+ // ── Long-title auto-fit ─────────────────────────────────────────────
218
+
219
+ describe('coverBlock long-title auto-fit', () => {
220
+ const LONG = 'Seattle: The Emerald City That Rebuilt Itself on Top of Itself';
221
+
222
+ it('fitCoverTitleSize returns full base for short titles', () => {
223
+ expect(fitCoverTitleSize('Kirkland')).toBe(120);
224
+ expect(fitCoverTitleSize('Test Title')).toBe(120);
225
+ });
226
+
227
+ it('fitCoverTitleSize shrinks long titles below the full base', () => {
228
+ const size = fitCoverTitleSize(LONG);
229
+ expect(size).toBeLessThan(120);
230
+ expect(size).toBeGreaterThanOrEqual(62); // never below the readability floor
231
+ });
232
+
233
+ it('fitCoverTitleSize never drops below the readability floor', () => {
234
+ const size = fitCoverTitleSize('x'.repeat(400));
235
+ expect(size).toBe(62);
236
+ });
237
+
238
+ it('fitCoverTitleSize is monotonic — longer titles are never larger', () => {
239
+ const a = fitCoverTitleSize('A moderately long cover title here');
240
+ const b = fitCoverTitleSize('A moderately long cover title here that keeps going and going');
241
+ expect(b).toBeLessThanOrEqual(a);
242
+ });
243
+
244
+ it('renders a long title with a smaller font than a short one', () => {
245
+ const shortTitle = findText(
246
+ coverBlock({ heroSrc: 'h.jpg', title: 'Short' }, landscapeContext),
247
+ 'cover-title',
248
+ );
249
+ const longTitle = findText(
250
+ coverBlock({ heroSrc: 'h.jpg', title: LONG }, landscapeContext),
251
+ 'cover-title',
252
+ );
253
+ expect(longTitle!.content.style.fontSize).toBeLessThan(shortTitle!.content.style.fontSize);
254
+ });
255
+ });
256
+
182
257
  // ── startBlockToCoverInput ──────────────────────────────────────────
183
258
 
184
259
  describe('startBlockToCoverInput', () => {
@@ -228,6 +303,7 @@ describe('expandCoverBlock', () => {
228
303
  const config: StartBlockConfig = { title: 'No Hero' };
229
304
  const layers = expandCoverBlock(config, landscapeContext);
230
305
  expect(layers.some((l) => l.id === 'cover-bg')).toBe(true);
306
+ expect(layers.some((l) => l.id === 'cover-bg-tint')).toBe(true);
231
307
  expect(layers.some((l) => l.id === 'cover-title')).toBe(true);
232
308
  expect(layers.some((l) => l.id === 'cover-hero')).toBe(false);
233
309
  });