summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editor-header.tsx98
-rw-r--r--apps/web/src/components/editor/media-panel/index.tsx2
-rw-r--r--apps/web/src/components/editor/properties-panel/index.tsx4
-rw-r--r--apps/web/src/components/editor/properties-panel/text-properties.tsx14
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx1
-rw-r--r--apps/web/src/components/header-base.tsx8
-rw-r--r--apps/web/src/components/header.tsx24
-rw-r--r--apps/web/src/components/landing/handlebars.tsx173
8 files changed, 134 insertions, 190 deletions
diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx
index f2b79479..837bd97a 100644
--- a/apps/web/src/components/editor-header.tsx
+++ b/apps/web/src/components/editor-header.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { Button } from "./ui/button";
-import { SquarePen, Download } from "lucide-react";
+import { ChevronLeft, Download } from "lucide-react";
import { useTimelineStore } from "@/stores/timeline-store";
import { HeaderBase } from "./header-base";
import { formatTimeCode } from "@/lib/time";
@@ -10,13 +10,6 @@ import { useProjectStore } from "@/stores/project-store";
import { KeyboardShortcutsHelp } from "./keyboard-shortcuts-help";
import { useState, useRef } from "react";
import { Input } from "@/components/ui/input";
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbSeparator,
-} from "./ui/breadcrumb";
import { toast } from "sonner";
export function EditorHeader() {
@@ -30,10 +23,10 @@ export function EditorHeader() {
// TODO: Implement export functionality
// NOTE: This is already being worked on
console.log("Export project");
- window.open('https://youtube.com/watch?v=dQw4w9WgXcQ', '_blank');
+ window.open("https://youtube.com/watch?v=dQw4w9WgXcQ", "_blank");
};
- const handleNameEdit = () => {
+ const handleNameClick = () => {
if (!activeProject) return;
setNewName(activeProject.name);
setIsEditing(true);
@@ -58,55 +51,46 @@ export function EditorHeader() {
const leftContent = (
<div className="flex items-center gap-2">
- <Breadcrumb>
- <BreadcrumbList className="text-current">
- <BreadcrumbItem>
- <BreadcrumbLink asChild>
- <Link
- href="/projects"
- className="hover:text-muted-foreground inline-flex items-center"
- >
- All Projects
- </Link>
- </BreadcrumbLink>
- </BreadcrumbItem>
- <BreadcrumbSeparator />
- <BreadcrumbItem>
- {isEditing ? (
- <Input
- ref={inputRef}
- className="text-sm font-medium w-40 xl:w-60 px-2 py-1 h-9 truncate"
- value={newName}
- onChange={(e) => setNewName(e.target.value)}
- onBlur={handleNameSave}
- onKeyDown={handleInputKeyDown}
- onFocus={(e) => e.target.select()}
- maxLength={64}
- aria-label="Project name"
- autoFocus
- />
- ) : (
- <Button
- className="text-sm font-medium cursor-pointer hover:text-muted-foreground flex items-center gap-2 group"
- title="Click to rename"
- variant="text"
- tabIndex={0}
- onClick={handleNameEdit}
- >
- <div className="truncate text-ellipsis overflow-clip max-w-40 xl:max-w-60">
- {activeProject?.name}
- </div>
- <SquarePen className="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" />
- </Button>
- )}
- </BreadcrumbItem>
- </BreadcrumbList>
- </Breadcrumb>
+ <Link
+ href="/projects"
+ className="font-medium tracking-tight flex items-center gap-2 hover:opacity-80 transition-opacity"
+ >
+ <ChevronLeft className="h-4 w-4" />
+ </Link>
+ <div className="w-40 xl:w-60 h-9 flex items-center">
+ {isEditing ? (
+ <Input
+ ref={inputRef}
+ className="text-sm font-medium w-full px-2 py-1 h-9 truncate"
+ value={newName}
+ onChange={(e) => setNewName(e.target.value)}
+ onBlur={handleNameSave}
+ onKeyDown={handleInputKeyDown}
+ onFocus={(e) => e.target.select()}
+ maxLength={64}
+ aria-label="Project name"
+ autoFocus
+ />
+ ) : (
+ <span
+ className="text-sm font-medium cursor-pointer hover:underline"
+ title="Click to rename"
+ role="button"
+ tabIndex={0}
+ onClick={handleNameClick}
+ onKeyDown={(e) => e.key === "Enter" && handleNameClick()}
+ >
+ <div className="truncate text-ellipsis overflow-clip max-w-40 xl:max-w-60">
+ {activeProject?.name}
+ </div>
+ </span>
+ )}
+ </div>
</div>
);
const centerContent = (
- <div className="flex items-center justify-center gap-2 text-xs">
+ <div className="flex items-center gap-2 text-xs">
<span>
{formatTimeCode(
getTotalDuration(),
@@ -118,7 +102,7 @@ export function EditorHeader() {
);
const rightContent = (
- <nav className="flex items-center justify-end gap-2">
+ <nav className="flex items-center gap-2">
<KeyboardShortcutsHelp />
<Button
size="sm"
@@ -137,7 +121,7 @@ export function EditorHeader() {
leftContent={leftContent}
centerContent={centerContent}
rightContent={rightContent}
- className="bg-background h-[3.2rem] px-4 items-center grid grid-cols-3"
+ className="bg-background h-[3.2rem] px-4 items-center"
/>
);
}
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx
index 2c743618..1dfa08c0 100644
--- a/apps/web/src/components/editor/media-panel/index.tsx
+++ b/apps/web/src/components/editor/media-panel/index.tsx
@@ -46,7 +46,7 @@ export function MediaPanel() {
};
return (
- <div className="h-full flex flex-col bg-panel rounded-sm overflow-hidden">
+ <div className="h-full flex flex-col bg-panel rounded-sm overflow-scroll">
<TabBar />
<div className="flex-1">{viewMap[activeTab]}</div>
</div>
diff --git a/apps/web/src/components/editor/properties-panel/index.tsx b/apps/web/src/components/editor/properties-panel/index.tsx
index 3613f271..3f38bd78 100644
--- a/apps/web/src/components/editor/properties-panel/index.tsx
+++ b/apps/web/src/components/editor/properties-panel/index.tsx
@@ -41,7 +41,7 @@ export function PropertiesPanel() {
label="Resolution:"
value={`${canvasSize.width} × ${canvasSize.height}`}
/>
- <div className="flex justify-between items-center">
+ <div className="flex flex-col gap-1">
<Label className="text-xs text-muted-foreground">Frame rate:</Label>
<Select
value={(activeProject?.fps || 30).toString()}
@@ -79,7 +79,7 @@ export function PropertiesPanel() {
}
if (element?.type === "media") {
const mediaItem = mediaItems.find(
- (item) => item.id === element.mediaId,
+ (item) => item.id === element.mediaId
);
if (mediaItem?.type === "audio") {
diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx
index c55c3dae..e147ea65 100644
--- a/apps/web/src/components/editor/properties-panel/text-properties.tsx
+++ b/apps/web/src/components/editor/properties-panel/text-properties.tsx
@@ -71,6 +71,20 @@ export function TextProperties({
</div>
</PropertyItemValue>
</PropertyItem>
+ <PropertyItem direction="row">
+ <PropertyItemLabel>Color</PropertyItemLabel>
+ <PropertyItemValue>
+ <Input
+ type="color"
+ value={element.color || "#ffffff"}
+ onChange={(e) => {
+ const color = e.target.value;
+ updateTextElement(trackId, element.id, { color });
+ }}
+ className="w-full cursor-pointer rounded-full"
+ />
+ </PropertyItemValue>
+ </PropertyItem>
</div>
);
}
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
index 509ce9d7..d0aea7fa 100644
--- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx
@@ -72,6 +72,7 @@ export function TimelinePlayhead({
top: 0,
height: `${totalHeight}px`,
width: "2px", // Slightly wider for better click target
+ zIndex: 100,
}}
onMouseDown={handlePlayheadMouseDown}
>
diff --git a/apps/web/src/components/header-base.tsx b/apps/web/src/components/header-base.tsx
index a4863d27..98f1c431 100644
--- a/apps/web/src/components/header-base.tsx
+++ b/apps/web/src/components/header-base.tsx
@@ -31,9 +31,11 @@ export function HeaderBase({
<header
className={cn("px-6 h-14 flex justify-between items-center", className)}
>
- {leftContent && leftContent}
- {centerContent && centerContent}
- {rightContent && rightContent}
+ {leftContent && <div className="flex items-center">{leftContent}</div>}
+ {centerContent && (
+ <div className="flex items-center">{centerContent}</div>
+ )}
+ {rightContent && <div className="flex items-center">{rightContent}</div>}
</header>
);
}
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx
index 2e6de534..f5f847be 100644
--- a/apps/web/src/components/header.tsx
+++ b/apps/web/src/components/header.tsx
@@ -15,17 +15,19 @@ export function Header() {
);
const rightContent = (
- <nav className="flex items-center gap-3">
- <Link href="/blog">
- <Button variant="text" className="text-sm p-0">
- Blog
- </Button>
- </Link>
- <Link href="/contributors">
- <Button variant="text" className="text-sm p-0">
- Contributors
- </Button>
- </Link>
+ <nav className="flex items-center gap-1">
+ <div className="flex items-center gap-4">
+ <Link href="/blog">
+ <Button variant="text" className="text-sm p-0">
+ Blog
+ </Button>
+ </Link>
+ <Link href="/contributors">
+ <Button variant="text" className="text-sm p-0">
+ Contributors
+ </Button>
+ </Link>
+ </div>
<Link href="/projects">
<Button size="sm" className="text-sm ml-4">
Projects
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx
index 94764dbe..26cf9254 100644
--- a/apps/web/src/components/landing/handlebars.tsx
+++ b/apps/web/src/components/landing/handlebars.tsx
@@ -1,163 +1,104 @@
"use client";
-import React, { useState, useRef, useEffect } from "react";
-import { motion, useMotionValue, useTransform, PanInfo } from "motion/react";
+import {
+ motion,
+ useMotionTemplate,
+ useMotionValue,
+ useMotionValueEvent,
+ useTransform,
+} from "motion/react";
+import { type PropsWithChildren, useEffect, useRef, useState } from "react";
-interface HandlebarsProps {
- children: React.ReactNode;
- minWidth?: number;
- maxWidth?: number;
- onRangeChange?: (left: number, right: number) => void;
-}
-
-export function Handlebars({
- children,
- minWidth = 50,
- maxWidth = 400,
- onRangeChange,
-}: HandlebarsProps) {
- const [leftHandle, setLeftHandle] = useState(0);
- const [rightHandle, setRightHandle] = useState(maxWidth);
- const [contentWidth, setContentWidth] = useState(maxWidth);
-
- const leftHandleX = useMotionValue(0);
- const rightHandleX = useMotionValue(maxWidth);
-
- const visibleWidth = useTransform(
- [leftHandleX, rightHandleX],
- (values: number[]) => values[1] - values[0]
- );
-
- const contentLeft = useTransform(leftHandleX, (left: number) => -left);
+type HandlebarsProps = PropsWithChildren;
+export function Handlebars({ children }: HandlebarsProps) {
const containerRef = useRef<HTMLDivElement>(null);
- const measureRef = useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (!measureRef.current) return;
-
- const measureContent = () => {
- if (measureRef.current) {
- const width = measureRef.current.scrollWidth;
- const paddedWidth = width + 32;
- setContentWidth(paddedWidth);
- setRightHandle(paddedWidth);
- rightHandleX.set(paddedWidth);
- }
- };
- measureContent();
- const timer = setTimeout(measureContent, 50);
+ const [width, setWidth] = useState(0);
- return () => clearTimeout(timer);
- }, [children, rightHandleX]);
+ const [leftHandle, setLeftHandle] = useState(0);
+ const [rightHandle, setRightHandle] = useState(width);
- useEffect(() => {
- leftHandleX.set(leftHandle);
- }, [leftHandle, leftHandleX]);
+ const leftHandleX = useMotionValue(0);
+ const rightHandleX = useMotionValue(width);
useEffect(() => {
- rightHandleX.set(rightHandle);
- }, [rightHandle, rightHandleX]);
+ const el = containerRef.current;
+ if (!el) return;
- useEffect(() => {
- onRangeChange?.(leftHandle, rightHandle);
- }, [leftHandle, rightHandle, onRangeChange]);
+ const observer = new ResizeObserver(() => {
+ setWidth(el.offsetWidth);
+ setRightHandle(el.offsetWidth);
+ rightHandleX.set(el.offsetWidth);
+ });
- const handleLeftDrag = (event: any, info: PanInfo) => {
- const newLeft = Math.max(
- 0,
- Math.min(leftHandle + info.offset.x, rightHandle - minWidth)
- );
- setLeftHandle(newLeft);
- };
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [rightHandleX.set]);
- const handleRightDrag = (event: any, info: PanInfo) => {
- const newRight = Math.max(
- leftHandle + minWidth,
- Math.min(contentWidth, rightHandle + info.offset.x)
- );
- setRightHandle(newRight);
- };
+ useMotionValueEvent(leftHandleX, "change", () => {
+ setLeftHandle(leftHandleX.get());
+ });
+ useMotionValueEvent(rightHandleX, "change", () => {
+ setRightHandle(rightHandleX.get());
+ });
+ const leftGradient = useTransform(leftHandleX, [0, width - 10], [0, 100]);
+ const rightGradient = useTransform(rightHandleX, [0, width + 10], [0, 100]);
return (
<div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
- <div
- ref={measureRef}
- className="absolute -left-[9999px] top-0 invisible px-4 whitespace-nowrap font-[inherit]"
- >
- {children}
- </div>
-
- <div
- ref={containerRef}
- className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2"
- style={{ width: contentWidth }}
- >
- <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between">
+ <div ref={containerRef} className="relative -rotate-[2.76deg] mt-2">
+ <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between z-[1]">
<motion.div
- className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none"
+ className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center select-none"
style={{
- position: "absolute",
x: leftHandleX,
- left: 0,
- zIndex: 10,
}}
drag="x"
- dragConstraints={{ left: 0, right: rightHandle - minWidth }}
+ dragConstraints={{ left: 0, right: rightHandle - 60 }}
dragElastic={0}
dragMomentum={false}
- onDrag={handleLeftDrag}
- whileHover={{ scale: 1.05 }}
- whileDrag={{ scale: 1.1 }}
+ whileHover={{ scale: 1.05, cursor: "grab" }}
+ whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
+ <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
</motion.div>
<motion.div
- className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none"
+ className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center select-none"
style={{
- position: "absolute",
x: rightHandleX,
- left: -30,
- zIndex: 10,
}}
drag="x"
dragConstraints={{
- left: leftHandle + minWidth,
- right: contentWidth,
+ left: leftHandle + 60,
+ right: width,
}}
dragElastic={0}
dragMomentum={false}
- onDrag={handleRightDrag}
- whileHover={{ scale: 1.05 }}
- whileDrag={{ scale: 1.1 }}
+ whileHover={{ scale: 1.05, cursor: "grab" }}
+ whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
- <div className="w-2 h-8 rounded-full bg-yellow-500" />
+ <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
</motion.div>
</div>
- <motion.div
- className="relative overflow-hidden rounded-2xl"
+ <motion.span
+ className="inline-flex items-center justify-center w-full h-full px-9 relative rounded-2xl will-change-auto"
style={{
- width: visibleWidth,
- x: leftHandleX,
- height: "100%",
+ mask: useMotionTemplate`linear-gradient(90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0) ${leftGradient}%,
+ rgba(0, 0, 0) ${leftGradient}%,
+ rgba(0, 0, 0) ${rightGradient}%,
+ rgba(255, 255, 255, 0) ${rightGradient}%,
+ rgba(255, 255, 255, 0) 100%)`,
}}
>
- <motion.div
- className="w-full h-full flex items-center justify-center px-4"
- style={{
- x: contentLeft,
- width: contentWidth,
- whiteSpace: "nowrap",
- }}
- >
- {children}
- </motion.div>
- </motion.div>
+ {children}
+ </motion.span>
</div>
</div>
);