@open-aippt/core 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- package/src/locale/zh-tw.ts +414 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { type MutableRefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { SlidePageProvider } from '../lib/page-context';
|
|
3
|
+
import type { Page } from '../lib/sdk';
|
|
4
|
+
import {
|
|
5
|
+
type EntryDirection,
|
|
6
|
+
type StepAggregate,
|
|
7
|
+
type StepController,
|
|
8
|
+
StepHost,
|
|
9
|
+
} from '../lib/step-context';
|
|
10
|
+
import {
|
|
11
|
+
resolveTransition,
|
|
12
|
+
type SharedElementTransition,
|
|
13
|
+
type SlideTransition,
|
|
14
|
+
type TransitionPhase,
|
|
15
|
+
} from '../lib/transition';
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
pages: Page[];
|
|
19
|
+
index: number;
|
|
20
|
+
total: number;
|
|
21
|
+
moduleTransition?: SlideTransition;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
stepControllerRef?: MutableRefObject<StepController | null>;
|
|
24
|
+
entryDirection?: EntryDirection;
|
|
25
|
+
onStepAggregateChange?: (aggregate: StepAggregate) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Direction = 'forward' | 'backward';
|
|
29
|
+
|
|
30
|
+
const DEFAULT_EASING = 'cubic-bezier(.4, 0, .2, 1)';
|
|
31
|
+
const SHARED_ELEMENT_SELECTOR = '[data-osd-shared-element]';
|
|
32
|
+
const TEXT_FILL_COLOR_PROPERTY = '-webkit-text-fill-color';
|
|
33
|
+
const SHARED_ELEMENT_VISUAL_PROPERTIES = [
|
|
34
|
+
'opacity',
|
|
35
|
+
'color',
|
|
36
|
+
'background-color',
|
|
37
|
+
'border-top-color',
|
|
38
|
+
'border-right-color',
|
|
39
|
+
'border-bottom-color',
|
|
40
|
+
'border-left-color',
|
|
41
|
+
'outline-color',
|
|
42
|
+
'text-decoration-color',
|
|
43
|
+
TEXT_FILL_COLOR_PROPERTY,
|
|
44
|
+
'-webkit-text-stroke-color',
|
|
45
|
+
'fill',
|
|
46
|
+
'stroke',
|
|
47
|
+
] as const;
|
|
48
|
+
const BORDER_COLOR_PROPERTIES = [
|
|
49
|
+
'border-top-color',
|
|
50
|
+
'border-right-color',
|
|
51
|
+
'border-bottom-color',
|
|
52
|
+
'border-left-color',
|
|
53
|
+
] as const;
|
|
54
|
+
const BORDER_SIDE_PROPERTIES = [
|
|
55
|
+
{
|
|
56
|
+
width: 'borderTopWidth',
|
|
57
|
+
style: 'borderTopStyle',
|
|
58
|
+
color: 'borderTopColor',
|
|
59
|
+
cssWidth: 'border-top-width',
|
|
60
|
+
cssStyle: 'border-top-style',
|
|
61
|
+
cssColor: 'border-top-color',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
width: 'borderRightWidth',
|
|
65
|
+
style: 'borderRightStyle',
|
|
66
|
+
color: 'borderRightColor',
|
|
67
|
+
cssWidth: 'border-right-width',
|
|
68
|
+
cssStyle: 'border-right-style',
|
|
69
|
+
cssColor: 'border-right-color',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
width: 'borderBottomWidth',
|
|
73
|
+
style: 'borderBottomStyle',
|
|
74
|
+
color: 'borderBottomColor',
|
|
75
|
+
cssWidth: 'border-bottom-width',
|
|
76
|
+
cssStyle: 'border-bottom-style',
|
|
77
|
+
cssColor: 'border-bottom-color',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
width: 'borderLeftWidth',
|
|
81
|
+
style: 'borderLeftStyle',
|
|
82
|
+
color: 'borderLeftColor',
|
|
83
|
+
cssWidth: 'border-left-width',
|
|
84
|
+
cssStyle: 'border-left-style',
|
|
85
|
+
cssColor: 'border-left-color',
|
|
86
|
+
},
|
|
87
|
+
] as const;
|
|
88
|
+
const INHERITED_VISUAL_PROPERTIES = new Set<string>([
|
|
89
|
+
'color',
|
|
90
|
+
'text-decoration-color',
|
|
91
|
+
TEXT_FILL_COLOR_PROPERTY,
|
|
92
|
+
'-webkit-text-stroke-color',
|
|
93
|
+
'fill',
|
|
94
|
+
'stroke',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
function runPhase(
|
|
98
|
+
el: HTMLElement,
|
|
99
|
+
phase: TransitionPhase | undefined,
|
|
100
|
+
fallbackDuration: number,
|
|
101
|
+
fallbackEasing: string,
|
|
102
|
+
): Animation | null {
|
|
103
|
+
if (!phase) return null;
|
|
104
|
+
return el.animate(phase.keyframes, {
|
|
105
|
+
duration: phase.duration ?? fallbackDuration,
|
|
106
|
+
easing: phase.easing ?? fallbackEasing,
|
|
107
|
+
delay: phase.delay ?? 0,
|
|
108
|
+
fill: 'both',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type ResolvedSharedElementTransition = Required<SharedElementTransition>;
|
|
113
|
+
|
|
114
|
+
function resolveSharedElementTransition(
|
|
115
|
+
sharedElements: SlideTransition['sharedElements'],
|
|
116
|
+
fallbackDuration: number,
|
|
117
|
+
fallbackEasing: string,
|
|
118
|
+
): ResolvedSharedElementTransition | null {
|
|
119
|
+
if (!sharedElements) return null;
|
|
120
|
+
if (sharedElements === true) {
|
|
121
|
+
return { duration: fallbackDuration, easing: fallbackEasing, delay: 0 };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
duration: sharedElements.duration ?? fallbackDuration,
|
|
125
|
+
easing: sharedElements.easing ?? fallbackEasing,
|
|
126
|
+
delay: sharedElements.delay ?? 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type LocalRect = {
|
|
131
|
+
left: number;
|
|
132
|
+
top: number;
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function measureUntransformedClientRect(el: HTMLElement): DOMRect {
|
|
138
|
+
const styles = getComputedStyle(el);
|
|
139
|
+
if (!localTransform(styles)) return el.getBoundingClientRect();
|
|
140
|
+
|
|
141
|
+
const value = el.style.getPropertyValue('transform');
|
|
142
|
+
const priority = el.style.getPropertyPriority('transform');
|
|
143
|
+
el.style.setProperty('transform', 'none', 'important');
|
|
144
|
+
try {
|
|
145
|
+
return el.getBoundingClientRect();
|
|
146
|
+
} finally {
|
|
147
|
+
if (value) el.style.setProperty('transform', value, priority);
|
|
148
|
+
else el.style.removeProperty('transform');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function measureLocalRect(el: HTMLElement, wrapper: HTMLElement, wrapperRect: DOMRect): LocalRect {
|
|
153
|
+
const rect = measureUntransformedClientRect(el);
|
|
154
|
+
const scaleX = wrapperRect.width / (wrapper.offsetWidth || wrapperRect.width || 1) || 1;
|
|
155
|
+
const scaleY = wrapperRect.height / (wrapper.offsetHeight || wrapperRect.height || 1) || 1;
|
|
156
|
+
return {
|
|
157
|
+
left: (rect.left - wrapperRect.left) / scaleX,
|
|
158
|
+
top: (rect.top - wrapperRect.top) / scaleY,
|
|
159
|
+
width: rect.width / scaleX,
|
|
160
|
+
height: rect.height / scaleY,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasUsableRect(rect: LocalRect): boolean {
|
|
165
|
+
return rect.width > 0 && rect.height > 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectSharedElements(root: HTMLElement): Map<string, HTMLElement> {
|
|
169
|
+
const elements = new Map<string, HTMLElement>();
|
|
170
|
+
for (const el of root.querySelectorAll<HTMLElement>(SHARED_ELEMENT_SELECTOR)) {
|
|
171
|
+
const id = el.dataset.osdSharedElement;
|
|
172
|
+
if (id && !elements.has(id)) elements.set(id, el);
|
|
173
|
+
}
|
|
174
|
+
return elements;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function shouldSkipCopiedStyle(
|
|
178
|
+
source: HTMLElement,
|
|
179
|
+
styles: CSSStyleDeclaration,
|
|
180
|
+
prop: string,
|
|
181
|
+
preserveInheritedVisualStyle: boolean,
|
|
182
|
+
): boolean {
|
|
183
|
+
if (prop === TEXT_FILL_COLOR_PROPERTY && getStyleProperty(styles, prop) === styles.color) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!preserveInheritedVisualStyle || !INHERITED_VISUAL_PROPERTIES.has(prop)) return false;
|
|
188
|
+
|
|
189
|
+
const parent = source.parentElement;
|
|
190
|
+
if (!parent) return false;
|
|
191
|
+
|
|
192
|
+
const parentStyles = getComputedStyle(parent);
|
|
193
|
+
return getStyleProperty(styles, prop) === getStyleProperty(parentStyles, prop);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function copyComputedStyles(
|
|
197
|
+
source: HTMLElement,
|
|
198
|
+
target: HTMLElement,
|
|
199
|
+
preserveInheritedVisualStyle = false,
|
|
200
|
+
): void {
|
|
201
|
+
const styles = getComputedStyle(source);
|
|
202
|
+
for (let i = 0; i < styles.length; i++) {
|
|
203
|
+
const prop = styles[i];
|
|
204
|
+
if (shouldSkipCopiedStyle(source, styles, prop, preserveInheritedVisualStyle)) continue;
|
|
205
|
+
target.style.setProperty(prop, styles.getPropertyValue(prop), styles.getPropertyPriority(prop));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const sourceChildren = source.querySelectorAll<HTMLElement>('*');
|
|
209
|
+
const targetChildren = target.querySelectorAll<HTMLElement>('*');
|
|
210
|
+
for (let i = 0; i < sourceChildren.length; i++) {
|
|
211
|
+
const child = targetChildren[i];
|
|
212
|
+
if (!child) continue;
|
|
213
|
+
copyComputedStyles(sourceChildren[i], child, true);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cloneSharedElement(source: HTMLElement): HTMLElement {
|
|
218
|
+
const clone = source.cloneNode(true) as HTMLElement;
|
|
219
|
+
copyComputedStyles(source, clone);
|
|
220
|
+
clone.removeAttribute('data-osd-shared-element');
|
|
221
|
+
return clone;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parsePx(value: string): number | null {
|
|
225
|
+
const match = value.trim().match(/^(-?\d*\.?\d+)px$/);
|
|
226
|
+
if (!match) return null;
|
|
227
|
+
const n = Number.parseFloat(match[1]);
|
|
228
|
+
return Number.isFinite(n) ? n : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function scaleRadius(value: string, scaleX: number, scaleY: number): string {
|
|
232
|
+
const parts = value.trim().split(/\s+/);
|
|
233
|
+
const x = parsePx(parts[0] ?? '');
|
|
234
|
+
const y = parsePx(parts[1] ?? parts[0] ?? '');
|
|
235
|
+
if (x === null || y === null) return value;
|
|
236
|
+
const nextX = x / scaleX;
|
|
237
|
+
const nextY = y / scaleY;
|
|
238
|
+
return Math.abs(nextX - nextY) < 0.001 ? `${nextX}px` : `${nextX}px ${nextY}px`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function radiusKeyframe(styles: CSSStyleDeclaration, scaleX = 1, scaleY = 1): Keyframe {
|
|
242
|
+
return {
|
|
243
|
+
borderTopLeftRadius: scaleRadius(styles.borderTopLeftRadius, scaleX, scaleY),
|
|
244
|
+
borderTopRightRadius: scaleRadius(styles.borderTopRightRadius, scaleX, scaleY),
|
|
245
|
+
borderBottomRightRadius: scaleRadius(styles.borderBottomRightRadius, scaleX, scaleY),
|
|
246
|
+
borderBottomLeftRadius: scaleRadius(styles.borderBottomLeftRadius, scaleX, scaleY),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function localTransform(styles: CSSStyleDeclaration): string {
|
|
251
|
+
return styles.transform && styles.transform !== 'none' ? styles.transform : '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sharedElementTransform(
|
|
255
|
+
rect: LocalRect,
|
|
256
|
+
scaleX: number,
|
|
257
|
+
scaleY: number,
|
|
258
|
+
local: string,
|
|
259
|
+
): string {
|
|
260
|
+
return [`translate(${rect.left}px, ${rect.top}px)`, `scale(${scaleX}, ${scaleY})`, local]
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.join(' ');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sharedElementTranslateTransform(rect: LocalRect, local: string): string {
|
|
266
|
+
return [`translate(${rect.left}px, ${rect.top}px)`, local].filter(Boolean).join(' ');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getStyleProperty(styles: CSSStyleDeclaration, css: string): string {
|
|
270
|
+
return styles.getPropertyValue(css);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function visualStyleKeyframe(
|
|
274
|
+
styles: CSSStyleDeclaration,
|
|
275
|
+
opacity?: string,
|
|
276
|
+
options: { includeBorderColors?: boolean } = {},
|
|
277
|
+
): Keyframe {
|
|
278
|
+
const frame: Record<string, string> = {};
|
|
279
|
+
for (const prop of SHARED_ELEMENT_VISUAL_PROPERTIES) {
|
|
280
|
+
if (
|
|
281
|
+
options.includeBorderColors === false &&
|
|
282
|
+
(BORDER_COLOR_PROPERTIES as readonly string[]).includes(prop)
|
|
283
|
+
) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const value = getStyleProperty(styles, prop);
|
|
287
|
+
if (prop === TEXT_FILL_COLOR_PROPERTY && value === styles.color) continue;
|
|
288
|
+
if (value) frame[prop] = value;
|
|
289
|
+
}
|
|
290
|
+
if (opacity !== undefined) frame.opacity = opacity;
|
|
291
|
+
return frame;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hideBorderColors(el: HTMLElement): void {
|
|
295
|
+
for (const { color } of BORDER_SIDE_PROPERTIES) {
|
|
296
|
+
el.style[color] = 'transparent';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isVisibleBorderSide(
|
|
301
|
+
styles: CSSStyleDeclaration,
|
|
302
|
+
side: (typeof BORDER_SIDE_PROPERTIES)[number],
|
|
303
|
+
): boolean {
|
|
304
|
+
const width = parsePx(getStyleProperty(styles, side.cssWidth)) ?? 0;
|
|
305
|
+
const borderStyle = getStyleProperty(styles, side.cssStyle);
|
|
306
|
+
return width > 0 && borderStyle !== 'none' && borderStyle !== 'hidden';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function hasVisibleBorder(styles: CSSStyleDeclaration): boolean {
|
|
310
|
+
return BORDER_SIDE_PROPERTIES.some((side) => isVisibleBorderSide(styles, side));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function borderFrameKeyframe(
|
|
314
|
+
rect: LocalRect,
|
|
315
|
+
styles: CSSStyleDeclaration,
|
|
316
|
+
opacity: string,
|
|
317
|
+
): Keyframe {
|
|
318
|
+
const frame: Keyframe = {
|
|
319
|
+
width: `${rect.width}px`,
|
|
320
|
+
height: `${rect.height}px`,
|
|
321
|
+
opacity,
|
|
322
|
+
transform: sharedElementTranslateTransform(rect, localTransform(styles)),
|
|
323
|
+
...radiusKeyframe(styles),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
for (const side of BORDER_SIDE_PROPERTIES) {
|
|
327
|
+
frame[side.width] = getStyleProperty(styles, side.cssWidth);
|
|
328
|
+
frame[side.color] = getStyleProperty(styles, side.cssColor);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return frame;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function appendBorderFrame(
|
|
335
|
+
wrapper: HTMLElement,
|
|
336
|
+
overlay: HTMLElement,
|
|
337
|
+
sourceStyles: CSSStyleDeclaration,
|
|
338
|
+
targetStyles: CSSStyleDeclaration,
|
|
339
|
+
rect: LocalRect,
|
|
340
|
+
): HTMLElement {
|
|
341
|
+
if (!overlay.parentElement) wrapper.appendChild(overlay);
|
|
342
|
+
|
|
343
|
+
const frame = document.createElement('div');
|
|
344
|
+
Object.assign(frame.style, {
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
left: '0',
|
|
347
|
+
top: '0',
|
|
348
|
+
width: `${rect.width}px`,
|
|
349
|
+
height: `${rect.height}px`,
|
|
350
|
+
margin: '0',
|
|
351
|
+
transformOrigin: 'top left',
|
|
352
|
+
pointerEvents: 'none',
|
|
353
|
+
boxSizing: 'border-box',
|
|
354
|
+
background: 'transparent',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
for (const side of BORDER_SIDE_PROPERTIES) {
|
|
358
|
+
const styles = isVisibleBorderSide(sourceStyles, side) ? sourceStyles : targetStyles;
|
|
359
|
+
frame.style[side.style] = getStyleProperty(styles, side.cssStyle);
|
|
360
|
+
frame.style[side.width] = getStyleProperty(sourceStyles, side.cssWidth);
|
|
361
|
+
frame.style[side.color] = getStyleProperty(sourceStyles, side.cssColor);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
overlay.appendChild(frame);
|
|
365
|
+
return frame;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function hasVisualStyleChange(from: CSSStyleDeclaration, to: CSSStyleDeclaration): boolean {
|
|
369
|
+
for (const prop of SHARED_ELEMENT_VISUAL_PROPERTIES) {
|
|
370
|
+
if (getStyleProperty(from, prop) !== getStyleProperty(to, prop)) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function effectiveOpacity(el: HTMLElement, boundary: HTMLElement): string {
|
|
376
|
+
let opacity = 1;
|
|
377
|
+
let node: HTMLElement | null = el;
|
|
378
|
+
while (node && node !== boundary) {
|
|
379
|
+
const value = Number.parseFloat(getComputedStyle(node).opacity);
|
|
380
|
+
if (Number.isFinite(value)) opacity *= value;
|
|
381
|
+
node = node.parentElement;
|
|
382
|
+
}
|
|
383
|
+
return String(opacity);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function hideOriginal(el: HTMLElement): () => void {
|
|
387
|
+
const value = el.style.getPropertyValue('visibility');
|
|
388
|
+
const priority = el.style.getPropertyPriority('visibility');
|
|
389
|
+
el.style.setProperty('visibility', 'hidden', 'important');
|
|
390
|
+
return () => {
|
|
391
|
+
if (value) el.style.setProperty('visibility', value, priority);
|
|
392
|
+
else el.style.removeProperty('visibility');
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function appendPositionedClone(
|
|
397
|
+
wrapper: HTMLElement,
|
|
398
|
+
overlay: HTMLElement,
|
|
399
|
+
source: HTMLElement,
|
|
400
|
+
rect: LocalRect,
|
|
401
|
+
): HTMLElement {
|
|
402
|
+
if (!overlay.parentElement) wrapper.appendChild(overlay);
|
|
403
|
+
|
|
404
|
+
const clone = cloneSharedElement(source);
|
|
405
|
+
Object.assign(clone.style, {
|
|
406
|
+
position: 'absolute',
|
|
407
|
+
left: '0',
|
|
408
|
+
top: '0',
|
|
409
|
+
width: `${rect.width}px`,
|
|
410
|
+
height: `${rect.height}px`,
|
|
411
|
+
margin: '0',
|
|
412
|
+
transformOrigin: 'top left',
|
|
413
|
+
pointerEvents: 'none',
|
|
414
|
+
boxSizing: 'border-box',
|
|
415
|
+
});
|
|
416
|
+
overlay.appendChild(clone);
|
|
417
|
+
return clone;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function sharedElementAnimationOptions(
|
|
421
|
+
phase: ResolvedSharedElementTransition,
|
|
422
|
+
): KeyframeAnimationOptions {
|
|
423
|
+
return {
|
|
424
|
+
duration: phase.duration,
|
|
425
|
+
easing: phase.easing,
|
|
426
|
+
delay: phase.delay,
|
|
427
|
+
fill: 'both',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function runDescendantVisualStyleTransitions(
|
|
432
|
+
clone: HTMLElement,
|
|
433
|
+
source: HTMLElement,
|
|
434
|
+
target: HTMLElement,
|
|
435
|
+
phase: ResolvedSharedElementTransition,
|
|
436
|
+
): Animation[] {
|
|
437
|
+
const animations: Animation[] = [];
|
|
438
|
+
const cloneChildren = clone.querySelectorAll<HTMLElement>('*');
|
|
439
|
+
const sourceChildren = source.querySelectorAll<HTMLElement>('*');
|
|
440
|
+
const targetChildren = target.querySelectorAll<HTMLElement>('*');
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < sourceChildren.length; i++) {
|
|
443
|
+
const cloneChild = cloneChildren[i];
|
|
444
|
+
const targetChild = targetChildren[i];
|
|
445
|
+
if (!cloneChild || !targetChild) continue;
|
|
446
|
+
|
|
447
|
+
const sourceStyles = getComputedStyle(sourceChildren[i]);
|
|
448
|
+
const targetStyles = getComputedStyle(targetChild);
|
|
449
|
+
if (!hasVisualStyleChange(sourceStyles, targetStyles)) continue;
|
|
450
|
+
|
|
451
|
+
animations.push(
|
|
452
|
+
cloneChild.animate(
|
|
453
|
+
[visualStyleKeyframe(sourceStyles), visualStyleKeyframe(targetStyles)],
|
|
454
|
+
sharedElementAnimationOptions(phase),
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return animations;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function runStationarySharedElementAnimation(
|
|
463
|
+
clone: HTMLElement,
|
|
464
|
+
rect: LocalRect,
|
|
465
|
+
styles: CSSStyleDeclaration,
|
|
466
|
+
fromOpacity: string,
|
|
467
|
+
toOpacity: string,
|
|
468
|
+
phase: ResolvedSharedElementTransition,
|
|
469
|
+
): Animation {
|
|
470
|
+
const transform = sharedElementTransform(rect, 1, 1, localTransform(styles));
|
|
471
|
+
return clone.animate(
|
|
472
|
+
[
|
|
473
|
+
{
|
|
474
|
+
...visualStyleKeyframe(styles, fromOpacity),
|
|
475
|
+
...radiusKeyframe(styles),
|
|
476
|
+
transform,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
...visualStyleKeyframe(styles, toOpacity),
|
|
480
|
+
...radiusKeyframe(styles),
|
|
481
|
+
transform,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
sharedElementAnimationOptions(phase),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function runSharedElementTransition(
|
|
489
|
+
wrapper: HTMLElement,
|
|
490
|
+
outgoingLayer: HTMLElement,
|
|
491
|
+
incomingLayer: HTMLElement,
|
|
492
|
+
phase: ResolvedSharedElementTransition,
|
|
493
|
+
): { animations: Animation[]; cleanup: () => void } {
|
|
494
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
495
|
+
if (wrapperRect.width === 0 || wrapperRect.height === 0) {
|
|
496
|
+
return { animations: [], cleanup: () => {} };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const incoming = collectSharedElements(incomingLayer);
|
|
500
|
+
const overlay = document.createElement('div');
|
|
501
|
+
overlay.setAttribute('data-osd-shared-layer', '');
|
|
502
|
+
Object.assign(overlay.style, {
|
|
503
|
+
position: 'absolute',
|
|
504
|
+
inset: '0',
|
|
505
|
+
zIndex: '2147483647',
|
|
506
|
+
pointerEvents: 'none',
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const animations: Animation[] = [];
|
|
510
|
+
const restore: Array<() => void> = [];
|
|
511
|
+
const outgoing = collectSharedElements(outgoingLayer);
|
|
512
|
+
const handledIncoming = new Set<string>();
|
|
513
|
+
|
|
514
|
+
for (const [id, source] of outgoing) {
|
|
515
|
+
const target = incoming.get(id);
|
|
516
|
+
const from = measureLocalRect(source, wrapper, wrapperRect);
|
|
517
|
+
if (!hasUsableRect(from)) {
|
|
518
|
+
if (target) {
|
|
519
|
+
const to = measureLocalRect(target, wrapper, wrapperRect);
|
|
520
|
+
if (hasUsableRect(to)) {
|
|
521
|
+
handledIncoming.add(id);
|
|
522
|
+
const clone = appendPositionedClone(wrapper, overlay, target, to);
|
|
523
|
+
restore.push(hideOriginal(target));
|
|
524
|
+
|
|
525
|
+
const targetStyles = getComputedStyle(target);
|
|
526
|
+
animations.push(
|
|
527
|
+
runStationarySharedElementAnimation(
|
|
528
|
+
clone,
|
|
529
|
+
to,
|
|
530
|
+
targetStyles,
|
|
531
|
+
'0',
|
|
532
|
+
effectiveOpacity(target, incomingLayer),
|
|
533
|
+
phase,
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!target) {
|
|
542
|
+
const clone = appendPositionedClone(wrapper, overlay, source, from);
|
|
543
|
+
restore.push(hideOriginal(source));
|
|
544
|
+
|
|
545
|
+
const sourceStyles = getComputedStyle(source);
|
|
546
|
+
animations.push(
|
|
547
|
+
runStationarySharedElementAnimation(
|
|
548
|
+
clone,
|
|
549
|
+
from,
|
|
550
|
+
sourceStyles,
|
|
551
|
+
effectiveOpacity(source, outgoingLayer),
|
|
552
|
+
'0',
|
|
553
|
+
phase,
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const to = measureLocalRect(target, wrapper, wrapperRect);
|
|
560
|
+
if (!hasUsableRect(to)) {
|
|
561
|
+
const clone = appendPositionedClone(wrapper, overlay, source, from);
|
|
562
|
+
restore.push(hideOriginal(source));
|
|
563
|
+
|
|
564
|
+
const sourceStyles = getComputedStyle(source);
|
|
565
|
+
animations.push(
|
|
566
|
+
runStationarySharedElementAnimation(
|
|
567
|
+
clone,
|
|
568
|
+
from,
|
|
569
|
+
sourceStyles,
|
|
570
|
+
effectiveOpacity(source, outgoingLayer),
|
|
571
|
+
'0',
|
|
572
|
+
phase,
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
handledIncoming.add(id);
|
|
579
|
+
|
|
580
|
+
const clone = appendPositionedClone(wrapper, overlay, source, from);
|
|
581
|
+
restore.push(hideOriginal(source), hideOriginal(target));
|
|
582
|
+
|
|
583
|
+
const sourceStyles = getComputedStyle(source);
|
|
584
|
+
const targetStyles = getComputedStyle(target);
|
|
585
|
+
const fromOpacity = effectiveOpacity(source, outgoingLayer);
|
|
586
|
+
const toOpacity = effectiveOpacity(target, incomingLayer);
|
|
587
|
+
const needsBorderFrame = hasVisibleBorder(sourceStyles) || hasVisibleBorder(targetStyles);
|
|
588
|
+
const scaleX = to.width / from.width;
|
|
589
|
+
const scaleY = to.height / from.height;
|
|
590
|
+
const fromTransform = sharedElementTransform(from, 1, 1, localTransform(sourceStyles));
|
|
591
|
+
const toTransform = sharedElementTransform(to, scaleX, scaleY, localTransform(targetStyles));
|
|
592
|
+
|
|
593
|
+
if (needsBorderFrame) {
|
|
594
|
+
hideBorderColors(clone);
|
|
595
|
+
const borderFrame = appendBorderFrame(wrapper, overlay, sourceStyles, targetStyles, from);
|
|
596
|
+
animations.push(
|
|
597
|
+
borderFrame.animate(
|
|
598
|
+
[
|
|
599
|
+
borderFrameKeyframe(from, sourceStyles, fromOpacity),
|
|
600
|
+
borderFrameKeyframe(to, targetStyles, toOpacity),
|
|
601
|
+
],
|
|
602
|
+
sharedElementAnimationOptions(phase),
|
|
603
|
+
),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
animations.push(
|
|
608
|
+
clone.animate(
|
|
609
|
+
[
|
|
610
|
+
{
|
|
611
|
+
...visualStyleKeyframe(sourceStyles, fromOpacity, {
|
|
612
|
+
includeBorderColors: !needsBorderFrame,
|
|
613
|
+
}),
|
|
614
|
+
...radiusKeyframe(sourceStyles),
|
|
615
|
+
transform: fromTransform,
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
...visualStyleKeyframe(targetStyles, toOpacity, {
|
|
619
|
+
includeBorderColors: !needsBorderFrame,
|
|
620
|
+
}),
|
|
621
|
+
...radiusKeyframe(targetStyles, scaleX, scaleY),
|
|
622
|
+
transform: toTransform,
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
sharedElementAnimationOptions(phase),
|
|
626
|
+
),
|
|
627
|
+
...runDescendantVisualStyleTransitions(clone, source, target, phase),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
for (const [id, target] of incoming) {
|
|
632
|
+
if (handledIncoming.has(id) || outgoing.has(id)) continue;
|
|
633
|
+
|
|
634
|
+
const to = measureLocalRect(target, wrapper, wrapperRect);
|
|
635
|
+
if (!hasUsableRect(to)) continue;
|
|
636
|
+
|
|
637
|
+
const clone = appendPositionedClone(wrapper, overlay, target, to);
|
|
638
|
+
restore.push(hideOriginal(target));
|
|
639
|
+
|
|
640
|
+
const targetStyles = getComputedStyle(target);
|
|
641
|
+
animations.push(
|
|
642
|
+
runStationarySharedElementAnimation(
|
|
643
|
+
clone,
|
|
644
|
+
to,
|
|
645
|
+
targetStyles,
|
|
646
|
+
'0',
|
|
647
|
+
effectiveOpacity(target, incomingLayer),
|
|
648
|
+
phase,
|
|
649
|
+
),
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (animations.length === 0) overlay.remove();
|
|
654
|
+
|
|
655
|
+
let cleaned = false;
|
|
656
|
+
return {
|
|
657
|
+
animations,
|
|
658
|
+
cleanup: () => {
|
|
659
|
+
if (cleaned) return;
|
|
660
|
+
cleaned = true;
|
|
661
|
+
for (const fn of restore) fn();
|
|
662
|
+
overlay.remove();
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function SlideTransitionLayer({
|
|
668
|
+
pages,
|
|
669
|
+
index,
|
|
670
|
+
total,
|
|
671
|
+
moduleTransition,
|
|
672
|
+
disabled,
|
|
673
|
+
stepControllerRef,
|
|
674
|
+
entryDirection = 'jump',
|
|
675
|
+
onStepAggregateChange,
|
|
676
|
+
}: Props) {
|
|
677
|
+
const [current, setCurrent] = useState(index);
|
|
678
|
+
const [outgoing, setOutgoing] = useState<number | null>(null);
|
|
679
|
+
const [direction, setDirection] = useState<Direction>('forward');
|
|
680
|
+
|
|
681
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
682
|
+
const outgoingLayerRef = useRef<HTMLDivElement | null>(null);
|
|
683
|
+
const incomingLayerRef = useRef<HTMLDivElement | null>(null);
|
|
684
|
+
const animsRef = useRef<Animation[]>([]);
|
|
685
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
686
|
+
const currentRef = useRef(current);
|
|
687
|
+
currentRef.current = current;
|
|
688
|
+
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
if (index === currentRef.current) return;
|
|
691
|
+
|
|
692
|
+
const prev = currentRef.current;
|
|
693
|
+
const next = index;
|
|
694
|
+
|
|
695
|
+
// Interrupt: cancel in-flight animations. The previously-incoming page
|
|
696
|
+
// (currentRef) becomes the new outgoing; React reuses its DOM slot.
|
|
697
|
+
for (const a of animsRef.current) {
|
|
698
|
+
try {
|
|
699
|
+
a.cancel();
|
|
700
|
+
} catch {}
|
|
701
|
+
}
|
|
702
|
+
animsRef.current = [];
|
|
703
|
+
cleanupRef.current?.();
|
|
704
|
+
cleanupRef.current = null;
|
|
705
|
+
|
|
706
|
+
const transition = resolveTransition(pages, next, moduleTransition);
|
|
707
|
+
if (disabled || !transition) {
|
|
708
|
+
setCurrent(next);
|
|
709
|
+
setOutgoing(null);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
setDirection(next > prev ? 'forward' : 'backward');
|
|
714
|
+
setOutgoing(prev);
|
|
715
|
+
setCurrent(next);
|
|
716
|
+
}, [index, pages, moduleTransition, disabled]);
|
|
717
|
+
|
|
718
|
+
useLayoutEffect(() => {
|
|
719
|
+
if (outgoing === null) return;
|
|
720
|
+
|
|
721
|
+
const transition = resolveTransition(pages, current, moduleTransition);
|
|
722
|
+
const wrapper = wrapperRef.current;
|
|
723
|
+
const out = outgoingLayerRef.current;
|
|
724
|
+
const inc = incomingLayerRef.current;
|
|
725
|
+
if (!transition || !wrapper || !out || !inc) {
|
|
726
|
+
setOutgoing(null);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
wrapper.dataset.osdDir = direction;
|
|
731
|
+
wrapper.style.setProperty('--osd-dir', direction === 'forward' ? '1' : '-1');
|
|
732
|
+
|
|
733
|
+
const easing = transition.easing ?? DEFAULT_EASING;
|
|
734
|
+
const duration = transition.duration;
|
|
735
|
+
|
|
736
|
+
const anims: Animation[] = [];
|
|
737
|
+
const exitAnim = runPhase(out, transition.exit, duration, easing);
|
|
738
|
+
const enterAnim = runPhase(inc, transition.enter, duration, easing);
|
|
739
|
+
if (exitAnim) anims.push(exitAnim);
|
|
740
|
+
if (enterAnim) anims.push(enterAnim);
|
|
741
|
+
|
|
742
|
+
const cleanups: Array<() => void> = [];
|
|
743
|
+
const sharedPhase = resolveSharedElementTransition(transition.sharedElements, duration, easing);
|
|
744
|
+
if (sharedPhase) {
|
|
745
|
+
const shared = runSharedElementTransition(wrapper, out, inc, sharedPhase);
|
|
746
|
+
anims.push(...shared.animations);
|
|
747
|
+
cleanups.push(shared.cleanup);
|
|
748
|
+
if (!exitAnim && shared.animations.length > 0) cleanups.push(hideOriginal(out));
|
|
749
|
+
}
|
|
750
|
+
animsRef.current = anims;
|
|
751
|
+
|
|
752
|
+
if (anims.length === 0) {
|
|
753
|
+
for (const fn of cleanups) fn();
|
|
754
|
+
setOutgoing(null);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let cleaned = false;
|
|
759
|
+
const cleanup = () => {
|
|
760
|
+
if (cleaned) return;
|
|
761
|
+
cleaned = true;
|
|
762
|
+
for (const fn of cleanups) fn();
|
|
763
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = null;
|
|
764
|
+
};
|
|
765
|
+
cleanupRef.current = cleanup;
|
|
766
|
+
|
|
767
|
+
let cancelled = false;
|
|
768
|
+
Promise.all(anims.map((a) => a.finished))
|
|
769
|
+
.then(() => {
|
|
770
|
+
if (cancelled) return;
|
|
771
|
+
cleanup();
|
|
772
|
+
animsRef.current = [];
|
|
773
|
+
setOutgoing(null);
|
|
774
|
+
})
|
|
775
|
+
.catch(() => {
|
|
776
|
+
// AbortError fires when we cancel mid-flight on an interrupt.
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
return () => {
|
|
780
|
+
cancelled = true;
|
|
781
|
+
};
|
|
782
|
+
}, [outgoing, current, direction, pages, moduleTransition]);
|
|
783
|
+
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
return () => {
|
|
786
|
+
for (const a of animsRef.current) {
|
|
787
|
+
try {
|
|
788
|
+
a.cancel();
|
|
789
|
+
} catch {}
|
|
790
|
+
}
|
|
791
|
+
animsRef.current = [];
|
|
792
|
+
cleanupRef.current?.();
|
|
793
|
+
cleanupRef.current = null;
|
|
794
|
+
};
|
|
795
|
+
}, []);
|
|
796
|
+
|
|
797
|
+
const CurrentPage = pages[current];
|
|
798
|
+
const OutgoingPage = outgoing !== null ? pages[outgoing] : null;
|
|
799
|
+
|
|
800
|
+
// Outgoing layer mirrors the direction we just navigated so its <Steps>
|
|
801
|
+
// re-mounts in the state the audience just saw: forward nav → outgoing was
|
|
802
|
+
// fully revealed; backward nav → outgoing was at zero reveals.
|
|
803
|
+
const outgoingEntryDirection: EntryDirection =
|
|
804
|
+
entryDirection === 'backward' ? 'forward' : 'backward';
|
|
805
|
+
|
|
806
|
+
const noopControllerRef = useRef<StepController | null>(null);
|
|
807
|
+
const activeControllerRef = stepControllerRef ?? noopControllerRef;
|
|
808
|
+
|
|
809
|
+
return (
|
|
810
|
+
<div
|
|
811
|
+
ref={wrapperRef}
|
|
812
|
+
className="relative h-full w-full overflow-hidden"
|
|
813
|
+
style={{ background: 'var(--osd-bg)' }}
|
|
814
|
+
>
|
|
815
|
+
{OutgoingPage && outgoing !== null ? (
|
|
816
|
+
<div ref={outgoingLayerRef} className="absolute inset-0">
|
|
817
|
+
<SlidePageProvider index={outgoing} total={total}>
|
|
818
|
+
<StepHost
|
|
819
|
+
isActivePage={false}
|
|
820
|
+
entryDirection={outgoingEntryDirection}
|
|
821
|
+
controllerRef={activeControllerRef}
|
|
822
|
+
>
|
|
823
|
+
<OutgoingPage />
|
|
824
|
+
</StepHost>
|
|
825
|
+
</SlidePageProvider>
|
|
826
|
+
</div>
|
|
827
|
+
) : null}
|
|
828
|
+
{CurrentPage ? (
|
|
829
|
+
<div ref={incomingLayerRef} className="absolute inset-0">
|
|
830
|
+
<SlidePageProvider index={current} total={total}>
|
|
831
|
+
<StepHost
|
|
832
|
+
isActivePage
|
|
833
|
+
entryDirection={entryDirection}
|
|
834
|
+
controllerRef={activeControllerRef}
|
|
835
|
+
onAggregateChange={onStepAggregateChange}
|
|
836
|
+
>
|
|
837
|
+
<CurrentPage />
|
|
838
|
+
</StepHost>
|
|
839
|
+
</SlidePageProvider>
|
|
840
|
+
</div>
|
|
841
|
+
) : null}
|
|
842
|
+
</div>
|
|
843
|
+
);
|
|
844
|
+
}
|