@open-slide/core 1.1.0 → 1.3.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/{build-DSqSio-T.js → build-_276DMmJ.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
- package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
- package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
- package/dist/en-CDKzoZvf.js +351 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +229 -39
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +166 -326
- package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +9 -1
- package/skills/create-slide/SKILL.md +1 -1
- package/skills/create-theme/SKILL.md +60 -12
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +59 -1
- package/src/app/app.tsx +11 -1
- package/src/app/components/asset-view.tsx +1 -13
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
- package/src/app/components/inspector/inspector-panel.tsx +163 -19
- package/src/app/components/inspector/inspector-provider.tsx +60 -7
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +11 -7
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/sidebar/folder-item.tsx +16 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/components/themes/theme-detail.tsx +300 -0
- package/src/app/components/themes/themes-gallery.tsx +146 -0
- package/src/app/components/thumbnail-rail.tsx +136 -29
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +10 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/routes/home-shell.tsx +173 -0
- package/src/app/routes/home.tsx +108 -204
- package/src/app/routes/slide.tsx +333 -68
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +61 -7
- package/src/locale/ja.ts +62 -7
- package/src/locale/types.ts +62 -5
- package/src/locale/zh-cn.ts +61 -7
- package/src/locale/zh-tw.ts +61 -7
- package/dist/sync-B4eLo2H6.js +0 -3
- /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
- /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
3
|
+
import { loadThemeDemo, type Theme, type ThemeDemoModule, themes } from '../../lib/themes';
|
|
4
|
+
import { SlideCanvas } from '../slide-canvas';
|
|
5
|
+
|
|
6
|
+
export function ThemesGallery({ onOpen }: { onOpen: (id: string) => void }) {
|
|
7
|
+
const t = useLocale();
|
|
8
|
+
|
|
9
|
+
if (themes.length === 0) {
|
|
10
|
+
return <ThemesEmptyState />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<ul className="grid grid-cols-[repeat(auto-fill,minmax(min(240px,100%),1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(340px,1fr))]">
|
|
15
|
+
{themes.map((theme) => (
|
|
16
|
+
<li key={theme.id}>
|
|
17
|
+
<ThemeCard
|
|
18
|
+
theme={theme}
|
|
19
|
+
onOpen={() => onOpen(theme.id)}
|
|
20
|
+
ariaLabel={format(t.themes.openThemeAria, { name: theme.name })}
|
|
21
|
+
/>
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ThemeCard({
|
|
29
|
+
theme,
|
|
30
|
+
onOpen,
|
|
31
|
+
ariaLabel,
|
|
32
|
+
}: {
|
|
33
|
+
theme: Theme;
|
|
34
|
+
onOpen: () => void;
|
|
35
|
+
ariaLabel: string;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={onOpen}
|
|
41
|
+
aria-label={ariaLabel}
|
|
42
|
+
className="group block w-full text-left focus-visible:outline-none"
|
|
43
|
+
>
|
|
44
|
+
<div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 motion-safe:transition-[box-shadow,--tw-ring-color] motion-safe:duration-200">
|
|
45
|
+
<ThemePreview theme={theme} />
|
|
46
|
+
</div>
|
|
47
|
+
<div className="mt-3">
|
|
48
|
+
<h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
|
|
49
|
+
{theme.name}
|
|
50
|
+
</h3>
|
|
51
|
+
</div>
|
|
52
|
+
{theme.description ? (
|
|
53
|
+
<p className="mt-1 line-clamp-2 text-[12px] leading-snug text-muted-foreground">
|
|
54
|
+
{theme.description}
|
|
55
|
+
</p>
|
|
56
|
+
) : null}
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ThemePreview({ theme }: { theme: Theme }) {
|
|
62
|
+
const t = useLocale();
|
|
63
|
+
const demo = useThemeDemo(theme);
|
|
64
|
+
|
|
65
|
+
if (!theme.hasDemo) {
|
|
66
|
+
return <NoDemoState />;
|
|
67
|
+
}
|
|
68
|
+
if (!demo) {
|
|
69
|
+
return (
|
|
70
|
+
<div className="grid h-full w-full place-items-center text-[10px] tracking-[0.16em] uppercase text-muted-foreground/60">
|
|
71
|
+
{t.common.loading}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const FirstPage = demo.default[0];
|
|
76
|
+
if (!FirstPage) return <NoDemoState />;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
|
|
80
|
+
<SlideCanvas flat freezeMotion design={demo.design}>
|
|
81
|
+
<FirstPage />
|
|
82
|
+
</SlideCanvas>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function NoDemoState() {
|
|
88
|
+
const t = useLocale();
|
|
89
|
+
return (
|
|
90
|
+
<div className="grid h-full w-full place-items-center bg-muted/40 px-6 text-center">
|
|
91
|
+
<div>
|
|
92
|
+
<p className="font-heading text-[12px] font-semibold tracking-tight text-foreground/80">
|
|
93
|
+
{t.themes.noDemoYet}
|
|
94
|
+
</p>
|
|
95
|
+
<p className="mt-1 text-[10.5px] leading-snug text-muted-foreground">
|
|
96
|
+
{t.themes.noDemoHintPrefix}
|
|
97
|
+
<code className="rounded-[3px] bg-card px-1 py-0.5 font-mono text-[10px] text-foreground">
|
|
98
|
+
/create-theme
|
|
99
|
+
</code>
|
|
100
|
+
{t.themes.noDemoHintSuffix}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ThemesEmptyState() {
|
|
108
|
+
const t = useLocale();
|
|
109
|
+
return (
|
|
110
|
+
<div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
|
|
111
|
+
<div className="mx-auto flex max-w-md flex-col items-center text-center">
|
|
112
|
+
<div className="text-2xl">🎨</div>
|
|
113
|
+
<p className="mt-3 font-heading text-[15px] font-semibold tracking-tight">
|
|
114
|
+
{t.themes.noThemesTitle}
|
|
115
|
+
</p>
|
|
116
|
+
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
|
|
117
|
+
{t.themes.noThemesHintPrefix}
|
|
118
|
+
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
119
|
+
/create-theme
|
|
120
|
+
</code>
|
|
121
|
+
{t.themes.noThemesHintSuffix}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function useThemeDemo(theme: Theme): ThemeDemoModule | null {
|
|
129
|
+
const [demo, setDemo] = useState<ThemeDemoModule | null>(null);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!theme.hasDemo) {
|
|
132
|
+
setDemo(null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let cancelled = false;
|
|
136
|
+
loadThemeDemo(theme.id)
|
|
137
|
+
.then((mod) => {
|
|
138
|
+
if (!cancelled) setDemo(mod);
|
|
139
|
+
})
|
|
140
|
+
.catch(() => {});
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
144
|
+
}, [theme.id, theme.hasDemo]);
|
|
145
|
+
return demo;
|
|
146
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
closestCenter,
|
|
3
3
|
DndContext,
|
|
4
4
|
type DragEndEvent,
|
|
5
|
+
type DragStartEvent,
|
|
5
6
|
KeyboardSensor,
|
|
6
7
|
PointerSensor,
|
|
7
8
|
useSensor,
|
|
@@ -14,7 +15,15 @@ import {
|
|
|
14
15
|
verticalListSortingStrategy,
|
|
15
16
|
} from '@dnd-kit/sortable';
|
|
16
17
|
import { CSS } from '@dnd-kit/utilities';
|
|
17
|
-
import {
|
|
18
|
+
import { Copy, Trash2 } from 'lucide-react';
|
|
19
|
+
import { Fragment, useEffect, useRef } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
ContextMenu,
|
|
22
|
+
ContextMenuContent,
|
|
23
|
+
ContextMenuItem,
|
|
24
|
+
ContextMenuSeparator,
|
|
25
|
+
ContextMenuTrigger,
|
|
26
|
+
} from '@/components/ui/context-menu';
|
|
18
27
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
19
28
|
import { format, useLocale } from '@/lib/use-locale';
|
|
20
29
|
import { cn } from '@/lib/utils';
|
|
@@ -25,16 +34,26 @@ import { SlideCanvas } from './slide-canvas';
|
|
|
25
34
|
|
|
26
35
|
type Orientation = 'vertical' | 'horizontal';
|
|
27
36
|
|
|
37
|
+
export type ThumbnailActions = {
|
|
38
|
+
onDuplicate: (index: number) => void;
|
|
39
|
+
onDelete: (index: number) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
28
42
|
type Props = {
|
|
29
43
|
pages: Page[];
|
|
30
44
|
design?: DesignSystem;
|
|
31
45
|
current: number;
|
|
32
46
|
onSelect: (index: number) => void;
|
|
33
47
|
onReorder?: (from: number, to: number) => void;
|
|
48
|
+
actions?: ThumbnailActions;
|
|
34
49
|
orientation?: Orientation;
|
|
50
|
+
/** Vertical-only: total rail width in px. Thumbnails scale to fit. */
|
|
51
|
+
width?: number;
|
|
35
52
|
};
|
|
36
53
|
|
|
37
|
-
const
|
|
54
|
+
const DEFAULT_VERTICAL_THUMB_WIDTH = 184;
|
|
55
|
+
const VERTICAL_RAIL_CHROME = 80;
|
|
56
|
+
const MIN_VERTICAL_THUMB_WIDTH = 120;
|
|
38
57
|
const HORIZONTAL_THUMB_HEIGHT = 64;
|
|
39
58
|
|
|
40
59
|
export function ThumbnailRail({
|
|
@@ -43,7 +62,9 @@ export function ThumbnailRail({
|
|
|
43
62
|
current,
|
|
44
63
|
onSelect,
|
|
45
64
|
onReorder,
|
|
65
|
+
actions,
|
|
46
66
|
orientation = 'vertical',
|
|
67
|
+
width,
|
|
47
68
|
}: Props) {
|
|
48
69
|
const activeRef = useRef<HTMLButtonElement | null>(null);
|
|
49
70
|
const t = useLocale();
|
|
@@ -68,7 +89,7 @@ export function ThumbnailRail({
|
|
|
68
89
|
<div className="flex items-center gap-2 px-3 py-2.5">
|
|
69
90
|
{pages.map((PageComp, i) => {
|
|
70
91
|
const active = i === current;
|
|
71
|
-
|
|
92
|
+
const button = (
|
|
72
93
|
<button
|
|
73
94
|
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
74
95
|
key={i}
|
|
@@ -89,7 +110,7 @@ export function ThumbnailRail({
|
|
|
89
110
|
</span>
|
|
90
111
|
<div
|
|
91
112
|
className={cn(
|
|
92
|
-
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-
|
|
113
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
|
|
93
114
|
active
|
|
94
115
|
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
95
116
|
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
@@ -102,6 +123,19 @@ export function ThumbnailRail({
|
|
|
102
123
|
</div>
|
|
103
124
|
</button>
|
|
104
125
|
);
|
|
126
|
+
if (!actions) return button;
|
|
127
|
+
return (
|
|
128
|
+
<ThumbContextMenu
|
|
129
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
130
|
+
key={i}
|
|
131
|
+
index={i}
|
|
132
|
+
actions={actions}
|
|
133
|
+
pageCount={pages.length}
|
|
134
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
135
|
+
>
|
|
136
|
+
{button}
|
|
137
|
+
</ThumbContextMenu>
|
|
138
|
+
);
|
|
105
139
|
})}
|
|
106
140
|
</div>
|
|
107
141
|
</div>
|
|
@@ -109,7 +143,11 @@ export function ThumbnailRail({
|
|
|
109
143
|
);
|
|
110
144
|
}
|
|
111
145
|
|
|
112
|
-
const
|
|
146
|
+
const thumbWidth =
|
|
147
|
+
width != null
|
|
148
|
+
? Math.max(MIN_VERTICAL_THUMB_WIDTH, width - VERTICAL_RAIL_CHROME)
|
|
149
|
+
: DEFAULT_VERTICAL_THUMB_WIDTH;
|
|
150
|
+
const scale = thumbWidth / CANVAS_WIDTH;
|
|
113
151
|
const height = CANVAS_HEIGHT * scale;
|
|
114
152
|
|
|
115
153
|
const renderThumb = (PageComp: Page, i: number) => {
|
|
@@ -121,28 +159,23 @@ export function ThumbnailRail({
|
|
|
121
159
|
page={PageComp}
|
|
122
160
|
design={design}
|
|
123
161
|
scale={scale}
|
|
162
|
+
thumbWidth={thumbWidth}
|
|
124
163
|
height={height}
|
|
125
164
|
/>
|
|
126
165
|
);
|
|
127
166
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</SortableThumb>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return (
|
|
167
|
+
const node = onReorder ? (
|
|
168
|
+
<SortableThumb
|
|
169
|
+
index={i}
|
|
170
|
+
active={active}
|
|
171
|
+
activeRef={active ? activeRef : undefined}
|
|
172
|
+
onSelect={() => onSelect(i)}
|
|
173
|
+
ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
174
|
+
>
|
|
175
|
+
{inner}
|
|
176
|
+
</SortableThumb>
|
|
177
|
+
) : (
|
|
144
178
|
<button
|
|
145
|
-
key={i}
|
|
146
179
|
type="button"
|
|
147
180
|
ref={active ? activeRef : undefined}
|
|
148
181
|
onClick={() => onSelect(i)}
|
|
@@ -153,6 +186,21 @@ export function ThumbnailRail({
|
|
|
153
186
|
{inner}
|
|
154
187
|
</button>
|
|
155
188
|
);
|
|
189
|
+
|
|
190
|
+
if (!actions) {
|
|
191
|
+
return <Fragment key={i}>{node}</Fragment>;
|
|
192
|
+
}
|
|
193
|
+
return (
|
|
194
|
+
<ThumbContextMenu
|
|
195
|
+
key={i}
|
|
196
|
+
index={i}
|
|
197
|
+
actions={actions}
|
|
198
|
+
pageCount={pages.length}
|
|
199
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
200
|
+
>
|
|
201
|
+
{node}
|
|
202
|
+
</ThumbContextMenu>
|
|
203
|
+
);
|
|
156
204
|
};
|
|
157
205
|
|
|
158
206
|
const list = (
|
|
@@ -171,7 +219,7 @@ export function ThumbnailRail({
|
|
|
171
219
|
|
|
172
220
|
return (
|
|
173
221
|
<ScrollArea className="h-full border-r border-hairline bg-sidebar">
|
|
174
|
-
<SortableRail pages={pages} onReorder={onReorder}>
|
|
222
|
+
<SortableRail pages={pages} onReorder={onReorder} onSelect={onSelect}>
|
|
175
223
|
{list}
|
|
176
224
|
</SortableRail>
|
|
177
225
|
</ScrollArea>
|
|
@@ -192,6 +240,7 @@ function ThumbContents({
|
|
|
192
240
|
page: PageComp,
|
|
193
241
|
design,
|
|
194
242
|
scale,
|
|
243
|
+
thumbWidth,
|
|
195
244
|
height,
|
|
196
245
|
}: {
|
|
197
246
|
index: number;
|
|
@@ -199,6 +248,7 @@ function ThumbContents({
|
|
|
199
248
|
page: Page;
|
|
200
249
|
design?: DesignSystem;
|
|
201
250
|
scale: number;
|
|
251
|
+
thumbWidth: number;
|
|
202
252
|
height: number;
|
|
203
253
|
}) {
|
|
204
254
|
return (
|
|
@@ -213,12 +263,12 @@ function ThumbContents({
|
|
|
213
263
|
</span>
|
|
214
264
|
<div
|
|
215
265
|
className={cn(
|
|
216
|
-
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-
|
|
266
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
|
|
217
267
|
active
|
|
218
268
|
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
219
269
|
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
220
270
|
)}
|
|
221
|
-
style={{ width:
|
|
271
|
+
style={{ width: thumbWidth, height }}
|
|
222
272
|
>
|
|
223
273
|
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
224
274
|
<PageComp />
|
|
@@ -234,13 +284,56 @@ function ThumbContents({
|
|
|
234
284
|
);
|
|
235
285
|
}
|
|
236
286
|
|
|
287
|
+
function ThumbContextMenu({
|
|
288
|
+
index,
|
|
289
|
+
actions,
|
|
290
|
+
pageCount,
|
|
291
|
+
ariaLabel,
|
|
292
|
+
children,
|
|
293
|
+
}: {
|
|
294
|
+
index: number;
|
|
295
|
+
actions: ThumbnailActions;
|
|
296
|
+
pageCount: number;
|
|
297
|
+
ariaLabel: string;
|
|
298
|
+
children: React.ReactNode;
|
|
299
|
+
}) {
|
|
300
|
+
const t = useLocale();
|
|
301
|
+
const canDelete = pageCount > 1;
|
|
302
|
+
return (
|
|
303
|
+
<ContextMenu>
|
|
304
|
+
<ContextMenuTrigger asChild aria-label={ariaLabel}>
|
|
305
|
+
{children}
|
|
306
|
+
</ContextMenuTrigger>
|
|
307
|
+
<ContextMenuContent className="min-w-[180px]">
|
|
308
|
+
<ContextMenuItem onSelect={() => actions.onDuplicate(index)}>
|
|
309
|
+
<Copy />
|
|
310
|
+
{t.thumbnailRail.duplicatePage}
|
|
311
|
+
</ContextMenuItem>
|
|
312
|
+
<ContextMenuSeparator />
|
|
313
|
+
<ContextMenuItem
|
|
314
|
+
variant="destructive"
|
|
315
|
+
disabled={!canDelete}
|
|
316
|
+
onSelect={() => {
|
|
317
|
+
if (canDelete) actions.onDelete(index);
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
<Trash2 />
|
|
321
|
+
{t.thumbnailRail.deletePage}
|
|
322
|
+
</ContextMenuItem>
|
|
323
|
+
</ContextMenuContent>
|
|
324
|
+
</ContextMenu>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
237
328
|
function SortableRail({
|
|
238
329
|
pages,
|
|
239
330
|
onReorder,
|
|
331
|
+
onSelect,
|
|
240
332
|
children,
|
|
241
333
|
}: {
|
|
242
334
|
pages: Page[];
|
|
243
335
|
onReorder: (from: number, to: number) => void;
|
|
336
|
+
onSelect: (index: number) => void;
|
|
244
337
|
children: React.ReactNode;
|
|
245
338
|
}) {
|
|
246
339
|
const sensors = useSensors(
|
|
@@ -250,6 +343,11 @@ function SortableRail({
|
|
|
250
343
|
|
|
251
344
|
const items = pages.map((_, i) => i + 1);
|
|
252
345
|
|
|
346
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
347
|
+
const i = (event.active.id as number) - 1;
|
|
348
|
+
if (i >= 0) onSelect(i);
|
|
349
|
+
};
|
|
350
|
+
|
|
253
351
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
254
352
|
const { active, over } = event;
|
|
255
353
|
if (!over || active.id === over.id) return;
|
|
@@ -260,7 +358,12 @@ function SortableRail({
|
|
|
260
358
|
};
|
|
261
359
|
|
|
262
360
|
return (
|
|
263
|
-
<DndContext
|
|
361
|
+
<DndContext
|
|
362
|
+
sensors={sensors}
|
|
363
|
+
collisionDetection={closestCenter}
|
|
364
|
+
onDragStart={handleDragStart}
|
|
365
|
+
onDragEnd={handleDragEnd}
|
|
366
|
+
>
|
|
264
367
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
|
265
368
|
{children}
|
|
266
369
|
</SortableContext>
|
|
@@ -275,6 +378,7 @@ function SortableThumb({
|
|
|
275
378
|
onSelect,
|
|
276
379
|
ariaLabel,
|
|
277
380
|
children,
|
|
381
|
+
...rest
|
|
278
382
|
}: {
|
|
279
383
|
index: number;
|
|
280
384
|
active: boolean;
|
|
@@ -282,7 +386,10 @@ function SortableThumb({
|
|
|
282
386
|
onSelect: () => void;
|
|
283
387
|
ariaLabel: string;
|
|
284
388
|
children: React.ReactNode;
|
|
285
|
-
}
|
|
389
|
+
} & Omit<
|
|
390
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
391
|
+
'onClick' | 'aria-label' | 'aria-current' | 'type' | 'style' | 'className' | 'ref' | 'children'
|
|
392
|
+
>) {
|
|
286
393
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
287
394
|
id: index + 1,
|
|
288
395
|
});
|
|
@@ -296,6 +403,7 @@ function SortableThumb({
|
|
|
296
403
|
|
|
297
404
|
return (
|
|
298
405
|
<button
|
|
406
|
+
{...rest}
|
|
299
407
|
ref={setRef}
|
|
300
408
|
type="button"
|
|
301
409
|
onClick={onSelect}
|
|
@@ -308,8 +416,7 @@ function SortableThumb({
|
|
|
308
416
|
}}
|
|
309
417
|
className={cn(
|
|
310
418
|
thumbButtonClass(active),
|
|
311
|
-
'
|
|
312
|
-
isDragging && 'z-10 opacity-60 shadow-edge ring-1 ring-brand',
|
|
419
|
+
isDragging && 'z-10 cursor-grabbing opacity-60 shadow-edge ring-1 ring-brand',
|
|
313
420
|
)}
|
|
314
421
|
{...attributes}
|
|
315
422
|
{...listeners}
|