@bendyline/squisq 1.4.1 → 1.5.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/{Doc-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
- package/dist/chunk-4AKUQPEE.js.map +1 -0
- package/dist/chunk-4ZFRZBU2.js +6582 -0
- package/dist/chunk-4ZFRZBU2.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
- package/dist/chunk-7MQGNR76.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/{chunk-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
- package/dist/chunk-FBKP5CAO.js.map +1 -0
- package/dist/chunk-KZNXZ5SM.js +333 -0
- package/dist/chunk-KZNXZ5SM.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
- package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
- package/dist/chunk-OH43RPTP.js.map +1 -0
- package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
- package/dist/chunk-OKJOQXK4.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/chunk-TVSGLUID.js +596 -0
- package/dist/chunk-TVSGLUID.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +4 -4
- package/dist/doc/index.js +124 -6
- package/dist/generate/index.d.ts +9 -2
- package/dist/generate/index.js +1 -1
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +11 -5
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +11 -8
- package/dist/index.js +243 -24
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/markdown/index.d.ts +212 -3
- package/dist/markdown/index.js +40 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +35 -5
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/story/index.d.ts +906 -40
- package/dist/story/index.js +124 -6
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/customTemplateSample.test.ts +83 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +118 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +23 -0
- package/src/__tests__/markdownSanitize.test.ts +64 -0
- package/src/__tests__/markdownToDoc.test.ts +223 -2
- package/src/__tests__/mediaAnnotations.test.ts +64 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +179 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +21 -0
- package/src/__tests__/templates.test.ts +145 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +100 -0
- package/src/doc/customTemplatesFrontmatter.ts +308 -0
- package/src/doc/customThemesFrontmatter.ts +90 -0
- package/src/doc/docToMarkdown.ts +73 -4
- package/src/doc/getLayers.ts +40 -17
- package/src/doc/index.ts +35 -1
- package/src/doc/markdownToDoc.ts +333 -12
- package/src/doc/mediaAnnotations.ts +118 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/structuredData.ts +230 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +151 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +26 -18
- package/src/doc/templates/coverBlock.ts +7 -4
- package/src/doc/templates/customTemplate.ts +45 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +51 -25
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +22 -9
- package/src/doc/templates/index.ts +215 -46
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +23 -12
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +221 -0
- package/src/doc/templates/tokens/resolveTokens.ts +0 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +13 -8
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +476 -0
- package/src/generate/slideshowGenerator.ts +7 -0
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +222 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +36 -0
- package/src/markdown/sanitize.ts +298 -0
- package/src/markdown/stringify.ts +84 -3
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +49 -1
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +74 -0
- package/src/schemas/Doc.ts +353 -29
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +23 -4
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { DefinitionCardInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
resolveColorScheme,
|
|
15
|
+
getThemeFont,
|
|
16
|
+
shouldUseShadow,
|
|
17
|
+
themedEntrance,
|
|
18
|
+
themedFontSize,
|
|
19
|
+
themedSurfaceGradient,
|
|
20
|
+
themedImageTreatment,
|
|
21
|
+
} from '../utils/themeUtils.js';
|
|
15
22
|
import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
|
|
16
23
|
import { createBackgroundLayer } from './captionUtils.js';
|
|
17
24
|
|
|
@@ -23,20 +30,21 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
23
30
|
// Get layout adjustments if accent image is present
|
|
24
31
|
const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
|
|
25
32
|
|
|
26
|
-
const termFontSize =
|
|
27
|
-
const defFontSize =
|
|
28
|
-
const originFontSize =
|
|
33
|
+
const termFontSize = themedFontSize(72, context, true);
|
|
34
|
+
const defFontSize = themedFontSize(32, context, false);
|
|
35
|
+
const originFontSize = themedFontSize(22, context, false);
|
|
29
36
|
|
|
30
|
-
const layers: Layer[] = [
|
|
31
|
-
createBackgroundLayer(
|
|
32
|
-
'bg',
|
|
33
|
-
`linear-gradient(145deg, #1e2030 0%, ${theme.colors.background} 100%)`,
|
|
34
|
-
),
|
|
35
|
-
];
|
|
37
|
+
const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 145))];
|
|
36
38
|
|
|
37
39
|
// Add accent image layers
|
|
38
40
|
if (accentImage) {
|
|
39
|
-
layers.push(
|
|
41
|
+
layers.push(
|
|
42
|
+
...createAccentLayers(
|
|
43
|
+
accentImage,
|
|
44
|
+
input.id,
|
|
45
|
+
themedImageTreatment(context, input.imageTreatment),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// Term — large, accent-colored
|
|
@@ -51,7 +59,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
51
59
|
fontWeight: 'bold',
|
|
52
60
|
color: colors.text,
|
|
53
61
|
textAlign: 'center',
|
|
54
|
-
shadow:
|
|
62
|
+
shadow: shouldUseShadow(context),
|
|
55
63
|
},
|
|
56
64
|
},
|
|
57
65
|
position: {
|
|
@@ -59,7 +67,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
59
67
|
y: adjustY('30%', accentLayout),
|
|
60
68
|
anchor: 'center',
|
|
61
69
|
},
|
|
62
|
-
animation: { type: 'fadeIn', duration: 1.5 },
|
|
70
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1.5 }),
|
|
63
71
|
});
|
|
64
72
|
|
|
65
73
|
// Horizontal separator line
|
|
@@ -92,7 +100,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
92
100
|
textAlign: 'center',
|
|
93
101
|
lineHeight: 1.6,
|
|
94
102
|
maxLines: 4,
|
|
95
|
-
shadow:
|
|
103
|
+
shadow: shouldUseShadow(context),
|
|
96
104
|
},
|
|
97
105
|
},
|
|
98
106
|
position: {
|
|
@@ -101,7 +109,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
101
109
|
width: accentLayout.textWidth,
|
|
102
110
|
anchor: 'center',
|
|
103
111
|
},
|
|
104
|
-
animation: { type: 'fadeIn', duration: 1, delay: 0.8 },
|
|
112
|
+
animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1, delay: 0.8 }),
|
|
105
113
|
});
|
|
106
114
|
|
|
107
115
|
// Origin if provided
|
|
@@ -116,12 +124,12 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
|
|
|
116
124
|
fontFamily: getThemeFont(context, 'body'),
|
|
117
125
|
color: theme.colors.textMuted,
|
|
118
126
|
textAlign: 'center',
|
|
119
|
-
shadow:
|
|
127
|
+
shadow: shouldUseShadow(context),
|
|
120
128
|
},
|
|
121
129
|
},
|
|
122
130
|
position: {
|
|
123
131
|
x: accentLayout.textCenterX,
|
|
124
|
-
y: adjustY('
|
|
132
|
+
y: adjustY('70%', accentLayout),
|
|
125
133
|
anchor: 'center',
|
|
126
134
|
},
|
|
127
135
|
animation: { type: 'fadeIn', duration: 0.8, delay: 1.5 },
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram block template.
|
|
3
|
+
*
|
|
4
|
+
* Renders the parent block's children as a node-and-edge diagram:
|
|
5
|
+
* - Each child becomes a rounded-rect node card with its title as label.
|
|
6
|
+
* - Each `child.connectsTo` entry becomes a path edge (cubic-bezier curve
|
|
7
|
+
* with an arrowhead at the target end).
|
|
8
|
+
* - Children with `x`/`y` set are placed at those coordinates; the rest
|
|
9
|
+
* are auto-laid out below in a square-ish grid (see `diagramLayout.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Layout coordinates from `x=`/`y=` are author-defined units. The
|
|
12
|
+
* template computes a bounding box of all nodes and scales it to fit
|
|
13
|
+
* the block's viewport with padding, so users don't have to think about
|
|
14
|
+
* absolute pixel ranges.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Layer, ShapeLayer, TextLayer, PathLayer, MarkerStyle } from '../../schemas/Doc.js';
|
|
18
|
+
import type { DiagramBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
|
+
import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
|
|
20
|
+
import { clipEndpoints, connectorPath, lineStyleDasharray } from '../utils/shapeGeometry.js';
|
|
21
|
+
import { computeDiagramLayout, type DiagramNodePosition } from './diagramLayout.js';
|
|
22
|
+
|
|
23
|
+
const NODE_WIDTH = 180;
|
|
24
|
+
const NODE_HEIGHT = 64;
|
|
25
|
+
const PADDING = 80;
|
|
26
|
+
|
|
27
|
+
export function diagramBlock(input: DiagramBlockInput, context: TemplateContext): Layer[] {
|
|
28
|
+
const { theme, viewport, children = [] } = context;
|
|
29
|
+
const colors = resolveColorScheme(context, input.colorScheme ?? 'blue');
|
|
30
|
+
|
|
31
|
+
const layout = computeDiagramLayout(children);
|
|
32
|
+
if (layout.nodes.length === 0) {
|
|
33
|
+
// Empty diagram — render a single hint label so the block has visible content.
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
type: 'text',
|
|
37
|
+
id: 'diagram-empty',
|
|
38
|
+
content: {
|
|
39
|
+
text: input.title ?? 'Empty diagram',
|
|
40
|
+
style: {
|
|
41
|
+
fontSize: themedFontSize(36, context, true),
|
|
42
|
+
fontFamily: getThemeFont(context, 'title'),
|
|
43
|
+
color: theme.colors.textMuted,
|
|
44
|
+
textAlign: 'center',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
position: { x: '50%', y: '50%', anchor: 'center' },
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compute the bounding box of all nodes in author coordinates.
|
|
53
|
+
const minX = Math.min(...layout.nodes.map((n) => n.x));
|
|
54
|
+
const maxX = Math.max(...layout.nodes.map((n) => n.x + NODE_WIDTH));
|
|
55
|
+
const minY = Math.min(...layout.nodes.map((n) => n.y));
|
|
56
|
+
const maxY = Math.max(...layout.nodes.map((n) => n.y + NODE_HEIGHT));
|
|
57
|
+
|
|
58
|
+
// Available area in viewport pixels (after reserving space for the title, if any).
|
|
59
|
+
const titleHeight = input.title ? 120 : 0;
|
|
60
|
+
const availW = Math.max(1, viewport.width - PADDING * 2);
|
|
61
|
+
const availH = Math.max(1, viewport.height - PADDING * 2 - titleHeight);
|
|
62
|
+
const contentW = Math.max(1, maxX - minX);
|
|
63
|
+
const contentH = Math.max(1, maxY - minY);
|
|
64
|
+
|
|
65
|
+
// Uniform scale so the diagram fits both dimensions. Small diagrams are
|
|
66
|
+
// allowed to grow (everything here is vector) up to 1.8× so a three-node
|
|
67
|
+
// flow doesn't render as a tiny cluster in an empty canvas; label fonts
|
|
68
|
+
// and strokes scale with the nodes, clamped so text never balloons.
|
|
69
|
+
const scale = Math.min(availW / contentW, availH / contentH, 1.8);
|
|
70
|
+
const fontAdj = Math.min(Math.max(scale, 1), 1.5);
|
|
71
|
+
const strokeW = Math.round(2 * fontAdj);
|
|
72
|
+
|
|
73
|
+
// Compute viewport-pixel coordinates of each node's top-left corner.
|
|
74
|
+
// The title and the scaled diagram are centered together as one group —
|
|
75
|
+
// a top-pinned title over a center-floated diagram split the block into
|
|
76
|
+
// two stranded pieces.
|
|
77
|
+
const scaledW = contentW * scale;
|
|
78
|
+
const scaledH = contentH * scale;
|
|
79
|
+
const groupTop = Math.max(PADDING / 2, (viewport.height - titleHeight - scaledH) / 2);
|
|
80
|
+
const offsetX = PADDING + (availW - scaledW) / 2;
|
|
81
|
+
const offsetY = groupTop + titleHeight;
|
|
82
|
+
const transform = (n: DiagramNodePosition): { x: number; y: number; w: number; h: number } => ({
|
|
83
|
+
x: offsetX + (n.x - minX) * scale,
|
|
84
|
+
y: offsetY + (n.y - minY) * scale,
|
|
85
|
+
w: NODE_WIDTH * scale,
|
|
86
|
+
h: NODE_HEIGHT * scale,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const layers: Layer[] = [];
|
|
90
|
+
|
|
91
|
+
// Optional title above the diagram.
|
|
92
|
+
if (input.title) {
|
|
93
|
+
layers.push({
|
|
94
|
+
type: 'text',
|
|
95
|
+
id: 'diagram-title',
|
|
96
|
+
content: {
|
|
97
|
+
text: input.title,
|
|
98
|
+
style: {
|
|
99
|
+
fontSize: themedFontSize(40, context, true),
|
|
100
|
+
fontFamily: getThemeFont(context, 'title'),
|
|
101
|
+
fontWeight: 'bold',
|
|
102
|
+
color: theme.colors.text,
|
|
103
|
+
textAlign: 'center',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
position: { x: '50%', y: groupTop + titleHeight / 2 - 16, anchor: 'center' },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Position lookup by id for edge routing.
|
|
111
|
+
const positions = new Map<string, { cx: number; cy: number; rx: number; ry: number }>();
|
|
112
|
+
for (const node of layout.nodes) {
|
|
113
|
+
const t = transform(node);
|
|
114
|
+
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
|
+
}
|
|
116
|
+
|
|
117
|
+
// Edge styling (applies to all edges; per-edge styling lives on the nodes).
|
|
118
|
+
const startMarker: MarkerStyle = input.startStyle ?? 'none';
|
|
119
|
+
const endMarker: MarkerStyle = input.endStyle ?? 'arrow';
|
|
120
|
+
const edgeDash = lineStyleDasharray(input.lineStyle);
|
|
121
|
+
|
|
122
|
+
// Edges first so they sit behind nodes.
|
|
123
|
+
for (const edge of layout.edges) {
|
|
124
|
+
const a = positions.get(edge.source);
|
|
125
|
+
const b = positions.get(edge.target);
|
|
126
|
+
if (!a || !b) continue;
|
|
127
|
+
const { start, end } = clipEndpoints(a, b);
|
|
128
|
+
const pathLayer: PathLayer = {
|
|
129
|
+
type: 'path',
|
|
130
|
+
id: `edge-${edge.id}`,
|
|
131
|
+
content: {
|
|
132
|
+
d: connectorPath(input.edgeStyle ?? 'curved', start, end),
|
|
133
|
+
stroke: colors.text ?? theme.colors.primary,
|
|
134
|
+
strokeWidth: strokeW,
|
|
135
|
+
fill: 'none',
|
|
136
|
+
...(edgeDash ? { dasharray: edgeDash } : {}),
|
|
137
|
+
...(startMarker !== 'none' ? { startMarker } : {}),
|
|
138
|
+
...(endMarker !== 'none' ? { endMarker } : {}),
|
|
139
|
+
},
|
|
140
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
141
|
+
};
|
|
142
|
+
layers.push(pathLayer);
|
|
143
|
+
|
|
144
|
+
if (edge.type) {
|
|
145
|
+
// Connection-type label, floated just above the edge midpoint so the
|
|
146
|
+
// line never strikes through the text.
|
|
147
|
+
const mx = (a.cx + b.cx) / 2;
|
|
148
|
+
const my = (a.cy + b.cy) / 2;
|
|
149
|
+
const labelFontSize = themedFontSize(Math.round(18 * fontAdj), context, false);
|
|
150
|
+
const labelLayer: TextLayer = {
|
|
151
|
+
type: 'text',
|
|
152
|
+
id: `edge-label-${edge.id}`,
|
|
153
|
+
content: {
|
|
154
|
+
text: edge.type,
|
|
155
|
+
style: {
|
|
156
|
+
fontSize: labelFontSize,
|
|
157
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
158
|
+
color: theme.colors.textMuted,
|
|
159
|
+
textAlign: 'center',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
position: { x: mx, y: my - labelFontSize * 0.9, anchor: 'center' },
|
|
163
|
+
};
|
|
164
|
+
layers.push(labelLayer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Nodes (card + label) on top of edges.
|
|
169
|
+
for (const node of layout.nodes) {
|
|
170
|
+
const t = transform(node);
|
|
171
|
+
const card: ShapeLayer = {
|
|
172
|
+
type: 'shape',
|
|
173
|
+
id: `node-card-${node.id}`,
|
|
174
|
+
content: {
|
|
175
|
+
shape: 'rect',
|
|
176
|
+
fill: colors.bg ?? theme.colors.backgroundLight,
|
|
177
|
+
stroke: colors.text ?? theme.colors.primary,
|
|
178
|
+
strokeWidth: strokeW,
|
|
179
|
+
borderRadius: input.nodeShape === 'pill' ? t.h / 2 : 10,
|
|
180
|
+
},
|
|
181
|
+
position: { x: t.x, y: t.y, width: t.w, height: t.h },
|
|
182
|
+
};
|
|
183
|
+
layers.push(card);
|
|
184
|
+
|
|
185
|
+
const label: TextLayer = {
|
|
186
|
+
type: 'text',
|
|
187
|
+
id: `node-label-${node.id}`,
|
|
188
|
+
content: {
|
|
189
|
+
text: node.label,
|
|
190
|
+
style: {
|
|
191
|
+
fontSize: themedFontSize(Math.round(22 * fontAdj), context, false),
|
|
192
|
+
fontFamily: getThemeFont(context, 'body'),
|
|
193
|
+
fontWeight: 'bold',
|
|
194
|
+
color: colors.text ?? theme.colors.text,
|
|
195
|
+
textAlign: 'center',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
position: { x: t.x + t.w / 2, y: t.y + t.h / 2, anchor: 'center', width: t.w },
|
|
199
|
+
};
|
|
200
|
+
layers.push(label);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return layers;
|
|
204
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram layout — shared between the read-only SVG render path
|
|
3
|
+
* (`diagramBlock` template) and the interactive editor (React Flow in
|
|
4
|
+
* `@bendyline/squisq-editor-react`). Same children + same options always
|
|
5
|
+
* produce the same node positions and edge list, so previews and the
|
|
6
|
+
* editor stay visually consistent.
|
|
7
|
+
*
|
|
8
|
+
* Layout rules:
|
|
9
|
+
* 1. Children with both `x` and `y` set keep their authored coordinates.
|
|
10
|
+
* 2. Children missing either coordinate are auto-placed in a square-ish
|
|
11
|
+
* grid in declaration order, offset below the bounding box of the
|
|
12
|
+
* explicitly-positioned children.
|
|
13
|
+
* 3. Edges come from each child's `connectsTo`. Targets that don't match
|
|
14
|
+
* any sibling id are dropped (and counted in `warnings`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Block, BlockConnection } from '../../schemas/Doc.js';
|
|
18
|
+
|
|
19
|
+
export interface DiagramNodePosition {
|
|
20
|
+
/** Block id (matches `Block.id`). */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Display label (block title or fallback to id). */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Pixel x coordinate (canvas-relative). */
|
|
25
|
+
x: number;
|
|
26
|
+
/** Pixel y coordinate (canvas-relative). */
|
|
27
|
+
y: number;
|
|
28
|
+
/** True when the position came from `x=`/`y=` attributes; false when auto-laid out. */
|
|
29
|
+
pinned: boolean;
|
|
30
|
+
/** Optional `.class` tokens carried over from the heading. */
|
|
31
|
+
classes?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DiagramEdge {
|
|
35
|
+
/** Synthetic stable edge id. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Source node id. */
|
|
38
|
+
source: string;
|
|
39
|
+
/** Target node id. */
|
|
40
|
+
target: string;
|
|
41
|
+
/** Connection type from `connectsTo=target:type` (used as edge label). */
|
|
42
|
+
type?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DiagramLayout {
|
|
46
|
+
nodes: DiagramNodePosition[];
|
|
47
|
+
edges: DiagramEdge[];
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DiagramLayoutOptions {
|
|
52
|
+
/** Horizontal grid pitch between auto-laid-out nodes. Default: 260 (node width 180 + 80 air). */
|
|
53
|
+
gapX?: number;
|
|
54
|
+
/** Vertical grid pitch between auto-laid-out nodes. Default: 140. */
|
|
55
|
+
gapY?: number;
|
|
56
|
+
/** Padding around the explicit-positions bounding box before the grid starts. Default: 80. */
|
|
57
|
+
gridPad?: number;
|
|
58
|
+
/** Origin x for the grid when no explicit nodes anchor it. Default: 80. */
|
|
59
|
+
gridOriginX?: number;
|
|
60
|
+
/** Origin y for the grid when no explicit nodes anchor it. Default: 80. */
|
|
61
|
+
gridOriginY?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULT_OPTS: Required<DiagramLayoutOptions> = {
|
|
65
|
+
gapX: 260,
|
|
66
|
+
gapY: 140,
|
|
67
|
+
gridPad: 80,
|
|
68
|
+
gridOriginX: 80,
|
|
69
|
+
gridOriginY: 80,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute node positions and edges for a diagram from a parent block's
|
|
74
|
+
* children. Pure function — same inputs always produce the same output.
|
|
75
|
+
*/
|
|
76
|
+
export function computeDiagramLayout(
|
|
77
|
+
children: readonly Block[],
|
|
78
|
+
options: DiagramLayoutOptions = {},
|
|
79
|
+
): DiagramLayout {
|
|
80
|
+
const opts = { ...DEFAULT_OPTS, ...options };
|
|
81
|
+
const warnings: string[] = [];
|
|
82
|
+
|
|
83
|
+
// First pass: extract explicit positions, record which children still need a grid slot.
|
|
84
|
+
type Entry = { child: Block; pinned: boolean; x: number; y: number };
|
|
85
|
+
const entries: Entry[] = [];
|
|
86
|
+
const unpositionedIdx: number[] = [];
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < children.length; i++) {
|
|
89
|
+
const c = children[i];
|
|
90
|
+
const hasX = typeof c.x === 'number';
|
|
91
|
+
const hasY = typeof c.y === 'number';
|
|
92
|
+
if (hasX && hasY) {
|
|
93
|
+
entries.push({ child: c, pinned: true, x: c.x as number, y: c.y as number });
|
|
94
|
+
} else {
|
|
95
|
+
entries.push({ child: c, pinned: false, x: 0, y: 0 });
|
|
96
|
+
unpositionedIdx.push(i);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Second pass: lay out unpositioned children in a square-ish grid.
|
|
101
|
+
if (unpositionedIdx.length > 0) {
|
|
102
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
|
|
103
|
+
// Find the bottom-right of the pinned bounding box so the grid sits below it.
|
|
104
|
+
let baseX = opts.gridOriginX;
|
|
105
|
+
let baseY = opts.gridOriginY;
|
|
106
|
+
const pinned = entries.filter((e) => e.pinned);
|
|
107
|
+
if (pinned.length > 0) {
|
|
108
|
+
const minX = Math.min(...pinned.map((e) => e.x));
|
|
109
|
+
const maxY = Math.max(...pinned.map((e) => e.y));
|
|
110
|
+
baseX = minX;
|
|
111
|
+
baseY = maxY + opts.gridPad + opts.gapY;
|
|
112
|
+
}
|
|
113
|
+
unpositionedIdx.forEach((entryIdx, k) => {
|
|
114
|
+
const col = k % cols;
|
|
115
|
+
const row = Math.floor(k / cols);
|
|
116
|
+
entries[entryIdx].x = baseX + col * opts.gapX;
|
|
117
|
+
entries[entryIdx].y = baseY + row * opts.gapY;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Third pass: emit nodes + the id-set used to validate edge targets.
|
|
122
|
+
const nodes: DiagramNodePosition[] = entries.map((e) => ({
|
|
123
|
+
id: e.child.id,
|
|
124
|
+
label: e.child.title ?? e.child.id,
|
|
125
|
+
x: e.x,
|
|
126
|
+
y: e.y,
|
|
127
|
+
pinned: e.pinned,
|
|
128
|
+
...(e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}),
|
|
129
|
+
}));
|
|
130
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
131
|
+
|
|
132
|
+
// Fourth pass: emit edges from each child's connectsTo, dropping ghosts.
|
|
133
|
+
const edges: DiagramEdge[] = [];
|
|
134
|
+
const seenEdgeIds = new Set<string>();
|
|
135
|
+
for (const child of children) {
|
|
136
|
+
if (!child.connectsTo) continue;
|
|
137
|
+
for (const conn of child.connectsTo) {
|
|
138
|
+
if (!nodeIds.has(conn.target)) {
|
|
139
|
+
warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const edgeId = makeEdgeId(child.id, conn);
|
|
143
|
+
if (seenEdgeIds.has(edgeId)) continue;
|
|
144
|
+
seenEdgeIds.add(edgeId);
|
|
145
|
+
edges.push({
|
|
146
|
+
id: edgeId,
|
|
147
|
+
source: child.id,
|
|
148
|
+
target: conn.target,
|
|
149
|
+
...(conn.type ? { type: conn.type } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { nodes, edges, warnings };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeEdgeId(source: string, conn: BlockConnection): string {
|
|
158
|
+
return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
|
|
159
|
+
}
|