@bendyline/squisq 1.5.1 → 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.
- package/dist/{chunk-LH7I6SH7.js → chunk-5TYS5V3G.js} +91 -43
- package/dist/chunk-5TYS5V3G.js.map +1 -0
- package/dist/{chunk-TEEEILMP.js → chunk-BEQODJWV.js} +9 -2
- package/dist/chunk-BEQODJWV.js.map +1 -0
- package/dist/{chunk-6ATE2PSM.js → chunk-GPLTCSXO.js} +2 -2
- package/dist/{chunk-RUDYOTA6.js → chunk-LTN6I7QW.js} +56 -1
- package/dist/chunk-LTN6I7QW.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-P3K7NLHF.js} +2 -2
- package/dist/doc/index.d.ts +1 -1
- package/dist/doc/index.js +10 -4
- package/dist/imageEdit/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -6
- package/dist/markdown/index.js +2 -2
- package/dist/story/index.d.ts +27 -3
- package/dist/story/index.js +10 -4
- package/package.json +1 -1
- package/src/__tests__/coverBlock.test.ts +33 -2
- package/src/__tests__/markdownToDoc.test.ts +5 -2
- package/src/__tests__/shapeGeometry.test.ts +37 -0
- package/src/doc/docToMarkdown.ts +56 -37
- package/src/doc/templates/coverBlock.ts +49 -13
- package/src/doc/templates/diagramBlock.ts +2 -2
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/utils/shapeGeometry.ts +90 -2
- package/src/markdown/convert.ts +11 -1
- package/dist/chunk-LH7I6SH7.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/dist/chunk-TEEEILMP.js.map +0 -1
- /package/dist/{chunk-6ATE2PSM.js.map → chunk-GPLTCSXO.js.map} +0 -0
- /package/dist/{chunk-2OIBZYKP.js.map → chunk-P3K7NLHF.js.map} +0 -0
package/dist/story/index.d.ts
CHANGED
|
@@ -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)` + `
|
|
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;
|
|
@@ -1774,4 +1798,4 @@ declare function coerceTemplateParams(template: string | undefined, params: Reco
|
|
|
1774
1798
|
*/
|
|
1775
1799
|
declare function lintTemplateParams(template: string, params: Record<string, string>): TemplateParamFinding[];
|
|
1776
1800
|
|
|
1777
|
-
export { type AccentLayout, type AudioSegmentTiming, BASE_INPUT_DESCRIPTORS, 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 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, normalizeShapeKind, parseDataFence, parseYamlSubset, photoGrid, pullQuote, quoteBlock, readCustomTemplatesFromFrontmatter, readCustomThemesFromFrontmatter, replaceDataFence, 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 };
|
|
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 };
|
package/dist/story/index.js
CHANGED
|
@@ -105,7 +105,7 @@ import {
|
|
|
105
105
|
wrapWithPersistentLayers,
|
|
106
106
|
writeCustomTemplatesToFrontmatter,
|
|
107
107
|
writeCustomThemesToFrontmatter
|
|
108
|
-
} from "../chunk-
|
|
108
|
+
} from "../chunk-5TYS5V3G.js";
|
|
109
109
|
import "../chunk-UY7KGQ5R.js";
|
|
110
110
|
import "../chunk-V543AI2F.js";
|
|
111
111
|
import "../chunk-FVTQSNR7.js";
|
|
@@ -116,8 +116,11 @@ import {
|
|
|
116
116
|
connectorPath,
|
|
117
117
|
lineStyleDasharray,
|
|
118
118
|
markerPath,
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
nearestSnapPoint,
|
|
120
|
+
shapePath,
|
|
121
|
+
snapEndpoints,
|
|
122
|
+
snapPoints
|
|
123
|
+
} from "../chunk-LTN6I7QW.js";
|
|
121
124
|
import {
|
|
122
125
|
DEFAULT_THEME,
|
|
123
126
|
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
@@ -136,7 +139,7 @@ import {
|
|
|
136
139
|
isTemplateBlock,
|
|
137
140
|
scaledFontSize2 as scaledFontSize
|
|
138
141
|
} from "../chunk-DLXZMS5K.js";
|
|
139
|
-
import "../chunk-
|
|
142
|
+
import "../chunk-BEQODJWV.js";
|
|
140
143
|
import "../chunk-HWVFJAAH.js";
|
|
141
144
|
import "../chunk-DBS6RIFL.js";
|
|
142
145
|
import "../chunk-QE7TWGVT.js";
|
|
@@ -230,6 +233,7 @@ export {
|
|
|
230
233
|
mapBlock,
|
|
231
234
|
markdownToDoc,
|
|
232
235
|
markerPath,
|
|
236
|
+
nearestSnapPoint,
|
|
233
237
|
normalizeShapeKind,
|
|
234
238
|
parseDataFence,
|
|
235
239
|
parseYamlSubset,
|
|
@@ -252,6 +256,8 @@ export {
|
|
|
252
256
|
sectionHeader,
|
|
253
257
|
shapePath,
|
|
254
258
|
shouldUseShadow,
|
|
259
|
+
snapEndpoints,
|
|
260
|
+
snapPoints,
|
|
255
261
|
statHighlight,
|
|
256
262
|
templateRegistry,
|
|
257
263
|
themeWantsAmbientMotion,
|
package/package.json
CHANGED
|
@@ -85,6 +85,7 @@ describe('coverBlock with hero image', () => {
|
|
|
85
85
|
it('does not produce theme background layers', () => {
|
|
86
86
|
const layers = coverBlock(input, landscapeContext);
|
|
87
87
|
expect(findLayer(layers, 'cover-bg')).toBeUndefined();
|
|
88
|
+
expect(findLayer(layers, 'cover-bg-tint')).toBeUndefined();
|
|
88
89
|
expect(findLayer(layers, 'cover-accent')).toBeUndefined();
|
|
89
90
|
});
|
|
90
91
|
|
|
@@ -110,12 +111,16 @@ describe('coverBlock without hero image', () => {
|
|
|
110
111
|
subtitle: 'Subtitle text',
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
it('produces theme background instead of hero', () => {
|
|
114
|
+
it('produces theme background and soft tint instead of hero', () => {
|
|
114
115
|
const layers = coverBlock(input, landscapeContext);
|
|
115
116
|
const bg = findShape(layers, 'cover-bg');
|
|
117
|
+
const tint = findShape(layers, 'cover-bg-tint');
|
|
116
118
|
expect(bg).toBeDefined();
|
|
117
119
|
expect(bg!.type).toBe('shape');
|
|
118
|
-
expect(bg!.content.fill).
|
|
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)');
|
|
119
124
|
});
|
|
120
125
|
|
|
121
126
|
it('produces decorative accent line', () => {
|
|
@@ -144,6 +149,31 @@ describe('coverBlock without hero image', () => {
|
|
|
144
149
|
const subtitle = findLayer(layers, 'cover-subtitle');
|
|
145
150
|
expect(subtitle!.position.y).not.toBe('82%');
|
|
146
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
|
+
});
|
|
147
177
|
});
|
|
148
178
|
|
|
149
179
|
// ── coverBlock title-only (no subtitle) ─────────────────────────────
|
|
@@ -273,6 +303,7 @@ describe('expandCoverBlock', () => {
|
|
|
273
303
|
const config: StartBlockConfig = { title: 'No Hero' };
|
|
274
304
|
const layers = expandCoverBlock(config, landscapeContext);
|
|
275
305
|
expect(layers.some((l) => l.id === 'cover-bg')).toBe(true);
|
|
306
|
+
expect(layers.some((l) => l.id === 'cover-bg-tint')).toBe(true);
|
|
276
307
|
expect(layers.some((l) => l.id === 'cover-title')).toBe(true);
|
|
277
308
|
expect(layers.some((l) => l.id === 'cover-hero')).toBe(false);
|
|
278
309
|
});
|
|
@@ -394,14 +394,15 @@ describe('template annotation in markdownToDoc', () => {
|
|
|
394
394
|
expect(output).toContain('{[factCard style=minimal]}');
|
|
395
395
|
});
|
|
396
396
|
|
|
397
|
-
it('docToMarkdown injects transition
|
|
397
|
+
it('docToMarkdown injects transition params into the squiggly annotation', () => {
|
|
398
398
|
const md = parseMarkdown('## Section\n\nBody');
|
|
399
399
|
const doc = markdownToDoc(md);
|
|
400
400
|
doc.blocks[0].transition = { type: 'wipe', duration: 0.8, direction: 'left' };
|
|
401
401
|
|
|
402
402
|
const output = stringifyMarkdown(docToMarkdown(doc));
|
|
403
403
|
|
|
404
|
-
expect(output).toContain('{transition=wipe transitionDuration=0.8 transitionDirection=left}');
|
|
404
|
+
expect(output).toContain('{[transition=wipe transitionDuration=0.8 transitionDirection=left]}');
|
|
405
|
+
expect(output).not.toContain('{transition=wipe');
|
|
405
406
|
});
|
|
406
407
|
|
|
407
408
|
it('a programmatic transition survives the string round trip', () => {
|
|
@@ -445,6 +446,8 @@ describe('template annotation in markdownToDoc', () => {
|
|
|
445
446
|
|
|
446
447
|
const output = stringifyMarkdown(docToMarkdown(doc));
|
|
447
448
|
expect(output).toContain('transition=wipe');
|
|
449
|
+
expect(output).toContain('{[transition=wipe transitionDirection=left]}');
|
|
450
|
+
expect(output).not.toContain('{transition=wipe');
|
|
448
451
|
const reparsed = markdownToDoc(parseMarkdown(output));
|
|
449
452
|
expect(reparsed.blocks[0].transition).toEqual({ type: 'wipe', direction: 'left' });
|
|
450
453
|
});
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
markerPath,
|
|
5
5
|
connectorPath,
|
|
6
6
|
clipEndpoints,
|
|
7
|
+
nearestSnapPoint,
|
|
7
8
|
lineStyleDasharray,
|
|
8
9
|
PATH_SHAPE_KINDS,
|
|
10
|
+
snapEndpoints,
|
|
11
|
+
snapPoints,
|
|
9
12
|
} from '../doc/utils/shapeGeometry.js';
|
|
10
13
|
|
|
11
14
|
describe('shapePath', () => {
|
|
@@ -79,6 +82,40 @@ describe('clipEndpoints', () => {
|
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
|
|
85
|
+
describe('snapEndpoints', () => {
|
|
86
|
+
const a = { cx: 50, cy: 50, rx: 30, ry: 20 };
|
|
87
|
+
const b = { cx: 180, cy: 50, rx: 30, ry: 20 };
|
|
88
|
+
|
|
89
|
+
it('exposes side and corner ports around a box', () => {
|
|
90
|
+
expect(snapPoints(a).map((p) => p.port)).toEqual([
|
|
91
|
+
'top',
|
|
92
|
+
'right',
|
|
93
|
+
'bottom',
|
|
94
|
+
'left',
|
|
95
|
+
'top-left',
|
|
96
|
+
'top-right',
|
|
97
|
+
'bottom-right',
|
|
98
|
+
'bottom-left',
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('snaps horizontal connections to side ports', () => {
|
|
103
|
+
const r = snapEndpoints(a, b);
|
|
104
|
+
expect(r.start).toMatchObject({ port: 'right', x: 80, y: 50 });
|
|
105
|
+
expect(r.end).toMatchObject({ port: 'left', x: 150, y: 50 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('snaps diagonal connections to nearest corner ports', () => {
|
|
109
|
+
const r = snapEndpoints(a, { cx: 180, cy: 140, rx: 30, ry: 20 });
|
|
110
|
+
expect(r.start).toMatchObject({ port: 'bottom-right', x: 80, y: 70 });
|
|
111
|
+
expect(r.end).toMatchObject({ port: 'top-left', x: 150, y: 120 });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('finds the nearest port toward a free pointer', () => {
|
|
115
|
+
expect(nearestSnapPoint(a, { x: 100, y: 10 }).port).toBe('top-right');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
82
119
|
describe('lineStyleDasharray', () => {
|
|
83
120
|
it('maps line styles to dash patterns', () => {
|
|
84
121
|
expect(lineStyleDasharray('dashed')).toBe('8 6');
|
package/src/doc/docToMarkdown.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
writeCustomThemesToFrontmatter,
|
|
40
40
|
} from './customThemesFrontmatter.js';
|
|
41
41
|
|
|
42
|
+
const TRANSITION_PARAM_KEYS = ['transition', 'transitionDuration', 'transitionDirection'] as const;
|
|
43
|
+
|
|
42
44
|
/**
|
|
43
45
|
* Convert a Doc with heading-driven blocks back to a MarkdownDocument.
|
|
44
46
|
*
|
|
@@ -134,64 +136,81 @@ function synthesizeAnnotationParagraph(block: Block): MarkdownParagraph {
|
|
|
134
136
|
* template and templateOverrides. Returns a (possibly cloned) heading.
|
|
135
137
|
*/
|
|
136
138
|
function ensureAnnotation(block: Block, heading: MarkdownHeading): MarkdownHeading {
|
|
137
|
-
const
|
|
139
|
+
const { attributes, transitionParams } = ensureTransitionMetadata(block, heading);
|
|
138
140
|
|
|
139
141
|
// If the block has a non-default template or overrides, inject an annotation
|
|
140
142
|
const hasExplicitTemplate =
|
|
141
143
|
block.template && block.template !== 'sectionHeader' && block.autoTemplate !== true;
|
|
142
144
|
const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
let templateAnnotation = heading.templateAnnotation;
|
|
147
|
+
if (!templateAnnotation && (hasExplicitTemplate || hasOverrides || transitionParams)) {
|
|
148
|
+
templateAnnotation = {
|
|
149
|
+
...(hasExplicitTemplate ? { template: block.template ?? 'sectionHeader' } : {}),
|
|
150
|
+
...(hasOverrides ? { params: block.templateOverrides } : {}),
|
|
151
|
+
};
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
154
|
+
if (transitionParams) {
|
|
155
|
+
templateAnnotation = {
|
|
156
|
+
...(templateAnnotation ?? {}),
|
|
157
|
+
params: {
|
|
158
|
+
...(templateAnnotation?.params ?? {}),
|
|
159
|
+
...transitionParams,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
templateAnnotation: {
|
|
160
|
-
template: block.template ?? 'sectionHeader',
|
|
161
|
-
...(hasOverrides ? { params: block.templateOverrides } : {}),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
+
if (attributes === heading.attributes && templateAnnotation === heading.templateAnnotation) {
|
|
165
|
+
return heading;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { ...heading, children: [...heading.children], attributes, templateAnnotation };
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
function
|
|
171
|
+
function ensureTransitionMetadata(
|
|
167
172
|
block: Block,
|
|
168
173
|
heading: MarkdownHeading,
|
|
169
|
-
):
|
|
174
|
+
): {
|
|
175
|
+
attributes: MarkdownHeading['attributes'];
|
|
176
|
+
transitionParams: Record<string, string> | undefined;
|
|
177
|
+
} {
|
|
170
178
|
const transition = block.transition;
|
|
171
|
-
if (!transition)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// would lose or resurrect the wrong transition.
|
|
181
|
-
const attrs = heading.attributes ?? {};
|
|
182
|
-
return {
|
|
179
|
+
if (!transition) {
|
|
180
|
+
return { attributes: heading.attributes, transitionParams: undefined };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The transition is written to the squisq-native `{[…]}` params for
|
|
184
|
+
// author-facing markdown, and to `attributes.blockMeta` for the in-memory
|
|
185
|
+
// Doc → MarkdownDocument → Doc path (which does not re-parse string params).
|
|
186
|
+
const attrs = removeTransitionParams(heading.attributes) ?? {};
|
|
187
|
+
const attributes = {
|
|
183
188
|
...attrs,
|
|
184
|
-
|
|
185
|
-
...(attrs.
|
|
189
|
+
blockMeta: {
|
|
190
|
+
...(attrs.blockMeta ?? {}),
|
|
191
|
+
transition,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
attributes,
|
|
196
|
+
transitionParams: {
|
|
186
197
|
transition: transition.type,
|
|
187
198
|
...(transition.duration !== undefined
|
|
188
199
|
? { transitionDuration: String(transition.duration) }
|
|
189
200
|
: {}),
|
|
190
201
|
...(transition.direction ? { transitionDirection: transition.direction } : {}),
|
|
191
202
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function removeTransitionParams(
|
|
207
|
+
attrs: MarkdownHeading['attributes'],
|
|
208
|
+
): MarkdownHeading['attributes'] {
|
|
209
|
+
if (!attrs?.params) return attrs;
|
|
210
|
+
const params = { ...attrs.params };
|
|
211
|
+
for (const key of TRANSITION_PARAM_KEYS) delete params[key];
|
|
212
|
+
return {
|
|
213
|
+
...attrs,
|
|
214
|
+
...(Object.keys(params).length > 0 ? { params } : { params: undefined }),
|
|
196
215
|
};
|
|
197
216
|
}
|
|
@@ -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
|
},
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import type { Layer, ShapeLayer, TextLayer, PathLayer, MarkerStyle } from '../../schemas/Doc.js';
|
|
18
18
|
import type { DiagramBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
19
|
import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
|
|
20
|
-
import {
|
|
20
|
+
import { connectorPath, lineStyleDasharray, snapEndpoints } from '../utils/shapeGeometry.js';
|
|
21
21
|
import { computeDiagramLayout, type DiagramNodePosition } from './diagramLayout.js';
|
|
22
22
|
|
|
23
23
|
const NODE_WIDTH = 180;
|
|
@@ -124,7 +124,7 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
|
|
|
124
124
|
const a = positions.get(edge.source);
|
|
125
125
|
const b = positions.get(edge.target);
|
|
126
126
|
if (!a || !b) continue;
|
|
127
|
-
const { start, end } =
|
|
127
|
+
const { start, end } = snapEndpoints(a, b);
|
|
128
128
|
const pathLayer: PathLayer = {
|
|
129
129
|
type: 'path',
|
|
130
130
|
id: `edge-${edge.id}`,
|
|
@@ -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}`,
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the
|
|
10
10
|
* standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the
|
|
11
11
|
* editor's edge renderer).
|
|
12
|
-
* - `connectorPath(routing, start, end)` + `
|
|
12
|
+
* - `connectorPath(routing, start, end)` + `snapEndpoints(a, b)` → the line/edge
|
|
13
13
|
* geometry between two shapes (straight / orthogonal / curved), lifted from the
|
|
14
|
-
* diagram template so drawings and diagrams draw identical connectors.
|
|
14
|
+
* diagram template so drawings and diagrams draw identical connectors. The older
|
|
15
|
+
* `clipEndpoints(a, b)` helper remains for free-angle clipping, but rendered
|
|
16
|
+
* connectors use snapped ports so authored lines land on stable attachment points.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import type { MarkerStyle } from '../../schemas/Doc.js';
|
|
@@ -411,6 +413,22 @@ export interface ClipBox {
|
|
|
411
413
|
ry: number;
|
|
412
414
|
}
|
|
413
415
|
|
|
416
|
+
export type ConnectorPort =
|
|
417
|
+
| 'top'
|
|
418
|
+
| 'right'
|
|
419
|
+
| 'bottom'
|
|
420
|
+
| 'left'
|
|
421
|
+
| 'top-left'
|
|
422
|
+
| 'top-right'
|
|
423
|
+
| 'bottom-right'
|
|
424
|
+
| 'bottom-left';
|
|
425
|
+
|
|
426
|
+
export interface ConnectorSnapPoint {
|
|
427
|
+
port: ConnectorPort;
|
|
428
|
+
x: number;
|
|
429
|
+
y: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
414
432
|
/** Intersection of the center-to-center line with `from`'s bounding box. */
|
|
415
433
|
export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
|
|
416
434
|
const dx = to.cx - from.cx;
|
|
@@ -430,6 +448,76 @@ export function clipEndpoints(
|
|
|
430
448
|
return { start: clipPoint(a, b), end: clipPoint(b, a) };
|
|
431
449
|
}
|
|
432
450
|
|
|
451
|
+
/** The stable connector ports exposed around a shape/card box. */
|
|
452
|
+
export function snapPoints(box: ClipBox): ConnectorSnapPoint[] {
|
|
453
|
+
const left = box.cx - box.rx;
|
|
454
|
+
const right = box.cx + box.rx;
|
|
455
|
+
const top = box.cy - box.ry;
|
|
456
|
+
const bottom = box.cy + box.ry;
|
|
457
|
+
return [
|
|
458
|
+
{ port: 'top', x: box.cx, y: top },
|
|
459
|
+
{ port: 'right', x: right, y: box.cy },
|
|
460
|
+
{ port: 'bottom', x: box.cx, y: bottom },
|
|
461
|
+
{ port: 'left', x: left, y: box.cy },
|
|
462
|
+
{ port: 'top-left', x: left, y: top },
|
|
463
|
+
{ port: 'top-right', x: right, y: top },
|
|
464
|
+
{ port: 'bottom-right', x: right, y: bottom },
|
|
465
|
+
{ port: 'bottom-left', x: left, y: bottom },
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Nearest stable connector port on `box` to an arbitrary point. */
|
|
470
|
+
export function nearestSnapPoint(
|
|
471
|
+
box: ClipBox,
|
|
472
|
+
point: { x: number; y: number },
|
|
473
|
+
): ConnectorSnapPoint {
|
|
474
|
+
let best = snapPoints(box)[0];
|
|
475
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
476
|
+
for (const candidate of snapPoints(box)) {
|
|
477
|
+
const score = distanceSq(candidate, point) + portTieBreak(candidate.port);
|
|
478
|
+
if (score < bestScore) {
|
|
479
|
+
best = candidate;
|
|
480
|
+
bestScore = score;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return best;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Pick the pair of connector ports, one on each box, with the shortest
|
|
488
|
+
* distance between them. This gives stable attachments to side/corner handles
|
|
489
|
+
* instead of arbitrary center-ray intersections.
|
|
490
|
+
*/
|
|
491
|
+
export function snapEndpoints(
|
|
492
|
+
a: ClipBox,
|
|
493
|
+
b: ClipBox,
|
|
494
|
+
): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } {
|
|
495
|
+
let bestStart = snapPoints(a)[0];
|
|
496
|
+
let bestEnd = snapPoints(b)[0];
|
|
497
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
498
|
+
for (const start of snapPoints(a)) {
|
|
499
|
+
for (const end of snapPoints(b)) {
|
|
500
|
+
const score = distanceSq(start, end) + portTieBreak(start.port) + portTieBreak(end.port);
|
|
501
|
+
if (score < bestScore) {
|
|
502
|
+
bestStart = start;
|
|
503
|
+
bestEnd = end;
|
|
504
|
+
bestScore = score;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return { start: bestStart, end: bestEnd };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function distanceSq(a: { x: number; y: number }, b: { x: number; y: number }): number {
|
|
512
|
+
const dx = a.x - b.x;
|
|
513
|
+
const dy = a.y - b.y;
|
|
514
|
+
return dx * dx + dy * dy;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function portTieBreak(port: ConnectorPort): number {
|
|
518
|
+
return port.includes('-') ? 0.02 : 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
433
521
|
/** Build the SVG `d` for a connector between two clipped endpoints. */
|
|
434
522
|
export function connectorPath(
|
|
435
523
|
routing: ConnectorRouting,
|