@textcortex/slidewise 1.5.0 → 1.7.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/index.mjs +5459 -5428
- package/dist/index.mjs.map +1 -1
- package/dist/slidewise.css +1 -1
- package/package.json +1 -1
- package/src/SlidewiseEditor.css +57 -0
- package/src/SlidewiseEditor.tsx +16 -1
- package/src/SlidewiseFileEditor.tsx +12 -0
- package/src/compound/SlidewiseRoot.tsx +69 -19
- package/src/compound/index.ts +13 -1
- package/src/compound/parts.tsx +4 -11
- package/src/compound/sliderail/AddButton.tsx +76 -0
- package/src/compound/sliderail/Header.tsx +84 -0
- package/src/compound/sliderail/Item.tsx +82 -0
- package/src/compound/sliderail/ItemContext.tsx +42 -0
- package/src/compound/sliderail/List.tsx +67 -0
- package/src/compound/sliderail/Number.tsx +57 -0
- package/src/compound/sliderail/Root.tsx +54 -0
- package/src/compound/sliderail/Thumbnail.tsx +58 -0
- package/src/compound/sliderail/index.tsx +74 -0
- package/src/index.ts +10 -0
- package/src/components/editor/SlideRail.tsx +0 -285
package/package.json
CHANGED
package/src/SlidewiseEditor.css
CHANGED
|
@@ -62,6 +62,26 @@
|
|
|
62
62
|
--slidewise-bg-pill: var(--smart-grad);
|
|
63
63
|
--slidewise-bg-unsaved-badge: rgba(232, 80, 76, 0.12);
|
|
64
64
|
|
|
65
|
+
/* Motion tokens. Hosts retune the editor's whole animation feel by
|
|
66
|
+
overriding these. Defaults match Material 3 "standard" easings. */
|
|
67
|
+
--slidewise-duration-instant: 0ms;
|
|
68
|
+
--slidewise-duration-fast: 120ms;
|
|
69
|
+
--slidewise-duration-base: 200ms;
|
|
70
|
+
--slidewise-duration-slow: 320ms;
|
|
71
|
+
--slidewise-easing-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
72
|
+
--slidewise-easing-emphasized: cubic-bezier(0.05, 0.7, 0.1, 1);
|
|
73
|
+
--slidewise-easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
74
|
+
|
|
75
|
+
/* Per-region animation flags. Set to 0 on a wrapping scope to disable a
|
|
76
|
+
specific region's animations without killing motion globally. They are
|
|
77
|
+
read internally by region-scoped transitions; CSS rules use them via
|
|
78
|
+
`transition-duration: calc(var(--slidewise-duration-base) * var(--slidewise-anim-topbar))`. */
|
|
79
|
+
--slidewise-anim-topbar: 1;
|
|
80
|
+
--slidewise-anim-rail: 1;
|
|
81
|
+
--slidewise-anim-canvas: 1;
|
|
82
|
+
--slidewise-anim-floating-toolbar: 1;
|
|
83
|
+
--slidewise-anim-play-mode: 1;
|
|
84
|
+
|
|
65
85
|
/* Layout backgrounds */
|
|
66
86
|
--app-bg: #ffffff;
|
|
67
87
|
--rail-bg: #fafafb;
|
|
@@ -295,3 +315,40 @@
|
|
|
295
315
|
transition: none;
|
|
296
316
|
}
|
|
297
317
|
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* Reduced motion plumbing.
|
|
321
|
+
*
|
|
322
|
+
* <Slidewise.Root reduceMotion="system"> → no class; respects the
|
|
323
|
+
* OS preference via the media
|
|
324
|
+
* query below.
|
|
325
|
+
* <Slidewise.Root reduceMotion={true}> → adds `.reduce-motion` class
|
|
326
|
+
* for hard-off.
|
|
327
|
+
* <Slidewise.Root reduceMotion={false}> → adds `.reduce-motion-off`
|
|
328
|
+
* class to override the OS
|
|
329
|
+
* preference (testing /
|
|
330
|
+
* previewing animations).
|
|
331
|
+
*/
|
|
332
|
+
.slidewise-editor.reduce-motion,
|
|
333
|
+
.slidewise-editor.reduce-motion *,
|
|
334
|
+
.slidewise-editor.reduce-motion *::before,
|
|
335
|
+
.slidewise-editor.reduce-motion *::after {
|
|
336
|
+
animation-duration: 0ms !important;
|
|
337
|
+
animation-delay: 0ms !important;
|
|
338
|
+
transition-duration: 0ms !important;
|
|
339
|
+
transition-delay: 0ms !important;
|
|
340
|
+
scroll-behavior: auto !important;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@media (prefers-reduced-motion: reduce) {
|
|
344
|
+
.slidewise-editor:not(.reduce-motion-off),
|
|
345
|
+
.slidewise-editor:not(.reduce-motion-off) *,
|
|
346
|
+
.slidewise-editor:not(.reduce-motion-off) *::before,
|
|
347
|
+
.slidewise-editor:not(.reduce-motion-off) *::after {
|
|
348
|
+
animation-duration: 0ms !important;
|
|
349
|
+
animation-delay: 0ms !important;
|
|
350
|
+
transition-duration: 0ms !important;
|
|
351
|
+
transition-delay: 0ms !important;
|
|
352
|
+
scroll-behavior: auto !important;
|
|
353
|
+
}
|
|
354
|
+
}
|
package/src/SlidewiseEditor.tsx
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type SelectionSnapshot,
|
|
8
8
|
} from "./compound/SlidewiseRoot";
|
|
9
9
|
import { TopBar } from "./compound/topbar";
|
|
10
|
+
import { SlideRail } from "./compound/sliderail";
|
|
10
11
|
import {
|
|
11
|
-
SlideRail,
|
|
12
12
|
Canvas,
|
|
13
13
|
BottomToolbar,
|
|
14
14
|
Body,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { SlidewiseIcons } from "./compound/IconContext";
|
|
18
18
|
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
19
19
|
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
20
|
+
import type { Transition } from "framer-motion";
|
|
20
21
|
import type { Deck } from "@/lib/types";
|
|
21
22
|
import "./SlidewiseEditor.css";
|
|
22
23
|
|
|
@@ -67,6 +68,16 @@ export interface SlidewiseEditorProps {
|
|
|
67
68
|
showBottomToolbar?: boolean;
|
|
68
69
|
/** Override the bundled Geist font; sets `--font-geist-sans` on the root. */
|
|
69
70
|
fontFamily?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Reduced-motion behavior. `"system"` (default) respects the OS
|
|
73
|
+
* preference; `true` forces motion off; `false` forces it on.
|
|
74
|
+
*/
|
|
75
|
+
reduceMotion?: boolean | "system";
|
|
76
|
+
/**
|
|
77
|
+
* Default framer-motion transition applied via `<MotionConfig>`. Use
|
|
78
|
+
* this to retune the editor's animation feel globally.
|
|
79
|
+
*/
|
|
80
|
+
transition?: Transition;
|
|
70
81
|
/**
|
|
71
82
|
* Per-action icon overrides. Pass a ReactNode for any of `undo`, `redo`,
|
|
72
83
|
* `save`, `play`, `themeLight`, `themeDark`, `export`, `smart` to skin the
|
|
@@ -135,6 +146,8 @@ export const SlidewiseEditor = forwardRef<
|
|
|
135
146
|
showTopBar = true,
|
|
136
147
|
showBottomToolbar = true,
|
|
137
148
|
fontFamily,
|
|
149
|
+
reduceMotion,
|
|
150
|
+
transition,
|
|
138
151
|
icons,
|
|
139
152
|
labels,
|
|
140
153
|
surfaces,
|
|
@@ -160,6 +173,8 @@ export const SlidewiseEditor = forwardRef<
|
|
|
160
173
|
theme,
|
|
161
174
|
initialSlideId,
|
|
162
175
|
fontFamily,
|
|
176
|
+
reduceMotion,
|
|
177
|
+
transition,
|
|
163
178
|
icons,
|
|
164
179
|
labels,
|
|
165
180
|
surfaces,
|
|
@@ -13,6 +13,7 @@ import type { SlidewiseIcons } from "./compound/IconContext";
|
|
|
13
13
|
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
14
14
|
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
15
15
|
import { DEFAULT_LABELS } from "./compound/LabelsContext";
|
|
16
|
+
import type { Transition } from "framer-motion";
|
|
16
17
|
import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
|
|
17
18
|
|
|
18
19
|
export interface SlidewiseFileEditorProps {
|
|
@@ -105,6 +106,13 @@ export interface SlidewiseFileEditorProps {
|
|
|
105
106
|
* `--slidewise-bg-*` CSS variables.
|
|
106
107
|
*/
|
|
107
108
|
surfaces?: SlidewiseSurfaces;
|
|
109
|
+
/**
|
|
110
|
+
* Reduced-motion behavior. `"system"` (default) respects the OS
|
|
111
|
+
* preference; `true` forces motion off; `false` forces it on.
|
|
112
|
+
*/
|
|
113
|
+
reduceMotion?: boolean | "system";
|
|
114
|
+
/** Default framer-motion transition applied via `<MotionConfig>`. */
|
|
115
|
+
transition?: Transition;
|
|
108
116
|
className?: string;
|
|
109
117
|
style?: CSSProperties;
|
|
110
118
|
/**
|
|
@@ -209,6 +217,8 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
209
217
|
icons,
|
|
210
218
|
labels,
|
|
211
219
|
surfaces,
|
|
220
|
+
reduceMotion,
|
|
221
|
+
transition,
|
|
212
222
|
className,
|
|
213
223
|
style,
|
|
214
224
|
parse = parsePptx,
|
|
@@ -384,6 +394,8 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
384
394
|
icons={icons}
|
|
385
395
|
labels={labels}
|
|
386
396
|
surfaces={surfaces}
|
|
397
|
+
reduceMotion={reduceMotion}
|
|
398
|
+
transition={transition}
|
|
387
399
|
onChange={(next) => {
|
|
388
400
|
onChangeRef.current?.(next);
|
|
389
401
|
}}
|
|
@@ -18,6 +18,7 @@ import { collectFontFamilies, ensureGoogleFontsLoaded } from "@/lib/fonts";
|
|
|
18
18
|
import type { Deck } from "@/lib/types";
|
|
19
19
|
import { GridView } from "@/components/editor/GridView";
|
|
20
20
|
import { PlayMode } from "@/components/editor/PlayMode";
|
|
21
|
+
import { MotionConfig, type Transition } from "framer-motion";
|
|
21
22
|
import { HostProvider } from "./HostContext";
|
|
22
23
|
import { IconProvider, type SlidewiseIcons } from "./IconContext";
|
|
23
24
|
import { ReadOnlyProvider } from "./ReadOnlyContext";
|
|
@@ -72,6 +73,29 @@ export interface SlidewiseRootProps {
|
|
|
72
73
|
initialSlideId?: string;
|
|
73
74
|
/** Override the default Geist font; sets `--slidewise-font-sans`. */
|
|
74
75
|
fontFamily?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Controls reduced-motion behavior.
|
|
78
|
+
*
|
|
79
|
+
* - `"system"` (default) — respect the user's OS preference via the
|
|
80
|
+
* `prefers-reduced-motion` media query.
|
|
81
|
+
* - `true` — force all CSS animations + transitions off and tell
|
|
82
|
+
* framer-motion to skip motion. Use for hosts whose own product
|
|
83
|
+
* already has a global motion-off toggle.
|
|
84
|
+
* - `false` — force motion on even when the OS reports reduced-motion.
|
|
85
|
+
* Useful for previewing animations during development; not generally
|
|
86
|
+
* recommended in production since it overrides an accessibility hint.
|
|
87
|
+
*/
|
|
88
|
+
reduceMotion?: boolean | "system";
|
|
89
|
+
/**
|
|
90
|
+
* Default framer-motion transition. Passed through to a wrapping
|
|
91
|
+
* `<MotionConfig>` so every motion component inside the editor inherits
|
|
92
|
+
* it. Useful for retuning the editor's overall feel (faster, springier,
|
|
93
|
+
* etc.) without touching individual components.
|
|
94
|
+
*
|
|
95
|
+
* For CSS transitions, override the duration/easing tokens instead —
|
|
96
|
+
* `--slidewise-duration-base`, `--slidewise-easing-standard`, etc.
|
|
97
|
+
*/
|
|
98
|
+
transition?: Transition;
|
|
75
99
|
/**
|
|
76
100
|
* Per-action icon overrides for the chrome. Hosts pass any subset to
|
|
77
101
|
* skin Slidewise with their own icon set; missing slots fall back to
|
|
@@ -212,6 +236,8 @@ function RootInner({
|
|
|
212
236
|
theme,
|
|
213
237
|
initialSlideId,
|
|
214
238
|
fontFamily,
|
|
239
|
+
reduceMotion = "system",
|
|
240
|
+
transition,
|
|
215
241
|
icons,
|
|
216
242
|
labels,
|
|
217
243
|
surfaces,
|
|
@@ -461,26 +487,50 @@ function RootInner({
|
|
|
461
487
|
? { ...style, ...(surfaceVars as CSSProperties) }
|
|
462
488
|
: style;
|
|
463
489
|
|
|
490
|
+
// Map our reduceMotion enum to:
|
|
491
|
+
// - a CSS class on the root (drives the @media + class rules in
|
|
492
|
+
// SlidewiseEditor.css)
|
|
493
|
+
// - framer-motion's reducedMotion prop on MotionConfig
|
|
494
|
+
const motionClass =
|
|
495
|
+
reduceMotion === true
|
|
496
|
+
? "reduce-motion"
|
|
497
|
+
: reduceMotion === false
|
|
498
|
+
? "reduce-motion-off"
|
|
499
|
+
: "";
|
|
500
|
+
const fmReducedMotion =
|
|
501
|
+
reduceMotion === true
|
|
502
|
+
? "always"
|
|
503
|
+
: reduceMotion === false
|
|
504
|
+
? "never"
|
|
505
|
+
: "user";
|
|
506
|
+
const combinedClassName = motionClass
|
|
507
|
+
? className
|
|
508
|
+
? `${className} ${motionClass}`
|
|
509
|
+
: motionClass
|
|
510
|
+
: className;
|
|
511
|
+
|
|
464
512
|
return (
|
|
465
|
-
<
|
|
466
|
-
<
|
|
467
|
-
<
|
|
468
|
-
<
|
|
469
|
-
<
|
|
470
|
-
<
|
|
471
|
-
<
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
513
|
+
<MotionConfig reducedMotion={fmReducedMotion} transition={transition}>
|
|
514
|
+
<ReadOnlyProvider readOnly={readOnly}>
|
|
515
|
+
<IconProvider icons={icons ?? {}}>
|
|
516
|
+
<LabelsProvider labels={labels}>
|
|
517
|
+
<SurfacesProvider surfaces={surfaces}>
|
|
518
|
+
<DirtyProvider dirty={dirty}>
|
|
519
|
+
<HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
|
|
520
|
+
<RootShell
|
|
521
|
+
fontFamily={fontFamily}
|
|
522
|
+
className={combinedClassName}
|
|
523
|
+
style={mergedStyle}
|
|
524
|
+
>
|
|
525
|
+
{children}
|
|
526
|
+
</RootShell>
|
|
527
|
+
</HostProvider>
|
|
528
|
+
</DirtyProvider>
|
|
529
|
+
</SurfacesProvider>
|
|
530
|
+
</LabelsProvider>
|
|
531
|
+
</IconProvider>
|
|
532
|
+
</ReadOnlyProvider>
|
|
533
|
+
</MotionConfig>
|
|
484
534
|
);
|
|
485
535
|
}
|
|
486
536
|
|
package/src/compound/index.ts
CHANGED
|
@@ -40,7 +40,6 @@ export {
|
|
|
40
40
|
type SelectionSnapshot,
|
|
41
41
|
} from "./SlidewiseRoot";
|
|
42
42
|
export {
|
|
43
|
-
SlideRail,
|
|
44
43
|
Canvas,
|
|
45
44
|
BottomToolbar,
|
|
46
45
|
RightPanel,
|
|
@@ -50,6 +49,19 @@ export {
|
|
|
50
49
|
} from "./parts";
|
|
51
50
|
|
|
52
51
|
export { TopBar, type TopBarProps, type TopBarSlotId } from "./topbar";
|
|
52
|
+
export {
|
|
53
|
+
SlideRail,
|
|
54
|
+
useSlideRailItem,
|
|
55
|
+
type SlideRailProps,
|
|
56
|
+
type SlideRailRootProps,
|
|
57
|
+
type SlideRailHeaderProps,
|
|
58
|
+
type SlideRailListProps,
|
|
59
|
+
type SlideRailItemProps,
|
|
60
|
+
type SlideRailThumbnailProps,
|
|
61
|
+
type SlideRailNumberProps,
|
|
62
|
+
type SlideRailAddButtonProps,
|
|
63
|
+
type SlideRailItemContextValue,
|
|
64
|
+
} from "./sliderail";
|
|
53
65
|
export type {
|
|
54
66
|
TopBarRootProps,
|
|
55
67
|
TopBarTitleProps,
|
package/src/compound/parts.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
import { SlideRail as SlideRailInternal } from "@/components/editor/SlideRail";
|
|
3
2
|
import { Canvas as CanvasInternal } from "@/components/editor/Canvas";
|
|
4
3
|
import { BottomToolbar as BottomToolbarInternal } from "@/components/editor/BottomToolbar";
|
|
5
4
|
|
|
@@ -8,9 +7,10 @@ import { BottomToolbar as BottomToolbarInternal } from "@/components/editor/Bott
|
|
|
8
7
|
* so any part can be omitted, wrapped, or replaced. None of these accept
|
|
9
8
|
* deck/onChange/onSave props — those live on `<Slidewise.Root>`.
|
|
10
9
|
*
|
|
11
|
-
* Note: `<Slidewise.TopBar>`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Note: `<Slidewise.TopBar>` and `<Slidewise.SlideRail>` are defined
|
|
11
|
+
* separately under `./topbar/` and `./sliderail/` because they decompose
|
|
12
|
+
* into their own subparts. Both ship a callable component for the default
|
|
13
|
+
* arrangement plus a namespace of named subparts.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export interface RegionProps {
|
|
@@ -18,13 +18,6 @@ export interface RegionProps {
|
|
|
18
18
|
style?: CSSProperties;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Left-side slide thumbnail rail with add/duplicate/delete.
|
|
23
|
-
*/
|
|
24
|
-
export function SlideRail(_props: RegionProps = {}) {
|
|
25
|
-
return <SlideRailInternal />;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
/**
|
|
29
22
|
* The main editing canvas. This is the only part that's effectively required
|
|
30
23
|
* — without it the editor renders nothing visible. Layout-wise it expects
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Plus } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
import { useReadOnly } from "../ReadOnlyContext";
|
|
5
|
+
import { useLabels } from "../LabelsContext";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Dashed "New Slide" button at the bottom of the rail. Calls
|
|
9
|
+
* `store.addSlide()`. Hidden in read-only mode.
|
|
10
|
+
*
|
|
11
|
+
* Replace with your own button when you need a different shape; the
|
|
12
|
+
* store action is exported (`useEditor((s) => s.addSlide)`).
|
|
13
|
+
*/
|
|
14
|
+
export interface SlideRailAddButtonProps {
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
ariaLabel?: string;
|
|
18
|
+
/** Override the visible label. Defaults to `labels.addSlide`. */
|
|
19
|
+
label?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AddButton({
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
ariaLabel,
|
|
27
|
+
label,
|
|
28
|
+
children,
|
|
29
|
+
}: SlideRailAddButtonProps = {}) {
|
|
30
|
+
const addSlide = useEditor((s) => s.addSlide);
|
|
31
|
+
const readOnly = useReadOnly();
|
|
32
|
+
const labels = useLabels();
|
|
33
|
+
if (readOnly) return null;
|
|
34
|
+
|
|
35
|
+
const resolved = label ?? labels.addSlide;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className={className}
|
|
41
|
+
aria-label={ariaLabel ?? resolved}
|
|
42
|
+
onClick={() => addSlide()}
|
|
43
|
+
style={{
|
|
44
|
+
height: 44,
|
|
45
|
+
margin: 12,
|
|
46
|
+
display: "flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
gap: 6,
|
|
50
|
+
background: "var(--slidewise-bg-app, var(--app-bg))",
|
|
51
|
+
border: "1px dashed var(--border-dashed)",
|
|
52
|
+
borderRadius: "var(--slidewise-radius, 10px)",
|
|
53
|
+
color: "var(--ink)",
|
|
54
|
+
fontSize: 13,
|
|
55
|
+
fontWeight: 500,
|
|
56
|
+
cursor: "pointer",
|
|
57
|
+
transition: "background 120ms, border-color 120ms, color 120ms",
|
|
58
|
+
fontFamily: "inherit",
|
|
59
|
+
...style,
|
|
60
|
+
}}
|
|
61
|
+
onMouseEnter={(e) => {
|
|
62
|
+
e.currentTarget.style.borderColor =
|
|
63
|
+
"var(--slidewise-accent, var(--accent))";
|
|
64
|
+
e.currentTarget.style.color =
|
|
65
|
+
"var(--slidewise-accent, var(--accent))";
|
|
66
|
+
}}
|
|
67
|
+
onMouseLeave={(e) => {
|
|
68
|
+
e.currentTarget.style.borderColor = "var(--border-dashed)";
|
|
69
|
+
e.currentTarget.style.color = "var(--ink)";
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{children ?? <Plus size={14} />}
|
|
73
|
+
{resolved}
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CSSProperties, PropsWithChildren } from "react";
|
|
2
|
+
import { LayoutGrid } from "lucide-react";
|
|
3
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default rail header. Renders the grid-view button and a "current / total"
|
|
7
|
+
* slide counter. Hosts can pass `children` to replace the whole content
|
|
8
|
+
* while keeping the height + border, or render their own `<header>` instead.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Default counter
|
|
12
|
+
* <Slidewise.SlideRail.Header />
|
|
13
|
+
*
|
|
14
|
+
* // Custom content
|
|
15
|
+
* <Slidewise.SlideRail.Header>
|
|
16
|
+
* <strong>{deckTitle}</strong>
|
|
17
|
+
* </Slidewise.SlideRail.Header>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface SlideRailHeaderProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Header({
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
children,
|
|
29
|
+
}: PropsWithChildren<SlideRailHeaderProps>) {
|
|
30
|
+
const slides = useEditor((s) => s.deck.slides);
|
|
31
|
+
const currentId = useEditor((s) => s.currentSlideId);
|
|
32
|
+
const setView = useEditor((s) => s.setView);
|
|
33
|
+
const idx = slides.findIndex((s) => s.id === currentId);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={className}
|
|
38
|
+
style={{
|
|
39
|
+
height: 36,
|
|
40
|
+
display: "flex",
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
justifyContent: "space-between",
|
|
43
|
+
padding: "0 12px",
|
|
44
|
+
fontSize: 12,
|
|
45
|
+
color: "var(--ink-muted)",
|
|
46
|
+
borderBottom: "1px solid var(--border)",
|
|
47
|
+
...style,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children ?? (
|
|
51
|
+
<>
|
|
52
|
+
<button
|
|
53
|
+
title="Slide overview"
|
|
54
|
+
aria-label="Open slide overview"
|
|
55
|
+
onClick={() => setView("grid")}
|
|
56
|
+
style={{
|
|
57
|
+
width: 28,
|
|
58
|
+
height: 28,
|
|
59
|
+
border: "none",
|
|
60
|
+
borderRadius: 6,
|
|
61
|
+
background: "transparent",
|
|
62
|
+
display: "flex",
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
color: "var(--ink)",
|
|
66
|
+
cursor: "pointer",
|
|
67
|
+
}}
|
|
68
|
+
onMouseEnter={(e) =>
|
|
69
|
+
(e.currentTarget.style.background = "var(--hover-strong)")
|
|
70
|
+
}
|
|
71
|
+
onMouseLeave={(e) =>
|
|
72
|
+
(e.currentTarget.style.background = "transparent")
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
<LayoutGrid size={14} />
|
|
76
|
+
</button>
|
|
77
|
+
<span>
|
|
78
|
+
{idx + 1} / {slides.length}
|
|
79
|
+
</span>
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { CSSProperties, PropsWithChildren } from "react";
|
|
2
|
+
import { useEditor } from "@/lib/StoreProvider";
|
|
3
|
+
import type { Slide } from "@/lib/types";
|
|
4
|
+
import { SlideRailItemProvider } from "./ItemContext";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a single slide in the rail. Provides the slide to descendants via
|
|
8
|
+
* context (read with `useSlideRailItem`) and wires click → `selectSlide`.
|
|
9
|
+
*
|
|
10
|
+
* The default subparts (`Thumbnail`, `Number`) read the slide off the
|
|
11
|
+
* context, so hosts can rearrange them freely. Host content (e.g. a
|
|
12
|
+
* three-dots menu, a duplicate button) drops in as additional children.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Slidewise.SlideRail.Item slide={slide}>
|
|
16
|
+
* <Slidewise.SlideRail.Thumbnail />
|
|
17
|
+
* <Slidewise.SlideRail.Number />
|
|
18
|
+
* <MyContextMenu slide={slide} />
|
|
19
|
+
* </Slidewise.SlideRail.Item>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface SlideRailItemProps {
|
|
23
|
+
slide: Slide;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
/**
|
|
27
|
+
* Override the click handler. The default selects the slide via the
|
|
28
|
+
* editor store; pass a function here to add host behavior (e.g. open
|
|
29
|
+
* a context drawer instead of selecting).
|
|
30
|
+
*/
|
|
31
|
+
onClick?: (slide: Slide) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Item({
|
|
35
|
+
slide,
|
|
36
|
+
className,
|
|
37
|
+
style,
|
|
38
|
+
onClick,
|
|
39
|
+
children,
|
|
40
|
+
}: PropsWithChildren<SlideRailItemProps>) {
|
|
41
|
+
const slides = useEditor((s) => s.deck.slides);
|
|
42
|
+
const currentId = useEditor((s) => s.currentSlideId);
|
|
43
|
+
const selectSlide = useEditor((s) => s.selectSlide);
|
|
44
|
+
const index = slides.findIndex((s) => s.id === slide.id);
|
|
45
|
+
const isCurrent = slide.id === currentId;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<SlideRailItemProvider value={{ slide, index, isCurrent }}>
|
|
49
|
+
<div
|
|
50
|
+
className={className}
|
|
51
|
+
onClick={() => (onClick ? onClick(slide) : selectSlide(slide.id))}
|
|
52
|
+
role="button"
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
aria-current={isCurrent ? "true" : undefined}
|
|
55
|
+
aria-label={`Open slide ${index + 1}`}
|
|
56
|
+
onKeyDown={(e) => {
|
|
57
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
onClick ? onClick(slide) : selectSlide(slide.id);
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
style={{
|
|
63
|
+
position: "relative",
|
|
64
|
+
padding: "0 12px",
|
|
65
|
+
marginBottom: 14,
|
|
66
|
+
cursor: "pointer",
|
|
67
|
+
background:
|
|
68
|
+
"var(--slidewise-bg-rail-item, transparent)",
|
|
69
|
+
...(isCurrent
|
|
70
|
+
? {
|
|
71
|
+
background:
|
|
72
|
+
"var(--slidewise-bg-rail-item-active, var(--accent-soft))",
|
|
73
|
+
}
|
|
74
|
+
: null),
|
|
75
|
+
...style,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
</SlideRailItemProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { Slide } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context handed to descendants of `<SlideRail.Item>` so leaf subparts
|
|
6
|
+
* (`Thumbnail`, `Number`) and host content can read which slide they're
|
|
7
|
+
* rendering against without prop drilling.
|
|
8
|
+
*/
|
|
9
|
+
export interface SlideRailItemContextValue {
|
|
10
|
+
slide: Slide;
|
|
11
|
+
/** Zero-based position in `deck.slides`. */
|
|
12
|
+
index: number;
|
|
13
|
+
/** True when this is the editor's currently-focused slide. */
|
|
14
|
+
isCurrent: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ItemContext = createContext<SlideRailItemContextValue | null>(null);
|
|
18
|
+
|
|
19
|
+
export function SlideRailItemProvider({
|
|
20
|
+
value,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
value: SlideRailItemContextValue;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the current item context. Throws when used outside a
|
|
31
|
+
* `<SlideRail.Item>` — the leaf subparts are meaningless without a
|
|
32
|
+
* concrete slide to render.
|
|
33
|
+
*/
|
|
34
|
+
export function useSlideRailItem(): SlideRailItemContextValue {
|
|
35
|
+
const ctx = useContext(ItemContext);
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"useSlideRailItem must be used inside <Slidewise.SlideRail.Item>"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return ctx;
|
|
42
|
+
}
|