@open-aippt/core 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- package/src/locale/zh-tw.ts +414 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
DndContext,
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
type DragStartEvent,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
useSensor,
|
|
9
|
+
useSensors,
|
|
10
|
+
} from '@dnd-kit/core';
|
|
11
|
+
import {
|
|
12
|
+
SortableContext,
|
|
13
|
+
sortableKeyboardCoordinates,
|
|
14
|
+
useSortable,
|
|
15
|
+
verticalListSortingStrategy,
|
|
16
|
+
} from '@dnd-kit/sortable';
|
|
17
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
18
|
+
import { Copy, Grid2x2, ListOrdered, type LucideIcon, Sparkles, Trash2 } from 'lucide-react';
|
|
19
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
ContextMenu,
|
|
22
|
+
ContextMenuContent,
|
|
23
|
+
ContextMenuItem,
|
|
24
|
+
ContextMenuSeparator,
|
|
25
|
+
ContextMenuTrigger,
|
|
26
|
+
} from '@/components/ui/context-menu';
|
|
27
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
28
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
29
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
30
|
+
import { cn } from '@/lib/utils';
|
|
31
|
+
import type { DesignSystem } from '../lib/design';
|
|
32
|
+
import { SlidePageProvider } from '../lib/page-context';
|
|
33
|
+
import type { Page } from '../lib/sdk';
|
|
34
|
+
import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
|
|
35
|
+
import type { SlideTransition } from '../lib/transition';
|
|
36
|
+
import { SlideCanvas } from './slide-canvas';
|
|
37
|
+
|
|
38
|
+
type Orientation = 'vertical' | 'horizontal';
|
|
39
|
+
|
|
40
|
+
export type ThumbnailActions = {
|
|
41
|
+
onDuplicate: (index: number) => void;
|
|
42
|
+
onDelete: (index: number) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type Props = {
|
|
46
|
+
pages: Page[];
|
|
47
|
+
design?: DesignSystem;
|
|
48
|
+
current: number;
|
|
49
|
+
onSelect: (index: number) => void;
|
|
50
|
+
onReorder?: (from: number, to: number) => void;
|
|
51
|
+
actions?: ThumbnailActions;
|
|
52
|
+
orientation?: Orientation;
|
|
53
|
+
/** Vertical-only: total rail width in px. Thumbnails scale to fit. */
|
|
54
|
+
width?: number;
|
|
55
|
+
/** Deck-level transition default; used to flag pages that inherit a transition. */
|
|
56
|
+
moduleTransition?: SlideTransition;
|
|
57
|
+
/** When provided, the vertical rail header renders a button that opens the overview grid. */
|
|
58
|
+
onOverview?: () => void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DEFAULT_VERTICAL_THUMB_WIDTH = 184;
|
|
62
|
+
const VERTICAL_RAIL_CHROME = 80;
|
|
63
|
+
const MIN_VERTICAL_THUMB_WIDTH = 120;
|
|
64
|
+
const HORIZONTAL_THUMB_HEIGHT = 64;
|
|
65
|
+
const HORIZONTAL_THUMB_GAP = 8;
|
|
66
|
+
const HORIZONTAL_RAIL_PADDING_X = 12;
|
|
67
|
+
const HORIZONTAL_RAIL_PADDING_Y = 10;
|
|
68
|
+
const HORIZONTAL_LABEL_HEIGHT = 12;
|
|
69
|
+
const HORIZONTAL_LABEL_GAP = 6;
|
|
70
|
+
const VERTICAL_THUMB_PADDING_Y = 12;
|
|
71
|
+
const VERTICAL_THUMB_GAP = 8;
|
|
72
|
+
const VIRTUAL_OVERSCAN = 4;
|
|
73
|
+
|
|
74
|
+
export function ThumbnailRail({
|
|
75
|
+
pages,
|
|
76
|
+
design,
|
|
77
|
+
current,
|
|
78
|
+
onSelect,
|
|
79
|
+
onReorder,
|
|
80
|
+
actions,
|
|
81
|
+
orientation = 'vertical',
|
|
82
|
+
width,
|
|
83
|
+
moduleTransition,
|
|
84
|
+
onOverview,
|
|
85
|
+
}: Props) {
|
|
86
|
+
const activeRef = useRef<HTMLButtonElement | null>(null);
|
|
87
|
+
const t = useLocale();
|
|
88
|
+
|
|
89
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `current` triggers re-scroll on selection change
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
92
|
+
|
|
93
|
+
activeRef.current?.scrollIntoView({
|
|
94
|
+
block: 'nearest',
|
|
95
|
+
inline: 'nearest',
|
|
96
|
+
behavior: reduceMotion ? 'auto' : 'smooth',
|
|
97
|
+
});
|
|
98
|
+
}, [current]);
|
|
99
|
+
|
|
100
|
+
const thumbWidth =
|
|
101
|
+
width != null
|
|
102
|
+
? Math.max(MIN_VERTICAL_THUMB_WIDTH, width - VERTICAL_RAIL_CHROME)
|
|
103
|
+
: DEFAULT_VERTICAL_THUMB_WIDTH;
|
|
104
|
+
const scale = thumbWidth / CANVAS_WIDTH;
|
|
105
|
+
const height = CANVAS_HEIGHT * scale;
|
|
106
|
+
const rowHeight = height + VERTICAL_THUMB_PADDING_Y + VERTICAL_THUMB_GAP;
|
|
107
|
+
|
|
108
|
+
const renderThumb = useCallback(
|
|
109
|
+
(PageComp: Page, i: number) => {
|
|
110
|
+
const active = i === current;
|
|
111
|
+
const inner = (
|
|
112
|
+
<ThumbContents
|
|
113
|
+
index={i}
|
|
114
|
+
total={pages.length}
|
|
115
|
+
active={active}
|
|
116
|
+
page={PageComp}
|
|
117
|
+
design={design}
|
|
118
|
+
scale={scale}
|
|
119
|
+
thumbWidth={thumbWidth}
|
|
120
|
+
height={height}
|
|
121
|
+
moduleTransition={moduleTransition}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const node = onReorder ? (
|
|
126
|
+
<SortableThumb
|
|
127
|
+
index={i}
|
|
128
|
+
active={active}
|
|
129
|
+
activeRef={active ? activeRef : undefined}
|
|
130
|
+
onSelect={() => onSelect(i)}
|
|
131
|
+
ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
132
|
+
>
|
|
133
|
+
{inner}
|
|
134
|
+
</SortableThumb>
|
|
135
|
+
) : (
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
ref={active ? activeRef : undefined}
|
|
139
|
+
onClick={() => onSelect(i)}
|
|
140
|
+
aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
141
|
+
aria-current={active ? 'true' : undefined}
|
|
142
|
+
className={thumbButtonClass(active)}
|
|
143
|
+
>
|
|
144
|
+
{inner}
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!actions) {
|
|
149
|
+
return <Fragment key={i}>{node}</Fragment>;
|
|
150
|
+
}
|
|
151
|
+
return (
|
|
152
|
+
<ThumbContextMenu
|
|
153
|
+
key={i}
|
|
154
|
+
index={i}
|
|
155
|
+
actions={actions}
|
|
156
|
+
pageCount={pages.length}
|
|
157
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
158
|
+
>
|
|
159
|
+
{node}
|
|
160
|
+
</ThumbContextMenu>
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
actions,
|
|
165
|
+
current,
|
|
166
|
+
design,
|
|
167
|
+
height,
|
|
168
|
+
moduleTransition,
|
|
169
|
+
onReorder,
|
|
170
|
+
onSelect,
|
|
171
|
+
pages.length,
|
|
172
|
+
scale,
|
|
173
|
+
thumbWidth,
|
|
174
|
+
t.thumbnailRail.goToPageAria,
|
|
175
|
+
t.thumbnailRail.pageActionsAria,
|
|
176
|
+
],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (orientation === 'horizontal') {
|
|
180
|
+
const scale = HORIZONTAL_THUMB_HEIGHT / CANVAS_HEIGHT;
|
|
181
|
+
const horizontalWidth = CANVAS_WIDTH * scale;
|
|
182
|
+
return (
|
|
183
|
+
<div className="bg-sidebar">
|
|
184
|
+
<div className="overflow-x-auto overflow-y-hidden">
|
|
185
|
+
<HorizontalVirtualThumbList
|
|
186
|
+
pages={pages}
|
|
187
|
+
design={design}
|
|
188
|
+
current={current}
|
|
189
|
+
actions={actions}
|
|
190
|
+
activeRef={activeRef}
|
|
191
|
+
onSelect={onSelect}
|
|
192
|
+
scale={scale}
|
|
193
|
+
thumbWidth={horizontalWidth}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const list = (
|
|
201
|
+
<aside className="flex flex-col gap-2 px-3 pb-3">
|
|
202
|
+
<div className="-mx-3 sticky top-0 z-10 bg-sidebar px-4 pt-3 pb-1">
|
|
203
|
+
<div className="flex items-center justify-between gap-2">
|
|
204
|
+
<span className="eyebrow">{t.thumbnailRail.pages}</span>
|
|
205
|
+
<div className="flex items-center gap-1.5">
|
|
206
|
+
<span className="folio">{pages.length.toString().padStart(2, '0')}</span>
|
|
207
|
+
{onOverview && (
|
|
208
|
+
<Tooltip>
|
|
209
|
+
<TooltipTrigger asChild>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={onOverview}
|
|
213
|
+
aria-label={t.thumbnailRail.overviewAria}
|
|
214
|
+
className={cn(
|
|
215
|
+
'flex size-5 items-center justify-center rounded-[3px] text-muted-foreground/70 outline-none',
|
|
216
|
+
'motion-safe:transition-colors hover:bg-muted hover:text-foreground',
|
|
217
|
+
'focus-visible:ring-1 focus-visible:ring-brand',
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
<Grid2x2 className="size-3.5" strokeWidth={1.75} />
|
|
221
|
+
</button>
|
|
222
|
+
</TooltipTrigger>
|
|
223
|
+
<TooltipContent side="bottom" sideOffset={6}>
|
|
224
|
+
{t.thumbnailRail.overviewAria}
|
|
225
|
+
</TooltipContent>
|
|
226
|
+
</Tooltip>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
<VirtualThumbList
|
|
232
|
+
pages={pages}
|
|
233
|
+
current={current}
|
|
234
|
+
rowHeight={rowHeight}
|
|
235
|
+
activeRef={activeRef}
|
|
236
|
+
renderThumb={renderThumb}
|
|
237
|
+
/>
|
|
238
|
+
</aside>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!onReorder) {
|
|
242
|
+
return (
|
|
243
|
+
<TooltipProvider delayDuration={200}>
|
|
244
|
+
<ScrollArea className="h-full border-r border-hairline bg-sidebar [&_[data-slot=scroll-area-scrollbar]]:z-20">
|
|
245
|
+
{list}
|
|
246
|
+
</ScrollArea>
|
|
247
|
+
</TooltipProvider>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<TooltipProvider delayDuration={200}>
|
|
253
|
+
<ScrollArea className="h-full border-r border-hairline bg-sidebar [&_[data-slot=scroll-area-scrollbar]]:z-20">
|
|
254
|
+
<SortableRail pages={pages} onReorder={onReorder} onSelect={onSelect}>
|
|
255
|
+
{list}
|
|
256
|
+
</SortableRail>
|
|
257
|
+
</ScrollArea>
|
|
258
|
+
</TooltipProvider>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function thumbButtonClass(active: boolean): string {
|
|
263
|
+
return cn(
|
|
264
|
+
'group/thumb flex w-full items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
|
|
265
|
+
'hover:bg-muted/60',
|
|
266
|
+
active && 'bg-muted',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function HorizontalVirtualThumbList({
|
|
271
|
+
pages,
|
|
272
|
+
design,
|
|
273
|
+
current,
|
|
274
|
+
actions,
|
|
275
|
+
activeRef,
|
|
276
|
+
onSelect,
|
|
277
|
+
scale,
|
|
278
|
+
thumbWidth,
|
|
279
|
+
}: {
|
|
280
|
+
pages: Page[];
|
|
281
|
+
design?: DesignSystem;
|
|
282
|
+
current: number;
|
|
283
|
+
actions?: ThumbnailActions;
|
|
284
|
+
activeRef: React.MutableRefObject<HTMLButtonElement | null>;
|
|
285
|
+
onSelect: (index: number) => void;
|
|
286
|
+
scale: number;
|
|
287
|
+
thumbWidth: number;
|
|
288
|
+
}) {
|
|
289
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
290
|
+
const viewportRef = useRef<HTMLElement | null>(null);
|
|
291
|
+
const t = useLocale();
|
|
292
|
+
const itemWidth = thumbWidth + HORIZONTAL_THUMB_GAP;
|
|
293
|
+
const listHeight =
|
|
294
|
+
HORIZONTAL_RAIL_PADDING_Y * 2 +
|
|
295
|
+
HORIZONTAL_LABEL_HEIGHT +
|
|
296
|
+
HORIZONTAL_LABEL_GAP +
|
|
297
|
+
HORIZONTAL_THUMB_HEIGHT;
|
|
298
|
+
const [range, setRange] = useState(() => getInitialVisibleRange(current, pages.length));
|
|
299
|
+
|
|
300
|
+
const updateRange = useCallback(() => {
|
|
301
|
+
const viewport = viewportRef.current;
|
|
302
|
+
if (!viewport) return;
|
|
303
|
+
const scrollLeft = Math.max(0, viewport.scrollLeft - HORIZONTAL_RAIL_PADDING_X);
|
|
304
|
+
setRange(getVisibleRange(scrollLeft, pages.length, viewport.clientWidth, itemWidth));
|
|
305
|
+
}, [itemWidth, pages.length]);
|
|
306
|
+
|
|
307
|
+
useLayoutEffect(() => {
|
|
308
|
+
const root = rootRef.current;
|
|
309
|
+
const viewport = root?.parentElement;
|
|
310
|
+
if (!viewport) return;
|
|
311
|
+
viewportRef.current = viewport;
|
|
312
|
+
|
|
313
|
+
let frame = 0;
|
|
314
|
+
const scheduleUpdate = () => {
|
|
315
|
+
cancelAnimationFrame(frame);
|
|
316
|
+
frame = requestAnimationFrame(updateRange);
|
|
317
|
+
};
|
|
318
|
+
const resizeObserver = new ResizeObserver(scheduleUpdate);
|
|
319
|
+
|
|
320
|
+
viewport.addEventListener('scroll', scheduleUpdate, { passive: true });
|
|
321
|
+
resizeObserver.observe(viewport);
|
|
322
|
+
scheduleUpdate();
|
|
323
|
+
|
|
324
|
+
return () => {
|
|
325
|
+
cancelAnimationFrame(frame);
|
|
326
|
+
viewport.removeEventListener('scroll', scheduleUpdate);
|
|
327
|
+
resizeObserver.disconnect();
|
|
328
|
+
if (viewportRef.current === viewport) viewportRef.current = null;
|
|
329
|
+
};
|
|
330
|
+
}, [updateRange]);
|
|
331
|
+
|
|
332
|
+
useLayoutEffect(() => {
|
|
333
|
+
if (pages.length <= 0) return;
|
|
334
|
+
const viewport = viewportRef.current ?? rootRef.current?.parentElement;
|
|
335
|
+
if (!viewport) return;
|
|
336
|
+
viewportRef.current = viewport;
|
|
337
|
+
|
|
338
|
+
const clampedCurrent = Math.min(Math.max(current, 0), pages.length - 1);
|
|
339
|
+
const left = HORIZONTAL_RAIL_PADDING_X + clampedCurrent * itemWidth;
|
|
340
|
+
const right = left + thumbWidth;
|
|
341
|
+
const viewportLeft = viewport.scrollLeft;
|
|
342
|
+
const viewportRight = viewportLeft + viewport.clientWidth;
|
|
343
|
+
|
|
344
|
+
if (left < viewportLeft) {
|
|
345
|
+
viewport.scrollLeft = left;
|
|
346
|
+
} else if (right > viewportRight) {
|
|
347
|
+
viewport.scrollLeft = right - viewport.clientWidth;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const scrollLeft = Math.max(0, viewport.scrollLeft - HORIZONTAL_RAIL_PADDING_X);
|
|
351
|
+
setRange(getVisibleRange(scrollLeft, pages.length, viewport.clientWidth, itemWidth));
|
|
352
|
+
}, [current, itemWidth, pages.length, thumbWidth]);
|
|
353
|
+
|
|
354
|
+
const visibleRange = clampVisibleRange(range, current, pages.length);
|
|
355
|
+
const visible = [];
|
|
356
|
+
for (let i = visibleRange.start; i < visibleRange.end; i++) {
|
|
357
|
+
const PageComp = pages[i];
|
|
358
|
+
const active = i === current;
|
|
359
|
+
const button = (
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
ref={active ? activeRef : undefined}
|
|
363
|
+
onClick={() => onSelect(i)}
|
|
364
|
+
aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
365
|
+
aria-current={active ? 'true' : undefined}
|
|
366
|
+
className={cn('group/thumb relative flex shrink-0 flex-col items-center gap-1.5')}
|
|
367
|
+
>
|
|
368
|
+
<span
|
|
369
|
+
className={cn(
|
|
370
|
+
'font-mono text-[9.5px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
371
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
372
|
+
)}
|
|
373
|
+
>
|
|
374
|
+
{(i + 1).toString().padStart(2, '0')}
|
|
375
|
+
</span>
|
|
376
|
+
<div
|
|
377
|
+
className={cn(
|
|
378
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
|
|
379
|
+
active
|
|
380
|
+
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
381
|
+
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
382
|
+
)}
|
|
383
|
+
style={{ width: thumbWidth, height: HORIZONTAL_THUMB_HEIGHT }}
|
|
384
|
+
>
|
|
385
|
+
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
386
|
+
<SlidePageProvider index={i} total={pages.length}>
|
|
387
|
+
<PageComp />
|
|
388
|
+
</SlidePageProvider>
|
|
389
|
+
</SlideCanvas>
|
|
390
|
+
</div>
|
|
391
|
+
</button>
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
visible.push(
|
|
395
|
+
<div
|
|
396
|
+
key={i}
|
|
397
|
+
className="absolute top-2.5"
|
|
398
|
+
style={{ left: HORIZONTAL_RAIL_PADDING_X + i * itemWidth, width: thumbWidth }}
|
|
399
|
+
>
|
|
400
|
+
{actions ? (
|
|
401
|
+
<ThumbContextMenu
|
|
402
|
+
index={i}
|
|
403
|
+
actions={actions}
|
|
404
|
+
pageCount={pages.length}
|
|
405
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
406
|
+
>
|
|
407
|
+
{button}
|
|
408
|
+
</ThumbContextMenu>
|
|
409
|
+
) : (
|
|
410
|
+
button
|
|
411
|
+
)}
|
|
412
|
+
</div>,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<div
|
|
418
|
+
ref={rootRef}
|
|
419
|
+
className="relative"
|
|
420
|
+
style={{
|
|
421
|
+
width: HORIZONTAL_RAIL_PADDING_X * 2 + pages.length * itemWidth - HORIZONTAL_THUMB_GAP,
|
|
422
|
+
height: listHeight,
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
{visible}
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function VirtualThumbList({
|
|
431
|
+
pages,
|
|
432
|
+
current,
|
|
433
|
+
rowHeight,
|
|
434
|
+
activeRef,
|
|
435
|
+
renderThumb,
|
|
436
|
+
}: {
|
|
437
|
+
pages: Page[];
|
|
438
|
+
current: number;
|
|
439
|
+
rowHeight: number;
|
|
440
|
+
activeRef: React.MutableRefObject<HTMLButtonElement | null>;
|
|
441
|
+
renderThumb: (page: Page, index: number) => React.ReactNode;
|
|
442
|
+
}) {
|
|
443
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
444
|
+
const viewportRef = useRef<HTMLElement | null>(null);
|
|
445
|
+
const [range, setRange] = useState(() => getInitialVisibleRange(current, pages.length));
|
|
446
|
+
|
|
447
|
+
const updateRange = useCallback(() => {
|
|
448
|
+
const viewport = viewportRef.current;
|
|
449
|
+
const root = rootRef.current;
|
|
450
|
+
if (!viewport || !root) return;
|
|
451
|
+
const scrollTop = Math.max(0, viewport.scrollTop - root.offsetTop);
|
|
452
|
+
setRange(getVisibleRange(scrollTop, pages.length, viewport.clientHeight, rowHeight));
|
|
453
|
+
}, [pages.length, rowHeight]);
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
const root = rootRef.current;
|
|
457
|
+
if (!root) return;
|
|
458
|
+
const viewport = root.closest('[data-slot="scroll-area-viewport"]') as HTMLElement | null;
|
|
459
|
+
if (!viewport) return;
|
|
460
|
+
viewportRef.current = viewport;
|
|
461
|
+
|
|
462
|
+
let frame = 0;
|
|
463
|
+
const scheduleUpdate = () => {
|
|
464
|
+
cancelAnimationFrame(frame);
|
|
465
|
+
frame = requestAnimationFrame(updateRange);
|
|
466
|
+
};
|
|
467
|
+
const resizeObserver = new ResizeObserver(scheduleUpdate);
|
|
468
|
+
|
|
469
|
+
viewport.addEventListener('scroll', scheduleUpdate, { passive: true });
|
|
470
|
+
resizeObserver.observe(viewport);
|
|
471
|
+
scheduleUpdate();
|
|
472
|
+
|
|
473
|
+
return () => {
|
|
474
|
+
cancelAnimationFrame(frame);
|
|
475
|
+
viewport.removeEventListener('scroll', scheduleUpdate);
|
|
476
|
+
resizeObserver.disconnect();
|
|
477
|
+
if (viewportRef.current === viewport) viewportRef.current = null;
|
|
478
|
+
};
|
|
479
|
+
}, [updateRange]);
|
|
480
|
+
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
const viewport = viewportRef.current;
|
|
483
|
+
const root = rootRef.current;
|
|
484
|
+
if (!viewport || !root) return;
|
|
485
|
+
|
|
486
|
+
const top = root.offsetTop + current * rowHeight;
|
|
487
|
+
const bottom = top + rowHeight;
|
|
488
|
+
const viewportTop = viewport.scrollTop;
|
|
489
|
+
const viewportBottom = viewportTop + viewport.clientHeight;
|
|
490
|
+
|
|
491
|
+
if (top < viewportTop) {
|
|
492
|
+
viewport.scrollTo({ top, behavior: scrollBehavior() });
|
|
493
|
+
} else if (bottom > viewportBottom) {
|
|
494
|
+
viewport.scrollTo({ top: bottom - viewport.clientHeight, behavior: scrollBehavior() });
|
|
495
|
+
} else {
|
|
496
|
+
activeRef.current?.scrollIntoView({
|
|
497
|
+
block: 'nearest',
|
|
498
|
+
inline: 'nearest',
|
|
499
|
+
behavior: scrollBehavior(),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}, [activeRef, current, rowHeight]);
|
|
503
|
+
|
|
504
|
+
const visibleRange = clampVisibleRange(range, current, pages.length);
|
|
505
|
+
const visible = [];
|
|
506
|
+
for (let i = visibleRange.start; i < visibleRange.end; i++) {
|
|
507
|
+
visible.push(
|
|
508
|
+
<div
|
|
509
|
+
key={i}
|
|
510
|
+
className="absolute right-0 left-0"
|
|
511
|
+
style={{ top: i * rowHeight, height: rowHeight }}
|
|
512
|
+
>
|
|
513
|
+
{renderThumb(pages[i], i)}
|
|
514
|
+
</div>,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<div ref={rootRef} className="relative" style={{ height: pages.length * rowHeight }}>
|
|
520
|
+
{visible}
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
type VisibleRange = { start: number; end: number };
|
|
526
|
+
|
|
527
|
+
function clampVisibleRange(range: VisibleRange, current: number, count: number): VisibleRange {
|
|
528
|
+
if (count <= 0) return { start: 0, end: 0 };
|
|
529
|
+
if (range.start >= 0 && range.start < count && range.start < range.end) {
|
|
530
|
+
return { start: range.start, end: Math.min(range.end, count) };
|
|
531
|
+
}
|
|
532
|
+
const clampedCurrent = Math.min(Math.max(current, 0), count - 1);
|
|
533
|
+
return getInitialVisibleRange(clampedCurrent, count);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getVisibleRange(
|
|
537
|
+
scrollTop: number,
|
|
538
|
+
count: number,
|
|
539
|
+
viewportHeight: number,
|
|
540
|
+
rowHeight: number,
|
|
541
|
+
): VisibleRange {
|
|
542
|
+
if (count <= 0) return { start: 0, end: 0 };
|
|
543
|
+
const visibleRows = Math.max(1, Math.ceil(viewportHeight / rowHeight));
|
|
544
|
+
const firstVisible = Math.min(count - 1, Math.max(0, Math.floor(scrollTop / rowHeight)));
|
|
545
|
+
const start = Math.max(0, firstVisible - VIRTUAL_OVERSCAN);
|
|
546
|
+
const end = Math.min(count, firstVisible + visibleRows + VIRTUAL_OVERSCAN + 1);
|
|
547
|
+
return { start, end };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function getInitialVisibleRange(current: number, count: number): VisibleRange {
|
|
551
|
+
if (count <= 0) return { start: 0, end: 0 };
|
|
552
|
+
const start = Math.max(0, current - VIRTUAL_OVERSCAN);
|
|
553
|
+
const end = Math.min(count, current + VIRTUAL_OVERSCAN + 1);
|
|
554
|
+
return { start, end };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function scrollBehavior(): ScrollBehavior {
|
|
558
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function ThumbContents({
|
|
562
|
+
index,
|
|
563
|
+
total,
|
|
564
|
+
active,
|
|
565
|
+
page: PageComp,
|
|
566
|
+
design,
|
|
567
|
+
scale,
|
|
568
|
+
thumbWidth,
|
|
569
|
+
height,
|
|
570
|
+
moduleTransition,
|
|
571
|
+
}: {
|
|
572
|
+
index: number;
|
|
573
|
+
total: number;
|
|
574
|
+
active: boolean;
|
|
575
|
+
page: Page;
|
|
576
|
+
design?: DesignSystem;
|
|
577
|
+
scale: number;
|
|
578
|
+
thumbWidth: number;
|
|
579
|
+
height: number;
|
|
580
|
+
moduleTransition?: SlideTransition;
|
|
581
|
+
}) {
|
|
582
|
+
const t = useLocale();
|
|
583
|
+
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
584
|
+
const [hasSteps, setHasSteps] = useState(false);
|
|
585
|
+
|
|
586
|
+
// Steps live in JSX and can't be introspected statically — detect them from
|
|
587
|
+
// the already-rendered thumbnail DOM, where each Step emits `data-osd-step`.
|
|
588
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-detect when the page at this slot changes (reorder/edit reuses the index)
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
setHasSteps(boxRef.current?.querySelector('[data-osd-step]') != null);
|
|
591
|
+
}, [PageComp]);
|
|
592
|
+
|
|
593
|
+
const hasTransition = Boolean(PageComp.transition ?? moduleTransition);
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<>
|
|
597
|
+
<div className="mt-1.5 flex w-7 shrink-0 flex-col items-end gap-1">
|
|
598
|
+
<span
|
|
599
|
+
className={cn(
|
|
600
|
+
'font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
601
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
602
|
+
)}
|
|
603
|
+
>
|
|
604
|
+
{(index + 1).toString().padStart(2, '0')}
|
|
605
|
+
</span>
|
|
606
|
+
{(hasTransition || hasSteps) && (
|
|
607
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
608
|
+
{hasTransition && (
|
|
609
|
+
<ThumbIndicator icon={Sparkles} label={t.thumbnailRail.transitionIndicator} />
|
|
610
|
+
)}
|
|
611
|
+
{hasSteps && (
|
|
612
|
+
<ThumbIndicator icon={ListOrdered} label={t.thumbnailRail.stepsIndicator} />
|
|
613
|
+
)}
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
616
|
+
</div>
|
|
617
|
+
<div
|
|
618
|
+
ref={boxRef}
|
|
619
|
+
className={cn(
|
|
620
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
|
|
621
|
+
active
|
|
622
|
+
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
623
|
+
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
624
|
+
)}
|
|
625
|
+
style={{ width: thumbWidth, height }}
|
|
626
|
+
>
|
|
627
|
+
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
628
|
+
<SlidePageProvider index={index} total={total}>
|
|
629
|
+
<PageComp />
|
|
630
|
+
</SlidePageProvider>
|
|
631
|
+
</SlideCanvas>
|
|
632
|
+
{active && (
|
|
633
|
+
<span
|
|
634
|
+
aria-hidden
|
|
635
|
+
className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
|
|
636
|
+
/>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
</>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function ThumbIndicator({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
|
|
644
|
+
return (
|
|
645
|
+
<Tooltip>
|
|
646
|
+
<TooltipTrigger asChild>
|
|
647
|
+
<span
|
|
648
|
+
role="img"
|
|
649
|
+
aria-label={label}
|
|
650
|
+
className={cn(
|
|
651
|
+
'flex size-3.5 items-center justify-center text-muted-foreground/55',
|
|
652
|
+
'motion-safe:transition-colors group-hover/thumb:text-muted-foreground/80',
|
|
653
|
+
)}
|
|
654
|
+
>
|
|
655
|
+
<Icon className="size-3" strokeWidth={2} />
|
|
656
|
+
</span>
|
|
657
|
+
</TooltipTrigger>
|
|
658
|
+
<TooltipContent side="right" sideOffset={6}>
|
|
659
|
+
{label}
|
|
660
|
+
</TooltipContent>
|
|
661
|
+
</Tooltip>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function ThumbContextMenu({
|
|
666
|
+
index,
|
|
667
|
+
actions,
|
|
668
|
+
pageCount,
|
|
669
|
+
ariaLabel,
|
|
670
|
+
children,
|
|
671
|
+
}: {
|
|
672
|
+
index: number;
|
|
673
|
+
actions: ThumbnailActions;
|
|
674
|
+
pageCount: number;
|
|
675
|
+
ariaLabel: string;
|
|
676
|
+
children: React.ReactNode;
|
|
677
|
+
}) {
|
|
678
|
+
const t = useLocale();
|
|
679
|
+
const canDelete = pageCount > 1;
|
|
680
|
+
return (
|
|
681
|
+
<ContextMenu>
|
|
682
|
+
<ContextMenuTrigger asChild aria-label={ariaLabel}>
|
|
683
|
+
{children}
|
|
684
|
+
</ContextMenuTrigger>
|
|
685
|
+
<ContextMenuContent className="min-w-[180px]">
|
|
686
|
+
<ContextMenuItem onSelect={() => actions.onDuplicate(index)}>
|
|
687
|
+
<Copy />
|
|
688
|
+
{t.thumbnailRail.duplicatePage}
|
|
689
|
+
</ContextMenuItem>
|
|
690
|
+
<ContextMenuSeparator />
|
|
691
|
+
<ContextMenuItem
|
|
692
|
+
variant="destructive"
|
|
693
|
+
disabled={!canDelete}
|
|
694
|
+
onSelect={() => {
|
|
695
|
+
if (canDelete) actions.onDelete(index);
|
|
696
|
+
}}
|
|
697
|
+
>
|
|
698
|
+
<Trash2 />
|
|
699
|
+
{t.thumbnailRail.deletePage}
|
|
700
|
+
</ContextMenuItem>
|
|
701
|
+
</ContextMenuContent>
|
|
702
|
+
</ContextMenu>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function SortableRail({
|
|
707
|
+
pages,
|
|
708
|
+
onReorder,
|
|
709
|
+
onSelect,
|
|
710
|
+
children,
|
|
711
|
+
}: {
|
|
712
|
+
pages: Page[];
|
|
713
|
+
onReorder: (from: number, to: number) => void;
|
|
714
|
+
onSelect: (index: number) => void;
|
|
715
|
+
children: React.ReactNode;
|
|
716
|
+
}) {
|
|
717
|
+
const sensors = useSensors(
|
|
718
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
719
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const items = pages.map((_, i) => i + 1);
|
|
723
|
+
|
|
724
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
725
|
+
const i = (event.active.id as number) - 1;
|
|
726
|
+
if (i >= 0) onSelect(i);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
730
|
+
const { active, over } = event;
|
|
731
|
+
if (!over || active.id === over.id) return;
|
|
732
|
+
const from = (active.id as number) - 1;
|
|
733
|
+
const to = (over.id as number) - 1;
|
|
734
|
+
if (from < 0 || to < 0 || from === to) return;
|
|
735
|
+
onReorder(from, to);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<DndContext
|
|
740
|
+
sensors={sensors}
|
|
741
|
+
collisionDetection={closestCenter}
|
|
742
|
+
onDragStart={handleDragStart}
|
|
743
|
+
onDragEnd={handleDragEnd}
|
|
744
|
+
>
|
|
745
|
+
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
|
746
|
+
{children}
|
|
747
|
+
</SortableContext>
|
|
748
|
+
</DndContext>
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function SortableThumb({
|
|
753
|
+
index,
|
|
754
|
+
active,
|
|
755
|
+
activeRef,
|
|
756
|
+
onSelect,
|
|
757
|
+
ariaLabel,
|
|
758
|
+
children,
|
|
759
|
+
...rest
|
|
760
|
+
}: {
|
|
761
|
+
index: number;
|
|
762
|
+
active: boolean;
|
|
763
|
+
activeRef: React.MutableRefObject<HTMLButtonElement | null> | undefined;
|
|
764
|
+
onSelect: () => void;
|
|
765
|
+
ariaLabel: string;
|
|
766
|
+
children: React.ReactNode;
|
|
767
|
+
} & Omit<
|
|
768
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
769
|
+
'onClick' | 'aria-label' | 'aria-current' | 'type' | 'style' | 'className' | 'ref' | 'children'
|
|
770
|
+
>) {
|
|
771
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
772
|
+
id: index + 1,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const setRef = (node: HTMLButtonElement | null) => {
|
|
776
|
+
setNodeRef(node);
|
|
777
|
+
if (activeRef) activeRef.current = node;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const yOnlyTransform = transform ? { ...transform, x: 0 } : transform;
|
|
781
|
+
|
|
782
|
+
return (
|
|
783
|
+
<button
|
|
784
|
+
{...rest}
|
|
785
|
+
ref={setRef}
|
|
786
|
+
type="button"
|
|
787
|
+
onClick={onSelect}
|
|
788
|
+
aria-label={ariaLabel}
|
|
789
|
+
aria-current={active ? 'true' : undefined}
|
|
790
|
+
style={{
|
|
791
|
+
transform: CSS.Transform.toString(yOnlyTransform),
|
|
792
|
+
transition,
|
|
793
|
+
touchAction: 'none',
|
|
794
|
+
}}
|
|
795
|
+
className={cn(
|
|
796
|
+
thumbButtonClass(active),
|
|
797
|
+
isDragging && 'z-10 cursor-grabbing opacity-60 shadow-edge ring-1 ring-brand',
|
|
798
|
+
)}
|
|
799
|
+
{...attributes}
|
|
800
|
+
{...listeners}
|
|
801
|
+
>
|
|
802
|
+
{children}
|
|
803
|
+
</button>
|
|
804
|
+
);
|
|
805
|
+
}
|