@marianmeres/stuic 3.16.0 → 3.17.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.
@@ -3,12 +3,31 @@
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
4
  import type { Snippet } from "svelte";
5
5
 
6
+ export interface BookPageArea {
7
+ id: string | number;
8
+ /** X position in natural image pixels */
9
+ x: number;
10
+ /** Y position in natural image pixels */
11
+ y: number;
12
+ /** Width in natural image pixels */
13
+ w: number;
14
+ /** Height in natural image pixels */
15
+ h: number;
16
+ [key: string]: any;
17
+ }
18
+
6
19
  export interface BookPage {
7
20
  id: string | number;
8
21
  src: string;
9
22
  srcset?: string;
10
23
  sizes?: string;
11
24
  title?: string;
25
+ /** Natural image width in px (required when areas are used) */
26
+ width?: number;
27
+ /** Natural image height in px (required when areas are used) */
28
+ height?: number;
29
+ /** Clickable areas on this page */
30
+ areas?: BookPageArea[];
12
31
  [key: string]: any;
13
32
  }
14
33
 
@@ -37,7 +56,7 @@
37
56
  keyboard?: boolean;
38
57
  /** Enable swipe gesture navigation (default: true) */
39
58
  swipe?: boolean;
40
- /** Flip animation duration in ms (default: 600) */
59
+ /** Flip animation duration in ms (default: 500) */
41
60
  duration?: number;
42
61
  /** Enable zoom capability (default: true) */
43
62
  zoom?: boolean;
@@ -54,6 +73,8 @@
54
73
  /** Callback when a page is clicked, with relative x/y coordinates (0–1)
55
74
  * (0, 0) = top-left corner, (1, 1) = bottom-right corner */
56
75
  onPageClick?: (data: { page: BookPage; x: number; y: number }) => void;
76
+ /** Callback when a clickable area on a page is clicked */
77
+ onAreaClick?: (data: { area: BookPageArea; page: BookPage }) => void;
57
78
  /** Custom render snippet for pages */
58
79
  renderPage?: Snippet<[{ page: BookPage; position: "left" | "right" | "cover" }]>;
59
80
  /** Custom class for container */
@@ -158,7 +179,7 @@
158
179
  activeSpread = $bindable(0),
159
180
  keyboard = true,
160
181
  swipe = true,
161
- duration = 600,
182
+ duration = 500,
162
183
  zoom: zoomEnabled = true,
163
184
  zoomLevels: ZOOM_LEVELS = [1, 1.5, 2, 3],
164
185
  clampPan = false,
@@ -166,6 +187,7 @@
166
187
  responsive = true,
167
188
  onSpreadChange,
168
189
  onPageClick,
190
+ onAreaClick,
169
191
  renderPage,
170
192
  class: classProp,
171
193
  classStage,
@@ -416,8 +438,6 @@
416
438
 
417
439
  export function next() {
418
440
  if (activeSpread < totalSpreads - 1) {
419
- // The sheet being flipped is the one at index === activeSpread
420
- setTransitioningSheet(activeSpread);
421
441
  coll.setActiveNext();
422
442
  resetZoom();
423
443
  }
@@ -425,8 +445,6 @@
425
445
 
426
446
  export function previous() {
427
447
  if (activeSpread > 0) {
428
- // The sheet being flipped back is the one at index === activeSpread - 1
429
- setTransitioningSheet(activeSpread - 1);
430
448
  coll.setActivePrevious();
431
449
  resetZoom();
432
450
  }
@@ -644,9 +662,9 @@
644
662
  role="region"
645
663
  aria-label="Book"
646
664
  aria-roledescription="book"
647
- style:margin-left={isSinglePageMode
665
+ style:translate={isSinglePageMode
648
666
  ? "calc(var(--stuic-book-page-width) * -1)"
649
- : "0px"}
667
+ : "0"}
650
668
  style:touch-action="none"
651
669
  style:user-select="none"
652
670
  style:transform={zoomLevel !== 1
@@ -693,6 +711,28 @@
693
711
  draggable="false"
694
712
  />
695
713
  {/if}
714
+ {#if onAreaClick && sheet.frontPage.areas?.length && sheet.frontPage.width && sheet.frontPage.height}
715
+ <svg
716
+ viewBox="0 0 {sheet.frontPage.width} {sheet.frontPage.height}"
717
+ preserveAspectRatio="xMidYMid meet"
718
+ class={!unstyled ? "stuic-book-areas" : undefined}
719
+ >
720
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
721
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
722
+ {#each sheet.frontPage.areas as area (area.id)}
723
+ <rect
724
+ x={area.x} y={area.y}
725
+ width={area.w} height={area.h}
726
+ class={!unstyled ? "stuic-book-area" : undefined}
727
+ onclick={(e: MouseEvent) => {
728
+ if (_wasDragged) return;
729
+ e.stopPropagation();
730
+ onAreaClick({ area, page: sheet.frontPage! });
731
+ }}
732
+ />
733
+ {/each}
734
+ </svg>
735
+ {/if}
696
736
  {/if}
697
737
  </div>
698
738
 
@@ -719,6 +759,28 @@
719
759
  draggable="false"
720
760
  />
721
761
  {/if}
762
+ {#if onAreaClick && sheet.backPage.areas?.length && sheet.backPage.width && sheet.backPage.height}
763
+ <svg
764
+ viewBox="0 0 {sheet.backPage.width} {sheet.backPage.height}"
765
+ preserveAspectRatio="xMidYMid meet"
766
+ class={!unstyled ? "stuic-book-areas" : undefined}
767
+ >
768
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
769
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
770
+ {#each sheet.backPage.areas as area (area.id)}
771
+ <rect
772
+ x={area.x} y={area.y}
773
+ width={area.w} height={area.h}
774
+ class={!unstyled ? "stuic-book-area" : undefined}
775
+ onclick={(e: MouseEvent) => {
776
+ if (_wasDragged) return;
777
+ e.stopPropagation();
778
+ onAreaClick({ area, page: sheet.backPage! });
779
+ }}
780
+ />
781
+ {/each}
782
+ </svg>
783
+ {/if}
722
784
  {/if}
723
785
  </div>
724
786
  </div>
@@ -1,12 +1,30 @@
1
1
  import type { ItemCollection as ItemCollectionBase } from "@marianmeres/item-collection";
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
3
  import type { Snippet } from "svelte";
4
+ export interface BookPageArea {
5
+ id: string | number;
6
+ /** X position in natural image pixels */
7
+ x: number;
8
+ /** Y position in natural image pixels */
9
+ y: number;
10
+ /** Width in natural image pixels */
11
+ w: number;
12
+ /** Height in natural image pixels */
13
+ h: number;
14
+ [key: string]: any;
15
+ }
4
16
  export interface BookPage {
5
17
  id: string | number;
6
18
  src: string;
7
19
  srcset?: string;
8
20
  sizes?: string;
9
21
  title?: string;
22
+ /** Natural image width in px (required when areas are used) */
23
+ width?: number;
24
+ /** Natural image height in px (required when areas are used) */
25
+ height?: number;
26
+ /** Clickable areas on this page */
27
+ areas?: BookPageArea[];
10
28
  [key: string]: any;
11
29
  }
12
30
  export interface BookSpread {
@@ -33,7 +51,7 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
33
51
  keyboard?: boolean;
34
52
  /** Enable swipe gesture navigation (default: true) */
35
53
  swipe?: boolean;
36
- /** Flip animation duration in ms (default: 600) */
54
+ /** Flip animation duration in ms (default: 500) */
37
55
  duration?: number;
38
56
  /** Enable zoom capability (default: true) */
39
57
  zoom?: boolean;
@@ -54,6 +72,11 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
54
72
  x: number;
55
73
  y: number;
56
74
  }) => void;
75
+ /** Callback when a clickable area on a page is clicked */
76
+ onAreaClick?: (data: {
77
+ area: BookPageArea;
78
+ page: BookPage;
79
+ }) => void;
57
80
  /** Custom render snippet for pages */
58
81
  renderPage?: Snippet<[{
59
82
  page: BookPage;
@@ -52,6 +52,7 @@ Pages are grouped into **spreads**:
52
52
  | `singlePage` | `boolean` | `false` | Force single-page layout (one page per flip) |
53
53
  | `responsive` | `boolean` | `true` | Auto-switch to single-page when container is too narrow |
54
54
  | `onSpreadChange` | `(spread, index) => void` | - | Callback when active spread changes |
55
+ | `onAreaClick` | `({area, page}) => void` | - | Callback when a clickable area is clicked |
55
56
  | `renderPage` | `Snippet` | - | Custom render snippet for pages |
56
57
  | `class` | `string` | - | Custom class for container |
57
58
  | `classStage` | `string` | - | Custom class for the 3D stage |
@@ -62,10 +63,22 @@ Pages are grouped into **spreads**:
62
63
  ## Interfaces
63
64
 
64
65
  ```typescript
66
+ interface BookPageArea {
67
+ id: string | number;
68
+ x: number; // X position in natural image pixels
69
+ y: number; // Y position in natural image pixels
70
+ w: number; // Width in natural image pixels
71
+ h: number; // Height in natural image pixels
72
+ [key: string]: any;
73
+ }
74
+
65
75
  interface BookPage {
66
76
  id: string | number;
67
77
  src: string;
68
78
  title?: string;
79
+ width?: number; // Natural image width in px (required for areas)
80
+ height?: number; // Natural image height in px (required for areas)
81
+ areas?: BookPageArea[];
69
82
  [key: string]: any;
70
83
  }
71
84
 
@@ -89,6 +102,32 @@ interface BookSpread {
89
102
  | `resetZoom()` | Reset zoom to 1x |
90
103
  | `getCollection()` | Get the underlying ItemCollection |
91
104
 
105
+ ## Clickable Areas
106
+
107
+ Pages can define clickable areas (e.g. product hotspots in a catalog). Areas are rendered as an SVG overlay that scales correctly with the page image. Requires `width`/`height` on the page (natural image dimensions) and the `onAreaClick` callback.
108
+
109
+ ```svelte
110
+ <script lang="ts">
111
+ import { Book, type BookPage } from "@marianmeres/stuic";
112
+
113
+ const pages: BookPage[] = [
114
+ {
115
+ id: 0,
116
+ src: "/catalog-page-1.jpg",
117
+ width: 2480,
118
+ height: 3508,
119
+ areas: [
120
+ { id: "SKU-001", x: 100, y: 200, w: 400, h: 300 },
121
+ { id: "SKU-002", x: 600, y: 200, w: 400, h: 300 },
122
+ ],
123
+ },
124
+ // ...
125
+ ];
126
+ </script>
127
+
128
+ <Book {pages} onAreaClick={({ area, page }) => addToCart(area.id)} />
129
+ ```
130
+
92
131
  ## Keyboard Navigation
93
132
 
94
133
  | Key | Action |
@@ -110,3 +149,4 @@ interface BookSpread {
110
149
  | `--stuic-book-page-bg` | `var(--stuic-color-surface)` | Page background color |
111
150
  | `--stuic-book-page-shadow` | `0 2px 16px rgba(0,0,0,0.15)` | Book shadow |
112
151
  | `--stuic-book-radius` | `var(--radius-sm)` | Page border radius |
152
+ | `--stuic-book-area-fill-hover` | `rgba(0, 0, 0, 0.06)` | Area hover highlight fill |
@@ -1,8 +1,8 @@
1
1
  /* Book Component Tokens */
2
2
  @theme inline {
3
- --stuic-book-perspective: 1200px;
4
- --stuic-book-duration: 600ms;
5
- --stuic-book-timing: ease-in-out;
3
+ --stuic-book-perspective: 2000px;
4
+ --stuic-book-duration: 500ms;
5
+ --stuic-book-timing: ease-out;
6
6
  --stuic-book-page-bg: transparent; /*var(--stuic-color-surface, #fff);*/
7
7
  --stuic-book-page-shadow: none; /*0 2px 16px rgba(0, 0, 0, 0.15);*/
8
8
  --stuic-book-radius: var(--radius-sm, 2px);
@@ -32,9 +32,9 @@
32
32
  }
33
33
 
34
34
  /*
35
- * Stage — always full double-page width, positioned via margin-left.
36
- * When the cover is shown (spread 0), margin-left shifts the stage left
37
- * so only the right half (the cover sheet) is visible in the wrapper.
35
+ * Stage — always full double-page width, positioned via translate.
36
+ * In single-page mode, translate shifts the stage left so only the
37
+ * right half (the visible sheet) is shown in the wrapper.
38
38
  * Transparent — the wrapper provides the page background.
39
39
  */
40
40
  .stuic-book-stage {
@@ -42,7 +42,7 @@
42
42
  perspective: var(--stuic-book-perspective);
43
43
  width: calc(var(--stuic-book-page-width) * 2);
44
44
  height: var(--stuic-book-page-height);
45
- transition: margin-left var(--stuic-book-duration) var(--stuic-book-timing);
45
+ transition: translate var(--stuic-book-duration) var(--stuic-book-timing);
46
46
  }
47
47
 
48
48
  /* ---- Sheet (the flippable paper) ---- */
@@ -54,8 +54,9 @@
54
54
  height: 100%;
55
55
  transform-style: preserve-3d;
56
56
  transform-origin: left center;
57
+ transform: rotateY(0deg);
57
58
  transition: transform var(--stuic-book-duration) var(--stuic-book-timing);
58
- will-change: transform;
59
+ /* outline: 3px solid black; */
59
60
  }
60
61
 
61
62
  /* Front face (visible when not flipped — right side of the book) */
@@ -67,7 +68,6 @@
67
68
  background: var(--stuic-book-page-bg);
68
69
  border-radius: 0 var(--stuic-book-radius) var(--stuic-book-radius) 0;
69
70
  overflow: hidden;
70
- clip-path: inset(0);
71
71
  }
72
72
 
73
73
  .stuic-book-sheet-front img {
@@ -86,7 +86,6 @@
86
86
  background: var(--stuic-book-page-bg);
87
87
  border-radius: var(--stuic-book-radius) 0 0 var(--stuic-book-radius);
88
88
  overflow: hidden;
89
- clip-path: inset(0);
90
89
  }
91
90
 
92
91
  .stuic-book-sheet-back img {
@@ -95,6 +94,26 @@
95
94
  object-fit: contain;
96
95
  }
97
96
 
97
+ /* ---- Clickable areas SVG overlay ---- */
98
+ .stuic-book-areas {
99
+ position: absolute;
100
+ inset: 0;
101
+ width: 100%;
102
+ height: 100%;
103
+ pointer-events: none;
104
+ }
105
+
106
+ .stuic-book-area {
107
+ pointer-events: all;
108
+ cursor: pointer;
109
+ fill: transparent;
110
+ transition: fill 150ms;
111
+ }
112
+
113
+ .stuic-book-area:hover {
114
+ fill: var(--stuic-book-area-fill-hover, rgba(0, 0, 0, 0.06));
115
+ }
116
+
98
117
  /* Placeholder: empty page face with content on the other side */
99
118
  .stuic-book-sheet-front[data-placeholder],
100
119
  .stuic-book-sheet-back[data-placeholder] {
@@ -1 +1 @@
1
- export { default as Book, type Props as BookProps, type BookPage, type BookSpread, type BookSheet, type BookCollection, buildSpreads, buildSinglePageSpreads, buildSheets, } from "./Book.svelte";
1
+ export { default as Book, type Props as BookProps, type BookPage, type BookPageArea, type BookSpread, type BookSheet, type BookCollection, buildSpreads, buildSinglePageSpreads, buildSheets, } from "./Book.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",
@@ -39,26 +39,26 @@
39
39
  "@marianmeres/icons-fns": "^5.0.0",
40
40
  "@marianmeres/random-human-readable": "^1.6.1",
41
41
  "@sveltejs/adapter-auto": "^4.0.0",
42
- "@sveltejs/kit": "^2.50.2",
42
+ "@sveltejs/kit": "^2.51.0",
43
43
  "@sveltejs/package": "^2.5.7",
44
44
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
45
45
  "@tailwindcss/cli": "^4.1.18",
46
46
  "@tailwindcss/forms": "^0.5.11",
47
47
  "@tailwindcss/typography": "^0.5.19",
48
48
  "@tailwindcss/vite": "^4.1.18",
49
- "@types/node": "^25.2.2",
49
+ "@types/node": "^25.2.3",
50
50
  "dotenv": "^16.6.1",
51
51
  "eslint": "^9.39.2",
52
52
  "globals": "^16.5.0",
53
53
  "prettier": "^3.8.1",
54
54
  "prettier-plugin-svelte": "^3.4.1",
55
55
  "publint": "^0.3.17",
56
- "svelte": "^5.50.0",
56
+ "svelte": "^5.50.3",
57
57
  "svelte-check": "^4.3.6",
58
58
  "tailwindcss": "^4.1.18",
59
59
  "tsx": "^4.21.0",
60
60
  "typescript": "^5.9.3",
61
- "typescript-eslint": "^8.54.0",
61
+ "typescript-eslint": "^8.55.0",
62
62
  "vite": "^7.3.1",
63
63
  "vitest": "^3.2.4"
64
64
  },