@bendyline/squisq-react 1.1.1 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-react",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "React component library for doc playback, block rendering, and media layers",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -52,7 +52,7 @@
52
52
  "react-dom": "^18.0.0 || ^19.0.0"
53
53
  },
54
54
  "dependencies": {
55
- "@bendyline/squisq": "1.2.1"
55
+ "@bendyline/squisq": "1.2.2"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/react": "18.3.28",
package/src/DocPlayer.tsx CHANGED
@@ -323,11 +323,17 @@ export function DocPlayer({
323
323
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
324
324
  coverWasShowing.current = false;
325
325
  setCoverGraceActive(true);
326
+ // Intentionally no cleanup: if coverBlock's memoized reference changes
327
+ // mid-grace (e.g., due to a preview re-render), clearing the timer would
328
+ // leave coverGraceActive stuck at true because the effect body won't
329
+ // re-run (coverWasShowing.current is now false).
326
330
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3000);
327
- return () => clearTimeout(coverGraceTimer.current);
328
331
  }
329
332
  }, [isPlaying, coverBlock, renderMode]);
330
333
 
334
+ // Always clear the grace timer on unmount
335
+ useEffect(() => () => clearTimeout(coverGraceTimer.current), []);
336
+
331
337
  // Determine if we should show the cover block
332
338
  // Show cover when: has cover block, not playing, at time 0, not in render mode
333
339
  // OR during the grace period after first play, OR when coverForced (render mode)
@@ -257,8 +257,10 @@ function getTemplateDefaults(
257
257
  return { fact: headingText, explanation: bodyText || headingText };
258
258
  case 'comparisonBar':
259
259
  return { leftLabel: 'A', leftValue: 60, rightLabel: 'B', rightValue: 40 };
260
- case 'listBlock':
261
- return { items: extractListItems(contents) || ['Item 1', 'Item 2', 'Item 3'] };
260
+ case 'listBlock': {
261
+ const items = extractListItems(contents);
262
+ return { items: items.length > 0 ? items : ['Item 1', 'Item 2', 'Item 3'] };
263
+ }
262
264
  case 'definitionCard':
263
265
  return { term: headingText, definition: bodyText || headingText };
264
266
  case 'dateEvent':
@@ -406,6 +408,12 @@ export function LinearDocView({
406
408
  border-top: 1px solid var(--squisq-linear-muted);
407
409
  margin: 1.5em 0;
408
410
  }
411
+ .squisq-linear-content img {
412
+ max-width: 100%;
413
+ height: auto;
414
+ border-radius: 6px;
415
+ margin: 0.5em 0;
416
+ }
409
417
  .squisq-linear-content strong {
410
418
  font-weight: 700;
411
419
  }
@@ -22,6 +22,7 @@ import type {
22
22
  MarkdownTableRow,
23
23
  MarkdownTableCell,
24
24
  } from '@bendyline/squisq/markdown';
25
+ import { useMediaUrl } from './hooks/MediaContext';
25
26
 
26
27
  // ── Props ──────────────────────────────────────────────────────────
27
28
 
@@ -86,13 +87,7 @@ function renderInline(nodes: MarkdownInlineNode[], keyPrefix = ''): React.ReactN
86
87
 
87
88
  case 'image':
88
89
  return (
89
- <img
90
- key={key}
91
- className="squisq-md-image"
92
- src={node.url}
93
- alt={node.alt ?? ''}
94
- title={node.title ?? undefined}
95
- />
90
+ <MdImage key={key} src={node.url} alt={node.alt ?? ''} title={node.title ?? undefined} />
96
91
  );
97
92
 
98
93
  case 'break':
@@ -343,6 +338,14 @@ function renderBlocks(nodes: MarkdownBlockNode[], keyPrefix = ''): React.ReactNo
343
338
  return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`));
344
339
  }
345
340
 
341
+ // ── Image with MediaProvider resolution ───────────────────────────
342
+
343
+ /** Renders an <img> that resolves its src through the MediaProvider when available. */
344
+ function MdImage({ src, alt, title }: { src: string; alt: string; title?: string }) {
345
+ const resolved = useMediaUrl(src, '.');
346
+ return <img className="squisq-md-image" src={resolved} alt={alt} title={title} />;
347
+ }
348
+
346
349
  // ── Main Component ─────────────────────────────────────────────────
347
350
 
348
351
  /**
@@ -61,8 +61,8 @@ export function ImageLayer({ layer, basePath, viewport, blockTime }: ImageLayerP
61
61
  <foreignObject x={finalX} y={finalY} width={width} height={height}>
62
62
  <div
63
63
  style={{
64
- width: '100%',
65
- height: '100%',
64
+ width: `${width}px`,
65
+ height: `${height}px`,
66
66
  overflow: 'hidden',
67
67
  }}
68
68
  >
@@ -71,8 +71,8 @@ export function ImageLayer({ layer, basePath, viewport, blockTime }: ImageLayerP
71
71
  alt={content.alt || ''}
72
72
  className={kbStyle.className}
73
73
  style={{
74
- width: '100%',
75
- height: '100%',
74
+ width: `${width}px`,
75
+ height: `${height}px`,
76
76
  objectFit: 'cover',
77
77
  objectPosition: 'center',
78
78
  display: 'block',
@@ -101,8 +101,8 @@ export function ImageLayer({ layer, basePath, viewport, blockTime }: ImageLayerP
101
101
  src={src}
102
102
  alt={content.alt || ''}
103
103
  style={{
104
- width: '100%',
105
- height: '100%',
104
+ width: `${width}px`,
105
+ height: `${height}px`,
106
106
  objectFit: 'cover',
107
107
  objectPosition: 'center',
108
108
  display: 'block',
@@ -49,8 +49,8 @@ export function ShapeLayer({ layer, viewport, blockTime }: ShapeLayerProps) {
49
49
  <foreignObject x={x} y={y} width={width} height={height}>
50
50
  <div
51
51
  style={{
52
- width: '100%',
53
- height: '100%',
52
+ width: `${width}px`,
53
+ height: `${height}px`,
54
54
  background: fill,
55
55
  borderRadius: content.borderRadius ? `${content.borderRadius}px` : undefined,
56
56
  pointerEvents: 'none',
@@ -51,8 +51,8 @@ export function TableLayer({ layer, viewport, blockTime }: TableLayerProps) {
51
51
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
52
  {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}
53
53
  style={{
54
- width: '100%',
55
- height: '100%',
54
+ width: `${width}px`,
55
+ height: `${height}px`,
56
56
  display: 'flex',
57
57
  alignItems: 'center',
58
58
  justifyContent: 'center',
@@ -134,8 +134,8 @@ export function VideoLayer({
134
134
  data-clip-start={content.clipStart}
135
135
  data-clip-end={content.clipEnd}
136
136
  style={{
137
- width: '100%',
138
- height: '100%',
137
+ width: `${width}px`,
138
+ height: `${height}px`,
139
139
  objectFit: content.fit || 'cover',
140
140
  objectPosition: 'center',
141
141
  display: 'block',
@@ -86,10 +86,22 @@ function createInlineMediaProvider(
86
86
  images: Record<string, string>,
87
87
  basePath: string,
88
88
  ): MediaProvider {
89
+ // Doc image references may not exactly match image map keys
90
+ // (e.g., "images/hero.jpg" vs "hero.jpg"). Build a filename-keyed
91
+ // lookup so resolution can fall back to basename matching.
92
+ const byFilename: Record<string, string> = {};
93
+ for (const key of Object.keys(images)) {
94
+ const filename = key.split('/').pop()!;
95
+ byFilename[filename] = images[key];
96
+ }
97
+
89
98
  return {
90
99
  async resolveUrl(relativePath: string): Promise<string> {
91
100
  if (relativePath in images) return images[relativePath];
92
- // Fallback to basePath
101
+ const stripped = relativePath.replace(/^\.\//, '');
102
+ if (stripped !== relativePath && stripped in images) return images[stripped];
103
+ const filename = relativePath.split('/').pop()!;
104
+ if (filename in byFilename) return byFilename[filename];
93
105
  if (
94
106
  relativePath.startsWith('http') ||
95
107
  relativePath.startsWith('data:') ||