lupine.components 1.1.58 → 1.1.59

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.components",
3
- "version": "1.1.58",
3
+ "version": "1.1.59",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -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(--card-bg, #ffffff)',
39
- borderBottom: '0.5px solid var(--card-border, rgba(60, 60, 67, 0.15))',
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(--text-secondary, #8e8e93)',
63
- backgroundColor: 'var(--bg-container, #f2f2f7)',
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, #0284c7)',
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 hook={sliderHook} direction={args.direction} defaultContent='' />
80
+ <SliderFrame
81
+ hook={sliderHook}
82
+ direction={args.direction}
83
+ addBottomSafearea={args.addBottomSafearea}
84
+ defaultContent=''
85
+ />
75
86
  </div>
76
87
  );
77
88
  },
@@ -27,6 +27,7 @@ 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;
@@ -39,6 +40,7 @@ export const SliderFrame = (props: SliderFrameProps) => {
39
40
  direction: props.direction || 'right',
40
41
  children,
41
42
  maxWidth: props.maxWidth,
43
+ 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',
@@ -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
  : {}),