@bendyline/squisq-react 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +6 -0
- package/dist/index.d.ts +101 -96
- package/dist/index.js +1483 -782
- package/dist/squisq-player.full.global.js +4165 -0
- package/dist/squisq-player.global.js +561 -168
- package/dist/standalone-source.d.ts +1 -0
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +64 -0
- package/package.json +8 -4
- package/dist/index.js.map +0 -1
- package/dist/squisq-player.css +0 -2
- package/dist/squisq-player.css.map +0 -1
- package/dist/squisq-player.global.js.map +0 -1
- package/src/BlockRenderer.tsx +0 -194
- package/src/CaptionOverlay.tsx +0 -111
- package/src/DocControlsBottom.tsx +0 -109
- package/src/DocControlsOverlay.tsx +0 -190
- package/src/DocControlsSidebar.tsx +0 -113
- package/src/DocControlsSlideshow.tsx +0 -360
- package/src/DocPlayer.tsx +0 -1528
- package/src/DocPlayerWithSidebar.tsx +0 -155
- package/src/DocProgressBar.tsx +0 -257
- package/src/InlineAudioPlayer.tsx +0 -46
- package/src/InlineVideoPlayer.tsx +0 -70
- package/src/LinearDocView.tsx +0 -569
- package/src/MarkdownRenderer.tsx +0 -624
- package/src/MediaClipLayer.tsx +0 -145
- package/src/SocialCaptionOverlay.tsx +0 -255
- package/src/__tests__/BlockRenderer.test.tsx +0 -235
- package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
- package/src/__tests__/DocPlayer.test.tsx +0 -635
- package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
- package/src/__tests__/DocProgressBar.test.tsx +0 -102
- package/src/__tests__/JsonView.test.tsx +0 -111
- package/src/__tests__/LinearDocView.test.tsx +0 -426
- package/src/__tests__/MapLayer.test.tsx +0 -63
- package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
- package/src/__tests__/MediaClipLayer.test.tsx +0 -70
- package/src/__tests__/MediaContext.test.tsx +0 -51
- package/src/__tests__/PathLayer.test.tsx +0 -84
- package/src/__tests__/TableLayer.test.tsx +0 -142
- package/src/__tests__/VideoLayer.test.tsx +0 -94
- package/src/__tests__/exports.test.ts +0 -55
- package/src/__tests__/fillStyle.test.tsx +0 -160
- package/src/__tests__/standaloneEntry.test.tsx +0 -103
- package/src/__tests__/transitionStyles.test.ts +0 -125
- package/src/__tests__/useAudioSync.test.ts +0 -49
- package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
- package/src/__tests__/useJsonViewTokens.test.ts +0 -41
- package/src/__tests__/useSlideSwipe.test.ts +0 -81
- package/src/__tests__/useViewportOrientation.test.ts +0 -22
- package/src/hooks/AudioController.ts +0 -114
- package/src/hooks/MediaContext.tsx +0 -97
- package/src/hooks/index.ts +0 -11
- package/src/hooks/useAudioSync.ts +0 -456
- package/src/hooks/useAutoSurface.ts +0 -33
- package/src/hooks/useDocPlayback.ts +0 -265
- package/src/hooks/useMediaSchedule.ts +0 -39
- package/src/hooks/useSlideSwipe.ts +0 -265
- package/src/hooks/useViewportOrientation.ts +0 -115
- package/src/index.ts +0 -70
- package/src/jsonView/JsonView.tsx +0 -51
- package/src/jsonView/RenderNode.tsx +0 -51
- package/src/jsonView/index.ts +0 -2
- package/src/jsonView/json-view.css +0 -206
- package/src/jsonView/useJsonViewTokens.ts +0 -32
- package/src/jsonView/viewers.tsx +0 -343
- package/src/layers/ImageLayer.tsx +0 -297
- package/src/layers/MapLayer.tsx +0 -185
- package/src/layers/PathLayer.tsx +0 -155
- package/src/layers/ShapeLayer.tsx +0 -158
- package/src/layers/TableLayer.tsx +0 -129
- package/src/layers/TextLayer.tsx +0 -571
- package/src/layers/TreeLayer.tsx +0 -167
- package/src/layers/VideoLayer.tsx +0 -171
- package/src/layers/index.ts +0 -6
- package/src/standalone-entry.tsx +0 -337
- package/src/standalone-source.d.ts +0 -10
- package/src/styles/doc-animations.css +0 -2431
- package/src/styles/index.css +0 -7
- package/src/types.ts +0 -183
- package/src/utils/animationUtils.ts +0 -13
- package/src/utils/fillStyle.tsx +0 -148
- package/src/utils/layerUtils.ts +0 -42
- package/src/utils/mapTileUtils.ts +0 -375
package/src/LinearDocView.tsx
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LinearDocView Component
|
|
3
|
-
*
|
|
4
|
-
* Renders a Doc as a long-scrolling document view. Each block is displayed
|
|
5
|
-
* as a readable section: non-annotated blocks render their markdown content
|
|
6
|
-
* as HTML, while template-annotated blocks render as inline SVG visual cards
|
|
7
|
-
* via BlockRenderer.
|
|
8
|
-
*
|
|
9
|
-
* This is the view used when `displayMode === 'linear'` in DocPlayer.
|
|
10
|
-
*
|
|
11
|
-
* Layout:
|
|
12
|
-
* - Scrollable container with max-width for readability
|
|
13
|
-
* - Headings from the block hierarchy rendered as HTML headings
|
|
14
|
-
* - Body content rendered via MarkdownRenderer
|
|
15
|
-
* - Template-annotated sections show an SVG card (BlockRenderer)
|
|
16
|
-
* using `materializeBlockLayers()` for on-demand layer computation
|
|
17
|
-
* - Blocks are rendered recursively to preserve the heading hierarchy
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
21
|
-
import { useAutoSurface } from './hooks/useAutoSurface';
|
|
22
|
-
import type { Doc, Block, DocBlock } from '@bendyline/squisq/schemas';
|
|
23
|
-
import type { ViewportConfig } from '@bendyline/squisq/schemas';
|
|
24
|
-
import {
|
|
25
|
-
applySurface,
|
|
26
|
-
resolveFontFamily,
|
|
27
|
-
type SurfaceScheme,
|
|
28
|
-
type Theme,
|
|
29
|
-
} from '@bendyline/squisq/schemas';
|
|
30
|
-
import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
31
|
-
import {
|
|
32
|
-
materializeBlockLayers,
|
|
33
|
-
markdownToDoc,
|
|
34
|
-
DEFAULT_THEME,
|
|
35
|
-
deriveTemplateInputs,
|
|
36
|
-
isTemplateBlock,
|
|
37
|
-
} from '@bendyline/squisq/doc';
|
|
38
|
-
import type { MaterializeBlockLayersOptions } from '@bendyline/squisq/doc';
|
|
39
|
-
import { extractPlainText, parseMarkdown } from '@bendyline/squisq/markdown';
|
|
40
|
-
import { BlockRenderer } from './BlockRenderer';
|
|
41
|
-
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
42
|
-
|
|
43
|
-
// ── Props ──────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
export interface LinearDocViewProps {
|
|
46
|
-
/**
|
|
47
|
-
* The Doc to render. Wins over `markdown` when both are provided.
|
|
48
|
-
* When neither `doc` nor `markdown` is given, an empty container renders.
|
|
49
|
-
*/
|
|
50
|
-
doc?: Doc;
|
|
51
|
-
/**
|
|
52
|
-
* Markdown source to render. When `doc` is absent, the markdown is parsed
|
|
53
|
-
* and converted to a Doc via `markdownToDoc(parseMarkdown(markdown))`.
|
|
54
|
-
* Ignored when `doc` is provided.
|
|
55
|
-
*/
|
|
56
|
-
markdown?: string;
|
|
57
|
-
/** Base path for resolving media URLs (images, etc.) */
|
|
58
|
-
basePath?: string;
|
|
59
|
-
/** Viewport config for SVG card rendering (default: landscape) */
|
|
60
|
-
viewport?: ViewportConfig;
|
|
61
|
-
/** Optional CSS class for the outer container */
|
|
62
|
-
className?: string;
|
|
63
|
-
/** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
|
|
64
|
-
theme?: Theme;
|
|
65
|
-
/** Whether inline visual cards render their layer animations (default: true). */
|
|
66
|
-
animationsEnabled?: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Optional surface scheme (light / dark paper) overlaid on top of the
|
|
69
|
-
* theme's colors. Orthogonal to `theme` — a theme picks editorial
|
|
70
|
-
* identity, a surface picks the paper. Pass `'auto'` to follow the
|
|
71
|
-
* user's OS `prefers-color-scheme`, a `SurfaceScheme` object to force a
|
|
72
|
-
* specific surface, or omit to use the theme's built-in colors.
|
|
73
|
-
*/
|
|
74
|
-
surface?: SurfaceScheme | 'auto';
|
|
75
|
-
/**
|
|
76
|
-
* Use tight padding + a wider content column. The default layout is
|
|
77
|
-
* designed for a reading surface with breathing room (720px column,
|
|
78
|
-
* 24×16px padding). Short conversational snippets like chat replies
|
|
79
|
-
* benefit from a much tighter layout. Set to `true` to render with
|
|
80
|
-
* minimal padding and no max-width cap so the content hugs its
|
|
81
|
-
* container.
|
|
82
|
-
*/
|
|
83
|
-
thinMargins?: boolean;
|
|
84
|
-
/**
|
|
85
|
-
* How images inside the doc should be sized. `'inline'` (default)
|
|
86
|
-
* flows them at natural size up to the column width; `'thumbnail'`
|
|
87
|
-
* constrains each image to a 100×100 box with aspect-preserving
|
|
88
|
-
* containment — use for chat history and other dense surfaces where
|
|
89
|
-
* full-size images would dominate the layout.
|
|
90
|
-
*/
|
|
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;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export type ImageDisplayMode = 'inline' | 'thumbnail';
|
|
100
|
-
|
|
101
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Determine whether a block has a template annotation that should be
|
|
105
|
-
* rendered as a visual SVG card. Unknown templates remain annotated so the
|
|
106
|
-
* materializer can return a visible fallback and structured diagnostic.
|
|
107
|
-
*/
|
|
108
|
-
function isAnnotatedBlock(block: Block): boolean {
|
|
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;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Count total blocks in a hierarchy for the materialization context.
|
|
121
|
-
*/
|
|
122
|
-
function countAll(blocks: Block[]): number {
|
|
123
|
-
let count = 0;
|
|
124
|
-
for (const b of blocks) {
|
|
125
|
-
count++;
|
|
126
|
-
if (b.children) count += countAll(b.children);
|
|
127
|
-
}
|
|
128
|
-
return count;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Block Section Renderer ─────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
interface BlockSectionProps {
|
|
134
|
-
block: Block;
|
|
135
|
-
basePath: string;
|
|
136
|
-
viewport: ViewportConfig;
|
|
137
|
-
renderContext: MaterializeBlockLayersOptions;
|
|
138
|
-
blockIndex: number;
|
|
139
|
-
blockIndices: ReadonlyMap<Block, number>;
|
|
140
|
-
animationsEnabled: boolean;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Render a single block section: heading + body content or SVG card.
|
|
145
|
-
* Recurses into children to render the full heading tree.
|
|
146
|
-
*/
|
|
147
|
-
function BlockSection({
|
|
148
|
-
block,
|
|
149
|
-
basePath,
|
|
150
|
-
viewport,
|
|
151
|
-
renderContext,
|
|
152
|
-
blockIndex,
|
|
153
|
-
blockIndices,
|
|
154
|
-
animationsEnabled,
|
|
155
|
-
}: BlockSectionProps) {
|
|
156
|
-
const isAnnotated = isAnnotatedBlock(block);
|
|
157
|
-
|
|
158
|
-
// For annotated blocks, compute layers and build a Block with them
|
|
159
|
-
const visualBlock = useMemo(() => {
|
|
160
|
-
if (!isAnnotated) return null;
|
|
161
|
-
|
|
162
|
-
const annotation = block.sourceHeading?.templateAnnotation;
|
|
163
|
-
const templateName = visualTemplateName(block) ?? 'sectionHeader';
|
|
164
|
-
|
|
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
|
-
};
|
|
195
|
-
|
|
196
|
-
const ctx: MaterializeBlockLayersOptions = {
|
|
197
|
-
...renderContext,
|
|
198
|
-
blockIndex,
|
|
199
|
-
};
|
|
200
|
-
const { layers } = materializeBlockLayers(templateBlock as unknown as DocBlock, ctx);
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
...block,
|
|
204
|
-
layers,
|
|
205
|
-
template: templateName,
|
|
206
|
-
} as Block;
|
|
207
|
-
}, [block, isAnnotated, renderContext, blockIndex]);
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<div
|
|
211
|
-
className="squisq-linear-section"
|
|
212
|
-
data-block-id={block.id}
|
|
213
|
-
data-block-index={blockIndex}
|
|
214
|
-
data-template={isAnnotated ? visualTemplateName(block) : undefined}
|
|
215
|
-
>
|
|
216
|
-
{/* Render the heading (if present — preamble has no sourceHeading) */}
|
|
217
|
-
{block.sourceHeading && !isAnnotated && <MarkdownRenderer nodes={[block.sourceHeading]} />}
|
|
218
|
-
|
|
219
|
-
{/* Annotated block: render SVG card.
|
|
220
|
-
The heading is intentionally *not* duplicated above the card —
|
|
221
|
-
every template card renders its own title layer internally, so
|
|
222
|
-
a separate `squisq-linear-card-label` only made the heading
|
|
223
|
-
appear twice in the linear view. The card also drops its
|
|
224
|
-
rounded border + drop shadow so it reads as a continuation of
|
|
225
|
-
the surrounding page rather than a chrome'd preview. */}
|
|
226
|
-
{isAnnotated && visualBlock && (
|
|
227
|
-
<div className="squisq-linear-card">
|
|
228
|
-
<div
|
|
229
|
-
className="squisq-linear-card-svg"
|
|
230
|
-
style={{
|
|
231
|
-
width: '100%',
|
|
232
|
-
aspectRatio: `${viewport.width} / ${viewport.height}`,
|
|
233
|
-
overflow: 'hidden',
|
|
234
|
-
marginBottom: '1em',
|
|
235
|
-
}}
|
|
236
|
-
>
|
|
237
|
-
<BlockRenderer
|
|
238
|
-
block={visualBlock}
|
|
239
|
-
blockTime={0}
|
|
240
|
-
basePath={basePath}
|
|
241
|
-
viewport={viewport}
|
|
242
|
-
animationsEnabled={animationsEnabled}
|
|
243
|
-
/>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
{/* Body content (always render for non-annotated blocks, skipped for annotated) */}
|
|
249
|
-
{!isAnnotated && block.contents && block.contents.length > 0 && (
|
|
250
|
-
<MarkdownRenderer nodes={block.contents} />
|
|
251
|
-
)}
|
|
252
|
-
|
|
253
|
-
{/* Recurse into children */}
|
|
254
|
-
{block.children && block.children.length > 0 && (
|
|
255
|
-
<div className="squisq-linear-children">
|
|
256
|
-
{block.children.map((child, i) => (
|
|
257
|
-
<BlockSection
|
|
258
|
-
key={child.id}
|
|
259
|
-
block={child}
|
|
260
|
-
basePath={basePath}
|
|
261
|
-
viewport={viewport}
|
|
262
|
-
renderContext={renderContext}
|
|
263
|
-
blockIndex={blockIndices.get(child) ?? blockIndex + i + 1}
|
|
264
|
-
blockIndices={blockIndices}
|
|
265
|
-
animationsEnabled={animationsEnabled}
|
|
266
|
-
/>
|
|
267
|
-
))}
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ── Main Component ─────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Renders a Doc as a long-scrolling, readable document.
|
|
278
|
-
*
|
|
279
|
-
* Non-annotated blocks are rendered as HTML text (headings, paragraphs,
|
|
280
|
-
* lists, etc.) via MarkdownRenderer. Template-annotated blocks are
|
|
281
|
-
* rendered as inline SVG visual cards via BlockRenderer.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```tsx
|
|
285
|
-
* <LinearDocView doc={doc} basePath="/media/" />
|
|
286
|
-
* ```
|
|
287
|
-
*/
|
|
288
|
-
export function LinearDocView({
|
|
289
|
-
doc,
|
|
290
|
-
markdown,
|
|
291
|
-
basePath = '/',
|
|
292
|
-
viewport,
|
|
293
|
-
className,
|
|
294
|
-
theme,
|
|
295
|
-
surface,
|
|
296
|
-
animationsEnabled = true,
|
|
297
|
-
thinMargins = false,
|
|
298
|
-
imageDisplayMode = 'inline',
|
|
299
|
-
globalKeyboardShortcuts = false,
|
|
300
|
-
}: LinearDocViewProps) {
|
|
301
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
302
|
-
const activeViewport = viewport ?? VIEWPORT_PRESETS.landscape;
|
|
303
|
-
|
|
304
|
-
// Parse markdown into a Doc only when no explicit doc is supplied.
|
|
305
|
-
const markdownDoc = useMemo(
|
|
306
|
-
() => (!doc && markdown !== undefined ? markdownToDoc(parseMarkdown(markdown)) : undefined),
|
|
307
|
-
[doc, markdown],
|
|
308
|
-
);
|
|
309
|
-
const resolvedDoc = doc ?? markdownDoc;
|
|
310
|
-
|
|
311
|
-
const totalBlocks = useMemo(
|
|
312
|
-
() => (resolvedDoc ? countAll(resolvedDoc.blocks) : 0),
|
|
313
|
-
[resolvedDoc],
|
|
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]);
|
|
327
|
-
const autoSurface = useAutoSurface(surface === 'auto');
|
|
328
|
-
const resolvedSurface: SurfaceScheme | undefined = surface === 'auto' ? autoSurface : surface;
|
|
329
|
-
|
|
330
|
-
const renderContext: MaterializeBlockLayersOptions = useMemo(() => {
|
|
331
|
-
const baseTheme = theme ?? DEFAULT_THEME;
|
|
332
|
-
const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
|
|
333
|
-
return {
|
|
334
|
-
theme: effectiveTheme,
|
|
335
|
-
viewport: activeViewport,
|
|
336
|
-
totalBlocks,
|
|
337
|
-
// Theme atmosphere (vignette/grain/gradient persistent layers) shows
|
|
338
|
-
// on the inline template cards so they match the player's look.
|
|
339
|
-
persistentLayers: effectiveTheme.persistentLayers,
|
|
340
|
-
customTemplates: resolvedDoc?.customTemplates,
|
|
341
|
-
};
|
|
342
|
-
}, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
|
|
343
|
-
|
|
344
|
-
const activeTheme = renderContext.theme!;
|
|
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
|
-
|
|
380
|
-
// Nothing to render — keep an empty (but classed) container so hosts can
|
|
381
|
-
// still target/measure the view.
|
|
382
|
-
if (!resolvedDoc) {
|
|
383
|
-
return (
|
|
384
|
-
<div ref={scrollRef} className={`squisq-linear squisq-linear--empty ${className || ''}`} />
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const bgColor = activeTheme.colors.background;
|
|
389
|
-
const textColor = activeTheme.colors.text;
|
|
390
|
-
const mutedColor = activeTheme.colors.textMuted;
|
|
391
|
-
const primaryColor = activeTheme.colors.primary;
|
|
392
|
-
const bodyFont = resolveFontFamily(activeTheme.typography.bodyFont, 'system-ui, sans-serif');
|
|
393
|
-
const titleFont = resolveFontFamily(activeTheme.typography.titleFont, 'Georgia, serif');
|
|
394
|
-
const lineHt = activeTheme.typography.lineHeight ?? 1.7;
|
|
395
|
-
|
|
396
|
-
return (
|
|
397
|
-
<div
|
|
398
|
-
ref={scrollRef}
|
|
399
|
-
className={`squisq-linear ${className || ''}`}
|
|
400
|
-
style={{
|
|
401
|
-
width: '100%',
|
|
402
|
-
// Thin-margins mode is the "embedded in someone else's container"
|
|
403
|
-
// signal (chat bubble, sidebar preview). Fit to content there so
|
|
404
|
-
// the host's bubble doesn't render a tall empty box when the doc
|
|
405
|
-
// is short. Standalone mode keeps height:100% for full-viewport
|
|
406
|
-
// scrolling.
|
|
407
|
-
height: thinMargins ? 'auto' : '100%',
|
|
408
|
-
overflowY: thinMargins ? 'visible' : 'auto',
|
|
409
|
-
overflowX: 'hidden',
|
|
410
|
-
background: bgColor,
|
|
411
|
-
}}
|
|
412
|
-
>
|
|
413
|
-
<div
|
|
414
|
-
className={`squisq-linear-content squisq-md${thinMargins ? ' squisq-linear-content--thin' : ''}${imageDisplayMode === 'thumbnail' ? ' squisq-linear-content--thumbnail-images' : ''}`}
|
|
415
|
-
style={
|
|
416
|
-
{
|
|
417
|
-
// Thin-margins mode drops the 720px reading column + generous
|
|
418
|
-
// page padding (right for standalone docs) in favor of a tight
|
|
419
|
-
// layout that hugs its container (right for chat bubbles and
|
|
420
|
-
// sidebar previews).
|
|
421
|
-
maxWidth: thinMargins ? 'none' : '720px',
|
|
422
|
-
margin: thinMargins ? '0' : '0 auto',
|
|
423
|
-
padding: thinMargins ? '0' : '24px 16px',
|
|
424
|
-
lineHeight: lineHt,
|
|
425
|
-
fontSize: '16px',
|
|
426
|
-
fontFamily: bodyFont,
|
|
427
|
-
color: textColor,
|
|
428
|
-
// CSS custom properties for MarkdownRenderer / nested elements
|
|
429
|
-
'--squisq-linear-title-font': titleFont,
|
|
430
|
-
'--squisq-linear-body-font': bodyFont,
|
|
431
|
-
'--squisq-linear-text': textColor,
|
|
432
|
-
'--squisq-linear-muted': mutedColor,
|
|
433
|
-
'--squisq-linear-primary': primaryColor,
|
|
434
|
-
'--squisq-linear-bg': bgColor,
|
|
435
|
-
} as React.CSSProperties
|
|
436
|
-
}
|
|
437
|
-
>
|
|
438
|
-
{/* Theme-aware typography and layout for document mode */}
|
|
439
|
-
<style>{`
|
|
440
|
-
.squisq-linear-content h1,
|
|
441
|
-
.squisq-linear-content h2,
|
|
442
|
-
.squisq-linear-content h3,
|
|
443
|
-
.squisq-linear-content h4,
|
|
444
|
-
.squisq-linear-content h5,
|
|
445
|
-
.squisq-linear-content h6 {
|
|
446
|
-
font-family: var(--squisq-linear-title-font);
|
|
447
|
-
color: var(--squisq-linear-text);
|
|
448
|
-
margin-top: 1.2em;
|
|
449
|
-
margin-bottom: 0.4em;
|
|
450
|
-
}
|
|
451
|
-
.squisq-linear-content h1 { font-size: 2em; }
|
|
452
|
-
.squisq-linear-content h2 { font-size: 1.5em; }
|
|
453
|
-
.squisq-linear-content h3 { font-size: 1.25em; }
|
|
454
|
-
.squisq-linear-content p {
|
|
455
|
-
margin-bottom: 0.75em;
|
|
456
|
-
}
|
|
457
|
-
.squisq-linear-content p + p {
|
|
458
|
-
margin-top: 1.25em;
|
|
459
|
-
}
|
|
460
|
-
.squisq-linear-content ul,
|
|
461
|
-
.squisq-linear-content ol {
|
|
462
|
-
padding-left: 2em;
|
|
463
|
-
margin-bottom: 0.75em;
|
|
464
|
-
}
|
|
465
|
-
.squisq-linear-content li {
|
|
466
|
-
margin-bottom: 0.3em;
|
|
467
|
-
}
|
|
468
|
-
.squisq-linear-content a {
|
|
469
|
-
/* Blend the theme's primary toward the body text color so
|
|
470
|
-
links stay theme-flavored but always have enough contrast
|
|
471
|
-
against the background. Some themes (e.g. Gezellig) pick a
|
|
472
|
-
mid-tone primary that's nearly invisible on a dark page
|
|
473
|
-
without this lift. */
|
|
474
|
-
color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
|
|
475
|
-
text-decoration: underline;
|
|
476
|
-
text-decoration-thickness: 1px;
|
|
477
|
-
text-underline-offset: 2px;
|
|
478
|
-
}
|
|
479
|
-
.squisq-linear-content a:hover {
|
|
480
|
-
color: var(--squisq-linear-primary);
|
|
481
|
-
}
|
|
482
|
-
.squisq-linear-content code {
|
|
483
|
-
color: var(--squisq-linear-primary);
|
|
484
|
-
font-size: 0.9em;
|
|
485
|
-
padding: 0.15em 0.3em;
|
|
486
|
-
border-radius: 3px;
|
|
487
|
-
background: rgba(128, 128, 128, 0.15);
|
|
488
|
-
}
|
|
489
|
-
.squisq-linear-content pre {
|
|
490
|
-
padding: 1em;
|
|
491
|
-
border-radius: 6px;
|
|
492
|
-
background: rgba(0, 0, 0, 0.2);
|
|
493
|
-
overflow-x: auto;
|
|
494
|
-
margin-bottom: 0.75em;
|
|
495
|
-
}
|
|
496
|
-
.squisq-linear-content pre code {
|
|
497
|
-
padding: 0;
|
|
498
|
-
background: none;
|
|
499
|
-
}
|
|
500
|
-
.squisq-linear-content blockquote {
|
|
501
|
-
border-left: 3px solid var(--squisq-linear-muted);
|
|
502
|
-
color: var(--squisq-linear-muted);
|
|
503
|
-
padding-left: 1em;
|
|
504
|
-
margin-left: 0;
|
|
505
|
-
margin-bottom: 0.75em;
|
|
506
|
-
}
|
|
507
|
-
.squisq-linear-content hr {
|
|
508
|
-
border: none;
|
|
509
|
-
border-top: 1px solid var(--squisq-linear-muted);
|
|
510
|
-
margin: 1.5em 0;
|
|
511
|
-
}
|
|
512
|
-
.squisq-linear-content img {
|
|
513
|
-
max-width: 100%;
|
|
514
|
-
height: auto;
|
|
515
|
-
border-radius: 6px;
|
|
516
|
-
margin: 0.5em 0;
|
|
517
|
-
}
|
|
518
|
-
.squisq-linear-content--thumbnail-images img {
|
|
519
|
-
max-width: 100px;
|
|
520
|
-
max-height: 100px;
|
|
521
|
-
width: auto;
|
|
522
|
-
height: auto;
|
|
523
|
-
object-fit: contain;
|
|
524
|
-
display: block;
|
|
525
|
-
}
|
|
526
|
-
.squisq-linear-content strong {
|
|
527
|
-
font-weight: 700;
|
|
528
|
-
}
|
|
529
|
-
.squisq-linear-content em {
|
|
530
|
-
font-style: italic;
|
|
531
|
-
}
|
|
532
|
-
.squisq-linear-content table {
|
|
533
|
-
width: 100%;
|
|
534
|
-
border-collapse: collapse;
|
|
535
|
-
margin: 1em 0;
|
|
536
|
-
font-size: 0.95em;
|
|
537
|
-
}
|
|
538
|
-
.squisq-linear-content thead th {
|
|
539
|
-
background: var(--squisq-linear-primary);
|
|
540
|
-
color: var(--squisq-linear-bg);
|
|
541
|
-
font-family: var(--squisq-linear-title-font);
|
|
542
|
-
font-weight: 600;
|
|
543
|
-
padding: 10px 14px;
|
|
544
|
-
text-align: left;
|
|
545
|
-
}
|
|
546
|
-
.squisq-linear-content tbody td {
|
|
547
|
-
padding: 8px 14px;
|
|
548
|
-
border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
|
|
549
|
-
}
|
|
550
|
-
.squisq-linear-content tbody tr:hover {
|
|
551
|
-
background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
|
|
552
|
-
}
|
|
553
|
-
`}</style>
|
|
554
|
-
{resolvedDoc.blocks.map((block, i) => (
|
|
555
|
-
<BlockSection
|
|
556
|
-
key={block.id}
|
|
557
|
-
block={block}
|
|
558
|
-
basePath={basePath}
|
|
559
|
-
viewport={activeViewport}
|
|
560
|
-
renderContext={renderContext}
|
|
561
|
-
blockIndex={blockIndices.get(block) ?? i}
|
|
562
|
-
blockIndices={blockIndices}
|
|
563
|
-
animationsEnabled={animationsEnabled}
|
|
564
|
-
/>
|
|
565
|
-
))}
|
|
566
|
-
</div>
|
|
567
|
-
</div>
|
|
568
|
-
);
|
|
569
|
-
}
|