diff options
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/editor-header.tsx | 98 | ||||
| -rw-r--r-- | apps/web/src/components/editor/media-panel/index.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel/index.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel/text-properties.tsx | 14 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline/timeline-playhead.tsx | 1 | ||||
| -rw-r--r-- | apps/web/src/components/header-base.tsx | 8 | ||||
| -rw-r--r-- | apps/web/src/components/header.tsx | 24 | ||||
| -rw-r--r-- | apps/web/src/components/landing/handlebars.tsx | 173 |
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> ); |
