@bycrux/editor 0.11.2 → 1.0.1
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/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { Check, Instagram, Music2, Slash, Youtube, type LucideIcon } from 'lucide-react'
|
|
4
|
+
import type { SocialPreviewPlatform } from './SocialSafeZoneOverlay'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Platform picker for the realistic social-media preview chrome — mirrors
|
|
8
|
+
* CapCut's "Preview your video for social media" menu. Opened from the
|
|
9
|
+
* preview controls-row button (see VideoEditor.tsx); lists TikTok, YouTube
|
|
10
|
+
* Shorts and Instagram Reels, plus a "None" entry that clears the selection.
|
|
11
|
+
*
|
|
12
|
+
* Positioning is the same "render once off-screen, measure, place" two-pass
|
|
13
|
+
* shape `TrackSettingsPopover.tsx` uses (portaled to `document.body` for the
|
|
14
|
+
* same reason: this trigger sits inside `previewRegion`'s `overflow-hidden`
|
|
15
|
+
* box, which would clip an `absolute`-positioned popover the moment it grew
|
|
16
|
+
* past the row's own bounds). It DEFAULTS to opening upward rather than
|
|
17
|
+
* `TrackSettingsPopover`'s downward-by-default: the trigger lives in the
|
|
18
|
+
* BOTTOM preview controls row, so there's rarely room below it, and this is
|
|
19
|
+
* the same "icon variant opens upward" call `ImageToneMenu.tsx` makes for its
|
|
20
|
+
* own bottom-toolbar placement.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const OFFSET_PX = 6
|
|
24
|
+
const VIEWPORT_MARGIN_PX = 8
|
|
25
|
+
|
|
26
|
+
interface Position {
|
|
27
|
+
left: number
|
|
28
|
+
top: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SocialPreviewMenuProps {
|
|
32
|
+
/** The controls-row button this popover is anchored to and positioned against. */
|
|
33
|
+
anchorRef: RefObject<HTMLButtonElement | null>
|
|
34
|
+
/** Currently selected platform. `null` = "None" is the active entry. */
|
|
35
|
+
value: SocialPreviewPlatform | null
|
|
36
|
+
/** Persist a new selection (or clear it, via `null`). */
|
|
37
|
+
onChange: (platform: SocialPreviewPlatform | null) => void
|
|
38
|
+
onClose: () => void
|
|
39
|
+
/** Editor theme mode — light/dark. This popover portals onto
|
|
40
|
+
* `--editor-surface`, NOT over the black video canvas like the rest of
|
|
41
|
+
* `preview/`, so its accents have to read on a light ground: the selected
|
|
42
|
+
* checkmark's sky-400 is ~2.9:1 on white. Defaults to `'dark'`, so a caller
|
|
43
|
+
* that omits it renders exactly as before. */
|
|
44
|
+
mode?: 'light' | 'dark'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A platform's small identifying mark: a nominative "this is the TikTok/
|
|
49
|
+
* YouTube/Instagram row" glyph, NOT a pixel-exact reproduction of the
|
|
50
|
+
* official trademarked logo. `Youtube`/`Instagram` are lucide's own
|
|
51
|
+
* simplified brand glyphs (already generic outline icons, not hand-traced
|
|
52
|
+
* copies of the real marks); TikTok has no such lucide glyph, so it gets a
|
|
53
|
+
* generic music-note icon instead, which is enough to read as "TikTok" next
|
|
54
|
+
* to the plain-text label without drawing the note/logo itself.
|
|
55
|
+
*/
|
|
56
|
+
export interface PlatformOption {
|
|
57
|
+
id: SocialPreviewPlatform
|
|
58
|
+
label: string
|
|
59
|
+
icon: LucideIcon
|
|
60
|
+
/** Tailwind background for the small badge behind the glyph — a solid
|
|
61
|
+
* color for YouTube's red, a two-stop gradient standing in for TikTok's
|
|
62
|
+
* cyan/pink and Instagram's purple/pink duotone marks. */
|
|
63
|
+
badgeClassName: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const PLATFORM_OPTIONS: PlatformOption[] = [
|
|
67
|
+
{ id: 'tiktok', label: 'TikTok', icon: Music2, badgeClassName: 'bg-gradient-to-br from-cyan-400 to-pink-500' },
|
|
68
|
+
{ id: 'youtube', label: 'YouTube Shorts', icon: Youtube, badgeClassName: 'bg-red-600' },
|
|
69
|
+
{ id: 'instagram', label: 'Instagram Reels', icon: Instagram, badgeClassName: 'bg-gradient-to-br from-purple-500 to-pink-500' },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
/** `PLATFORM_OPTIONS` entry for a platform, or `null` for "None"/unset — the
|
|
73
|
+
* lookup VideoEditor's trigger button uses to show the active selection's
|
|
74
|
+
* glyph (falling back to its own Smartphone icon on `null`). */
|
|
75
|
+
export function platformOption(platform: SocialPreviewPlatform | null): PlatformOption | null {
|
|
76
|
+
return platform ? PLATFORM_OPTIONS.find(o => o.id === platform) ?? null : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The glyph itself — a small colored badge, decorative only. The
|
|
80
|
+
* accessible name is always the platform's plain-text label (on the menu
|
|
81
|
+
* row) or the trigger's own `aria-label` — never this glyph, hence
|
|
82
|
+
* `aria-hidden`. */
|
|
83
|
+
export function PlatformGlyph({ icon: Icon, badgeClassName, size = 20 }: { icon: LucideIcon; badgeClassName: string; size?: number }) {
|
|
84
|
+
return (
|
|
85
|
+
<span
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
className={`flex items-center justify-center rounded-md shrink-0 ${badgeClassName}`}
|
|
88
|
+
style={{ width: size, height: size }}
|
|
89
|
+
>
|
|
90
|
+
<Icon size={Math.round(size * 0.65)} strokeWidth={2.25} className="text-white" />
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default function SocialPreviewMenu({ anchorRef, value, onChange, onClose, mode = 'dark' }: SocialPreviewMenuProps) {
|
|
96
|
+
const popoverRef = useRef<HTMLDivElement>(null)
|
|
97
|
+
const [position, setPosition] = useState<Position | null>(null)
|
|
98
|
+
|
|
99
|
+
// Measure once the popover has a real size, then place it. Anchored from
|
|
100
|
+
// the trigger's right edge (the trigger sits at the right end of the
|
|
101
|
+
// controls row, alongside Zoom-to-fit/Fullscreen) so the menu's right edge
|
|
102
|
+
// lines up with the button rather than running off the right edge of the
|
|
103
|
+
// window. Opens upward by default (see the file-level doc comment); falls
|
|
104
|
+
// back to downward only when there truly isn't room above.
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
const anchorRect = anchorRef.current?.getBoundingClientRect()
|
|
107
|
+
const popover = popoverRef.current
|
|
108
|
+
if (!anchorRect || !popover) return
|
|
109
|
+
const { width, height } = popover.getBoundingClientRect()
|
|
110
|
+
|
|
111
|
+
let left = anchorRect.right - width
|
|
112
|
+
if (left < VIEWPORT_MARGIN_PX) left = VIEWPORT_MARGIN_PX
|
|
113
|
+
if (left + width > window.innerWidth - VIEWPORT_MARGIN_PX) {
|
|
114
|
+
left = window.innerWidth - VIEWPORT_MARGIN_PX - width
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const fitsAbove = anchorRect.top - OFFSET_PX - height >= VIEWPORT_MARGIN_PX
|
|
118
|
+
const top = fitsAbove ? anchorRect.top - OFFSET_PX - height : anchorRect.bottom + OFFSET_PX
|
|
119
|
+
setPosition({ left, top })
|
|
120
|
+
}, [anchorRef])
|
|
121
|
+
|
|
122
|
+
// Close on outside click / Escape — same shape as ImageToneMenu.tsx's,
|
|
123
|
+
// extended to exclude the portaled popover itself (which isn't a DOM
|
|
124
|
+
// descendant of the trigger once portaled to document.body).
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const onDown = (e: MouseEvent) => {
|
|
127
|
+
const target = e.target as Node
|
|
128
|
+
if (anchorRef.current?.contains(target)) return
|
|
129
|
+
if (popoverRef.current?.contains(target)) return
|
|
130
|
+
onClose()
|
|
131
|
+
}
|
|
132
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
|
133
|
+
document.addEventListener('mousedown', onDown)
|
|
134
|
+
document.addEventListener('keydown', onKey)
|
|
135
|
+
return () => {
|
|
136
|
+
document.removeEventListener('mousedown', onDown)
|
|
137
|
+
document.removeEventListener('keydown', onKey)
|
|
138
|
+
}
|
|
139
|
+
}, [anchorRef, onClose])
|
|
140
|
+
|
|
141
|
+
if (typeof document === 'undefined') return null
|
|
142
|
+
|
|
143
|
+
return createPortal(
|
|
144
|
+
<div
|
|
145
|
+
ref={popoverRef}
|
|
146
|
+
role="menu"
|
|
147
|
+
aria-label="Preview for social media"
|
|
148
|
+
className="fixed z-[100] w-64 rounded-xl border border-[var(--editor-border)] bg-[var(--editor-surface)] shadow-2xl p-2 flex flex-col gap-1"
|
|
149
|
+
style={{
|
|
150
|
+
left: position?.left ?? -9999,
|
|
151
|
+
top: position?.top ?? -9999,
|
|
152
|
+
// Invisible until positioned: the first render has nothing measured
|
|
153
|
+
// to position against yet, so it paints once off-screen for the
|
|
154
|
+
// effect above to measure, then becomes visible in its real place —
|
|
155
|
+
// never a visible jump.
|
|
156
|
+
visibility: position ? 'visible' : 'hidden',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<p className="px-2 pt-1 pb-1.5">
|
|
160
|
+
<span className="block text-[11px] font-semibold text-[var(--editor-text)]/90">Preview for social media</span>
|
|
161
|
+
<span className="block text-[10px] text-[var(--editor-text)]/50">What you see may vary depending on your device.</span>
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
{PLATFORM_OPTIONS.map(opt => {
|
|
165
|
+
const active = value === opt.id
|
|
166
|
+
return (
|
|
167
|
+
<button
|
|
168
|
+
key={opt.id}
|
|
169
|
+
type="button"
|
|
170
|
+
title={opt.label}
|
|
171
|
+
aria-label={opt.label}
|
|
172
|
+
role="menuitemradio"
|
|
173
|
+
aria-checked={active}
|
|
174
|
+
onClick={() => { onChange(opt.id); onClose() }}
|
|
175
|
+
className={`flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition-colors ${
|
|
176
|
+
active
|
|
177
|
+
? 'text-[var(--editor-text)] bg-sky-400/10'
|
|
178
|
+
: 'text-[var(--editor-text)]/80 hover:bg-[var(--editor-text)]/5'
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
<span className="flex items-center gap-2">
|
|
182
|
+
<PlatformGlyph icon={opt.icon} badgeClassName={opt.badgeClassName} />
|
|
183
|
+
<span>{opt.label}</span>
|
|
184
|
+
</span>
|
|
185
|
+
{active && <Check size={13} className={mode === 'light' ? 'text-sky-600' : 'text-sky-400'} />}
|
|
186
|
+
</button>
|
|
187
|
+
)
|
|
188
|
+
})}
|
|
189
|
+
|
|
190
|
+
<div className="my-1 h-px bg-[var(--editor-border)]" />
|
|
191
|
+
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
title="None"
|
|
195
|
+
aria-label="None"
|
|
196
|
+
role="menuitemradio"
|
|
197
|
+
aria-checked={value === null}
|
|
198
|
+
onClick={() => { onChange(null); onClose() }}
|
|
199
|
+
className={`flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition-colors ${
|
|
200
|
+
value === null
|
|
201
|
+
? 'text-[var(--editor-text)] bg-sky-400/10'
|
|
202
|
+
: 'text-[var(--editor-text)]/80 hover:bg-[var(--editor-text)]/5'
|
|
203
|
+
}`}
|
|
204
|
+
>
|
|
205
|
+
<span className="flex items-center gap-2">
|
|
206
|
+
<PlatformGlyph icon={Slash} badgeClassName="bg-[var(--editor-text)]/15" />
|
|
207
|
+
<span>None</span>
|
|
208
|
+
</span>
|
|
209
|
+
{value === null && <Check size={13} className={mode === 'light' ? 'text-sky-600' : 'text-sky-400'} />}
|
|
210
|
+
</button>
|
|
211
|
+
</div>,
|
|
212
|
+
document.body,
|
|
213
|
+
)
|
|
214
|
+
}
|