@marianmeres/stuic 3.31.1 → 3.32.1

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.
@@ -741,6 +741,7 @@
741
741
  <!-- Sheets (3D flippable elements) — no static pages needed -->
742
742
  {#each sheets as sheet (sheet.id)}
743
743
  {@const flipped = sheet.id < activeSpread}
744
+ {@const isNearby = Math.abs(sheet.id - activeSpread) <= 2}
744
745
  <div
745
746
  class={twMerge(!unstyled && "stuic-book-sheet")}
746
747
  data-flipped={flipped ? "true" : undefined}
@@ -755,7 +756,7 @@
755
756
  data-placeholder={!sheet.frontPage && sheet.backPage ? "" : undefined}
756
757
  onclick={(e) => handlePageClick(e, sheet.frontPage)}
757
758
  >
758
- {#if sheet.frontPage}
759
+ {#if isNearby && sheet.frontPage}
759
760
  {#if renderPage}
760
761
  {@render renderPage({
761
762
  page: sheet.frontPage,
@@ -844,7 +845,7 @@
844
845
  data-placeholder={!sheet.backPage && sheet.frontPage ? "" : undefined}
845
846
  onclick={(e) => handlePageClick(e, sheet.backPage)}
846
847
  >
847
- {#if sheet.backPage}
848
+ {#if isNearby && sheet.backPage}
848
849
  {#if renderPage}
849
850
  {@render renderPage({
850
851
  page: sheet.backPage,
@@ -0,0 +1,138 @@
1
+ <script lang="ts" module>
2
+ import type { Props as BookProps } from "./Book.svelte";
3
+
4
+ export interface Props extends Omit<BookProps, "responsive" | "singlePage"> {
5
+ /** Minimum page width (px) before switching to single-page mode (default: 150) */
6
+ minPageWidth?: number;
7
+ /** Resize debounce delay in ms (default: 150) */
8
+ debounce?: number;
9
+ }
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import Book, { computeBookPageSize, type BookPage } from "./Book.svelte";
14
+ import { waitForTwoRepaints } from "../../utils/paint.js";
15
+
16
+ let bookRef: ReturnType<typeof Book> | undefined = $state();
17
+
18
+ let {
19
+ pages,
20
+ minPageWidth = 150,
21
+ debounce: debounceMs = 150,
22
+ activeSpread = $bindable(0),
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ // derive aspect ratio from actual page dimensions
27
+ const ratio = $derived.by(() => {
28
+ const size = computeBookPageSize(pages);
29
+ return size.width / size.height;
30
+ });
31
+
32
+ let containerEl: HTMLDivElement | undefined = $state();
33
+ let pageWidth = $state(0);
34
+ let pageHeight = $state(0);
35
+ let forceSingle = $state(false);
36
+ let resizing = $state(false);
37
+ let measured = $state(false);
38
+
39
+ // ---- Proxy API (safe no-ops during remount) ----
40
+
41
+ export function next() { bookRef?.next(); }
42
+ export function previous() { bookRef?.previous(); }
43
+ export function goTo(spreadIndex: number) { bookRef?.goTo(spreadIndex); }
44
+ export function goToPage(pageId: BookPage["id"]) { bookRef?.goToPage(pageId); }
45
+ export function zoomIn() { bookRef?.zoomIn(); }
46
+ export function zoomOut() { bookRef?.zoomOut(); }
47
+ export function resetZoom() { bookRef?.resetZoom(); }
48
+ export function getCollection() { return bookRef?.getCollection(); }
49
+
50
+ // Dynamically size the book to fill available container space.
51
+ // We own the single/dual page decision (responsive={false}) to avoid feedback loops —
52
+ // the Book's internal responsive detection uses bind:clientWidth which conflicts with
53
+ // our external sizing and can oscillate during flip animations.
54
+ // Uses window resize event instead of ResizeObserver for the same reason.
55
+ $effect(() => {
56
+ if (!containerEl) return;
57
+ let timer: ReturnType<typeof setTimeout>;
58
+
59
+ const apply = () => {
60
+ const cw = containerEl!.clientWidth;
61
+ const ch = containerEl!.clientHeight;
62
+ if (!cw || !ch) return;
63
+
64
+ // suppress width/translate transitions during dimension changes,
65
+ // then restore so flip animations (stage translate) work normally
66
+ resizing = true;
67
+
68
+ // try dual-page first: 2 pages side by side
69
+ const dualHeight = Math.floor(Math.min(ch, cw / 2 / ratio));
70
+ const dualWidth = Math.floor(dualHeight * ratio);
71
+
72
+ if (dualWidth >= minPageWidth) {
73
+ // dual-page mode — pages are large enough
74
+ pageWidth = dualWidth;
75
+ pageHeight = dualHeight;
76
+ forceSingle = false;
77
+ } else {
78
+ // single-page mode — container too narrow for readable dual pages
79
+ const singleHeight = Math.floor(Math.min(ch, cw / ratio));
80
+ pageWidth = Math.floor(singleHeight * ratio);
81
+ pageHeight = singleHeight;
82
+ forceSingle = true;
83
+ }
84
+
85
+ // force-remount Book so 3D context reinitializes with new dimensions
86
+ // (overflow:hidden flattens preserve-3d when dimensions change mid-life)
87
+ measured = false;
88
+ waitForTwoRepaints().then(() => {
89
+ resizing = false;
90
+ measured = true;
91
+ });
92
+ };
93
+
94
+ // initial measurement
95
+ apply();
96
+
97
+ // subsequent resizes — debounced to avoid flickering during drag
98
+ const onResize = () => {
99
+ clearTimeout(timer);
100
+ timer = setTimeout(apply, debounceMs);
101
+ };
102
+ window.addEventListener("resize", onResize);
103
+
104
+ return () => {
105
+ clearTimeout(timer);
106
+ window.removeEventListener("resize", onResize);
107
+ };
108
+ });
109
+ </script>
110
+
111
+ <div
112
+ bind:this={containerEl}
113
+ class="stuic-book-responsive"
114
+ style="--stuic-book-page-width: {pageWidth}px; --stuic-book-page-height: {pageHeight}px;{resizing
115
+ ? ' --stuic-book-duration: 0ms;'
116
+ : ''}"
117
+ >
118
+ {#if measured}
119
+ <Book
120
+ bind:this={bookRef}
121
+ {pages}
122
+ responsive={false}
123
+ singlePage={forceSingle}
124
+ bind:activeSpread
125
+ {...rest}
126
+ />
127
+ {/if}
128
+ </div>
129
+
130
+ <style>
131
+ .stuic-book-responsive {
132
+ flex: 1;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ overflow: hidden;
137
+ }
138
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Props as BookProps } from "./Book.svelte";
2
+ export interface Props extends Omit<BookProps, "responsive" | "singlePage"> {
3
+ /** Minimum page width (px) before switching to single-page mode (default: 150) */
4
+ minPageWidth?: number;
5
+ /** Resize debounce delay in ms (default: 150) */
6
+ debounce?: number;
7
+ }
8
+ import { type BookPage } from "./Book.svelte";
9
+ declare const BookResponsive: import("svelte").Component<Props, {
10
+ next: () => void;
11
+ previous: () => void;
12
+ goTo: (spreadIndex: number) => void;
13
+ goToPage: (pageId: BookPage["id"]) => void;
14
+ zoomIn: () => void;
15
+ zoomOut: () => void;
16
+ resetZoom: () => void;
17
+ getCollection: () => import("./Book.svelte").BookCollection | undefined;
18
+ }, "activeSpread">;
19
+ type BookResponsive = ReturnType<typeof BookResponsive>;
20
+ export default BookResponsive;
@@ -1 +1,2 @@
1
1
  export { default as Book, type Props as BookProps, type BookPage, type BookPageArea, type BookSpread, type BookSheet, type BookCollection, buildSpreads, buildSinglePageSpreads, buildSheets, computeBookPageSize, } from "./Book.svelte";
2
+ export { default as BookResponsive, type Props as BookResponsiveProps, } from "./BookResponsive.svelte";
@@ -1 +1,2 @@
1
1
  export { default as Book, buildSpreads, buildSinglePageSpreads, buildSheets, computeBookPageSize, } from "./Book.svelte";
2
+ export { default as BookResponsive, } from "./BookResponsive.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.31.1",
3
+ "version": "3.32.1",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",