@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,102 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { fireEvent, render, screen } from '@testing-library/react';
3
- import { DocProgressBar } from '../DocProgressBar';
4
- import type { PlaybackState, PlaybackActions } from '../types';
5
-
6
- function makeState(overrides: Partial<PlaybackState> = {}): PlaybackState {
7
- return {
8
- isPlaying: true,
9
- currentTime: 0,
10
- totalDuration: 60,
11
- currentBlockIndex: 0,
12
- totalBlocks: 3,
13
- docProgress: 0,
14
- hasCaptions: false,
15
- captionsEnabled: false,
16
- captionMode: 'off',
17
- currentSegmentIndex: 0,
18
- currentSegmentName: null,
19
- currentBlock: null,
20
- ...overrides,
21
- };
22
- }
23
-
24
- const actions: PlaybackActions = {
25
- toggle: () => {},
26
- restart: () => {},
27
- seekTo: () => {},
28
- setCaptionsEnabled: () => {},
29
- cycleCaptionMode: () => {},
30
- };
31
-
32
- /** The progress fill is the div tagged `doc-progress-fill`. */
33
- function fillWidth(container: HTMLElement): string {
34
- const fill = container.querySelector<HTMLElement>('[data-testid="doc-progress-fill"]');
35
- if (!fill) throw new Error('progress fill not found');
36
- return fill.style.width;
37
- }
38
-
39
- describe('DocProgressBar fill', () => {
40
- it('tracks elapsed time over totalDuration (the clock/marker timeline)', () => {
41
- const { container } = render(
42
- <DocProgressBar
43
- state={makeState({ currentTime: 12, totalDuration: 23, docProgress: 0.99 })}
44
- actions={actions}
45
- blockMarkers={[]}
46
- expandedBlocks={[]}
47
- />,
48
- );
49
- // Must reflect 12/23 ≈ 52%, NOT the divergent docProgress (0.99).
50
- expect(fillWidth(container)).toBe(`${(12 / 23) * 100}%`);
51
- });
52
-
53
- it('is 0% when totalDuration is 0 (no divide-by-zero)', () => {
54
- const { container } = render(
55
- <DocProgressBar
56
- state={makeState({ currentTime: 5, totalDuration: 0, docProgress: 0.5 })}
57
- actions={actions}
58
- blockMarkers={[]}
59
- expandedBlocks={[]}
60
- />,
61
- );
62
- expect(fillWidth(container)).toBe('0%');
63
- });
64
-
65
- it('clamps to 100% when currentTime exceeds totalDuration', () => {
66
- const { container } = render(
67
- <DocProgressBar
68
- state={makeState({ currentTime: 30, totalDuration: 23 })}
69
- actions={actions}
70
- blockMarkers={[]}
71
- expandedBlocks={[]}
72
- />,
73
- );
74
- expect(fillWidth(container)).toBe('100%');
75
- });
76
-
77
- it('exposes keyboard seeking and accessible marker buttons', () => {
78
- const seekTo = vi.fn();
79
- render(
80
- <DocProgressBar
81
- state={makeState({ currentTime: 12, totalDuration: 23 })}
82
- actions={{ ...actions, seekTo }}
83
- blockMarkers={[
84
- {
85
- block: { id: 'b', startTime: 8, duration: 5, audioSegment: 0, layers: [] },
86
- index: 0,
87
- position: 35,
88
- title: 'Chapter two',
89
- isSectionStart: false,
90
- },
91
- ]}
92
- expandedBlocks={[]}
93
- />,
94
- );
95
- const slider = screen.getByRole('slider', { name: 'Playback position' });
96
- expect(slider.getAttribute('aria-valuenow')).toBe('12');
97
- fireEvent.keyDown(slider, { key: 'ArrowRight' });
98
- expect(seekTo).toHaveBeenCalledWith(17);
99
- fireEvent.click(screen.getByRole('button', { name: 'Seek to Chapter two' }));
100
- expect(seekTo).toHaveBeenLastCalledWith(8);
101
- });
102
- });
@@ -1,111 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render } from '@testing-library/react';
3
- import { JsonView } from '../jsonView';
4
- import type { SquisqAnnotatedSchema } from '@bendyline/squisq/jsonForm';
5
-
6
- describe('JsonView', () => {
7
- it('renders an object schema as labeled rows', () => {
8
- const schema: SquisqAnnotatedSchema = {
9
- type: 'object',
10
- properties: {
11
- title: { type: 'string', title: 'Page Title' },
12
- active: { type: 'boolean' },
13
- },
14
- };
15
- const { container } = render(
16
- <JsonView schema={schema} value={{ title: 'Hello', active: true }} />,
17
- );
18
- expect(container.textContent).toContain('Page Title');
19
- expect(container.textContent).toContain('Hello');
20
- expect(container.textContent).toContain('On');
21
- });
22
-
23
- it('renders an array of primitives as chips', () => {
24
- const schema: SquisqAnnotatedSchema = {
25
- type: 'array',
26
- items: { type: 'string' },
27
- };
28
- const { container } = render(<JsonView schema={schema} value={['a', 'b', 'c']} />);
29
- expect(container.querySelectorAll('.squisq-jv-chip')).toHaveLength(3);
30
- });
31
-
32
- it('renders an array of objects as cards with itemLabel.fromField', () => {
33
- const schema: SquisqAnnotatedSchema = {
34
- type: 'array',
35
- items: {
36
- type: 'object',
37
- properties: {
38
- heading: { type: 'string' },
39
- body: { type: 'string' },
40
- },
41
- squisq: { itemLabel: { fromField: 'heading' } },
42
- },
43
- };
44
- const { container } = render(
45
- <JsonView
46
- schema={schema}
47
- value={[
48
- { heading: 'Section A', body: 'Body A' },
49
- { heading: 'Section B', body: 'Body B' },
50
- ]}
51
- />,
52
- );
53
- const cards = container.querySelectorAll('.squisq-jv-card');
54
- expect(cards).toHaveLength(2);
55
- expect(cards[0].textContent).toContain('Section A');
56
- expect(cards[1].textContent).toContain('Section B');
57
- });
58
-
59
- it('renders a color value as a swatch + hex', () => {
60
- const schema: SquisqAnnotatedSchema = {
61
- type: 'string',
62
- format: 'color',
63
- };
64
- const { container } = render(<JsonView schema={schema} value="#ff0080" />);
65
- const swatch = container.querySelector('.squisq-jv-color__swatch') as HTMLElement | null;
66
- expect(swatch).not.toBeNull();
67
- expect(swatch?.style.background).toContain('rgb(255, 0, 128)');
68
- expect(container.textContent).toContain('#ff0080');
69
- });
70
-
71
- it('hides fields whose squisq.hidden rule matches', () => {
72
- const schema: SquisqAnnotatedSchema = {
73
- type: 'object',
74
- properties: {
75
- showAuthor: { type: 'boolean' },
76
- authorName: {
77
- type: 'string',
78
- squisq: { hidden: { field: 'showAuthor', truthy: false } },
79
- },
80
- },
81
- };
82
- const visible = render(
83
- <JsonView schema={schema} value={{ showAuthor: true, authorName: 'Alex' }} />,
84
- );
85
- expect(visible.container.textContent).toContain('Alex');
86
-
87
- const hidden = render(
88
- <JsonView schema={schema} value={{ showAuthor: false, authorName: 'Alex' }} />,
89
- );
90
- expect(hidden.container.textContent).not.toContain('Alex');
91
- });
92
-
93
- it('renders empty values as the em-dash placeholder', () => {
94
- const schema: SquisqAnnotatedSchema = {
95
- type: 'object',
96
- properties: { title: { type: 'string' } },
97
- };
98
- const { container } = render(<JsonView schema={schema} value={{}} />);
99
- expect(container.textContent).toContain('—');
100
- });
101
-
102
- it('respects squisq.enumLabels when displaying enum values', () => {
103
- const schema: SquisqAnnotatedSchema = {
104
- type: 'string',
105
- enum: ['s', 'm', 'l'],
106
- squisq: { enumLabels: { s: 'Small', m: 'Medium', l: 'Large' } },
107
- };
108
- const { container } = render(<JsonView schema={schema} value="m" />);
109
- expect(container.textContent).toContain('Medium');
110
- });
111
- });
@@ -1,426 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { fireEvent, render } from '@testing-library/react';
3
- import { LinearDocView } from '../LinearDocView';
4
- import type { Doc, Block } from '@bendyline/squisq/schemas';
5
- import { DARK_SURFACE, DEFAULT_THEME, LIGHT_SURFACE } from '@bendyline/squisq/schemas';
6
- import type {
7
- MarkdownBlockNode,
8
- MarkdownInlineNode,
9
- MarkdownHeading,
10
- } from '@bendyline/squisq/markdown';
11
-
12
- // ── Helpers ────────────────────────────────────────────────────────
13
-
14
- function text(value: string): MarkdownInlineNode {
15
- return { type: 'text', value };
16
- }
17
-
18
- function paragraph(...children: MarkdownInlineNode[]): MarkdownBlockNode {
19
- return { type: 'paragraph', children };
20
- }
21
-
22
- function mkHeading(depth: 1 | 2 | 3, value: string): MarkdownHeading {
23
- return { type: 'heading', depth, children: [text(value)] };
24
- }
25
-
26
- function mkBlock(overrides: Partial<Block> = {}): Block {
27
- return {
28
- id: 'block-1',
29
- startTime: 0,
30
- duration: 3,
31
- audioSegment: 0,
32
- ...overrides,
33
- };
34
- }
35
-
36
- function mkDoc(blocks: Block[]): Doc {
37
- return {
38
- articleId: 'test-article',
39
- duration: 10,
40
- blocks,
41
- audio: { segments: [] },
42
- };
43
- }
44
-
45
- // ── Tests ──────────────────────────────────────────────────────────
46
-
47
- describe('LinearDocView', () => {
48
- it('renders a scrollable container', () => {
49
- const doc = mkDoc([
50
- mkBlock({
51
- id: 'preamble',
52
- contents: [paragraph(text('Introduction text'))],
53
- }),
54
- ]);
55
- const { container } = render(<LinearDocView doc={doc} />);
56
- const el = container.querySelector('.squisq-linear');
57
- expect(el).toBeTruthy();
58
- expect((el as HTMLElement).style.overflowY).toBe('auto');
59
- });
60
-
61
- it('globally scrolls with up and down arrows when enabled', () => {
62
- const doc = mkDoc([mkBlock({ contents: [paragraph(text('Scrollable body'))] })]);
63
- const { container } = render(<LinearDocView doc={doc} globalKeyboardShortcuts />);
64
- const scroller = container.querySelector<HTMLElement>('.squisq-linear')!;
65
- const scrollBy = vi.fn();
66
- Object.defineProperty(scroller, 'scrollBy', { configurable: true, value: scrollBy });
67
-
68
- fireEvent.keyDown(document, { key: 'ArrowDown' });
69
- fireEvent.keyDown(document, { key: 'ArrowUp' });
70
-
71
- expect(scrollBy).toHaveBeenNthCalledWith(1, { top: 64, behavior: 'smooth' });
72
- expect(scrollBy).toHaveBeenNthCalledWith(2, { top: -64, behavior: 'smooth' });
73
- });
74
-
75
- it('renders preamble content (no heading)', () => {
76
- const doc = mkDoc([
77
- mkBlock({
78
- id: 'preamble',
79
- contents: [paragraph(text('Preamble body'))],
80
- }),
81
- ]);
82
- const { container } = render(<LinearDocView doc={doc} />);
83
- // Should render paragraph but no heading
84
- expect(container.textContent).toContain('Preamble body');
85
- const headings = container.querySelectorAll('h1, h2, h3');
86
- expect(headings.length).toBe(0);
87
- });
88
-
89
- it('renders non-annotated block with heading + content', () => {
90
- const doc = mkDoc([
91
- mkBlock({
92
- id: 'section-1',
93
- sourceHeading: mkHeading(2, 'My Section'),
94
- contents: [paragraph(text('Section body text'))],
95
- }),
96
- ]);
97
- const { container } = render(<LinearDocView doc={doc} />);
98
- expect(container.querySelector('h2')?.textContent).toBe('My Section');
99
- expect(container.textContent).toContain('Section body text');
100
- });
101
-
102
- it('feeds imageWithCaption blocks the first body image as imageSrc', () => {
103
- // Regression: without auto-extracting imageSrc from contents, the
104
- // template renders a layer with `src=undefined` and the SVG card
105
- // shows a broken image in document (linear) mode.
106
- const doc = mkDoc([
107
- mkBlock({
108
- id: 'img-1',
109
- sourceHeading: {
110
- type: 'heading',
111
- depth: 1,
112
- children: [text('Mike Ammerlaan')],
113
- templateAnnotation: { template: 'imageWithCaption' },
114
- },
115
- contents: [paragraph({ type: 'image', url: 'mikehome_files/profile.png', alt: 'Mike' })],
116
- }),
117
- ]);
118
- const { container } = render(<LinearDocView doc={doc} />);
119
- const card = container.querySelector('.squisq-linear-card');
120
- expect(card).toBeTruthy();
121
- const img = card!.querySelector('image, img') as Element | null;
122
- expect(img).toBeTruthy();
123
- const href =
124
- img!.getAttribute('href') ?? img!.getAttribute('xlink:href') ?? img!.getAttribute('src');
125
- expect(href).toContain('mikehome_files/profile.png');
126
- });
127
-
128
- it('extracts imageSrc from raw HTML <img> (resized image)', () => {
129
- // The WYSIWYG editor emits `<img src width>` for resized images
130
- // because markdown shorthand has no width syntax. The linear view
131
- // must read that form too, or every resized imageWithCaption block
132
- // renders as a broken card.
133
- const doc = mkDoc([
134
- mkBlock({
135
- id: 'img-2',
136
- sourceHeading: {
137
- type: 'heading',
138
- depth: 1,
139
- children: [text('Resized')],
140
- templateAnnotation: { template: 'imageWithCaption' },
141
- },
142
- contents: [
143
- {
144
- type: 'htmlBlock',
145
- rawHtml: '<img alt="resized" src="resized.png" width="194">',
146
- htmlChildren: [
147
- {
148
- type: 'htmlElement',
149
- tagName: 'img',
150
- attributes: { src: 'resized.png', alt: 'resized', width: '194' },
151
- children: [],
152
- selfClosing: true,
153
- },
154
- ],
155
- } as unknown as MarkdownBlockNode,
156
- ],
157
- }),
158
- ]);
159
- const { container } = render(<LinearDocView doc={doc} />);
160
- const card = container.querySelector('.squisq-linear-card');
161
- expect(card).toBeTruthy();
162
- const img = card!.querySelector('image, img') as Element | null;
163
- const href =
164
- img?.getAttribute('href') ?? img?.getAttribute('xlink:href') ?? img?.getAttribute('src');
165
- expect(href).toContain('resized.png');
166
- });
167
-
168
- it('renders annotated block as SVG card', () => {
169
- const doc = mkDoc([
170
- mkBlock({
171
- id: 'annotated-1',
172
- template: 'sectionHeader',
173
- sourceHeading: {
174
- type: 'heading',
175
- depth: 2,
176
- children: [text('Visual Block')],
177
- templateAnnotation: {
178
- template: 'sectionHeader',
179
- },
180
- },
181
- contents: [paragraph(text('Body'))],
182
- }),
183
- ]);
184
- const { container } = render(<LinearDocView doc={doc} />);
185
- // Should have a card wrapper
186
- const card = container.querySelector('.squisq-linear-card');
187
- expect(card).toBeTruthy();
188
- // Should contain an SVG (from BlockRenderer)
189
- const svg = card?.querySelector('svg');
190
- expect(svg).toBeTruthy();
191
- });
192
-
193
- it('renders transform-generated template blocks without authoring nodes', () => {
194
- const doc = mkDoc([
195
- mkBlock({
196
- id: 'transform-stat',
197
- template: 'statHighlight',
198
- stat: '42%',
199
- description: 'Year-over-year growth',
200
- } as Partial<Block>),
201
- ]);
202
-
203
- const { container } = render(<LinearDocView doc={doc} />);
204
- const section = container.querySelector('[data-block-id="transform-stat"]');
205
- expect(section?.getAttribute('data-template')).toBe('statHighlight');
206
- expect(section?.querySelector('.squisq-linear-card svg')).toBeTruthy();
207
- expect(section?.textContent).toContain('42%');
208
- expect(section?.textContent).toContain('Year-over-year growth');
209
- });
210
-
211
- it('renders children recursively', () => {
212
- const doc = mkDoc([
213
- mkBlock({
214
- id: 'parent',
215
- sourceHeading: mkHeading(1, 'Parent'),
216
- contents: [paragraph(text('Parent body'))],
217
- children: [
218
- mkBlock({
219
- id: 'child',
220
- sourceHeading: mkHeading(2, 'Child'),
221
- contents: [paragraph(text('Child body'))],
222
- }),
223
- ],
224
- }),
225
- ]);
226
- const { container } = render(<LinearDocView doc={doc} />);
227
- expect(container.querySelector('h1')?.textContent).toBe('Parent');
228
- expect(container.querySelector('h2')?.textContent).toBe('Child');
229
- expect(container.textContent).toContain('Parent body');
230
- expect(container.textContent).toContain('Child body');
231
- });
232
-
233
- it('assigns unique pre-order indices across nested and top-level blocks', () => {
234
- const doc = mkDoc([
235
- mkBlock({
236
- id: 'parent',
237
- children: [mkBlock({ id: 'child' })],
238
- }),
239
- mkBlock({ id: 'sibling' }),
240
- ]);
241
- const { container } = render(<LinearDocView doc={doc} />);
242
- expect(
243
- Array.from(container.querySelectorAll('.squisq-linear-section')).map((node) =>
244
- node.getAttribute('data-block-index'),
245
- ),
246
- ).toEqual(['0', '1', '2']);
247
- });
248
-
249
- it('renders multiple top-level blocks', () => {
250
- const doc = mkDoc([
251
- mkBlock({
252
- id: 'b1',
253
- sourceHeading: mkHeading(1, 'First'),
254
- contents: [],
255
- }),
256
- mkBlock({
257
- id: 'b2',
258
- sourceHeading: mkHeading(1, 'Second'),
259
- contents: [],
260
- }),
261
- ]);
262
- const { container } = render(<LinearDocView doc={doc} />);
263
- const sections = container.querySelectorAll('.squisq-linear-section');
264
- expect(sections.length).toBe(2);
265
- });
266
-
267
- it('does not render SVG for non-annotated blocks', () => {
268
- const doc = mkDoc([
269
- mkBlock({
270
- id: 'plain',
271
- sourceHeading: mkHeading(2, 'Plain Section'),
272
- contents: [paragraph(text('Just text'))],
273
- }),
274
- ]);
275
- const { container } = render(<LinearDocView doc={doc} />);
276
- expect(container.querySelector('.squisq-linear-card')).toBeNull();
277
- expect(container.querySelector('svg')).toBeNull();
278
- });
279
-
280
- it('applies custom className', () => {
281
- const doc = mkDoc([mkBlock({ id: 'x', contents: [] })]);
282
- const { container } = render(<LinearDocView doc={doc} className="my-class" />);
283
- expect(container.querySelector('.squisq-linear.my-class')).toBeTruthy();
284
- });
285
-
286
- it('sets data-block-id on each section', () => {
287
- const doc = mkDoc([
288
- mkBlock({ id: 'alpha', contents: [] }),
289
- mkBlock({ id: 'beta', contents: [] }),
290
- ]);
291
- const { container } = render(<LinearDocView doc={doc} />);
292
- expect(container.querySelector('[data-block-id="alpha"]')).toBeTruthy();
293
- expect(container.querySelector('[data-block-id="beta"]')).toBeTruthy();
294
- });
295
-
296
- it('uses the theme background by default', () => {
297
- const doc = mkDoc([mkBlock({ id: 'b', contents: [paragraph(text('hi'))] })]);
298
- const { container } = render(<LinearDocView doc={doc} />);
299
- const el = container.querySelector('.squisq-linear') as HTMLElement;
300
- expect(el.style.background).toBeTruthy();
301
- // DEFAULT_THEME has a specific background; applying LIGHT_SURFACE below
302
- // must produce a different value to prove override is working.
303
- expect(el.style.background).not.toBe(LIGHT_SURFACE.background);
304
- });
305
-
306
- it('light surface overlays the theme background', () => {
307
- const doc = mkDoc([mkBlock({ id: 'b', contents: [paragraph(text('hi'))] })]);
308
- const { container } = render(
309
- <LinearDocView doc={doc} theme={DEFAULT_THEME} surface={LIGHT_SURFACE} />,
310
- );
311
- const el = container.querySelector('.squisq-linear') as HTMLElement;
312
- // React inline style background may round-trip as hex or rgb; compare the
313
- // colour-normalized value by mounting a plain div with the expected.
314
- const probe = document.createElement('div');
315
- probe.style.background = LIGHT_SURFACE.background;
316
- expect(el.style.background).toBe(probe.style.background);
317
- });
318
-
319
- it('dark surface overlays the theme background', () => {
320
- const doc = mkDoc([mkBlock({ id: 'b', contents: [paragraph(text('hi'))] })]);
321
- const { container } = render(
322
- <LinearDocView doc={doc} theme={DEFAULT_THEME} surface={DARK_SURFACE} />,
323
- );
324
- const el = container.querySelector('.squisq-linear') as HTMLElement;
325
- const probe = document.createElement('div');
326
- probe.style.background = DARK_SURFACE.background;
327
- expect(el.style.background).toBe(probe.style.background);
328
- });
329
- });
330
-
331
- describe('LinearDocView markdown prop', () => {
332
- it('renders from the markdown prop when no doc is given', () => {
333
- const { container } = render(<LinearDocView markdown={'# From Markdown\n\nParagraph body.'} />);
334
- expect(container.textContent).toContain('From Markdown');
335
- expect(container.textContent).toContain('Paragraph body.');
336
- });
337
-
338
- it('adds extra spacing between blank-line-separated paragraphs', () => {
339
- const { container } = render(
340
- <LinearDocView markdown={'First paragraph.\n\nSecond paragraph.'} />,
341
- );
342
- const paragraphs = container.querySelectorAll('.squisq-md-p');
343
- const styles = container.querySelector('.squisq-linear-content > style')?.textContent;
344
-
345
- expect(paragraphs).toHaveLength(2);
346
- expect(paragraphs[0]?.nextElementSibling).toBe(paragraphs[1]);
347
- expect(styles).toContain('.squisq-linear-content p + p');
348
- expect(styles).toContain('margin-top: 1.25em');
349
- });
350
-
351
- it('doc wins over markdown when both are provided', () => {
352
- const doc = mkDoc([mkBlock({ id: 'b', contents: [paragraph(text('Doc body wins'))] })]);
353
- const { container } = render(<LinearDocView doc={doc} markdown="# Markdown Loses" />);
354
- expect(container.textContent).toContain('Doc body wins');
355
- expect(container.textContent).not.toContain('Markdown Loses');
356
- });
357
-
358
- it('renders an empty container when neither doc nor markdown is given', () => {
359
- const { container } = render(<LinearDocView />);
360
- expect(container.querySelector('.squisq-linear--empty')).toBeTruthy();
361
- });
362
- });
363
-
364
- describe('LinearDocView unknown template annotations', () => {
365
- it('renders the canonical visible fallback without hidden console output', () => {
366
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
367
- const doc = mkDoc([
368
- mkBlock({
369
- id: 'unknown-1',
370
- sourceHeading: {
371
- type: 'heading',
372
- depth: 2,
373
- children: [text('Mystery Section')],
374
- templateAnnotation: { template: 'no-such-template-xyz' },
375
- },
376
- contents: [paragraph(text('Fallback body content'))],
377
- }),
378
- ]);
379
-
380
- const { container } = render(<LinearDocView doc={doc} />);
381
- expect(container.textContent).toContain('Mystery Section');
382
- expect(container.textContent).toContain('Fallback body content');
383
- expect(container.textContent).toContain('Unknown template "no-such-template-xyz"');
384
- expect(container.querySelector('.squisq-linear-card')).not.toBeNull();
385
- expect(warnSpy).not.toHaveBeenCalled();
386
- warnSpy.mockRestore();
387
- });
388
- });
389
-
390
- describe('LinearDocView custom template materialization', () => {
391
- it('renders document-scoped templates through the canonical API', () => {
392
- const doc: Doc = {
393
- ...mkDoc([
394
- mkBlock({
395
- id: 'custom-1',
396
- sourceHeading: {
397
- type: 'heading',
398
- depth: 2,
399
- children: [text('Custom Hero')],
400
- templateAnnotation: { template: 'hero' },
401
- },
402
- contents: [paragraph(text('Custom body'))],
403
- }),
404
- ]),
405
- customTemplates: [
406
- {
407
- name: 'hero',
408
- label: 'Hero',
409
- viewport: { width: 1920, height: 1080 },
410
- layers: [
411
- {
412
- id: 'hero-title',
413
- type: 'text',
414
- position: { x: '5%', y: '10%', width: '90%' },
415
- content: { text: '{title}: {content}', style: { fontSize: 48, color: '#000000' } },
416
- },
417
- ],
418
- },
419
- ],
420
- };
421
-
422
- const { container } = render(<LinearDocView doc={doc} />);
423
- expect(container.textContent).toContain('Custom Hero: Custom body');
424
- expect(container.textContent).not.toContain('Unknown template');
425
- });
426
- });