@bendyline/squisq-react 1.0.3 → 1.1.1
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/BlockRenderer.d.ts.map +1 -1
- package/dist/BlockRenderer.js +3 -0
- package/dist/BlockRenderer.js.map +1 -1
- package/dist/LinearDocView.d.ts.map +1 -1
- package/dist/LinearDocView.js +43 -1
- package/dist/LinearDocView.js.map +1 -1
- package/dist/__tests__/TableLayer.test.d.ts +2 -0
- package/dist/__tests__/TableLayer.test.d.ts.map +1 -0
- package/dist/__tests__/TableLayer.test.js +121 -0
- package/dist/__tests__/TableLayer.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/TableLayer.d.ts +23 -0
- package/dist/layers/TableLayer.d.ts.map +1 -0
- package/dist/layers/TableLayer.js +58 -0
- package/dist/layers/TableLayer.js.map +1 -0
- package/dist/squisq-player.global.js +27 -6
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/package.json +11 -11
- package/src/BlockRenderer.tsx +3 -0
- package/src/LinearDocView.tsx +47 -2
- package/src/__tests__/TableLayer.test.tsx +142 -0
- package/src/index.ts +1 -0
- package/src/layers/TableLayer.tsx +129 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "React component library for doc playback, block rendering, and media layers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@bendyline/squisq": "1.
|
|
55
|
+
"@bendyline/squisq": "1.2.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/react": "
|
|
59
|
-
"preact": "
|
|
60
|
-
"react": "
|
|
61
|
-
"react-dom": "
|
|
62
|
-
"@testing-library/react": "
|
|
63
|
-
"@testing-library/jest-dom": "
|
|
64
|
-
"jsdom": "
|
|
65
|
-
"tsup": "
|
|
66
|
-
"typescript": "
|
|
58
|
+
"@types/react": "18.3.28",
|
|
59
|
+
"preact": "10.29.0",
|
|
60
|
+
"react": "18.3.1",
|
|
61
|
+
"react-dom": "18.3.1",
|
|
62
|
+
"@testing-library/react": "16.3.2",
|
|
63
|
+
"@testing-library/jest-dom": "6.9.1",
|
|
64
|
+
"jsdom": "25.0.1",
|
|
65
|
+
"tsup": "8.5.1",
|
|
66
|
+
"typescript": "5.9.3"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/BlockRenderer.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { TextLayer } from './layers/TextLayer';
|
|
|
12
12
|
import { ShapeLayer } from './layers/ShapeLayer';
|
|
13
13
|
import { MapLayer } from './layers/MapLayer';
|
|
14
14
|
import { VideoLayer } from './layers/VideoLayer';
|
|
15
|
+
import { TableLayer } from './layers/TableLayer';
|
|
15
16
|
import { getTransitionClass } from './utils/animationUtils';
|
|
16
17
|
|
|
17
18
|
/** Default viewport dimensions (1080p landscape) - for backwards compatibility */
|
|
@@ -137,6 +138,8 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }: Laye
|
|
|
137
138
|
isPlaying={isPlaying}
|
|
138
139
|
/>
|
|
139
140
|
);
|
|
141
|
+
case 'table':
|
|
142
|
+
return <TableLayer layer={layer} viewport={viewport} blockTime={blockTime} />;
|
|
140
143
|
default:
|
|
141
144
|
console.warn(`Unknown layer type: ${(layer as Layer).type}`);
|
|
142
145
|
return null;
|
package/src/LinearDocView.tsx
CHANGED
|
@@ -25,7 +25,7 @@ import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
|
25
25
|
import { getLayers, hasTemplate, DEFAULT_THEME } from '@bendyline/squisq/doc';
|
|
26
26
|
import type { RenderContext } from '@bendyline/squisq/doc';
|
|
27
27
|
import { extractPlainText } from '@bendyline/squisq/markdown';
|
|
28
|
-
import type { MarkdownBlockNode, MarkdownList } from '@bendyline/squisq/markdown';
|
|
28
|
+
import type { MarkdownBlockNode, MarkdownList, MarkdownTable } from '@bendyline/squisq/markdown';
|
|
29
29
|
import { BlockRenderer } from './BlockRenderer';
|
|
30
30
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
31
31
|
|
|
@@ -216,6 +216,26 @@ function extractListItems(contents?: MarkdownBlockNode[]): string[] {
|
|
|
216
216
|
return items;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/** Extract table data (headers, rows, alignment) from block contents. */
|
|
220
|
+
function extractTableData(contents?: MarkdownBlockNode[]): {
|
|
221
|
+
headers: string[];
|
|
222
|
+
rows: string[][];
|
|
223
|
+
align?: (('left' | 'right' | 'center') | null)[];
|
|
224
|
+
} | null {
|
|
225
|
+
if (!contents) return null;
|
|
226
|
+
for (const node of contents) {
|
|
227
|
+
if (node.type === 'table') {
|
|
228
|
+
const table = node as MarkdownTable;
|
|
229
|
+
const [headerRow, ...bodyRows] = table.children;
|
|
230
|
+
if (!headerRow) return null;
|
|
231
|
+
const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
|
|
232
|
+
const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
|
|
233
|
+
return { headers, rows, align: table.align };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
219
239
|
/**
|
|
220
240
|
* Provide sensible default fields for templates that require more than
|
|
221
241
|
* just a `title`. Prevents crashes from undefined required fields.
|
|
@@ -243,6 +263,10 @@ function getTemplateDefaults(
|
|
|
243
263
|
return { term: headingText, definition: bodyText || headingText };
|
|
244
264
|
case 'dateEvent':
|
|
245
265
|
return { date: headingText, description: bodyText || headingText };
|
|
266
|
+
case 'dataTable': {
|
|
267
|
+
const tableData = extractTableData(contents);
|
|
268
|
+
return tableData ?? { headers: ['Column'], rows: [['Data']] };
|
|
269
|
+
}
|
|
246
270
|
default:
|
|
247
271
|
return {};
|
|
248
272
|
}
|
|
@@ -281,7 +305,7 @@ export function LinearDocView({
|
|
|
281
305
|
[activeViewport, totalBlocks, theme],
|
|
282
306
|
);
|
|
283
307
|
|
|
284
|
-
const activeTheme = theme
|
|
308
|
+
const activeTheme = renderContext.theme!;
|
|
285
309
|
const bgColor = activeTheme.colors.background;
|
|
286
310
|
const textColor = activeTheme.colors.text;
|
|
287
311
|
const mutedColor = activeTheme.colors.textMuted;
|
|
@@ -388,6 +412,27 @@ export function LinearDocView({
|
|
|
388
412
|
.squisq-linear-content em {
|
|
389
413
|
font-style: italic;
|
|
390
414
|
}
|
|
415
|
+
.squisq-linear-content table {
|
|
416
|
+
width: 100%;
|
|
417
|
+
border-collapse: collapse;
|
|
418
|
+
margin: 1em 0;
|
|
419
|
+
font-size: 0.95em;
|
|
420
|
+
}
|
|
421
|
+
.squisq-linear-content thead th {
|
|
422
|
+
background: var(--squisq-linear-primary);
|
|
423
|
+
color: var(--squisq-linear-bg);
|
|
424
|
+
font-family: var(--squisq-linear-title-font);
|
|
425
|
+
font-weight: 600;
|
|
426
|
+
padding: 10px 14px;
|
|
427
|
+
text-align: left;
|
|
428
|
+
}
|
|
429
|
+
.squisq-linear-content tbody td {
|
|
430
|
+
padding: 8px 14px;
|
|
431
|
+
border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
|
|
432
|
+
}
|
|
433
|
+
.squisq-linear-content tbody tr:hover {
|
|
434
|
+
background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
|
|
435
|
+
}
|
|
391
436
|
`}</style>
|
|
392
437
|
{doc.blocks.map((block, i) => (
|
|
393
438
|
<BlockSection
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { TableLayer } from '../layers/TableLayer';
|
|
4
|
+
import type { TableLayer as TableLayerType } from '@bendyline/squisq/schemas';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function makeTableLayer(overrides: Partial<TableLayerType> = {}): TableLayerType {
|
|
11
|
+
return {
|
|
12
|
+
type: 'table',
|
|
13
|
+
id: 'test-table',
|
|
14
|
+
content: {
|
|
15
|
+
headers: ['Name', 'Age', 'City'],
|
|
16
|
+
rows: [
|
|
17
|
+
['Alice', '30', 'Seattle'],
|
|
18
|
+
['Bob', '25', 'Portland'],
|
|
19
|
+
],
|
|
20
|
+
style: {
|
|
21
|
+
headerBackground: '#1e3a5f',
|
|
22
|
+
headerColor: '#ffffff',
|
|
23
|
+
cellBackground: 'rgba(255,255,255,0.05)',
|
|
24
|
+
cellColor: '#e0e0e0',
|
|
25
|
+
borderColor: 'rgba(255,255,255,0.12)',
|
|
26
|
+
fontSize: 28,
|
|
27
|
+
fontFamily: 'system-ui',
|
|
28
|
+
headerFontFamily: 'Georgia',
|
|
29
|
+
borderRadius: 8,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
position: { x: '10%', y: '10%', width: '80%', height: '80%' },
|
|
33
|
+
...overrides,
|
|
34
|
+
} as TableLayerType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const viewport = { width: 1920, height: 1080 };
|
|
38
|
+
|
|
39
|
+
function renderTableLayer(layer?: TableLayerType) {
|
|
40
|
+
return render(
|
|
41
|
+
<svg>
|
|
42
|
+
<TableLayer layer={layer ?? makeTableLayer()} viewport={viewport} blockTime={0} />
|
|
43
|
+
</svg>,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Tests
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
describe('TableLayer', () => {
|
|
52
|
+
it('renders a foreignObject element', () => {
|
|
53
|
+
const { container } = renderTableLayer();
|
|
54
|
+
const fo = container.querySelector('foreignObject');
|
|
55
|
+
expect(fo).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders a table element', () => {
|
|
59
|
+
const { container } = renderTableLayer();
|
|
60
|
+
const table = container.querySelector('table');
|
|
61
|
+
expect(table).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders the correct number of header cells', () => {
|
|
65
|
+
const { container } = renderTableLayer();
|
|
66
|
+
const headers = container.querySelectorAll('th');
|
|
67
|
+
expect(headers.length).toBe(3);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders header text content', () => {
|
|
71
|
+
const { container } = renderTableLayer();
|
|
72
|
+
const headers = container.querySelectorAll('th');
|
|
73
|
+
expect(headers[0].textContent).toBe('Name');
|
|
74
|
+
expect(headers[1].textContent).toBe('Age');
|
|
75
|
+
expect(headers[2].textContent).toBe('City');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders the correct number of data rows', () => {
|
|
79
|
+
const { container } = renderTableLayer();
|
|
80
|
+
const rows = container.querySelectorAll('tbody tr');
|
|
81
|
+
expect(rows.length).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders cell text content', () => {
|
|
85
|
+
const { container } = renderTableLayer();
|
|
86
|
+
const cells = container.querySelectorAll('td');
|
|
87
|
+
expect(cells[0].textContent).toBe('Alice');
|
|
88
|
+
expect(cells[1].textContent).toBe('30');
|
|
89
|
+
expect(cells[2].textContent).toBe('Seattle');
|
|
90
|
+
expect(cells[3].textContent).toBe('Bob');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('applies header background color', () => {
|
|
94
|
+
const { container } = renderTableLayer();
|
|
95
|
+
const th = container.querySelector('th');
|
|
96
|
+
expect(th?.style.background).toBe('rgb(30, 58, 95)');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('applies font size from style', () => {
|
|
100
|
+
const { container } = renderTableLayer();
|
|
101
|
+
const table = container.querySelector('table');
|
|
102
|
+
expect(table?.style.fontSize).toBe('28px');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('applies column alignment when provided', () => {
|
|
106
|
+
const layer = makeTableLayer();
|
|
107
|
+
layer.content.align = ['left', 'center', 'right'];
|
|
108
|
+
const { container } = renderTableLayer(layer);
|
|
109
|
+
|
|
110
|
+
const headers = container.querySelectorAll('th');
|
|
111
|
+
expect(headers[0].style.textAlign).toBe('left');
|
|
112
|
+
expect(headers[1].style.textAlign).toBe('center');
|
|
113
|
+
expect(headers[2].style.textAlign).toBe('right');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('handles empty headers gracefully', () => {
|
|
117
|
+
const layer = makeTableLayer();
|
|
118
|
+
layer.content.headers = [];
|
|
119
|
+
const { container } = renderTableLayer(layer);
|
|
120
|
+
const thead = container.querySelector('thead');
|
|
121
|
+
// No thead or empty thead
|
|
122
|
+
expect(thead).toBeFalsy();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('handles empty rows gracefully', () => {
|
|
126
|
+
const layer = makeTableLayer();
|
|
127
|
+
layer.content.rows = [];
|
|
128
|
+
const { container } = renderTableLayer(layer);
|
|
129
|
+
const tbody = container.querySelector('tbody');
|
|
130
|
+
// No tbody or empty tbody
|
|
131
|
+
expect(tbody).toBeFalsy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('sets foreignObject dimensions based on viewport percentage', () => {
|
|
135
|
+
const { container } = renderTableLayer();
|
|
136
|
+
const fo = container.querySelector('foreignObject');
|
|
137
|
+
// 80% of 1920 = 1536
|
|
138
|
+
expect(fo?.getAttribute('width')).toBe('1536');
|
|
139
|
+
// 80% of 1080 = 864
|
|
140
|
+
expect(fo?.getAttribute('height')).toBe('864');
|
|
141
|
+
});
|
|
142
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { ImageLayer } from './layers/ImageLayer.js';
|
|
|
17
17
|
export { TextLayer } from './layers/TextLayer.js';
|
|
18
18
|
export { ShapeLayer } from './layers/ShapeLayer.js';
|
|
19
19
|
export { VideoLayer } from './layers/VideoLayer.js';
|
|
20
|
+
export { TableLayer } from './layers/TableLayer.js';
|
|
20
21
|
export { MapLayer } from './layers/MapLayer.js';
|
|
21
22
|
|
|
22
23
|
// Hooks
|
|
@@ -0,0 +1,129 @@
|
|
|
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: '100%',
|
|
55
|
+
height: '100%',
|
|
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
|
+
}
|