pdf-flipbook 1.0.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.
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/pdf-flipbook.css +304 -0
- package/dist/pdf-flipbook.esm.js +21508 -0
- package/dist/pdf-flipbook.esm.js.map +7 -0
- package/dist/pdf-flipbook.esm.min.js +42 -0
- package/dist/pdf-flipbook.esm.min.js.map +7 -0
- package/dist/pdf-flipbook.umd.js +21535 -0
- package/dist/pdf-flipbook.umd.js.map +7 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# pdf-flipbook
|
|
2
|
+
|
|
3
|
+
> **A responsive, animated PDF flipbook viewer.**
|
|
4
|
+
> Give it a PDF URL and it becomes a beautiful interactive book — complete with page-turn animations, keyboard/swipe navigation, and full responsiveness.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/pdf-flipbook)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 📖 Realistic 3D page-flip animation (CSS transforms, no WebGL needed)
|
|
14
|
+
- 📄 Renders any PDF via **PDF.js** — no server required
|
|
15
|
+
- 📱 Fully responsive — two-page spread on desktop, single page on mobile
|
|
16
|
+
- ⌨️ Keyboard navigation (← →) and touch swipe support
|
|
17
|
+
- 🎨 Dark & light themes built-in, fully customisable via CSS variables
|
|
18
|
+
- 🧩 Works with **React**, **Vue**, vanilla JS, or any framework
|
|
19
|
+
- 🌐 No jQuery, no heavy UI libraries
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install pdf-flipbook
|
|
27
|
+
# or
|
|
28
|
+
yarn add pdf-flipbook
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Quick Start
|
|
34
|
+
|
|
35
|
+
### 1 — Vanilla JS (ESM)
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<div id="my-book" style="height: 600px;"></div>
|
|
39
|
+
|
|
40
|
+
<script type="module">
|
|
41
|
+
import PdfFlipbook from 'pdf-flipbook';
|
|
42
|
+
import 'pdf-flipbook/style.css';
|
|
43
|
+
|
|
44
|
+
const book = new PdfFlipbook('#my-book', 'https://example.com/document.pdf');
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2 — Vanilla JS (UMD / CDN)
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<link rel="stylesheet" href="https://unpkg.com/pdf-flipbook/dist/pdf-flipbook.css">
|
|
52
|
+
<script src="https://unpkg.com/pdf-flipbook/dist/pdf-flipbook.umd.js"></script>
|
|
53
|
+
|
|
54
|
+
<div id="my-book"></div>
|
|
55
|
+
<script>
|
|
56
|
+
const { PdfFlipbook } = window.PdfFlipbook;
|
|
57
|
+
new PdfFlipbook('#my-book', '/path/to/file.pdf');
|
|
58
|
+
</script>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3 — React
|
|
62
|
+
|
|
63
|
+
```jsx
|
|
64
|
+
import { useEffect, useRef } from 'react';
|
|
65
|
+
import PdfFlipbook from 'pdf-flipbook';
|
|
66
|
+
import 'pdf-flipbook/style.css';
|
|
67
|
+
|
|
68
|
+
export default function BookViewer({ pdfUrl }) {
|
|
69
|
+
const ref = useRef(null);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!ref.current) return;
|
|
73
|
+
const book = new PdfFlipbook(ref.current, pdfUrl, { theme: 'light' });
|
|
74
|
+
return () => book.destroy();
|
|
75
|
+
}, [pdfUrl]);
|
|
76
|
+
|
|
77
|
+
return <div ref={ref} style={{ height: 600 }} />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4 — Vue 3
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<template>
|
|
85
|
+
<div ref="container" style="height: 600px" />
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<script setup>
|
|
89
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
90
|
+
import PdfFlipbook from 'pdf-flipbook';
|
|
91
|
+
import 'pdf-flipbook/style.css';
|
|
92
|
+
|
|
93
|
+
const props = defineProps({ pdfUrl: String });
|
|
94
|
+
const container = ref(null);
|
|
95
|
+
let book;
|
|
96
|
+
|
|
97
|
+
onMounted(() => {
|
|
98
|
+
book = new PdfFlipbook(container.value, props.pdfUrl);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
onUnmounted(() => book?.destroy());
|
|
102
|
+
</script>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ⚙️ API
|
|
108
|
+
|
|
109
|
+
### `new PdfFlipbook(container, pdfUrl, options?)`
|
|
110
|
+
|
|
111
|
+
| Parameter | Type | Description |
|
|
112
|
+
|-------------|---------------------------|--------------------------------------------|
|
|
113
|
+
| `container` | `string \| HTMLElement` | CSS selector or DOM element to mount into |
|
|
114
|
+
| `pdfUrl` | `string` | URL of the PDF to display |
|
|
115
|
+
| `options` | `object` | Optional configuration (see below) |
|
|
116
|
+
|
|
117
|
+
### Options
|
|
118
|
+
|
|
119
|
+
| Option | Type | Default | Description |
|
|
120
|
+
|--------------------|------------|-----------|----------------------------------------------------|
|
|
121
|
+
| `flipDuration` | `number` | `700` | Page-flip animation duration (ms) |
|
|
122
|
+
| `shadowIntensity` | `number` | `0.4` | Shadow opacity at peak flip (0–1) |
|
|
123
|
+
| `maxDpr` | `number` | `2` | Max device-pixel-ratio for canvas quality |
|
|
124
|
+
| `toolbar` | `boolean` | `true` | Show the bottom toolbar with page info |
|
|
125
|
+
| `theme` | `string` | `"dark"` | `"dark"` or `"light"` |
|
|
126
|
+
| `preloadAhead` | `number` | `2` | Pages to preload ahead of current spread |
|
|
127
|
+
| `onFlip` | `function` | `null` | Callback: `(fromPage, toPage) => void` |
|
|
128
|
+
| `onReady` | `function` | `null` | Callback: `() => void` — all pages loaded |
|
|
129
|
+
| `onError` | `function` | `null` | Callback: `(error) => void` |
|
|
130
|
+
|
|
131
|
+
### Instance Methods
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
book.next() // Turn to the next spread
|
|
135
|
+
book.prev() // Turn to the previous spread
|
|
136
|
+
book.goTo(pageNum) // Jump to a specific page (1-indexed)
|
|
137
|
+
book.destroy() // Remove the flipbook and clean up events
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Static Properties
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// Override the PDF.js worker URL (do this before instantiating):
|
|
144
|
+
PdfFlipbook.workerSrc = '/path/to/pdf.worker.min.mjs';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🎨 Theming
|
|
150
|
+
|
|
151
|
+
The flipbook is fully customisable via CSS variables on the `.pfb` element:
|
|
152
|
+
|
|
153
|
+
```css
|
|
154
|
+
.pfb {
|
|
155
|
+
--pfb-bg: #1a1a2e; /* stage background */
|
|
156
|
+
--pfb-page-bg: #fdfaf5; /* page background colour */
|
|
157
|
+
--pfb-spine-color: #0f0f1a; /* spine colour */
|
|
158
|
+
--pfb-spine-width: 12px; /* spine width */
|
|
159
|
+
--pfb-accent: #c9a96e; /* loader/accent colour */
|
|
160
|
+
--pfb-flip-z: 600px; /* perspective depth */
|
|
161
|
+
--pfb-book-shadow: 0 30px 80px rgba(0,0,0,.6);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🌍 CORS & PDF Sources
|
|
168
|
+
|
|
169
|
+
PDF.js fetches the file from the browser, so the PDF server must serve the correct CORS headers:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Access-Control-Allow-Origin: *
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If you control the server, add that header. If you're loading local files during development, serve them via a local HTTP server (not `file://`).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 📋 Browser Support
|
|
180
|
+
|
|
181
|
+
| Browser | Support |
|
|
182
|
+
|---------|---------|
|
|
183
|
+
| Chrome 90+ | ✅ |
|
|
184
|
+
| Firefox 88+ | ✅ |
|
|
185
|
+
| Safari 14+ | ✅ |
|
|
186
|
+
| Edge 90+ | ✅ |
|
|
187
|
+
| IE 11 | ❌ |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 📝 License
|
|
192
|
+
|
|
193
|
+
[MIT](LICENSE) © 2024
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
pdf-flipbook — styles
|
|
3
|
+
Import via: import 'pdf-flipbook/style.css'
|
|
4
|
+
============================================================ */
|
|
5
|
+
|
|
6
|
+
/* ── Reset / Container ─────────────────────────────────────── */
|
|
7
|
+
.pfb {
|
|
8
|
+
--pfb-bg: #1a1a2e;
|
|
9
|
+
--pfb-book-shadow: 0 30px 80px rgba(0,0,0,.6);
|
|
10
|
+
--pfb-spine-color: #0f0f1a;
|
|
11
|
+
--pfb-spine-width: 12px;
|
|
12
|
+
--pfb-page-bg: #fdfaf5;
|
|
13
|
+
--pfb-toolbar-bg: rgba(10, 10, 20, 0.92);
|
|
14
|
+
--pfb-toolbar-color: #e8e0d0;
|
|
15
|
+
--pfb-btn-bg: rgba(255,255,255,.08);
|
|
16
|
+
--pfb-btn-hover-bg: rgba(255,255,255,.18);
|
|
17
|
+
--pfb-btn-color: #e8e0d0;
|
|
18
|
+
--pfb-accent: #c9a96e;
|
|
19
|
+
--pfb-radius: 4px;
|
|
20
|
+
--pfb-flip-z: 600px;
|
|
21
|
+
--pfb-duration: 0.7s;
|
|
22
|
+
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: 0;
|
|
27
|
+
width: 100%;
|
|
28
|
+
background: var(--pfb-bg);
|
|
29
|
+
font-family: 'Georgia', 'Times New Roman', serif;
|
|
30
|
+
-webkit-font-smoothing: antialiased;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
user-select: none;
|
|
33
|
+
-webkit-user-select: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.pfb *,
|
|
37
|
+
.pfb *::before,
|
|
38
|
+
.pfb *::after {
|
|
39
|
+
box-sizing: inherit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Light theme ────────────────────────────────────────────── */
|
|
43
|
+
.pfb--light {
|
|
44
|
+
--pfb-bg: #e8e4dc;
|
|
45
|
+
--pfb-toolbar-bg: rgba(240,236,228,0.96);
|
|
46
|
+
--pfb-toolbar-color: #3a3028;
|
|
47
|
+
--pfb-btn-bg: rgba(0,0,0,.07);
|
|
48
|
+
--pfb-btn-hover-bg: rgba(0,0,0,.14);
|
|
49
|
+
--pfb-btn-color: #3a3028;
|
|
50
|
+
--pfb-book-shadow: 0 20px 60px rgba(0,0,0,.3);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ── Stage ──────────────────────────────────────────────────── */
|
|
54
|
+
.pfb__stage {
|
|
55
|
+
position: relative;
|
|
56
|
+
width: 100%;
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
align-items: center;
|
|
60
|
+
padding: 32px 16px 20px;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
min-height: 300px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── Book ───────────────────────────────────────────────────── */
|
|
66
|
+
.pfb__book {
|
|
67
|
+
position: relative;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: stretch;
|
|
70
|
+
perspective: var(--pfb-flip-z);
|
|
71
|
+
box-shadow: var(--pfb-book-shadow);
|
|
72
|
+
max-width: min(92vw, 1100px);
|
|
73
|
+
width: 100%;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── Page panels ────────────────────────────────────────────── */
|
|
77
|
+
.pfb__page {
|
|
78
|
+
position: relative;
|
|
79
|
+
flex: 1;
|
|
80
|
+
transform-style: preserve-3d;
|
|
81
|
+
transition: transform var(--pfb-duration) cubic-bezier(.645,.045,.355,1);
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
min-height: 300px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.pfb__page--left { transform-origin: right center; border-radius: var(--pfb-radius) 0 0 var(--pfb-radius); }
|
|
87
|
+
.pfb__page--right { transform-origin: left center; border-radius: 0 var(--pfb-radius) var(--pfb-radius) 0; }
|
|
88
|
+
|
|
89
|
+
/* ── Page faces (front / back) ──────────────────────────────── */
|
|
90
|
+
.pfb__page-front,
|
|
91
|
+
.pfb__page-back {
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
background: var(--pfb-page-bg);
|
|
95
|
+
backface-visibility: hidden;
|
|
96
|
+
-webkit-backface-visibility: hidden;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.pfb__page-back {
|
|
101
|
+
transform: rotateY(180deg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.pfb__page-front canvas,
|
|
105
|
+
.pfb__page-back canvas {
|
|
106
|
+
display: block;
|
|
107
|
+
width: 100% !important;
|
|
108
|
+
height: 100% !important;
|
|
109
|
+
object-fit: contain;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.pfb__face--blank {
|
|
113
|
+
background: linear-gradient(135deg, #fdfaf5 0%, #f0e8d8 100%);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ── Spine ──────────────────────────────────────────────────── */
|
|
117
|
+
.pfb__spine {
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
width: var(--pfb-spine-width);
|
|
120
|
+
background: var(--pfb-spine-color);
|
|
121
|
+
position: relative;
|
|
122
|
+
z-index: 10;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.pfb__spine::before {
|
|
126
|
+
content: '';
|
|
127
|
+
position: absolute;
|
|
128
|
+
inset: 0;
|
|
129
|
+
background: linear-gradient(
|
|
130
|
+
to right,
|
|
131
|
+
rgba(255,255,255,.05) 0%,
|
|
132
|
+
rgba(0,0,0,.4) 50%,
|
|
133
|
+
rgba(255,255,255,.05) 100%
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── Shadows ────────────────────────────────────────────────── */
|
|
138
|
+
.pfb__shadow {
|
|
139
|
+
position: absolute;
|
|
140
|
+
inset: 0;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
opacity: 0;
|
|
143
|
+
transition: opacity 0.1s linear;
|
|
144
|
+
z-index: 5;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.pfb__shadow--left { background: linear-gradient(to left, rgba(0,0,0,.4), transparent 80%); }
|
|
148
|
+
.pfb__shadow--right { background: linear-gradient(to right, rgba(0,0,0,.4), transparent 80%); }
|
|
149
|
+
|
|
150
|
+
/* ── Nav arrow buttons (on stage) ───────────────────────────── */
|
|
151
|
+
.pfb__btn {
|
|
152
|
+
position: absolute;
|
|
153
|
+
top: 50%;
|
|
154
|
+
transform: translateY(-50%);
|
|
155
|
+
width: 44px;
|
|
156
|
+
height: 44px;
|
|
157
|
+
border: none;
|
|
158
|
+
border-radius: 50%;
|
|
159
|
+
background: var(--pfb-btn-bg);
|
|
160
|
+
color: var(--pfb-btn-color);
|
|
161
|
+
font-size: 20px;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
transition: background 0.2s, transform 0.2s, opacity 0.2s;
|
|
167
|
+
z-index: 20;
|
|
168
|
+
backdrop-filter: blur(4px);
|
|
169
|
+
-webkit-backdrop-filter: blur(4px);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.pfb__btn--prev { left: 8px; }
|
|
173
|
+
.pfb__btn--next { right: 8px; }
|
|
174
|
+
|
|
175
|
+
.pfb__btn:hover:not(:disabled) {
|
|
176
|
+
background: var(--pfb-btn-hover-bg);
|
|
177
|
+
transform: translateY(-50%) scale(1.1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.pfb__btn:disabled {
|
|
181
|
+
opacity: 0.2;
|
|
182
|
+
cursor: not-allowed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ── Toolbar ────────────────────────────────────────────────── */
|
|
186
|
+
.pfb__toolbar {
|
|
187
|
+
width: 100%;
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
gap: 16px;
|
|
192
|
+
padding: 10px 16px;
|
|
193
|
+
background: var(--pfb-toolbar-bg);
|
|
194
|
+
backdrop-filter: blur(8px);
|
|
195
|
+
-webkit-backdrop-filter: blur(8px);
|
|
196
|
+
border-top: 1px solid rgba(255,255,255,.07);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.pfb__tb-btn {
|
|
200
|
+
width: 36px;
|
|
201
|
+
height: 36px;
|
|
202
|
+
border: 1px solid rgba(255,255,255,.12);
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
background: var(--pfb-btn-bg);
|
|
205
|
+
color: var(--pfb-toolbar-color);
|
|
206
|
+
font-size: 16px;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
transition: background 0.2s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.pfb__tb-btn:hover:not(:disabled) { background: var(--pfb-btn-hover-bg); }
|
|
215
|
+
.pfb__tb-btn:disabled { opacity: 0.25; cursor: not-allowed; }
|
|
216
|
+
|
|
217
|
+
.pfb__page-info {
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
letter-spacing: 0.05em;
|
|
220
|
+
color: var(--pfb-toolbar-color);
|
|
221
|
+
min-width: 90px;
|
|
222
|
+
text-align: center;
|
|
223
|
+
font-family: 'Georgia', serif;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ── Loader ─────────────────────────────────────────────────── */
|
|
227
|
+
.pfb__loader {
|
|
228
|
+
position: absolute;
|
|
229
|
+
inset: 0;
|
|
230
|
+
display: flex;
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
align-items: center;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
gap: 16px;
|
|
235
|
+
background: rgba(10,10,20,.85);
|
|
236
|
+
z-index: 30;
|
|
237
|
+
backdrop-filter: blur(4px);
|
|
238
|
+
-webkit-backdrop-filter: blur(4px);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.pfb__loader[hidden] { display: none; }
|
|
242
|
+
|
|
243
|
+
.pfb__spinner {
|
|
244
|
+
width: 48px;
|
|
245
|
+
height: 48px;
|
|
246
|
+
border: 3px solid rgba(201,169,110,.2);
|
|
247
|
+
border-top-color: var(--pfb-accent);
|
|
248
|
+
border-radius: 50%;
|
|
249
|
+
animation: pfb-spin 0.9s linear infinite;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@keyframes pfb-spin {
|
|
253
|
+
to { transform: rotate(360deg); }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.pfb__loader-text {
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
color: var(--pfb-toolbar-color, #c9a96e);
|
|
259
|
+
letter-spacing: 0.08em;
|
|
260
|
+
text-transform: uppercase;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── Error ──────────────────────────────────────────────────── */
|
|
264
|
+
.pfb__error {
|
|
265
|
+
position: absolute;
|
|
266
|
+
inset: 0;
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
justify-content: center;
|
|
270
|
+
padding: 24px;
|
|
271
|
+
text-align: center;
|
|
272
|
+
color: #e07070;
|
|
273
|
+
font-size: 14px;
|
|
274
|
+
background: rgba(10,10,20,.9);
|
|
275
|
+
z-index: 30;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.pfb__error[hidden] { display: none; }
|
|
279
|
+
|
|
280
|
+
/* ── Responsive ─────────────────────────────────────────────── */
|
|
281
|
+
@media (max-width: 640px) {
|
|
282
|
+
/* On narrow screens show one page at a time */
|
|
283
|
+
.pfb__book {
|
|
284
|
+
max-width: min(96vw, 520px);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.pfb__page--left {
|
|
288
|
+
display: none;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.pfb__spine {
|
|
292
|
+
display: none;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pfb__page--right {
|
|
296
|
+
border-radius: var(--pfb-radius);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.pfb__btn {
|
|
300
|
+
width: 38px;
|
|
301
|
+
height: 38px;
|
|
302
|
+
font-size: 16px;
|
|
303
|
+
}
|
|
304
|
+
}
|