@bendyline/squisq-react 2.11.2 → 2.11.3

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.
@@ -0,0 +1,31 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { Block, TableLayer as TableLayer$1 } from '@bendyline/squisq/schemas';
4
+
5
+ interface TableLayerContentRendererProps {
6
+ /** Owning block, including sourceBlockId for transformed projections. */
7
+ block: Block;
8
+ layer: TableLayer$1;
9
+ width: number;
10
+ height: number;
11
+ /** Native themed table used while custom data is unavailable. */
12
+ fallback: ReactNode;
13
+ }
14
+ type TableLayerContentRenderer = ComponentType<TableLayerContentRendererProps>;
15
+ interface TableLayerProps {
16
+ /** Owning block (custom renderers use its id to resolve sidecar data). */
17
+ block?: Block;
18
+ layer: TableLayer$1;
19
+ /** Viewport dimensions for percentage calculations */
20
+ viewport: {
21
+ width: number;
22
+ height: number;
23
+ };
24
+ /** Current time relative to block start (for animation) */
25
+ blockTime: number;
26
+ /** Optional host renderer for interactive table content. */
27
+ contentRenderer?: TableLayerContentRenderer;
28
+ }
29
+ declare function TableLayer({ block, layer, viewport, blockTime, contentRenderer, }: TableLayerProps): react_jsx_runtime.JSX.Element;
30
+
31
+ export { TableLayer as T, type TableLayerContentRenderer as a, type TableLayerContentRendererProps as b };
@@ -9,7 +9,7 @@ import {
9
9
  TreeLayer,
10
10
  VideoLayer,
11
11
  getTransitionClass
12
- } from "./chunk-4MP7CH3C.js";
12
+ } from "./chunk-XSOMFBH2.js";
13
13
  import {
14
14
  useAutoSurface
15
15
  } from "./chunk-TT6ENR6T.js";
@@ -22,13 +22,93 @@ import {
22
22
  } from "./chunk-LR3AIGDD.js";
23
23
 
24
24
  // src/BlockRenderer.tsx
25
- import { useId } from "react";
25
+ import { useId, useLayoutEffect, useRef, useState } from "react";
26
26
  import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
27
+
28
+ // src/utils/textFit.ts
29
+ function largestFittingTextScale(fits, maximum = 2) {
30
+ if (maximum <= 1 || !fits(1)) return 1;
31
+ if (fits(maximum)) return maximum;
32
+ let low = 1;
33
+ let high = maximum;
34
+ for (let attempt = 0; attempt < 10; attempt += 1) {
35
+ const candidate = (low + high) / 2;
36
+ if (fits(candidate)) low = candidate;
37
+ else high = candidate;
38
+ }
39
+ return Math.max(1, Math.floor(low * 100) / 100);
40
+ }
41
+
42
+ // src/BlockRenderer.tsx
27
43
  import { jsx, jsxs } from "react/jsx-runtime";
28
44
  var DEFAULT_VIEWPORT = {
29
45
  width: 1920,
30
46
  height: 1080
31
47
  };
48
+ var FIT_TOLERANCE_PX = 1;
49
+ function applyTextScale(targets, scale) {
50
+ for (const target of targets) {
51
+ if (target.dataset.squisqTextFit === "html") {
52
+ const baseFontSize = Number(target.dataset.squisqBaseFontSize);
53
+ if (Number.isFinite(baseFontSize)) target.style.fontSize = `${baseFontSize * scale}px`;
54
+ } else {
55
+ target.style.transform = `scale(${scale})`;
56
+ }
57
+ }
58
+ }
59
+ function cssPixels(value) {
60
+ const parsed = parseFloat(value);
61
+ return Number.isFinite(parsed) ? parsed : 0;
62
+ }
63
+ function htmlTargetFits(target) {
64
+ const content = target.querySelector('[data-squisq-text-content="true"]');
65
+ if (!content || content.scrollWidth <= 0 || content.scrollHeight <= 0) return false;
66
+ const view = target.ownerDocument.defaultView;
67
+ const computed = view?.getComputedStyle(target);
68
+ const horizontalInset = computed ? cssPixels(computed.paddingLeft) + cssPixels(computed.paddingRight) + cssPixels(computed.borderLeftWidth) + cssPixels(computed.borderRightWidth) : 0;
69
+ const verticalInset = computed ? cssPixels(computed.paddingTop) + cssPixels(computed.paddingBottom) + cssPixels(computed.borderTopWidth) + cssPixels(computed.borderBottomWidth) : 0;
70
+ const availableWidth = Math.max(0, target.clientWidth - horizontalInset);
71
+ const availableHeight = Math.max(0, target.clientHeight - verticalInset);
72
+ if (content.scrollWidth > availableWidth + FIT_TOLERANCE_PX || content.scrollHeight > availableHeight + FIT_TOLERANCE_PX) {
73
+ return false;
74
+ }
75
+ for (const scrollRegion of content.querySelectorAll("[data-squisq-table-scroll]")) {
76
+ if (scrollRegion.clientWidth > 0 && scrollRegion.scrollWidth > scrollRegion.clientWidth + FIT_TOLERANCE_PX || scrollRegion.clientHeight > 0 && scrollRegion.scrollHeight > scrollRegion.clientHeight + FIT_TOLERANCE_PX) {
77
+ return false;
78
+ }
79
+ }
80
+ return true;
81
+ }
82
+ function svgTargetFits(target, scale) {
83
+ const content = target.querySelector('[data-squisq-text-content="true"]');
84
+ if (!content || typeof content.getBBox !== "function") return false;
85
+ try {
86
+ const box = content.getBBox();
87
+ const availableWidth = Number(target.dataset.squisqFitWidth);
88
+ const availableHeight = Number(target.dataset.squisqFitHeight);
89
+ return box.width > 0 && box.height > 0 && box.width * scale <= availableWidth + FIT_TOLERANCE_PX && box.height * scale <= availableHeight + FIT_TOLERANCE_PX;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ function renderedTextRects(targets) {
95
+ return targets.map(
96
+ (target) => target.querySelector('[data-squisq-text-content="true"]')?.getBoundingClientRect()
97
+ ).filter((rect) => !!rect && rect.width > 0 && rect.height > 0);
98
+ }
99
+ function overlapPairs(rects) {
100
+ const pairs = /* @__PURE__ */ new Set();
101
+ for (let left = 0; left < rects.length; left += 1) {
102
+ for (let right = left + 1; right < rects.length; right += 1) {
103
+ const a = rects[left];
104
+ const b = rects[right];
105
+ if (a.left < b.right - FIT_TOLERANCE_PX && a.right > b.left + FIT_TOLERANCE_PX && a.top < b.bottom - FIT_TOLERANCE_PX && a.bottom > b.top + FIT_TOLERANCE_PX) {
106
+ pairs.add(`${left}:${right}`);
107
+ }
108
+ }
109
+ }
110
+ return pairs;
111
+ }
32
112
  function BlockRenderer({
33
113
  block,
34
114
  blockTime,
@@ -40,7 +120,9 @@ function BlockRenderer({
40
120
  isPlaying,
41
121
  muted = false,
42
122
  animationsEnabled = true,
43
- theme
123
+ theme,
124
+ tableContentRenderer,
125
+ growTextToFit = false
44
126
  }) {
45
127
  let transitionClass = "";
46
128
  const transitionStyle = {};
@@ -54,20 +136,63 @@ function BlockRenderer({
54
136
  }
55
137
  const instanceId = useId().replace(/:/g, "");
56
138
  const clipId = `vb-clip-${instanceId}-${block.id}`;
139
+ const svgRef = useRef(null);
140
+ const [textScale, setTextScale] = useState(1);
141
+ useLayoutEffect(() => {
142
+ const svg = svgRef.current;
143
+ let cancelled = false;
144
+ if (!svg) return;
145
+ const measure = () => {
146
+ if (cancelled) return;
147
+ const targets = Array.from(svg.querySelectorAll("[data-squisq-text-fit]"));
148
+ if (!growTextToFit || targets.length === 0) {
149
+ applyTextScale(targets, 1);
150
+ setTextScale(1);
151
+ return;
152
+ }
153
+ applyTextScale(targets, 1);
154
+ const baselineOverlaps = overlapPairs(renderedTextRects(targets));
155
+ const fits = (scale) => {
156
+ applyTextScale(targets, scale);
157
+ if (targets.some(
158
+ (target) => target.dataset.squisqTextFit === "html" ? !htmlTargetFits(target) : !svgTargetFits(target, scale)
159
+ )) {
160
+ return false;
161
+ }
162
+ const candidateOverlaps = overlapPairs(renderedTextRects(targets));
163
+ for (const pair of candidateOverlaps) {
164
+ if (!baselineOverlaps.has(pair)) return false;
165
+ }
166
+ return true;
167
+ };
168
+ const nextScale = largestFittingTextScale(fits);
169
+ applyTextScale(targets, nextScale);
170
+ setTextScale((current) => current === nextScale ? current : nextScale);
171
+ };
172
+ measure();
173
+ const fonts = svg.ownerDocument?.fonts;
174
+ if (fonts && fonts.status !== "loaded") void fonts.ready.then(measure);
175
+ return () => {
176
+ cancelled = true;
177
+ };
178
+ }, [block.layers, growTextToFit, viewport.height, viewport.width]);
57
179
  return /* @__PURE__ */ jsxs(
58
180
  "svg",
59
181
  {
182
+ ref: svgRef,
60
183
  className: `block-svg ${transitionClass}`,
61
184
  style: transitionStyle,
62
185
  viewBox: `0 0 ${viewport.width} ${viewport.height}`,
63
186
  preserveAspectRatio: "xMidYMid meet",
64
187
  overflow: "hidden",
65
188
  "data-block-id": block.id,
189
+ "data-squisq-text-scale": growTextToFit ? textScale : void 0,
66
190
  children: [
67
191
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
68
192
  /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx(
69
193
  LayerRenderer,
70
194
  {
195
+ block,
71
196
  layer,
72
197
  basePath,
73
198
  viewport,
@@ -75,7 +200,9 @@ function BlockRenderer({
75
200
  isPlaying,
76
201
  muted,
77
202
  animationsEnabled,
78
- theme
203
+ theme,
204
+ tableContentRenderer,
205
+ textScale
79
206
  },
80
207
  layer.id
81
208
  )) })
@@ -84,6 +211,7 @@ function BlockRenderer({
84
211
  );
85
212
  }
86
213
  function LayerRenderer({
214
+ block,
87
215
  layer,
88
216
  basePath,
89
217
  viewport,
@@ -91,7 +219,9 @@ function LayerRenderer({
91
219
  isPlaying,
92
220
  muted,
93
221
  animationsEnabled,
94
- theme
222
+ theme,
223
+ tableContentRenderer,
224
+ textScale
95
225
  }) {
96
226
  const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
97
227
  switch (renderedLayer.type) {
@@ -107,7 +237,15 @@ function LayerRenderer({
107
237
  }
108
238
  );
109
239
  case "text":
110
- return /* @__PURE__ */ jsx(TextLayer, { layer: renderedLayer, viewport, blockTime });
240
+ return /* @__PURE__ */ jsx(
241
+ TextLayer,
242
+ {
243
+ layer: renderedLayer,
244
+ viewport,
245
+ blockTime,
246
+ textScale
247
+ }
248
+ );
111
249
  case "shape":
112
250
  return /* @__PURE__ */ jsx(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
113
251
  case "path":
@@ -135,7 +273,16 @@ function LayerRenderer({
135
273
  }
136
274
  );
137
275
  case "table":
138
- return /* @__PURE__ */ jsx(TableLayer, { layer: renderedLayer, viewport, blockTime });
276
+ return /* @__PURE__ */ jsx(
277
+ TableLayer,
278
+ {
279
+ block,
280
+ layer: renderedLayer,
281
+ viewport,
282
+ blockTime,
283
+ contentRenderer: tableContentRenderer
284
+ }
285
+ );
139
286
  case "tree":
140
287
  return /* @__PURE__ */ jsx(TreeLayer, { layer: renderedLayer, viewport, blockTime });
141
288
  case "mermaid":
@@ -625,7 +772,7 @@ function PageSectionView({ entry, featureFlip, isLeadProse }) {
625
772
  }
626
773
 
627
774
  // src/LinearDocView.tsx
628
- import { useEffect, useMemo as useMemo2, useRef } from "react";
775
+ import { useEffect, useMemo as useMemo2, useRef as useRef2 } from "react";
629
776
  import {
630
777
  applySurface,
631
778
  resolveFontFamily
@@ -663,7 +810,7 @@ function LinearDocView({
663
810
  htmlPolicy,
664
811
  linkSchemes
665
812
  }) {
666
- const scrollRef = useRef(null);
813
+ const scrollRef = useRef2(null);
667
814
  const activeViewport = viewport ?? VIEWPORT_PRESETS2.landscape;
668
815
  const markdownDoc = useMemo2(
669
816
  () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-ZC7LI5UF.js";
4
+ } from "./chunk-5UGIE2QO.js";
5
5
  import {
6
6
  useAudioSync,
7
7
  useDocPlayback,
@@ -2693,7 +2693,8 @@ function DocPlayerContent({
2693
2693
  globalKeyboardShortcuts = false,
2694
2694
  showCodeCopyButton = false,
2695
2695
  onCopyCode,
2696
- fenceRenderers
2696
+ fenceRenderers,
2697
+ tableContentRenderer
2697
2698
  }) {
2698
2699
  const isSlideshowMode = displayMode === "slideshow";
2699
2700
  const isLinearMode = displayMode === "linear";
@@ -3600,7 +3601,9 @@ function DocPlayerContent({
3600
3601
  viewport: activeViewport,
3601
3602
  muted: renderMode || muted,
3602
3603
  animationsEnabled,
3603
- theme: effectiveTheme
3604
+ theme: effectiveTheme,
3605
+ tableContentRenderer,
3606
+ growTextToFit: isSlideshowMode
3604
3607
  }
3605
3608
  ) }),
3606
3609
  animationsEnabled && !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
@@ -3618,7 +3621,9 @@ function DocPlayerContent({
3618
3621
  viewport: activeViewport,
3619
3622
  muted: renderMode || muted,
3620
3623
  animationsEnabled,
3621
- theme: effectiveTheme
3624
+ theme: effectiveTheme,
3625
+ tableContentRenderer,
3626
+ growTextToFit: isSlideshowMode
3622
3627
  }
3623
3628
  ) }, previousBlock.id),
3624
3629
  currentBlock && /* @__PURE__ */ jsx9(
@@ -3641,7 +3646,9 @@ function DocPlayerContent({
3641
3646
  isPlaying,
3642
3647
  muted: renderMode || muted,
3643
3648
  animationsEnabled,
3644
- theme: effectiveTheme
3649
+ theme: effectiveTheme,
3650
+ tableContentRenderer,
3651
+ growTextToFit: isSlideshowMode
3645
3652
  }
3646
3653
  )
3647
3654
  },