@bendyline/squisq-react 2.0.1 → 2.2.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/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +6 -0
- package/dist/index.d.ts +101 -96
- package/dist/index.js +1483 -782
- package/dist/squisq-player.full.global.js +4165 -0
- package/dist/squisq-player.global.js +561 -168
- package/dist/standalone-source.d.ts +1 -0
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +64 -0
- package/package.json +8 -4
- package/dist/index.js.map +0 -1
- package/dist/squisq-player.css +0 -2
- package/dist/squisq-player.css.map +0 -1
- package/dist/squisq-player.global.js.map +0 -1
- package/src/BlockRenderer.tsx +0 -194
- package/src/CaptionOverlay.tsx +0 -111
- package/src/DocControlsBottom.tsx +0 -109
- package/src/DocControlsOverlay.tsx +0 -190
- package/src/DocControlsSidebar.tsx +0 -113
- package/src/DocControlsSlideshow.tsx +0 -360
- package/src/DocPlayer.tsx +0 -1528
- package/src/DocPlayerWithSidebar.tsx +0 -155
- package/src/DocProgressBar.tsx +0 -257
- package/src/InlineAudioPlayer.tsx +0 -46
- package/src/InlineVideoPlayer.tsx +0 -70
- package/src/LinearDocView.tsx +0 -569
- package/src/MarkdownRenderer.tsx +0 -624
- package/src/MediaClipLayer.tsx +0 -145
- package/src/SocialCaptionOverlay.tsx +0 -255
- package/src/__tests__/BlockRenderer.test.tsx +0 -235
- package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
- package/src/__tests__/DocPlayer.test.tsx +0 -635
- package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
- package/src/__tests__/DocProgressBar.test.tsx +0 -102
- package/src/__tests__/JsonView.test.tsx +0 -111
- package/src/__tests__/LinearDocView.test.tsx +0 -426
- package/src/__tests__/MapLayer.test.tsx +0 -63
- package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
- package/src/__tests__/MediaClipLayer.test.tsx +0 -70
- package/src/__tests__/MediaContext.test.tsx +0 -51
- package/src/__tests__/PathLayer.test.tsx +0 -84
- package/src/__tests__/TableLayer.test.tsx +0 -142
- package/src/__tests__/VideoLayer.test.tsx +0 -94
- package/src/__tests__/exports.test.ts +0 -55
- package/src/__tests__/fillStyle.test.tsx +0 -160
- package/src/__tests__/standaloneEntry.test.tsx +0 -103
- package/src/__tests__/transitionStyles.test.ts +0 -125
- package/src/__tests__/useAudioSync.test.ts +0 -49
- package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
- package/src/__tests__/useJsonViewTokens.test.ts +0 -41
- package/src/__tests__/useSlideSwipe.test.ts +0 -81
- package/src/__tests__/useViewportOrientation.test.ts +0 -22
- package/src/hooks/AudioController.ts +0 -114
- package/src/hooks/MediaContext.tsx +0 -97
- package/src/hooks/index.ts +0 -11
- package/src/hooks/useAudioSync.ts +0 -456
- package/src/hooks/useAutoSurface.ts +0 -33
- package/src/hooks/useDocPlayback.ts +0 -265
- package/src/hooks/useMediaSchedule.ts +0 -39
- package/src/hooks/useSlideSwipe.ts +0 -265
- package/src/hooks/useViewportOrientation.ts +0 -115
- package/src/index.ts +0 -70
- package/src/jsonView/JsonView.tsx +0 -51
- package/src/jsonView/RenderNode.tsx +0 -51
- package/src/jsonView/index.ts +0 -2
- package/src/jsonView/json-view.css +0 -206
- package/src/jsonView/useJsonViewTokens.ts +0 -32
- package/src/jsonView/viewers.tsx +0 -343
- package/src/layers/ImageLayer.tsx +0 -297
- package/src/layers/MapLayer.tsx +0 -185
- package/src/layers/PathLayer.tsx +0 -155
- package/src/layers/ShapeLayer.tsx +0 -158
- package/src/layers/TableLayer.tsx +0 -129
- package/src/layers/TextLayer.tsx +0 -571
- package/src/layers/TreeLayer.tsx +0 -167
- package/src/layers/VideoLayer.tsx +0 -171
- package/src/layers/index.ts +0 -6
- package/src/standalone-entry.tsx +0 -337
- package/src/standalone-source.d.ts +0 -10
- package/src/styles/doc-animations.css +0 -2431
- package/src/styles/index.css +0 -7
- package/src/types.ts +0 -183
- package/src/utils/animationUtils.ts +0 -13
- package/src/utils/fillStyle.tsx +0 -148
- package/src/utils/layerUtils.ts +0 -42
- package/src/utils/mapTileUtils.ts +0 -375
package/src/layers/MapLayer.tsx
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MapLayer Component
|
|
3
|
-
*
|
|
4
|
-
* Renders a geographic map layer within an SVG block. Maps are composed from
|
|
5
|
-
* static tile images fetched from free/open-source providers.
|
|
6
|
-
*
|
|
7
|
-
* For video export reliability, maps can use pre-rendered static images via
|
|
8
|
-
* the staticSrc property, avoiding tile loading race conditions during capture.
|
|
9
|
-
*
|
|
10
|
-
* Tile fetching: Tiles are loaded on mount and composited into a data URL
|
|
11
|
-
* for SVG embedding. This ensures correct rendering in both browser and
|
|
12
|
-
* Playwright screenshot contexts.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useId, useState, useEffect } from 'react';
|
|
16
|
-
import type { MapLayer as MapLayerType } from '@bendyline/squisq/schemas';
|
|
17
|
-
import { getAnimationStyle } from '../utils/animationUtils';
|
|
18
|
-
import { resolveValue, getAnchorOffset } from '../utils/layerUtils';
|
|
19
|
-
import { composeMapImage } from '../utils/mapTileUtils';
|
|
20
|
-
|
|
21
|
-
interface MapLayerProps {
|
|
22
|
-
layer: MapLayerType;
|
|
23
|
-
/** Base path for resolving relative image URLs */
|
|
24
|
-
basePath: string;
|
|
25
|
-
/** Viewport dimensions for percentage calculations */
|
|
26
|
-
viewport: { width: number; height: number };
|
|
27
|
-
/** Current time relative to block start (for animation timing) */
|
|
28
|
-
blockTime: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function MapLayer({ layer, basePath, viewport, blockTime }: MapLayerProps) {
|
|
32
|
-
const { content, position, animation } = layer;
|
|
33
|
-
const clipId = `map-clip-${useId().replace(/:/g, '')}-${layer.id}`;
|
|
34
|
-
const [mapImageUrl, setMapImageUrl] = useState<string | null>(null);
|
|
35
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
36
|
-
const [error, setError] = useState<string | null>(null);
|
|
37
|
-
|
|
38
|
-
// Resolve position values to pixels
|
|
39
|
-
const x = resolveValue(position.x, viewport.width);
|
|
40
|
-
const y = resolveValue(position.y, viewport.height);
|
|
41
|
-
const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
|
|
42
|
-
const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
|
|
43
|
-
|
|
44
|
-
// Apply anchor offset
|
|
45
|
-
const offset = getAnchorOffset(position.anchor, width, height);
|
|
46
|
-
const finalX = x + offset.x;
|
|
47
|
-
const finalY = y + offset.y;
|
|
48
|
-
|
|
49
|
-
// Use static image if provided, otherwise fetch and compose tiles
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
let cancelled = false;
|
|
52
|
-
|
|
53
|
-
if (content.staticSrc) {
|
|
54
|
-
// Use pre-rendered static image
|
|
55
|
-
const src = content.staticSrc.startsWith('http')
|
|
56
|
-
? content.staticSrc
|
|
57
|
-
: `${basePath}/${content.staticSrc}`;
|
|
58
|
-
setMapImageUrl(src);
|
|
59
|
-
setIsLoading(false);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Compose map from tiles
|
|
64
|
-
setIsLoading(true);
|
|
65
|
-
setError(null);
|
|
66
|
-
|
|
67
|
-
composeMapImage({
|
|
68
|
-
center: content.center,
|
|
69
|
-
zoom: content.zoom,
|
|
70
|
-
style: content.style,
|
|
71
|
-
width,
|
|
72
|
-
height,
|
|
73
|
-
markers: content.markers,
|
|
74
|
-
showAttribution: content.showAttribution !== false,
|
|
75
|
-
})
|
|
76
|
-
.then((dataUrl) => {
|
|
77
|
-
if (!cancelled) {
|
|
78
|
-
setMapImageUrl(dataUrl);
|
|
79
|
-
setIsLoading(false);
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
.catch((err: unknown) => {
|
|
83
|
-
if (!cancelled) {
|
|
84
|
-
console.error('Failed to compose map:', err);
|
|
85
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
86
|
-
setIsLoading(false);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return () => {
|
|
91
|
-
cancelled = true;
|
|
92
|
-
};
|
|
93
|
-
}, [
|
|
94
|
-
content.center,
|
|
95
|
-
content.zoom,
|
|
96
|
-
content.style,
|
|
97
|
-
content.staticSrc,
|
|
98
|
-
content.markers,
|
|
99
|
-
content.showAttribution,
|
|
100
|
-
width,
|
|
101
|
-
height,
|
|
102
|
-
basePath,
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
// Get animation styles
|
|
106
|
-
const animStyle = getAnimationStyle(animation, blockTime);
|
|
107
|
-
|
|
108
|
-
// Render loading state
|
|
109
|
-
if (isLoading) {
|
|
110
|
-
return (
|
|
111
|
-
<g
|
|
112
|
-
className={`block-layer block-layer--map ${animStyle.className}`}
|
|
113
|
-
style={animStyle.style}
|
|
114
|
-
data-layer-id={layer.id}
|
|
115
|
-
>
|
|
116
|
-
<rect x={finalX} y={finalY} width={width} height={height} fill="#e5e7eb" />
|
|
117
|
-
<text
|
|
118
|
-
x={finalX + width / 2}
|
|
119
|
-
y={finalY + height / 2}
|
|
120
|
-
textAnchor="middle"
|
|
121
|
-
dominantBaseline="middle"
|
|
122
|
-
fill="#9ca3af"
|
|
123
|
-
fontSize="24"
|
|
124
|
-
fontFamily="system-ui, sans-serif"
|
|
125
|
-
>
|
|
126
|
-
Loading map...
|
|
127
|
-
</text>
|
|
128
|
-
</g>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Render error state
|
|
133
|
-
if (error || !mapImageUrl) {
|
|
134
|
-
return (
|
|
135
|
-
<g
|
|
136
|
-
className={`block-layer block-layer--map ${animStyle.className}`}
|
|
137
|
-
style={animStyle.style}
|
|
138
|
-
data-layer-id={layer.id}
|
|
139
|
-
>
|
|
140
|
-
<rect x={finalX} y={finalY} width={width} height={height} fill="#fef2f2" />
|
|
141
|
-
<text
|
|
142
|
-
x={finalX + width / 2}
|
|
143
|
-
y={finalY + height / 2}
|
|
144
|
-
textAnchor="middle"
|
|
145
|
-
dominantBaseline="middle"
|
|
146
|
-
fill="#dc2626"
|
|
147
|
-
fontSize="18"
|
|
148
|
-
fontFamily="system-ui, sans-serif"
|
|
149
|
-
>
|
|
150
|
-
Map failed to load
|
|
151
|
-
</text>
|
|
152
|
-
</g>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<g
|
|
158
|
-
className={`block-layer block-layer--map ${animStyle.className}`}
|
|
159
|
-
style={animStyle.style}
|
|
160
|
-
data-layer-id={layer.id}
|
|
161
|
-
>
|
|
162
|
-
{/* Clip path for overflow handling */}
|
|
163
|
-
<defs>
|
|
164
|
-
<clipPath id={clipId}>
|
|
165
|
-
<rect x={finalX} y={finalY} width={width} height={height} />
|
|
166
|
-
</clipPath>
|
|
167
|
-
</defs>
|
|
168
|
-
|
|
169
|
-
{/* Map image */}
|
|
170
|
-
<g clipPath={`url(#${clipId})`}>
|
|
171
|
-
<image
|
|
172
|
-
href={mapImageUrl}
|
|
173
|
-
x={finalX}
|
|
174
|
-
y={finalY}
|
|
175
|
-
width={width}
|
|
176
|
-
height={height}
|
|
177
|
-
preserveAspectRatio="xMidYMid slice"
|
|
178
|
-
style={{ pointerEvents: 'none' }}
|
|
179
|
-
/>
|
|
180
|
-
</g>
|
|
181
|
-
</g>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export default MapLayer;
|
package/src/layers/PathLayer.tsx
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PathLayer Component
|
|
3
|
-
*
|
|
4
|
-
* Renders an SVG `<path>` for arbitrary curves, connectors, arrows, and the
|
|
5
|
-
* drawing template's computed shapes. Used by the diagram template for edges
|
|
6
|
-
* between nodes; usable by any template that needs a non-rect/circle/line
|
|
7
|
-
* shape.
|
|
8
|
-
*
|
|
9
|
-
* The path's `d` attribute uses absolute SVG coordinates relative to the
|
|
10
|
-
* block viewport (independent of the layer's `position` box, which is
|
|
11
|
-
* present only so animations and clipping match the other layer types).
|
|
12
|
-
*
|
|
13
|
-
* Exception: when `content.shapeKind` is set (a standard named shape like
|
|
14
|
-
* `diamond` / `star` / `arrow-right`), `d` is re-derived from the layer's
|
|
15
|
-
* `position` box resolved against the viewport. That keeps named shapes
|
|
16
|
-
* movable, resizable, and aspect-ratio-adaptive — matching how the native
|
|
17
|
-
* rect/circle/line `ShapeLayer` behaves — rather than pinned to a baked
|
|
18
|
-
* absolute path.
|
|
19
|
-
*
|
|
20
|
-
* End markers are configured via `startMarker`/`endMarker`. Marker geometry
|
|
21
|
-
* comes from `markerPath` in core so the SSR renderer and the editor agree.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { useId } from 'react';
|
|
25
|
-
import type { PathLayer as PathLayerType, MarkerStyle } from '@bendyline/squisq/schemas';
|
|
26
|
-
import { markerPath, shapePath } from '@bendyline/squisq/doc';
|
|
27
|
-
import { getAnimationStyle } from '../utils/animationUtils';
|
|
28
|
-
import { resolveValue, getAnchorOffset } from '../utils/layerUtils';
|
|
29
|
-
import { resolveFill, borderDashArray } from '../utils/fillStyle';
|
|
30
|
-
|
|
31
|
-
interface PathLayerProps {
|
|
32
|
-
layer: PathLayerType;
|
|
33
|
-
/** Viewport dimensions — used to resolve `%` positions for named shapes. */
|
|
34
|
-
viewport: { width: number; height: number };
|
|
35
|
-
/** Current time relative to block start. */
|
|
36
|
-
blockTime: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The effective `d`: a named shape (`content.shapeKind`) is re-derived
|
|
41
|
-
* from the resolved `position` box so it tracks moves/resizes and adapts
|
|
42
|
-
* to the viewport; everything else uses the stored absolute path.
|
|
43
|
-
*/
|
|
44
|
-
function effectivePath(layer: PathLayerType, viewport: { width: number; height: number }): string {
|
|
45
|
-
const { content, position } = layer;
|
|
46
|
-
if (!content.shapeKind) return content.d;
|
|
47
|
-
const w = position.width ? resolveValue(position.width, viewport.width) : 0;
|
|
48
|
-
const h = position.height ? resolveValue(position.height, viewport.height) : 0;
|
|
49
|
-
const rawX = resolveValue(position.x, viewport.width);
|
|
50
|
-
const rawY = resolveValue(position.y, viewport.height);
|
|
51
|
-
const anchor = getAnchorOffset(position.anchor, w, h);
|
|
52
|
-
const derived = shapePath(content.shapeKind, rawX + anchor.x, rawY + anchor.y, w, h);
|
|
53
|
-
// Fall back to the stored path if the kind is unknown to `shapePath`.
|
|
54
|
-
return derived ?? content.d;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type LegacyArrow = 'none' | 'end' | 'start' | 'both';
|
|
58
|
-
|
|
59
|
-
/** Tolerant-reader support for documents authored before endpoint markers. */
|
|
60
|
-
function readLegacyArrow(content: PathLayerType['content']): LegacyArrow | undefined {
|
|
61
|
-
return (content as PathLayerType['content'] & { arrow?: LegacyArrow }).arrow;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Resolve the effective marker for an endpoint. */
|
|
65
|
-
function effectiveMarker(
|
|
66
|
-
explicit: MarkerStyle | undefined,
|
|
67
|
-
legacyArrow: LegacyArrow | undefined,
|
|
68
|
-
end: 'start' | 'end',
|
|
69
|
-
): MarkerStyle {
|
|
70
|
-
if (explicit) return explicit;
|
|
71
|
-
const wants = legacyArrow === 'both' || legacyArrow === end;
|
|
72
|
-
return wants ? 'arrow' : 'none';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function PathLayer({ layer, viewport, blockTime }: PathLayerProps) {
|
|
76
|
-
const { content, animation, id } = layer;
|
|
77
|
-
const defsId = `${useId().replace(/:/g, '')}-${id}`;
|
|
78
|
-
const d = effectivePath(layer, viewport);
|
|
79
|
-
const stroke = content.stroke ?? '#1e293b';
|
|
80
|
-
const strokeWidth = content.strokeWidth ?? 2;
|
|
81
|
-
const { fill, def: fillDef } = resolveFill(defsId, content.fill ?? 'none', content.gradient);
|
|
82
|
-
// `borderStyle` (named shapes) takes precedence over a raw `dasharray`.
|
|
83
|
-
const dash = content.borderStyle
|
|
84
|
-
? borderDashArray(content.borderStyle, strokeWidth)
|
|
85
|
-
: content.dasharray;
|
|
86
|
-
const animStyle = getAnimationStyle(animation, blockTime);
|
|
87
|
-
|
|
88
|
-
const startId = `marker-start-${defsId}`;
|
|
89
|
-
const endId = `marker-end-${defsId}`;
|
|
90
|
-
const legacyArrow = readLegacyArrow(content);
|
|
91
|
-
const start = markerPath(effectiveMarker(content.startMarker, legacyArrow, 'start'), 'start');
|
|
92
|
-
const end = markerPath(effectiveMarker(content.endMarker, legacyArrow, 'end'), 'end');
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<g
|
|
96
|
-
className={`block-layer block-layer--path ${animStyle.className}`}
|
|
97
|
-
style={animStyle.style}
|
|
98
|
-
data-layer-id={id}
|
|
99
|
-
>
|
|
100
|
-
<defs>
|
|
101
|
-
{fillDef}
|
|
102
|
-
{end && <MarkerDef id={endId} dir="end" d={end.d} filled={end.filled} stroke={stroke} />}
|
|
103
|
-
{start && (
|
|
104
|
-
<MarkerDef id={startId} dir="start" d={start.d} filled={start.filled} stroke={stroke} />
|
|
105
|
-
)}
|
|
106
|
-
</defs>
|
|
107
|
-
<path
|
|
108
|
-
d={d}
|
|
109
|
-
stroke={stroke}
|
|
110
|
-
strokeWidth={strokeWidth}
|
|
111
|
-
fill={fill}
|
|
112
|
-
fillOpacity={content.fillOpacity}
|
|
113
|
-
strokeDasharray={dash}
|
|
114
|
-
markerStart={start ? `url(#${startId})` : undefined}
|
|
115
|
-
markerEnd={end ? `url(#${endId})` : undefined}
|
|
116
|
-
/>
|
|
117
|
-
</g>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function MarkerDef({
|
|
122
|
-
id,
|
|
123
|
-
dir,
|
|
124
|
-
d,
|
|
125
|
-
filled,
|
|
126
|
-
stroke,
|
|
127
|
-
}: {
|
|
128
|
-
id: string;
|
|
129
|
-
dir: 'start' | 'end';
|
|
130
|
-
d: string;
|
|
131
|
-
filled: boolean;
|
|
132
|
-
stroke: string;
|
|
133
|
-
}) {
|
|
134
|
-
return (
|
|
135
|
-
<marker
|
|
136
|
-
id={id}
|
|
137
|
-
viewBox="0 0 10 10"
|
|
138
|
-
refX={dir === 'end' ? 9 : 1}
|
|
139
|
-
refY={5}
|
|
140
|
-
markerWidth={4}
|
|
141
|
-
markerHeight={4}
|
|
142
|
-
orient="auto-start-reverse"
|
|
143
|
-
markerUnits="strokeWidth"
|
|
144
|
-
>
|
|
145
|
-
<path
|
|
146
|
-
d={d}
|
|
147
|
-
fill={filled ? stroke : 'none'}
|
|
148
|
-
stroke={filled ? 'none' : stroke}
|
|
149
|
-
strokeWidth={filled ? undefined : 1.5}
|
|
150
|
-
/>
|
|
151
|
-
</marker>
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export default PathLayer;
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ShapeLayer Component
|
|
3
|
-
*
|
|
4
|
-
* Renders simple geometric shapes (rect, circle, line) within an SVG block.
|
|
5
|
-
* Useful for visual accents, dividers, and background elements.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useId } from 'react';
|
|
9
|
-
import type { ShapeLayer as ShapeLayerType } from '@bendyline/squisq/schemas';
|
|
10
|
-
import { getAnimationStyle } from '../utils/animationUtils';
|
|
11
|
-
import { resolveValue, getAnchorOffset } from '../utils/layerUtils';
|
|
12
|
-
import { resolveFill, resolveShapeFilter, borderDashArray } from '../utils/fillStyle';
|
|
13
|
-
|
|
14
|
-
interface ShapeLayerProps {
|
|
15
|
-
layer: ShapeLayerType;
|
|
16
|
-
/** Viewport dimensions for percentage calculations */
|
|
17
|
-
viewport: { width: number; height: number };
|
|
18
|
-
/** Current time relative to block start */
|
|
19
|
-
blockTime: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Full-bleed HTML and SVG layers can be rasterized on slightly different
|
|
24
|
-
* subpixel boundaries. Extend unbordered background rectangles beyond the
|
|
25
|
-
* viewport so an image cannot peek through at the outermost pixel; the block
|
|
26
|
-
* renderer clips the combined layer stack back to the viewBox.
|
|
27
|
-
*/
|
|
28
|
-
const FULL_BLEED_OVERSCAN = 1;
|
|
29
|
-
|
|
30
|
-
export function ShapeLayer({ layer, viewport, blockTime }: ShapeLayerProps) {
|
|
31
|
-
const { content, position, animation } = layer;
|
|
32
|
-
const defsId = `${useId().replace(/:/g, '')}-${layer.id}`;
|
|
33
|
-
|
|
34
|
-
// Resolve position values to pixels
|
|
35
|
-
const rawX = resolveValue(position.x, viewport.width);
|
|
36
|
-
const rawY = resolveValue(position.y, viewport.height);
|
|
37
|
-
const width = position.width ? resolveValue(position.width, viewport.width) : 100;
|
|
38
|
-
const height = position.height ? resolveValue(position.height, viewport.height) : 100;
|
|
39
|
-
|
|
40
|
-
// Apply anchor offset (e.g., 'center' shifts x by -width/2 and y by -height/2)
|
|
41
|
-
const anchorOffset = getAnchorOffset(position.anchor, width, height);
|
|
42
|
-
const x = rawX + anchorOffset.x;
|
|
43
|
-
const y = rawY + anchorOffset.y;
|
|
44
|
-
|
|
45
|
-
const isUnborderedFullBleedRect =
|
|
46
|
-
content.shape === 'rect' &&
|
|
47
|
-
x === 0 &&
|
|
48
|
-
y === 0 &&
|
|
49
|
-
width === viewport.width &&
|
|
50
|
-
height === viewport.height &&
|
|
51
|
-
!content.stroke &&
|
|
52
|
-
!content.borderRadius;
|
|
53
|
-
const overscan = isUnborderedFullBleedRect ? FULL_BLEED_OVERSCAN : 0;
|
|
54
|
-
const paintX = x - overscan;
|
|
55
|
-
const paintY = y - overscan;
|
|
56
|
-
const paintWidth = width + overscan * 2;
|
|
57
|
-
const paintHeight = height + overscan * 2;
|
|
58
|
-
|
|
59
|
-
// Get animation styles
|
|
60
|
-
const animStyle = getAnimationStyle(animation, blockTime);
|
|
61
|
-
|
|
62
|
-
const fill = content.fill || 'none';
|
|
63
|
-
// Legacy: a CSS gradient string baked into `fill` (e.g. from older docs)
|
|
64
|
-
// only works as an HTML background, so rect renders it via foreignObject.
|
|
65
|
-
// The structured `content.gradient` (preferred) is handled below for all
|
|
66
|
-
// shapes via an SVG <linearGradient>.
|
|
67
|
-
const isCSSGradient = typeof fill === 'string' && fill.includes('gradient(');
|
|
68
|
-
|
|
69
|
-
if (content.shape === 'rect' && isCSSGradient && !content.gradient) {
|
|
70
|
-
return (
|
|
71
|
-
<g
|
|
72
|
-
className={`block-layer block-layer--shape ${animStyle.className}`}
|
|
73
|
-
style={animStyle.style}
|
|
74
|
-
data-layer-id={layer.id}
|
|
75
|
-
>
|
|
76
|
-
<foreignObject x={paintX} y={paintY} width={paintWidth} height={paintHeight}>
|
|
77
|
-
<div
|
|
78
|
-
style={{
|
|
79
|
-
width: `${paintWidth}px`,
|
|
80
|
-
height: `${paintHeight}px`,
|
|
81
|
-
background: fill,
|
|
82
|
-
borderRadius: content.borderRadius ? `${content.borderRadius}px` : undefined,
|
|
83
|
-
pointerEvents: 'none',
|
|
84
|
-
}}
|
|
85
|
-
/>
|
|
86
|
-
</foreignObject>
|
|
87
|
-
</g>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { fill: fillValue, def: fillDef } = resolveFill(
|
|
92
|
-
defsId,
|
|
93
|
-
fill,
|
|
94
|
-
content.gradient,
|
|
95
|
-
content.pattern,
|
|
96
|
-
);
|
|
97
|
-
const { filterAttr, def: filterDef } = resolveShapeFilter(defsId, content.filter);
|
|
98
|
-
const dash = borderDashArray(content.borderStyle, content.strokeWidth);
|
|
99
|
-
|
|
100
|
-
// Common style props for native SVG shapes. `line` is stroke-only.
|
|
101
|
-
const shapeProps = {
|
|
102
|
-
fill: fillValue,
|
|
103
|
-
fillOpacity: content.fillOpacity,
|
|
104
|
-
stroke: content.stroke,
|
|
105
|
-
strokeWidth: content.strokeWidth,
|
|
106
|
-
strokeDasharray: dash,
|
|
107
|
-
...(filterAttr ? { filter: filterAttr } : {}),
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<g
|
|
112
|
-
className={`block-layer block-layer--shape ${animStyle.className}`}
|
|
113
|
-
style={animStyle.style}
|
|
114
|
-
data-layer-id={layer.id}
|
|
115
|
-
>
|
|
116
|
-
{(fillDef || filterDef) && (
|
|
117
|
-
<defs>
|
|
118
|
-
{fillDef}
|
|
119
|
-
{filterDef}
|
|
120
|
-
</defs>
|
|
121
|
-
)}
|
|
122
|
-
{content.shape === 'rect' && (
|
|
123
|
-
<rect
|
|
124
|
-
x={paintX}
|
|
125
|
-
y={paintY}
|
|
126
|
-
width={paintWidth}
|
|
127
|
-
height={paintHeight}
|
|
128
|
-
rx={content.borderRadius}
|
|
129
|
-
ry={content.borderRadius}
|
|
130
|
-
{...shapeProps}
|
|
131
|
-
/>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{content.shape === 'circle' && (
|
|
135
|
-
<circle
|
|
136
|
-
cx={x + width / 2}
|
|
137
|
-
cy={y + height / 2}
|
|
138
|
-
r={Math.min(width, height) / 2}
|
|
139
|
-
{...shapeProps}
|
|
140
|
-
/>
|
|
141
|
-
)}
|
|
142
|
-
|
|
143
|
-
{content.shape === 'line' && (
|
|
144
|
-
<line
|
|
145
|
-
x1={x}
|
|
146
|
-
y1={y}
|
|
147
|
-
x2={x + width}
|
|
148
|
-
y2={y + height}
|
|
149
|
-
stroke={content.stroke || '#ffffff'}
|
|
150
|
-
strokeWidth={content.strokeWidth || 2}
|
|
151
|
-
strokeDasharray={dash}
|
|
152
|
-
/>
|
|
153
|
-
)}
|
|
154
|
-
</g>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export default ShapeLayer;
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TableLayer Component
|
|
3
|
-
*
|
|
4
|
-
* Renders a data table within an SVG block using a <foreignObject> to embed
|
|
5
|
-
* an HTML table. This gives us native table layout inside SVG viewports.
|
|
6
|
-
*
|
|
7
|
-
* The table is styled inline using the TableLayerStyle properties, which are
|
|
8
|
-
* typically derived from the active theme by the dataTable template.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { TableLayer as TableLayerType } from '@bendyline/squisq/schemas';
|
|
12
|
-
import { resolveValue, getAnchorOffset } from '../utils/layerUtils';
|
|
13
|
-
import { getAnimationStyle } from '../utils/animationUtils';
|
|
14
|
-
|
|
15
|
-
interface TableLayerProps {
|
|
16
|
-
layer: TableLayerType;
|
|
17
|
-
/** Viewport dimensions for percentage calculations */
|
|
18
|
-
viewport: { width: number; height: number };
|
|
19
|
-
/** Current time relative to block start (for animation) */
|
|
20
|
-
blockTime: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function TableLayer({ layer, viewport, blockTime }: TableLayerProps) {
|
|
24
|
-
const { content, position, animation } = layer;
|
|
25
|
-
const { headers, rows, align, style } = content;
|
|
26
|
-
|
|
27
|
-
// Resolve position values to pixels
|
|
28
|
-
const x = resolveValue(position.x, viewport.width);
|
|
29
|
-
const y = resolveValue(position.y, viewport.height);
|
|
30
|
-
const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
|
|
31
|
-
const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
|
|
32
|
-
|
|
33
|
-
// Apply anchor offset
|
|
34
|
-
const offset = getAnchorOffset(position.anchor, width, height);
|
|
35
|
-
const finalX = x + offset.x;
|
|
36
|
-
const finalY = y + offset.y;
|
|
37
|
-
|
|
38
|
-
// Build animation style
|
|
39
|
-
const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
|
|
40
|
-
|
|
41
|
-
const cellAlign = (ci: number): React.CSSProperties | undefined => {
|
|
42
|
-
const a = align?.[ci];
|
|
43
|
-
return a ? { textAlign: a } : undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const borderRadius = style.borderRadius ?? 8;
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<foreignObject x={finalX} y={finalY} width={width} height={height} style={animStyle}>
|
|
50
|
-
<div
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
-
{...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}
|
|
53
|
-
style={{
|
|
54
|
-
width: `${width}px`,
|
|
55
|
-
height: `${height}px`,
|
|
56
|
-
display: 'flex',
|
|
57
|
-
alignItems: 'center',
|
|
58
|
-
justifyContent: 'center',
|
|
59
|
-
padding: '16px',
|
|
60
|
-
boxSizing: 'border-box',
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
<table
|
|
64
|
-
style={{
|
|
65
|
-
width: '100%',
|
|
66
|
-
borderCollapse: 'separate',
|
|
67
|
-
borderSpacing: 0,
|
|
68
|
-
fontSize: `${style.fontSize}px`,
|
|
69
|
-
fontFamily: style.fontFamily ?? 'system-ui, sans-serif',
|
|
70
|
-
overflow: 'hidden',
|
|
71
|
-
borderRadius: `${borderRadius}px`,
|
|
72
|
-
border: `1px solid ${style.borderColor}`,
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{headers.length > 0 && (
|
|
76
|
-
<thead>
|
|
77
|
-
<tr>
|
|
78
|
-
{headers.map((header, ci) => (
|
|
79
|
-
<th
|
|
80
|
-
key={ci}
|
|
81
|
-
style={{
|
|
82
|
-
background: style.headerBackground,
|
|
83
|
-
color: style.headerColor,
|
|
84
|
-
fontFamily:
|
|
85
|
-
style.headerFontFamily ?? style.fontFamily ?? 'system-ui, sans-serif',
|
|
86
|
-
fontWeight: 600,
|
|
87
|
-
padding: '12px 16px',
|
|
88
|
-
borderBottom: `2px solid ${style.borderColor}`,
|
|
89
|
-
borderRight:
|
|
90
|
-
ci < headers.length - 1 ? `1px solid ${style.borderColor}` : undefined,
|
|
91
|
-
...cellAlign(ci),
|
|
92
|
-
}}
|
|
93
|
-
>
|
|
94
|
-
{header}
|
|
95
|
-
</th>
|
|
96
|
-
))}
|
|
97
|
-
</tr>
|
|
98
|
-
</thead>
|
|
99
|
-
)}
|
|
100
|
-
{rows.length > 0 && (
|
|
101
|
-
<tbody>
|
|
102
|
-
{rows.map((row, ri) => (
|
|
103
|
-
<tr key={ri}>
|
|
104
|
-
{row.map((cell, ci) => (
|
|
105
|
-
<td
|
|
106
|
-
key={ci}
|
|
107
|
-
style={{
|
|
108
|
-
background: style.cellBackground,
|
|
109
|
-
color: style.cellColor,
|
|
110
|
-
padding: '10px 16px',
|
|
111
|
-
borderBottom:
|
|
112
|
-
ri < rows.length - 1 ? `1px solid ${style.borderColor}` : undefined,
|
|
113
|
-
borderRight:
|
|
114
|
-
ci < row.length - 1 ? `1px solid ${style.borderColor}` : undefined,
|
|
115
|
-
...cellAlign(ci),
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
{cell}
|
|
119
|
-
</td>
|
|
120
|
-
))}
|
|
121
|
-
</tr>
|
|
122
|
-
))}
|
|
123
|
-
</tbody>
|
|
124
|
-
)}
|
|
125
|
-
</table>
|
|
126
|
-
</div>
|
|
127
|
-
</foreignObject>
|
|
128
|
-
);
|
|
129
|
-
}
|