@bendyline/squisq 1.5.1 → 2.0.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/README.md +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
- package/dist/chunk-GF3BVNLU.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.js.map +1 -0
- package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +134 -34
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +253 -81
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/coverBlock.test.ts +33 -2
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +41 -0
- package/src/__tests__/markdownToDoc.test.ts +5 -2
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +49 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +126 -39
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +135 -1
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/coverBlock.ts +49 -13
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +157 -7
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/convert.ts +11 -1
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-2OIBZYKP.js.map +0 -1
- package/dist/chunk-6ATE2PSM.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LH7I6SH7.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-TEEEILMP.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1777
- package/dist/story/index.js +0 -273
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -18,6 +18,7 @@ import type { Layer } from '../../schemas/Doc.js';
|
|
|
18
18
|
import type { TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
19
|
import type { StartBlockConfig } from '../../schemas/Doc.js';
|
|
20
20
|
import { getThemeFont, themedFontSize, themedImageTreatment } from '../utils/themeUtils.js';
|
|
21
|
+
import { relativeLuminance, withAlpha } from '../../schemas/colorUtils.js';
|
|
21
22
|
import { mapAmbientMotion } from './accentImage.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -61,6 +62,11 @@ export function fitCoverTitleSize(title: string): number {
|
|
|
61
62
|
return Math.max(COVER_TITLE_MIN_BASE_PX, Math.round(scaled));
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function isStandardLightCover(context: TemplateContext): boolean {
|
|
66
|
+
const { theme } = context;
|
|
67
|
+
return theme.id === 'standard' && relativeLuminance(theme.colors.background) > 0.85;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
/**
|
|
65
71
|
* Generate cover block layers from StartBlockConfig.
|
|
66
72
|
*/
|
|
@@ -68,6 +74,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
68
74
|
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
69
75
|
const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
|
|
70
76
|
const { theme, layout } = context;
|
|
77
|
+
const standardLightCover = !heroSrc && isStandardLightCover(context);
|
|
71
78
|
|
|
72
79
|
// Scale font sizes for viewport - cover titles are larger than regular title blocks.
|
|
73
80
|
// The cover title wraps within maxTextWidth but does not otherwise shrink, so a long
|
|
@@ -113,16 +120,42 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
113
120
|
},
|
|
114
121
|
);
|
|
115
122
|
} else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
if (standardLightCover) {
|
|
124
|
+
// Standard Light needs a restrained cover-poster treatment; a
|
|
125
|
+
// full-strength primary radial hotspot overwhelms dark serif text
|
|
126
|
+
// and makes the subtitle muddy.
|
|
127
|
+
layers.push(
|
|
128
|
+
{
|
|
129
|
+
type: 'shape',
|
|
130
|
+
id: 'cover-bg',
|
|
131
|
+
content: {
|
|
132
|
+
shape: 'rect',
|
|
133
|
+
fill: theme.colors.background,
|
|
134
|
+
},
|
|
135
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'shape',
|
|
139
|
+
id: 'cover-bg-tint',
|
|
140
|
+
content: {
|
|
141
|
+
shape: 'rect',
|
|
142
|
+
fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(theme.colors.primary, 0.16)} 0%, ${withAlpha(theme.colors.primary, 0.07)} 38%, ${withAlpha(theme.colors.primary, 0)} 78%)`,
|
|
143
|
+
},
|
|
144
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
// Non-Standard themes keep the existing dramatic radial cover look.
|
|
149
|
+
layers.push({
|
|
150
|
+
type: 'shape',
|
|
151
|
+
id: 'cover-bg',
|
|
152
|
+
content: {
|
|
153
|
+
shape: 'rect',
|
|
154
|
+
fill: `radial-gradient(ellipse at 50% 40%, ${theme.colors.primary} 0%, ${theme.colors.background} 100%)`,
|
|
155
|
+
},
|
|
156
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
126
159
|
|
|
127
160
|
// Subtle decorative accent line below title
|
|
128
161
|
layers.push({
|
|
@@ -130,7 +163,9 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
130
163
|
id: 'cover-accent',
|
|
131
164
|
content: {
|
|
132
165
|
shape: 'rect',
|
|
133
|
-
fill:
|
|
166
|
+
fill: standardLightCover
|
|
167
|
+
? withAlpha(theme.colors.primary, 0.24)
|
|
168
|
+
: 'rgba(255, 255, 255, 0.2)',
|
|
134
169
|
},
|
|
135
170
|
position: {
|
|
136
171
|
x: '35%',
|
|
@@ -153,7 +188,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
153
188
|
fontWeight: 'bold',
|
|
154
189
|
color: theme.colors.text,
|
|
155
190
|
textAlign: 'center',
|
|
156
|
-
shadow:
|
|
191
|
+
shadow: !standardLightCover,
|
|
157
192
|
},
|
|
158
193
|
},
|
|
159
194
|
position: {
|
|
@@ -176,7 +211,8 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
176
211
|
style: {
|
|
177
212
|
fontSize: subtitleFontSize,
|
|
178
213
|
fontFamily: getThemeFont(context, 'body'),
|
|
179
|
-
|
|
214
|
+
fontWeight: standardLightCover ? 'bold' : 'normal',
|
|
215
|
+
color: standardLightCover ? withAlpha(theme.colors.text, 0.78) : theme.colors.textMuted,
|
|
180
216
|
textAlign: 'center',
|
|
181
217
|
lineHeight: 1.5,
|
|
182
218
|
},
|
|
@@ -1,35 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Diagram block template.
|
|
3
3
|
*
|
|
4
|
-
* Renders
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Renders a node-and-edge diagram from one of two data sources:
|
|
5
|
+
* 1. Child headings (`context.children`) — the legacy authored form; each
|
|
6
|
+
* child becomes a node positioned by `x`/`y` and connected by
|
|
7
|
+
* `connectsTo` (see `diagramLayout.ts` for auto-placement rules).
|
|
8
|
+
* 2. `input.nodes`/`input.edges` (usually via `templateData`) — the
|
|
9
|
+
* data-driven form derived from an ASCII-art diagram fence. Nodes may
|
|
10
|
+
* carry per-node sizes and a `container` reference; containers draw
|
|
11
|
+
* behind their children as translucent grouping cards.
|
|
10
12
|
*
|
|
11
|
-
* Layout coordinates
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* absolute pixel ranges.
|
|
13
|
+
* Layout coordinates are author-defined units. The template computes a
|
|
14
|
+
* bounding box of all nodes and scales it to fit the block's viewport
|
|
15
|
+
* with padding, so users don't have to think about absolute pixel ranges.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import type { Layer, ShapeLayer, TextLayer, PathLayer, MarkerStyle } from '../../schemas/Doc.js';
|
|
18
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
DiagramBlockInput,
|
|
21
|
+
DiagramEdgeAnchor,
|
|
22
|
+
DiagramTemplateEdge,
|
|
23
|
+
DiagramTemplateNode,
|
|
24
|
+
TemplateContext,
|
|
25
|
+
} from '../../schemas/BlockTemplates.js';
|
|
19
26
|
import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
27
|
+
import { DIAGRAM_LABEL_LINE_HEIGHT, fitDiagramLabel } from '../utils/diagramText.js';
|
|
28
|
+
import {
|
|
29
|
+
anchorPoint,
|
|
30
|
+
connectorPath,
|
|
31
|
+
lineStyleDasharray,
|
|
32
|
+
snapEndpoints,
|
|
33
|
+
type ConnectorRouting,
|
|
34
|
+
} from '../utils/shapeGeometry.js';
|
|
35
|
+
import { computeDiagramLayout } from './diagramLayout.js';
|
|
22
36
|
|
|
23
37
|
const NODE_WIDTH = 180;
|
|
24
38
|
const NODE_HEIGHT = 64;
|
|
25
39
|
const PADDING = 80;
|
|
26
40
|
|
|
41
|
+
interface ResolvedDiagramNode {
|
|
42
|
+
id: string;
|
|
43
|
+
label: string;
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
w: number;
|
|
47
|
+
h: number;
|
|
48
|
+
/** Containment depth (0 = top level); containers draw depth-ascending. */
|
|
49
|
+
depth: number;
|
|
50
|
+
isContainer: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ResolvedDiagramEdge {
|
|
54
|
+
id: string;
|
|
55
|
+
source: string;
|
|
56
|
+
target: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
directed: boolean;
|
|
59
|
+
sourceAnchor?: DiagramEdgeAnchor;
|
|
60
|
+
targetAnchor?: DiagramEdgeAnchor;
|
|
61
|
+
routing?: ConnectorRouting;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ResolvedDiagram {
|
|
65
|
+
nodes: ResolvedDiagramNode[];
|
|
66
|
+
edges: ResolvedDiagramEdge[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nodesFromChildren(context: TemplateContext): ResolvedDiagram {
|
|
70
|
+
const layout = computeDiagramLayout(context.children ?? []);
|
|
71
|
+
return {
|
|
72
|
+
nodes: layout.nodes.map((n) => ({
|
|
73
|
+
id: n.id,
|
|
74
|
+
label: n.label,
|
|
75
|
+
x: n.x,
|
|
76
|
+
y: n.y,
|
|
77
|
+
w: NODE_WIDTH,
|
|
78
|
+
h: NODE_HEIGHT,
|
|
79
|
+
depth: 0,
|
|
80
|
+
isContainer: false,
|
|
81
|
+
})),
|
|
82
|
+
edges: layout.edges.map((e) => ({
|
|
83
|
+
id: e.id,
|
|
84
|
+
source: e.source,
|
|
85
|
+
target: e.target,
|
|
86
|
+
...(e.type ? { label: e.type } : {}),
|
|
87
|
+
directed: true,
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Defensive resolution of `templateData`-supplied nodes/edges: the data is
|
|
94
|
+
* untyped at runtime (it can come from a hand-written `json data` fence),
|
|
95
|
+
* so malformed entries are dropped rather than crashing the template.
|
|
96
|
+
*/
|
|
97
|
+
function nodesFromTemplateData(input: DiagramBlockInput): ResolvedDiagram {
|
|
98
|
+
const rawNodes = Array.isArray(input.nodes) ? input.nodes : [];
|
|
99
|
+
const valid = rawNodes.filter(
|
|
100
|
+
(n): n is DiagramTemplateNode =>
|
|
101
|
+
typeof n === 'object' &&
|
|
102
|
+
n !== null &&
|
|
103
|
+
typeof n.id === 'string' &&
|
|
104
|
+
n.id.length > 0 &&
|
|
105
|
+
typeof n.label === 'string' &&
|
|
106
|
+
typeof n.x === 'number' &&
|
|
107
|
+
Number.isFinite(n.x) &&
|
|
108
|
+
typeof n.y === 'number' &&
|
|
109
|
+
Number.isFinite(n.y),
|
|
110
|
+
);
|
|
111
|
+
const ids = new Set(valid.map((n) => n.id));
|
|
112
|
+
const containerIds = new Set(
|
|
113
|
+
valid.map((n) => n.container).filter((c): c is string => typeof c === 'string' && ids.has(c)),
|
|
114
|
+
);
|
|
115
|
+
const depthOf = (node: DiagramTemplateNode): number => {
|
|
116
|
+
let depth = 0;
|
|
117
|
+
let current: DiagramTemplateNode | undefined = node;
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
while (current?.container && ids.has(current.container) && !seen.has(current.id)) {
|
|
120
|
+
seen.add(current.id);
|
|
121
|
+
depth++;
|
|
122
|
+
current = valid.find((n) => n.id === current?.container);
|
|
123
|
+
}
|
|
124
|
+
return depth;
|
|
125
|
+
};
|
|
126
|
+
const nodes: ResolvedDiagramNode[] = valid.map((n) => ({
|
|
127
|
+
id: n.id,
|
|
128
|
+
label: n.label,
|
|
129
|
+
x: n.x,
|
|
130
|
+
y: n.y,
|
|
131
|
+
w: typeof n.w === 'number' && Number.isFinite(n.w) && n.w > 0 ? n.w : NODE_WIDTH,
|
|
132
|
+
h: typeof n.h === 'number' && Number.isFinite(n.h) && n.h > 0 ? n.h : NODE_HEIGHT,
|
|
133
|
+
depth: depthOf(n),
|
|
134
|
+
isContainer: containerIds.has(n.id),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const rawEdges = Array.isArray(input.edges) ? input.edges : [];
|
|
138
|
+
const edges: ResolvedDiagramEdge[] = rawEdges
|
|
139
|
+
.filter(
|
|
140
|
+
(e): e is DiagramTemplateEdge =>
|
|
141
|
+
typeof e === 'object' &&
|
|
142
|
+
e !== null &&
|
|
143
|
+
typeof e.source === 'string' &&
|
|
144
|
+
typeof e.target === 'string' &&
|
|
145
|
+
ids.has(e.source) &&
|
|
146
|
+
ids.has(e.target),
|
|
147
|
+
)
|
|
148
|
+
.map((e, i) => ({
|
|
149
|
+
id: e.label ? `${e.source}->${e.target}:${e.label}` : `${e.source}->${e.target}-${i}`,
|
|
150
|
+
source: e.source,
|
|
151
|
+
target: e.target,
|
|
152
|
+
...(e.label ? { label: e.label } : {}),
|
|
153
|
+
directed: e.directed !== false,
|
|
154
|
+
...(e.sourceAnchor ? { sourceAnchor: e.sourceAnchor } : {}),
|
|
155
|
+
...(e.targetAnchor ? { targetAnchor: e.targetAnchor } : {}),
|
|
156
|
+
...(e.routing ? { routing: e.routing } : {}),
|
|
157
|
+
}));
|
|
158
|
+
return { nodes, edges };
|
|
159
|
+
}
|
|
160
|
+
|
|
27
161
|
export function diagramBlock(input: DiagramBlockInput, context: TemplateContext): Layer[] {
|
|
28
162
|
const { theme, viewport, children = [] } = context;
|
|
29
163
|
const colors = resolveColorScheme(context, input.colorScheme ?? 'blue');
|
|
30
164
|
|
|
31
|
-
const
|
|
32
|
-
|
|
165
|
+
const resolved =
|
|
166
|
+
children.length > 0
|
|
167
|
+
? nodesFromChildren(context)
|
|
168
|
+
: input.nodes && input.nodes.length > 0
|
|
169
|
+
? nodesFromTemplateData(input)
|
|
170
|
+
: { nodes: [], edges: [] };
|
|
171
|
+
|
|
172
|
+
if (resolved.nodes.length === 0) {
|
|
33
173
|
// Empty diagram — render a single hint label so the block has visible content.
|
|
34
174
|
return [
|
|
35
175
|
{
|
|
@@ -50,10 +190,10 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
50
190
|
}
|
|
51
191
|
|
|
52
192
|
// Compute the bounding box of all nodes in author coordinates.
|
|
53
|
-
const minX = Math.min(...
|
|
54
|
-
const maxX = Math.max(...
|
|
55
|
-
const minY = Math.min(...
|
|
56
|
-
const maxY = Math.max(...
|
|
193
|
+
const minX = Math.min(...resolved.nodes.map((n) => n.x));
|
|
194
|
+
const maxX = Math.max(...resolved.nodes.map((n) => n.x + n.w));
|
|
195
|
+
const minY = Math.min(...resolved.nodes.map((n) => n.y));
|
|
196
|
+
const maxY = Math.max(...resolved.nodes.map((n) => n.y + n.h));
|
|
57
197
|
|
|
58
198
|
// Available area in viewport pixels (after reserving space for the title, if any).
|
|
59
199
|
const titleHeight = input.title ? 120 : 0;
|
|
@@ -79,11 +219,11 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
79
219
|
const groupTop = Math.max(PADDING / 2, (viewport.height - titleHeight - scaledH) / 2);
|
|
80
220
|
const offsetX = PADDING + (availW - scaledW) / 2;
|
|
81
221
|
const offsetY = groupTop + titleHeight;
|
|
82
|
-
const transform = (n:
|
|
222
|
+
const transform = (n: ResolvedDiagramNode): { x: number; y: number; w: number; h: number } => ({
|
|
83
223
|
x: offsetX + (n.x - minX) * scale,
|
|
84
224
|
y: offsetY + (n.y - minY) * scale,
|
|
85
|
-
w:
|
|
86
|
-
h:
|
|
225
|
+
w: n.w * scale,
|
|
226
|
+
h: n.h * scale,
|
|
87
227
|
});
|
|
88
228
|
|
|
89
229
|
const layers: Layer[] = [];
|
|
@@ -107,29 +247,57 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
107
247
|
});
|
|
108
248
|
}
|
|
109
249
|
|
|
110
|
-
|
|
250
|
+
const containers = resolved.nodes
|
|
251
|
+
.filter((n) => n.isContainer)
|
|
252
|
+
.sort((a, b) => a.depth - b.depth || a.y - b.y || a.x - b.x);
|
|
253
|
+
const leaves = resolved.nodes.filter((n) => !n.isContainer);
|
|
254
|
+
|
|
255
|
+
// Container cards first — translucent grouping surfaces behind everything.
|
|
256
|
+
for (const node of containers) {
|
|
257
|
+
const t = transform(node);
|
|
258
|
+
const card: ShapeLayer = {
|
|
259
|
+
type: 'shape',
|
|
260
|
+
id: `node-card-${node.id}`,
|
|
261
|
+
content: {
|
|
262
|
+
shape: 'rect',
|
|
263
|
+
fill: colors.bg ?? theme.colors.backgroundLight,
|
|
264
|
+
fillOpacity: 0.25,
|
|
265
|
+
stroke: colors.text ?? theme.colors.primary,
|
|
266
|
+
strokeWidth: Math.max(1, strokeW - 1),
|
|
267
|
+
borderRadius: 10,
|
|
268
|
+
},
|
|
269
|
+
position: { x: t.x, y: t.y, width: t.w, height: t.h },
|
|
270
|
+
};
|
|
271
|
+
layers.push(card);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Position lookup by id for edge routing (containers included — edges may
|
|
275
|
+
// attach to them, e.g. a through-border split from a parsed fence).
|
|
111
276
|
const positions = new Map<string, { cx: number; cy: number; rx: number; ry: number }>();
|
|
112
|
-
for (const node of
|
|
277
|
+
for (const node of resolved.nodes) {
|
|
113
278
|
const t = transform(node);
|
|
114
279
|
positions.set(node.id, { cx: t.x + t.w / 2, cy: t.y + t.h / 2, rx: t.w / 2, ry: t.h / 2 });
|
|
115
280
|
}
|
|
116
281
|
|
|
117
|
-
// Edge styling (applies to all edges; per-edge
|
|
282
|
+
// Edge styling (applies to all edges; direction comes per-edge).
|
|
118
283
|
const startMarker: MarkerStyle = input.startStyle ?? 'none';
|
|
119
|
-
const
|
|
284
|
+
const configuredEnd: MarkerStyle = input.endStyle ?? 'arrow';
|
|
120
285
|
const edgeDash = lineStyleDasharray(input.lineStyle);
|
|
121
286
|
|
|
122
|
-
// Edges
|
|
123
|
-
for (const edge of
|
|
287
|
+
// Edges above containers, behind leaf nodes.
|
|
288
|
+
for (const edge of resolved.edges) {
|
|
124
289
|
const a = positions.get(edge.source);
|
|
125
290
|
const b = positions.get(edge.target);
|
|
126
291
|
if (!a || !b) continue;
|
|
127
|
-
const
|
|
292
|
+
const endMarker: MarkerStyle = edge.directed ? configuredEnd : 'none';
|
|
293
|
+
const snapped = snapEndpoints(a, b);
|
|
294
|
+
const start = edge.sourceAnchor ? anchorPoint(a, edge.sourceAnchor) : snapped.start;
|
|
295
|
+
const end = edge.targetAnchor ? anchorPoint(b, edge.targetAnchor) : snapped.end;
|
|
128
296
|
const pathLayer: PathLayer = {
|
|
129
297
|
type: 'path',
|
|
130
298
|
id: `edge-${edge.id}`,
|
|
131
299
|
content: {
|
|
132
|
-
d: connectorPath(input.edgeStyle ?? 'curved', start, end),
|
|
300
|
+
d: connectorPath(edge.routing ?? input.edgeStyle ?? 'curved', start, end),
|
|
133
301
|
stroke: colors.text ?? theme.colors.primary,
|
|
134
302
|
strokeWidth: strokeW,
|
|
135
303
|
fill: 'none',
|
|
@@ -141,9 +309,9 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
141
309
|
};
|
|
142
310
|
layers.push(pathLayer);
|
|
143
311
|
|
|
144
|
-
if (edge.
|
|
145
|
-
//
|
|
146
|
-
//
|
|
312
|
+
if (edge.label) {
|
|
313
|
+
// Edge label, floated just above the edge midpoint so the line never
|
|
314
|
+
// strikes through the text.
|
|
147
315
|
const mx = (a.cx + b.cx) / 2;
|
|
148
316
|
const my = (a.cy + b.cy) / 2;
|
|
149
317
|
const labelFontSize = themedFontSize(Math.round(18 * fontAdj), context, false);
|
|
@@ -151,7 +319,7 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
151
319
|
type: 'text',
|
|
152
320
|
id: `edge-label-${edge.id}`,
|
|
153
321
|
content: {
|
|
154
|
-
text: edge.
|
|
322
|
+
text: edge.label,
|
|
155
323
|
style: {
|
|
156
324
|
fontSize: labelFontSize,
|
|
157
325
|
fontFamily: getThemeFont(context, 'body'),
|
|
@@ -165,8 +333,8 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
165
333
|
}
|
|
166
334
|
}
|
|
167
335
|
|
|
168
|
-
//
|
|
169
|
-
for (const node of
|
|
336
|
+
// Leaf nodes (card + label) on top of edges.
|
|
337
|
+
for (const node of leaves) {
|
|
170
338
|
const t = transform(node);
|
|
171
339
|
const card: ShapeLayer = {
|
|
172
340
|
type: 'shape',
|
|
@@ -182,20 +350,51 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
182
350
|
};
|
|
183
351
|
layers.push(card);
|
|
184
352
|
|
|
353
|
+
const preferredFontSize = themedFontSize(Math.round(22 * fontAdj), context, false);
|
|
354
|
+
const fit = fitDiagramLabel(node.label, t.w, t.h, preferredFontSize);
|
|
185
355
|
const label: TextLayer = {
|
|
186
356
|
type: 'text',
|
|
187
357
|
id: `node-label-${node.id}`,
|
|
188
358
|
content: {
|
|
189
359
|
text: node.label,
|
|
190
360
|
style: {
|
|
191
|
-
fontSize:
|
|
361
|
+
fontSize: fit.fontSize,
|
|
192
362
|
fontFamily: getThemeFont(context, 'body'),
|
|
193
363
|
fontWeight: 'bold',
|
|
194
364
|
color: colors.text ?? theme.colors.text,
|
|
195
365
|
textAlign: 'center',
|
|
366
|
+
lineHeight: DIAGRAM_LABEL_LINE_HEIGHT,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
position: {
|
|
370
|
+
x: t.x + t.w / 2,
|
|
371
|
+
y: t.y + t.h / 2 + fit.firstLineOffset,
|
|
372
|
+
anchor: 'center',
|
|
373
|
+
width: fit.textWidth,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
layers.push(label);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Container titles last — anchored near the container's top edge, above
|
|
380
|
+
// any child content that could otherwise overprint them.
|
|
381
|
+
for (const node of containers) {
|
|
382
|
+
const t = transform(node);
|
|
383
|
+
const fontSize = themedFontSize(Math.round(20 * fontAdj), context, false);
|
|
384
|
+
const label: TextLayer = {
|
|
385
|
+
type: 'text',
|
|
386
|
+
id: `node-label-${node.id}`,
|
|
387
|
+
content: {
|
|
388
|
+
text: node.label.split('\n')[0] ?? '',
|
|
389
|
+
style: {
|
|
390
|
+
fontSize,
|
|
391
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
392
|
+
fontWeight: 'bold',
|
|
393
|
+
color: theme.colors.textMuted,
|
|
394
|
+
textAlign: 'center',
|
|
196
395
|
},
|
|
197
396
|
},
|
|
198
|
-
position: { x: t.x + t.w / 2, y: t.y +
|
|
397
|
+
position: { x: t.x + t.w / 2, y: t.y + fontSize, anchor: 'center', width: t.w },
|
|
199
398
|
};
|
|
200
399
|
layers.push(label);
|
|
201
400
|
}
|
|
@@ -18,7 +18,7 @@ import type { Layer, ShapeLayer, TextLayer, PathLayer } from '../../schemas/Doc.
|
|
|
18
18
|
import type { DrawingBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
19
|
import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
|
|
20
20
|
import { computeDrawingLayout, type DrawingShape, type DrawingConnector } from './drawingLayout.js';
|
|
21
|
-
import { shapePath, connectorPath,
|
|
21
|
+
import { shapePath, connectorPath, snapEndpoints } from '../utils/shapeGeometry.js';
|
|
22
22
|
|
|
23
23
|
const PADDING = 80;
|
|
24
24
|
|
|
@@ -102,7 +102,7 @@ export function drawingBlock(input: DrawingBlockInput, context: TemplateContext)
|
|
|
102
102
|
const a = boxes.get(conn.from);
|
|
103
103
|
const b = boxes.get(conn.to);
|
|
104
104
|
if (!a || !b) continue;
|
|
105
|
-
const { start, end } =
|
|
105
|
+
const { start, end } = snapEndpoints(a, b);
|
|
106
106
|
const path: PathLayer = {
|
|
107
107
|
type: 'path',
|
|
108
108
|
id: `connector-${conn.id}`,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* agents reviewing screenshots) can see *why* the block degraded.
|
|
9
9
|
*
|
|
10
10
|
* This is intentionally not a registered template: it can't be requested by
|
|
11
|
-
* name, only reached when a requested template fails. See `
|
|
11
|
+
* name, only reached when a requested template fails. See `materializeBlockLayers()`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { Layer } from '../../schemas/Doc.js';
|