@bendyline/squisq-react 1.4.2 → 2.0.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/README.md +30 -3
- package/dist/index.d.ts +174 -27
- package/dist/index.js +1244 -603
- package/dist/index.js.map +1 -1
- package/dist/squisq-player.global.js +54 -37
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/package.json +2 -2
- package/src/BlockRenderer.tsx +53 -17
- package/src/DocControlsSlideshow.tsx +222 -5
- package/src/DocPlayer.tsx +367 -183
- package/src/DocPlayerWithSidebar.tsx +4 -0
- package/src/DocProgressBar.tsx +40 -1
- package/src/LinearDocView.tsx +135 -62
- package/src/MarkdownRenderer.tsx +40 -97
- package/src/MediaClipLayer.tsx +12 -2
- package/src/__tests__/BlockRenderer.test.tsx +79 -8
- package/src/__tests__/DocControlsSlideshow.test.tsx +94 -1
- package/src/__tests__/DocPlayer.test.tsx +505 -0
- package/src/__tests__/DocProgressBar.test.tsx +28 -2
- package/src/__tests__/LinearDocView.test.tsx +91 -11
- package/src/__tests__/MapLayer.test.tsx +63 -0
- package/src/__tests__/MarkdownRenderer.test.tsx +13 -2
- package/src/__tests__/MediaClipLayer.test.tsx +70 -0
- package/src/__tests__/MediaContext.test.tsx +51 -0
- package/src/__tests__/PathLayer.test.tsx +12 -1
- package/src/__tests__/VideoLayer.test.tsx +94 -0
- package/src/__tests__/fillStyle.test.tsx +3 -2
- package/src/__tests__/standaloneEntry.test.tsx +103 -0
- package/src/__tests__/useAudioSync.test.ts +49 -0
- package/src/__tests__/useDocPlayback.transition.test.ts +48 -5
- package/src/__tests__/useViewportOrientation.test.ts +22 -0
- package/src/hooks/MediaContext.tsx +12 -3
- package/src/hooks/useAudioSync.ts +61 -12
- package/src/hooks/useDocPlayback.ts +40 -12
- package/src/hooks/useViewportOrientation.ts +2 -4
- package/src/index.ts +5 -2
- package/src/layers/MapLayer.tsx +7 -6
- package/src/layers/PathLayer.tsx +20 -11
- package/src/layers/ShapeLayer.tsx +4 -2
- package/src/layers/TextLayer.tsx +4 -3
- package/src/layers/TreeLayer.tsx +167 -0
- package/src/layers/VideoLayer.tsx +20 -6
- package/src/standalone-entry.tsx +91 -14
- package/src/types.ts +13 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "React component library for doc playback, block rendering, and media layers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@bendyline/squisq": "
|
|
56
|
+
"@bendyline/squisq": "2.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/react": "18.3.28",
|
package/src/BlockRenderer.tsx
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Handles positioning, animations, and transitions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { useId } from 'react';
|
|
9
10
|
import type { Block, Layer, Transition } from '@bendyline/squisq/schemas';
|
|
10
11
|
import { resolveTransitionDuration } from '@bendyline/squisq/schemas';
|
|
11
12
|
import { ImageLayer } from './layers/ImageLayer';
|
|
@@ -15,11 +16,11 @@ import { PathLayer } from './layers/PathLayer';
|
|
|
15
16
|
import { MapLayer } from './layers/MapLayer';
|
|
16
17
|
import { VideoLayer } from './layers/VideoLayer';
|
|
17
18
|
import { TableLayer } from './layers/TableLayer';
|
|
19
|
+
import { TreeLayer } from './layers/TreeLayer';
|
|
18
20
|
import { getTransitionClass } from './utils/animationUtils';
|
|
19
21
|
|
|
20
|
-
/** Default viewport dimensions (1080p landscape)
|
|
21
|
-
|
|
22
|
-
export const VIEWPORT = {
|
|
22
|
+
/** Default viewport dimensions (1080p landscape). */
|
|
23
|
+
const DEFAULT_VIEWPORT = {
|
|
23
24
|
width: 1920,
|
|
24
25
|
height: 1080,
|
|
25
26
|
};
|
|
@@ -47,6 +48,12 @@ interface BlockRendererProps {
|
|
|
47
48
|
viewport?: ViewportDimensions;
|
|
48
49
|
/** Whether the doc is currently playing (controls video playback) */
|
|
49
50
|
isPlaying?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether to render block transitions and layer animations (default: true).
|
|
53
|
+
* Disabling this only removes authored/render-style motion; timed video
|
|
54
|
+
* layers continue to advance normally.
|
|
55
|
+
*/
|
|
56
|
+
animationsEnabled?: boolean;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export function BlockRenderer({
|
|
@@ -56,13 +63,14 @@ export function BlockRenderer({
|
|
|
56
63
|
isEntering = false,
|
|
57
64
|
isExiting = false,
|
|
58
65
|
transition,
|
|
59
|
-
viewport =
|
|
66
|
+
viewport = DEFAULT_VIEWPORT,
|
|
60
67
|
isPlaying,
|
|
68
|
+
animationsEnabled = true,
|
|
61
69
|
}: BlockRendererProps) {
|
|
62
70
|
// Build transition class and inline style for dynamic duration
|
|
63
71
|
let transitionClass = '';
|
|
64
72
|
const transitionStyle: Record<string, string> = {};
|
|
65
|
-
const activeTransition = transition ?? block.transition;
|
|
73
|
+
const activeTransition = animationsEnabled ? (transition ?? block.transition) : undefined;
|
|
66
74
|
if (activeTransition && isEntering) {
|
|
67
75
|
transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
|
|
68
76
|
transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
|
|
@@ -71,8 +79,10 @@ export function BlockRenderer({
|
|
|
71
79
|
transitionStyle['--transition-duration'] = `${resolveTransitionDuration(activeTransition)}s`;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
//
|
|
75
|
-
|
|
82
|
+
// React's instance id keeps SVG fragment references local even when the
|
|
83
|
+
// same block is rendered in a thumbnail, preview, and player at once.
|
|
84
|
+
const instanceId = useId().replace(/:/g, '');
|
|
85
|
+
const clipId = `vb-clip-${instanceId}-${block.id}`;
|
|
76
86
|
|
|
77
87
|
return (
|
|
78
88
|
<svg
|
|
@@ -102,6 +112,7 @@ export function BlockRenderer({
|
|
|
102
112
|
viewport={viewport}
|
|
103
113
|
blockTime={blockTime}
|
|
104
114
|
isPlaying={isPlaying}
|
|
115
|
+
animationsEnabled={animationsEnabled}
|
|
105
116
|
/>
|
|
106
117
|
))}
|
|
107
118
|
</g>
|
|
@@ -115,31 +126,54 @@ interface LayerRendererProps {
|
|
|
115
126
|
viewport: { width: number; height: number };
|
|
116
127
|
blockTime: number;
|
|
117
128
|
isPlaying?: boolean;
|
|
129
|
+
animationsEnabled: boolean;
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
/**
|
|
121
133
|
* Dispatch to the appropriate layer component based on type.
|
|
122
134
|
*/
|
|
123
|
-
function LayerRenderer({
|
|
124
|
-
|
|
135
|
+
function LayerRenderer({
|
|
136
|
+
layer,
|
|
137
|
+
basePath,
|
|
138
|
+
viewport,
|
|
139
|
+
blockTime,
|
|
140
|
+
isPlaying,
|
|
141
|
+
animationsEnabled,
|
|
142
|
+
}: LayerRendererProps) {
|
|
143
|
+
// Render policy must not mutate caller-owned Docs. A shallow copy is enough:
|
|
144
|
+
// every layer renderer reads animation only from the base layer field.
|
|
145
|
+
const renderedLayer: Layer =
|
|
146
|
+
animationsEnabled || !layer.animation ? layer : { ...layer, animation: undefined };
|
|
147
|
+
|
|
148
|
+
switch (renderedLayer.type) {
|
|
125
149
|
case 'image':
|
|
126
150
|
return (
|
|
127
|
-
<ImageLayer
|
|
151
|
+
<ImageLayer
|
|
152
|
+
layer={renderedLayer}
|
|
153
|
+
basePath={basePath}
|
|
154
|
+
viewport={viewport}
|
|
155
|
+
blockTime={blockTime}
|
|
156
|
+
/>
|
|
128
157
|
);
|
|
129
158
|
case 'text':
|
|
130
|
-
return <TextLayer layer={
|
|
159
|
+
return <TextLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
|
|
131
160
|
case 'shape':
|
|
132
|
-
return <ShapeLayer layer={
|
|
161
|
+
return <ShapeLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
|
|
133
162
|
case 'path':
|
|
134
|
-
return <PathLayer layer={
|
|
163
|
+
return <PathLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
|
|
135
164
|
case 'map':
|
|
136
165
|
return (
|
|
137
|
-
<MapLayer
|
|
166
|
+
<MapLayer
|
|
167
|
+
layer={renderedLayer}
|
|
168
|
+
basePath={basePath}
|
|
169
|
+
viewport={viewport}
|
|
170
|
+
blockTime={blockTime}
|
|
171
|
+
/>
|
|
138
172
|
);
|
|
139
173
|
case 'video':
|
|
140
174
|
return (
|
|
141
175
|
<VideoLayer
|
|
142
|
-
layer={
|
|
176
|
+
layer={renderedLayer}
|
|
143
177
|
basePath={basePath}
|
|
144
178
|
viewport={viewport}
|
|
145
179
|
blockTime={blockTime}
|
|
@@ -147,9 +181,11 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }: Laye
|
|
|
147
181
|
/>
|
|
148
182
|
);
|
|
149
183
|
case 'table':
|
|
150
|
-
return <TableLayer layer={
|
|
184
|
+
return <TableLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
|
|
185
|
+
case 'tree':
|
|
186
|
+
return <TreeLayer layer={renderedLayer} viewport={viewport} blockTime={blockTime} />;
|
|
151
187
|
default:
|
|
152
|
-
console.warn(`Unknown layer type: ${(
|
|
188
|
+
console.warn(`Unknown layer type: ${(renderedLayer as Layer).type}`);
|
|
153
189
|
return null;
|
|
154
190
|
}
|
|
155
191
|
}
|
|
@@ -11,14 +11,51 @@
|
|
|
11
11
|
* - types.ts — Shared type definitions
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
15
|
+
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
14
16
|
import type { PlaybackState, SlideNavActions } from './types';
|
|
15
17
|
|
|
18
|
+
export interface SlideshowPickerItem {
|
|
19
|
+
/** Stable identifier for the slide/block. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Human-facing slide number, or a special label such as "Cover". */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Short description shown in the slide picker. */
|
|
24
|
+
summary: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
interface DocControlsSlideshowProps {
|
|
17
28
|
state: PlaybackState;
|
|
18
29
|
slideNav: SlideNavActions;
|
|
30
|
+
/** Slides available for direct navigation from the counter popover. */
|
|
31
|
+
slides?: readonly SlideshowPickerItem[];
|
|
32
|
+
/** Controlled open state for hosts that expose their own picker shortcut. */
|
|
33
|
+
pickerOpen?: boolean;
|
|
34
|
+
/** Called whenever the slide picker should open or close. */
|
|
35
|
+
onPickerOpenChange?: (open: boolean) => void;
|
|
19
36
|
}
|
|
20
37
|
|
|
21
|
-
export function DocControlsSlideshow({
|
|
38
|
+
export function DocControlsSlideshow({
|
|
39
|
+
state,
|
|
40
|
+
slideNav,
|
|
41
|
+
slides = [],
|
|
42
|
+
pickerOpen,
|
|
43
|
+
onPickerOpenChange,
|
|
44
|
+
}: DocControlsSlideshowProps) {
|
|
45
|
+
const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState(false);
|
|
46
|
+
const isPickerOpen = pickerOpen ?? uncontrolledPickerOpen;
|
|
47
|
+
const setPickerOpen = useCallback(
|
|
48
|
+
(open: boolean) => {
|
|
49
|
+
if (pickerOpen === undefined) setUncontrolledPickerOpen(open);
|
|
50
|
+
onPickerOpenChange?.(open);
|
|
51
|
+
},
|
|
52
|
+
[pickerOpen, onPickerOpenChange],
|
|
53
|
+
);
|
|
54
|
+
const [pickerMaxHeight, setPickerMaxHeight] = useState(280);
|
|
55
|
+
const controlsRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
57
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
58
|
+
const menuId = useId();
|
|
22
59
|
const {
|
|
23
60
|
currentBlockIndex,
|
|
24
61
|
currentSlideLabel,
|
|
@@ -34,8 +71,80 @@ export function DocControlsSlideshow({ state, slideNav }: DocControlsSlideshowPr
|
|
|
34
71
|
`${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}`)
|
|
35
72
|
: '—';
|
|
36
73
|
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!isPickerOpen) return;
|
|
76
|
+
|
|
77
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
78
|
+
if (!controlsRef.current?.contains(event.target as Node)) setPickerOpen(false);
|
|
79
|
+
};
|
|
80
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
81
|
+
if (event.key !== 'Escape') return;
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
setPickerOpen(false);
|
|
84
|
+
triggerRef.current?.focus();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
88
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
91
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
92
|
+
};
|
|
93
|
+
}, [isPickerOpen, setPickerOpen]);
|
|
94
|
+
|
|
95
|
+
useLayoutEffect(() => {
|
|
96
|
+
if (!isPickerOpen) return;
|
|
97
|
+
|
|
98
|
+
const controls = controlsRef.current;
|
|
99
|
+
const player = controls?.closest<HTMLElement>('.doc-player') ?? controls?.parentElement;
|
|
100
|
+
if (!controls || !player) return;
|
|
101
|
+
|
|
102
|
+
const updateMaxHeight = () => {
|
|
103
|
+
const controlsBounds = controls.getBoundingClientRect();
|
|
104
|
+
const playerBounds = player.getBoundingClientRect();
|
|
105
|
+
// The picker sits 8px above the toolbar; retain another 16px of
|
|
106
|
+
// breathing room at the top of the player.
|
|
107
|
+
const availableHeight = Math.floor(controlsBounds.top - playerBounds.top - 8 - 16);
|
|
108
|
+
setPickerMaxHeight(Math.max(72, availableHeight));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
updateMaxHeight();
|
|
112
|
+
const resizeObserver =
|
|
113
|
+
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateMaxHeight);
|
|
114
|
+
resizeObserver?.observe(player);
|
|
115
|
+
resizeObserver?.observe(controls);
|
|
116
|
+
window.addEventListener('resize', updateMaxHeight);
|
|
117
|
+
return () => {
|
|
118
|
+
resizeObserver?.disconnect();
|
|
119
|
+
window.removeEventListener('resize', updateMaxHeight);
|
|
120
|
+
};
|
|
121
|
+
}, [isPickerOpen]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!isPickerOpen) return;
|
|
125
|
+
menuRef.current?.querySelector<HTMLButtonElement>('[aria-current="true"]')?.focus();
|
|
126
|
+
}, [isPickerOpen]);
|
|
127
|
+
|
|
128
|
+
const handleMenuKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
129
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
130
|
+
const items = Array.from(
|
|
131
|
+
menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]') ?? [],
|
|
132
|
+
);
|
|
133
|
+
if (items.length === 0) return;
|
|
134
|
+
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const focusedIndex = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
137
|
+
let nextIndex = focusedIndex;
|
|
138
|
+
if (event.key === 'Home') nextIndex = 0;
|
|
139
|
+
else if (event.key === 'End') nextIndex = items.length - 1;
|
|
140
|
+
else if (event.key === 'ArrowDown') nextIndex = (focusedIndex + 1) % items.length;
|
|
141
|
+
else nextIndex = (focusedIndex - 1 + items.length) % items.length;
|
|
142
|
+
items[nextIndex]?.focus();
|
|
143
|
+
};
|
|
144
|
+
|
|
37
145
|
return (
|
|
38
146
|
<div
|
|
147
|
+
ref={controlsRef}
|
|
39
148
|
className="doc-controls-slideshow"
|
|
40
149
|
data-testid="slideshow-controls"
|
|
41
150
|
style={{
|
|
@@ -88,22 +197,130 @@ export function DocControlsSlideshow({ state, slideNav }: DocControlsSlideshowPr
|
|
|
88
197
|
</svg>
|
|
89
198
|
</button>
|
|
90
199
|
|
|
91
|
-
{/* Slide counter */}
|
|
92
|
-
<
|
|
200
|
+
{/* Slide counter / direct-navigation picker */}
|
|
201
|
+
<button
|
|
202
|
+
ref={triggerRef}
|
|
203
|
+
type="button"
|
|
93
204
|
data-testid="slide-counter"
|
|
205
|
+
aria-label={`Choose slide, current ${counterText}`}
|
|
206
|
+
aria-haspopup="menu"
|
|
207
|
+
aria-expanded={isPickerOpen}
|
|
208
|
+
aria-controls={isPickerOpen ? menuId : undefined}
|
|
209
|
+
title="Choose slide"
|
|
210
|
+
disabled={slides.length === 0}
|
|
211
|
+
onClick={(event) => {
|
|
212
|
+
event.stopPropagation();
|
|
213
|
+
setPickerOpen(!isPickerOpen);
|
|
214
|
+
}}
|
|
94
215
|
style={{
|
|
216
|
+
background: isPickerOpen ? 'rgba(255,255,255,0.12)' : 'none',
|
|
217
|
+
border: 'none',
|
|
95
218
|
color: 'rgba(255,255,255,0.9)',
|
|
219
|
+
cursor: slides.length > 0 ? 'pointer' : 'default',
|
|
96
220
|
fontSize: '13px',
|
|
97
221
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
98
222
|
fontVariantNumeric: 'tabular-nums',
|
|
99
223
|
minWidth: '48px',
|
|
100
224
|
textAlign: 'center',
|
|
101
|
-
padding: '
|
|
225
|
+
padding: '6px 4px',
|
|
102
226
|
letterSpacing: '0.02em',
|
|
227
|
+
borderRadius: '4px',
|
|
228
|
+
transition: 'background 0.15s',
|
|
103
229
|
}}
|
|
104
230
|
>
|
|
105
231
|
{counterText}
|
|
106
|
-
</
|
|
232
|
+
</button>
|
|
233
|
+
|
|
234
|
+
{isPickerOpen && (
|
|
235
|
+
<div
|
|
236
|
+
ref={menuRef}
|
|
237
|
+
id={menuId}
|
|
238
|
+
role="menu"
|
|
239
|
+
aria-label="Choose a slide"
|
|
240
|
+
data-testid="slide-picker"
|
|
241
|
+
onClick={(event) => event.stopPropagation()}
|
|
242
|
+
onKeyDown={handleMenuKeyDown}
|
|
243
|
+
style={{
|
|
244
|
+
position: 'absolute',
|
|
245
|
+
right: 0,
|
|
246
|
+
bottom: 'calc(100% + 8px)',
|
|
247
|
+
width: 'min(280px, calc(100vw - 32px))',
|
|
248
|
+
maxHeight: `${pickerMaxHeight}px`,
|
|
249
|
+
overflowY: 'auto',
|
|
250
|
+
padding: '6px',
|
|
251
|
+
background: 'rgba(20, 20, 20, 0.94)',
|
|
252
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
253
|
+
borderRadius: '8px',
|
|
254
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.38)',
|
|
255
|
+
backdropFilter: 'blur(12px)',
|
|
256
|
+
WebkitBackdropFilter: 'blur(12px)',
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{slides.map((slide, index) => {
|
|
260
|
+
const isCurrent = index === currentBlockIndex;
|
|
261
|
+
return (
|
|
262
|
+
<button
|
|
263
|
+
key={`${slide.id}-${index}`}
|
|
264
|
+
type="button"
|
|
265
|
+
role="menuitem"
|
|
266
|
+
aria-current={isCurrent ? 'true' : undefined}
|
|
267
|
+
data-testid={`slide-picker-item-${index}`}
|
|
268
|
+
onClick={() => {
|
|
269
|
+
slideNav.goToSlide(index);
|
|
270
|
+
setPickerOpen(false);
|
|
271
|
+
}}
|
|
272
|
+
style={{
|
|
273
|
+
display: 'grid',
|
|
274
|
+
gridTemplateColumns: '42px minmax(0, 1fr)',
|
|
275
|
+
alignItems: 'center',
|
|
276
|
+
gap: '8px',
|
|
277
|
+
width: '100%',
|
|
278
|
+
padding: '8px 10px',
|
|
279
|
+
background: isCurrent ? 'rgba(255,255,255,0.14)' : 'transparent',
|
|
280
|
+
border: 'none',
|
|
281
|
+
borderRadius: '5px',
|
|
282
|
+
color: 'rgba(255,255,255,0.94)',
|
|
283
|
+
cursor: 'pointer',
|
|
284
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
285
|
+
textAlign: 'left',
|
|
286
|
+
}}
|
|
287
|
+
onMouseEnter={(event) => {
|
|
288
|
+
event.currentTarget.style.background = 'rgba(255,255,255,0.1)';
|
|
289
|
+
}}
|
|
290
|
+
onMouseLeave={(event) => {
|
|
291
|
+
event.currentTarget.style.background = isCurrent
|
|
292
|
+
? 'rgba(255,255,255,0.14)'
|
|
293
|
+
: 'transparent';
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<span
|
|
297
|
+
style={{
|
|
298
|
+
color: isCurrent ? '#fff' : 'rgba(255,255,255,0.58)',
|
|
299
|
+
fontSize: '12px',
|
|
300
|
+
fontVariantNumeric: 'tabular-nums',
|
|
301
|
+
textAlign: 'right',
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{slide.label}
|
|
305
|
+
</span>
|
|
306
|
+
<span
|
|
307
|
+
style={{
|
|
308
|
+
minWidth: 0,
|
|
309
|
+
overflow: 'hidden',
|
|
310
|
+
color: isCurrent ? '#fff' : 'rgba(255,255,255,0.82)',
|
|
311
|
+
fontSize: '13px',
|
|
312
|
+
lineHeight: 1.3,
|
|
313
|
+
textOverflow: 'ellipsis',
|
|
314
|
+
whiteSpace: 'nowrap',
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
{slide.summary}
|
|
318
|
+
</span>
|
|
319
|
+
</button>
|
|
320
|
+
);
|
|
321
|
+
})}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
107
324
|
|
|
108
325
|
{/* Next button */}
|
|
109
326
|
<button
|