Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions apps/web/src/components/editor/panels/preview/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import { PREVIEW_ZOOM_PRESETS } from "@/lib/preview/zoom";
import { usePreviewViewport } from "./preview-viewport";
import { GridPopover } from "./guide-popover";
import { usePreviewStore } from "@/stores/preview-store";

import { useKeyboardShortcutsHelp } from "@/hooks/use-keyboard-shortcuts-help";
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Kbd } from "@/components/ui/kbd";
export function PreviewToolbar({
onToggleFullscreen,
}: {
Expand Down Expand Up @@ -135,14 +137,32 @@ function ZoomSelect() {

function PlayPauseButton() {
const isPlaying = useEditor((e) => e.playback.getIsPlaying());

const { shortcuts } = useKeyboardShortcutsHelp();
const togglePlay = shortcuts.find(
item => item.action === "toggle-play"
);
return (
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
<TooltipProvider duration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
</TooltipTrigger>

<TooltipContent className="flex items-center gap-2">
<span> {togglePlay?.description} </span>
<div className="flex gap-1">
<Kbd>
{togglePlay?.keys?.join(" / ")}
</Kbd>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Comment on lines 144 to +166
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What is the correct prop name for configuring the hover delay on Radix UI TooltipProvider?

💡 Result:

The correct prop name for configuring the hover delay on Radix UI TooltipProvider is delayDuration. It controls the duration (in milliseconds) from when the mouse enters a tooltip trigger until the tooltip opens, with a default value of 700ms. This is confirmed in the official Radix Primitives documentation, which lists delayDuration under TooltipProvider props. Examples show usage like <Tooltip.Provider delayDuration={200}>. The same prop is available on Tooltip.Root for per-tooltip overrides, and skipDelayDuration (default 300ms) handles delays when moving between triggers.

Citations:


🏁 Script executed:

# First, locate the toolbar.tsx file
find . -name "toolbar.tsx" -path "*/preview/*" 2>/dev/null

Repository: OpenCut-app/OpenCut

Length of output: 122


🏁 Script executed:

# Read the specific lines from toolbar.tsx to confirm the code
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
  sed -n '144,166p' "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fi

Repository: OpenCut-app/OpenCut

Length of output: 831


🏁 Script executed:

# Check timeline-toolbar.tsx for the delayDuration usage references
if [ -f "apps/web/src/components/editor/panels/timeline-toolbar.tsx" ]; then
  sed -n '250,260p' "apps/web/src/components/editor/panels/timeline-toolbar.tsx" | cat -n
  echo "---"
  sed -n '345,355p' "apps/web/src/components/editor/panels/timeline-toolbar.tsx" | cat -n
fi

Repository: OpenCut-app/OpenCut

Length of output: 45


🏁 Script executed:

# Search for togglePlay definition in toolbar.tsx to understand its type
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
  head -150 "apps/web/src/components/editor/panels/preview/toolbar.tsx" | grep -n "togglePlay" | head -10
fi

Repository: OpenCut-app/OpenCut

Length of output: 102


🏁 Script executed:

# Get more context around togglePlay definition
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
  sed -n '135,150p' "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fi

Repository: OpenCut-app/OpenCut

Length of output: 565


🏁 Script executed:

# Search for timeline-toolbar.tsx in the editor panels directory
find . -name "timeline-toolbar.tsx" 2>/dev/null | head -5

Repository: OpenCut-app/OpenCut

Length of output: 132


🏁 Script executed:

# Check the imports at the top of toolbar.tsx to verify TooltipProvider source
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
  head -30 "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fi

Repository: OpenCut-app/OpenCut

Length of output: 1356


🏁 Script executed:

# Check the actual usage in timeline-toolbar.tsx at lines 254 and 348
if [ -f "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" ]; then
  sed -n '250,260p' "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" | cat -n
  echo "---"
  sed -n '344,354p' "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" | cat -n
fi

Repository: OpenCut-app/OpenCut

Length of output: 850


🏁 Script executed:

# Search for all TooltipProvider usages in the codebase to see the pattern
rg "TooltipProvider" -A 2 -B 1 apps/web/src/components/

Repository: OpenCut-app/OpenCut

Length of output: 6671


Change duration to delayDuration on TooltipProvider.

Radix UI's TooltipProvider accepts delayDuration, not duration. The prop is silently ignored, so the intended 500ms hover delay has no effect. This file is the only one using the incorrect prop name; all other usages in the codebase (timeline-toolbar.tsx, properties/index.tsx) correctly use delayDuration={500}.

Also, guard the <Kbd> element with a conditional to avoid rendering empty badges when togglePlay?.keys is undefined or empty:

Proposed fix
-		<TooltipProvider duration={500}>
+		<TooltipProvider delayDuration={500}>
 			<Tooltip>
-        <TooltipTrigger asChild>
-          <Button
-            variant="text"
-            size="icon"
-            onClick={() => invokeAction("toggle-play")}
-          >
-            <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
-          </Button>
-        </TooltipTrigger>
-
-        <TooltipContent className="flex items-center gap-2">
-			<span> {togglePlay?.description} </span>
-			<div className="flex gap-1">
-				<Kbd>
-					{togglePlay?.keys?.join(" / ")}
-				</Kbd>
-			</div>
-		</TooltipContent>
-      </Tooltip>
+				<TooltipTrigger asChild>
+					<Button
+						variant="text"
+						size="icon"
+						onClick={() => invokeAction("toggle-play")}
+					>
+						<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
+					</Button>
+				</TooltipTrigger>
+				<TooltipContent className="flex items-center gap-2">
+					<span>{togglePlay?.description}</span>
+					{togglePlay?.keys?.length ? (
+						<Kbd>{togglePlay.keys.join(" / ")}</Kbd>
+					) : null}
+				</TooltipContent>
+			</Tooltip>
 		</TooltipProvider>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
<TooltipProvider duration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-2">
<span> {togglePlay?.description} </span>
<div className="flex gap-1">
<Kbd>
{togglePlay?.keys?.join(" / ")}
</Kbd>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
return (
<TooltipProvider delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-2">
<span>{togglePlay?.description}</span>
{togglePlay?.keys?.length ? (
<Kbd>{togglePlay.keys.join(" / ")}</Kbd>
) : null}
</TooltipContent>
</Tooltip>
</TooltipProvider>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/editor/panels/preview/toolbar.tsx` around lines 144 -
166, Replace the incorrect prop name on TooltipProvider: change the prop
currently named duration to delayDuration and set it to 500 (i.e.,
TooltipProvider delayDuration={500}) so Radix's hover delay is applied;
additionally, prevent rendering an empty keyboard badge by conditionally
rendering the <Kbd> element only when togglePlay?.keys exists and has length
(e.g., check togglePlay?.keys?.length > 0) inside the TooltipContent where
togglePlay?.description and keys are shown—this touches the TooltipProvider,
TooltipContent and Kbd usages around invokeAction("toggle-play") and the
togglePlay references.

);
}
175 changes: 119 additions & 56 deletions apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import { OcRippleIcon } from "@/components/icons";
import { GraphEditorPopover } from "./graph-editor/popover";
import { PopoverTrigger } from "@/components/ui/popover";
import { useGraphEditorController } from "./graph-editor/use-controller";

import { useKeyboardShortcutsHelp } from "@/hooks/use-keyboard-shortcuts-help";
import { Kbd } from "@/components/ui/kbd";
export function TimelineToolbar({
zoomLevel,
minZoom,
Expand Down Expand Up @@ -95,6 +96,12 @@ function ToolbarLeftSection() {
const isCurrentlyBookmarked = useEditor((e) =>
e.scenes.isBookmarked({ time: e.playback.getCurrentTime() }),
);
const { shortcuts } = useKeyboardShortcutsHelp();
const shortcutMap = Object.fromEntries(
shortcuts.map(s => [s.action, s])
);


const selectedElement =
selectedElements.length === 1
? (editor.timeline.getElementsWithTracks({
Expand Down Expand Up @@ -128,6 +135,109 @@ function ToolbarLeftSection() {
element: selectedElement.element,
});

const ToolTipShortcuts = {
split: {
icon: <HugeiconsIcon icon={ScissorIcon} />,
tooltip: (
<div className="flex items-center gap-2">
<span>Split element</span>
<Kbd>{shortcutMap["split"]?.keys?.[0]}</Kbd>
</div>
),
disabled: false,
onClick: ({ event }) =>
handleAction({ action: "split", event }),
},

"split-left": {
icon: <HugeiconsIcon icon={AlignLeftIcon} />,
tooltip: (
<div className="flex items-center gap-2">
<span>Split left</span>
<Kbd>{shortcutMap["split-left"]?.keys?.[0]}</Kbd>
</div>
),
disabled: false,
onClick: ({ event }) =>
handleAction({ action: "split-left", event }),
},

"split-right": {
icon: <HugeiconsIcon icon={AlignRightIcon} />,
tooltip: (
<div className="flex items-center gap-2">
<span>Split right</span>
<Kbd>{shortcutMap["split-right"]?.keys?.[0]}</Kbd>
</div>
),
disabled: false,
onClick: ({ event }) =>
handleAction({ action: "split-right", event }),
},

"toggle-source-audio": {
icon: (
<HugeiconsIcon
icon={
isSelectedSourceAudioSeparated
? Unlink02Icon
: Link02Icon
}
/>
),
tooltip: sourceAudioLabel,
disabled: !canToggleSelectedSourceAudio,
onClick: ({ event }) =>
handleAction({
action: "toggle-source-audio",
event,
}),
},

"duplicate-selected": {
icon: <HugeiconsIcon icon={Copy01Icon} />,
tooltip: (
<div className="flex items-center gap-2">
<span>Duplicate element</span>
<Kbd>
{shortcutMap["duplicate-selected"]?.keys?.[0]}
</Kbd>
</div>
),
disabled: false,
onClick: ({ event }) =>
handleAction({
action: "duplicate-selected",
event,
}),
},

"freeze-frame": {
icon: <HugeiconsIcon icon={SnowIcon} />,
tooltip: "Freeze frame (coming soon)",
disabled: true,
onClick: () => {},
},

"delete-selected": {
icon: <HugeiconsIcon icon={Delete02Icon} />,
tooltip: (
<div className="flex items-center gap-2">
<span>Delete element</span>
<Kbd>
{shortcutMap["delete-selected"]?.keys?.join(" / ")}
</Kbd>
</div>
),
disabled: false,
onClick: ({ event }) =>
handleAction({
action: "delete-selected",
event,
}),
},
};

const handleAction = ({
action,
event,
Expand All @@ -142,61 +252,14 @@ function ToolbarLeftSection() {
return (
<div className="flex items-center gap-1">
<TooltipProvider delayDuration={500}>
<ToolbarButton
icon={<HugeiconsIcon icon={ScissorIcon} />}
tooltip="Split element"
onClick={({ event }) => handleAction({ action: "split", event })}
/>

<ToolbarButton
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
tooltip="Split left"
onClick={({ event }) => handleAction({ action: "split-left", event })}
/>

<ToolbarButton
icon={<HugeiconsIcon icon={AlignRightIcon} />}
tooltip="Split right"
onClick={({ event }) =>
handleAction({ action: "split-right", event })
}
/>

<ToolbarButton
icon={
<HugeiconsIcon
icon={isSelectedSourceAudioSeparated ? Unlink02Icon : Link02Icon}
{Object.values(ToolTipShortcuts).map((item)=> (
<ToolbarButton
icon={item.icon}
disabled={item.disabled}
tooltip={item.tooltip}
onClick={item.onClick}
/>
}
tooltip={sourceAudioLabel}
disabled={!canToggleSelectedSourceAudio}
onClick={({ event }) =>
handleAction({ action: "toggle-source-audio", event })
}
/>

<ToolbarButton
icon={<HugeiconsIcon icon={Copy01Icon} />}
tooltip="Duplicate element"
onClick={({ event }) =>
handleAction({ action: "duplicate-selected", event })
}
/>

<ToolbarButton
icon={<HugeiconsIcon icon={SnowIcon} />}
tooltip="Freeze frame (coming soon)"
disabled={true}
onClick={({ event: _event }) => {}}
/>

<ToolbarButton
icon={<HugeiconsIcon icon={Delete02Icon} />}
tooltip="Delete element"
onClick={({ event }) =>
handleAction({ action: "delete-selected", event })
}
/>
))}
Comment on lines +255 to +262
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing key prop in iterable.

Biome flags this correctly: React requires a stable key when rendering a list from .map(). Since each entry in ToolTipShortcuts has a unique action key, iterate with Object.entries and use the key.

🔑 Proposed fix
-				{Object.values(ToolTipShortcuts).map((item)=> (
-					<ToolbarButton
-						icon={item.icon}
-						disabled={item.disabled}
-						tooltip={item.tooltip}
-						onClick={item.onClick}
-						/>
-				))}
+				{Object.entries(ToolTipShortcuts).map(([key, item]) => (
+					<ToolbarButton
+						key={key}
+						icon={item.icon}
+						disabled={item.disabled}
+						tooltip={item.tooltip}
+						onClick={item.onClick}
+					/>
+				))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{Object.values(ToolTipShortcuts).map((item)=> (
<ToolbarButton
icon={item.icon}
disabled={item.disabled}
tooltip={item.tooltip}
onClick={item.onClick}
/>
}
tooltip={sourceAudioLabel}
disabled={!canToggleSelectedSourceAudio}
onClick={({ event }) =>
handleAction({ action: "toggle-source-audio", event })
}
/>
<ToolbarButton
icon={<HugeiconsIcon icon={Copy01Icon} />}
tooltip="Duplicate element"
onClick={({ event }) =>
handleAction({ action: "duplicate-selected", event })
}
/>
<ToolbarButton
icon={<HugeiconsIcon icon={SnowIcon} />}
tooltip="Freeze frame (coming soon)"
disabled={true}
onClick={({ event: _event }) => {}}
/>
<ToolbarButton
icon={<HugeiconsIcon icon={Delete02Icon} />}
tooltip="Delete element"
onClick={({ event }) =>
handleAction({ action: "delete-selected", event })
}
/>
))}
{Object.entries(ToolTipShortcuts).map(([key, item]) => (
<ToolbarButton
key={key}
icon={item.icon}
disabled={item.disabled}
tooltip={item.tooltip}
onClick={item.onClick}
/>
))}
🧰 Tools
🪛 Biome (2.4.11)

[error] 256-261: Missing key property for this element in iterable.

(lint/correctness/useJsxKeyInIterable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx` around
lines 255 - 262, The list rendered from Object.values(ToolTipShortcuts) is
missing a stable React key; update the mapping to iterate with
Object.entries(ToolTipShortcuts) (or retain values but obtain the unique action
key) and pass a key prop to each ToolbarButton using the unique action
identifier (e.g., the ToolTipShortcuts entry key or action name) so that
ToolbarButton receives key={uniqueKey} along with the existing props (icon,
disabled, tooltip, onClick).


<div className="bg-border mx-1 h-6 w-px" />

Expand Down Expand Up @@ -339,7 +402,7 @@ function ToolbarButton({
buttonWrapper,
}: {
icon: React.ReactNode;
tooltip: string;
tooltip: React.ReactNode | string;
onClick?: ({ event }: { event: React.MouseEvent }) => void;
disabled?: boolean;
isActive?: boolean;
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/components/ui/kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client"
import * as React from "react";
import { cn } from "@/utils/ui";
type KbdProps = {
children: React.ReactNode;
className?: string;
}
export const Kbd = (
{
children,
className

}: KbdProps) => {
return (
<kbd className={cn("rounded bg-muted px-1.5 py-0.5 text-xs font-mono border", className)}>
{children}
</kbd>
);
}
11 changes: 6 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"better-auth": "^1.4.15",
"next": "^16.1.3"
"next": "^16.1.3",
"opencut-wasm": "^0.2.5"
},
Comment on lines +30 to 32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm where opencut-wasm is actually consumed and whether apps/web already declares it.
rg -nP "from ['\"]opencut-wasm['\"]" --type=ts --type=tsx
fd -t f package.json | xargs -I{} sh -c 'echo "=== {} ==="; jq ".dependencies[\"opencut-wasm\"] // empty" {}'

Repository: OpenCut-app/OpenCut

Length of output: 176


🏁 Script executed:

# Search for any import/require of opencut-wasm with broader patterns
rg -i "opencut-wasm" --type=js --type=ts
# Also check if the specific file exists
find . -path "*preview/toolbar*" -type f
# Check the workspace configuration
fd -t f "package.json|yarn.lock|bun.lock|pnpm-workspace.yaml" | head -5

Repository: OpenCut-app/OpenCut

Length of output: 3505


🏁 Script executed:

# Check bun workspace configuration
cat package.json | jq '.workspaces'
# Check if root has direct dependencies that would need opencut-wasm
rg "opencut-wasm" package.json apps/package.json 2>/dev/null | head -20
# Look at bun.lock to see how versions are resolved
head -100 bun.lock | grep -A5 -B5 "opencut-wasm" || echo "Pattern not in first 100 lines; searching entire lock file..."
rg "opencut-wasm" bun.lock | head -20

Repository: OpenCut-app/OpenCut

Length of output: 1004


Version conflict — opencut-wasm appears in both root and app package.json with different versions.

This PR is scoped to keyboard-shortcut tooltip hints, but it adds opencut-wasm to the root package.json with version ^0.2.5. However, opencut-wasm is already declared in apps/web/package.json with version ^0.2.6, and all 30+ imports are confined to apps/web (e.g., formatTimecode in toolbar.tsx, timeline managers, hooks, etc.). In a Bun monorepo, the root version will shadow the higher app-level version, causing the lower version (0.2.5) to resolve instead of 0.2.6.

Remove this dependency from the root package.json, or if there's a legitimate reason for it there, align both declarations to the same version and document why root needs it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 30 - 32, The root package.json currently declares
"opencut-wasm": "^0.2.5" which conflicts with the apps/web/package.json version
"^0.2.6" and will shadow the app's version in a Bun monorepo; remove the
"opencut-wasm" entry from the root package.json so the app-level dependency in
apps/web/package.json (used by files like toolbar.tsx and various timeline
managers/hooks) is the single source of truth, or if the root truly needs it,
update both package.json files to the same version (^0.2.6) and add a short
comment in the root explaining why root requires the dependency; after changing
package.json, run your install to update the lockfile.

"devDependencies": {
"turbo": "^2.8.20",
Expand Down