@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.
Files changed (37) hide show
  1. package/dist/{build-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
  4. package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
  5. package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/locale/index.d.ts +1 -1
  8. package/dist/locale/index.js +136 -24
  9. package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
  10. package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
  11. package/dist/vite/index.d.ts +2 -2
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +5 -1
  14. package/skills/current-slide/SKILL.md +110 -0
  15. package/skills/slide-authoring/SKILL.md +48 -1
  16. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  17. package/src/app/components/inspector/inspect-overlay.tsx +17 -2
  18. package/src/app/components/inspector/inspector-panel.tsx +90 -26
  19. package/src/app/components/inspector/inspector-provider.tsx +136 -1
  20. package/src/app/components/notes-drawer.tsx +117 -0
  21. package/src/app/components/player.tsx +26 -8
  22. package/src/app/components/present/overview-grid.tsx +2 -2
  23. package/src/app/components/present/use-idle.ts +6 -4
  24. package/src/app/components/style-panel/design-provider.tsx +13 -0
  25. package/src/app/components/style-panel/style-panel.tsx +23 -11
  26. package/src/app/components/thumbnail-rail.tsx +317 -55
  27. package/src/app/components/ui/context-menu.tsx +237 -0
  28. package/src/app/lib/design-presets.ts +94 -0
  29. package/src/app/lib/inspector/use-notes.ts +134 -0
  30. package/src/app/routes/home.tsx +34 -12
  31. package/src/app/routes/presenter.tsx +27 -24
  32. package/src/app/routes/slide.tsx +238 -51
  33. package/src/locale/en.ts +35 -4
  34. package/src/locale/ja.ts +35 -4
  35. package/src/locale/types.ts +38 -4
  36. package/src/locale/zh-cn.ts +35 -4
  37. package/src/locale/zh-tw.ts +35 -4
@@ -1,4 +1,29 @@
1
- import { useEffect, useRef } from 'react';
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
- return (
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
- <aside className="flex flex-col gap-2 px-3 py-3">
99
- <div className="flex items-baseline justify-between px-1 pb-1">
100
- <span className="eyebrow">{t.thumbnailRail.pages}</span>
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
+ };