@open-slide/core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-D_5nlXFU.d.ts → config-mwmC1XI1.d.ts} +1 -1
- package/dist/{format-CYOb2cAQ.js → format-BvBmqbNW.js} +12 -4
- package/dist/index.d.ts +26 -6
- package/dist/index.js +84 -8
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +1 -1
- package/dist/{types-AalTbxMj.d.ts → types-D_q_ylIe.d.ts} +2 -0
- package/dist/vite/index.d.ts +2 -2
- package/package.json +1 -1
- package/skills/slide-authoring/SKILL.md +42 -0
- package/src/app/components/player.tsx +30 -11
- package/src/app/components/slide-transition-layer.tsx +36 -4
- package/src/app/components/thumbnail-rail.tsx +77 -15
- package/src/app/lib/step-context.tsx +169 -0
- package/src/app/routes/slide.tsx +2 -0
- package/src/locale/en.ts +2 -0
- package/src/locale/ja.ts +2 -0
- package/src/locale/types.ts +2 -0
- package/src/locale/zh-cn.ts +2 -0
- package/src/locale/zh-tw.ts +2 -0
|
@@ -329,7 +329,9 @@ const en = {
|
|
|
329
329
|
toastDeleted: "Deleted page {n}",
|
|
330
330
|
toastDuplicateFailed: "Could not duplicate page",
|
|
331
331
|
toastDeleteFailed: "Could not delete page",
|
|
332
|
-
resizeRail: "Resize thumbnail rail"
|
|
332
|
+
resizeRail: "Resize thumbnail rail",
|
|
333
|
+
transitionIndicator: "Has slide transition",
|
|
334
|
+
stepsIndicator: "Has step-by-step reveals"
|
|
333
335
|
},
|
|
334
336
|
pdfToast: {
|
|
335
337
|
title: "Exporting PDF",
|
|
@@ -719,7 +721,9 @@ const ja = {
|
|
|
719
721
|
toastDeleted: "ページ {n} を削除しました",
|
|
720
722
|
toastDuplicateFailed: "ページを複製できませんでした",
|
|
721
723
|
toastDeleteFailed: "ページを削除できませんでした",
|
|
722
|
-
resizeRail: "サムネイル幅を調整"
|
|
724
|
+
resizeRail: "サムネイル幅を調整",
|
|
725
|
+
transitionIndicator: "スライドトランジションあり",
|
|
726
|
+
stepsIndicator: "ステップ表示あり"
|
|
723
727
|
},
|
|
724
728
|
pdfToast: {
|
|
725
729
|
title: "PDF を書き出し中",
|
|
@@ -1109,7 +1113,9 @@ const zhCN = {
|
|
|
1109
1113
|
toastDeleted: "已删除第 {n} 页",
|
|
1110
1114
|
toastDuplicateFailed: "无法复制页面",
|
|
1111
1115
|
toastDeleteFailed: "无法删除页面",
|
|
1112
|
-
resizeRail: "调整缩略图栏宽度"
|
|
1116
|
+
resizeRail: "调整缩略图栏宽度",
|
|
1117
|
+
transitionIndicator: "有换页转场",
|
|
1118
|
+
stepsIndicator: "有逐步揭示"
|
|
1113
1119
|
},
|
|
1114
1120
|
pdfToast: {
|
|
1115
1121
|
title: "导出 PDF",
|
|
@@ -1499,7 +1505,9 @@ const zhTW = {
|
|
|
1499
1505
|
toastDeleted: "已刪除第 {n} 頁",
|
|
1500
1506
|
toastDuplicateFailed: "無法複製頁面",
|
|
1501
1507
|
toastDeleteFailed: "無法刪除頁面",
|
|
1502
|
-
resizeRail: "調整縮圖欄寬度"
|
|
1508
|
+
resizeRail: "調整縮圖欄寬度",
|
|
1509
|
+
transitionIndicator: "有換頁轉場",
|
|
1510
|
+
stepsIndicator: "有逐步揭示"
|
|
1503
1511
|
},
|
|
1504
1512
|
pdfToast: {
|
|
1505
1513
|
title: "匯出 PDF",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Locale, Plural } from "./types-
|
|
2
|
-
import { OpenSlideConfig } from "./config-
|
|
3
|
-
import { CSSProperties, ComponentType, HTMLAttributes } from "react";
|
|
4
|
-
import * as
|
|
1
|
+
import { Locale, Plural } from "./types-D_q_ylIe.js";
|
|
2
|
+
import { OpenSlideConfig } from "./config-mwmC1XI1.js";
|
|
3
|
+
import { CSSProperties, ComponentType, HTMLAttributes, PropsWithChildren } from "react";
|
|
4
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
5
|
+
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
6
|
+
import * as react_jsx_runtime4 from "react/jsx-runtime";
|
|
5
7
|
|
|
6
8
|
//#region src/app/components/image-placeholder.d.ts
|
|
7
9
|
type ImagePlaceholderProps = {
|
|
@@ -18,7 +20,7 @@ declare function ImagePlaceholder({
|
|
|
18
20
|
style,
|
|
19
21
|
className,
|
|
20
22
|
...rest
|
|
21
|
-
}: ImagePlaceholderProps):
|
|
23
|
+
}: ImagePlaceholderProps): react_jsx_runtime0.JSX.Element;
|
|
22
24
|
|
|
23
25
|
//#endregion
|
|
24
26
|
//#region src/app/lib/design.d.ts
|
|
@@ -89,4 +91,22 @@ declare const CANVAS_WIDTH = 1920;
|
|
|
89
91
|
declare const CANVAS_HEIGHT = 1080;
|
|
90
92
|
|
|
91
93
|
//#endregion
|
|
92
|
-
|
|
94
|
+
//#region src/app/lib/step-context.d.ts
|
|
95
|
+
type StepsProps = PropsWithChildren;
|
|
96
|
+
declare function Steps({
|
|
97
|
+
children
|
|
98
|
+
}: StepsProps): react_jsx_runtime3.JSX.Element;
|
|
99
|
+
type StepProps = PropsWithChildren<{
|
|
100
|
+
duration?: number;
|
|
101
|
+
}>;
|
|
102
|
+
type InternalStepProps = StepProps & {
|
|
103
|
+
_revealed?: boolean;
|
|
104
|
+
};
|
|
105
|
+
declare function Step({
|
|
106
|
+
children,
|
|
107
|
+
duration,
|
|
108
|
+
_revealed
|
|
109
|
+
}: InternalStepProps): react_jsx_runtime4.JSX.Element;
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { CANVAS_HEIGHT, CANVAS_WIDTH, DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, ImagePlaceholder, ImagePlaceholderProps, Locale, OpenSlideConfig, Page, Plural, SlideMeta, SlideModule, SlideTransition, Step, StepProps, Steps, StepsProps, TransitionPhase, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { en, ja, zhCN, zhTW } from "./format-
|
|
1
|
+
import { en, ja, zhCN, zhTW } from "./format-BvBmqbNW.js";
|
|
2
2
|
import { cssVarsToString, defaultDesign, designToCssVars } from "./design-cpzS8aud.js";
|
|
3
|
-
import { createContext, useContext, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
5
|
import config from "virtual:open-slide/config";
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
|
|
8
8
|
//#region src/app/lib/assets.ts
|
|
9
9
|
async function listAssets(slideId) {
|
|
@@ -333,10 +333,10 @@ function PlaceholderIcon() {
|
|
|
333
333
|
|
|
334
334
|
//#endregion
|
|
335
335
|
//#region src/app/lib/page-context.tsx
|
|
336
|
-
const GLOBAL_KEY = "__open_slide_page_context__";
|
|
337
|
-
const g = globalThis;
|
|
338
|
-
if (!g[GLOBAL_KEY]) g[GLOBAL_KEY] = createContext(null);
|
|
339
|
-
const SlidePageContext = g[GLOBAL_KEY];
|
|
336
|
+
const GLOBAL_KEY$1 = "__open_slide_page_context__";
|
|
337
|
+
const g$1 = globalThis;
|
|
338
|
+
if (!g$1[GLOBAL_KEY$1]) g$1[GLOBAL_KEY$1] = createContext(null);
|
|
339
|
+
const SlidePageContext = g$1[GLOBAL_KEY$1];
|
|
340
340
|
function useSlidePageNumber() {
|
|
341
341
|
const ctx = useContext(SlidePageContext);
|
|
342
342
|
if (!ctx) throw new Error("useSlidePageNumber must be called from a slide page rendered by @open-slide/core");
|
|
@@ -352,4 +352,80 @@ const CANVAS_WIDTH = 1920;
|
|
|
352
352
|
const CANVAS_HEIGHT = 1080;
|
|
353
353
|
|
|
354
354
|
//#endregion
|
|
355
|
-
|
|
355
|
+
//#region src/app/lib/use-prefers-reduced-motion.ts
|
|
356
|
+
const QUERY = "(prefers-reduced-motion: reduce)";
|
|
357
|
+
function usePrefersReducedMotion() {
|
|
358
|
+
const [reduce, setReduce] = useState(() => {
|
|
359
|
+
if (typeof window === "undefined") return false;
|
|
360
|
+
return window.matchMedia(QUERY).matches;
|
|
361
|
+
});
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
const mql = window.matchMedia(QUERY);
|
|
364
|
+
const onChange = (e) => setReduce(e.matches);
|
|
365
|
+
mql.addEventListener("change", onChange);
|
|
366
|
+
return () => mql.removeEventListener("change", onChange);
|
|
367
|
+
}, []);
|
|
368
|
+
return reduce;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/app/lib/step-context.tsx
|
|
373
|
+
const GLOBAL_KEY = "__open_slide_step_host_context__";
|
|
374
|
+
const g = globalThis;
|
|
375
|
+
if (!g[GLOBAL_KEY]) g[GLOBAL_KEY] = createContext(null);
|
|
376
|
+
const StepHostContext = g[GLOBAL_KEY];
|
|
377
|
+
function Steps({ children }) {
|
|
378
|
+
const host = useContext(StepHostContext);
|
|
379
|
+
const flat = Children.toArray(children);
|
|
380
|
+
const stepCount = flat.filter((c) => isValidElement(c) && c.type === Step).length;
|
|
381
|
+
const initial = host?.entryDirection === "forward" ? 0 : stepCount;
|
|
382
|
+
const revealedRef = useRef(initial);
|
|
383
|
+
const [revealed, setRevealed] = useState(initial);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!host) return;
|
|
386
|
+
const ctrl = {
|
|
387
|
+
advance: () => {
|
|
388
|
+
if (revealedRef.current >= stepCount) return false;
|
|
389
|
+
revealedRef.current += 1;
|
|
390
|
+
setRevealed(revealedRef.current);
|
|
391
|
+
return true;
|
|
392
|
+
},
|
|
393
|
+
retreat: () => {
|
|
394
|
+
if (revealedRef.current <= 0) return false;
|
|
395
|
+
revealedRef.current -= 1;
|
|
396
|
+
setRevealed(revealedRef.current);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
return host.register(ctrl);
|
|
401
|
+
}, [host, stepCount]);
|
|
402
|
+
const effectiveRevealed = host ? revealed : stepCount;
|
|
403
|
+
let stepIdx = 0;
|
|
404
|
+
return /* @__PURE__ */ jsx(Fragment, { children: flat.map((child, key) => {
|
|
405
|
+
if (isValidElement(child) && child.type === Step) {
|
|
406
|
+
const idx = stepIdx++;
|
|
407
|
+
return cloneElement(child, {
|
|
408
|
+
key: child.key ?? key,
|
|
409
|
+
_revealed: idx < effectiveRevealed
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return child;
|
|
413
|
+
}) });
|
|
414
|
+
}
|
|
415
|
+
function Step({ children, duration = 180, _revealed }) {
|
|
416
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
417
|
+
const revealed = _revealed ?? true;
|
|
418
|
+
const ms = reduceMotion ? 0 : duration;
|
|
419
|
+
return /* @__PURE__ */ jsx("div", {
|
|
420
|
+
"data-osd-step": revealed ? "revealed" : "pending",
|
|
421
|
+
style: {
|
|
422
|
+
opacity: revealed ? 1 : 0,
|
|
423
|
+
visibility: revealed ? "visible" : "hidden",
|
|
424
|
+
transition: `opacity ${ms}ms cubic-bezier(0, 0, 0.2, 1)`
|
|
425
|
+
},
|
|
426
|
+
children
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
//#endregion
|
|
431
|
+
export { CANVAS_HEIGHT, CANVAS_WIDTH, ImagePlaceholder, Step, Steps, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
|
package/dist/locale/index.d.ts
CHANGED
package/dist/locale/index.js
CHANGED
package/dist/vite/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -308,6 +308,45 @@ const Footer = () => {
|
|
|
308
308
|
|
|
309
309
|
`current` is 1-indexed (matches what readers see) and `total` is the slide's page count. The hook works in every render context (main viewer, thumbnails, overview grid, present mode, presenter window, HTML/PDF export) — the same `<Footer />` JSX is correct everywhere. Call the hook inside a component that's used **per page**; don't try to call it at module top level.
|
|
310
310
|
|
|
311
|
+
## Stepped reveals (`<Steps>` / `<Step>`)
|
|
312
|
+
|
|
313
|
+
Reveal a page one beat at a time instead of showing everything at once. Wrap the deferred parts in `<Step>`, wrap the group in `<Steps>`. Each `→` reveals the next `<Step>`; `→` after the last one advances to the next page. `←` peels the last reveal back. Use it to stage attention — show framing first, then the consequence, then the turn — so the audience reads at the speaker's pace, not ahead.
|
|
314
|
+
|
|
315
|
+
`slides/build-on-reveal/` is the canonical worked example; study it before authoring a stepped page.
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
import { Step, Steps } from '@open-slide/core';
|
|
319
|
+
|
|
320
|
+
<Steps>
|
|
321
|
+
<Step><div style={BULLET_ROW}>An audience reads faster than a presenter speaks.</div></Step>
|
|
322
|
+
<Step><div style={BULLET_ROW}>Showing every bullet at once invites pre-reading.</div></Step>
|
|
323
|
+
<Step><div style={BULLET_ROW}>Revealing in time stages attention.</div></Step>
|
|
324
|
+
</Steps>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Rules
|
|
328
|
+
|
|
329
|
+
- **`<Step>` must be a *direct* child of `<Steps>`.** A `<Step>` nested deeper (or used without a `<Steps>` parent) renders fully revealed and defers nothing.
|
|
330
|
+
- **Non-`Step` children render immediately.** Put a headline or intro paragraph *inside* `<Steps>` as a plain element and it shows from the start; only the `<Step>` blocks wait. This is the "headline always, body in turn" pattern:
|
|
331
|
+
```tsx
|
|
332
|
+
<Steps>
|
|
333
|
+
<h2>Not everything has to wait.</h2>{/* visible immediately */}
|
|
334
|
+
<Step><p>First, set the stage…</p></Step>
|
|
335
|
+
<Step><p>Then, layer the consequence…</p></Step>
|
|
336
|
+
</Steps>
|
|
337
|
+
```
|
|
338
|
+
- **Multiple `<Steps>` blocks on one page compose in document order.** The first block reveals all its steps before the second begins; `←` unwinds in reverse. Use this for two columns that build left-then-right, each column owning its own `<Steps>`:
|
|
339
|
+
```tsx
|
|
340
|
+
<div style={COL}><Steps><Step>…</Step><Step>…</Step></Steps></div>{/* finishes first */}
|
|
341
|
+
<div style={COL}><Steps><Step>…</Step><Step>…</Step></Steps></div>{/* then this */}
|
|
342
|
+
```
|
|
343
|
+
- **Entry direction decides the starting state — same content, two rhythms.** Entering forward (`→` from the previous page) starts empty and builds up. Jumping in via the overview grid, or arriving backward from a later page, shows the page **fully composed** with every step already revealed. Design the page to read well both ways: a thumbnail or overview jump should look complete, not blank.
|
|
344
|
+
- **`<Step>` fades in over `duration` ms (default 180).** Pass `<Step duration={...}>` to adjust. `prefers-reduced-motion: reduce` collapses it to an instant cut automatically — don't write a fallback.
|
|
345
|
+
|
|
346
|
+
### When to reach for it
|
|
347
|
+
|
|
348
|
+
Use stepped reveals when the *order* of ideas is the point — a list whose payoff is the last item, a build-up to a conclusion, a before/after. Don't wrap every page's content in `<Step>` reflexively: a page the audience should take in at a glance (a hero title, a single quote, a diagram) is stronger shown whole. Reveals are timing, not decoration — same restraint as transitions.
|
|
349
|
+
|
|
311
350
|
## Page transitions
|
|
312
351
|
|
|
313
352
|
The framework can run an enter/exit animation between every slide change. There's **no default** — pages snap unless you declare a `SlideTransition`. Snap-swap is a perfectly tasteful default; only opt in when motion adds something.
|
|
@@ -541,6 +580,7 @@ This applies whenever the *visual element* repeats, not whenever the *data* does
|
|
|
541
580
|
- [ ] Visually repeated elements (cards, tiles, logo rows) are rendered as explicit `<Component />` instances, not via `array.map` over a data list.
|
|
542
581
|
- [ ] All imported assets exist on disk — slide-local under `slides/<id>/assets/`, or global under `assets/` (imported via `@assets/...`).
|
|
543
582
|
- [ ] Every `<ImagePlaceholder>` corresponds to a real image the user must supply — not decorative filler. If it could be replaced by typography or layout, it should be.
|
|
583
|
+
- [ ] If a page uses `<Steps>`/`<Step>`, every `<Step>` is a direct child of a `<Steps>`, and the page still reads as complete when jumped to via the overview grid (entering forward builds up; jumping in shows it fully revealed).
|
|
544
584
|
- [ ] If a `SlideTransition` is declared, every page sits in one family — same duration band (140–280 ms), same easing pair, same out-then-in stagger, magnitude under 12 px / 3%. No six-different-vocabularies decks. When in doubt, omit transitions entirely.
|
|
545
585
|
- [ ] Nothing outside `slides/<id>/` was edited.
|
|
546
586
|
|
|
@@ -561,3 +601,5 @@ This applies whenever the *visual element* repeats, not whenever the *data* does
|
|
|
561
601
|
- ❌ Sprinkling `<ImagePlaceholder>` across pages "for visual interest". Placeholders are for content the user owns; they're not stock-photo slots.
|
|
562
602
|
- ❌ Using a placeholder for an icon or decorative shape — those are typography/SVG problems, not asset problems.
|
|
563
603
|
- ❌ Rendering visually repeated elements with `array.map(...)` over a data array. Define a component and instantiate it explicitly per item (`<Card />`, `<Card />`, `<Card />`) so the inspector can edit each independently — a shared `map` body mutates every instance at once.
|
|
604
|
+
- ❌ Wrapping every page's content in `<Step>` reflexively. Stepped reveals are for content whose *order* is the point; a glance-and-get-it page (hero title, single quote, diagram) is stronger shown whole.
|
|
605
|
+
- ❌ A `<Step>` that isn't a direct child of `<Steps>` (nested deeper, or with no `<Steps>` parent). It renders fully revealed and defers nothing — the reveal silently does nothing.
|
|
@@ -4,6 +4,7 @@ import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
|
4
4
|
import { cn } from '@/lib/utils';
|
|
5
5
|
import type { DesignSystem } from '../lib/design';
|
|
6
6
|
import type { Page } from '../lib/sdk';
|
|
7
|
+
import type { EntryDirection, StepController } from '../lib/step-context';
|
|
7
8
|
import type { SlideTransition } from '../lib/transition';
|
|
8
9
|
import { usePrefersReducedMotion } from '../lib/use-prefers-reduced-motion';
|
|
9
10
|
import { PresentBlackoutOverlay } from './present/blackout-overlay';
|
|
@@ -84,12 +85,28 @@ export function Player({
|
|
|
84
85
|
const canPrev = index > 0;
|
|
85
86
|
const canNext = index < pages.length - 1;
|
|
86
87
|
|
|
88
|
+
const stepControllerRef = useRef<StepController | null>(null);
|
|
89
|
+
const [entryDirection, setEntryDirection] = useState<EntryDirection>('jump');
|
|
90
|
+
|
|
91
|
+
// Every navigation funnels through here so entryDirection is settled
|
|
92
|
+
// synchronously, before the incoming page's <Steps> reads it on mount.
|
|
93
|
+
const handleIndexChange = useCallback(
|
|
94
|
+
(next: number) => {
|
|
95
|
+
const delta = next - index;
|
|
96
|
+
setEntryDirection(delta === 1 ? 'forward' : delta === -1 ? 'backward' : 'jump');
|
|
97
|
+
onIndexChange(next);
|
|
98
|
+
},
|
|
99
|
+
[index, onIndexChange],
|
|
100
|
+
);
|
|
101
|
+
|
|
87
102
|
const goPrev = useCallback(() => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
103
|
+
if (stepControllerRef.current?.retreat()) return;
|
|
104
|
+
if (index > 0) handleIndexChange(index - 1);
|
|
105
|
+
}, [index, handleIndexChange]);
|
|
90
106
|
const goNext = useCallback(() => {
|
|
91
|
-
if (
|
|
92
|
-
|
|
107
|
+
if (stepControllerRef.current?.advance()) return;
|
|
108
|
+
if (index < pages.length - 1) handleIndexChange(index + 1);
|
|
109
|
+
}, [index, pages.length, handleIndexChange]);
|
|
93
110
|
|
|
94
111
|
const overlayActive = controls && (overviewOpen || helpOpen);
|
|
95
112
|
|
|
@@ -158,14 +175,14 @@ export function Player({
|
|
|
158
175
|
if (msg.type === 'next') goNext();
|
|
159
176
|
else if (msg.type === 'prev') goPrev();
|
|
160
177
|
else if (msg.type === 'goto') {
|
|
161
|
-
|
|
178
|
+
handleIndexChange(Math.max(0, Math.min(pages.length - 1, msg.index)));
|
|
162
179
|
} else if (msg.type === 'toggle-blackout') {
|
|
163
180
|
setBlackout((cur) => (cur === msg.mode ? null : msg.mode));
|
|
164
181
|
} else if (msg.type === 'request-state') {
|
|
165
182
|
send({ type: 'state', state: presenterStateRef.current });
|
|
166
183
|
}
|
|
167
184
|
},
|
|
168
|
-
[goNext, goPrev,
|
|
185
|
+
[goNext, goPrev, handleIndexChange, pages.length],
|
|
169
186
|
);
|
|
170
187
|
|
|
171
188
|
const channel = usePresenterChannel(slideId ?? '__none__', (msg) => {
|
|
@@ -231,12 +248,12 @@ export function Player({
|
|
|
231
248
|
}
|
|
232
249
|
if (e.key === 'Home') {
|
|
233
250
|
setKeyboardDriven(true);
|
|
234
|
-
|
|
251
|
+
handleIndexChange(0);
|
|
235
252
|
return;
|
|
236
253
|
}
|
|
237
254
|
if (e.key === 'End') {
|
|
238
255
|
setKeyboardDriven(true);
|
|
239
|
-
|
|
256
|
+
handleIndexChange(pages.length - 1);
|
|
240
257
|
return;
|
|
241
258
|
}
|
|
242
259
|
|
|
@@ -277,7 +294,7 @@ export function Player({
|
|
|
277
294
|
onExit,
|
|
278
295
|
goNext,
|
|
279
296
|
goPrev,
|
|
280
|
-
|
|
297
|
+
handleIndexChange,
|
|
281
298
|
pages.length,
|
|
282
299
|
slideId,
|
|
283
300
|
]);
|
|
@@ -315,6 +332,8 @@ export function Player({
|
|
|
315
332
|
total={pages.length}
|
|
316
333
|
moduleTransition={transition}
|
|
317
334
|
disabled={prefersReducedMotion}
|
|
335
|
+
stepControllerRef={stepControllerRef}
|
|
336
|
+
entryDirection={entryDirection}
|
|
318
337
|
/>
|
|
319
338
|
</SlideCanvas>
|
|
320
339
|
|
|
@@ -322,7 +341,7 @@ export function Player({
|
|
|
322
341
|
<div data-osd-chrome style={{ display: 'contents' }}>
|
|
323
342
|
<PresentProgressBar index={index} total={pages.length} visible={chromeVisible} />
|
|
324
343
|
<PresentBlackoutOverlay mode={blackout} />
|
|
325
|
-
<PresentJumpInput pageCount={pages.length} onJump={
|
|
344
|
+
<PresentJumpInput pageCount={pages.length} onJump={handleIndexChange} />
|
|
326
345
|
<PresentLaserPointer enabled={laser} />
|
|
327
346
|
<PresentControlBar
|
|
328
347
|
tooltipContainer={rootEl}
|
|
@@ -350,7 +369,7 @@ export function Player({
|
|
|
350
369
|
open={overviewOpen}
|
|
351
370
|
current={index}
|
|
352
371
|
onClose={() => setOverviewOpen(false)}
|
|
353
|
-
onSelect={
|
|
372
|
+
onSelect={handleIndexChange}
|
|
354
373
|
/>
|
|
355
374
|
<PresentHelpOverlay open={helpOpen} onOpenChange={setHelpOpen} container={rootEl} />
|
|
356
375
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { type MutableRefObject, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { SlidePageProvider } from '../lib/page-context';
|
|
3
3
|
import type { Page } from '../lib/sdk';
|
|
4
|
+
import { type EntryDirection, type StepController, StepHost } from '../lib/step-context';
|
|
4
5
|
import { resolveTransition, type SlideTransition, type TransitionPhase } from '../lib/transition';
|
|
5
6
|
|
|
6
7
|
type Props = {
|
|
@@ -9,6 +10,8 @@ type Props = {
|
|
|
9
10
|
total: number;
|
|
10
11
|
moduleTransition?: SlideTransition;
|
|
11
12
|
disabled?: boolean;
|
|
13
|
+
stepControllerRef?: MutableRefObject<StepController | null>;
|
|
14
|
+
entryDirection?: EntryDirection;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
17
|
type Direction = 'forward' | 'backward';
|
|
@@ -30,7 +33,15 @@ function runPhase(
|
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export function SlideTransitionLayer({
|
|
36
|
+
export function SlideTransitionLayer({
|
|
37
|
+
pages,
|
|
38
|
+
index,
|
|
39
|
+
total,
|
|
40
|
+
moduleTransition,
|
|
41
|
+
disabled,
|
|
42
|
+
stepControllerRef,
|
|
43
|
+
entryDirection = 'jump',
|
|
44
|
+
}: Props) {
|
|
34
45
|
const [current, setCurrent] = useState(index);
|
|
35
46
|
const [outgoing, setOutgoing] = useState<number | null>(null);
|
|
36
47
|
const [direction, setDirection] = useState<Direction>('forward');
|
|
@@ -129,6 +140,15 @@ export function SlideTransitionLayer({ pages, index, total, moduleTransition, di
|
|
|
129
140
|
const CurrentPage = pages[current];
|
|
130
141
|
const OutgoingPage = outgoing !== null ? pages[outgoing] : null;
|
|
131
142
|
|
|
143
|
+
// Outgoing layer mirrors the direction we just navigated so its <Steps>
|
|
144
|
+
// re-mounts in the state the audience just saw: forward nav → outgoing was
|
|
145
|
+
// fully revealed; backward nav → outgoing was at zero reveals.
|
|
146
|
+
const outgoingEntryDirection: EntryDirection =
|
|
147
|
+
entryDirection === 'backward' ? 'forward' : 'backward';
|
|
148
|
+
|
|
149
|
+
const noopControllerRef = useRef<StepController | null>(null);
|
|
150
|
+
const activeControllerRef = stepControllerRef ?? noopControllerRef;
|
|
151
|
+
|
|
132
152
|
return (
|
|
133
153
|
<div
|
|
134
154
|
ref={wrapperRef}
|
|
@@ -138,14 +158,26 @@ export function SlideTransitionLayer({ pages, index, total, moduleTransition, di
|
|
|
138
158
|
{OutgoingPage && outgoing !== null ? (
|
|
139
159
|
<div ref={outgoingLayerRef} className="absolute inset-0">
|
|
140
160
|
<SlidePageProvider index={outgoing} total={total}>
|
|
141
|
-
<
|
|
161
|
+
<StepHost
|
|
162
|
+
isActivePage={false}
|
|
163
|
+
entryDirection={outgoingEntryDirection}
|
|
164
|
+
controllerRef={activeControllerRef}
|
|
165
|
+
>
|
|
166
|
+
<OutgoingPage />
|
|
167
|
+
</StepHost>
|
|
142
168
|
</SlidePageProvider>
|
|
143
169
|
</div>
|
|
144
170
|
) : null}
|
|
145
171
|
{CurrentPage ? (
|
|
146
172
|
<div ref={incomingLayerRef} className="absolute inset-0">
|
|
147
173
|
<SlidePageProvider index={current} total={total}>
|
|
148
|
-
<
|
|
174
|
+
<StepHost
|
|
175
|
+
isActivePage
|
|
176
|
+
entryDirection={entryDirection}
|
|
177
|
+
controllerRef={activeControllerRef}
|
|
178
|
+
>
|
|
179
|
+
<CurrentPage />
|
|
180
|
+
</StepHost>
|
|
149
181
|
</SlidePageProvider>
|
|
150
182
|
</div>
|
|
151
183
|
) : null}
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
verticalListSortingStrategy,
|
|
16
16
|
} from '@dnd-kit/sortable';
|
|
17
17
|
import { CSS } from '@dnd-kit/utilities';
|
|
18
|
-
import { Copy, Trash2 } from 'lucide-react';
|
|
19
|
-
import { Fragment, useEffect, useRef } from 'react';
|
|
18
|
+
import { Copy, ListOrdered, type LucideIcon, Sparkles, Trash2 } from 'lucide-react';
|
|
19
|
+
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
20
20
|
import {
|
|
21
21
|
ContextMenu,
|
|
22
22
|
ContextMenuContent,
|
|
@@ -25,12 +25,14 @@ import {
|
|
|
25
25
|
ContextMenuTrigger,
|
|
26
26
|
} from '@/components/ui/context-menu';
|
|
27
27
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
28
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
28
29
|
import { format, useLocale } from '@/lib/use-locale';
|
|
29
30
|
import { cn } from '@/lib/utils';
|
|
30
31
|
import type { DesignSystem } from '../lib/design';
|
|
31
32
|
import { SlidePageProvider } from '../lib/page-context';
|
|
32
33
|
import type { Page } from '../lib/sdk';
|
|
33
34
|
import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
|
|
35
|
+
import type { SlideTransition } from '../lib/transition';
|
|
34
36
|
import { SlideCanvas } from './slide-canvas';
|
|
35
37
|
|
|
36
38
|
type Orientation = 'vertical' | 'horizontal';
|
|
@@ -50,6 +52,8 @@ type Props = {
|
|
|
50
52
|
orientation?: Orientation;
|
|
51
53
|
/** Vertical-only: total rail width in px. Thumbnails scale to fit. */
|
|
52
54
|
width?: number;
|
|
55
|
+
/** Deck-level transition default; used to flag pages that inherit a transition. */
|
|
56
|
+
moduleTransition?: SlideTransition;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
const DEFAULT_VERTICAL_THUMB_WIDTH = 184;
|
|
@@ -66,6 +70,7 @@ export function ThumbnailRail({
|
|
|
66
70
|
actions,
|
|
67
71
|
orientation = 'vertical',
|
|
68
72
|
width,
|
|
73
|
+
moduleTransition,
|
|
69
74
|
}: Props) {
|
|
70
75
|
const activeRef = useRef<HTMLButtonElement | null>(null);
|
|
71
76
|
const t = useLocale();
|
|
@@ -165,6 +170,7 @@ export function ThumbnailRail({
|
|
|
165
170
|
scale={scale}
|
|
166
171
|
thumbWidth={thumbWidth}
|
|
167
172
|
height={height}
|
|
173
|
+
moduleTransition={moduleTransition}
|
|
168
174
|
/>
|
|
169
175
|
);
|
|
170
176
|
|
|
@@ -218,15 +224,21 @@ export function ThumbnailRail({
|
|
|
218
224
|
);
|
|
219
225
|
|
|
220
226
|
if (!onReorder) {
|
|
221
|
-
return
|
|
227
|
+
return (
|
|
228
|
+
<TooltipProvider delayDuration={200}>
|
|
229
|
+
<ScrollArea className="h-full border-r border-hairline bg-sidebar">{list}</ScrollArea>
|
|
230
|
+
</TooltipProvider>
|
|
231
|
+
);
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
return (
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
<TooltipProvider delayDuration={200}>
|
|
236
|
+
<ScrollArea className="h-full border-r border-hairline bg-sidebar">
|
|
237
|
+
<SortableRail pages={pages} onReorder={onReorder} onSelect={onSelect}>
|
|
238
|
+
{list}
|
|
239
|
+
</SortableRail>
|
|
240
|
+
</ScrollArea>
|
|
241
|
+
</TooltipProvider>
|
|
230
242
|
);
|
|
231
243
|
}
|
|
232
244
|
|
|
@@ -247,6 +259,7 @@ function ThumbContents({
|
|
|
247
259
|
scale,
|
|
248
260
|
thumbWidth,
|
|
249
261
|
height,
|
|
262
|
+
moduleTransition,
|
|
250
263
|
}: {
|
|
251
264
|
index: number;
|
|
252
265
|
total: number;
|
|
@@ -256,18 +269,45 @@ function ThumbContents({
|
|
|
256
269
|
scale: number;
|
|
257
270
|
thumbWidth: number;
|
|
258
271
|
height: number;
|
|
272
|
+
moduleTransition?: SlideTransition;
|
|
259
273
|
}) {
|
|
274
|
+
const t = useLocale();
|
|
275
|
+
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
276
|
+
const [hasSteps, setHasSteps] = useState(false);
|
|
277
|
+
|
|
278
|
+
// Steps live in JSX and can't be introspected statically — detect them from
|
|
279
|
+
// the already-rendered thumbnail DOM, where each Step emits `data-osd-step`.
|
|
280
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-detect when the page at this slot changes (reorder/edit reuses the index)
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
setHasSteps(boxRef.current?.querySelector('[data-osd-step]') != null);
|
|
283
|
+
}, [PageComp]);
|
|
284
|
+
|
|
285
|
+
const hasTransition = Boolean(PageComp.transition ?? moduleTransition);
|
|
286
|
+
|
|
260
287
|
return (
|
|
261
288
|
<>
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
289
|
+
<div className="mt-1.5 flex w-7 shrink-0 flex-col items-end gap-1">
|
|
290
|
+
<span
|
|
291
|
+
className={cn(
|
|
292
|
+
'font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
293
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
{(index + 1).toString().padStart(2, '0')}
|
|
297
|
+
</span>
|
|
298
|
+
{(hasTransition || hasSteps) && (
|
|
299
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
300
|
+
{hasTransition && (
|
|
301
|
+
<ThumbIndicator icon={Sparkles} label={t.thumbnailRail.transitionIndicator} />
|
|
302
|
+
)}
|
|
303
|
+
{hasSteps && (
|
|
304
|
+
<ThumbIndicator icon={ListOrdered} label={t.thumbnailRail.stepsIndicator} />
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
266
307
|
)}
|
|
267
|
-
>
|
|
268
|
-
{(index + 1).toString().padStart(2, '0')}
|
|
269
|
-
</span>
|
|
308
|
+
</div>
|
|
270
309
|
<div
|
|
310
|
+
ref={boxRef}
|
|
271
311
|
className={cn(
|
|
272
312
|
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
|
|
273
313
|
active
|
|
@@ -292,6 +332,28 @@ function ThumbContents({
|
|
|
292
332
|
);
|
|
293
333
|
}
|
|
294
334
|
|
|
335
|
+
function ThumbIndicator({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
|
|
336
|
+
return (
|
|
337
|
+
<Tooltip>
|
|
338
|
+
<TooltipTrigger asChild>
|
|
339
|
+
<span
|
|
340
|
+
role="img"
|
|
341
|
+
aria-label={label}
|
|
342
|
+
className={cn(
|
|
343
|
+
'flex size-3.5 items-center justify-center text-muted-foreground/55',
|
|
344
|
+
'motion-safe:transition-colors group-hover/thumb:text-muted-foreground/80',
|
|
345
|
+
)}
|
|
346
|
+
>
|
|
347
|
+
<Icon className="size-3" strokeWidth={2} />
|
|
348
|
+
</span>
|
|
349
|
+
</TooltipTrigger>
|
|
350
|
+
<TooltipContent side="right" sideOffset={6}>
|
|
351
|
+
{label}
|
|
352
|
+
</TooltipContent>
|
|
353
|
+
</Tooltip>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
295
357
|
function ThumbContextMenu({
|
|
296
358
|
index,
|
|
297
359
|
actions,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
type Context,
|
|
4
|
+
cloneElement,
|
|
5
|
+
createContext,
|
|
6
|
+
isValidElement,
|
|
7
|
+
type MutableRefObject,
|
|
8
|
+
type PropsWithChildren,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import { usePrefersReducedMotion } from './use-prefers-reduced-motion';
|
|
18
|
+
|
|
19
|
+
export type EntryDirection = 'forward' | 'backward' | 'jump';
|
|
20
|
+
|
|
21
|
+
export type StepController = {
|
|
22
|
+
advance: () => boolean;
|
|
23
|
+
retreat: () => boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type StepHostContextValue = {
|
|
27
|
+
register: (ctrl: StepController) => () => void;
|
|
28
|
+
entryDirection: EntryDirection;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const GLOBAL_KEY = '__open_slide_step_host_context__';
|
|
32
|
+
type GlobalWithCtx = typeof globalThis & {
|
|
33
|
+
[GLOBAL_KEY]?: Context<StepHostContextValue | null>;
|
|
34
|
+
};
|
|
35
|
+
const g = globalThis as GlobalWithCtx;
|
|
36
|
+
if (!g[GLOBAL_KEY]) {
|
|
37
|
+
g[GLOBAL_KEY] = createContext<StepHostContextValue | null>(null);
|
|
38
|
+
}
|
|
39
|
+
const StepHostContext = g[GLOBAL_KEY];
|
|
40
|
+
|
|
41
|
+
type StepHostProps = PropsWithChildren<{
|
|
42
|
+
isActivePage: boolean;
|
|
43
|
+
entryDirection: EntryDirection;
|
|
44
|
+
controllerRef: MutableRefObject<StepController | null>;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export function StepHost({ isActivePage, entryDirection, controllerRef, children }: StepHostProps) {
|
|
48
|
+
const controllersRef = useRef<StepController[]>([]);
|
|
49
|
+
|
|
50
|
+
const composite = useMemo<StepController>(
|
|
51
|
+
() => ({
|
|
52
|
+
advance: () => {
|
|
53
|
+
for (const c of controllersRef.current) {
|
|
54
|
+
if (c.advance()) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
},
|
|
58
|
+
retreat: () => {
|
|
59
|
+
for (let i = controllersRef.current.length - 1; i >= 0; i--) {
|
|
60
|
+
if (controllersRef.current[i].retreat()) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
[],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// useLayoutEffect cleanup-then-mount ordering keeps the registry slot
|
|
69
|
+
// continuous across page swaps — the outgoing host clears its composite
|
|
70
|
+
// before the next active host installs its own, with no gap and no overlap.
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (!isActivePage) return;
|
|
73
|
+
controllerRef.current = composite;
|
|
74
|
+
return () => {
|
|
75
|
+
if (controllerRef.current === composite) controllerRef.current = null;
|
|
76
|
+
};
|
|
77
|
+
}, [isActivePage, composite, controllerRef]);
|
|
78
|
+
|
|
79
|
+
const value = useMemo<StepHostContextValue>(
|
|
80
|
+
() => ({
|
|
81
|
+
register: (ctrl) => {
|
|
82
|
+
if (!isActivePage) return () => {};
|
|
83
|
+
controllersRef.current.push(ctrl);
|
|
84
|
+
return () => {
|
|
85
|
+
const i = controllersRef.current.indexOf(ctrl);
|
|
86
|
+
if (i !== -1) controllersRef.current.splice(i, 1);
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
entryDirection,
|
|
90
|
+
}),
|
|
91
|
+
[isActivePage, entryDirection],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return <StepHostContext.Provider value={value}>{children}</StepHostContext.Provider>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type StepsProps = PropsWithChildren;
|
|
98
|
+
|
|
99
|
+
export function Steps({ children }: StepsProps) {
|
|
100
|
+
const host = useContext(StepHostContext);
|
|
101
|
+
const flat = Children.toArray(children);
|
|
102
|
+
const stepCount = flat.filter((c) => isValidElement(c) && c.type === Step).length;
|
|
103
|
+
|
|
104
|
+
const initial = host?.entryDirection === 'forward' ? 0 : stepCount;
|
|
105
|
+
const revealedRef = useRef(initial);
|
|
106
|
+
const [revealed, setRevealed] = useState(initial);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!host) return;
|
|
110
|
+
const ctrl: StepController = {
|
|
111
|
+
advance: () => {
|
|
112
|
+
if (revealedRef.current >= stepCount) return false;
|
|
113
|
+
revealedRef.current += 1;
|
|
114
|
+
setRevealed(revealedRef.current);
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
retreat: () => {
|
|
118
|
+
if (revealedRef.current <= 0) return false;
|
|
119
|
+
revealedRef.current -= 1;
|
|
120
|
+
setRevealed(revealedRef.current);
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return host.register(ctrl);
|
|
125
|
+
}, [host, stepCount]);
|
|
126
|
+
|
|
127
|
+
const effectiveRevealed = host ? revealed : stepCount;
|
|
128
|
+
|
|
129
|
+
let stepIdx = 0;
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
{flat.map((child, key) => {
|
|
133
|
+
if (isValidElement(child) && child.type === Step) {
|
|
134
|
+
const idx = stepIdx++;
|
|
135
|
+
return cloneElement(child as ReactElement<{ _revealed?: boolean }>, {
|
|
136
|
+
key: child.key ?? key,
|
|
137
|
+
_revealed: idx < effectiveRevealed,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return child;
|
|
141
|
+
})}
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type StepProps = PropsWithChildren<{
|
|
147
|
+
duration?: number;
|
|
148
|
+
}>;
|
|
149
|
+
|
|
150
|
+
type InternalStepProps = StepProps & { _revealed?: boolean };
|
|
151
|
+
|
|
152
|
+
export function Step({ children, duration = 180, _revealed }: InternalStepProps) {
|
|
153
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
154
|
+
const revealed = _revealed ?? true;
|
|
155
|
+
const ms = reduceMotion ? 0 : duration;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
data-osd-step={revealed ? 'revealed' : 'pending'}
|
|
160
|
+
style={{
|
|
161
|
+
opacity: revealed ? 1 : 0,
|
|
162
|
+
visibility: revealed ? 'visible' : 'hidden',
|
|
163
|
+
transition: `opacity ${ms}ms cubic-bezier(0, 0, 0.2, 1)`,
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
package/src/app/routes/slide.tsx
CHANGED
|
@@ -642,6 +642,7 @@ export function Slide() {
|
|
|
642
642
|
onSelect={goTo}
|
|
643
643
|
onReorder={import.meta.env.DEV ? reorderPage : undefined}
|
|
644
644
|
actions={thumbnailActions}
|
|
645
|
+
moduleTransition={slide.transition}
|
|
645
646
|
/>
|
|
646
647
|
<main
|
|
647
648
|
ref={slideViewportRef}
|
|
@@ -724,6 +725,7 @@ function ResizableRail(props: {
|
|
|
724
725
|
onSelect: (i: number) => void;
|
|
725
726
|
onReorder?: (from: number, to: number) => void;
|
|
726
727
|
actions?: ThumbnailActions;
|
|
728
|
+
moduleTransition?: SlideModule['transition'];
|
|
727
729
|
}) {
|
|
728
730
|
const t = useLocale();
|
|
729
731
|
const [width, setWidth] = useState<number>(readStoredRailWidth);
|
package/src/locale/en.ts
CHANGED
|
@@ -343,6 +343,8 @@ export const en: Locale = {
|
|
|
343
343
|
toastDuplicateFailed: 'Could not duplicate page',
|
|
344
344
|
toastDeleteFailed: 'Could not delete page',
|
|
345
345
|
resizeRail: 'Resize thumbnail rail',
|
|
346
|
+
transitionIndicator: 'Has slide transition',
|
|
347
|
+
stepsIndicator: 'Has step-by-step reveals',
|
|
346
348
|
},
|
|
347
349
|
|
|
348
350
|
pdfToast: {
|
package/src/locale/ja.ts
CHANGED
package/src/locale/types.ts
CHANGED
package/src/locale/zh-cn.ts
CHANGED