From 041d47996219f4758d86e5a125798ffbb3b32643 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Fri, 8 May 2026 12:10:00 +0200 Subject: [PATCH] fix: findings that were high, removed monolithic timeline canvas --- .../components/viewport/TimelineCanvas.tsx | 298 ++++-------------- .../viewport/TimelineCanvasRows.tsx | 222 +++++++++++++ .../viewport/useTimelineCanvasHover.ts | 144 +++++++++ .../hooks/useTimelineKeyboardShortcuts.ts | 28 +- 4 files changed, 434 insertions(+), 258 deletions(-) create mode 100644 src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx create mode 100644 src/components/video-editor/timeline/components/viewport/useTimelineCanvasHover.ts diff --git a/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx b/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx index 1bcceb27..da19a3cc 100644 --- a/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx +++ b/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx @@ -1,10 +1,5 @@ import { useTimelineContext } from "dnd-timeline"; -import { useCallback, useMemo, useRef, useState } from "react"; -import { Plus } from "@phosphor-icons/react"; -import { cn } from "@/lib/utils"; -import glassStyles from "../../ItemGlass.module.css"; -import Item from "../../Item"; -import Row from "../../Row"; +import { useCallback, useMemo, useRef } from "react"; import { getTimelineContentMinHeightPx, getTimelineRowsMinHeightPx, @@ -12,20 +7,11 @@ import { TIMELINE_AXIS_HEIGHT_PX, } from "../../timelineLayout"; import type { AudioPeaksData } from "../../useAudioPeaks"; -import AudioWaveform from "../../AudioWaveform"; -import { CLIP_ROW_ID, ZOOM_ROW_ID } from "../../core/constants"; -import { - getAnnotationTrackIndex, - getAnnotationTrackRowId, - getAudioTrackIndex, - getAudioTrackRowId, - isAnnotationTrackRowId, - isAudioTrackRowId, -} from "../../core/rows"; import type { TimelineRenderItem } from "../../model/timelineModel"; import TimelineAxis from "../axis/TimelineAxis"; -import ClipMarkerOverlay from "../overlays/ClipMarkerOverlay"; import PlaybackCursor from "../playhead/PlaybackCursor"; +import { TimelineCanvasRows } from "./TimelineCanvasRows"; +import { useTimelineCanvasHover } from "./useTimelineCanvasHover"; interface TimelineCanvasProps { items: TimelineRenderItem[]; @@ -79,10 +65,6 @@ export default function TimelineCanvas({ const { setTimelineRef, style, sidebarWidth, direction, range, valueToPixels, pixelsToValue } = useTimelineContext(); const localTimelineRef = useRef(null); - const [isTimelineHovered, setIsTimelineHovered] = useState(false); - const [timelineHoverMs, setTimelineHoverMs] = useState(null); - const [isZoomRowHovered, setIsZoomRowHovered] = useState(false); - const [zoomRowHoverMs, setZoomRowHoverMs] = useState(null); const setRefs = useCallback( (node: HTMLDivElement | null) => { @@ -127,124 +109,40 @@ export default function TimelineCanvas({ ], ); - const zoomItems = items.filter((item) => item.rowId === ZOOM_ROW_ID); - const clipItems = items.filter((item) => item.rowId === CLIP_ROW_ID); - const annotationItems = items.filter((item) => isAnnotationTrackRowId(item.rowId)); - const audioItems = items.filter((item) => isAudioTrackRowId(item.rowId)); const audioRowIds = useMemo( - () => - Array.from(new Set(audioItems.map((item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId))))).sort( - (left, right) => getAudioTrackIndex(left) - getAudioTrackIndex(right), - ), - [audioItems], - ); - const annotationRowIds = useMemo( - () => - Array.from( - new Set(annotationItems.map((item) => getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)))), - ).sort((left, right) => getAnnotationTrackIndex(left) - getAnnotationTrackIndex(right)), - [annotationItems], + () => new Set(items.map((item) => item.rowId)).size, + [items], ); - const timelineRowCount = 2 + annotationRowIds.length + audioRowIds.length; + const timelineRowCount = Math.max(2, audioRowIds); const timelineRowsMinHeightPx = getTimelineRowsMinHeightPx(timelineRowCount); const timelineContentMinHeightPx = getTimelineContentMinHeightPx(timelineRowCount); const timelineViewportStretchFactor = getTimelineViewportStretchFactor(timelineRowCount); const sideProperty = direction === "rtl" ? "right" : "left"; - const visibleDurationMs = Math.max(1, range.end - range.start); - - const ghostStartMs = zoomRowHoverMs === null ? null : Math.max(0, Math.min(zoomRowHoverMs, videoDurationMs)); - const ghostDurationMs = Math.min(1000, videoDurationMs); - const ghostEndMs = - ghostStartMs === null - ? null - : Math.max(ghostStartMs, Math.min(videoDurationMs, ghostStartMs + ghostDurationMs)); - const ghostStartOffsetPx = ghostStartMs === null ? 0 : valueToPixels(Math.max(0, ghostStartMs - range.start)); - const ghostEndOffsetPx = ghostEndMs === null ? 0 : valueToPixels(Math.max(0, ghostEndMs - range.start)); - const ghostWidthPx = Math.max(18, ghostEndOffsetPx - ghostStartOffsetPx); - const timelineGhostOffsetPx = timelineHoverMs === null ? 0 : valueToPixels(Math.max(0, timelineHoverMs - range.start)); - const canShowGhostPlayhead = isTimelineHovered && timelineHoverMs !== null; - const canShowGhostZoom = - isZoomRowHovered && ghostStartMs !== null && (onAddZoomAtMs ? (canPlaceZoomAtMs?.(ghostStartMs) ?? true) : false); - - const updateTimelineHoverTime = useCallback( - (clientX: number, rect: DOMRect) => { - const contentWidth = Math.max(1, rect.width - sidebarWidth); - const contentX = - direction === "rtl" ? rect.right - sidebarWidth - clientX : clientX - rect.left - sidebarWidth; - const clampedX = Math.max(0, Math.min(contentX, contentWidth)); - const ratio = clampedX / contentWidth; - const nextMs = range.start + ratio * visibleDurationMs; - setTimelineHoverMs(Math.max(0, Math.min(nextMs, videoDurationMs))); - }, - [direction, range.start, sidebarWidth, videoDurationMs, visibleDurationMs], - ); - - const handleTimelineMouseEnter = useCallback( - (event: React.MouseEvent) => { - setIsTimelineHovered(true); - updateTimelineHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); - }, - [updateTimelineHoverTime], - ); - - const handleTimelineMouseMove = useCallback( - (event: React.MouseEvent) => { - if (!isTimelineHovered) setIsTimelineHovered(true); - updateTimelineHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); - }, - [isTimelineHovered, updateTimelineHoverTime], - ); - - const handleTimelineMouseLeave = useCallback(() => { - setIsTimelineHovered(false); - setTimelineHoverMs(null); - setIsZoomRowHovered(false); - setZoomRowHoverMs(null); - }, []); - - const updateZoomRowHoverTime = useCallback( - (clientX: number, rect: DOMRect) => { - if (rect.width <= 0) return; - const position = - direction === "rtl" - ? Math.max(0, Math.min(rect.right - clientX, rect.width)) - : Math.max(0, Math.min(clientX - rect.left, rect.width)); - const ratio = position / rect.width; - const nextMs = range.start + ratio * visibleDurationMs; - setZoomRowHoverMs(Math.max(0, Math.min(nextMs, videoDurationMs))); - }, - [direction, range.start, videoDurationMs, visibleDurationMs], - ); - - const handleZoomRowMouseEnter = useCallback( - (event: React.MouseEvent) => { - setIsZoomRowHovered(true); - updateZoomRowHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); - }, - [updateZoomRowHoverTime], - ); - const handleZoomRowMouseMove = useCallback( - (event: React.MouseEvent) => { - if (!isZoomRowHovered) setIsZoomRowHovered(true); - updateZoomRowHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); - }, - [isZoomRowHovered, updateZoomRowHoverTime], - ); - const handleZoomRowMouseLeave = useCallback(() => { - setIsZoomRowHovered(false); - setZoomRowHoverMs(null); - }, []); - const handleZoomRowClick = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation(); - if (!onAddZoomAtMs || zoomRowHoverMs === null) return; - const startMs = Math.max(0, Math.min(zoomRowHoverMs, videoDurationMs)); - if (canPlaceZoomAtMs && !canPlaceZoomAtMs(startMs)) return; - onAddZoomAtMs(startMs); - }, - [canPlaceZoomAtMs, onAddZoomAtMs, videoDurationMs, zoomRowHoverMs], - ); + const { + canShowGhostPlayhead, + timelineGhostOffsetPx, + handleTimelineMouseEnter, + handleTimelineMouseMove, + handleTimelineMouseLeave, + canShowGhostZoom, + ghostStartMs, + ghostStartOffsetPx, + ghostWidthPx, + handleZoomRowMouseEnter, + handleZoomRowMouseMove, + handleZoomRowMouseLeave, + handleZoomRowClick, + } = useTimelineCanvasHover({ + direction, + sidebarWidth, + rangeStart: range.start, + rangeEnd: range.end, + videoDurationMs, + onAddZoomAtMs, + canPlaceZoomAtMs, + valueToPixels, + }); return (
- - {audioPeaks && } - - {clipItems.map((item) => ( - onSelectClip?.(item.id)} - variant="clip" - > - {item.label} - - ))} - - - - {canShowGhostZoom && ghostStartMs !== null && ( -
-
-
-
-
-
- -
-
-
-
- )} - {zoomItems.map((item) => ( - onSelectZoom?.(item.id)} - zoomDepth={item.zoomDepth} - zoomMode={item.zoomMode} - variant="zoom" - > - {item.label} - - ))} - - - {annotationRowIds.map((rowId, index) => { - const rowItems = annotationItems.filter( - (item) => getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)) === rowId, - ); - return ( - - {rowItems.map((item) => ( - onSelectAnnotation?.(item.id)} - variant="annotation" - > - {item.label} - - ))} - - ); - })} - - {audioRowIds.map((rowId, index) => { - const rowItems = audioItems.filter( - (item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)) === rowId, - ); - return ( - - {rowItems.map((item) => ( - onSelectAudio?.(item.id)} - variant="audio" - > - {item.label} - - ))} - - ); - })} +
); diff --git a/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx b/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx new file mode 100644 index 00000000..85d37d07 --- /dev/null +++ b/src/components/video-editor/timeline/components/viewport/TimelineCanvasRows.tsx @@ -0,0 +1,222 @@ +import { Plus } from "@phosphor-icons/react"; +import { useMemo, type MouseEventHandler } from "react"; +import { cn } from "@/lib/utils"; +import AudioWaveform from "../../AudioWaveform"; +import glassStyles from "../../ItemGlass.module.css"; +import Item from "../../Item"; +import Row from "../../Row"; +import { CLIP_ROW_ID, ZOOM_ROW_ID } from "../../core/constants"; +import { + getAnnotationTrackIndex, + getAnnotationTrackRowId, + getAudioTrackIndex, + getAudioTrackRowId, + isAnnotationTrackRowId, + isAudioTrackRowId, +} from "../../core/rows"; +import type { TimelineRenderItem } from "../../model/timelineModel"; +import type { AudioPeaksData } from "../../useAudioPeaks"; +import ClipMarkerOverlay from "../overlays/ClipMarkerOverlay"; + +interface TimelineCanvasRowsProps { + items: TimelineRenderItem[]; + videoDurationMs: number; + selectAllBlocksActive: boolean; + selectedZoomId: string | null; + selectedClipId?: string | null; + selectedAnnotationId?: string | null; + selectedAudioId?: string | null; + onSelectZoom?: (id: string | null) => void; + onSelectClip?: (id: string | null) => void; + onSelectAnnotation?: (id: string | null) => void; + onSelectAudio?: (id: string | null) => void; + audioPeaks?: AudioPeaksData | null; + direction: "ltr" | "rtl"; + canShowGhostZoom: boolean; + ghostStartMs: number | null; + ghostStartOffsetPx: number; + ghostWidthPx: number; + onZoomRowMouseEnter: MouseEventHandler; + onZoomRowMouseMove: MouseEventHandler; + onZoomRowMouseLeave: MouseEventHandler; + onZoomRowClick: MouseEventHandler; +} + +export function TimelineCanvasRows({ + items, + videoDurationMs, + selectAllBlocksActive, + selectedZoomId, + selectedClipId, + selectedAnnotationId, + selectedAudioId, + onSelectZoom, + onSelectClip, + onSelectAnnotation, + onSelectAudio, + audioPeaks, + direction, + canShowGhostZoom, + ghostStartMs, + ghostStartOffsetPx, + ghostWidthPx, + onZoomRowMouseEnter, + onZoomRowMouseMove, + onZoomRowMouseLeave, + onZoomRowClick, +}: TimelineCanvasRowsProps) { + const zoomItems = useMemo(() => items.filter((item) => item.rowId === ZOOM_ROW_ID), [items]); + const clipItems = useMemo(() => items.filter((item) => item.rowId === CLIP_ROW_ID), [items]); + const annotationItems = useMemo( + () => items.filter((item) => isAnnotationTrackRowId(item.rowId)), + [items], + ); + const audioItems = useMemo(() => items.filter((item) => isAudioTrackRowId(item.rowId)), [items]); + + const audioRowIds = useMemo( + () => + Array.from( + new Set(audioItems.map((item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)))), + ).sort((left, right) => getAudioTrackIndex(left) - getAudioTrackIndex(right)), + [audioItems], + ); + const annotationRowIds = useMemo( + () => + Array.from( + new Set( + annotationItems.map((item) => + getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)), + ), + ), + ).sort((left, right) => getAnnotationTrackIndex(left) - getAnnotationTrackIndex(right)), + [annotationItems], + ); + + return ( + <> + + {audioPeaks && } + + {clipItems.map((item) => ( + onSelectClip?.(item.id)} + variant="clip" + > + {item.label} + + ))} + + + + {canShowGhostZoom && ghostStartMs !== null && ( +
+
+
+
+
+
+ +
+
+
+
+ )} + {zoomItems.map((item) => ( + onSelectZoom?.(item.id)} + zoomDepth={item.zoomDepth} + zoomMode={item.zoomMode} + variant="zoom" + > + {item.label} + + ))} + + + {annotationRowIds.map((rowId, index) => { + const rowItems = annotationItems.filter( + (item) => getAnnotationTrackRowId(getAnnotationTrackIndex(item.rowId)) === rowId, + ); + return ( + + {rowItems.map((item) => ( + onSelectAnnotation?.(item.id)} + variant="annotation" + > + {item.label} + + ))} + + ); + })} + + {audioRowIds.map((rowId, index) => { + const rowItems = audioItems.filter( + (item) => getAudioTrackRowId(getAudioTrackIndex(item.rowId)) === rowId, + ); + return ( + + {rowItems.map((item) => ( + onSelectAudio?.(item.id)} + variant="audio" + > + {item.label} + + ))} + + ); + })} + + ); +} diff --git a/src/components/video-editor/timeline/components/viewport/useTimelineCanvasHover.ts b/src/components/video-editor/timeline/components/viewport/useTimelineCanvasHover.ts new file mode 100644 index 00000000..3069f835 --- /dev/null +++ b/src/components/video-editor/timeline/components/viewport/useTimelineCanvasHover.ts @@ -0,0 +1,144 @@ +import { useCallback, useState, type MouseEvent } from "react"; + +interface UseTimelineCanvasHoverParams { + direction: "ltr" | "rtl"; + sidebarWidth: number; + rangeStart: number; + rangeEnd: number; + videoDurationMs: number; + onAddZoomAtMs?: (startMs: number) => void; + canPlaceZoomAtMs?: (startMs: number) => boolean; + valueToPixels: (value: number) => number; +} + +export function useTimelineCanvasHover({ + direction, + sidebarWidth, + rangeStart, + rangeEnd, + videoDurationMs, + onAddZoomAtMs, + canPlaceZoomAtMs, + valueToPixels, +}: UseTimelineCanvasHoverParams) { + const [isTimelineHovered, setIsTimelineHovered] = useState(false); + const [timelineHoverMs, setTimelineHoverMs] = useState(null); + const [isZoomRowHovered, setIsZoomRowHovered] = useState(false); + const [zoomRowHoverMs, setZoomRowHoverMs] = useState(null); + + const visibleDurationMs = Math.max(1, rangeEnd - rangeStart); + + const updateTimelineHoverTime = useCallback( + (clientX: number, rect: DOMRect) => { + const contentWidth = Math.max(1, rect.width - sidebarWidth); + const contentX = + direction === "rtl" ? rect.right - sidebarWidth - clientX : clientX - rect.left - sidebarWidth; + const clampedX = Math.max(0, Math.min(contentX, contentWidth)); + const ratio = clampedX / contentWidth; + const nextMs = rangeStart + ratio * visibleDurationMs; + setTimelineHoverMs(Math.max(0, Math.min(nextMs, videoDurationMs))); + }, + [direction, rangeStart, sidebarWidth, videoDurationMs, visibleDurationMs], + ); + + const handleTimelineMouseEnter = useCallback( + (event: MouseEvent) => { + setIsTimelineHovered(true); + updateTimelineHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); + }, + [updateTimelineHoverTime], + ); + + const handleTimelineMouseMove = useCallback( + (event: MouseEvent) => { + if (!isTimelineHovered) setIsTimelineHovered(true); + updateTimelineHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); + }, + [isTimelineHovered, updateTimelineHoverTime], + ); + + const handleTimelineMouseLeave = useCallback(() => { + setIsTimelineHovered(false); + setTimelineHoverMs(null); + setIsZoomRowHovered(false); + setZoomRowHoverMs(null); + }, []); + + const updateZoomRowHoverTime = useCallback( + (clientX: number, rect: DOMRect) => { + if (rect.width <= 0) return; + const position = + direction === "rtl" + ? Math.max(0, Math.min(rect.right - clientX, rect.width)) + : Math.max(0, Math.min(clientX - rect.left, rect.width)); + const ratio = position / rect.width; + const nextMs = rangeStart + ratio * visibleDurationMs; + setZoomRowHoverMs(Math.max(0, Math.min(nextMs, videoDurationMs))); + }, + [direction, rangeStart, videoDurationMs, visibleDurationMs], + ); + + const handleZoomRowMouseEnter = useCallback( + (event: MouseEvent) => { + setIsZoomRowHovered(true); + updateZoomRowHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); + }, + [updateZoomRowHoverTime], + ); + const handleZoomRowMouseMove = useCallback( + (event: MouseEvent) => { + if (!isZoomRowHovered) setIsZoomRowHovered(true); + updateZoomRowHoverTime(event.clientX, event.currentTarget.getBoundingClientRect()); + }, + [isZoomRowHovered, updateZoomRowHoverTime], + ); + const handleZoomRowMouseLeave = useCallback(() => { + setIsZoomRowHovered(false); + setZoomRowHoverMs(null); + }, []); + const handleZoomRowClick = useCallback( + (event: MouseEvent) => { + event.stopPropagation(); + if (!onAddZoomAtMs || zoomRowHoverMs === null) return; + const startMs = Math.max(0, Math.min(zoomRowHoverMs, videoDurationMs)); + if (canPlaceZoomAtMs && !canPlaceZoomAtMs(startMs)) return; + onAddZoomAtMs(startMs); + }, + [canPlaceZoomAtMs, onAddZoomAtMs, videoDurationMs, zoomRowHoverMs], + ); + + const ghostStartMs = + zoomRowHoverMs === null ? null : Math.max(0, Math.min(zoomRowHoverMs, videoDurationMs)); + const ghostDurationMs = Math.min(1000, videoDurationMs); + const ghostEndMs = + ghostStartMs === null + ? null + : Math.max(ghostStartMs, Math.min(videoDurationMs, ghostStartMs + ghostDurationMs)); + const ghostStartOffsetPx = + ghostStartMs === null ? 0 : valueToPixels(Math.max(0, ghostStartMs - rangeStart)); + const ghostEndOffsetPx = ghostEndMs === null ? 0 : valueToPixels(Math.max(0, ghostEndMs - rangeStart)); + const ghostWidthPx = Math.max(18, ghostEndOffsetPx - ghostStartOffsetPx); + const timelineGhostOffsetPx = + timelineHoverMs === null ? 0 : valueToPixels(Math.max(0, timelineHoverMs - rangeStart)); + const canShowGhostPlayhead = isTimelineHovered && timelineHoverMs !== null; + const canShowGhostZoom = + isZoomRowHovered && + ghostStartMs !== null && + (onAddZoomAtMs ? (canPlaceZoomAtMs?.(ghostStartMs) ?? true) : false); + + return { + canShowGhostPlayhead, + timelineGhostOffsetPx, + handleTimelineMouseEnter, + handleTimelineMouseMove, + handleTimelineMouseLeave, + canShowGhostZoom, + ghostStartMs, + ghostStartOffsetPx, + ghostWidthPx, + handleZoomRowMouseEnter, + handleZoomRowMouseMove, + handleZoomRowMouseLeave, + handleZoomRowClick, + }; +} diff --git a/src/components/video-editor/timeline/hooks/useTimelineKeyboardShortcuts.ts b/src/components/video-editor/timeline/hooks/useTimelineKeyboardShortcuts.ts index 3cb1fdbc..434b3648 100644 --- a/src/components/video-editor/timeline/hooks/useTimelineKeyboardShortcuts.ts +++ b/src/components/video-editor/timeline/hooks/useTimelineKeyboardShortcuts.ts @@ -1,15 +1,17 @@ import { useEffect, type RefObject } from "react"; -import { matchesShortcut } from "@/lib/shortcuts"; +import { matchesShortcut, type ShortcutBinding } from "@/lib/shortcuts"; + +interface TimelineShortcutBindings { + addKeyframe: ShortcutBinding; + addZoom: ShortcutBinding; + splitClip: ShortcutBinding; + addAnnotation: ShortcutBinding; + deleteSelected: ShortcutBinding; +} interface UseTimelineKeyboardShortcutsParams { isMac: boolean; - keyShortcuts: { - addKeyframe: unknown; - addZoom: unknown; - splitClip: unknown; - addAnnotation: unknown; - deleteSelected: unknown; - }; + keyShortcuts: TimelineShortcutBindings; isTimelineFocusedRef: RefObject; hasAnyTimelineBlocks: boolean; annotationCount: number; @@ -76,10 +78,10 @@ export function useTimelineKeyboardShortcuts({ return; } - if (matchesShortcut(e, keyShortcuts.addKeyframe as never, isMac)) addKeyframe(); - if (matchesShortcut(e, keyShortcuts.addZoom as never, isMac)) handleAddZoom(); - if (matchesShortcut(e, keyShortcuts.splitClip as never, isMac)) handleSplitClip(); - if (matchesShortcut(e, keyShortcuts.addAnnotation as never, isMac)) { + if (matchesShortcut(e, keyShortcuts.addKeyframe, isMac)) addKeyframe(); + if (matchesShortcut(e, keyShortcuts.addZoom, isMac)) handleAddZoom(); + if (matchesShortcut(e, keyShortcuts.splitClip, isMac)) handleSplitClip(); + if (matchesShortcut(e, keyShortcuts.addAnnotation, isMac)) { handleAddAnnotation(); } @@ -92,7 +94,7 @@ export function useTimelineKeyboardShortcuts({ if ( e.key === "Delete" || e.key === "Backspace" || - matchesShortcut(e, keyShortcuts.deleteSelected as never, isMac) + matchesShortcut(e, keyShortcuts.deleteSelected, isMac) ) { if (selectAllBlocksActive) { e.preventDefault();