@bendyline/squisq-react 2.0.1 → 2.2.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +6 -0
  4. package/dist/index.d.ts +101 -96
  5. package/dist/index.js +1483 -782
  6. package/dist/squisq-player.full.global.js +4165 -0
  7. package/dist/squisq-player.global.js +561 -168
  8. package/dist/standalone-source.d.ts +1 -0
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +64 -0
  11. package/package.json +8 -4
  12. package/dist/index.js.map +0 -1
  13. package/dist/squisq-player.css +0 -2
  14. package/dist/squisq-player.css.map +0 -1
  15. package/dist/squisq-player.global.js.map +0 -1
  16. package/src/BlockRenderer.tsx +0 -194
  17. package/src/CaptionOverlay.tsx +0 -111
  18. package/src/DocControlsBottom.tsx +0 -109
  19. package/src/DocControlsOverlay.tsx +0 -190
  20. package/src/DocControlsSidebar.tsx +0 -113
  21. package/src/DocControlsSlideshow.tsx +0 -360
  22. package/src/DocPlayer.tsx +0 -1528
  23. package/src/DocPlayerWithSidebar.tsx +0 -155
  24. package/src/DocProgressBar.tsx +0 -257
  25. package/src/InlineAudioPlayer.tsx +0 -46
  26. package/src/InlineVideoPlayer.tsx +0 -70
  27. package/src/LinearDocView.tsx +0 -569
  28. package/src/MarkdownRenderer.tsx +0 -624
  29. package/src/MediaClipLayer.tsx +0 -145
  30. package/src/SocialCaptionOverlay.tsx +0 -255
  31. package/src/__tests__/BlockRenderer.test.tsx +0 -235
  32. package/src/__tests__/DocControlsSlideshow.test.tsx +0 -221
  33. package/src/__tests__/DocPlayer.test.tsx +0 -635
  34. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +0 -41
  35. package/src/__tests__/DocProgressBar.test.tsx +0 -102
  36. package/src/__tests__/JsonView.test.tsx +0 -111
  37. package/src/__tests__/LinearDocView.test.tsx +0 -426
  38. package/src/__tests__/MapLayer.test.tsx +0 -63
  39. package/src/__tests__/MarkdownRenderer.test.tsx +0 -357
  40. package/src/__tests__/MediaClipLayer.test.tsx +0 -70
  41. package/src/__tests__/MediaContext.test.tsx +0 -51
  42. package/src/__tests__/PathLayer.test.tsx +0 -84
  43. package/src/__tests__/TableLayer.test.tsx +0 -142
  44. package/src/__tests__/VideoLayer.test.tsx +0 -94
  45. package/src/__tests__/exports.test.ts +0 -55
  46. package/src/__tests__/fillStyle.test.tsx +0 -160
  47. package/src/__tests__/standaloneEntry.test.tsx +0 -103
  48. package/src/__tests__/transitionStyles.test.ts +0 -125
  49. package/src/__tests__/useAudioSync.test.ts +0 -49
  50. package/src/__tests__/useDocPlayback.transition.test.ts +0 -113
  51. package/src/__tests__/useJsonViewTokens.test.ts +0 -41
  52. package/src/__tests__/useSlideSwipe.test.ts +0 -81
  53. package/src/__tests__/useViewportOrientation.test.ts +0 -22
  54. package/src/hooks/AudioController.ts +0 -114
  55. package/src/hooks/MediaContext.tsx +0 -97
  56. package/src/hooks/index.ts +0 -11
  57. package/src/hooks/useAudioSync.ts +0 -456
  58. package/src/hooks/useAutoSurface.ts +0 -33
  59. package/src/hooks/useDocPlayback.ts +0 -265
  60. package/src/hooks/useMediaSchedule.ts +0 -39
  61. package/src/hooks/useSlideSwipe.ts +0 -265
  62. package/src/hooks/useViewportOrientation.ts +0 -115
  63. package/src/index.ts +0 -70
  64. package/src/jsonView/JsonView.tsx +0 -51
  65. package/src/jsonView/RenderNode.tsx +0 -51
  66. package/src/jsonView/index.ts +0 -2
  67. package/src/jsonView/json-view.css +0 -206
  68. package/src/jsonView/useJsonViewTokens.ts +0 -32
  69. package/src/jsonView/viewers.tsx +0 -343
  70. package/src/layers/ImageLayer.tsx +0 -297
  71. package/src/layers/MapLayer.tsx +0 -185
  72. package/src/layers/PathLayer.tsx +0 -155
  73. package/src/layers/ShapeLayer.tsx +0 -158
  74. package/src/layers/TableLayer.tsx +0 -129
  75. package/src/layers/TextLayer.tsx +0 -571
  76. package/src/layers/TreeLayer.tsx +0 -167
  77. package/src/layers/VideoLayer.tsx +0 -171
  78. package/src/layers/index.ts +0 -6
  79. package/src/standalone-entry.tsx +0 -337
  80. package/src/standalone-source.d.ts +0 -10
  81. package/src/styles/doc-animations.css +0 -2431
  82. package/src/styles/index.css +0 -7
  83. package/src/types.ts +0 -183
  84. package/src/utils/animationUtils.ts +0 -13
  85. package/src/utils/fillStyle.tsx +0 -148
  86. package/src/utils/layerUtils.ts +0 -42
  87. package/src/utils/mapTileUtils.ts +0 -375
@@ -1,113 +0,0 @@
1
- /**
2
- * DocControlsSidebar Component
3
- *
4
- * Vertical control strip that renders alongside the doc player video.
5
- * Used in portrait orientation to eliminate black bars -- controls sit in a
6
- * "tab" to the right of the 9:16 video instead of overlaying it.
7
- *
8
- * Contains: restart, play/pause, time display, segment indicator, CC toggle.
9
- * Does NOT contain the scrubber/progress bar (that stays in the video via
10
- * DocPlayer's showScrubber prop).
11
- *
12
- * The sidebar stretches to match the full height of the video and has rounded
13
- * corners on the right side to create a connected "tab" appearance.
14
- *
15
- * Related Files:
16
- * - DocPlayerWithSidebar.tsx -- Wrapper that composes player + sidebar
17
- * - DocPlayer.tsx -- Core player (renders with showScrubber=true)
18
- * - types.ts -- PlaybackState/PlaybackActions interfaces
19
- */
20
-
21
- import type { PlaybackState, PlaybackActions } from './types';
22
- import { formatTime } from './types';
23
-
24
- interface DocControlsSidebarProps {
25
- state: PlaybackState;
26
- actions: PlaybackActions;
27
- }
28
-
29
- export function DocControlsSidebar({ state, actions }: DocControlsSidebarProps) {
30
- return (
31
- <div className="doc-controls-sidebar">
32
- {/* Restart button */}
33
- <button
34
- className="sidebar-ctrl-btn"
35
- onClick={actions.restart}
36
- title="Restart"
37
- aria-label="Restart from beginning"
38
- >
39
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
40
- <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" />
41
- </svg>
42
- </button>
43
-
44
- {/* Play/Pause button */}
45
- <button
46
- className="sidebar-ctrl-btn sidebar-play-btn"
47
- onClick={actions.toggle}
48
- aria-label={state.isPlaying ? 'Pause' : 'Play'}
49
- >
50
- {state.isPlaying ? (
51
- <svg viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
52
- <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
53
- </svg>
54
- ) : (
55
- <svg viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
56
- <path d="M8 5v14l11-7z" />
57
- </svg>
58
- )}
59
- </button>
60
-
61
- {/* Time display -- stacked current / total */}
62
- <div className="sidebar-time">
63
- <div>{formatTime(state.currentTime)}</div>
64
- <div className="sidebar-time-total">{formatTime(state.totalDuration)}</div>
65
- </div>
66
-
67
- {/* Segment indicator */}
68
- <div className="sidebar-segment">
69
- {state.currentBlockIndex + 1}/{state.totalBlocks}
70
- </div>
71
-
72
- {/* Caption mode cycle */}
73
- {state.hasCaptions && (
74
- <button
75
- className={`sidebar-ctrl-btn ${state.captionMode !== 'off' ? 'sidebar-ctrl-btn--active' : ''}`}
76
- onClick={() => actions.cycleCaptionMode()}
77
- title={
78
- state.captionMode === 'off'
79
- ? 'Captions: Off'
80
- : state.captionMode === 'standard'
81
- ? 'Captions: Standard'
82
- : 'Captions: Social'
83
- }
84
- aria-label="Cycle caption style"
85
- >
86
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
87
- <path d="M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" />
88
- </svg>
89
- </button>
90
- )}
91
-
92
- {/* Fullscreen toggle */}
93
- {actions.toggleFullscreen && (
94
- <button
95
- className={`sidebar-ctrl-btn ${state.isFullscreen ? 'sidebar-ctrl-btn--active' : ''}`}
96
- onClick={actions.toggleFullscreen}
97
- title={state.isFullscreen ? 'Exit fullscreen (F)' : 'Fullscreen (F)'}
98
- aria-label={state.isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
99
- >
100
- {state.isFullscreen ? (
101
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
102
- <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
103
- </svg>
104
- ) : (
105
- <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
106
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
107
- </svg>
108
- )}
109
- </button>
110
- )}
111
- </div>
112
- );
113
- }
@@ -1,360 +0,0 @@
1
- /**
2
- * DocControlsSlideshow Component
3
- *
4
- * Compact slide navigation controls for the DocPlayer slideshow display mode.
5
- * Renders in the lower-right corner with previous / slide counter / next buttons,
6
- * similar to PowerPoint/Keynote presentation navigation.
7
- *
8
- * Related Files:
9
- * - DocPlayer.tsx — Parent component (renders this when displayMode='slideshow')
10
- * - DocControlsOverlay.tsx — Video-mode controls (overlay layout)
11
- * - types.ts — Shared type definitions
12
- */
13
-
14
- import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
15
- import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
16
- import type { PlaybackState, SlideNavActions } from './types';
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
-
27
- interface DocControlsSlideshowProps {
28
- state: PlaybackState;
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;
36
- }
37
-
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();
59
- const {
60
- currentBlockIndex,
61
- currentSlideLabel,
62
- currentSlideNumber,
63
- totalBlocks,
64
- totalSlideNumber,
65
- } = state;
66
- const isFirst = currentBlockIndex <= 0;
67
- const isLast = currentBlockIndex >= totalBlocks - 1;
68
- const counterText =
69
- totalBlocks > 0
70
- ? (currentSlideLabel ??
71
- `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}`)
72
- : '—';
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
-
145
- return (
146
- <div
147
- ref={controlsRef}
148
- className="doc-controls-slideshow"
149
- data-testid="slideshow-controls"
150
- style={{
151
- position: 'absolute',
152
- bottom: '16px',
153
- right: '16px',
154
- display: 'flex',
155
- alignItems: 'center',
156
- gap: '2px',
157
- background: 'rgba(0, 0, 0, 0.65)',
158
- borderRadius: '8px',
159
- padding: '4px 6px',
160
- zIndex: 100,
161
- userSelect: 'none',
162
- backdropFilter: 'blur(8px)',
163
- WebkitBackdropFilter: 'blur(8px)',
164
- }}
165
- >
166
- {/* Previous button */}
167
- <button
168
- onClick={(e) => {
169
- e.stopPropagation();
170
- slideNav.prevSlide();
171
- }}
172
- disabled={isFirst}
173
- data-testid="slide-prev"
174
- aria-label="Previous slide"
175
- title="Previous slide"
176
- style={{
177
- background: 'none',
178
- border: 'none',
179
- color: isFirst ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.9)',
180
- cursor: isFirst ? 'default' : 'pointer',
181
- padding: '6px 8px',
182
- display: 'flex',
183
- alignItems: 'center',
184
- justifyContent: 'center',
185
- borderRadius: '4px',
186
- transition: 'background 0.15s',
187
- }}
188
- onMouseEnter={(e) => {
189
- if (!isFirst) e.currentTarget.style.background = 'rgba(255,255,255,0.1)';
190
- }}
191
- onMouseLeave={(e) => {
192
- e.currentTarget.style.background = 'none';
193
- }}
194
- >
195
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
196
- <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
197
- </svg>
198
- </button>
199
-
200
- {/* Slide counter / direct-navigation picker */}
201
- <button
202
- ref={triggerRef}
203
- type="button"
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
- }}
215
- style={{
216
- background: isPickerOpen ? 'rgba(255,255,255,0.12)' : 'none',
217
- border: 'none',
218
- color: 'rgba(255,255,255,0.9)',
219
- cursor: slides.length > 0 ? 'pointer' : 'default',
220
- fontSize: '13px',
221
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
222
- fontVariantNumeric: 'tabular-nums',
223
- minWidth: '48px',
224
- textAlign: 'center',
225
- padding: '6px 4px',
226
- letterSpacing: '0.02em',
227
- borderRadius: '4px',
228
- transition: 'background 0.15s',
229
- }}
230
- >
231
- {counterText}
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
- )}
324
-
325
- {/* Next button */}
326
- <button
327
- onClick={(e) => {
328
- e.stopPropagation();
329
- slideNav.nextSlide();
330
- }}
331
- disabled={isLast}
332
- data-testid="slide-next"
333
- aria-label="Next slide"
334
- title="Next slide"
335
- style={{
336
- background: 'none',
337
- border: 'none',
338
- color: isLast ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.9)',
339
- cursor: isLast ? 'default' : 'pointer',
340
- padding: '6px 8px',
341
- display: 'flex',
342
- alignItems: 'center',
343
- justifyContent: 'center',
344
- borderRadius: '4px',
345
- transition: 'background 0.15s',
346
- }}
347
- onMouseEnter={(e) => {
348
- if (!isLast) e.currentTarget.style.background = 'rgba(255,255,255,0.1)';
349
- }}
350
- onMouseLeave={(e) => {
351
- e.currentTarget.style.background = 'none';
352
- }}
353
- >
354
- <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
355
- <path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
356
- </svg>
357
- </button>
358
- </div>
359
- );
360
- }