lupine.components 1.1.58 → 1.1.60
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/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/mobile-components/mobile-side-menu.tsx +0 -1
- package/src/components/notice-message.tsx +0 -1
- package/src/components/slide-page-component-demo.tsx +364 -0
- package/src/components/slide-page-component.tsx +777 -0
- package/src/components/slide-tab-component.tsx +5 -5
- package/src/demo/demo-frame-helper.tsx +7 -0
- package/src/demo/demo-registry.ts +2 -0
- package/src/frames/responsive-frame.tsx +0 -1
- package/src/frames/slider-frame-demo.tsx +12 -1
- package/src/frames/slider-frame.tsx +4 -2
- package/src/frames/slider-helper.tsx +7 -1
- package/src/frames/top-frame.tsx +0 -1
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from './redirect';
|
|
|
36
36
|
export * from './resizable-splitter';
|
|
37
37
|
export * from './select-angle-component';
|
|
38
38
|
export * from './select-with-title';
|
|
39
|
+
export * from './slide-page-component';
|
|
39
40
|
export * from './slide-tab-component';
|
|
40
41
|
export * from './spinner';
|
|
41
42
|
export * from './stars-component';
|
|
@@ -244,7 +244,6 @@ export const MobileSideMenu = (props: { children: VNode<any>; autoExtend?: boole
|
|
|
244
244
|
overflowY: 'auto',
|
|
245
245
|
transform: 'translateX(-100%)',
|
|
246
246
|
boxShadow: 'var(--cover-box-shadow)',
|
|
247
|
-
'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '0',
|
|
248
247
|
'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '0',
|
|
249
248
|
},
|
|
250
249
|
[MediaQueryRange.TabletAbove]: {
|
|
@@ -57,7 +57,6 @@ export class NotificationMessage {
|
|
|
57
57
|
padding: '0 10px',
|
|
58
58
|
zIndex: 'var(--layer-notice)',
|
|
59
59
|
pointerEvents: 'none',
|
|
60
|
-
'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '',
|
|
61
60
|
'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '',
|
|
62
61
|
'>div': {
|
|
63
62
|
pointerEvents: 'auto',
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { DemoStory } from '../demo/demo-types';
|
|
2
|
+
import { SlidePageComponent, SlidePageProps } from './slide-page-component';
|
|
3
|
+
|
|
4
|
+
const samplePages = [
|
|
5
|
+
{
|
|
6
|
+
id: 'page-1',
|
|
7
|
+
title: 'Explore Lupine Framework',
|
|
8
|
+
link: 'https://github.com/uuware/lupine.js',
|
|
9
|
+
target: '_blank',
|
|
10
|
+
content: (
|
|
11
|
+
<div
|
|
12
|
+
style={{
|
|
13
|
+
width: '100%',
|
|
14
|
+
height: '100%',
|
|
15
|
+
padding: '28px 32px',
|
|
16
|
+
boxSizing: 'border-box',
|
|
17
|
+
background: 'linear-gradient(135deg, #091e3a 0%, #173865 50%, #2b6cb0 100%)',
|
|
18
|
+
color: '#ffffff',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
position: 'relative',
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<div>
|
|
27
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
28
|
+
<span
|
|
29
|
+
style={{
|
|
30
|
+
fontSize: '11px',
|
|
31
|
+
fontWeight: 700,
|
|
32
|
+
textTransform: 'uppercase',
|
|
33
|
+
letterSpacing: '0.05em',
|
|
34
|
+
padding: '3px 10px',
|
|
35
|
+
borderRadius: '12px',
|
|
36
|
+
background: 'rgba(2, 132, 199, 0.45)',
|
|
37
|
+
border: '1px solid rgba(56, 189, 248, 0.5)',
|
|
38
|
+
color: '#e0f2fe',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
🔗 Clickable Card (With Link)
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
<h2 style={{ margin: '0 0 8px 0', fontSize: '22px', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
|
45
|
+
Explore Lupine.js Architecture
|
|
46
|
+
</h2>
|
|
47
|
+
<p style={{ margin: 0, fontSize: '14px', lineHeight: '1.5', color: '#cbd5e1', maxWidth: '520px' }}>
|
|
48
|
+
A lightweight, high-performance TypeScript full-stack framework with zero-re-render DOM updates and native mobile navigation.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '16px' }}>
|
|
52
|
+
<span
|
|
53
|
+
style={{
|
|
54
|
+
display: 'inline-flex',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: '6px',
|
|
57
|
+
fontSize: '13px',
|
|
58
|
+
fontWeight: 600,
|
|
59
|
+
padding: '6px 14px',
|
|
60
|
+
borderRadius: '8px',
|
|
61
|
+
backgroundColor: '#38bdf8',
|
|
62
|
+
color: '#0f172a',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
Visit GitHub Repository ↗
|
|
66
|
+
</span>
|
|
67
|
+
<span style={{ fontSize: '12px', color: '#94a3b8' }}>Click anywhere on this slide to open</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'page-2',
|
|
74
|
+
title: 'Reactive DOM Performance',
|
|
75
|
+
// No link: behaves as a normal card
|
|
76
|
+
content: (
|
|
77
|
+
<div
|
|
78
|
+
style={{
|
|
79
|
+
width: '100%',
|
|
80
|
+
height: '100%',
|
|
81
|
+
padding: '28px 32px',
|
|
82
|
+
boxSizing: 'border-box',
|
|
83
|
+
background: 'linear-gradient(135deg, #18181b 0%, #27272a 60%, #3f3f46 100%)',
|
|
84
|
+
color: '#ffffff',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
flexDirection: 'column',
|
|
87
|
+
justifyContent: 'space-between',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<div>
|
|
91
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
92
|
+
<span
|
|
93
|
+
style={{
|
|
94
|
+
fontSize: '11px',
|
|
95
|
+
fontWeight: 700,
|
|
96
|
+
textTransform: 'uppercase',
|
|
97
|
+
letterSpacing: '0.05em',
|
|
98
|
+
padding: '3px 10px',
|
|
99
|
+
borderRadius: '12px',
|
|
100
|
+
background: 'rgba(161, 161, 170, 0.25)',
|
|
101
|
+
border: '1px solid rgba(212, 212, 216, 0.35)',
|
|
102
|
+
color: '#f4f4f5',
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
📄 Normal Card (No Link)
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
<h2 style={{ margin: '0 0 8px 0', fontSize: '22px', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
|
109
|
+
Direct DOM Updates & State Isolation
|
|
110
|
+
</h2>
|
|
111
|
+
<p style={{ margin: '0 0 16px 0', fontSize: '14px', lineHeight: '1.5', color: '#d4d4d8' }}>
|
|
112
|
+
Standard non-clickable presentation card showcasing core reactive capabilities:
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
padding: '8px 14px',
|
|
119
|
+
borderRadius: '8px',
|
|
120
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
121
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<div style={{ fontSize: '16px', fontWeight: 700, color: '#38bdf8' }}>⚡ 0ms</div>
|
|
125
|
+
<div style={{ fontSize: '11px', color: '#a1a1aa' }}>Re-render overhead</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
padding: '8px 14px',
|
|
130
|
+
borderRadius: '8px',
|
|
131
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
132
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<div style={{ fontSize: '16px', fontWeight: 700, color: '#4ade80' }}>🎯 CSS &</div>
|
|
136
|
+
<div style={{ fontSize: '11px', color: '#a1a1aa' }}>Scoped namespace</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div
|
|
139
|
+
style={{
|
|
140
|
+
padding: '8px 14px',
|
|
141
|
+
borderRadius: '8px',
|
|
142
|
+
background: 'rgba(255, 255, 255, 0.08)',
|
|
143
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<div style={{ fontSize: '16px', fontWeight: 700, color: '#fbbf24' }}>📱 60fps</div>
|
|
147
|
+
<div style={{ fontSize: '11px', color: '#a1a1aa' }}>Native touch snap</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'page-3',
|
|
155
|
+
title: 'Component Pool & Widgets',
|
|
156
|
+
link: '#component-pool',
|
|
157
|
+
content: (
|
|
158
|
+
<div
|
|
159
|
+
style={{
|
|
160
|
+
width: '100%',
|
|
161
|
+
height: '100%',
|
|
162
|
+
padding: '28px 32px',
|
|
163
|
+
boxSizing: 'border-box',
|
|
164
|
+
background: 'linear-gradient(135deg, #064e3b 0%, #047857 55%, #059669 100%)',
|
|
165
|
+
color: '#ffffff',
|
|
166
|
+
display: 'flex',
|
|
167
|
+
flexDirection: 'column',
|
|
168
|
+
justifyContent: 'space-between',
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<div>
|
|
172
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
173
|
+
<span
|
|
174
|
+
style={{
|
|
175
|
+
fontSize: '11px',
|
|
176
|
+
fontWeight: 700,
|
|
177
|
+
textTransform: 'uppercase',
|
|
178
|
+
letterSpacing: '0.05em',
|
|
179
|
+
padding: '3px 10px',
|
|
180
|
+
borderRadius: '12px',
|
|
181
|
+
background: 'rgba(16, 185, 129, 0.35)',
|
|
182
|
+
border: '1px solid rgba(110, 231, 183, 0.5)',
|
|
183
|
+
color: '#d1fae5',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
🔗 Clickable Card (With Link)
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
<h2 style={{ margin: '0 0 8px 0', fontSize: '22px', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
|
190
|
+
Slide Tab & Slide Page Suite
|
|
191
|
+
</h2>
|
|
192
|
+
<p style={{ margin: 0, fontSize: '14px', lineHeight: '1.5', color: '#a7f3d0', maxWidth: '520px' }}>
|
|
193
|
+
Standardized components built on top of native CSS scroll-snap with automated indicators and gesture support.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '16px' }}>
|
|
197
|
+
<span
|
|
198
|
+
style={{
|
|
199
|
+
display: 'inline-flex',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
gap: '6px',
|
|
202
|
+
fontSize: '13px',
|
|
203
|
+
fontWeight: 600,
|
|
204
|
+
padding: '6px 14px',
|
|
205
|
+
borderRadius: '8px',
|
|
206
|
+
backgroundColor: '#6ee7b7',
|
|
207
|
+
color: '#064e3b',
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
Explore Components ↗
|
|
211
|
+
</span>
|
|
212
|
+
<span style={{ fontSize: '12px', color: '#d1fae5' }}>Click to navigate internal link</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'page-4',
|
|
219
|
+
title: 'Customizable Slide Options',
|
|
220
|
+
// No link: behaves as a normal card
|
|
221
|
+
content: (
|
|
222
|
+
<div
|
|
223
|
+
style={{
|
|
224
|
+
width: '100%',
|
|
225
|
+
height: '100%',
|
|
226
|
+
padding: '28px 32px',
|
|
227
|
+
boxSizing: 'border-box',
|
|
228
|
+
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 55%, #7c3aed 100%)',
|
|
229
|
+
color: '#ffffff',
|
|
230
|
+
display: 'flex',
|
|
231
|
+
flexDirection: 'column',
|
|
232
|
+
justifyContent: 'space-between',
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
<div>
|
|
236
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
237
|
+
<span
|
|
238
|
+
style={{
|
|
239
|
+
fontSize: '11px',
|
|
240
|
+
fontWeight: 700,
|
|
241
|
+
textTransform: 'uppercase',
|
|
242
|
+
letterSpacing: '0.05em',
|
|
243
|
+
padding: '3px 10px',
|
|
244
|
+
borderRadius: '12px',
|
|
245
|
+
background: 'rgba(196, 181, 253, 0.25)',
|
|
246
|
+
border: '1px solid rgba(221, 214, 254, 0.4)',
|
|
247
|
+
color: '#f5f3ff',
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
📄 Normal Card (No Link)
|
|
251
|
+
</span>
|
|
252
|
+
</div>
|
|
253
|
+
<h2 style={{ margin: '0 0 8px 0', fontSize: '22px', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
|
254
|
+
Configurable Controls & Hooks
|
|
255
|
+
</h2>
|
|
256
|
+
<p style={{ margin: 0, fontSize: '14px', lineHeight: '1.5', color: '#ddd6fe', maxWidth: '540px' }}>
|
|
257
|
+
Configure autoplay duration, dot indicators, arrow toggles, hover pause, or take programmatic control via hook moveTo(), setIndex(), and setPause().
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
261
|
+
<span style={{ fontSize: '12px', color: '#c4b5fd' }}>
|
|
262
|
+
💡 Tip: Hovering over any slide automatically pauses the autoplay cycle.
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
),
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export const slidePageDemo: DemoStory<SlidePageProps> = {
|
|
271
|
+
id: 'slide-page-demo',
|
|
272
|
+
text: 'Slide Page',
|
|
273
|
+
args: {
|
|
274
|
+
pages: samplePages,
|
|
275
|
+
autoplay: true,
|
|
276
|
+
interval: 4000,
|
|
277
|
+
showDots: true,
|
|
278
|
+
showArrows: true,
|
|
279
|
+
pauseOnHover: true,
|
|
280
|
+
},
|
|
281
|
+
argTypes: {
|
|
282
|
+
autoplay: {
|
|
283
|
+
control: 'boolean',
|
|
284
|
+
description: 'Automatically rotate slides at a set interval',
|
|
285
|
+
},
|
|
286
|
+
interval: {
|
|
287
|
+
control: 'number',
|
|
288
|
+
description: 'Autoplay rotation interval in milliseconds',
|
|
289
|
+
},
|
|
290
|
+
showDots: {
|
|
291
|
+
control: 'boolean',
|
|
292
|
+
description: 'Display navigation dot indicators at the bottom',
|
|
293
|
+
},
|
|
294
|
+
showArrows: {
|
|
295
|
+
control: 'boolean',
|
|
296
|
+
description: 'Display left and right arrow buttons',
|
|
297
|
+
},
|
|
298
|
+
pauseOnHover: {
|
|
299
|
+
control: 'boolean',
|
|
300
|
+
description: 'Pause autoplay rotation when mouse hovers over slide',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
render: (args: SlidePageProps) => {
|
|
304
|
+
return (
|
|
305
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '100%', maxWidth: '800px' }}>
|
|
306
|
+
<div
|
|
307
|
+
style={{
|
|
308
|
+
height: '240px',
|
|
309
|
+
width: '100%',
|
|
310
|
+
display: 'flex',
|
|
311
|
+
flexDirection: 'column',
|
|
312
|
+
border: 'var(--primary-border, 1px solid rgba(60, 60, 67, 0.15))',
|
|
313
|
+
borderRadius: '16px',
|
|
314
|
+
overflow: 'hidden',
|
|
315
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12)',
|
|
316
|
+
position: 'relative',
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<SlidePageComponent {...args} />
|
|
320
|
+
</div>
|
|
321
|
+
<div
|
|
322
|
+
style={{
|
|
323
|
+
padding: '12px 16px',
|
|
324
|
+
borderRadius: '8px',
|
|
325
|
+
background: 'var(--bg-container, #f2f2f7)',
|
|
326
|
+
fontSize: '13px',
|
|
327
|
+
color: 'var(--text-secondary, #64748b)',
|
|
328
|
+
lineHeight: '1.5',
|
|
329
|
+
display: 'flex',
|
|
330
|
+
flexDirection: 'column',
|
|
331
|
+
gap: '4px',
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<div>
|
|
335
|
+
<strong>Interactive behavior:</strong> Items with <code>link</code> are rendered as clickable elements (e.g. Slide 1 & Slide 3) with pointer cursor and hover states. Items without <code>link</code> (Slide 2 & Slide 4) render as standard content cards.
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
code: `import { SlidePageComponent } from 'lupine.components';
|
|
342
|
+
|
|
343
|
+
const pages = [
|
|
344
|
+
{
|
|
345
|
+
title: 'Slide with Link',
|
|
346
|
+
link: 'https://example.com',
|
|
347
|
+
target: '_blank',
|
|
348
|
+
content: <div>Clickable Slide Content</div>,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
title: 'Regular Slide Card',
|
|
352
|
+
content: <div>Standard Non-Clickable Card Content</div>,
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
<SlidePageComponent
|
|
357
|
+
pages={pages}
|
|
358
|
+
autoplay={true}
|
|
359
|
+
interval={4000}
|
|
360
|
+
showDots={true}
|
|
361
|
+
showArrows={true}
|
|
362
|
+
/>
|
|
363
|
+
`,
|
|
364
|
+
};
|
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { CssProps, RefProps, VNode, bindGlobalStyle } from 'lupine.components';
|
|
2
|
+
|
|
3
|
+
export interface SlidePageHookProps {
|
|
4
|
+
moveTo?: (index: number) => void;
|
|
5
|
+
getIndex?: () => number;
|
|
6
|
+
setIndex?: (index: number) => void;
|
|
7
|
+
setPause?: (pause: boolean) => void;
|
|
8
|
+
isPaused?: () => boolean;
|
|
9
|
+
togglePause?: () => boolean;
|
|
10
|
+
next?: () => void;
|
|
11
|
+
prev?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SlidePageItemProps {
|
|
15
|
+
id?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
content: VNode<any> | any;
|
|
18
|
+
link?: string;
|
|
19
|
+
target?: string;
|
|
20
|
+
onClick?: (event: any, page: SlidePageItemProps, index: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SlidePageItem = SlidePageItemProps;
|
|
24
|
+
|
|
25
|
+
export interface SlidePageProps {
|
|
26
|
+
pages: SlidePageItemProps[];
|
|
27
|
+
hook?: SlidePageHookProps;
|
|
28
|
+
initialIndex?: number;
|
|
29
|
+
index?: number;
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
interval?: number;
|
|
32
|
+
showDots?: boolean;
|
|
33
|
+
dotsPosition?: 'inside' | 'outside';
|
|
34
|
+
showArrows?: boolean;
|
|
35
|
+
pause?: boolean;
|
|
36
|
+
pauseOnHover?: boolean;
|
|
37
|
+
pauseOnClick?: boolean;
|
|
38
|
+
pauseOnInteraction?: boolean;
|
|
39
|
+
draggable?: boolean;
|
|
40
|
+
onChange?: (index: number, page: SlidePageItemProps) => void;
|
|
41
|
+
class?: string;
|
|
42
|
+
style?: CssProps;
|
|
43
|
+
dotClass?: string;
|
|
44
|
+
height?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const SlidePageComponent = (props: SlidePageProps) => {
|
|
48
|
+
if (!props.pages || props.pages.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const css: CssProps = {
|
|
53
|
+
display: 'flex',
|
|
54
|
+
flexDirection: 'column',
|
|
55
|
+
flex: 1,
|
|
56
|
+
width: '100%',
|
|
57
|
+
height: props.height || '100%',
|
|
58
|
+
minHeight: '160px',
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
position: 'relative',
|
|
61
|
+
boxSizing: 'border-box',
|
|
62
|
+
|
|
63
|
+
'.slide-page-c-list': {
|
|
64
|
+
flex: 1,
|
|
65
|
+
display: 'flex',
|
|
66
|
+
flexDirection: 'row',
|
|
67
|
+
overflowX: 'auto',
|
|
68
|
+
overflowY: 'hidden',
|
|
69
|
+
width: '100%',
|
|
70
|
+
height: '100%',
|
|
71
|
+
scrollSnapType: 'x mandatory',
|
|
72
|
+
scrollBehavior: 'smooth',
|
|
73
|
+
WebkitOverflowScrolling: 'touch',
|
|
74
|
+
boxSizing: 'border-box',
|
|
75
|
+
scrollbarWidth: 'none',
|
|
76
|
+
cursor: 'grab',
|
|
77
|
+
userSelect: 'none',
|
|
78
|
+
WebkitUserSelect: 'none',
|
|
79
|
+
touchAction: 'pan-y pinch-zoom',
|
|
80
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
81
|
+
'&.is-dragging': {
|
|
82
|
+
cursor: 'grabbing',
|
|
83
|
+
scrollSnapType: 'none !important',
|
|
84
|
+
scrollBehavior: 'auto !important',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
'.slide-page-c-item': {
|
|
89
|
+
width: '100%',
|
|
90
|
+
minWidth: '100%',
|
|
91
|
+
maxWidth: '100%',
|
|
92
|
+
flexShrink: 0,
|
|
93
|
+
height: '100%',
|
|
94
|
+
overflowY: 'auto',
|
|
95
|
+
position: 'relative',
|
|
96
|
+
scrollSnapAlign: 'start',
|
|
97
|
+
scrollSnapStop: 'always',
|
|
98
|
+
boxSizing: 'border-box',
|
|
99
|
+
cursor: 'default',
|
|
100
|
+
display: 'flex',
|
|
101
|
+
flexDirection: 'column',
|
|
102
|
+
scrollbarWidth: 'none',
|
|
103
|
+
WebkitOverflowScrolling: 'touch',
|
|
104
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
'.slide-page-c-item.slide-page-c-item-link': {
|
|
108
|
+
cursor: 'pointer',
|
|
109
|
+
textDecoration: 'none',
|
|
110
|
+
color: 'inherit',
|
|
111
|
+
transition: 'opacity 0.2s ease, transform 0.2s ease',
|
|
112
|
+
WebkitTapHighlightColor: 'transparent',
|
|
113
|
+
'&:hover': {
|
|
114
|
+
opacity: 0.95,
|
|
115
|
+
},
|
|
116
|
+
'&:active': {
|
|
117
|
+
transform: 'scale(0.995)',
|
|
118
|
+
opacity: 0.88,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
'.slide-page-c-arrow': {
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
top: '50%',
|
|
125
|
+
transform: 'translateY(-50%)',
|
|
126
|
+
width: '32px',
|
|
127
|
+
height: '32px',
|
|
128
|
+
borderRadius: '50%',
|
|
129
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
130
|
+
border: '1px solid var(--secondary-border-color)',
|
|
131
|
+
color: 'var(--primary-color)',
|
|
132
|
+
fontFamily: 'monospace',
|
|
133
|
+
fontSize: '22px',
|
|
134
|
+
lineHeight: '1',
|
|
135
|
+
display: 'flex',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
cursor: 'pointer',
|
|
139
|
+
zIndex: 10,
|
|
140
|
+
backdropFilter: 'blur(8px)',
|
|
141
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
142
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
143
|
+
transition: 'all 0.2s ease',
|
|
144
|
+
userSelect: 'none',
|
|
145
|
+
WebkitTapHighlightColor: 'transparent',
|
|
146
|
+
padding: '0',
|
|
147
|
+
outline: 'none',
|
|
148
|
+
'&:hover': {
|
|
149
|
+
transform: 'translateY(-50%) scale(1.08)',
|
|
150
|
+
opacity: 0.92,
|
|
151
|
+
},
|
|
152
|
+
'&:active': {
|
|
153
|
+
transform: 'translateY(-50%) scale(0.96)',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
'.slide-page-c-arrow-prev': {
|
|
157
|
+
left: '12px',
|
|
158
|
+
},
|
|
159
|
+
'.slide-page-c-arrow-next': {
|
|
160
|
+
right: '12px',
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
'.slide-page-c-dots': {
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
bottom: '12px',
|
|
166
|
+
left: '0',
|
|
167
|
+
right: '0',
|
|
168
|
+
display: 'flex',
|
|
169
|
+
flexDirection: 'row',
|
|
170
|
+
justifyContent: 'center',
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
gap: '8px',
|
|
173
|
+
zIndex: 10,
|
|
174
|
+
pointerEvents: 'auto',
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
'.slide-page-c-dots.dots-outside': {
|
|
178
|
+
position: 'relative',
|
|
179
|
+
bottom: 'auto',
|
|
180
|
+
marginTop: '10px',
|
|
181
|
+
marginBottom: '4px',
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
'.slide-page-c-dot': {
|
|
185
|
+
cursor: 'pointer',
|
|
186
|
+
width: '8px',
|
|
187
|
+
height: '8px',
|
|
188
|
+
borderRadius: '4px',
|
|
189
|
+
backgroundColor: 'var(--secondary-color)',
|
|
190
|
+
opacity: 0.5,
|
|
191
|
+
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
192
|
+
WebkitTapHighlightColor: 'transparent',
|
|
193
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.15)',
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
'.slide-page-c-dot.active': {
|
|
197
|
+
width: '24px',
|
|
198
|
+
borderRadius: '4px',
|
|
199
|
+
backgroundColor: 'var(--primary-accent-color)',
|
|
200
|
+
opacity: 1,
|
|
201
|
+
boxShadow: '0 2px 6px rgba(2, 132, 199, 0.4)',
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
'.slide-page-c-dot:hover': {
|
|
205
|
+
backgroundColor: 'var(--primary-accent-color)',
|
|
206
|
+
opacity: 0.85,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
bindGlobalStyle('slide-page-c-box', css);
|
|
211
|
+
|
|
212
|
+
const ref: RefProps = {};
|
|
213
|
+
let slideIndex = props.initialIndex ?? props.index ?? 0;
|
|
214
|
+
if (slideIndex < 0 || slideIndex >= props.pages.length) {
|
|
215
|
+
slideIndex = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let manualScroll = false;
|
|
219
|
+
let scrollEndTimer: any = null;
|
|
220
|
+
let loopTimer: any = null;
|
|
221
|
+
let isHovered = false;
|
|
222
|
+
let isUserPaused = props.pause || false;
|
|
223
|
+
|
|
224
|
+
const autoplay = props.autoplay ?? true;
|
|
225
|
+
const interval = props.interval || 4000;
|
|
226
|
+
|
|
227
|
+
const resetSlides = (index: number) => {
|
|
228
|
+
const dots = ref.$all?.('.slide-page-c-dot');
|
|
229
|
+
if (dots) {
|
|
230
|
+
for (let i = 0; i < dots.length; i++) {
|
|
231
|
+
dots[i].classList.toggle('active', i === index);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (props.onChange && props.pages[index]) {
|
|
235
|
+
props.onChange(index, props.pages[index]);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const pauseAutoplay = (userPaused: boolean = false) => {
|
|
240
|
+
if (loopTimer) {
|
|
241
|
+
clearTimeout(loopTimer);
|
|
242
|
+
loopTimer = null;
|
|
243
|
+
}
|
|
244
|
+
if (scrollEndTimer) {
|
|
245
|
+
clearTimeout(scrollEndTimer);
|
|
246
|
+
scrollEndTimer = null;
|
|
247
|
+
}
|
|
248
|
+
if (userPaused) {
|
|
249
|
+
isUserPaused = true;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const canHover = () => {
|
|
254
|
+
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
255
|
+
return window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const scheduleAutoplay = (customDelay?: number) => {
|
|
259
|
+
if (loopTimer) {
|
|
260
|
+
clearTimeout(loopTimer);
|
|
261
|
+
loopTimer = null;
|
|
262
|
+
}
|
|
263
|
+
if (!autoplay || props.pages.length <= 1 || isUserPaused) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const container = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
267
|
+
// If container is not yet connected or has width <= 0 (e.g. parent tab hidden),
|
|
268
|
+
// do not abandon autoplay; poll every 1s until visible again
|
|
269
|
+
if (!container || !container.isConnected || container.clientWidth <= 0) {
|
|
270
|
+
loopTimer = setTimeout(() => scheduleAutoplay(), 1000);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// If user is currently dragging or touching, wait and retry
|
|
274
|
+
if (isPointerDown || isTouching || isDragging || isInteracting) {
|
|
275
|
+
loopTimer = setTimeout(() => scheduleAutoplay(), 1500);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// If hovered on a device with hover support, pause temporarily and check back
|
|
279
|
+
if (isHovered && props.pauseOnHover !== false && canHover()) {
|
|
280
|
+
loopTimer = setTimeout(() => scheduleAutoplay(), 2500);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const delay = customDelay !== undefined ? customDelay : interval;
|
|
284
|
+
loopTimer = setTimeout(() => {
|
|
285
|
+
autoNextSlide();
|
|
286
|
+
}, delay);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const autoNextSlide = () => {
|
|
290
|
+
const container = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
291
|
+
if (!container || !container.isConnected || container.clientWidth <= 0) {
|
|
292
|
+
scheduleAutoplay(1000);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
isUserPaused ||
|
|
297
|
+
isPointerDown ||
|
|
298
|
+
isTouching ||
|
|
299
|
+
isDragging ||
|
|
300
|
+
isInteracting
|
|
301
|
+
) {
|
|
302
|
+
scheduleAutoplay(1500);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (isHovered && props.pauseOnHover !== false && canHover()) {
|
|
306
|
+
scheduleAutoplay(2500);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const nextIndex = (slideIndex + 1) % props.pages.length;
|
|
311
|
+
moveSlide(nextIndex);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let isPointerDown = false;
|
|
315
|
+
let isTouching = false;
|
|
316
|
+
let isDragging = false;
|
|
317
|
+
let isInteracting = false;
|
|
318
|
+
let wasDragging = false;
|
|
319
|
+
let startX = 0;
|
|
320
|
+
let startY = 0;
|
|
321
|
+
let lastX = 0;
|
|
322
|
+
let startScrollLeft = 0;
|
|
323
|
+
let pointerDownTime = 0;
|
|
324
|
+
|
|
325
|
+
const drawerScrollStop = () => {
|
|
326
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
327
|
+
if (!drawer) return;
|
|
328
|
+
const width = drawer.clientWidth;
|
|
329
|
+
if (width <= 0) {
|
|
330
|
+
scheduleAutoplay(1000);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const currentScrollIndex = Math.round(drawer.scrollLeft / width);
|
|
334
|
+
if (currentScrollIndex !== slideIndex && currentScrollIndex >= 0 && currentScrollIndex < props.pages.length) {
|
|
335
|
+
slideIndex = currentScrollIndex;
|
|
336
|
+
resetSlides(slideIndex);
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
!isPointerDown &&
|
|
340
|
+
!isTouching &&
|
|
341
|
+
!isDragging &&
|
|
342
|
+
!isInteracting &&
|
|
343
|
+
!isUserPaused
|
|
344
|
+
) {
|
|
345
|
+
scheduleAutoplay();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const drawerScroll = () => {
|
|
350
|
+
if (isPointerDown || isTouching || isDragging || isInteracting) {
|
|
351
|
+
pauseAutoplay(false);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (manualScroll) return;
|
|
355
|
+
pauseAutoplay(false);
|
|
356
|
+
if (scrollEndTimer) clearTimeout(scrollEndTimer);
|
|
357
|
+
scrollEndTimer = setTimeout(() => {
|
|
358
|
+
drawerScrollStop();
|
|
359
|
+
}, 120);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const moveSlide = (targetIndex: number) => {
|
|
363
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
364
|
+
if (!drawer) {
|
|
365
|
+
scheduleAutoplay(1000);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const width = drawer.clientWidth;
|
|
369
|
+
if (width <= 0) {
|
|
370
|
+
scheduleAutoplay(1000);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (props.pages.length > 0) {
|
|
375
|
+
targetIndex = (targetIndex + props.pages.length) % props.pages.length;
|
|
376
|
+
}
|
|
377
|
+
manualScroll = true;
|
|
378
|
+
slideIndex = targetIndex;
|
|
379
|
+
resetSlides(slideIndex);
|
|
380
|
+
|
|
381
|
+
drawer.scrollTo({
|
|
382
|
+
left: targetIndex * width,
|
|
383
|
+
behavior: 'smooth',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
manualScroll = false;
|
|
388
|
+
isInteracting = false;
|
|
389
|
+
if (
|
|
390
|
+
!isPointerDown &&
|
|
391
|
+
!isTouching &&
|
|
392
|
+
!isDragging &&
|
|
393
|
+
!isUserPaused
|
|
394
|
+
) {
|
|
395
|
+
scheduleAutoplay();
|
|
396
|
+
}
|
|
397
|
+
}, 350);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const handleStart = (clientX: number, clientY: number, pointerId?: number) => {
|
|
401
|
+
if (props.draggable === false) return;
|
|
402
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
403
|
+
if (!drawer) return;
|
|
404
|
+
|
|
405
|
+
isPointerDown = true;
|
|
406
|
+
isTouching = true;
|
|
407
|
+
isInteracting = true;
|
|
408
|
+
isDragging = false;
|
|
409
|
+
wasDragging = false;
|
|
410
|
+
startX = clientX;
|
|
411
|
+
startY = clientY;
|
|
412
|
+
lastX = clientX;
|
|
413
|
+
startScrollLeft = drawer.scrollLeft;
|
|
414
|
+
pointerDownTime = Date.now();
|
|
415
|
+
|
|
416
|
+
// Immediately freeze/pause autoplay while pressing down
|
|
417
|
+
pauseAutoplay(false);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const handleMove = (clientX: number, clientY: number, e?: any) => {
|
|
421
|
+
if (!isPointerDown && !isTouching) return;
|
|
422
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
423
|
+
if (!drawer) return;
|
|
424
|
+
|
|
425
|
+
lastX = clientX;
|
|
426
|
+
const deltaX = clientX - startX;
|
|
427
|
+
const deltaY = clientY - startY;
|
|
428
|
+
|
|
429
|
+
// Immediately freeze/pause autoplay while sliding
|
|
430
|
+
pauseAutoplay(false);
|
|
431
|
+
|
|
432
|
+
if (!isDragging) {
|
|
433
|
+
// Highly responsive drag detection (2px threshold)
|
|
434
|
+
if (Math.abs(deltaX) > 2) {
|
|
435
|
+
isDragging = true;
|
|
436
|
+
wasDragging = true;
|
|
437
|
+
manualScroll = true;
|
|
438
|
+
if (e && e.pointerId !== undefined) {
|
|
439
|
+
try {
|
|
440
|
+
drawer.setPointerCapture?.(e.pointerId);
|
|
441
|
+
} catch { }
|
|
442
|
+
}
|
|
443
|
+
drawer.classList.add('is-dragging');
|
|
444
|
+
drawer.style.scrollSnapType = 'none';
|
|
445
|
+
drawer.style.scrollBehavior = 'auto';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (isDragging) {
|
|
450
|
+
if (e && e.cancelable) e.preventDefault();
|
|
451
|
+
drawer.scrollLeft = startScrollLeft - deltaX;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const handleEnd = (clientX?: number, e?: any) => {
|
|
456
|
+
if (!isPointerDown && !isTouching) return;
|
|
457
|
+
isPointerDown = false;
|
|
458
|
+
isTouching = false;
|
|
459
|
+
|
|
460
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
461
|
+
if (!drawer) return;
|
|
462
|
+
|
|
463
|
+
if (isDragging) {
|
|
464
|
+
isDragging = false;
|
|
465
|
+
drawer.classList.remove('is-dragging');
|
|
466
|
+
drawer.style.scrollSnapType = 'x mandatory';
|
|
467
|
+
drawer.style.scrollBehavior = 'smooth';
|
|
468
|
+
if (e && e.pointerId !== undefined) {
|
|
469
|
+
try {
|
|
470
|
+
drawer.releasePointerCapture?.(e.pointerId);
|
|
471
|
+
} catch { }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const finalX = clientX !== undefined ? clientX : lastX;
|
|
475
|
+
const deltaX = finalX - startX;
|
|
476
|
+
const elapsed = Math.max(Date.now() - pointerDownTime, 1);
|
|
477
|
+
const velocity = Math.abs(deltaX) / elapsed;
|
|
478
|
+
const width = drawer.clientWidth || 1;
|
|
479
|
+
|
|
480
|
+
if (Math.abs(deltaX) > width * 0.15 || velocity > 0.25) {
|
|
481
|
+
const direction = deltaX < 0 ? 1 : -1;
|
|
482
|
+
moveSlide(slideIndex + direction);
|
|
483
|
+
} else {
|
|
484
|
+
moveSlide(slideIndex);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
// Clean tap without drag
|
|
488
|
+
if (props.pauseOnClick) {
|
|
489
|
+
props.hook?.togglePause ? props.hook.togglePause() : (isUserPaused = !isUserPaused);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setTimeout(() => {
|
|
494
|
+
wasDragging = false;
|
|
495
|
+
manualScroll = false;
|
|
496
|
+
isInteracting = false;
|
|
497
|
+
// Resume autoplay after releasing if not user paused
|
|
498
|
+
if (!isPointerDown && !isTouching && !isUserPaused) {
|
|
499
|
+
scheduleAutoplay();
|
|
500
|
+
}
|
|
501
|
+
}, 120);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
if (props.hook) {
|
|
505
|
+
props.hook.moveTo = moveSlide;
|
|
506
|
+
props.hook.setIndex = moveSlide;
|
|
507
|
+
props.hook.getIndex = () => slideIndex;
|
|
508
|
+
props.hook.isPaused = () => isUserPaused;
|
|
509
|
+
props.hook.setPause = (inPause: boolean) => {
|
|
510
|
+
isUserPaused = inPause;
|
|
511
|
+
if (inPause) {
|
|
512
|
+
pauseAutoplay(true);
|
|
513
|
+
} else {
|
|
514
|
+
scheduleAutoplay();
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
props.hook.togglePause = () => {
|
|
518
|
+
const next = !isUserPaused;
|
|
519
|
+
props.hook?.setPause?.(next);
|
|
520
|
+
return next;
|
|
521
|
+
};
|
|
522
|
+
props.hook.next = () => moveSlide(slideIndex + 1);
|
|
523
|
+
props.hook.prev = () => moveSlide(slideIndex - 1);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const handleMouseEnter = () => {
|
|
527
|
+
if (props.pauseOnHover !== false && canHover()) {
|
|
528
|
+
isHovered = true;
|
|
529
|
+
pauseAutoplay(false);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const handleMouseLeave = () => {
|
|
534
|
+
isHovered = false;
|
|
535
|
+
if (
|
|
536
|
+
!isPointerDown &&
|
|
537
|
+
!isTouching &&
|
|
538
|
+
!isDragging &&
|
|
539
|
+
!isInteracting &&
|
|
540
|
+
!isUserPaused
|
|
541
|
+
) {
|
|
542
|
+
scheduleAutoplay();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
547
|
+
let intersectionObserver: IntersectionObserver | null = null;
|
|
548
|
+
let watchdogTimer: any = null;
|
|
549
|
+
|
|
550
|
+
const wakeUpAutoplay = () => {
|
|
551
|
+
isPointerDown = false;
|
|
552
|
+
isTouching = false;
|
|
553
|
+
isDragging = false;
|
|
554
|
+
isInteracting = false;
|
|
555
|
+
isHovered = false;
|
|
556
|
+
if (autoplay && !isUserPaused) {
|
|
557
|
+
scheduleAutoplay(400);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
ref.onLoad = async () => {
|
|
562
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
563
|
+
if (drawer) {
|
|
564
|
+
drawer.addEventListener('scrollend', drawerScrollStop);
|
|
565
|
+
|
|
566
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
567
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
if (entry.contentRect.width > 0) {
|
|
570
|
+
if (autoplay && !isUserPaused && !loopTimer) {
|
|
571
|
+
scheduleAutoplay(400);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
resizeObserver.observe(drawer);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
580
|
+
intersectionObserver = new IntersectionObserver((entries) => {
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
if (entry.isIntersecting) {
|
|
583
|
+
if (autoplay && !isUserPaused && !loopTimer) {
|
|
584
|
+
scheduleAutoplay(400);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
intersectionObserver.observe(drawer);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (typeof window !== 'undefined') {
|
|
594
|
+
window.addEventListener('focus', wakeUpAutoplay);
|
|
595
|
+
window.addEventListener('updateTheme', wakeUpAutoplay);
|
|
596
|
+
window.addEventListener('theme-bg-change', wakeUpAutoplay);
|
|
597
|
+
window.addEventListener('id-photo-theme-changed', wakeUpAutoplay);
|
|
598
|
+
window.addEventListener('id-photo-tab-switched', wakeUpAutoplay);
|
|
599
|
+
document.addEventListener('visibilitychange', () => {
|
|
600
|
+
if (document.visibilityState === 'visible') {
|
|
601
|
+
wakeUpAutoplay();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Watchdog timer: every 8s ensure rotation hasn't stalled unexpectedly
|
|
607
|
+
watchdogTimer = setInterval(() => {
|
|
608
|
+
if (autoplay && !isUserPaused && !loopTimer) {
|
|
609
|
+
scheduleAutoplay();
|
|
610
|
+
}
|
|
611
|
+
}, 8000);
|
|
612
|
+
|
|
613
|
+
resetSlides(slideIndex);
|
|
614
|
+
if (slideIndex > 0) {
|
|
615
|
+
setTimeout(() => {
|
|
616
|
+
moveSlide(slideIndex);
|
|
617
|
+
}, 50);
|
|
618
|
+
}
|
|
619
|
+
scheduleAutoplay();
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
ref.onUnload = async () => {
|
|
623
|
+
const drawer = ref.$?.('.slide-page-c-list') as HTMLElement | undefined;
|
|
624
|
+
if (drawer) {
|
|
625
|
+
drawer.removeEventListener('scrollend', drawerScrollStop);
|
|
626
|
+
}
|
|
627
|
+
if (resizeObserver) {
|
|
628
|
+
resizeObserver.disconnect();
|
|
629
|
+
resizeObserver = null;
|
|
630
|
+
}
|
|
631
|
+
if (intersectionObserver) {
|
|
632
|
+
intersectionObserver.disconnect();
|
|
633
|
+
intersectionObserver = null;
|
|
634
|
+
}
|
|
635
|
+
if (watchdogTimer) {
|
|
636
|
+
clearInterval(watchdogTimer);
|
|
637
|
+
watchdogTimer = null;
|
|
638
|
+
}
|
|
639
|
+
if (typeof window !== 'undefined') {
|
|
640
|
+
window.removeEventListener('focus', wakeUpAutoplay);
|
|
641
|
+
window.removeEventListener('updateTheme', wakeUpAutoplay);
|
|
642
|
+
window.removeEventListener('theme-bg-change', wakeUpAutoplay);
|
|
643
|
+
window.removeEventListener('id-photo-theme-changed', wakeUpAutoplay);
|
|
644
|
+
window.removeEventListener('id-photo-tab-switched', wakeUpAutoplay);
|
|
645
|
+
document.removeEventListener('visibilitychange', wakeUpAutoplay);
|
|
646
|
+
}
|
|
647
|
+
pauseAutoplay(false);
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const handleItemClick = (event: any, page: SlidePageItemProps, index: number) => {
|
|
651
|
+
if (wasDragging) {
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
event.stopPropagation();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (page.onClick) {
|
|
657
|
+
page.onClick(event, page, index);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
return (
|
|
662
|
+
<section
|
|
663
|
+
class={`slide-page-c-box ${props.class || ''}`}
|
|
664
|
+
ref={ref}
|
|
665
|
+
css={props.style}
|
|
666
|
+
onMouseEnter={handleMouseEnter}
|
|
667
|
+
onMouseLeave={handleMouseLeave}
|
|
668
|
+
>
|
|
669
|
+
<div
|
|
670
|
+
class="slide-page-c-list no-scrollbar-container"
|
|
671
|
+
onScroll={drawerScroll}
|
|
672
|
+
onPointerDown={(e: any) => {
|
|
673
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
674
|
+
handleStart(e.clientX, e.clientY, e.pointerId);
|
|
675
|
+
}}
|
|
676
|
+
onPointerMove={(e: any) => handleMove(e.clientX, e.clientY, e)}
|
|
677
|
+
onPointerUp={(e: any) => handleEnd(e.clientX, e)}
|
|
678
|
+
onPointerCancel={(e: any) => handleEnd(e.clientX, e)}
|
|
679
|
+
onTouchStart={(e: any) => {
|
|
680
|
+
if (e.touches && e.touches[0]) {
|
|
681
|
+
handleStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
682
|
+
}
|
|
683
|
+
}}
|
|
684
|
+
onTouchMove={(e: any) => {
|
|
685
|
+
if (e.touches && e.touches[0]) {
|
|
686
|
+
handleMove(e.touches[0].clientX, e.touches[0].clientY, e);
|
|
687
|
+
}
|
|
688
|
+
}}
|
|
689
|
+
onTouchEnd={(e: any) => {
|
|
690
|
+
const t = e.changedTouches?.[0];
|
|
691
|
+
handleEnd(t ? t.clientX : undefined, e);
|
|
692
|
+
}}
|
|
693
|
+
onTouchCancel={(e: any) => {
|
|
694
|
+
const t = e.changedTouches?.[0];
|
|
695
|
+
handleEnd(t ? t.clientX : undefined, e);
|
|
696
|
+
}}
|
|
697
|
+
onMouseDown={(e: any) => {
|
|
698
|
+
if (e.button !== 0) return;
|
|
699
|
+
handleStart(e.clientX, e.clientY);
|
|
700
|
+
}}
|
|
701
|
+
>
|
|
702
|
+
{props.pages.map((page, index) => {
|
|
703
|
+
if (page.link) {
|
|
704
|
+
return (
|
|
705
|
+
<a
|
|
706
|
+
class={`slide-page-c-item slide-page-c-item-link ${page.id ? `slide-page-item-${page.id}` : ''}`}
|
|
707
|
+
href={page.link}
|
|
708
|
+
target={page.target || '_self'}
|
|
709
|
+
title={page.title}
|
|
710
|
+
onClick={(event: any) => handleItemClick(event, page, index)}
|
|
711
|
+
>
|
|
712
|
+
{page.content}
|
|
713
|
+
</a>
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
return (
|
|
717
|
+
<div
|
|
718
|
+
class={`slide-page-c-item ${page.id ? `slide-page-item-${page.id}` : ''}`}
|
|
719
|
+
title={page.title}
|
|
720
|
+
onClick={(event: any) => handleItemClick(event, page, index)}
|
|
721
|
+
>
|
|
722
|
+
{page.content}
|
|
723
|
+
</div>
|
|
724
|
+
);
|
|
725
|
+
})}
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
{props.showArrows && props.pages.length > 1 && (
|
|
729
|
+
<>
|
|
730
|
+
<button
|
|
731
|
+
type="button"
|
|
732
|
+
class="slide-page-c-arrow slide-page-c-arrow-prev"
|
|
733
|
+
aria-label="Previous slide"
|
|
734
|
+
onClick={(e: any) => {
|
|
735
|
+
e.preventDefault();
|
|
736
|
+
e.stopPropagation();
|
|
737
|
+
moveSlide(slideIndex - 1);
|
|
738
|
+
scheduleAutoplay(interval);
|
|
739
|
+
}}
|
|
740
|
+
>
|
|
741
|
+
‹
|
|
742
|
+
</button>
|
|
743
|
+
<button
|
|
744
|
+
type="button"
|
|
745
|
+
class="slide-page-c-arrow slide-page-c-arrow-next"
|
|
746
|
+
aria-label="Next slide"
|
|
747
|
+
onClick={(e: any) => {
|
|
748
|
+
e.preventDefault();
|
|
749
|
+
e.stopPropagation();
|
|
750
|
+
moveSlide(slideIndex + 1);
|
|
751
|
+
scheduleAutoplay(interval);
|
|
752
|
+
}}
|
|
753
|
+
>
|
|
754
|
+
›
|
|
755
|
+
</button>
|
|
756
|
+
</>
|
|
757
|
+
)}
|
|
758
|
+
|
|
759
|
+
{props.showDots !== false && props.pages.length > 1 && (
|
|
760
|
+
<div class={`slide-page-c-dots ${props.dotsPosition === 'outside' ? 'dots-outside' : ''} ${props.dotClass || ''}`}>
|
|
761
|
+
{props.pages.map((page, index) => (
|
|
762
|
+
<div
|
|
763
|
+
class={`slide-page-c-dot ${index === slideIndex ? 'active' : ''}`}
|
|
764
|
+
title={page.title || `Page ${index + 1}`}
|
|
765
|
+
onClick={(event: any) => {
|
|
766
|
+
event.preventDefault();
|
|
767
|
+
event.stopPropagation();
|
|
768
|
+
moveSlide(index);
|
|
769
|
+
scheduleAutoplay(interval);
|
|
770
|
+
}}
|
|
771
|
+
/>
|
|
772
|
+
))}
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
775
|
+
</section>
|
|
776
|
+
);
|
|
777
|
+
};
|
|
@@ -35,8 +35,8 @@ export const SlideTabComponent = (props: SlideTabProps) => {
|
|
|
35
35
|
display: 'flex',
|
|
36
36
|
flexDirection: 'row',
|
|
37
37
|
alignItems: 'center',
|
|
38
|
-
backgroundColor: 'var(--
|
|
39
|
-
borderBottom: '
|
|
38
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
39
|
+
borderBottom: '1px solid var(--secondary-border-color)',
|
|
40
40
|
padding: '10px 16px 8px 16px',
|
|
41
41
|
overflowX: 'auto',
|
|
42
42
|
WebkitOverflowScrolling: 'touch',
|
|
@@ -59,8 +59,8 @@ export const SlideTabComponent = (props: SlideTabProps) => {
|
|
|
59
59
|
borderRadius: '20px',
|
|
60
60
|
fontSize: '13px',
|
|
61
61
|
fontWeight: 600,
|
|
62
|
-
color: 'var(--
|
|
63
|
-
backgroundColor: 'var(--bg-
|
|
62
|
+
color: 'var(--secondary-color)',
|
|
63
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
64
64
|
whiteSpace: 'nowrap',
|
|
65
65
|
transition: 'all 0.18s ease',
|
|
66
66
|
WebkitTapHighlightColor: 'transparent',
|
|
@@ -73,7 +73,7 @@ export const SlideTabComponent = (props: SlideTabProps) => {
|
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
'.slide-tab-c-nav-item.active': {
|
|
76
|
-
backgroundColor: 'var(--primary-accent-color
|
|
76
|
+
backgroundColor: 'var(--primary-accent-color)',
|
|
77
77
|
color: '#ffffff',
|
|
78
78
|
boxShadow: '0 2px 8px rgba(2, 132, 199, 0.35)',
|
|
79
79
|
},
|
|
@@ -54,6 +54,7 @@ import { searchInputDemo } from '../component-pool/search-input';
|
|
|
54
54
|
import { hEditorDemo } from '../component-pool/h-editor';
|
|
55
55
|
import { pagingLinkDemo } from '../components/paging-link-demo';
|
|
56
56
|
import { slideTabDemo } from '../components/slide-tab-component-demo';
|
|
57
|
+
import { slidePageDemo } from '../components/slide-page-component-demo';
|
|
57
58
|
import { qrcodeDemo } from '../component-pool/qrcode';
|
|
58
59
|
import { iEditorDemo } from '../component-pool/i-editor';
|
|
59
60
|
import { youtubePlayerDemo } from '../component-pool/youtube-player';
|
|
@@ -336,6 +337,12 @@ export class DemoFrameHelper {
|
|
|
336
337
|
url: '',
|
|
337
338
|
js: () => this.addPanel(slideTabDemo.text, <DemoPage story={slideTabDemo} />),
|
|
338
339
|
},
|
|
340
|
+
{
|
|
341
|
+
id: slidePageDemo.id,
|
|
342
|
+
text: slidePageDemo.text,
|
|
343
|
+
url: '',
|
|
344
|
+
js: () => this.addPanel(slidePageDemo.text, <DemoPage story={slidePageDemo} />),
|
|
345
|
+
},
|
|
339
346
|
{
|
|
340
347
|
id: pagingLinkDemo.id,
|
|
341
348
|
text: pagingLinkDemo.text,
|
|
@@ -52,6 +52,7 @@ import { searchInputDemo } from '../component-pool/search-input';
|
|
|
52
52
|
import { pagingLinkDemo } from '../components/paging-link-demo';
|
|
53
53
|
import { hEditorDemo } from '../component-pool/h-editor';
|
|
54
54
|
import { slideTabDemo } from '../components/slide-tab-component-demo';
|
|
55
|
+
import { slidePageDemo } from '../components/slide-page-component-demo';
|
|
55
56
|
import { qrcodeDemo } from '../component-pool/qrcode';
|
|
56
57
|
import { iEditorDemo } from '../component-pool/i-editor';
|
|
57
58
|
import { youtubePlayerDemo } from '../component-pool/youtube-player';
|
|
@@ -125,6 +126,7 @@ export const demoRegistry: Record<string, DemoStory<any>> = {
|
|
|
125
126
|
[pagingLinkDemo.id]: pagingLinkDemo,
|
|
126
127
|
[hEditorDemo.id]: hEditorDemo,
|
|
127
128
|
[slideTabDemo.id]: slideTabDemo,
|
|
129
|
+
[slidePageDemo.id]: slidePageDemo,
|
|
128
130
|
[qrcodeDemo.id]: qrcodeDemo,
|
|
129
131
|
[iEditorDemo.id]: iEditorDemo,
|
|
130
132
|
[youtubePlayerDemo.id]: youtubePlayerDemo,
|
|
@@ -38,7 +38,6 @@ export const ResponsiveFrame = (props: ResponsiveFrameProps) => {
|
|
|
38
38
|
margin: '0 auto',
|
|
39
39
|
overflowX: 'hidden',
|
|
40
40
|
...(!props.noSafeAreaTop ? {
|
|
41
|
-
'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '',
|
|
42
41
|
'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '',
|
|
43
42
|
} : {}),
|
|
44
43
|
'.frame-top-menu': {
|
|
@@ -24,6 +24,7 @@ export const sliderFrameDemo: DemoStory<any> = {
|
|
|
24
24
|
text: 'Slider Frame Demo',
|
|
25
25
|
args: {
|
|
26
26
|
direction: 'right',
|
|
27
|
+
addBottomSafearea: false,
|
|
27
28
|
},
|
|
28
29
|
argTypes: {
|
|
29
30
|
direction: {
|
|
@@ -31,6 +32,10 @@ export const sliderFrameDemo: DemoStory<any> = {
|
|
|
31
32
|
options: ['right', 'bottom'],
|
|
32
33
|
description: 'Direction the slider enters from',
|
|
33
34
|
},
|
|
35
|
+
addBottomSafearea: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Add safe area padding at bottom',
|
|
38
|
+
},
|
|
34
39
|
},
|
|
35
40
|
render: (args) => {
|
|
36
41
|
const sliderHook: SliderFrameHookProps = {};
|
|
@@ -54,6 +59,7 @@ export const sliderFrameDemo: DemoStory<any> = {
|
|
|
54
59
|
let close: SliderHelperCloseProps;
|
|
55
60
|
close = await SliderHelper.show({
|
|
56
61
|
direction: args.direction,
|
|
62
|
+
addBottomSafearea: args.addBottomSafearea,
|
|
57
63
|
children: (
|
|
58
64
|
<HeaderWithBackFrame title='Slider Helper Inner Page' onBack={() => close()}>
|
|
59
65
|
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
@@ -71,7 +77,12 @@ export const sliderFrameDemo: DemoStory<any> = {
|
|
|
71
77
|
</div>
|
|
72
78
|
|
|
73
79
|
{/* The SliderFrame is placed here but initially hidden */}
|
|
74
|
-
<SliderFrame
|
|
80
|
+
<SliderFrame
|
|
81
|
+
hook={sliderHook}
|
|
82
|
+
direction={args.direction}
|
|
83
|
+
addBottomSafearea={args.addBottomSafearea}
|
|
84
|
+
defaultContent=''
|
|
85
|
+
/>
|
|
75
86
|
</div>
|
|
76
87
|
);
|
|
77
88
|
},
|
|
@@ -16,7 +16,7 @@ import { MediaQueryMaxWidth, VNode, stopPropagation } from 'lupine.components';
|
|
|
16
16
|
import { SliderHelper, SliderHelperCloseProps } from './slider-helper';
|
|
17
17
|
|
|
18
18
|
export type SliderFrameHookProps = {
|
|
19
|
-
load?: (children: VNode<any
|
|
19
|
+
load?: (children: VNode<any>, addBottomSafearea?: boolean) => void;
|
|
20
20
|
close?: (event: Event) => void;
|
|
21
21
|
isOpened?: () => boolean;
|
|
22
22
|
};
|
|
@@ -27,18 +27,20 @@ export type SliderFrameProps = {
|
|
|
27
27
|
hook?: SliderFrameHookProps;
|
|
28
28
|
afterClose?: () => void | Promise<void>;
|
|
29
29
|
maxWidth?: string;
|
|
30
|
+
addBottomSafearea?: boolean;
|
|
30
31
|
};
|
|
31
32
|
export const SliderFrame = (props: SliderFrameProps) => {
|
|
32
33
|
let closeSlider: SliderHelperCloseProps | undefined;
|
|
33
34
|
let opened = false;
|
|
34
35
|
|
|
35
36
|
if (props.hook) {
|
|
36
|
-
props.hook.load = (children) => {
|
|
37
|
+
props.hook.load = (children, addBottomSafearea) => {
|
|
37
38
|
opened = true;
|
|
38
39
|
SliderHelper.show({
|
|
39
40
|
direction: props.direction || 'right',
|
|
40
41
|
children,
|
|
41
42
|
maxWidth: props.maxWidth,
|
|
43
|
+
addBottomSafearea: typeof addBottomSafearea === 'boolean' ? addBottomSafearea : (props.addBottomSafearea ?? false),
|
|
42
44
|
closeEvent: () => {
|
|
43
45
|
opened = false;
|
|
44
46
|
closeSlider = undefined;
|
|
@@ -16,6 +16,7 @@ export type SliderHelperShowProps = {
|
|
|
16
16
|
className?: string;
|
|
17
17
|
contentClassName?: string;
|
|
18
18
|
mountTarget?: HTMLElement;
|
|
19
|
+
addBottomSafearea?: boolean;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export class SliderHelper {
|
|
@@ -29,6 +30,7 @@ export class SliderHelper {
|
|
|
29
30
|
className = '',
|
|
30
31
|
contentClassName = '',
|
|
31
32
|
mountTarget,
|
|
33
|
+
addBottomSafearea = false,
|
|
32
34
|
}: SliderHelperShowProps): Promise<SliderHelperCloseProps> {
|
|
33
35
|
const base = document.createElement('div');
|
|
34
36
|
const isHorizontal = direction === 'left' || direction === 'right';
|
|
@@ -157,8 +159,12 @@ export class SliderHelper {
|
|
|
157
159
|
transform: startTransform,
|
|
158
160
|
transition: 'transform 0.4s ease-in-out',
|
|
159
161
|
// trick: to put two padding-top properties
|
|
160
|
-
'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '0',
|
|
161
162
|
'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '0',
|
|
163
|
+
...(addBottomSafearea
|
|
164
|
+
? {
|
|
165
|
+
'padding-bottom': isSafeAreaSupported() ? 'env(safe-area-inset-bottom)' : '0',
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
162
168
|
...contentCss,
|
|
163
169
|
'&::-webkit-scrollbar': {
|
|
164
170
|
display: 'none',
|
package/src/frames/top-frame.tsx
CHANGED
|
@@ -28,7 +28,6 @@ export const TopFrame = async ({
|
|
|
28
28
|
// trick: to put two padding-top properties
|
|
29
29
|
...(!noSafeAreaTop
|
|
30
30
|
? {
|
|
31
|
-
'padding-top ': isSafeAreaSupported() ? 'constant(safe-area-inset-top)' : '',
|
|
32
31
|
'padding-top': isSafeAreaSupported() ? 'env(safe-area-inset-top)' : '',
|
|
33
32
|
}
|
|
34
33
|
: {}),
|