Skip to content
Draft
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
18 changes: 14 additions & 4 deletions packages/video/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,34 @@
"name": "@react-grab/video",
"version": "1.0.0",
"private": true,
"description": "React Grab video content",
"description": "React Grab promo video content",
"scripts": {
"dev": "remotion studio",
"build": "remotion bundle",
"validate": "remotion bundle --log=verbose",
"upgrade": "remotion upgrade",
"lint": "eslint src && tsc"
},
"dependencies": {
"@remotion/cli": "^4.0.0",
"@remotion/cli": "4.0.424",
"@remotion/google-fonts": "4.0.424",
"@remotion/transitions": "4.0.424",
"clsx": "^2.1.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"remotion": "^4.0.0"
"remotion": "4.0.424",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.0",
"zod": "3.22.3"
},
"devDependencies": {
"@remotion/eslint-config-flat": "^4.0.0",
"@remotion/eslint-config-flat": "4.0.424",
"@tailwindcss/postcss": "^4.1.0",
"@types/react": "19.2.7",
"@types/web": "0.0.166",
"eslint": "9.19.0",
"postcss": "^8.5.0",
"postcss-loader": "^8.1.0",
"typescript": "5.9.3"
}
}
35 changes: 35 additions & 0 deletions packages/video/remotion.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,38 @@ import { Config } from "@remotion/cli/config";

Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);

Config.overrideWebpackConfig((currentConfiguration) => {
const rules = currentConfiguration.module?.rules ?? [];

return {
...currentConfiguration,
module: {
...currentConfiguration.module,
rules: rules.map((rule) => {
if (
rule &&
rule !== "..." &&
rule.test instanceof RegExp &&
rule.test.test("test.css")
) {
return {
...rule,
use: [
...(Array.isArray(rule.use) ? rule.use : []),
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["@tailwindcss/postcss"],
},
},
},
],
};
}
return rule;
}),
},
};
});
3 changes: 0 additions & 3 deletions packages/video/src/Composition.tsx

This file was deleted.

23 changes: 15 additions & 8 deletions packages/video/src/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type React from "react";
import { Composition } from "remotion";
import { MyComposition } from "./Composition";
import { MainComposition } from "./compositions/main";
import {
VIDEO_WIDTH_PX,
VIDEO_HEIGHT_PX,
VIDEO_FPS,
TOTAL_DURATION_FRAMES,
} from "./constants";

export const RemotionRoot = () => {
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="MyComp"
component={MyComposition}
durationInFrames={60}
fps={30}
width={1280}
height={720}
id="ReactGrabPromo"
component={MainComposition}
durationInFrames={TOTAL_DURATION_FRAMES}
fps={VIDEO_FPS}
width={VIDEO_WIDTH_PX}
height={VIDEO_HEIGHT_PX}
/>
);
};
79 changes: 79 additions & 0 deletions packages/video/src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type React from "react";
import { cn } from "../utils/cn";
import { PANEL_STYLES } from "../constants";
import { TagBadge } from "./selection-label/TagBadge";
import { BottomSection } from "./selection-label/BottomSection";

interface ContextMenuItem {
label: string;
shortcut?: string;
active?: boolean;
}

interface ContextMenuProps {
/** Absolute x position */
x: number;
/** Absolute y position */
y: number;
tagName: string;
componentName?: string;
items: ContextMenuItem[];
}

export const ContextMenu: React.FC<ContextMenuProps> = ({
x,
y,
tagName,
componentName,
items,
}) => {
return (
<div
style={{
position: "absolute",
top: y,
left: x,
filter: "drop-shadow(0px 1px 2px #51515140)",
zIndex: 2147483647,
}}
className="font-sans text-[13px] antialiased select-none"
>
<div
className={cn(
"flex flex-col justify-center items-start rounded-[10px] antialiased w-fit h-fit min-w-[100px]",
PANEL_STYLES,
)}
>
<div className="shrink-0 flex items-center gap-1 pt-1.5 pb-1 w-fit h-fit px-2">
<TagBadge
tagName={tagName}
componentName={componentName}
shrink
/>
</div>
<BottomSection>
<div className="flex flex-col w-[calc(100%+16px)] -mx-2 -my-1.5">
{items.map((item) => (
<div
key={item.label}
className={cn(
"flex items-center justify-between w-full px-2 py-1",
item.active && "bg-black/5",
)}
>
<span className="text-[13px] leading-4 font-sans font-medium text-black">
{item.label}
</span>
{item.shortcut && (
<span className="text-[11px] font-sans text-black/50 ml-4">
{item.shortcut}
</span>
)}
</div>
))}
</div>
</BottomSection>
</div>
</div>
);
};
189 changes: 189 additions & 0 deletions packages/video/src/components/Cursor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { GRAB_PURPLE, VIDEO_HEIGHT_PX, VIDEO_WIDTH_PX } from "../constants";

export type CursorType = "default" | "crosshair" | "grabbing";

const CURSOR_OFFSET_PX = 5;

const DefaultCursor: React.FC = () => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd" transform="translate(10 7)">
<path
d="m6.148 18.473 1.863-1.003 1.615-.839-2.568-4.816h4.332l-11.379-11.408v16.015l3.316-3.221z"
fill="#fff"
/>
<path
d="m6.431 17 1.765-.941-2.775-5.202h3.604l-8.025-8.043v11.188l2.53-2.442z"
fill="#000"
/>
</g>
</svg>
);

const CrosshairCursor: React.FC = () => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" transform="translate(9 9)">
<path
d="m15 6h-6.01v-6h-2.98v6h-6.01v3h6.01v6h2.98v-6h6.01z"
fill="#fff"
/>
<path
d="m13.99 7.01h-6v-6.01h-.98v6.01h-6v.98h6v6.01h.98v-6.01h6z"
fill="#231f1f"
/>
</g>
</svg>
);

const GrabbingCursor: React.FC = () => {
const frame = useCurrentFrame();
const rotation = interpolate(frame, [0, 40], [0, 360], {
extrapolateRight: "extend",
});

return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<linearGradient id="busya" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0" stopColor="#4ab4ef" />
<stop offset="1" stopColor="#3582e5" />
</linearGradient>
<linearGradient id="busyb" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0" stopColor="#3481e4" />
<stop offset="1" stopColor="#2051db" />
</linearGradient>
<linearGradient id="busyc" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0" stopColor="#6bdcfc" />
<stop offset="1" stopColor="#4dc6fa" />
</linearGradient>
<linearGradient id="busyd" x1="50%" x2="50%" y1="0%" y2="100%">
<stop offset="0" stopColor="#4bc5f9" />
<stop offset="1" stopColor="#2fb0f8" />
</linearGradient>
<mask id="busye" fill="#fff">
<path
d="m1 23c0 4.971 4.03 9 9 9 4.97 0 9-4.029 9-9 0-4.971-4.03-9-9-9-4.97 0-9 4.029-9 9z"
fill="#fff"
fillRule="evenodd"
/>
</mask>
</defs>
<g fill="none" fillRule="evenodd" transform="translate(7)">
<g
mask="url(#busye)"
style={{
transformOrigin: "10px 23px",
transform: `rotate(${rotation}deg)`,
}}
>
<g transform="translate(1 14)">
<path d="m0 0h9v9h-9z" fill="url(#busya)" />
<path d="m9 9h9v9h-9z" fill="url(#busyb)" />
<path d="m9 0h9v9h-9z" fill="url(#busyc)" />
<path d="m0 9h9v9h-9z" fill="url(#busyd)" />
</g>
</g>
<g fillRule="nonzero">
<path
d="m0 16.422v-16.015l11.591 11.619h-7.041l-.151.124z"
fill="#fff"
/>
<path
d="m1 2.814v11.188l2.969-2.866.16-.139h5.036z"
fill="#000"
/>
</g>
</g>
</svg>
);
};

const CursorIcon: React.FC<{ type: CursorType }> = ({ type }) => {
if (type === "default") return <DefaultCursor />;
if (type === "crosshair") return <CrosshairCursor />;
if (type === "grabbing") return <GrabbingCursor />;
return null;
};

export interface CursorProps {
x: number;
y: number;
type: CursorType;
/** Opacity value (0-1), driven by interpolate() from the parent composition */
opacity: number;
}

export const Cursor: React.FC<CursorProps> = ({ x, y, type, opacity }) => {
if (opacity <= 0) return null;

return (
<>
{/* Crosshair lines — full viewport */}
{type === "crosshair" && (
<div
style={{
position: "absolute",
inset: 0,
zIndex: 50,
pointerEvents: "none",
opacity,
}}
>
{/* Horizontal line */}
<div
style={{
position: "absolute",
left: 0,
width: VIDEO_WIDTH_PX,
height: 1,
backgroundColor: GRAB_PURPLE,
top: y,
}}
/>
{/* Vertical line */}
<div
style={{
position: "absolute",
top: 0,
height: VIDEO_HEIGHT_PX,
width: 1,
backgroundColor: GRAB_PURPLE,
left: x,
}}
/>
</div>
)}

{/* Cursor icon */}
<div
style={{
position: "absolute",
zIndex: 60,
pointerEvents: "none",
opacity,
left: x - CURSOR_OFFSET_PX,
top: y - CURSOR_OFFSET_PX,
}}
>
<CursorIcon type={type} />
</div>
</>
);
};
Loading
Loading