@bendyline/squisq-react 2.11.4 → 2.11.6

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,240 @@
1
+ import {
2
+ LinearDocView
3
+ } from "./chunk-5UGIE2QO.js";
4
+
5
+ // src/PaginatedDocView.tsx
6
+ import {
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState
12
+ } from "react";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ function PaginatedDocView({
15
+ doc,
16
+ pages,
17
+ linearViewProps,
18
+ className = "",
19
+ scrollerClassName = "",
20
+ pageClassName = "",
21
+ previousButtonClassName = "",
22
+ nextButtonClassName = "",
23
+ indicatorClassName = "",
24
+ indicatorTextClassName = "",
25
+ renderPreviousIcon,
26
+ renderNextIcon,
27
+ ariaLabel = "Document pages",
28
+ initialPage = 0,
29
+ focusOnMount = true,
30
+ einkMode = false,
31
+ onPageChange,
32
+ style,
33
+ scrollerStyle,
34
+ pageStyle: pageStyleOverride
35
+ }) {
36
+ const resolvedPages = useMemo(() => {
37
+ if (pages) return pages;
38
+ if (!doc) return [];
39
+ return doc.blocks.map((block, index) => ({
40
+ key: block.id || `page-${index}`,
41
+ label: block.sourceHeading ? `Document section ${index + 1}` : `Document page ${index + 1}`,
42
+ content: /* @__PURE__ */ jsx(
43
+ LinearDocView,
44
+ {
45
+ ...linearViewProps,
46
+ doc: {
47
+ ...doc,
48
+ blocks: [block],
49
+ startBlock: index === 0 ? doc.startBlock : void 0
50
+ },
51
+ thinMargins: linearViewProps?.thinMargins ?? true
52
+ }
53
+ )
54
+ }));
55
+ }, [doc, linearViewProps, pages]);
56
+ const lastPage = Math.max(0, resolvedPages.length - 1);
57
+ const [currentPage, setCurrentPage] = useState(
58
+ () => Math.min(Math.max(0, initialPage), lastPage)
59
+ );
60
+ const scrollRef = useRef(null);
61
+ const pendingPageRef = useRef(null);
62
+ const pendingPageTimeoutRef = useRef(null);
63
+ const commitPage = useCallback(
64
+ (next) => {
65
+ setCurrentPage((current) => {
66
+ if (current !== next) onPageChange?.(next);
67
+ return next;
68
+ });
69
+ },
70
+ [onPageChange]
71
+ );
72
+ const updatePage = useCallback(
73
+ (index) => {
74
+ const next = Math.min(Math.max(0, index), lastPage);
75
+ pendingPageRef.current = next;
76
+ if (pendingPageTimeoutRef.current !== null) {
77
+ clearTimeout(pendingPageTimeoutRef.current);
78
+ }
79
+ pendingPageTimeoutRef.current = setTimeout(() => {
80
+ pendingPageRef.current = null;
81
+ pendingPageTimeoutRef.current = null;
82
+ commitPage(next);
83
+ }, 750);
84
+ const container = scrollRef.current;
85
+ const pageNodes = container?.querySelectorAll(":scope > .squisq-paginated-page");
86
+ const target = pageNodes?.[next];
87
+ if (container && target) {
88
+ const origin = pageNodes?.[0]?.offsetLeft ?? 0;
89
+ container.scrollTo({
90
+ left: target.offsetLeft - origin,
91
+ behavior: einkMode ? "auto" : "smooth"
92
+ });
93
+ }
94
+ },
95
+ [commitPage, einkMode, lastPage]
96
+ );
97
+ useEffect(() => {
98
+ const container = scrollRef.current;
99
+ if (!container) return;
100
+ const initial = Math.min(Math.max(0, initialPage), lastPage);
101
+ pendingPageRef.current = null;
102
+ container.scrollLeft = initial * container.clientWidth;
103
+ setCurrentPage(initial);
104
+ if (focusOnMount) container.focus({ preventScroll: true });
105
+ }, [focusOnMount, initialPage, lastPage, resolvedPages]);
106
+ useEffect(
107
+ () => () => {
108
+ if (pendingPageTimeoutRef.current !== null) {
109
+ clearTimeout(pendingPageTimeoutRef.current);
110
+ }
111
+ },
112
+ []
113
+ );
114
+ useEffect(() => {
115
+ const container = scrollRef.current;
116
+ if (!container) return;
117
+ let frame = 0;
118
+ const onScroll = () => {
119
+ cancelAnimationFrame(frame);
120
+ frame = requestAnimationFrame(() => {
121
+ if (container.clientWidth <= 0) return;
122
+ const pageNodes = Array.from(
123
+ container.querySelectorAll(":scope > .squisq-paginated-page")
124
+ );
125
+ const origin = pageNodes[0]?.offsetLeft ?? 0;
126
+ const pendingPage = pendingPageRef.current;
127
+ if (pendingPage !== null) {
128
+ const pendingTarget = pageNodes[pendingPage];
129
+ const pendingLeft = pendingTarget ? pendingTarget.offsetLeft - origin : 0;
130
+ if (Math.abs(container.scrollLeft - pendingLeft) > 1) return;
131
+ pendingPageRef.current = null;
132
+ if (pendingPageTimeoutRef.current !== null) {
133
+ clearTimeout(pendingPageTimeoutRef.current);
134
+ pendingPageTimeoutRef.current = null;
135
+ }
136
+ }
137
+ const next = pageNodes.reduce(
138
+ (nearest, page, index) => Math.abs(container.scrollLeft - (page.offsetLeft - origin)) < Math.abs(container.scrollLeft - (pageNodes[nearest]?.offsetLeft ?? origin) + origin) ? index : nearest,
139
+ 0
140
+ );
141
+ commitPage(next);
142
+ });
143
+ };
144
+ container.addEventListener("scroll", onScroll, { passive: true });
145
+ return () => {
146
+ cancelAnimationFrame(frame);
147
+ container.removeEventListener("scroll", onScroll);
148
+ };
149
+ }, [commitPage, lastPage]);
150
+ const onKeyDown = (event) => {
151
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
152
+ event.preventDefault();
153
+ updatePage((pendingPageRef.current ?? currentPage) + 1);
154
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
155
+ event.preventDefault();
156
+ updatePage((pendingPageRef.current ?? currentPage) - 1);
157
+ }
158
+ };
159
+ const outerStyle = { position: "relative", width: "100%", height: "100%" };
160
+ const scrollStyle = {
161
+ display: "flex",
162
+ width: "100%",
163
+ height: "100%",
164
+ overflowX: "auto",
165
+ overflowY: "hidden",
166
+ scrollSnapType: "x mandatory"
167
+ };
168
+ const pageStyle = {
169
+ flex: "0 0 100%",
170
+ minWidth: "100%",
171
+ height: "100%",
172
+ overflowY: "auto",
173
+ overflowX: "hidden",
174
+ scrollSnapAlign: "start",
175
+ scrollSnapStop: "always",
176
+ boxSizing: "border-box"
177
+ };
178
+ return /* @__PURE__ */ jsxs("div", { className: `squisq-paginated ${className}`.trim(), style: { ...outerStyle, ...style }, children: [
179
+ /* @__PURE__ */ jsx(
180
+ "div",
181
+ {
182
+ ref: scrollRef,
183
+ className: `squisq-paginated-scroll ${scrollerClassName}`.trim(),
184
+ style: { ...scrollStyle, ...scrollerStyle },
185
+ tabIndex: 0,
186
+ role: "region",
187
+ "aria-label": ariaLabel,
188
+ "aria-roledescription": "paginated reader",
189
+ onKeyDown,
190
+ children: resolvedPages.map((page) => /* @__PURE__ */ jsx(
191
+ "div",
192
+ {
193
+ className: `squisq-paginated-page ${pageClassName} ${page.className ?? ""}`.trim(),
194
+ style: { ...pageStyle, ...pageStyleOverride },
195
+ role: "group",
196
+ "aria-label": page.label,
197
+ children: page.content
198
+ },
199
+ page.key
200
+ ))
201
+ }
202
+ ),
203
+ currentPage > 0 && /* @__PURE__ */ jsx(
204
+ "button",
205
+ {
206
+ type: "button",
207
+ className: `squisq-paginated-nav squisq-paginated-nav--previous ${previousButtonClassName}`.trim(),
208
+ "aria-label": "Previous page",
209
+ onClick: () => updatePage((pendingPageRef.current ?? currentPage) - 1),
210
+ children: renderPreviousIcon?.() ?? "\u2039"
211
+ }
212
+ ),
213
+ currentPage < lastPage && /* @__PURE__ */ jsx(
214
+ "button",
215
+ {
216
+ type: "button",
217
+ className: `squisq-paginated-nav squisq-paginated-nav--next ${nextButtonClassName}`.trim(),
218
+ "aria-label": "Next page",
219
+ onClick: () => updatePage((pendingPageRef.current ?? currentPage) + 1),
220
+ children: renderNextIcon?.() ?? "\u203A"
221
+ }
222
+ ),
223
+ resolvedPages.length > 0 && /* @__PURE__ */ jsx(
224
+ "div",
225
+ {
226
+ className: `squisq-paginated-indicator ${indicatorClassName}`.trim(),
227
+ "aria-live": "polite",
228
+ children: /* @__PURE__ */ jsxs("span", { className: indicatorTextClassName, children: [
229
+ currentPage + 1,
230
+ " / ",
231
+ resolvedPages.length
232
+ ] })
233
+ }
234
+ )
235
+ ] });
236
+ }
237
+
238
+ export {
239
+ PaginatedDocView
240
+ };
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { getAnimationStyle, getTransitionClass } from '@bendyline/squisq/doc';
6
6
  import { C as CodeBlockCopyHandler } from './MarkdownRenderer-CC9dOc6b.js';
7
7
  export { a as CodeBlockCopyContext, M as MarkdownRenderer, b as MarkdownRendererProps } from './MarkdownRenderer-CC9dOc6b.js';
8
8
  export { FenceRendererContext, MermaidDiagram, MermaidDiagramProps, useFenceRenderers } from './markdown/index.js';
9
- export { CanvasSection, CanvasSectionProps, ImageDisplayMode, LinearDocView, LinearDocViewProps, PageSectionView, PageSectionViewProps, PageViewContext, PageViewContextValue, usePageView } from './page/index.js';
9
+ export { CanvasSection, CanvasSectionProps, ImageDisplayMode, LinearDocView, LinearDocViewProps, PageSectionView, PageSectionViewProps, PageViewContext, PageViewContextValue, PaginatedDocPage, PaginatedDocView, PaginatedDocViewProps, usePageView } from './page/index.js';
10
10
  import * as react_jsx_runtime from 'react/jsx-runtime';
11
11
  export { ImageLayer, MapLayer, MermaidLayer, PathLayer, ShapeLayer, TextLayer, TreeLayer, VideoLayer } from './layers/index.js';
12
12
  export { T as TableLayer, a as TableLayerContentRenderer, b as TableLayerContentRendererProps } from './TableLayer-B7ADuUGc.js';
package/dist/index.js CHANGED
@@ -16,6 +16,9 @@ import {
16
16
  resolveDocPlayerAppearance,
17
17
  useMediaClipDurations
18
18
  } from "./chunk-KGWT454C.js";
19
+ import {
20
+ PaginatedDocView
21
+ } from "./chunk-7QA6EW6Y.js";
19
22
  import {
20
23
  BlockRenderer,
21
24
  CanvasSection,
@@ -98,6 +101,7 @@ export {
98
101
  MermaidLayer,
99
102
  PageSectionView,
100
103
  PageViewContext,
104
+ PaginatedDocView,
101
105
  PathLayer,
102
106
  ResourcePolicyContext,
103
107
  ShapeLayer,
@@ -6,6 +6,7 @@ import { FenceRendererMap } from '@bendyline/squisq/fence';
6
6
  import { C as CodeBlockCopyHandler } from '../MarkdownRenderer-CC9dOc6b.js';
7
7
  export { a as CodeBlockCopyContext } from '../MarkdownRenderer-CC9dOc6b.js';
8
8
  import * as react from 'react';
9
+ import { ReactNode, CSSProperties } from 'react';
9
10
 
10
11
  interface LinearDocViewProps {
11
12
  /**
@@ -95,6 +96,40 @@ type ImageDisplayMode = 'inline' | 'thumbnail';
95
96
  */
96
97
  declare function LinearDocView({ doc, markdown, basePath, viewport, className, theme, surface, animationsEnabled, thinMargins, imageDisplayMode, globalKeyboardShortcuts, showCover, transformPage, showCodeCopyButton, onCopyCode, fenceRenderers, htmlPolicy, linkSchemes, }: LinearDocViewProps): react_jsx_runtime.JSX.Element;
97
98
 
99
+ interface PaginatedDocPage {
100
+ key: string;
101
+ label: string;
102
+ /** React or react-compatible host node (for example Preact compat). */
103
+ content: ReactNode | object | bigint;
104
+ className?: string;
105
+ }
106
+ interface PaginatedDocViewProps {
107
+ /** A Doc to split into one page per top-level block. */
108
+ doc?: Doc;
109
+ /** Host-composed pages. Wins over doc when supplied. */
110
+ pages?: readonly PaginatedDocPage[];
111
+ /** Props applied to Doc pages rendered with LinearDocView. */
112
+ linearViewProps?: Omit<LinearDocViewProps, 'doc'>;
113
+ className?: string;
114
+ scrollerClassName?: string;
115
+ pageClassName?: string;
116
+ previousButtonClassName?: string;
117
+ nextButtonClassName?: string;
118
+ indicatorClassName?: string;
119
+ indicatorTextClassName?: string;
120
+ renderPreviousIcon?: () => ReactNode | object | bigint;
121
+ renderNextIcon?: () => ReactNode | object | bigint;
122
+ ariaLabel?: string;
123
+ initialPage?: number;
124
+ focusOnMount?: boolean;
125
+ einkMode?: boolean;
126
+ onPageChange?: (pageIndex: number) => void;
127
+ style?: CSSProperties;
128
+ scrollerStyle?: CSSProperties;
129
+ pageStyle?: CSSProperties;
130
+ }
131
+ declare function PaginatedDocView({ doc, pages, linearViewProps, className, scrollerClassName, pageClassName, previousButtonClassName, nextButtonClassName, indicatorClassName, indicatorTextClassName, renderPreviousIcon, renderNextIcon, ariaLabel, initialPage, focusOnMount, einkMode, onPageChange, style, scrollerStyle, pageStyle: pageStyleOverride, }: PaginatedDocViewProps): react_jsx_runtime.JSX.Element;
132
+
98
133
  interface PageSectionViewProps {
99
134
  entry: PageSectionMaterialization;
100
135
  /** Alternation flip for feature-split sections (theme `alternate` hint). */
@@ -130,4 +165,4 @@ interface PageViewContextValue {
130
165
  declare const PageViewContext: react.Context<PageViewContextValue>;
131
166
  declare function usePageView(): PageViewContextValue;
132
167
 
133
- export { CanvasSection, type CanvasSectionProps, CodeBlockCopyHandler, type ImageDisplayMode, LinearDocView, type LinearDocViewProps, PageSectionView, type PageSectionViewProps, PageViewContext, type PageViewContextValue, usePageView };
168
+ export { CanvasSection, type CanvasSectionProps, CodeBlockCopyHandler, type ImageDisplayMode, LinearDocView, type LinearDocViewProps, PageSectionView, type PageSectionViewProps, PageViewContext, type PageViewContextValue, type PaginatedDocPage, PaginatedDocView, type PaginatedDocViewProps, usePageView };
@@ -1,3 +1,6 @@
1
+ import {
2
+ PaginatedDocView
3
+ } from "../chunk-7QA6EW6Y.js";
1
4
  import {
2
5
  CanvasSection,
3
6
  LinearDocView,
@@ -15,5 +18,6 @@ export {
15
18
  LinearDocView,
16
19
  PageSectionView,
17
20
  PageViewContext,
21
+ PaginatedDocView,
18
22
  usePageView
19
23
  };