@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +6 -0
  4. package/dist/index.d.ts +101 -96
  5. package/dist/index.js +1483 -782
  6. package/dist/squisq-player.full.global.js +4165 -0
  7. package/dist/squisq-player.global.js +561 -168
  8. package/dist/standalone-source.d.ts +1 -0
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +64 -0
  11. package/package.json +8 -4
  12. package/dist/index.js.map +0 -1
  13. package/dist/squisq-player.css +0 -2
  14. package/dist/squisq-player.css.map +0 -1
  15. package/dist/squisq-player.global.js.map +0 -1
  16. package/src/BlockRenderer.tsx +0 -194
  17. package/src/CaptionOverlay.tsx +0 -111
  18. package/src/DocControlsBottom.tsx +0 -109
  19. package/src/DocControlsOverlay.tsx +0 -190
  20. package/src/DocControlsSidebar.tsx +0 -113
  21. package/src/DocControlsSlideshow.tsx +0 -360
  22. package/src/DocPlayer.tsx +0 -1528
  23. package/src/DocPlayerWithSidebar.tsx +0 -155
  24. package/src/DocProgressBar.tsx +0 -257
  25. package/src/InlineAudioPlayer.tsx +0 -46
  26. package/src/InlineVideoPlayer.tsx +0 -70
  27. package/src/LinearDocView.tsx +0 -569
  28. package/src/MarkdownRenderer.tsx +0 -624
  29. package/src/MediaClipLayer.tsx +0 -145
  30. package/src/SocialCaptionOverlay.tsx +0 -255
  31. package/src/__tests__/BlockRenderer.test.tsx +0 -235
  32. package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
  33. package/src/__tests__/DocPlayer.test.tsx +0 -635
  34. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
  35. package/src/__tests__/DocProgressBar.test.tsx +0 -102
  36. package/src/__tests__/JsonView.test.tsx +0 -111
  37. package/src/__tests__/LinearDocView.test.tsx +0 -426
  38. package/src/__tests__/MapLayer.test.tsx +0 -63
  39. package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
  40. package/src/__tests__/MediaClipLayer.test.tsx +0 -70
  41. package/src/__tests__/MediaContext.test.tsx +0 -51
  42. package/src/__tests__/PathLayer.test.tsx +0 -84
  43. package/src/__tests__/TableLayer.test.tsx +0 -142
  44. package/src/__tests__/VideoLayer.test.tsx +0 -94
  45. package/src/__tests__/exports.test.ts +0 -55
  46. package/src/__tests__/fillStyle.test.tsx +0 -160
  47. package/src/__tests__/standaloneEntry.test.tsx +0 -103
  48. package/src/__tests__/transitionStyles.test.ts +0 -125
  49. package/src/__tests__/useAudioSync.test.ts +0 -49
  50. package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
  51. package/src/__tests__/useJsonViewTokens.test.ts +0 -41
  52. package/src/__tests__/useSlideSwipe.test.ts +0 -81
  53. package/src/__tests__/useViewportOrientation.test.ts +0 -22
  54. package/src/hooks/AudioController.ts +0 -114
  55. package/src/hooks/MediaContext.tsx +0 -97
  56. package/src/hooks/index.ts +0 -11
  57. package/src/hooks/useAudioSync.ts +0 -456
  58. package/src/hooks/useAutoSurface.ts +0 -33
  59. package/src/hooks/useDocPlayback.ts +0 -265
  60. package/src/hooks/useMediaSchedule.ts +0 -39
  61. package/src/hooks/useSlideSwipe.ts +0 -265
  62. package/src/hooks/useViewportOrientation.ts +0 -115
  63. package/src/index.ts +0 -70
  64. package/src/jsonView/JsonView.tsx +0 -51
  65. package/src/jsonView/RenderNode.tsx +0 -51
  66. package/src/jsonView/index.ts +0 -2
  67. package/src/jsonView/json-view.css +0 -206
  68. package/src/jsonView/useJsonViewTokens.ts +0 -32
  69. package/src/jsonView/viewers.tsx +0 -343
  70. package/src/layers/ImageLayer.tsx +0 -297
  71. package/src/layers/MapLayer.tsx +0 -185
  72. package/src/layers/PathLayer.tsx +0 -155
  73. package/src/layers/ShapeLayer.tsx +0 -158
  74. package/src/layers/TableLayer.tsx +0 -129
  75. package/src/layers/TextLayer.tsx +0 -571
  76. package/src/layers/TreeLayer.tsx +0 -167
  77. package/src/layers/VideoLayer.tsx +0 -171
  78. package/src/layers/index.ts +0 -6
  79. package/src/standalone-entry.tsx +0 -337
  80. package/src/standalone-source.d.ts +0 -10
  81. package/src/styles/doc-animations.css +0 -2431
  82. package/src/styles/index.css +0 -7
  83. package/src/types.ts +0 -183
  84. package/src/utils/animationUtils.ts +0 -13
  85. package/src/utils/fillStyle.tsx +0 -148
  86. package/src/utils/layerUtils.ts +0 -42
  87. package/src/utils/mapTileUtils.ts +0 -375
@@ -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;
@@ -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
- }