@open-slide/core 1.0.6 → 1.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.
- package/dist/{build-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
- package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
- package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +136 -24
- package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
- package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +5 -1
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +48 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +17 -2
- package/src/app/components/inspector/inspector-panel.tsx +90 -26
- package/src/app/components/inspector/inspector-provider.tsx +136 -1
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +26 -8
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/present/use-idle.ts +6 -4
- package/src/app/components/style-panel/design-provider.tsx +13 -0
- package/src/app/components/style-panel/style-panel.tsx +23 -11
- package/src/app/components/thumbnail-rail.tsx +317 -55
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/routes/home.tsx +34 -12
- package/src/app/routes/presenter.tsx +27 -24
- package/src/app/routes/slide.tsx +238 -51
- package/src/locale/en.ts +35 -4
- package/src/locale/ja.ts +35 -4
- package/src/locale/types.ts +38 -4
- package/src/locale/zh-cn.ts +35 -4
- package/src/locale/zh-tw.ts +35 -4
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
import {
|
|
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, 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';
|
|
2
27
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
3
28
|
import { format, useLocale } from '@/lib/use-locale';
|
|
4
29
|
import { cn } from '@/lib/utils';
|
|
@@ -9,11 +34,18 @@ import { SlideCanvas } from './slide-canvas';
|
|
|
9
34
|
|
|
10
35
|
type Orientation = 'vertical' | 'horizontal';
|
|
11
36
|
|
|
37
|
+
export type ThumbnailActions = {
|
|
38
|
+
onDuplicate: (index: number) => void;
|
|
39
|
+
onDelete: (index: number) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
12
42
|
type Props = {
|
|
13
43
|
pages: Page[];
|
|
14
44
|
design?: DesignSystem;
|
|
15
45
|
current: number;
|
|
16
46
|
onSelect: (index: number) => void;
|
|
47
|
+
onReorder?: (from: number, to: number) => void;
|
|
48
|
+
actions?: ThumbnailActions;
|
|
17
49
|
orientation?: Orientation;
|
|
18
50
|
};
|
|
19
51
|
|
|
@@ -25,6 +57,8 @@ export function ThumbnailRail({
|
|
|
25
57
|
design,
|
|
26
58
|
current,
|
|
27
59
|
onSelect,
|
|
60
|
+
onReorder,
|
|
61
|
+
actions,
|
|
28
62
|
orientation = 'vertical',
|
|
29
63
|
}: Props) {
|
|
30
64
|
const activeRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -50,7 +84,7 @@ export function ThumbnailRail({
|
|
|
50
84
|
<div className="flex items-center gap-2 px-3 py-2.5">
|
|
51
85
|
{pages.map((PageComp, i) => {
|
|
52
86
|
const active = i === current;
|
|
53
|
-
|
|
87
|
+
const button = (
|
|
54
88
|
<button
|
|
55
89
|
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
56
90
|
key={i}
|
|
@@ -84,6 +118,19 @@ export function ThumbnailRail({
|
|
|
84
118
|
</div>
|
|
85
119
|
</button>
|
|
86
120
|
);
|
|
121
|
+
if (!actions) return button;
|
|
122
|
+
return (
|
|
123
|
+
<ThumbContextMenu
|
|
124
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
125
|
+
key={i}
|
|
126
|
+
index={i}
|
|
127
|
+
actions={actions}
|
|
128
|
+
pageCount={pages.length}
|
|
129
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
130
|
+
>
|
|
131
|
+
{button}
|
|
132
|
+
</ThumbContextMenu>
|
|
133
|
+
);
|
|
87
134
|
})}
|
|
88
135
|
</div>
|
|
89
136
|
</div>
|
|
@@ -93,61 +140,276 @@ export function ThumbnailRail({
|
|
|
93
140
|
|
|
94
141
|
const scale = VERTICAL_THUMB_WIDTH / CANVAS_WIDTH;
|
|
95
142
|
const height = CANVAS_HEIGHT * scale;
|
|
143
|
+
|
|
144
|
+
const renderThumb = (PageComp: Page, i: number) => {
|
|
145
|
+
const active = i === current;
|
|
146
|
+
const inner = (
|
|
147
|
+
<ThumbContents
|
|
148
|
+
index={i}
|
|
149
|
+
active={active}
|
|
150
|
+
page={PageComp}
|
|
151
|
+
design={design}
|
|
152
|
+
scale={scale}
|
|
153
|
+
height={height}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const node = onReorder ? (
|
|
158
|
+
<SortableThumb
|
|
159
|
+
index={i}
|
|
160
|
+
active={active}
|
|
161
|
+
activeRef={active ? activeRef : undefined}
|
|
162
|
+
onSelect={() => onSelect(i)}
|
|
163
|
+
ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
164
|
+
>
|
|
165
|
+
{inner}
|
|
166
|
+
</SortableThumb>
|
|
167
|
+
) : (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
ref={active ? activeRef : undefined}
|
|
171
|
+
onClick={() => onSelect(i)}
|
|
172
|
+
aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
173
|
+
aria-current={active ? 'true' : undefined}
|
|
174
|
+
className={thumbButtonClass(active)}
|
|
175
|
+
>
|
|
176
|
+
{inner}
|
|
177
|
+
</button>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!actions) {
|
|
181
|
+
return <Fragment key={i}>{node}</Fragment>;
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<ThumbContextMenu
|
|
185
|
+
key={i}
|
|
186
|
+
index={i}
|
|
187
|
+
actions={actions}
|
|
188
|
+
pageCount={pages.length}
|
|
189
|
+
ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
|
|
190
|
+
>
|
|
191
|
+
{node}
|
|
192
|
+
</ThumbContextMenu>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const list = (
|
|
197
|
+
<aside className="flex flex-col gap-2 px-3 py-3">
|
|
198
|
+
<div className="flex items-baseline justify-between px-1 pb-1">
|
|
199
|
+
<span className="eyebrow">{t.thumbnailRail.pages}</span>
|
|
200
|
+
<span className="folio">{pages.length.toString().padStart(2, '0')}</span>
|
|
201
|
+
</div>
|
|
202
|
+
{pages.map(renderThumb)}
|
|
203
|
+
</aside>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!onReorder) {
|
|
207
|
+
return <ScrollArea className="h-full border-r border-hairline bg-sidebar">{list}</ScrollArea>;
|
|
208
|
+
}
|
|
209
|
+
|
|
96
210
|
return (
|
|
97
211
|
<ScrollArea className="h-full border-r border-hairline bg-sidebar">
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<span className="folio">{pages.length.toString().padStart(2, '0')}</span>
|
|
102
|
-
</div>
|
|
103
|
-
{pages.map((PageComp, i) => {
|
|
104
|
-
const active = i === current;
|
|
105
|
-
return (
|
|
106
|
-
<button
|
|
107
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
108
|
-
key={i}
|
|
109
|
-
type="button"
|
|
110
|
-
ref={active ? activeRef : undefined}
|
|
111
|
-
onClick={() => onSelect(i)}
|
|
112
|
-
aria-label={`Go to page ${i + 1}`}
|
|
113
|
-
aria-current={active ? 'true' : undefined}
|
|
114
|
-
className={cn(
|
|
115
|
-
'group/thumb flex items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
|
|
116
|
-
'hover:bg-muted/60',
|
|
117
|
-
active && 'bg-muted',
|
|
118
|
-
)}
|
|
119
|
-
>
|
|
120
|
-
<span
|
|
121
|
-
className={cn(
|
|
122
|
-
'mt-1.5 w-7 shrink-0 text-right font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
123
|
-
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
124
|
-
)}
|
|
125
|
-
>
|
|
126
|
-
{(i + 1).toString().padStart(2, '0')}
|
|
127
|
-
</span>
|
|
128
|
-
<div
|
|
129
|
-
className={cn(
|
|
130
|
-
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
|
|
131
|
-
active
|
|
132
|
-
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
133
|
-
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
134
|
-
)}
|
|
135
|
-
style={{ width: VERTICAL_THUMB_WIDTH, height }}
|
|
136
|
-
>
|
|
137
|
-
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
138
|
-
<PageComp />
|
|
139
|
-
</SlideCanvas>
|
|
140
|
-
{active && (
|
|
141
|
-
<span
|
|
142
|
-
aria-hidden
|
|
143
|
-
className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
|
|
144
|
-
/>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
</button>
|
|
148
|
-
);
|
|
149
|
-
})}
|
|
150
|
-
</aside>
|
|
212
|
+
<SortableRail pages={pages} onReorder={onReorder} onSelect={onSelect}>
|
|
213
|
+
{list}
|
|
214
|
+
</SortableRail>
|
|
151
215
|
</ScrollArea>
|
|
152
216
|
);
|
|
153
217
|
}
|
|
218
|
+
|
|
219
|
+
function thumbButtonClass(active: boolean): string {
|
|
220
|
+
return cn(
|
|
221
|
+
'group/thumb flex w-full items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
|
|
222
|
+
'hover:bg-muted/60',
|
|
223
|
+
active && 'bg-muted',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ThumbContents({
|
|
228
|
+
index,
|
|
229
|
+
active,
|
|
230
|
+
page: PageComp,
|
|
231
|
+
design,
|
|
232
|
+
scale,
|
|
233
|
+
height,
|
|
234
|
+
}: {
|
|
235
|
+
index: number;
|
|
236
|
+
active: boolean;
|
|
237
|
+
page: Page;
|
|
238
|
+
design?: DesignSystem;
|
|
239
|
+
scale: number;
|
|
240
|
+
height: number;
|
|
241
|
+
}) {
|
|
242
|
+
return (
|
|
243
|
+
<>
|
|
244
|
+
<span
|
|
245
|
+
className={cn(
|
|
246
|
+
'mt-1.5 w-7 shrink-0 text-right font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
247
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
{(index + 1).toString().padStart(2, '0')}
|
|
251
|
+
</span>
|
|
252
|
+
<div
|
|
253
|
+
className={cn(
|
|
254
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
|
|
255
|
+
active
|
|
256
|
+
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
257
|
+
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
258
|
+
)}
|
|
259
|
+
style={{ width: VERTICAL_THUMB_WIDTH, height }}
|
|
260
|
+
>
|
|
261
|
+
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
262
|
+
<PageComp />
|
|
263
|
+
</SlideCanvas>
|
|
264
|
+
{active && (
|
|
265
|
+
<span
|
|
266
|
+
aria-hidden
|
|
267
|
+
className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ThumbContextMenu({
|
|
276
|
+
index,
|
|
277
|
+
actions,
|
|
278
|
+
pageCount,
|
|
279
|
+
ariaLabel,
|
|
280
|
+
children,
|
|
281
|
+
}: {
|
|
282
|
+
index: number;
|
|
283
|
+
actions: ThumbnailActions;
|
|
284
|
+
pageCount: number;
|
|
285
|
+
ariaLabel: string;
|
|
286
|
+
children: React.ReactNode;
|
|
287
|
+
}) {
|
|
288
|
+
const t = useLocale();
|
|
289
|
+
const canDelete = pageCount > 1;
|
|
290
|
+
return (
|
|
291
|
+
<ContextMenu>
|
|
292
|
+
<ContextMenuTrigger asChild aria-label={ariaLabel}>
|
|
293
|
+
{children}
|
|
294
|
+
</ContextMenuTrigger>
|
|
295
|
+
<ContextMenuContent className="min-w-[180px]">
|
|
296
|
+
<ContextMenuItem onSelect={() => actions.onDuplicate(index)}>
|
|
297
|
+
<Copy />
|
|
298
|
+
{t.thumbnailRail.duplicatePage}
|
|
299
|
+
</ContextMenuItem>
|
|
300
|
+
<ContextMenuSeparator />
|
|
301
|
+
<ContextMenuItem
|
|
302
|
+
variant="destructive"
|
|
303
|
+
disabled={!canDelete}
|
|
304
|
+
onSelect={() => {
|
|
305
|
+
if (canDelete) actions.onDelete(index);
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
<Trash2 />
|
|
309
|
+
{t.thumbnailRail.deletePage}
|
|
310
|
+
</ContextMenuItem>
|
|
311
|
+
</ContextMenuContent>
|
|
312
|
+
</ContextMenu>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function SortableRail({
|
|
317
|
+
pages,
|
|
318
|
+
onReorder,
|
|
319
|
+
onSelect,
|
|
320
|
+
children,
|
|
321
|
+
}: {
|
|
322
|
+
pages: Page[];
|
|
323
|
+
onReorder: (from: number, to: number) => void;
|
|
324
|
+
onSelect: (index: number) => void;
|
|
325
|
+
children: React.ReactNode;
|
|
326
|
+
}) {
|
|
327
|
+
const sensors = useSensors(
|
|
328
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
329
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const items = pages.map((_, i) => i + 1);
|
|
333
|
+
|
|
334
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
335
|
+
const i = (event.active.id as number) - 1;
|
|
336
|
+
if (i >= 0) onSelect(i);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
340
|
+
const { active, over } = event;
|
|
341
|
+
if (!over || active.id === over.id) return;
|
|
342
|
+
const from = (active.id as number) - 1;
|
|
343
|
+
const to = (over.id as number) - 1;
|
|
344
|
+
if (from < 0 || to < 0 || from === to) return;
|
|
345
|
+
onReorder(from, to);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<DndContext
|
|
350
|
+
sensors={sensors}
|
|
351
|
+
collisionDetection={closestCenter}
|
|
352
|
+
onDragStart={handleDragStart}
|
|
353
|
+
onDragEnd={handleDragEnd}
|
|
354
|
+
>
|
|
355
|
+
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
|
356
|
+
{children}
|
|
357
|
+
</SortableContext>
|
|
358
|
+
</DndContext>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function SortableThumb({
|
|
363
|
+
index,
|
|
364
|
+
active,
|
|
365
|
+
activeRef,
|
|
366
|
+
onSelect,
|
|
367
|
+
ariaLabel,
|
|
368
|
+
children,
|
|
369
|
+
...rest
|
|
370
|
+
}: {
|
|
371
|
+
index: number;
|
|
372
|
+
active: boolean;
|
|
373
|
+
activeRef: React.MutableRefObject<HTMLButtonElement | null> | undefined;
|
|
374
|
+
onSelect: () => void;
|
|
375
|
+
ariaLabel: string;
|
|
376
|
+
children: React.ReactNode;
|
|
377
|
+
} & Omit<
|
|
378
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
379
|
+
'onClick' | 'aria-label' | 'aria-current' | 'type' | 'style' | 'className' | 'ref' | 'children'
|
|
380
|
+
>) {
|
|
381
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
382
|
+
id: index + 1,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const setRef = (node: HTMLButtonElement | null) => {
|
|
386
|
+
setNodeRef(node);
|
|
387
|
+
if (activeRef) activeRef.current = node;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const yOnlyTransform = transform ? { ...transform, x: 0 } : transform;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<button
|
|
394
|
+
{...rest}
|
|
395
|
+
ref={setRef}
|
|
396
|
+
type="button"
|
|
397
|
+
onClick={onSelect}
|
|
398
|
+
aria-label={ariaLabel}
|
|
399
|
+
aria-current={active ? 'true' : undefined}
|
|
400
|
+
style={{
|
|
401
|
+
transform: CSS.Transform.toString(yOnlyTransform),
|
|
402
|
+
transition,
|
|
403
|
+
touchAction: 'none',
|
|
404
|
+
}}
|
|
405
|
+
className={cn(
|
|
406
|
+
thumbButtonClass(active),
|
|
407
|
+
isDragging && 'z-10 cursor-grabbing opacity-60 shadow-edge ring-1 ring-brand',
|
|
408
|
+
)}
|
|
409
|
+
{...attributes}
|
|
410
|
+
{...listeners}
|
|
411
|
+
>
|
|
412
|
+
{children}
|
|
413
|
+
</button>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
|
5
|
+
import { ContextMenu as ContextMenuPrimitive } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
|
10
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ContextMenuTrigger({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
|
16
|
+
return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
|
20
|
+
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
24
|
+
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
28
|
+
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ContextMenuRadioGroup({
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
34
|
+
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ContextMenuSubTrigger({
|
|
38
|
+
className,
|
|
39
|
+
inset,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
43
|
+
inset?: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<ContextMenuPrimitive.SubTrigger
|
|
47
|
+
data-slot="context-menu-sub-trigger"
|
|
48
|
+
data-inset={inset}
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex cursor-default items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[inset]:pl-8 data-[state=open]:bg-muted',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
<ChevronRightIcon className="ml-auto size-3.5 opacity-60" />
|
|
57
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ContextMenuSubContent({
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
65
|
+
return (
|
|
66
|
+
<ContextMenuPrimitive.SubContent
|
|
67
|
+
data-slot="context-menu-sub-content"
|
|
68
|
+
className={cn(
|
|
69
|
+
'z-50 min-w-[9rem] overflow-hidden rounded-[8px] border border-border bg-popover p-1 text-popover-foreground shadow-floating',
|
|
70
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
71
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ContextMenuContent({
|
|
80
|
+
className,
|
|
81
|
+
...props
|
|
82
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
|
83
|
+
return (
|
|
84
|
+
<ContextMenuPrimitive.Portal>
|
|
85
|
+
<ContextMenuPrimitive.Content
|
|
86
|
+
data-slot="context-menu-content"
|
|
87
|
+
className={cn(
|
|
88
|
+
'z-50 max-h-(--radix-context-menu-content-available-height) min-w-[9rem] origin-(--radix-context-menu-content-transform-origin)',
|
|
89
|
+
'overflow-x-hidden overflow-y-auto rounded-[8px] border border-border bg-popover p-1 text-popover-foreground shadow-floating',
|
|
90
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
91
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
</ContextMenuPrimitive.Portal>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ContextMenuItem({
|
|
101
|
+
className,
|
|
102
|
+
inset,
|
|
103
|
+
variant = 'default',
|
|
104
|
+
...props
|
|
105
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
|
106
|
+
inset?: boolean;
|
|
107
|
+
variant?: 'default' | 'destructive';
|
|
108
|
+
}) {
|
|
109
|
+
return (
|
|
110
|
+
<ContextMenuPrimitive.Item
|
|
111
|
+
data-slot="context-menu-item"
|
|
112
|
+
data-inset={inset}
|
|
113
|
+
data-variant={variant}
|
|
114
|
+
className={cn(
|
|
115
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-hidden select-none transition-colors',
|
|
116
|
+
'focus:bg-foreground focus:text-background',
|
|
117
|
+
'data-[active=true]:bg-muted data-[active=true]:text-foreground',
|
|
118
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[inset]:pl-8',
|
|
119
|
+
'data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive data-[variant=destructive]:focus:text-white',
|
|
120
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-current [&_svg]:opacity-80",
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ContextMenuCheckboxItem({
|
|
129
|
+
className,
|
|
130
|
+
children,
|
|
131
|
+
checked,
|
|
132
|
+
...props
|
|
133
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
|
134
|
+
return (
|
|
135
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
136
|
+
data-slot="context-menu-checkbox-item"
|
|
137
|
+
className={cn(
|
|
138
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] py-1.5 pr-2 pl-8 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-45',
|
|
139
|
+
className,
|
|
140
|
+
)}
|
|
141
|
+
checked={checked}
|
|
142
|
+
{...props}
|
|
143
|
+
>
|
|
144
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
145
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
146
|
+
<CheckIcon className="size-3.5" />
|
|
147
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
148
|
+
</span>
|
|
149
|
+
{children}
|
|
150
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ContextMenuRadioItem({
|
|
155
|
+
className,
|
|
156
|
+
children,
|
|
157
|
+
...props
|
|
158
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
|
159
|
+
return (
|
|
160
|
+
<ContextMenuPrimitive.RadioItem
|
|
161
|
+
data-slot="context-menu-radio-item"
|
|
162
|
+
className={cn(
|
|
163
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] py-1.5 pr-2 pl-8 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-45',
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
169
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
170
|
+
<CircleIcon className="size-2 fill-current" />
|
|
171
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
172
|
+
</span>
|
|
173
|
+
{children}
|
|
174
|
+
</ContextMenuPrimitive.RadioItem>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ContextMenuLabel({
|
|
179
|
+
className,
|
|
180
|
+
inset,
|
|
181
|
+
...props
|
|
182
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
|
183
|
+
inset?: boolean;
|
|
184
|
+
}) {
|
|
185
|
+
return (
|
|
186
|
+
<ContextMenuPrimitive.Label
|
|
187
|
+
data-slot="context-menu-label"
|
|
188
|
+
data-inset={inset}
|
|
189
|
+
className={cn('eyebrow px-2 py-1.5 data-[inset]:pl-8', className)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ContextMenuSeparator({
|
|
196
|
+
className,
|
|
197
|
+
...props
|
|
198
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
|
199
|
+
return (
|
|
200
|
+
<ContextMenuPrimitive.Separator
|
|
201
|
+
data-slot="context-menu-separator"
|
|
202
|
+
className={cn('-mx-1 my-1 h-px bg-hairline', className)}
|
|
203
|
+
{...props}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
|
209
|
+
return (
|
|
210
|
+
<span
|
|
211
|
+
data-slot="context-menu-shortcut"
|
|
212
|
+
className={cn(
|
|
213
|
+
'ml-auto font-mono text-[10.5px] tracking-[0.06em] text-muted-foreground/80',
|
|
214
|
+
className,
|
|
215
|
+
)}
|
|
216
|
+
{...props}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
ContextMenu,
|
|
223
|
+
ContextMenuTrigger,
|
|
224
|
+
ContextMenuContent,
|
|
225
|
+
ContextMenuItem,
|
|
226
|
+
ContextMenuCheckboxItem,
|
|
227
|
+
ContextMenuRadioItem,
|
|
228
|
+
ContextMenuLabel,
|
|
229
|
+
ContextMenuSeparator,
|
|
230
|
+
ContextMenuShortcut,
|
|
231
|
+
ContextMenuGroup,
|
|
232
|
+
ContextMenuPortal,
|
|
233
|
+
ContextMenuSub,
|
|
234
|
+
ContextMenuSubContent,
|
|
235
|
+
ContextMenuSubTrigger,
|
|
236
|
+
ContextMenuRadioGroup,
|
|
237
|
+
};
|