@bendyline/squisq-react 2.2.0 → 2.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.
@@ -0,0 +1,789 @@
1
+ import {
2
+ ImageLayer,
3
+ MapLayer,
4
+ MermaidLayer,
5
+ PathLayer,
6
+ ShapeLayer,
7
+ TableLayer,
8
+ TextLayer,
9
+ TreeLayer,
10
+ VideoLayer,
11
+ getTransitionClass
12
+ } from "./chunk-XYS7HMP4.js";
13
+ import {
14
+ useAutoSurface
15
+ } from "./chunk-TT6ENR6T.js";
16
+ import {
17
+ InlineVideoPlayer,
18
+ MarkdownRenderer
19
+ } from "./chunk-D7KN4CRG.js";
20
+ import {
21
+ useMediaUrl
22
+ } from "./chunk-LR3AIGDD.js";
23
+
24
+ // src/BlockRenderer.tsx
25
+ import { useId } from "react";
26
+ import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
27
+ import { jsx, jsxs } from "react/jsx-runtime";
28
+ var DEFAULT_VIEWPORT = {
29
+ width: 1920,
30
+ height: 1080
31
+ };
32
+ function BlockRenderer({
33
+ block,
34
+ blockTime,
35
+ basePath,
36
+ isEntering = false,
37
+ isExiting = false,
38
+ transition,
39
+ viewport = DEFAULT_VIEWPORT,
40
+ isPlaying,
41
+ animationsEnabled = true,
42
+ theme
43
+ }) {
44
+ let transitionClass = "";
45
+ const transitionStyle = {};
46
+ const activeTransition = animationsEnabled ? transition ?? block.transition : void 0;
47
+ if (activeTransition && isEntering) {
48
+ transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
49
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
50
+ } else if (activeTransition && isExiting) {
51
+ transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
52
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
53
+ }
54
+ const instanceId = useId().replace(/:/g, "");
55
+ const clipId = `vb-clip-${instanceId}-${block.id}`;
56
+ return /* @__PURE__ */ jsxs(
57
+ "svg",
58
+ {
59
+ className: `block-svg ${transitionClass}`,
60
+ style: transitionStyle,
61
+ viewBox: `0 0 ${viewport.width} ${viewport.height}`,
62
+ preserveAspectRatio: "xMidYMid meet",
63
+ overflow: "hidden",
64
+ "data-block-id": block.id,
65
+ children: [
66
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
67
+ /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx(
68
+ LayerRenderer,
69
+ {
70
+ layer,
71
+ basePath,
72
+ viewport,
73
+ blockTime,
74
+ isPlaying,
75
+ animationsEnabled,
76
+ theme
77
+ },
78
+ layer.id
79
+ )) })
80
+ ]
81
+ }
82
+ );
83
+ }
84
+ function LayerRenderer({
85
+ layer,
86
+ basePath,
87
+ viewport,
88
+ blockTime,
89
+ isPlaying,
90
+ animationsEnabled,
91
+ theme
92
+ }) {
93
+ const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
94
+ switch (renderedLayer.type) {
95
+ case "image":
96
+ return /* @__PURE__ */ jsx(
97
+ ImageLayer,
98
+ {
99
+ layer: renderedLayer,
100
+ basePath,
101
+ viewport,
102
+ blockTime,
103
+ animationsEnabled
104
+ }
105
+ );
106
+ case "text":
107
+ return /* @__PURE__ */ jsx(TextLayer, { layer: renderedLayer, viewport, blockTime });
108
+ case "shape":
109
+ return /* @__PURE__ */ jsx(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
110
+ case "path":
111
+ return /* @__PURE__ */ jsx(PathLayer, { layer: renderedLayer, viewport, blockTime });
112
+ case "map":
113
+ return /* @__PURE__ */ jsx(
114
+ MapLayer,
115
+ {
116
+ layer: renderedLayer,
117
+ basePath,
118
+ viewport,
119
+ blockTime
120
+ }
121
+ );
122
+ case "video":
123
+ return /* @__PURE__ */ jsx(
124
+ VideoLayer,
125
+ {
126
+ layer: renderedLayer,
127
+ basePath,
128
+ viewport,
129
+ blockTime,
130
+ isPlaying
131
+ }
132
+ );
133
+ case "table":
134
+ return /* @__PURE__ */ jsx(TableLayer, { layer: renderedLayer, viewport, blockTime });
135
+ case "tree":
136
+ return /* @__PURE__ */ jsx(TreeLayer, { layer: renderedLayer, viewport, blockTime });
137
+ case "mermaid":
138
+ return /* @__PURE__ */ jsx(
139
+ MermaidLayer,
140
+ {
141
+ layer: renderedLayer,
142
+ viewport,
143
+ blockTime,
144
+ theme
145
+ }
146
+ );
147
+ default:
148
+ console.warn(`Unknown layer type: ${renderedLayer.type}`);
149
+ return null;
150
+ }
151
+ }
152
+
153
+ // src/page/PageViewContext.ts
154
+ import { createContext, useContext } from "react";
155
+ import { DEFAULT_THEME, resolvePageStyle } from "@bendyline/squisq/doc";
156
+ import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
157
+ var defaultValue = {
158
+ theme: DEFAULT_THEME,
159
+ pageStyle: resolvePageStyle(DEFAULT_THEME),
160
+ basePath: "/",
161
+ viewport: VIEWPORT_PRESETS.landscape,
162
+ renderContext: { theme: DEFAULT_THEME },
163
+ animationsEnabled: true,
164
+ imageDisplayMode: "inline"
165
+ };
166
+ var PageViewContext = createContext(defaultValue);
167
+ function usePageView() {
168
+ return useContext(PageViewContext);
169
+ }
170
+
171
+ // src/page/CanvasSection.tsx
172
+ import { useMemo } from "react";
173
+ import { materializeBlockLayers } from "@bendyline/squisq/doc";
174
+ import { jsx as jsx2 } from "react/jsx-runtime";
175
+ function CanvasSection({ section }) {
176
+ const { renderContext, basePath, animationsEnabled, theme } = usePageView();
177
+ const media = section.slots.media;
178
+ const canvas = media?.type === "canvas" ? media : void 0;
179
+ const visualBlock = useMemo(() => {
180
+ if (!canvas) return null;
181
+ const { layers } = materializeBlockLayers(canvas.block, {
182
+ ...renderContext,
183
+ blockIndex: section.index
184
+ });
185
+ return { ...canvas.block, layers };
186
+ }, [canvas, renderContext, section.index]);
187
+ if (!canvas || !visualBlock) return null;
188
+ const aspect = canvas.aspect;
189
+ const frame = section.hints?.frame;
190
+ const frameClass = frame === "terminal" ? " squisq-page-canvas--terminal squisq-page-canvas--framed" : " squisq-page-canvas--framed";
191
+ return /* @__PURE__ */ jsx2(
192
+ "div",
193
+ {
194
+ className: `squisq-page-canvas${frameClass}`,
195
+ style: {
196
+ aspectRatio: `${aspect.width} / ${aspect.height}`,
197
+ // Clamp tall canvases; derive the width cap from the aspect so a
198
+ // portrait viewport doesn't produce a page-height skyscraper.
199
+ maxHeight: "min(70vh, 720px)",
200
+ maxWidth: `calc(min(70vh, 720px) * ${(aspect.width / Math.max(1, aspect.height)).toFixed(4)})`
201
+ },
202
+ children: /* @__PURE__ */ jsx2(
203
+ BlockRenderer,
204
+ {
205
+ block: visualBlock,
206
+ blockTime: 0,
207
+ basePath,
208
+ viewport: { width: aspect.width, height: aspect.height },
209
+ animationsEnabled,
210
+ theme
211
+ }
212
+ )
213
+ }
214
+ );
215
+ }
216
+
217
+ // src/page/sections.tsx
218
+ import { sanitizeUrl } from "@bendyline/squisq/markdown";
219
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
220
+ function PageImg({
221
+ media,
222
+ eager,
223
+ className
224
+ }) {
225
+ const { basePath } = usePageView();
226
+ const safeSrc = sanitizeUrl(media.src, "media");
227
+ const resolved = useMediaUrl(safeSrc ?? "", basePath);
228
+ if (!safeSrc) return null;
229
+ return /* @__PURE__ */ jsx3(
230
+ "img",
231
+ {
232
+ className,
233
+ src: resolved,
234
+ alt: media.alt,
235
+ loading: eager ? "eager" : "lazy",
236
+ decoding: "async",
237
+ width: media.width,
238
+ height: media.height
239
+ }
240
+ );
241
+ }
242
+ function BackdropVideo({ media }) {
243
+ const { basePath } = usePageView();
244
+ const safeSrc = sanitizeUrl(media.src, "media");
245
+ const resolvedSrc = useMediaUrl(safeSrc ?? "", basePath);
246
+ const safePoster = media.posterSrc ? sanitizeUrl(media.posterSrc, "media") : void 0;
247
+ const resolvedPoster = useMediaUrl(safePoster ?? "", basePath);
248
+ if (!safeSrc) return null;
249
+ return /* @__PURE__ */ jsx3(
250
+ "video",
251
+ {
252
+ src: resolvedSrc,
253
+ poster: safePoster ? resolvedPoster : void 0,
254
+ muted: true,
255
+ loop: true,
256
+ autoPlay: true,
257
+ playsInline: true,
258
+ preload: "metadata"
259
+ }
260
+ );
261
+ }
262
+ function Eyebrow({ section }) {
263
+ if (!section.slots.eyebrow) return null;
264
+ return /* @__PURE__ */ jsx3("span", { className: "squisq-page-eyebrow", children: section.slots.eyebrow });
265
+ }
266
+ function MediaCredit({ media }) {
267
+ if (!media || media.type === "canvas" || !media.credit) return null;
268
+ return /* @__PURE__ */ jsxs2("span", { className: "squisq-page-media-credit", children: [
269
+ media.credit,
270
+ media.license ? ` \xB7 ${media.license}` : ""
271
+ ] });
272
+ }
273
+ function HeroSection({ section }) {
274
+ const { pageStyle } = usePageView();
275
+ const media = section.slots.media;
276
+ const image = media?.type === "image" ? media : void 0;
277
+ const heroStyle = pageStyle.tokens.heroStyle;
278
+ const body = /* @__PURE__ */ jsxs2("div", { className: "squisq-page-hero-body", children: [
279
+ /* @__PURE__ */ jsx3(Eyebrow, { section }),
280
+ /* @__PURE__ */ jsx3("h1", { className: "squisq-page-hero-title", children: section.slots.title }),
281
+ section.slots.subtitle && /* @__PURE__ */ jsx3("p", { className: "squisq-page-hero-subtitle", children: section.slots.subtitle })
282
+ ] });
283
+ if (image && heroStyle === "split") {
284
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-hero-grid", children: [
285
+ body,
286
+ /* @__PURE__ */ jsxs2("div", { className: "squisq-page-hero-media squisq-page-media-frame", children: [
287
+ /* @__PURE__ */ jsx3(PageImg, { media: image, eager: true }),
288
+ /* @__PURE__ */ jsx3(MediaCredit, { media: image })
289
+ ] })
290
+ ] });
291
+ }
292
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
293
+ image && section.background === "media" && /* @__PURE__ */ jsx3("div", { className: "squisq-page-hero-backdrop", children: /* @__PURE__ */ jsx3(PageImg, { media: image, eager: true }) }),
294
+ body
295
+ ] });
296
+ }
297
+ function BannerSection({ section }) {
298
+ const media = section.slots.media;
299
+ const image = media?.type === "image" ? media : void 0;
300
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
301
+ image && section.background === "media" && /* @__PURE__ */ jsx3("div", { className: "squisq-page-hero-backdrop", children: /* @__PURE__ */ jsx3(PageImg, { media: image }) }),
302
+ /* @__PURE__ */ jsxs2("div", { className: "squisq-page-banner-body", children: [
303
+ /* @__PURE__ */ jsx3(Eyebrow, { section }),
304
+ /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-banner-title", children: section.slots.title })
305
+ ] })
306
+ ] });
307
+ }
308
+ function StatBandSection({ section }) {
309
+ const items = section.slots.items ?? [];
310
+ const comparison = section.variant === "comparison";
311
+ const numericValues = items.map(
312
+ (item) => typeof item.value === "number" ? item.value : Number.parseFloat(String(item.value ?? ""))
313
+ );
314
+ const maxValue = Math.max(1, ...numericValues.filter((value) => Number.isFinite(value)));
315
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
316
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-stats", children: items.map((item, i) => /* @__PURE__ */ jsxs2("div", { className: `squisq-page-stat${comparison ? " squisq-page-stat--bars" : ""}`, children: [
317
+ /* @__PURE__ */ jsx3("span", { className: "squisq-page-stat-value", children: item.value }),
318
+ comparison && Number.isFinite(numericValues[i]) && /* @__PURE__ */ jsx3(
319
+ "div",
320
+ {
321
+ className: "squisq-page-stat-bar",
322
+ style: { width: `${Math.max(4, numericValues[i] / maxValue * 100)}%` }
323
+ }
324
+ ),
325
+ item.title && /* @__PURE__ */ jsx3("span", { className: "squisq-page-stat-title", children: item.title }),
326
+ item.body && /* @__PURE__ */ jsx3("span", { className: "squisq-page-stat-body", children: item.body })
327
+ ] }, i)) }),
328
+ section.slots.meta && /* @__PURE__ */ jsx3("div", { className: "squisq-page-stat-meta", children: section.slots.meta })
329
+ ] });
330
+ }
331
+ function QuoteBandSection({ section }) {
332
+ const media = section.slots.media;
333
+ const backdrop = section.background === "media" && media && media.type !== "canvas";
334
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
335
+ backdrop && /* @__PURE__ */ jsx3("div", { className: "squisq-page-quote-backdrop", children: media.type === "image" ? /* @__PURE__ */ jsx3(PageImg, { media }) : /* @__PURE__ */ jsx3(BackdropVideo, { media }) }),
336
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title", children: section.slots.title }),
337
+ /* @__PURE__ */ jsxs2("figure", { className: "squisq-page-quote-figure", style: { margin: 0 }, children: [
338
+ /* @__PURE__ */ jsx3("blockquote", { className: "squisq-page-quote", children: section.slots.body?.text }),
339
+ section.slots.attribution && /* @__PURE__ */ jsx3("figcaption", { className: "squisq-page-quote-attribution", children: section.slots.attribution })
340
+ ] })
341
+ ] });
342
+ }
343
+ function FeatureSplitSection({
344
+ section,
345
+ flip
346
+ }) {
347
+ const media = section.slots.media;
348
+ const image = media?.type === "image" ? media : void 0;
349
+ const mediaRight = flip ? section.variant !== "media-right" : section.variant === "media-right";
350
+ return /* @__PURE__ */ jsxs2("div", { className: `squisq-page-feature${mediaRight ? " squisq-page-feature--media-right" : ""}`, children: [
351
+ /* @__PURE__ */ jsxs2("div", { className: "squisq-page-feature-media squisq-page-media-frame", children: [
352
+ image && /* @__PURE__ */ jsx3(PageImg, { media: image }),
353
+ /* @__PURE__ */ jsx3(MediaCredit, { media: image })
354
+ ] }),
355
+ /* @__PURE__ */ jsxs2("div", { className: "squisq-page-feature-text", children: [
356
+ /* @__PURE__ */ jsx3(Eyebrow, { section }),
357
+ section.slots.title && /* @__PURE__ */ jsx3("h3", { className: "squisq-page-section-title squisq-page-feature-title", children: section.slots.title }),
358
+ section.slots.body?.text && /* @__PURE__ */ jsx3("p", { className: "squisq-page-feature-body", children: section.slots.body.text })
359
+ ] })
360
+ ] });
361
+ }
362
+ function MediaFigureSection({ section }) {
363
+ const { basePath } = usePageView();
364
+ const media = section.slots.media;
365
+ if (!media || media.type === "canvas") return null;
366
+ return /* @__PURE__ */ jsxs2("figure", { className: "squisq-page-figure", children: [
367
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-media-frame", children: media.type === "image" ? /* @__PURE__ */ jsx3(PageImg, { media }) : /* @__PURE__ */ jsx3(InlineVideoPlayer, { src: media.src, poster: media.posterSrc, basePath }) }),
368
+ (section.slots.caption || media.credit) && /* @__PURE__ */ jsxs2("figcaption", { children: [
369
+ section.slots.caption,
370
+ media.credit && /* @__PURE__ */ jsxs2(Fragment, { children: [
371
+ section.slots.caption ? " " : "",
372
+ /* @__PURE__ */ jsx3(MediaCredit, { media })
373
+ ] })
374
+ ] })
375
+ ] });
376
+ }
377
+ function GallerySection({ section }) {
378
+ const items = section.slots.items ?? [];
379
+ if (items.length === 0) {
380
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-gallery-empty", children: [
381
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title", children: section.slots.title }),
382
+ section.slots.body?.text && /* @__PURE__ */ jsx3("p", { children: section.slots.body.text }),
383
+ section.slots.caption && /* @__PURE__ */ jsx3("div", { className: "squisq-page-gallery-caption", children: section.slots.caption })
384
+ ] });
385
+ }
386
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
387
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-gallery", children: items.map(
388
+ (item, i) => item.media?.type === "image" ? /* @__PURE__ */ jsx3("div", { className: "squisq-page-media-frame", children: /* @__PURE__ */ jsx3(PageImg, { media: item.media }) }, i) : null
389
+ ) }),
390
+ section.slots.caption && /* @__PURE__ */ jsx3("div", { className: "squisq-page-gallery-caption", children: section.slots.caption })
391
+ ] });
392
+ }
393
+ function CalloutSection({ section }) {
394
+ const media = section.slots.media;
395
+ const image = media?.type === "image" ? media : void 0;
396
+ const framing = section.hints?.framing;
397
+ return /* @__PURE__ */ jsxs2(
398
+ "div",
399
+ {
400
+ className: "squisq-page-callout",
401
+ "data-hint-framing": typeof framing === "string" ? framing : void 0,
402
+ children: [
403
+ image && /* @__PURE__ */ jsx3("div", { className: "squisq-page-media-frame", children: /* @__PURE__ */ jsx3(PageImg, { media: image }) }),
404
+ /* @__PURE__ */ jsxs2("div", { children: [
405
+ /* @__PURE__ */ jsx3(Eyebrow, { section }),
406
+ section.slots.title && /* @__PURE__ */ jsx3("h3", { className: "squisq-page-callout-title", children: section.slots.title }),
407
+ section.slots.body?.text && /* @__PURE__ */ jsx3("p", { className: "squisq-page-callout-body", children: section.slots.body.text }),
408
+ section.slots.meta && /* @__PURE__ */ jsx3("div", { className: "squisq-page-callout-meta", children: section.slots.meta })
409
+ ] })
410
+ ]
411
+ }
412
+ );
413
+ }
414
+ function CardGridSection({ section }) {
415
+ const { theme } = usePageView();
416
+ const items = section.slots.items ?? [];
417
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
418
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-cards-title", children: section.slots.title }),
419
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-cards", children: items.map((item, i) => {
420
+ const scheme = item.accentScheme ? theme.colorSchemes?.[item.accentScheme] : void 0;
421
+ return /* @__PURE__ */ jsxs2(
422
+ "div",
423
+ {
424
+ className: "squisq-page-card",
425
+ style: scheme ? { "--squisq-page-card-accent": scheme.accent } : void 0,
426
+ children: [
427
+ item.title && /* @__PURE__ */ jsx3("h3", { className: "squisq-page-card-title", children: item.title }),
428
+ item.body && /* @__PURE__ */ jsx3("p", { className: "squisq-page-card-body", children: item.body })
429
+ ]
430
+ },
431
+ i
432
+ );
433
+ }) })
434
+ ] });
435
+ }
436
+ function ItemListSection({ section }) {
437
+ const items = section.slots.items ?? [];
438
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
439
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-items-title", children: section.slots.title }),
440
+ /* @__PURE__ */ jsx3("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx3("li", { children: item.body }, i)) })
441
+ ] });
442
+ }
443
+ function TimelineRailSection({ section }) {
444
+ const milestone = section.slots.milestone;
445
+ if (!milestone) return null;
446
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-milestone", "data-mood": milestone.mood, children: [
447
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-milestone-date", children: milestone.date }),
448
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-milestone-rail", "aria-hidden": "true" }),
449
+ /* @__PURE__ */ jsxs2("div", { className: "squisq-page-milestone-body", children: [
450
+ milestone.description,
451
+ milestone.footer && /* @__PURE__ */ jsx3("div", { className: "squisq-page-milestone-footer", children: milestone.footer })
452
+ ] })
453
+ ] });
454
+ }
455
+ function TableSection({ section }) {
456
+ const table = section.slots.table;
457
+ if (!table) return null;
458
+ const alignFor = (i) => table.align?.[i] ?? void 0;
459
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
460
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-table-title", children: section.slots.title }),
461
+ /* @__PURE__ */ jsx3("div", { className: "squisq-page-table-scroll", children: /* @__PURE__ */ jsxs2("table", { className: "squisq-page-table", children: [
462
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: table.headers.map((header, i) => /* @__PURE__ */ jsx3("th", { style: { textAlign: alignFor(i) }, children: header }, i)) }) }),
463
+ /* @__PURE__ */ jsx3("tbody", { children: table.rows.map((row, r) => /* @__PURE__ */ jsx3("tr", { children: row.map((cell, c) => /* @__PURE__ */ jsx3("td", { style: { textAlign: alignFor(c) }, children: cell }, c)) }, r)) })
464
+ ] }) })
465
+ ] });
466
+ }
467
+ function ProseSection({ section, block }) {
468
+ const { theme } = usePageView();
469
+ const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx3(MarkdownRenderer, { nodes: [block.sourceHeading], theme }) : null;
470
+ const bodyNodes = section.slots.body?.markdown ?? block?.contents;
471
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-prose", children: [
472
+ heading,
473
+ bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx3(MarkdownRenderer, { nodes: bodyNodes, theme })
474
+ ] });
475
+ }
476
+ function FooterSection({ section }) {
477
+ return /* @__PURE__ */ jsxs2("div", { children: [
478
+ /* @__PURE__ */ jsx3(Eyebrow, { section }),
479
+ section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title", children: section.slots.title }),
480
+ section.slots.body?.text && /* @__PURE__ */ jsx3("p", { children: section.slots.body.text })
481
+ ] });
482
+ }
483
+
484
+ // src/page/PageSectionView.tsx
485
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
486
+ function heroHasBackdrop(section, heroStyle) {
487
+ return section.kind === "hero" && section.background === "media" && section.slots.media?.type === "image" && heroStyle !== "split";
488
+ }
489
+ function accentVars(section) {
490
+ const scheme = section.accent.scheme;
491
+ if (!scheme) return void 0;
492
+ return {
493
+ "--squisq-page-accent": scheme.accent,
494
+ "--squisq-page-accent-bg": scheme.bg,
495
+ "--squisq-page-accent-text": scheme.text
496
+ };
497
+ }
498
+ function sectionBody(entry, featureFlip) {
499
+ const { section, block } = entry;
500
+ switch (section.kind) {
501
+ case "hero":
502
+ return /* @__PURE__ */ jsx4(HeroSection, { section, block });
503
+ case "banner":
504
+ return /* @__PURE__ */ jsx4(BannerSection, { section, block });
505
+ case "stat-band":
506
+ return /* @__PURE__ */ jsx4(StatBandSection, { section, block });
507
+ case "quote-band":
508
+ return /* @__PURE__ */ jsx4(QuoteBandSection, { section, block });
509
+ case "feature-split":
510
+ return /* @__PURE__ */ jsx4(FeatureSplitSection, { section, block, flip: featureFlip });
511
+ case "media-figure":
512
+ return /* @__PURE__ */ jsx4(MediaFigureSection, { section, block });
513
+ case "gallery":
514
+ return /* @__PURE__ */ jsx4(GallerySection, { section, block });
515
+ case "callout":
516
+ return /* @__PURE__ */ jsx4(CalloutSection, { section, block });
517
+ case "card-grid":
518
+ return /* @__PURE__ */ jsx4(CardGridSection, { section, block });
519
+ case "item-list":
520
+ return /* @__PURE__ */ jsx4(ItemListSection, { section, block });
521
+ case "timeline-rail":
522
+ return /* @__PURE__ */ jsx4(TimelineRailSection, { section, block });
523
+ case "table-section":
524
+ return /* @__PURE__ */ jsx4(TableSection, { section, block });
525
+ case "canvas-embed":
526
+ return /* @__PURE__ */ jsx4(CanvasSection, { section });
527
+ case "footer":
528
+ return /* @__PURE__ */ jsx4(FooterSection, { section, block });
529
+ case "prose":
530
+ default:
531
+ return /* @__PURE__ */ jsx4(ProseSection, { section, block });
532
+ }
533
+ }
534
+ function PageSectionView({ entry, featureFlip, isLeadProse }) {
535
+ const { pageStyle, theme } = usePageView();
536
+ const { section } = entry;
537
+ const richContent = section.slots.richContent?.markdown;
538
+ const classes = [
539
+ "squisq-linear-section",
540
+ "squisq-page-section",
541
+ `squisq-page-section--${section.kind}`,
542
+ `squisq-page-section--bg-${section.background}`,
543
+ `squisq-page-section--em-${section.emphasis}`
544
+ ];
545
+ if (section.variant) classes.push(`squisq-page-section--v-${section.variant}`);
546
+ if (heroHasBackdrop(section, pageStyle.tokens.heroStyle)) {
547
+ classes.push("squisq-page-section--hero-media");
548
+ }
549
+ const hintAttrs = {};
550
+ if (section.hints?.vignette) hintAttrs["data-hint-vignette"] = "";
551
+ if (section.hints?.dropCap && isLeadProse) hintAttrs["data-hint-drop-cap"] = "";
552
+ return /* @__PURE__ */ jsx4(
553
+ "section",
554
+ {
555
+ className: classes.join(" "),
556
+ "data-section-kind": section.kind,
557
+ "data-block-id": section.blockId,
558
+ "data-block-index": section.index,
559
+ "data-template": section.template,
560
+ style: accentVars(section),
561
+ ...hintAttrs,
562
+ children: /* @__PURE__ */ jsxs3("div", { className: "squisq-page-section-inner", children: [
563
+ sectionBody(entry, featureFlip),
564
+ richContent && richContent.length > 0 && /* @__PURE__ */ jsx4("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx4(MarkdownRenderer, { nodes: richContent, theme }) })
565
+ ] })
566
+ }
567
+ );
568
+ }
569
+
570
+ // src/LinearDocView.tsx
571
+ import { useEffect, useMemo as useMemo2, useRef } from "react";
572
+ import {
573
+ applySurface,
574
+ resolveFontFamily
575
+ } from "@bendyline/squisq/schemas";
576
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS2 } from "@bendyline/squisq/schemas";
577
+ import {
578
+ markdownToDoc,
579
+ materializePageSections,
580
+ resolvePageStyle as resolvePageStyle2,
581
+ buildPageCssVars,
582
+ pageStyleDataAttributes,
583
+ PAGE_BASE_CSS,
584
+ DEFAULT_THEME as DEFAULT_THEME2
585
+ } from "@bendyline/squisq/doc";
586
+ import { parseMarkdown } from "@bendyline/squisq/markdown";
587
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
588
+ function LinearDocView({
589
+ doc,
590
+ markdown,
591
+ basePath = "/",
592
+ viewport,
593
+ className,
594
+ theme,
595
+ surface,
596
+ animationsEnabled = true,
597
+ thinMargins = false,
598
+ imageDisplayMode = "inline",
599
+ globalKeyboardShortcuts = false,
600
+ showCover = true,
601
+ transformPage
602
+ }) {
603
+ const scrollRef = useRef(null);
604
+ const activeViewport = viewport ?? VIEWPORT_PRESETS2.landscape;
605
+ const markdownDoc = useMemo2(
606
+ () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
607
+ [doc, markdown]
608
+ );
609
+ const resolvedDoc = doc ?? markdownDoc;
610
+ const autoSurface = useAutoSurface(surface === "auto");
611
+ const resolvedSurface = surface === "auto" ? autoSurface : surface;
612
+ const activeTheme = useMemo2(() => {
613
+ const baseTheme = theme ?? DEFAULT_THEME2;
614
+ return resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
615
+ }, [theme, resolvedSurface]);
616
+ const pageStyle = useMemo2(
617
+ () => resolvePageStyle2(activeTheme, transformPage),
618
+ [activeTheme, transformPage]
619
+ );
620
+ const sections = useMemo2(() => {
621
+ if (!resolvedDoc) return [];
622
+ return materializePageSections(resolvedDoc, {
623
+ theme: activeTheme,
624
+ viewport: activeViewport,
625
+ customTemplates: resolvedDoc.customTemplates,
626
+ cover: showCover !== false ? resolvedDoc.startBlock : false,
627
+ transformPage
628
+ });
629
+ }, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage]);
630
+ const renderContext = useMemo2(
631
+ () => ({
632
+ theme: activeTheme,
633
+ viewport: activeViewport,
634
+ totalBlocks: sections.length,
635
+ persistentLayers: activeTheme.persistentLayers,
636
+ customTemplates: resolvedDoc?.customTemplates
637
+ }),
638
+ [activeTheme, activeViewport, sections.length, resolvedDoc?.customTemplates]
639
+ );
640
+ const pageContext = useMemo2(
641
+ () => ({
642
+ theme: activeTheme,
643
+ pageStyle,
644
+ basePath,
645
+ viewport: activeViewport,
646
+ renderContext,
647
+ animationsEnabled,
648
+ imageDisplayMode
649
+ }),
650
+ [
651
+ activeTheme,
652
+ pageStyle,
653
+ basePath,
654
+ activeViewport,
655
+ renderContext,
656
+ animationsEnabled,
657
+ imageDisplayMode
658
+ ]
659
+ );
660
+ const { featureOrdinals, leadProseIndex } = useMemo2(() => {
661
+ const ordinals = /* @__PURE__ */ new Map();
662
+ let featureCount = 0;
663
+ let firstProse = -1;
664
+ for (const entry of sections) {
665
+ if (entry.section.kind === "feature-split") {
666
+ ordinals.set(entry.section.index, featureCount++);
667
+ }
668
+ if (firstProse < 0 && entry.section.kind === "prose") {
669
+ firstProse = entry.section.index;
670
+ }
671
+ }
672
+ return { featureOrdinals: ordinals, leadProseIndex: firstProse };
673
+ }, [sections]);
674
+ useEffect(() => {
675
+ if (!globalKeyboardShortcuts) return;
676
+ const handleKeyDown = (event) => {
677
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.key !== "ArrowDown" && event.key !== "ArrowUp") {
678
+ return;
679
+ }
680
+ const target = event.target instanceof Element ? event.target : null;
681
+ if (target?.closest(
682
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor'
683
+ )) {
684
+ return;
685
+ }
686
+ const scroller = scrollRef.current;
687
+ if (!scroller) return;
688
+ event.preventDefault();
689
+ const distance = Math.max(64, Math.round(scroller.clientHeight * 0.12));
690
+ scroller.scrollBy({
691
+ top: event.key === "ArrowDown" ? distance : -distance,
692
+ behavior: "smooth"
693
+ });
694
+ };
695
+ document.addEventListener("keydown", handleKeyDown);
696
+ return () => document.removeEventListener("keydown", handleKeyDown);
697
+ }, [globalKeyboardShortcuts]);
698
+ if (!resolvedDoc) {
699
+ return /* @__PURE__ */ jsx5("div", { ref: scrollRef, className: `squisq-linear squisq-linear--empty ${className || ""}` });
700
+ }
701
+ const bgColor = activeTheme.colors.background;
702
+ const textColor = activeTheme.colors.text;
703
+ const mutedColor = activeTheme.colors.textMuted;
704
+ const primaryColor = activeTheme.colors.primary;
705
+ const bodyFont = resolveFontFamily(activeTheme.typography.bodyFont, "system-ui, sans-serif");
706
+ const titleFont = resolveFontFamily(activeTheme.typography.titleFont, "Georgia, serif");
707
+ const lineHt = activeTheme.typography.lineHeight ?? 1.65;
708
+ const contentClasses = [
709
+ "squisq-linear-content",
710
+ "squisq-md",
711
+ "squisq-page",
712
+ thinMargins ? "squisq-linear-content--thin squisq-page--thin" : "",
713
+ imageDisplayMode === "thumbnail" ? "squisq-linear-content--thumbnail-images squisq-page--thumbnail-images" : ""
714
+ ].filter(Boolean).join(" ");
715
+ return /* @__PURE__ */ jsx5(
716
+ "div",
717
+ {
718
+ ref: scrollRef,
719
+ className: `squisq-linear ${className || ""}`,
720
+ role: "region",
721
+ "aria-label": "Document page",
722
+ tabIndex: thinMargins ? void 0 : 0,
723
+ style: {
724
+ width: "100%",
725
+ // Thin-margins mode is the "embedded in someone else's container"
726
+ // signal (chat bubble, sidebar preview). Fit to content there so
727
+ // the host's bubble doesn't render a tall empty box when the doc
728
+ // is short. Standalone mode keeps height:100% for full-viewport
729
+ // scrolling.
730
+ height: thinMargins ? "auto" : "100%",
731
+ overflowY: thinMargins ? "visible" : "auto",
732
+ overflowX: "hidden",
733
+ background: bgColor,
734
+ // Container queries drive the responsive page layout — the page
735
+ // often lives inside editor panes / chat bubbles where viewport
736
+ // media queries lie about the available width.
737
+ containerType: "inline-size",
738
+ containerName: "squisq-page"
739
+ },
740
+ children: /* @__PURE__ */ jsxs4(
741
+ "div",
742
+ {
743
+ className: contentClasses,
744
+ ...pageStyleDataAttributes(pageStyle),
745
+ style: {
746
+ lineHeight: lineHt,
747
+ fontSize: "16px",
748
+ fontFamily: bodyFont,
749
+ color: textColor,
750
+ ...buildPageCssVars(activeTheme, pageStyle),
751
+ // Legacy custom properties kept for one release so host CSS
752
+ // and MarkdownRenderer content keyed on them keep working.
753
+ "--squisq-linear-title-font": titleFont,
754
+ "--squisq-linear-body-font": bodyFont,
755
+ "--squisq-linear-text": textColor,
756
+ "--squisq-linear-muted": mutedColor,
757
+ "--squisq-linear-primary": primaryColor,
758
+ "--squisq-linear-bg": bgColor
759
+ },
760
+ children: [
761
+ /* @__PURE__ */ jsx5("style", { "data-squisq-page": "", children: PAGE_BASE_CSS }),
762
+ /* @__PURE__ */ jsx5(PageViewContext.Provider, { value: pageContext, children: sections.map((entry) => {
763
+ const ordinal = featureOrdinals.get(entry.section.index) ?? 0;
764
+ const flip = !!entry.section.hints?.alternate && ordinal % 2 === 1;
765
+ return /* @__PURE__ */ jsx5(
766
+ PageSectionView,
767
+ {
768
+ entry,
769
+ featureFlip: flip,
770
+ isLeadProse: entry.section.index === leadProseIndex
771
+ },
772
+ `${entry.section.blockId}-${entry.section.index}`
773
+ );
774
+ }) })
775
+ ]
776
+ }
777
+ )
778
+ }
779
+ );
780
+ }
781
+
782
+ export {
783
+ BlockRenderer,
784
+ PageViewContext,
785
+ usePageView,
786
+ CanvasSection,
787
+ PageSectionView,
788
+ LinearDocView
789
+ };