@bendyline/squisq-react 1.4.2 → 2.0.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/README.md +30 -3
- package/dist/index.d.ts +177 -28
- package/dist/index.js +1330 -611
- package/dist/index.js.map +1 -1
- package/dist/squisq-player.css +1 -1
- package/dist/squisq-player.css.map +1 -1
- package/dist/squisq-player.global.js +57 -37
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +28 -0
- package/package.json +2 -2
- package/src/BlockRenderer.tsx +54 -17
- package/src/DocControlsSlideshow.tsx +222 -5
- package/src/DocPlayer.tsx +367 -183
- package/src/DocPlayerWithSidebar.tsx +4 -0
- package/src/DocProgressBar.tsx +40 -1
- package/src/LinearDocView.tsx +138 -62
- package/src/MarkdownRenderer.tsx +40 -97
- package/src/MediaClipLayer.tsx +12 -2
- package/src/__tests__/BlockRenderer.test.tsx +138 -8
- package/src/__tests__/DocControlsSlideshow.test.tsx +94 -1
- package/src/__tests__/DocPlayer.test.tsx +505 -0
- package/src/__tests__/DocProgressBar.test.tsx +28 -2
- package/src/__tests__/LinearDocView.test.tsx +104 -11
- package/src/__tests__/MapLayer.test.tsx +63 -0
- package/src/__tests__/MarkdownRenderer.test.tsx +16 -5
- package/src/__tests__/MediaClipLayer.test.tsx +70 -0
- package/src/__tests__/MediaContext.test.tsx +51 -0
- package/src/__tests__/PathLayer.test.tsx +12 -1
- package/src/__tests__/VideoLayer.test.tsx +94 -0
- package/src/__tests__/fillStyle.test.tsx +50 -2
- package/src/__tests__/standaloneEntry.test.tsx +103 -0
- package/src/__tests__/useAudioSync.test.ts +49 -0
- package/src/__tests__/useDocPlayback.transition.test.ts +48 -5
- package/src/__tests__/useViewportOrientation.test.ts +22 -0
- package/src/hooks/MediaContext.tsx +12 -3
- package/src/hooks/useAudioSync.ts +61 -12
- package/src/hooks/useDocPlayback.ts +40 -12
- package/src/hooks/useViewportOrientation.ts +2 -4
- package/src/index.ts +5 -2
- package/src/layers/ImageLayer.tsx +106 -1
- package/src/layers/MapLayer.tsx +7 -6
- package/src/layers/PathLayer.tsx +20 -11
- package/src/layers/ShapeLayer.tsx +33 -9
- package/src/layers/TextLayer.tsx +4 -3
- package/src/layers/TreeLayer.tsx +167 -0
- package/src/layers/VideoLayer.tsx +20 -6
- package/src/standalone-entry.tsx +91 -14
- package/src/styles/doc-animations.css +36 -0
- package/src/types.ts +13 -13
|
@@ -36,6 +36,8 @@ interface DocPlayerWithSidebarProps {
|
|
|
36
36
|
onTimeUpdate?: (time: number) => void;
|
|
37
37
|
/** Optional audio controller (if not provided, uses default HTML5 audio) */
|
|
38
38
|
audioController?: AudioController;
|
|
39
|
+
/** Whether to render slide transitions and per-layer animations (default: true). */
|
|
40
|
+
animationsEnabled?: boolean;
|
|
39
41
|
muted?: boolean;
|
|
40
42
|
captionsEnabled?: boolean;
|
|
41
43
|
isFullscreen?: boolean;
|
|
@@ -75,6 +77,7 @@ export function DocPlayerWithSidebar({
|
|
|
75
77
|
onEnded,
|
|
76
78
|
onTimeUpdate,
|
|
77
79
|
audioController,
|
|
80
|
+
animationsEnabled = true,
|
|
78
81
|
muted,
|
|
79
82
|
captionsEnabled,
|
|
80
83
|
isFullscreen,
|
|
@@ -132,6 +135,7 @@ export function DocPlayerWithSidebar({
|
|
|
132
135
|
onEnded={onEnded}
|
|
133
136
|
onTimeUpdate={onTimeUpdate}
|
|
134
137
|
audioController={audioController}
|
|
138
|
+
animationsEnabled={animationsEnabled}
|
|
135
139
|
muted={muted}
|
|
136
140
|
captionsEnabled={captionsEnabled}
|
|
137
141
|
showControls={isFullscreen}
|
package/src/DocProgressBar.tsx
CHANGED
|
@@ -63,6 +63,32 @@ export function DocProgressBar({
|
|
|
63
63
|
setHoverPosition(null);
|
|
64
64
|
}, []);
|
|
65
65
|
|
|
66
|
+
const handleProgressKeyDown = useCallback(
|
|
67
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
68
|
+
let next: number | null = null;
|
|
69
|
+
switch (e.key) {
|
|
70
|
+
case 'ArrowLeft':
|
|
71
|
+
case 'ArrowDown':
|
|
72
|
+
next = state.currentTime - 5;
|
|
73
|
+
break;
|
|
74
|
+
case 'ArrowRight':
|
|
75
|
+
case 'ArrowUp':
|
|
76
|
+
next = state.currentTime + 5;
|
|
77
|
+
break;
|
|
78
|
+
case 'Home':
|
|
79
|
+
next = 0;
|
|
80
|
+
break;
|
|
81
|
+
case 'End':
|
|
82
|
+
next = state.totalDuration;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (next == null) return;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
actions.seekTo(Math.max(0, Math.min(state.totalDuration, next)));
|
|
88
|
+
},
|
|
89
|
+
[actions, state.currentTime, state.totalDuration],
|
|
90
|
+
);
|
|
91
|
+
|
|
66
92
|
const getBlockAtTimeLocal = useCallback(
|
|
67
93
|
(time: number): { block: Block; index: number } | null => {
|
|
68
94
|
for (let i = expandedBlocks.length - 1; i >= 0; i--) {
|
|
@@ -79,6 +105,8 @@ export function DocProgressBar({
|
|
|
79
105
|
return (
|
|
80
106
|
<div
|
|
81
107
|
ref={progressBarRef}
|
|
108
|
+
role="group"
|
|
109
|
+
aria-label="Playback timeline"
|
|
82
110
|
style={{
|
|
83
111
|
flex: 1,
|
|
84
112
|
height: '24px',
|
|
@@ -98,6 +126,14 @@ export function DocProgressBar({
|
|
|
98
126
|
>
|
|
99
127
|
{/* Track background */}
|
|
100
128
|
<div
|
|
129
|
+
role="slider"
|
|
130
|
+
tabIndex={0}
|
|
131
|
+
aria-label="Playback position"
|
|
132
|
+
aria-valuemin={0}
|
|
133
|
+
aria-valuemax={state.totalDuration}
|
|
134
|
+
aria-valuenow={Math.max(0, Math.min(state.totalDuration, state.currentTime))}
|
|
135
|
+
aria-valuetext={`${formatTime(state.currentTime)} of ${formatTime(state.totalDuration)}`}
|
|
136
|
+
onKeyDown={handleProgressKeyDown}
|
|
101
137
|
style={{
|
|
102
138
|
position: 'absolute',
|
|
103
139
|
left: 0,
|
|
@@ -132,7 +168,8 @@ export function DocProgressBar({
|
|
|
132
168
|
|
|
133
169
|
{/* Block markers (dots) */}
|
|
134
170
|
{blockMarkers.map((marker, i) => (
|
|
135
|
-
<
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
136
173
|
key={`${marker.block.id}-${i}`}
|
|
137
174
|
style={{
|
|
138
175
|
position: 'absolute',
|
|
@@ -144,11 +181,13 @@ export function DocProgressBar({
|
|
|
144
181
|
background:
|
|
145
182
|
marker.index === state.currentBlockIndex ? '#ffffff' : 'rgba(255,255,255,0.5)',
|
|
146
183
|
border: '2px solid #5b9bd5',
|
|
184
|
+
padding: 0,
|
|
147
185
|
cursor: 'pointer',
|
|
148
186
|
zIndex: 2,
|
|
149
187
|
transition: 'transform 0.15s, background 0.15s',
|
|
150
188
|
}}
|
|
151
189
|
title={marker.title}
|
|
190
|
+
aria-label={`Seek to ${marker.title}`}
|
|
152
191
|
onClick={(e) => {
|
|
153
192
|
e.stopPropagation();
|
|
154
193
|
actions.seekTo(marker.block.startTime);
|
package/src/LinearDocView.tsx
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* - Headings from the block hierarchy rendered as HTML headings
|
|
14
14
|
* - Body content rendered via MarkdownRenderer
|
|
15
15
|
* - Template-annotated sections show an SVG card (BlockRenderer)
|
|
16
|
-
* using `
|
|
16
|
+
* using `materializeBlockLayers()` for on-demand layer computation
|
|
17
17
|
* - Blocks are rendered recursively to preserve the heading hierarchy
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { useMemo } from 'react';
|
|
20
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
21
21
|
import { useAutoSurface } from './hooks/useAutoSurface';
|
|
22
22
|
import type { Doc, Block, DocBlock } from '@bendyline/squisq/schemas';
|
|
23
23
|
import type { ViewportConfig } from '@bendyline/squisq/schemas';
|
|
@@ -29,13 +29,13 @@ import {
|
|
|
29
29
|
} from '@bendyline/squisq/schemas';
|
|
30
30
|
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
hasTemplate,
|
|
32
|
+
materializeBlockLayers,
|
|
34
33
|
markdownToDoc,
|
|
35
34
|
DEFAULT_THEME,
|
|
36
35
|
deriveTemplateInputs,
|
|
36
|
+
isTemplateBlock,
|
|
37
37
|
} from '@bendyline/squisq/doc';
|
|
38
|
-
import type {
|
|
38
|
+
import type { MaterializeBlockLayersOptions } from '@bendyline/squisq/doc';
|
|
39
39
|
import { extractPlainText, parseMarkdown } from '@bendyline/squisq/markdown';
|
|
40
40
|
import { BlockRenderer } from './BlockRenderer';
|
|
41
41
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
@@ -62,6 +62,8 @@ export interface LinearDocViewProps {
|
|
|
62
62
|
className?: string;
|
|
63
63
|
/** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
|
|
64
64
|
theme?: Theme;
|
|
65
|
+
/** Whether inline visual cards render their layer animations (default: true). */
|
|
66
|
+
animationsEnabled?: boolean;
|
|
65
67
|
/**
|
|
66
68
|
* Optional surface scheme (light / dark paper) overlaid on top of the
|
|
67
69
|
* theme's colors. Orthogonal to `theme` — a theme picks editorial
|
|
@@ -87,43 +89,35 @@ export interface LinearDocViewProps {
|
|
|
87
89
|
* full-size images would dominate the layout.
|
|
88
90
|
*/
|
|
89
91
|
imageDisplayMode?: ImageDisplayMode;
|
|
92
|
+
/**
|
|
93
|
+
* Let unmodified Up/Down arrows scroll this view even when it does not
|
|
94
|
+
* currently hold focus. Intended for a primary document preview.
|
|
95
|
+
*/
|
|
96
|
+
globalKeyboardShortcuts?: boolean;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export type ImageDisplayMode = 'inline' | 'thumbnail';
|
|
93
100
|
|
|
94
101
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
95
102
|
|
|
96
|
-
// Unknown template names we've already warned about (module-level so each
|
|
97
|
-
// name warns at most once per page, not once per render).
|
|
98
|
-
const warnedUnknownTemplates = new Set<string>();
|
|
99
|
-
|
|
100
103
|
/**
|
|
101
104
|
* Determine whether a block has a template annotation that should be
|
|
102
|
-
* rendered as a visual SVG card.
|
|
103
|
-
*
|
|
104
|
-
* 2. The annotated template exists in the registry
|
|
105
|
-
*
|
|
106
|
-
* Blocks annotated with a template that is NOT in the registry fall back
|
|
107
|
-
* to plain markdown rendering, with a one-shot dev-visible warning per
|
|
108
|
-
* unknown template name.
|
|
105
|
+
* rendered as a visual SVG card. Unknown templates remain annotated so the
|
|
106
|
+
* materializer can return a visible fallback and structured diagnostic.
|
|
109
107
|
*/
|
|
110
108
|
function isAnnotatedBlock(block: Block): boolean {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
return true;
|
|
109
|
+
return (
|
|
110
|
+
!!block.sourceHeading?.templateAnnotation?.template ||
|
|
111
|
+
(!block.sourceHeading && isTemplateBlock(block as DocBlock))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function visualTemplateName(block: Block): string | undefined {
|
|
116
|
+
return block.sourceHeading?.templateAnnotation?.template ?? block.template;
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
/**
|
|
126
|
-
* Count total blocks in a hierarchy
|
|
120
|
+
* Count total blocks in a hierarchy for the materialization context.
|
|
127
121
|
*/
|
|
128
122
|
function countAll(blocks: Block[]): number {
|
|
129
123
|
let count = 0;
|
|
@@ -140,55 +134,75 @@ interface BlockSectionProps {
|
|
|
140
134
|
block: Block;
|
|
141
135
|
basePath: string;
|
|
142
136
|
viewport: ViewportConfig;
|
|
143
|
-
renderContext:
|
|
137
|
+
renderContext: MaterializeBlockLayersOptions;
|
|
144
138
|
blockIndex: number;
|
|
139
|
+
blockIndices: ReadonlyMap<Block, number>;
|
|
140
|
+
animationsEnabled: boolean;
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
/**
|
|
148
144
|
* Render a single block section: heading + body content or SVG card.
|
|
149
145
|
* Recurses into children to render the full heading tree.
|
|
150
146
|
*/
|
|
151
|
-
function BlockSection({
|
|
147
|
+
function BlockSection({
|
|
148
|
+
block,
|
|
149
|
+
basePath,
|
|
150
|
+
viewport,
|
|
151
|
+
renderContext,
|
|
152
|
+
blockIndex,
|
|
153
|
+
blockIndices,
|
|
154
|
+
animationsEnabled,
|
|
155
|
+
}: BlockSectionProps) {
|
|
152
156
|
const isAnnotated = isAnnotatedBlock(block);
|
|
153
157
|
|
|
154
158
|
// For annotated blocks, compute layers and build a Block with them
|
|
155
159
|
const visualBlock = useMemo(() => {
|
|
156
160
|
if (!isAnnotated) return null;
|
|
157
161
|
|
|
158
|
-
const annotation = block.sourceHeading
|
|
159
|
-
const
|
|
162
|
+
const annotation = block.sourceHeading?.templateAnnotation;
|
|
163
|
+
const templateName = visualTemplateName(block) ?? 'sectionHeader';
|
|
160
164
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
// Authored Markdown blocks derive their typed template inputs from the
|
|
166
|
+
// heading/body. Transform-generated blocks already ARE typed template
|
|
167
|
+
// inputs, so materialize them directly instead of looking for authoring
|
|
168
|
+
// nodes they intentionally do not carry.
|
|
169
|
+
const templateBlock: Record<string, unknown> = annotation
|
|
170
|
+
? (() => {
|
|
171
|
+
const headingText = extractPlainText(block.sourceHeading!);
|
|
172
|
+
return {
|
|
173
|
+
id: block.id,
|
|
174
|
+
template: templateName,
|
|
175
|
+
startTime: 0,
|
|
176
|
+
duration: 1,
|
|
177
|
+
audioSegment: 0,
|
|
178
|
+
title: headingText,
|
|
179
|
+
contents: block.contents,
|
|
180
|
+
children: block.children,
|
|
181
|
+
...(deriveTemplateInputs(templateName, headingText, block.contents, {
|
|
182
|
+
placeholders: true,
|
|
183
|
+
}) ?? {}),
|
|
184
|
+
...annotation.params,
|
|
185
|
+
...block.templateOverrides,
|
|
186
|
+
};
|
|
187
|
+
})()
|
|
188
|
+
: {
|
|
189
|
+
...block,
|
|
190
|
+
startTime: block.startTime ?? 0,
|
|
191
|
+
duration: block.duration ?? 1,
|
|
192
|
+
audioSegment: block.audioSegment ?? 0,
|
|
193
|
+
template: templateName,
|
|
194
|
+
};
|
|
180
195
|
|
|
181
|
-
|
|
182
|
-
const ctx: RenderContext = {
|
|
196
|
+
const ctx: MaterializeBlockLayersOptions = {
|
|
183
197
|
...renderContext,
|
|
184
198
|
blockIndex,
|
|
185
199
|
};
|
|
186
|
-
const layers =
|
|
200
|
+
const { layers } = materializeBlockLayers(templateBlock as unknown as DocBlock, ctx);
|
|
187
201
|
|
|
188
202
|
return {
|
|
189
203
|
...block,
|
|
190
204
|
layers,
|
|
191
|
-
template:
|
|
205
|
+
template: templateName,
|
|
192
206
|
} as Block;
|
|
193
207
|
}, [block, isAnnotated, renderContext, blockIndex]);
|
|
194
208
|
|
|
@@ -196,7 +210,8 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
196
210
|
<div
|
|
197
211
|
className="squisq-linear-section"
|
|
198
212
|
data-block-id={block.id}
|
|
199
|
-
data-
|
|
213
|
+
data-block-index={blockIndex}
|
|
214
|
+
data-template={isAnnotated ? visualTemplateName(block) : undefined}
|
|
200
215
|
>
|
|
201
216
|
{/* Render the heading (if present — preamble has no sourceHeading) */}
|
|
202
217
|
{block.sourceHeading && !isAnnotated && <MarkdownRenderer nodes={[block.sourceHeading]} />}
|
|
@@ -224,6 +239,7 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
224
239
|
blockTime={0}
|
|
225
240
|
basePath={basePath}
|
|
226
241
|
viewport={viewport}
|
|
242
|
+
animationsEnabled={animationsEnabled}
|
|
227
243
|
/>
|
|
228
244
|
</div>
|
|
229
245
|
</div>
|
|
@@ -244,7 +260,9 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
|
|
|
244
260
|
basePath={basePath}
|
|
245
261
|
viewport={viewport}
|
|
246
262
|
renderContext={renderContext}
|
|
247
|
-
blockIndex={blockIndex + i + 1}
|
|
263
|
+
blockIndex={blockIndices.get(child) ?? blockIndex + i + 1}
|
|
264
|
+
blockIndices={blockIndices}
|
|
265
|
+
animationsEnabled={animationsEnabled}
|
|
248
266
|
/>
|
|
249
267
|
))}
|
|
250
268
|
</div>
|
|
@@ -275,9 +293,12 @@ export function LinearDocView({
|
|
|
275
293
|
className,
|
|
276
294
|
theme,
|
|
277
295
|
surface,
|
|
296
|
+
animationsEnabled = true,
|
|
278
297
|
thinMargins = false,
|
|
279
298
|
imageDisplayMode = 'inline',
|
|
299
|
+
globalKeyboardShortcuts = false,
|
|
280
300
|
}: LinearDocViewProps) {
|
|
301
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
281
302
|
const activeViewport = viewport ?? VIEWPORT_PRESETS.landscape;
|
|
282
303
|
|
|
283
304
|
// Parse markdown into a Doc only when no explicit doc is supplied.
|
|
@@ -291,10 +312,22 @@ export function LinearDocView({
|
|
|
291
312
|
() => (resolvedDoc ? countAll(resolvedDoc.blocks) : 0),
|
|
292
313
|
[resolvedDoc],
|
|
293
314
|
);
|
|
315
|
+
const blockIndices = useMemo(() => {
|
|
316
|
+
const indices = new Map<Block, number>();
|
|
317
|
+
let index = 0;
|
|
318
|
+
const visit = (blocks: Block[]) => {
|
|
319
|
+
for (const block of blocks) {
|
|
320
|
+
indices.set(block, index++);
|
|
321
|
+
if (block.children) visit(block.children);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
if (resolvedDoc) visit(resolvedDoc.blocks);
|
|
325
|
+
return indices;
|
|
326
|
+
}, [resolvedDoc]);
|
|
294
327
|
const autoSurface = useAutoSurface(surface === 'auto');
|
|
295
328
|
const resolvedSurface: SurfaceScheme | undefined = surface === 'auto' ? autoSurface : surface;
|
|
296
329
|
|
|
297
|
-
const renderContext:
|
|
330
|
+
const renderContext: MaterializeBlockLayersOptions = useMemo(() => {
|
|
298
331
|
const baseTheme = theme ?? DEFAULT_THEME;
|
|
299
332
|
const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
|
|
300
333
|
return {
|
|
@@ -304,15 +337,52 @@ export function LinearDocView({
|
|
|
304
337
|
// Theme atmosphere (vignette/grain/gradient persistent layers) shows
|
|
305
338
|
// on the inline template cards so they match the player's look.
|
|
306
339
|
persistentLayers: effectiveTheme.persistentLayers,
|
|
340
|
+
customTemplates: resolvedDoc?.customTemplates,
|
|
307
341
|
};
|
|
308
|
-
}, [activeViewport, totalBlocks, theme, resolvedSurface]);
|
|
342
|
+
}, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
|
|
309
343
|
|
|
310
344
|
const activeTheme = renderContext.theme!;
|
|
311
345
|
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!globalKeyboardShortcuts) return;
|
|
348
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
349
|
+
if (
|
|
350
|
+
event.defaultPrevented ||
|
|
351
|
+
event.altKey ||
|
|
352
|
+
event.ctrlKey ||
|
|
353
|
+
event.metaKey ||
|
|
354
|
+
event.shiftKey ||
|
|
355
|
+
(event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
356
|
+
) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
360
|
+
if (
|
|
361
|
+
target?.closest(
|
|
362
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor',
|
|
363
|
+
)
|
|
364
|
+
) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const scroller = scrollRef.current;
|
|
368
|
+
if (!scroller) return;
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
const distance = Math.max(64, Math.round(scroller.clientHeight * 0.12));
|
|
371
|
+
scroller.scrollBy({
|
|
372
|
+
top: event.key === 'ArrowDown' ? distance : -distance,
|
|
373
|
+
behavior: 'smooth',
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
377
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
378
|
+
}, [globalKeyboardShortcuts]);
|
|
379
|
+
|
|
312
380
|
// Nothing to render — keep an empty (but classed) container so hosts can
|
|
313
381
|
// still target/measure the view.
|
|
314
382
|
if (!resolvedDoc) {
|
|
315
|
-
return
|
|
383
|
+
return (
|
|
384
|
+
<div ref={scrollRef} className={`squisq-linear squisq-linear--empty ${className || ''}`} />
|
|
385
|
+
);
|
|
316
386
|
}
|
|
317
387
|
|
|
318
388
|
const bgColor = activeTheme.colors.background;
|
|
@@ -325,6 +395,7 @@ export function LinearDocView({
|
|
|
325
395
|
|
|
326
396
|
return (
|
|
327
397
|
<div
|
|
398
|
+
ref={scrollRef}
|
|
328
399
|
className={`squisq-linear ${className || ''}`}
|
|
329
400
|
style={{
|
|
330
401
|
width: '100%',
|
|
@@ -383,6 +454,9 @@ export function LinearDocView({
|
|
|
383
454
|
.squisq-linear-content p {
|
|
384
455
|
margin-bottom: 0.75em;
|
|
385
456
|
}
|
|
457
|
+
.squisq-linear-content p + p {
|
|
458
|
+
margin-top: 1.25em;
|
|
459
|
+
}
|
|
386
460
|
.squisq-linear-content ul,
|
|
387
461
|
.squisq-linear-content ol {
|
|
388
462
|
padding-left: 2em;
|
|
@@ -484,7 +558,9 @@ export function LinearDocView({
|
|
|
484
558
|
basePath={basePath}
|
|
485
559
|
viewport={activeViewport}
|
|
486
560
|
renderContext={renderContext}
|
|
487
|
-
blockIndex={i}
|
|
561
|
+
blockIndex={blockIndices.get(block) ?? i}
|
|
562
|
+
blockIndices={blockIndices}
|
|
563
|
+
animationsEnabled={animationsEnabled}
|
|
488
564
|
/>
|
|
489
565
|
))}
|
|
490
566
|
</div>
|