@bendyline/squisq-react 1.1.2 → 1.3.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 (88) hide show
  1. package/dist/DocPlayer.d.ts +9 -2
  2. package/dist/DocPlayer.d.ts.map +1 -1
  3. package/dist/DocPlayer.js +27 -8
  4. package/dist/DocPlayer.js.map +1 -1
  5. package/dist/InlineAudioPlayer.d.ts +15 -0
  6. package/dist/InlineAudioPlayer.d.ts.map +1 -0
  7. package/dist/InlineAudioPlayer.js +22 -0
  8. package/dist/InlineAudioPlayer.js.map +1 -0
  9. package/dist/InlineVideoPlayer.d.ts +21 -0
  10. package/dist/InlineVideoPlayer.d.ts.map +1 -0
  11. package/dist/InlineVideoPlayer.js +29 -0
  12. package/dist/InlineVideoPlayer.js.map +1 -0
  13. package/dist/LinearDocView.d.ts +28 -2
  14. package/dist/LinearDocView.d.ts.map +1 -1
  15. package/dist/LinearDocView.js +159 -24
  16. package/dist/LinearDocView.js.map +1 -1
  17. package/dist/MarkdownRenderer.d.ts.map +1 -1
  18. package/dist/MarkdownRenderer.js +123 -4
  19. package/dist/MarkdownRenderer.js.map +1 -1
  20. package/dist/SocialCaptionOverlay.d.ts.map +1 -1
  21. package/dist/SocialCaptionOverlay.js +3 -2
  22. package/dist/SocialCaptionOverlay.js.map +1 -1
  23. package/dist/__tests__/JsonView.test.d.ts +2 -0
  24. package/dist/__tests__/JsonView.test.d.ts.map +1 -0
  25. package/dist/__tests__/JsonView.test.js +93 -0
  26. package/dist/__tests__/JsonView.test.js.map +1 -0
  27. package/dist/__tests__/LinearDocView.test.js +90 -0
  28. package/dist/__tests__/LinearDocView.test.js.map +1 -1
  29. package/dist/hooks/MediaContext.d.ts.map +1 -1
  30. package/dist/hooks/MediaContext.js +13 -7
  31. package/dist/hooks/MediaContext.js.map +1 -1
  32. package/dist/hooks/useAutoSurface.d.ts +11 -0
  33. package/dist/hooks/useAutoSurface.d.ts.map +1 -0
  34. package/dist/hooks/useAutoSurface.js +24 -0
  35. package/dist/hooks/useAutoSurface.js.map +1 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/jsonView/JsonView.d.ts +26 -0
  41. package/dist/jsonView/JsonView.d.ts.map +1 -0
  42. package/dist/jsonView/JsonView.js +12 -0
  43. package/dist/jsonView/JsonView.js.map +1 -0
  44. package/dist/jsonView/RenderNode.d.ts +22 -0
  45. package/dist/jsonView/RenderNode.d.ts.map +1 -0
  46. package/dist/jsonView/RenderNode.js +30 -0
  47. package/dist/jsonView/RenderNode.js.map +1 -0
  48. package/dist/jsonView/index.d.ts +3 -0
  49. package/dist/jsonView/index.d.ts.map +1 -0
  50. package/dist/jsonView/index.js +2 -0
  51. package/dist/jsonView/index.js.map +1 -0
  52. package/dist/jsonView/useJsonViewTokens.d.ts +14 -0
  53. package/dist/jsonView/useJsonViewTokens.d.ts.map +1 -0
  54. package/dist/jsonView/useJsonViewTokens.js +34 -0
  55. package/dist/jsonView/useJsonViewTokens.js.map +1 -0
  56. package/dist/jsonView/viewers.d.ts +30 -0
  57. package/dist/jsonView/viewers.d.ts.map +1 -0
  58. package/dist/jsonView/viewers.js +226 -0
  59. package/dist/jsonView/viewers.js.map +1 -0
  60. package/dist/squisq-player.css +1 -1
  61. package/dist/squisq-player.css.map +1 -1
  62. package/dist/squisq-player.global.js +32 -8
  63. package/dist/squisq-player.global.js.map +1 -1
  64. package/dist/standalone-source.js +1 -1
  65. package/dist/types.d.ts +5 -1
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +8 -7
  69. package/src/DocPlayer.tsx +44 -8
  70. package/src/InlineAudioPlayer.tsx +46 -0
  71. package/src/InlineVideoPlayer.tsx +70 -0
  72. package/src/LinearDocView.tsx +198 -26
  73. package/src/MarkdownRenderer.tsx +189 -12
  74. package/src/SocialCaptionOverlay.tsx +3 -2
  75. package/src/__tests__/JsonView.test.tsx +111 -0
  76. package/src/__tests__/LinearDocView.test.tsx +101 -0
  77. package/src/hooks/MediaContext.tsx +15 -8
  78. package/src/hooks/useAutoSurface.ts +33 -0
  79. package/src/index.ts +10 -0
  80. package/src/jsonView/JsonView.tsx +51 -0
  81. package/src/jsonView/RenderNode.tsx +51 -0
  82. package/src/jsonView/index.ts +2 -0
  83. package/src/jsonView/json-view.css +206 -0
  84. package/src/jsonView/useJsonViewTokens.ts +57 -0
  85. package/src/jsonView/viewers.tsx +343 -0
  86. package/src/styles/doc-animations.css +36 -0
  87. package/src/styles/index.css +7 -0
  88. package/src/types.ts +5 -1
package/dist/types.d.ts CHANGED
@@ -26,8 +26,12 @@ export type ControlsLayout = 'overlay' | 'sidebar' | 'bottom';
26
26
  * - `'linear'` — Long-scrolling document view. Renders the full markdown as
27
27
  * readable HTML with template-annotated sections shown as inline SVG cards.
28
28
  * No audio, no timeline, no controls — just a scrollable page.
29
+ * - `'page'` — Plain semantic HTML preview matching what the
30
+ * `markdownDocToPlainHtml` export produces. No SquisqPlayer, no SVG
31
+ * cards — just `<h1>`/`<p>`/`<ul>` etc. inside a sandboxed iframe.
32
+ * Use when you want a WYSIWYG view of the simple HTML export.
29
33
  */
30
- export type DisplayMode = 'video' | 'slideshow' | 'linear';
34
+ export type DisplayMode = 'video' | 'slideshow' | 'linear' | 'page';
31
35
  /**
32
36
  * Caption display style.
33
37
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0JH,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACvD,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8JH,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACvD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-react",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "React component library for doc playback, block rendering, and media layers",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -31,20 +31,21 @@
31
31
  ],
32
32
  "exports": {
33
33
  ".": {
34
- "import": "./dist/index.js",
35
- "types": "./dist/index.d.ts"
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
36
  },
37
- "./styles": "./src/styles/doc-animations.css",
37
+ "./styles": "./src/styles/index.css",
38
38
  "./standalone": "./dist/squisq-player.global.js",
39
39
  "./standalone-source": {
40
- "import": "./dist/standalone-source.js",
41
- "types": "./dist/standalone-source.d.ts"
40
+ "types": "./dist/standalone-source.d.ts",
41
+ "import": "./dist/standalone-source.js"
42
42
  }
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup && tsup --config tsup.standalone.config.ts && node scripts/generate-standalone-source.mjs",
46
46
  "build:esm": "tsup",
47
47
  "build:standalone": "tsup --config tsup.standalone.config.ts && node scripts/generate-standalone-source.mjs",
48
+ "dev": "npm run build:standalone && concurrently -n js,dts -c blue,gray -r \"tsup --watch --no-dts --no-clean\" \"tsup --watch --dts-only --no-clean\"",
48
49
  "typecheck": "tsc --noEmit"
49
50
  },
50
51
  "peerDependencies": {
@@ -52,7 +53,7 @@
52
53
  "react-dom": "^18.0.0 || ^19.0.0"
53
54
  },
54
55
  "dependencies": {
55
- "@bendyline/squisq": "1.2.2"
56
+ "@bendyline/squisq": "1.4.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/react": "18.3.28",
package/src/DocPlayer.tsx CHANGED
@@ -25,9 +25,11 @@
25
25
  import { Fragment, useRef, useState, useEffect, useCallback, useMemo } from 'react';
26
26
  import type { Doc, Block, TextLayer, StartBlockConfig, DocBlock } from '@bendyline/squisq/schemas';
27
27
  import { isTemplateBlock, getCaptionAtTime } from '@bendyline/squisq/schemas';
28
- import type { Theme } from '@bendyline/squisq/schemas';
28
+ import type { SurfaceScheme, Theme } from '@bendyline/squisq/schemas';
29
+ import { applySurface } from '@bendyline/squisq/schemas';
29
30
  import { BlockRenderer } from './BlockRenderer';
30
31
  import { CaptionOverlay } from './CaptionOverlay';
32
+ import { useAutoSurface } from './hooks/useAutoSurface';
31
33
  import { useAudioSync } from './hooks/useAudioSync';
32
34
  import { useDocPlayback } from './hooks/useDocPlayback';
33
35
  import { useViewportOrientation } from './hooks/useViewportOrientation';
@@ -162,6 +164,13 @@ interface DocPlayerProps {
162
164
  forceViewport?: ViewportConfig;
163
165
  /** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
164
166
  theme?: Theme;
167
+ /**
168
+ * Optional surface scheme (light / dark paper) overlaid on top of the
169
+ * theme's colors. Passed through to the underlying LinearDocView when
170
+ * `displayMode === 'linear'`; otherwise overlaid onto the theme that
171
+ * renders the player's SVG blocks.
172
+ */
173
+ surface?: SurfaceScheme | 'auto';
165
174
  /**
166
175
  * Display mode for the player.
167
176
  * - `'video'` (default) — Traditional video playback with play/pause, scrub bar, auto-advance.
@@ -197,6 +206,7 @@ export function DocPlayer({
197
206
  forceViewport,
198
207
  displayMode = 'video',
199
208
  theme,
209
+ surface,
200
210
  captionStyle = 'standard',
201
211
  }: DocPlayerProps) {
202
212
  const isSlideshowMode = displayMode === 'slideshow';
@@ -275,6 +285,15 @@ export function DocPlayer({
275
285
  [renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode],
276
286
  );
277
287
 
288
+ // Resolve surface (light/dark paper) and apply it to the theme before
289
+ // handing off to downstream renderers. Orthogonal to the editorial theme.
290
+ const autoSurface = useAutoSurface(surface === 'auto');
291
+ const resolvedSurface = surface === 'auto' ? autoSurface : surface;
292
+ const effectiveTheme = useMemo(() => {
293
+ const base = theme ?? DEFAULT_THEME;
294
+ return resolvedSurface ? applySurface(base, resolvedSurface) : base;
295
+ }, [theme, resolvedSurface]);
296
+
278
297
  // Doc playback hook - pass viewport for responsive template expansion
279
298
  const {
280
299
  currentBlock,
@@ -288,14 +307,14 @@ export function DocPlayer({
288
307
  nextBlock: _nextBlock,
289
308
  prevBlock: _prevBlock,
290
309
  blocks: expandedBlocks,
291
- } = useDocPlayback(script, currentTime, activeViewport, renderMode, theme);
310
+ } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
292
311
 
293
312
  // Expand cover block (startBlock) if present - uses active viewport
294
313
  const coverBlock = useMemo((): Block | null => {
295
314
  const startBlockConfig = script.startBlock as StartBlockConfig | undefined;
296
315
  if (!startBlockConfig) return null;
297
316
 
298
- const context = createTemplateContext(theme ?? DEFAULT_THEME, 0, 1, activeViewport);
317
+ const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
299
318
  const layers = expandCoverBlock(startBlockConfig, context);
300
319
 
301
320
  return {
@@ -305,7 +324,7 @@ export function DocPlayer({
305
324
  audioSegment: -1,
306
325
  layers,
307
326
  };
308
- }, [script.startBlock, activeViewport, theme]);
327
+ }, [script.startBlock, activeViewport, effectiveTheme]);
309
328
 
310
329
  // Render-mode cover block control: allows Playwright to force-show the cover block
311
330
  const [coverForced, setCoverForced] = useState(false);
@@ -314,14 +333,25 @@ export function DocPlayer({
314
333
  const [coverGraceActive, setCoverGraceActive] = useState(false);
315
334
  const coverGraceTimer = useRef<ReturnType<typeof setTimeout>>();
316
335
  const coverWasShowing = useRef(false);
336
+ // Track whether playback has ever been initiated — prevents the cover block
337
+ // from re-appearing when paused at currentTime === 0 (e.g., no audio source).
338
+ const hasPlayedOnce = useRef(false);
317
339
 
318
340
  // Track when cover is showing at rest (before play)
319
- const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !renderMode && !autoPlay);
341
+ const atRest = !!(
342
+ coverBlock &&
343
+ !isPlaying &&
344
+ currentTime === 0 &&
345
+ !hasPlayedOnce.current &&
346
+ !renderMode &&
347
+ !autoPlay
348
+ );
320
349
  if (atRest) coverWasShowing.current = true;
321
350
 
322
351
  useEffect(() => {
323
352
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
324
353
  coverWasShowing.current = false;
354
+ hasPlayedOnce.current = true;
325
355
  setCoverGraceActive(true);
326
356
  // Intentionally no cleanup: if coverBlock's memoized reference changes
327
357
  // mid-grace (e.g., due to a preview re-render), clearing the timer would
@@ -344,7 +374,7 @@ export function DocPlayer({
344
374
  coverBlock &&
345
375
  (coverForced ||
346
376
  coverGraceActive ||
347
- (!isPlaying && currentTime === 0 && !renderMode && !autoPlay));
377
+ (!isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay));
348
378
 
349
379
  // Auto-play if enabled (wait for audio to be ready)
350
380
  // Use a ref to track if we've already auto-played to avoid repeating on every render
@@ -785,7 +815,13 @@ export function DocPlayer({
785
815
  overflow: 'hidden',
786
816
  }}
787
817
  >
788
- <LinearDocView doc={script} basePath={basePath} viewport={activeViewport} />
818
+ <LinearDocView
819
+ doc={script}
820
+ basePath={basePath}
821
+ viewport={activeViewport}
822
+ theme={theme}
823
+ surface={surface}
824
+ />
789
825
  </div>
790
826
  );
791
827
  }
@@ -857,7 +893,7 @@ export function DocPlayer({
857
893
  enabled={captionsEnabled && (renderMode || isPlaying || currentTime > 0)}
858
894
  fontSize={16}
859
895
  captionStyle={activeCaptionStyle}
860
- theme={theme}
896
+ theme={effectiveTheme}
861
897
  viewport={activeViewport}
862
898
  />
863
899
  )}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * InlineAudioPlayer
3
+ *
4
+ * Small inline HTML5 audio player for `<audio>` tags embedded directly in
5
+ * a markdown document — e.g. a narration recording the user wants to
6
+ * audition in the editor. Resolves the `src` through `MediaContext` so a
7
+ * container-backed `audio/foo.webm` path returns a playable blob URL.
8
+ *
9
+ * Deliberately thin: native browser controls, no autoplay. The
10
+ * MarkdownRenderer swaps this in for raw `<audio>` htmlElements; the
11
+ * editor-react Tiptap audio node reuses it for its NodeView.
12
+ */
13
+ import { useMediaUrl } from './hooks/MediaContext.js';
14
+
15
+ export interface InlineAudioPlayerProps {
16
+ /** Source path — resolved through MediaContext when relative. */
17
+ src: string;
18
+ /** Base path used when no MediaProvider is in context. */
19
+ basePath?: string;
20
+ /** Whether to show native controls. Defaults to true. */
21
+ controls?: boolean;
22
+ /** `preload` attribute passthrough. Defaults to `'metadata'`. */
23
+ preload?: 'none' | 'metadata' | 'auto';
24
+ /** Extra className on the wrapper. */
25
+ className?: string;
26
+ }
27
+
28
+ export function InlineAudioPlayer({
29
+ src,
30
+ basePath = '',
31
+ controls = true,
32
+ preload = 'metadata',
33
+ className,
34
+ }: InlineAudioPlayerProps) {
35
+ const resolvedSrc = useMediaUrl(src, basePath);
36
+
37
+ if (!resolvedSrc) return null;
38
+
39
+ return (
40
+ <span className={`squisq-inline-audio-player ${className ?? ''}`.trim()}>
41
+ <audio src={resolvedSrc} controls={controls} preload={preload} />
42
+ </span>
43
+ );
44
+ }
45
+
46
+ export default InlineAudioPlayer;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * InlineVideoPlayer
3
+ *
4
+ * Small inline HTML5 video player for `<video>` tags embedded directly in
5
+ * a markdown document — e.g. a recording dropped into the doc by the
6
+ * recorder. Resolves the `src` through `MediaContext` (same path
7
+ * `VideoLayer` uses) so a container-backed `audio/foo.webm` or
8
+ * `video/foo.mp4` resolves to a blob URL instead of 404-ing on the
9
+ * filesystem path.
10
+ *
11
+ * Deliberately thin: native browser controls, intrinsic aspect ratio,
12
+ * no autoplay. The MarkdownRenderer swaps this in for a raw `<video>`
13
+ * htmlElement; the editor-react Tiptap video node uses the same
14
+ * component for its NodeView.
15
+ */
16
+ import { useMediaUrl } from './hooks/MediaContext.js';
17
+
18
+ export interface InlineVideoPlayerProps {
19
+ /** Source path — resolved through MediaContext when relative. */
20
+ src: string;
21
+ /** Base path used when no MediaProvider is in context. */
22
+ basePath?: string;
23
+ /** Optional explicit width (pixels or CSS length). */
24
+ width?: number | string;
25
+ /** Optional explicit height (pixels or CSS length). */
26
+ height?: number | string;
27
+ /** Optional poster image src — also resolved through MediaContext. */
28
+ poster?: string;
29
+ /** Whether to show native controls. Defaults to true. */
30
+ controls?: boolean;
31
+ /** `preload` attribute passthrough. Defaults to `'metadata'`. */
32
+ preload?: 'none' | 'metadata' | 'auto';
33
+ /** Extra className on the wrapper. */
34
+ className?: string;
35
+ }
36
+
37
+ export function InlineVideoPlayer({
38
+ src,
39
+ basePath = '',
40
+ width,
41
+ height,
42
+ poster,
43
+ controls = true,
44
+ preload = 'metadata',
45
+ className,
46
+ }: InlineVideoPlayerProps) {
47
+ const resolvedSrc = useMediaUrl(src, basePath);
48
+ // Always call the hook (Rules of Hooks); only use the result if a poster
49
+ // was actually requested. Empty input → fallback path which we discard.
50
+ const resolvedPoster = useMediaUrl(poster ?? '', basePath);
51
+ const posterUrl = poster ? resolvedPoster : undefined;
52
+
53
+ if (!resolvedSrc) return null;
54
+
55
+ return (
56
+ <span className={`squisq-inline-video-player ${className ?? ''}`.trim()}>
57
+ <video
58
+ src={resolvedSrc}
59
+ poster={posterUrl}
60
+ controls={controls}
61
+ preload={preload}
62
+ width={width}
63
+ height={height}
64
+ playsInline
65
+ />
66
+ </span>
67
+ );
68
+ }
69
+
70
+ export default InlineVideoPlayer;
@@ -18,9 +18,15 @@
18
18
  */
19
19
 
20
20
  import { useMemo } from 'react';
21
+ import { useAutoSurface } from './hooks/useAutoSurface';
21
22
  import type { Doc, Block, DocBlock } from '@bendyline/squisq/schemas';
22
23
  import type { ViewportConfig } from '@bendyline/squisq/schemas';
23
- import type { Theme } from '@bendyline/squisq/schemas';
24
+ import {
25
+ applySurface,
26
+ resolveFontFamily,
27
+ type SurfaceScheme,
28
+ type Theme,
29
+ } from '@bendyline/squisq/schemas';
24
30
  import { VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
25
31
  import { getLayers, hasTemplate, DEFAULT_THEME } from '@bendyline/squisq/doc';
26
32
  import type { RenderContext } from '@bendyline/squisq/doc';
@@ -42,8 +48,35 @@ export interface LinearDocViewProps {
42
48
  className?: string;
43
49
  /** Theme to use for rendering (default: DEFAULT_THEME from the theme library) */
44
50
  theme?: Theme;
51
+ /**
52
+ * Optional surface scheme (light / dark paper) overlaid on top of the
53
+ * theme's colors. Orthogonal to `theme` — a theme picks editorial
54
+ * identity, a surface picks the paper. Pass `'auto'` to follow the
55
+ * user's OS `prefers-color-scheme`, a `SurfaceScheme` object to force a
56
+ * specific surface, or omit to use the theme's built-in colors.
57
+ */
58
+ surface?: SurfaceScheme | 'auto';
59
+ /**
60
+ * Use tight padding + a wider content column. The default layout is
61
+ * designed for a reading surface with breathing room (720px column,
62
+ * 24×16px padding). Short conversational snippets like chat replies
63
+ * benefit from a much tighter layout. Set to `true` to render with
64
+ * minimal padding and no max-width cap so the content hugs its
65
+ * container.
66
+ */
67
+ thinMargins?: boolean;
68
+ /**
69
+ * How images inside the doc should be sized. `'inline'` (default)
70
+ * flows them at natural size up to the column width; `'thumbnail'`
71
+ * constrains each image to a 100×100 box with aspect-preserving
72
+ * containment — use for chat history and other dense surfaces where
73
+ * full-size images would dominate the layout.
74
+ */
75
+ imageDisplayMode?: ImageDisplayMode;
45
76
  }
46
77
 
78
+ export type ImageDisplayMode = 'inline' | 'thumbnail';
79
+
47
80
  // ── Helpers ────────────────────────────────────────────────────────
48
81
 
49
82
  /**
@@ -136,23 +169,21 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }:
136
169
  {/* Render the heading (if present — preamble has no sourceHeading) */}
137
170
  {block.sourceHeading && !isAnnotated && <MarkdownRenderer nodes={[block.sourceHeading]} />}
138
171
 
139
- {/* Annotated block: render SVG card */}
172
+ {/* Annotated block: render SVG card.
173
+ The heading is intentionally *not* duplicated above the card —
174
+ every template card renders its own title layer internally, so
175
+ a separate `squisq-linear-card-label` only made the heading
176
+ appear twice in the linear view. The card also drops its
177
+ rounded border + drop shadow so it reads as a continuation of
178
+ the surrounding page rather than a chrome'd preview. */}
140
179
  {isAnnotated && visualBlock && (
141
180
  <div className="squisq-linear-card">
142
- {/* Optional heading label above the card */}
143
- {block.sourceHeading && (
144
- <div className="squisq-linear-card-label squisq-md">
145
- <MarkdownRenderer nodes={[block.sourceHeading]} />
146
- </div>
147
- )}
148
181
  <div
149
182
  className="squisq-linear-card-svg"
150
183
  style={{
151
184
  width: '100%',
152
185
  aspectRatio: `${viewport.width} / ${viewport.height}`,
153
186
  overflow: 'hidden',
154
- borderRadius: '8px',
155
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
156
187
  marginBottom: '1em',
157
188
  }}
158
189
  >
@@ -216,6 +247,83 @@ function extractListItems(contents?: MarkdownBlockNode[]): string[] {
216
247
  return items;
217
248
  }
218
249
 
250
+ /** First image discovered in a block's body, with any explicit
251
+ * dimensions captured (set by `<img width>` / `<img height>` in raw
252
+ * HTML — markdown shorthand has no syntax for dimensions). */
253
+ interface FirstImage {
254
+ src: string;
255
+ alt: string;
256
+ width?: number;
257
+ height?: number;
258
+ }
259
+
260
+ /**
261
+ * Find the first image referenced anywhere in block contents — both
262
+ * markdown shorthand `![alt](url)` (type `image`) and raw HTML `<img>`
263
+ * tags (type `htmlBlock`/`htmlInline`). The WYSIWYG editor emits the
264
+ * HTML form whenever a user resizes an image (markdown shorthand has no
265
+ * width syntax), so missing that path silently breaks every resized
266
+ * image in the linear view.
267
+ */
268
+ function extractFirstImage(contents: MarkdownBlockNode[] | undefined): FirstImage | null {
269
+ if (!contents || contents.length === 0) return null;
270
+
271
+ function fromHtml(nodes: unknown[]): FirstImage | null {
272
+ for (const node of nodes) {
273
+ if (!node || typeof node !== 'object') continue;
274
+ const n = node as Record<string, unknown>;
275
+ if (n.type === 'htmlElement' && n.tagName === 'img') {
276
+ const attrs = n.attributes as Record<string, string> | undefined;
277
+ if (attrs && typeof attrs.src === 'string' && attrs.src) {
278
+ return {
279
+ src: attrs.src,
280
+ alt: typeof attrs.alt === 'string' ? attrs.alt : '',
281
+ width: parseDim(attrs.width),
282
+ height: parseDim(attrs.height),
283
+ };
284
+ }
285
+ }
286
+ if (Array.isArray(n.children)) {
287
+ const found = fromHtml(n.children);
288
+ if (found) return found;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+
294
+ function walk(node: unknown): FirstImage | null {
295
+ if (!node || typeof node !== 'object') return null;
296
+ const n = node as Record<string, unknown>;
297
+ if (n.type === 'image' && typeof n.url === 'string' && n.url) {
298
+ return { src: n.url, alt: typeof n.alt === 'string' ? n.alt : '' };
299
+ }
300
+ if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
301
+ const found = fromHtml(n.htmlChildren);
302
+ if (found) return found;
303
+ }
304
+ if (Array.isArray(n.children)) {
305
+ for (const child of n.children) {
306
+ const found = walk(child);
307
+ if (found) return found;
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+
313
+ for (const node of contents) {
314
+ const found = walk(node);
315
+ if (found) return found;
316
+ }
317
+ return null;
318
+ }
319
+
320
+ /** Parse a `width`/`height` HTML attribute to a positive number. */
321
+ function parseDim(raw: string | undefined): number | undefined {
322
+ if (raw === undefined) return undefined;
323
+ const n = parseFloat(raw);
324
+ return Number.isFinite(n) && n > 0 ? n : undefined;
325
+ }
326
+
219
327
  /** Extract table data (headers, rows, alignment) from block contents. */
220
328
  function extractTableData(contents?: MarkdownBlockNode[]): {
221
329
  headers: string[];
@@ -249,7 +357,7 @@ function getTemplateDefaults(
249
357
  switch (templateName) {
250
358
  case 'statHighlight':
251
359
  return { stat: headingText, description: bodyText || headingText };
252
- case 'quoteBlock':
360
+ case 'quote':
253
361
  case 'fullBleedQuote':
254
362
  case 'pullQuote':
255
363
  return { quote: bodyText || headingText };
@@ -257,7 +365,7 @@ function getTemplateDefaults(
257
365
  return { fact: headingText, explanation: bodyText || headingText };
258
366
  case 'comparisonBar':
259
367
  return { leftLabel: 'A', leftValue: 60, rightLabel: 'B', rightValue: 40 };
260
- case 'listBlock': {
368
+ case 'list': {
261
369
  const items = extractListItems(contents);
262
370
  return { items: items.length > 0 ? items : ['Item 1', 'Item 2', 'Item 3'] };
263
371
  }
@@ -269,6 +377,37 @@ function getTemplateDefaults(
269
377
  const tableData = extractTableData(contents);
270
378
  return tableData ?? { headers: ['Column'], rows: [['Data']] };
271
379
  }
380
+ case 'imageWithCaption': {
381
+ // The template requires `imageSrc` — without it the image layer
382
+ // renders a broken `<img src=undefined>`. Pull the first image
383
+ // out of the block's body (markdown shorthand or HTML <img>) and
384
+ // use the heading as a caption fallback so a bare `# Title
385
+ // [Image with Caption]` block renders something sensible.
386
+ const img = extractFirstImage(contents);
387
+ if (!img) return { caption: headingText };
388
+ return {
389
+ imageSrc: img.src,
390
+ imageAlt: img.alt || headingText,
391
+ caption: headingText,
392
+ };
393
+ }
394
+ case 'leftFeature':
395
+ case 'rightFeature': {
396
+ // Feature blocks pair an image with the heading + the first
397
+ // paragraph of body text. Both inputs come from the section's
398
+ // contents — the image via the same scan used by imageWithCaption,
399
+ // the body text via the plain-text extractor that's already in
400
+ // scope here.
401
+ const img = extractFirstImage(contents);
402
+ return {
403
+ imageSrc: img?.src ?? '',
404
+ imageAlt: img?.alt || headingText,
405
+ imageWidth: img?.width,
406
+ imageHeight: img?.height,
407
+ title: headingText,
408
+ body: bodyText,
409
+ };
410
+ }
272
411
  default:
273
412
  return {};
274
413
  }
@@ -294,26 +433,31 @@ export function LinearDocView({
294
433
  viewport,
295
434
  className,
296
435
  theme,
436
+ surface,
437
+ thinMargins = false,
438
+ imageDisplayMode = 'inline',
297
439
  }: LinearDocViewProps) {
298
440
  const activeViewport = viewport ?? VIEWPORT_PRESETS.landscape;
299
441
  const totalBlocks = useMemo(() => countAll(doc.blocks), [doc.blocks]);
442
+ const autoSurface = useAutoSurface(surface === 'auto');
443
+ const resolvedSurface: SurfaceScheme | undefined = surface === 'auto' ? autoSurface : surface;
300
444
 
301
- const renderContext: RenderContext = useMemo(
302
- () => ({
303
- theme: theme ?? DEFAULT_THEME,
445
+ const renderContext: RenderContext = useMemo(() => {
446
+ const baseTheme = theme ?? DEFAULT_THEME;
447
+ return {
448
+ theme: resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme,
304
449
  viewport: activeViewport,
305
450
  totalBlocks,
306
- }),
307
- [activeViewport, totalBlocks, theme],
308
- );
451
+ };
452
+ }, [activeViewport, totalBlocks, theme, resolvedSurface]);
309
453
 
310
454
  const activeTheme = renderContext.theme!;
311
455
  const bgColor = activeTheme.colors.background;
312
456
  const textColor = activeTheme.colors.text;
313
457
  const mutedColor = activeTheme.colors.textMuted;
314
458
  const primaryColor = activeTheme.colors.primary;
315
- const bodyFont = activeTheme.typography.bodyFontFamily;
316
- const titleFont = activeTheme.typography.titleFontFamily;
459
+ const bodyFont = resolveFontFamily(activeTheme.typography.bodyFont, 'system-ui, sans-serif');
460
+ const titleFont = resolveFontFamily(activeTheme.typography.titleFont, 'Georgia, serif');
317
461
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
318
462
 
319
463
  return (
@@ -321,19 +465,28 @@ export function LinearDocView({
321
465
  className={`squisq-linear ${className || ''}`}
322
466
  style={{
323
467
  width: '100%',
324
- height: '100%',
325
- overflowY: 'auto',
468
+ // Thin-margins mode is the "embedded in someone else's container"
469
+ // signal (chat bubble, sidebar preview). Fit to content there so
470
+ // the host's bubble doesn't render a tall empty box when the doc
471
+ // is short. Standalone mode keeps height:100% for full-viewport
472
+ // scrolling.
473
+ height: thinMargins ? 'auto' : '100%',
474
+ overflowY: thinMargins ? 'visible' : 'auto',
326
475
  overflowX: 'hidden',
327
476
  background: bgColor,
328
477
  }}
329
478
  >
330
479
  <div
331
- className="squisq-linear-content squisq-md"
480
+ className={`squisq-linear-content squisq-md${thinMargins ? ' squisq-linear-content--thin' : ''}${imageDisplayMode === 'thumbnail' ? ' squisq-linear-content--thumbnail-images' : ''}`}
332
481
  style={
333
482
  {
334
- maxWidth: '720px',
335
- margin: '0 auto',
336
- padding: '24px 16px',
483
+ // Thin-margins mode drops the 720px reading column + generous
484
+ // page padding (right for standalone docs) in favor of a tight
485
+ // layout that hugs its container (right for chat bubbles and
486
+ // sidebar previews).
487
+ maxWidth: thinMargins ? 'none' : '720px',
488
+ margin: thinMargins ? '0' : '0 auto',
489
+ padding: thinMargins ? '0' : '24px 16px',
337
490
  lineHeight: lineHt,
338
491
  fontSize: '16px',
339
492
  fontFamily: bodyFont,
@@ -376,6 +529,17 @@ export function LinearDocView({
376
529
  margin-bottom: 0.3em;
377
530
  }
378
531
  .squisq-linear-content a {
532
+ /* Blend the theme's primary toward the body text color so
533
+ links stay theme-flavored but always have enough contrast
534
+ against the background. Some themes (e.g. Gezellig) pick a
535
+ mid-tone primary that's nearly invisible on a dark page
536
+ without this lift. */
537
+ color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
538
+ text-decoration: underline;
539
+ text-decoration-thickness: 1px;
540
+ text-underline-offset: 2px;
541
+ }
542
+ .squisq-linear-content a:hover {
379
543
  color: var(--squisq-linear-primary);
380
544
  }
381
545
  .squisq-linear-content code {
@@ -414,6 +578,14 @@ export function LinearDocView({
414
578
  border-radius: 6px;
415
579
  margin: 0.5em 0;
416
580
  }
581
+ .squisq-linear-content--thumbnail-images img {
582
+ max-width: 100px;
583
+ max-height: 100px;
584
+ width: auto;
585
+ height: auto;
586
+ object-fit: contain;
587
+ display: block;
588
+ }
417
589
  .squisq-linear-content strong {
418
590
  font-weight: 700;
419
591
  }