lots more icons to do but

This commit is contained in:
Maze Winther
2026-01-24 15:53:46 +01:00
parent 33f453efc7
commit 954160d03b
39 changed files with 519 additions and 710 deletions
+5 -38
View File
@@ -1179,7 +1179,7 @@ worker.ts
## apps/web/src/stores ## apps/web/src/stores
assets-panel-store.ts assets-panel-store.tsx
export const TAB_KEYS export const TAB_KEYS
export type Tab = (typeof TAB_KEYS)[number] export type Tab = (typeof TAB_KEYS)[number]
export const tabs export const tabs
@@ -1678,8 +1678,6 @@ ui.ts
## packages/ui/src/icons ## packages/ui/src/icons
brand.tsx brand.tsx
export function OcGoogleIcon({ className }: { className?: string })
export function OcGithubIcon({ className }: { className?: string })
export function OcVercelIcon({ className }: { className?: string }) export function OcVercelIcon({ className }: { className?: string })
export function OcMarbleIcon({␍ export function OcMarbleIcon({␍
className = "",␍ className = "",␍
@@ -1696,48 +1694,17 @@ brand.tsx
size?: number;␍ size?: number;␍
}) })
editor.tsx ui.tsx
export function OcBackgroundIcon({ className }: { className?: string }) export function OcSplitKeepLeftIcon({␍
export function OcSocialsIcon({␍
className = "",␍ className = "",␍
size = 32,␍ size = 32,␍
}: {␍ }: {␍
className?: string;␍ className?: string;␍
size?: number;␍ size?: number;␍
}) })
export function OcTransitionUpIcon({␍ export function OcSplitKeepRightIcon({␍
className = "",␍ className = "",␍
size = 16,␍ size = 32,␍
}: {␍
className?: string;␍
size?: number;␍
})
ui.tsx
export function OcMenuIcon({␍
className = "",␍
size = 16,␍
}: {␍
className?: string;␍
size?: number;␍
})
export function OcPencilIcon({␍
className = "",␍
size = 24,␍
}: {␍
className?: string;␍
size?: number;␍
})
export function OcLeftArrowIcon({␍
className = "",␍
size = 24,␍
}: {␍
className?: string;␍
size?: number;␍
})
export function OcTrashIcon({␍
className = "",␍
size = 24,␍
}: {␍ }: {␍
className?: string;␍ className?: string;␍
size?: number;␍ size?: number;␍
+2
View File
@@ -21,6 +21,8 @@
"@ffmpeg/util": "^0.12.2", "@ffmpeg/util": "^0.12.2",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@huggingface/transformers": "^3.8.1", "@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*", "@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*", "@opencut/ui": "workspace:*",
+1 -35
View File
@@ -1,12 +1,9 @@
import { OcGithubIcon } from "@opencut/ui/icons";
import { ExternalLink } from "lucide-react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { GitHubContributeSection } from "@/components/gitHub-contribute-section"; import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { EXTERNAL_TOOLS, SOCIAL_LINKS } from "@/constants/site-constants"; import { EXTERNAL_TOOLS } from "@/constants/site-constants";
import { BasePage } from "../base-page"; import { BasePage } from "../base-page";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -87,7 +84,6 @@ export default async function ContributorsPage() {
{otherContributors.length > 0 && ( {otherContributors.length > 0 && (
<AllContributorsSection contributors={otherContributors} /> <AllContributorsSection contributors={otherContributors} />
)} )}
{contributors.length === 0 && <EmptyState />}
<ExternalToolsSection /> <ExternalToolsSection />
<GitHubContributeSection <GitHubContributeSection
title="Join the community" title="Join the community"
@@ -210,36 +206,6 @@ function AllContributorsSection({
); );
} }
function EmptyState() {
return (
<div className="flex flex-col gap-8 py-20 text-center">
<div className="flex flex-col gap-6">
<div className="bg-muted/50 mx-auto flex size-20 items-center justify-center rounded-full">
<OcGithubIcon className="text-muted-foreground size-8" />
</div>
<div className="flex flex-col gap-3">
<h3 className="text-xl font-medium">No contributors found</h3>
<p className="text-muted-foreground mx-auto max-w-md">
Unable to load contributors at the moment. Check back later or view
on GitHub.
</p>
</div>
</div>
<Link
href={`${SOCIAL_LINKS.github}/graphs/contributors`}
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" className="gap-2">
<OcGithubIcon />
View on GitHub
<ExternalLink />
</Button>
</Link>
</div>
);
}
function ExternalToolsSection() { function ExternalToolsSection() {
return ( return (
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
+38 -26
View File
@@ -1,16 +1,5 @@
"use client"; "use client";
import {
ArrowDown01,
Calendar,
ChevronLeft,
MoreHorizontal,
Plus,
Search,
Trash2,
Video,
X,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -41,6 +30,18 @@ import {
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import type { TProjectMetadata } from "@/types/project"; import type { TProjectMetadata } from "@/types/project";
import { formatDate } from "@/utils/date"; import { formatDate } from "@/utils/date";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ArrowHorizontalIcon,
ArrowLeft01Icon,
Calendar04Icon,
Delete02Icon,
MultiplicationSignIcon,
PlusSignIcon,
Search01Icon,
SortingNineOneIcon,
Video01Icon,
} from "@hugeicons/core-free-icons";
export default function ProjectsPage() { export default function ProjectsPage() {
const [isSelectionMode, setIsSelectionMode] = useState(false); const [isSelectionMode, setIsSelectionMode] = useState(false);
@@ -160,7 +161,7 @@ export default function ProjectsPage() {
href="/" href="/"
className="hover:text-muted-foreground flex items-center gap-1 transition-colors" className="hover:text-muted-foreground flex items-center gap-1 transition-colors"
> >
<ChevronLeft className="size-5! shrink-0" /> <HugeiconsIcon icon={ArrowLeft01Icon} className="size-5! shrink-0" />
<span className="text-sm font-medium">Back</span> <span className="text-sm font-medium">Back</span>
</Link> </Link>
<div className="block md:hidden"> <div className="block md:hidden">
@@ -171,7 +172,7 @@ export default function ProjectsPage() {
size="sm" size="sm"
onClick={handleCancelSelection} onClick={handleCancelSelection}
> >
<X /> <HugeiconsIcon icon={MultiplicationSignIcon} />
Cancel Cancel
</Button> </Button>
{selectedProjects.size > 0 && ( {selectedProjects.size > 0 && (
@@ -180,7 +181,7 @@ export default function ProjectsPage() {
size="sm" size="sm"
onClick={() => setIsBulkDeleteDialogOpen(true)} onClick={() => setIsBulkDeleteDialogOpen(true)}
> >
<Trash2 /> <HugeiconsIcon icon={Delete02Icon} />
Delete ({selectedProjects.size}) Delete ({selectedProjects.size})
</Button> </Button>
)} )}
@@ -210,7 +211,7 @@ export default function ProjectsPage() {
{isSelectionMode ? ( {isSelectionMode ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={handleCancelSelection}> <Button variant="outline" onClick={handleCancelSelection}>
<X /> <HugeiconsIcon icon={MultiplicationSignIcon} />
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -218,7 +219,7 @@ export default function ProjectsPage() {
disabled={selectedProjects.size === 0} disabled={selectedProjects.size === 0}
onClick={() => setIsBulkDeleteDialogOpen(true)} onClick={() => setIsBulkDeleteDialogOpen(true)}
> >
<Trash2 /> <HugeiconsIcon icon={Delete02Icon} />
Delete {selectedProjects.size} projects Delete {selectedProjects.size} projects
</Button> </Button>
</div> </div>
@@ -257,9 +258,8 @@ export default function ProjectsPage() {
variant="outline" variant="outline"
className="size-9 items-center justify-center" className="size-9 items-center justify-center"
> >
<ArrowDown01 <HugeiconsIcon
strokeWidth={1.5} icon={SortingNineOneIcon}
className="!size-[1.05rem]"
aria-hidden="true" aria-hidden="true"
/> />
</Button> </Button>
@@ -430,7 +430,10 @@ function ProjectCard({
/> />
) : ( ) : (
<div className="bg-muted/50 flex size-full items-center justify-center"> <div className="bg-muted/50 flex size-full items-center justify-center">
<Video className="text-muted-foreground size-12 shrink-0" /> <HugeiconsIcon
icon={Video01Icon}
className="text-muted-foreground size-12 shrink-0"
/>
</div> </div>
)} )}
</div> </div>
@@ -458,7 +461,10 @@ function ProjectCard({
}`} }`}
onClick={(event) => event.preventDefault()} onClick={(event) => event.preventDefault()}
> >
<MoreHorizontal aria-hidden="true" /> <HugeiconsIcon
icon={ArrowHorizontalIcon}
aria-hidden="true"
/>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
@@ -506,7 +512,7 @@ function ProjectCard({
<div className="space-y-1"> <div className="space-y-1">
<div className="text-muted-foreground flex items-center gap-1.5 text-sm"> <div className="text-muted-foreground flex items-center gap-1.5 text-sm">
<Calendar className="size-4!" /> <HugeiconsIcon icon={Calendar04Icon} className="size-4" />
<span>Created {formatDate({ date: project.createdAt })}</span> <span>Created {formatDate({ date: project.createdAt })}</span>
</div> </div>
</div> </div>
@@ -575,7 +581,7 @@ function ProjectsLoader() {
function CreateButton({ onClick }: { onClick?: () => void }) { function CreateButton({ onClick }: { onClick?: () => void }) {
return ( return (
<Button className="flex" onClick={onClick}> <Button className="flex" onClick={onClick}>
<Plus /> <HugeiconsIcon icon={PlusSignIcon} />
<span className="text-sm font-medium">New project</span> <span className="text-sm font-medium">New project</span>
</Button> </Button>
); );
@@ -596,7 +602,10 @@ function EmptyState({
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center"> <div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full"> <div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
<Search className="text-muted-foreground size-8" /> <HugeiconsIcon
icon={Search01Icon}
className="text-muted-foreground size-8"
/>
</div> </div>
<h3 className="text-lg font-medium">No results found</h3> <h3 className="text-lg font-medium">No results found</h3>
<p className="text-muted-foreground max-w-md"> <p className="text-muted-foreground max-w-md">
@@ -614,7 +623,10 @@ function EmptyState({
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center"> <div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full"> <div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
<Video className="text-muted-foreground size-8" /> <HugeiconsIcon
icon={Video01Icon}
className="text-muted-foreground size-8"
/>
</div> </div>
<h3 className="text-lg font-medium">No projects yet</h3> <h3 className="text-lg font-medium">No projects yet</h3>
<p className="text-muted-foreground max-w-md"> <p className="text-muted-foreground max-w-md">
@@ -623,7 +635,7 @@ function EmptyState({
</p> </p>
</div> </div>
<Button size="lg" className="gap-2" onClick={onCreateProject}> <Button size="lg" className="gap-2" onClick={onCreateProject}>
<Plus /> <HugeiconsIcon icon={PlusSignIcon} />
Create your first project Create your first project
</Button> </Button>
</div> </div>
+56 -46
View File
@@ -1,17 +1,18 @@
"use client"; "use client";
import {
ArrowDown,
Clock3,
Folder,
LayoutGrid,
List,
MoreHorizontal,
Plus,
Search,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ArrowHorizontalIcon,
Clock03Icon,
Folder03Icon,
GridViewIcon,
LeftToRightListDashIcon,
PlusSignIcon,
SearchIcon,
SortingNineOneIcon,
} from "@hugeicons/core-free-icons";
const projects = [ const projects = [
{ {
@@ -110,7 +111,7 @@ export default function ProjectsPage() {
href="/projects" href="/projects"
className="flex items-center gap-3 rounded-2xl bg-blue-50/50 px-4 py-3 text-sm font-medium text-[#00A3FF]" className="flex items-center gap-3 rounded-2xl bg-blue-50/50 px-4 py-3 text-sm font-medium text-[#00A3FF]"
> >
<LayoutGrid className="size-5" /> <HugeiconsIcon icon={GridViewIcon} className="size-5" />
Projects Projects
</a> </a>
<div className="px-4 pt-6 pb-2"> <div className="px-4 pt-6 pb-2">
@@ -122,14 +123,20 @@ export default function ProjectsPage() {
href="/projects" href="/projects"
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900" className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900"
> >
<Folder className="size-5 text-slate-400" /> <HugeiconsIcon
icon={Folder03Icon}
className="size-5 text-slate-400"
/>
Marketing Marketing
</a> </a>
<a <a
href="/projects" href="/projects"
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900" className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900"
> >
<Folder className="size-5 text-slate-400" /> <HugeiconsIcon
icon={Folder03Icon}
className="size-5 text-slate-400"
/>
Social media Social media
</a> </a>
</nav> </nav>
@@ -143,7 +150,10 @@ export default function ProjectsPage() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="group relative"> <div className="group relative">
<Search className="absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-slate-400" /> <HugeiconsIcon
icon={SearchIcon}
className="absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-slate-400"
/>
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search..."
@@ -157,18 +167,22 @@ export default function ProjectsPage() {
className="rounded-full bg-slate-100 p-2 text-slate-900" className="rounded-full bg-slate-100 p-2 text-slate-900"
aria-label="Grid view" aria-label="Grid view"
> >
<LayoutGrid className="size-4" aria-hidden="true"> <HugeiconsIcon
<title>Grid view</title> icon={GridViewIcon}
</LayoutGrid> className="size-4"
aria-hidden="true"
/>
</button> </button>
<button <button
type="button" type="button"
className="rounded-full p-2 text-slate-400 hover:text-slate-600" className="rounded-full p-2 text-slate-400 hover:text-slate-600"
aria-label="List view" aria-label="List view"
> >
<List className="size-4" aria-hidden="true"> <HugeiconsIcon
<title>List view</title> icon={LeftToRightListDashIcon}
</List> className="size-4"
aria-hidden="true"
/>
</button> </button>
</div> </div>
@@ -176,7 +190,7 @@ export default function ProjectsPage() {
type="button" type="button"
className="flex items-center gap-2 rounded-full bg-[#00A3FF] px-5 py-2.5 text-sm font-semibold text-white hover:bg-[#008BE0] active:scale-95" className="flex items-center gap-2 rounded-full bg-[#00A3FF] px-5 py-2.5 text-sm font-semibold text-white hover:bg-[#008BE0] active:scale-95"
> >
<Plus className="size-4" /> <HugeiconsIcon icon={PlusSignIcon} className="size-4" />
New project New project
</button> </button>
</div> </div>
@@ -210,7 +224,7 @@ export default function ProjectsPage() {
className="inline-flex items-center gap-1.5 rounded-lg p-1 text-sm font-medium text-slate-500 hover:text-slate-900" className="inline-flex items-center gap-1.5 rounded-lg p-1 text-sm font-medium text-slate-500 hover:text-slate-900"
> >
Name Name
<ArrowDown className="size-3.5" /> <HugeiconsIcon icon={SortingNineOneIcon} className="size-3.5" />
</button> </button>
</div> </div>
@@ -243,9 +257,11 @@ export default function ProjectsPage() {
className="rounded-full bg-white/90 p-2 text-slate-600 backdrop-blur-sm hover:bg-white hover:text-[#00A3FF]" className="rounded-full bg-white/90 p-2 text-slate-600 backdrop-blur-sm hover:bg-white hover:text-[#00A3FF]"
aria-label="Project menu" aria-label="Project menu"
> >
<MoreHorizontal className="size-4" aria-hidden="true"> <HugeiconsIcon
<title>Project menu</title> icon={ArrowHorizontalIcon}
</MoreHorizontal> className="size-4"
aria-hidden="true"
/>
</button> </button>
</div> </div>
@@ -268,7 +284,11 @@ export default function ProjectsPage() {
{name} {name}
</h3> </h3>
<div className="mt-2 flex items-center gap-2 text-slate-400"> <div className="mt-2 flex items-center gap-2 text-slate-400">
<Clock3 className="size-4" aria-hidden="true" /> <HugeiconsIcon
icon={Clock03Icon}
className="size-4"
aria-hidden="true"
/>
<p className="text-xs font-medium">{createdAtLabel}</p> <p className="text-xs font-medium">{createdAtLabel}</p>
</div> </div>
</div> </div>
@@ -281,42 +301,33 @@ export default function ProjectsPage() {
</main> </main>
<style jsx global>{` <style jsx global>{`
.project-card:hover .card-actions { .project-card:hover .card-actions
opacity: 1; opacity: 1;
} .checkbox-wrapper:checked + div
.checkbox-wrapper:checked + div {
border-color: var(--brand-blue); border-color: var(--brand-blue);
background-color: #eff6ff; background-color: #eff6ff;
} .controls-row.has-selection .controls-select
.controls-row.has-selection .controls-select {
opacity: 1; opacity: 1;
} .project-select-wrapper
.project-select-wrapper {
opacity: 0; opacity: 0;
} .project-card:hover .project-select-wrapper
.project-card:hover .project-select-wrapper {
opacity: 1; opacity: 1;
} .project-card:has(.project-select:checked) .project-select-wrapper
.project-card:has(.project-select:checked) .project-select-wrapper {
opacity: 1; opacity: 1;
} .multi-select .project-menu
.multi-select .project-menu {
opacity: 0 !important; opacity: 0 !important;
pointer-events: none; pointer-events: none;
} .custom-checkbox
.custom-checkbox {
appearance: none; appearance: none;
background-color: #fff; background-color: #fff;
border: 2px solid #e2e8f0; border: 2px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;
display: inline-grid; display: inline-grid;
place-content: center; place-content: center;
} .custom-checkbox:checked
.custom-checkbox:checked {
background-color: var(--brand-blue); background-color: var(--brand-blue);
border-color: var(--brand-blue); border-color: var(--brand-blue);
} .custom-checkbox:checked::after
.custom-checkbox:checked::after {
content: ""; content: "";
width: 10px; width: 10px;
height: 10px; height: 10px;
@@ -329,7 +340,6 @@ export default function ProjectsPage() {
86% 10%, 86% 10%,
40% 70% 40% 70%
); );
}
`}</style> `}</style>
</div> </div>
); );
+9 -5
View File
@@ -1,10 +1,11 @@
import { ExternalLink } from "lucide-react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { BasePage } from "@/app/base-page"; import { BasePage } from "@/app/base-page";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { SPONSORS, type Sponsor } from "@/constants/site-constants"; import { SPONSORS, type Sponsor } from "@/constants/site-constants";
import { HugeiconsIcon } from "@hugeicons/react";
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sponsors - OpenCut", title: "Sponsors - OpenCut",
@@ -25,9 +26,9 @@ export default function SponsorsPage() {
<h1 className="text-5xl font-bold tracking-tight md:text-6xl"> <h1 className="text-5xl font-bold tracking-tight md:text-6xl">
Sponsors Sponsors
</h1> </h1>
<p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed"> <p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed text-pretty">
Support OpenCut and help us build the future of free and open-source Support OpenCut and help us build the future of privacy-first video
video editing. editing.
</p> </p>
</div> </div>
<SponsorsGrid /> <SponsorsGrid />
@@ -67,7 +68,10 @@ function SponsorCard({ sponsor }: { sponsor: Sponsor }) {
<h3 className="text-xl font-semibold group-hover:underline"> <h3 className="text-xl font-semibold group-hover:underline">
{sponsor.name} {sponsor.name}
</h3> </h3>
<ExternalLink className="text-muted-foreground size-4" /> <HugeiconsIcon
icon={LinkSquare02Icon}
className="text-muted-foreground size-4"
/>
</div> </div>
<p className="text-muted-foreground">{sponsor.description}</p> <p className="text-muted-foreground">{sponsor.description}</p>
</div> </div>
@@ -1,4 +1,5 @@
import { Upload } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react";
import { UploadIcon } from "@hugeicons/core-free-icons";
interface MediaDragOverlayProps { interface MediaDragOverlayProps {
isVisible: boolean; isVisible: boolean;
@@ -34,7 +35,7 @@ export function MediaDragOverlay({
onClick={(event) => handleClick({ event })} onClick={(event) => handleClick({ event })}
> >
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Upload className="text-foreground size-10" /> <HugeiconsIcon icon={UploadIcon} className="text-foreground size-10" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -59,7 +59,7 @@ export function TabBar() {
type="button" type="button"
aria-label={tab.label} aria-label={tab.label}
className={cn( className={cn(
"flex cursor-pointer flex-col items-center gap-0.5", "flex cursor-pointer flex-col items-center gap-0.5 [&>svg]:size-4.5! opacity-100 hover:opacity-75",
activeTab === tabKey activeTab === tabKey
? "text-primary !opacity-100" ? "text-primary !opacity-100"
: "text-muted-foreground", : "text-muted-foreground",
@@ -72,7 +72,7 @@ export function TabBar() {
} }
}} }}
> >
<tab.icon className="size-[1.1rem]! opacity-100 hover:opacity-75" /> <tab.icon className=" " />
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
@@ -13,7 +13,8 @@ import { extractTimelineAudio } from "@/lib/media/mediabunny";
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants"; import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
import { TRANSCRIPTION_LANGUAGES } from "@/constants/transcription-constants"; import { TRANSCRIPTION_LANGUAGES } from "@/constants/transcription-constants";
import { Loader2 } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react";
import { Loading03Icon } from "@hugeicons/core-free-icons";
import type { import type {
TranscriptionLanguage, TranscriptionLanguage,
TranscriptionProgress, TranscriptionProgress,
@@ -21,6 +22,7 @@ import type {
import { transcriptionService } from "@/services/transcription"; import { transcriptionService } from "@/services/transcription";
import { decodeAudioToFloat32 } from "@/lib/media/audio"; import { decodeAudioToFloat32 } from "@/lib/media/audio";
import { buildCaptionChunks } from "@/lib/transcription/caption"; import { buildCaptionChunks } from "@/lib/transcription/caption";
import { Spinner } from "@/components/ui/spinner";
export function Captions() { export function Captions() {
const [selectedLanguage, setSelectedLanguage] = const [selectedLanguage, setSelectedLanguage] =
@@ -143,7 +145,7 @@ export function Captions() {
onClick={handleGenerateTranscript} onClick={handleGenerateTranscript}
disabled={isProcessing} disabled={isProcessing}
> >
{isProcessing && <Loader2 className="mr-1 animate-spin" />} {isProcessing && <Spinner className="mr-1" />}
{isProcessing ? processingStep : "Generate transcript"} {isProcessing ? processingStep : "Generate transcript"}
</Button> </Button>
</div> </div>
@@ -1,16 +1,5 @@
"use client"; "use client";
import type { LucideIcon } from "lucide-react";
import {
ArrowDown01,
CloudUpload,
Grid2X2,
ImageIcon,
List,
Loader2,
Music,
Video,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -44,6 +33,17 @@ import { useAssetsPanelStore } from "@/stores/assets-panel-store";
import type { MediaAsset } from "@/types/assets"; import type { MediaAsset } from "@/types/assets";
import type { CreateTimelineElement } from "@/types/timeline"; import type { CreateTimelineElement } from "@/types/timeline";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import {
CloudUploadIcon,
GridViewIcon,
LeftToRightListDashIcon,
SortingOneNineIcon,
Image02Icon,
Loading03Icon,
MusicNote03Icon,
Video01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
export function MediaView() { export function MediaView() {
const editor = useEditor(); const editor = useEditor();
@@ -207,9 +207,9 @@ export function MediaView() {
className="w-full" className="w-full"
> >
{isProcessing ? ( {isProcessing ? (
<Loader2 className="animate-spin" /> <HugeiconsIcon icon={Loading03Icon} className="animate-spin" />
) : ( ) : (
<CloudUpload /> <HugeiconsIcon icon={CloudUploadIcon} />
)} )}
<span>Upload</span> <span>Upload</span>
</Button> </Button>
@@ -229,12 +229,9 @@ export function MediaView() {
className="items-center justify-center" className="items-center justify-center"
> >
{mediaViewMode === "grid" ? ( {mediaViewMode === "grid" ? (
<List strokeWidth={1.5} className="!size-[1.05rem]" /> <HugeiconsIcon icon={LeftToRightListDashIcon} />
) : ( ) : (
<Grid2X2 <HugeiconsIcon icon={GridViewIcon} />
strokeWidth={1.5}
className="!size-[1.05rem]"
/>
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -255,10 +252,7 @@ export function MediaView() {
disabled={isProcessing} disabled={isProcessing}
className="items-center justify-center" className="items-center justify-center"
> >
<ArrowDown01 <HugeiconsIcon icon={SortingOneNineIcon} />
strokeWidth={1.5}
className="!size-[1.05rem]"
/>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger> </TooltipTrigger>
@@ -531,12 +525,12 @@ function MediaDurationLabel({ duration }: { duration?: number }) {
} }
function MediaTypePlaceholder({ function MediaTypePlaceholder({
icon: Icon, icon,
label, label,
duration, duration,
variant, variant,
}: { }: {
icon: LucideIcon; icon: IconSvgElement;
label: string; label: string;
duration?: number; duration?: number;
variant: "muted" | "bordered"; variant: "muted" | "bordered";
@@ -550,7 +544,7 @@ function MediaTypePlaceholder({
variant === "muted" ? "bg-muted/30" : "border", variant === "muted" ? "bg-muted/30" : "border",
)} )}
> >
<Icon className={iconClassName} aria-hidden="true" /> <HugeiconsIcon icon={icon} className={iconClassName} />
<span className="text-xs">{label}</span> <span className="text-xs">{label}</span>
<MediaDurationLabel duration={duration} /> <MediaDurationLabel duration={duration} />
</div> </div>
@@ -596,7 +590,10 @@ function MediaPreview({
/> />
{shouldShowVideoOverlay ? ( {shouldShowVideoOverlay ? (
<div className="absolute inset-0 flex items-center justify-center rounded bg-black/20"> <div className="absolute inset-0 flex items-center justify-center rounded bg-black/20">
<Video className="size-6 text-white drop-shadow-md" /> <HugeiconsIcon
icon={Video01Icon}
className="size-6 text-white drop-shadow-md"
/>
</div> </div>
) : null} ) : null}
{shouldShowDurationBadge ? ( {shouldShowDurationBadge ? (
@@ -608,7 +605,7 @@ function MediaPreview({
return ( return (
<MediaTypePlaceholder <MediaTypePlaceholder
icon={Video} icon={Video01Icon}
label="Video" label="Video"
duration={item.duration} duration={item.duration}
variant="muted" variant="muted"
@@ -619,7 +616,7 @@ function MediaPreview({
if (item.type === "audio") { if (item.type === "audio") {
return ( return (
<MediaTypePlaceholder <MediaTypePlaceholder
icon={Music} icon={MusicNote03Icon}
label="Audio" label="Audio"
duration={item.duration} duration={item.duration}
variant="bordered" variant="bordered"
@@ -628,7 +625,7 @@ function MediaPreview({
} }
return ( return (
<MediaTypePlaceholder icon={ImageIcon} label="Unknown" variant="muted" /> <MediaTypePlaceholder icon={Image02Icon} label="Unknown" variant="muted" />
); );
} }
@@ -1,6 +1,5 @@
"use client"; "use client";
import { PipetteIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { memo, useCallback, useMemo } from "react"; import { memo, useCallback, useMemo } from "react";
import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view"; import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
@@ -31,6 +30,8 @@ import {
PropertyItemLabel, PropertyItemLabel,
PropertyItemValue, PropertyItemValue,
} from "../../properties-panel/property-item"; } from "../../properties-panel/property-item";
import { HugeiconsIcon } from "@hugeicons/react";
import { DropperIcon } from "@hugeicons/core-free-icons";
export function SettingsView() { export function SettingsView() {
return <ProjectSettingsTabs />; return <ProjectSettingsTabs />;
@@ -355,7 +356,7 @@ function BackgroundView() {
<PropertyGroup title="Colors" defaultExpanded={false}> <PropertyGroup title="Colors" defaultExpanded={false}>
<div className="grid w-full grid-cols-4 gap-2"> <div className="grid w-full grid-cols-4 gap-2">
<div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border"> <div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
<PipetteIcon className="size-4" /> <HugeiconsIcon icon={DropperIcon} className="size-4" />
</div> </div>
<BackgroundPreviews <BackgroundPreviews
backgrounds={colors} backgrounds={colors}
@@ -1,12 +1,5 @@
"use client"; "use client";
import {
HeartIcon,
ListFilter,
PauseIcon,
PlayIcon,
PlusIcon,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -33,6 +26,14 @@ import { useSoundSearch } from "@/hooks/use-sound-search";
import { useSoundsStore } from "@/stores/sounds-store"; import { useSoundsStore } from "@/stores/sounds-store";
import type { SavedSound, SoundEffect } from "@/types/sounds"; import type { SavedSound, SoundEffect } from "@/types/sounds";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import {
FavouriteIcon,
FilterMailIcon,
PauseIcon,
PlayIcon,
PlusSignIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export function SoundsView() { export function SoundsView() {
return ( return (
@@ -250,7 +251,7 @@ function SoundEffectsView() {
size="icon" size="icon"
className={cn(showCommercialOnly && "text-primary")} className={cn(showCommercialOnly && "text-primary")}
> >
<ListFilter /> <HugeiconsIcon icon={FilterMailIcon} />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
@@ -406,9 +407,9 @@ function SavedSoundsView() {
if (savedSounds.length === 0) { if (savedSounds.length === 0) {
return ( return (
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4"> <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
<HeartIcon <HugeiconsIcon
icon={FavouriteIcon}
className="text-muted-foreground size-10" className="text-muted-foreground size-10"
strokeWidth={1.5}
/> />
<div className="flex flex-col gap-2 text-center"> <div className="flex flex-col gap-2 text-center">
<p className="text-lg font-medium">No saved sounds</p> <p className="text-lg font-medium">No saved sounds</p>
@@ -527,9 +528,9 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
<div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md"> <div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md">
<div className="from-primary/20 absolute inset-0 bg-gradient-to-br to-transparent" /> <div className="from-primary/20 absolute inset-0 bg-gradient-to-br to-transparent" />
{isPlaying ? ( {isPlaying ? (
<PauseIcon className="size-5" /> <HugeiconsIcon icon={PauseIcon} className="size-5" />
) : ( ) : (
<PlayIcon className="size-5" /> <HugeiconsIcon icon={PlayIcon} className="size-5" />
)} )}
</div> </div>
@@ -549,7 +550,7 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
onClick={handleAddToTimeline} onClick={handleAddToTimeline}
title="Add to timeline" title="Add to timeline"
> >
<PlusIcon /> <HugeiconsIcon icon={PlusSignIcon} />
</Button> </Button>
<Button <Button
variant="text" variant="text"
@@ -562,7 +563,10 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
onClick={handleSaveClick} onClick={handleSaveClick}
title={isSaved ? "Remove from saved" : "Save sound"} title={isSaved ? "Remove from saved" : "Save sound"}
> >
<HeartIcon className={`${isSaved ? "fill-current" : ""}`} /> <HugeiconsIcon
icon={FavouriteIcon}
className={`${isSaved ? "fill-current" : ""}`}
/>
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,16 +1,5 @@
"use client"; "use client";
import {
ArrowRight,
Clock,
Grid3X3,
Hash,
Loader2,
Smile,
Sparkles,
StickerIcon,
X,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -37,6 +26,17 @@ import {
import { useStickersStore } from "@/stores/stickers-store"; import { useStickersStore } from "@/stores/stickers-store";
import type { StickerCategory } from "@/types/stickers"; import type { StickerCategory } from "@/types/stickers";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import {
ArrowRightIcon,
HappyIcon,
ClockIcon,
LayoutGridIcon,
MultiplicationSignIcon,
SparklesIcon,
HashtagIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Spinner } from "@/components/ui/spinner";
function isStickerCategory(value: string): value is StickerCategory { function isStickerCategory(value: string): value is StickerCategory {
return STICKER_CATEGORIES.includes(value as StickerCategory); return STICKER_CATEGORIES.includes(value as StickerCategory);
@@ -57,25 +57,25 @@ export function StickersView() {
{ {
value: "all", value: "all",
label: "All", label: "All",
icon: <Grid3X3 className="size-3" />, icon: <HugeiconsIcon icon={LayoutGridIcon} className="size-3" />,
content: <StickersContentView category="all" />, content: <StickersContentView category="all" />,
}, },
{ {
value: "general", value: "general",
label: "Icons", label: "Icons",
icon: <Sparkles className="size-3" />, icon: <HugeiconsIcon icon={SparklesIcon} className="size-3" />,
content: <StickersContentView category="general" />, content: <StickersContentView category="general" />,
}, },
{ {
value: "brands", value: "brands",
label: "Brands", label: "Brands",
icon: <Hash className="size-3" />, icon: <HugeiconsIcon icon={HashtagIcon} className="size-3" />,
content: <StickersContentView category="brands" />, content: <StickersContentView category="brands" />,
}, },
{ {
value: "emoji", value: "emoji",
label: "Emoji", label: "Emoji",
icon: <Smile className="size-3" />, icon: <HugeiconsIcon icon={HappyIcon} className="size-3" />,
content: <StickersContentView category="emoji" />, content: <StickersContentView category="emoji" />,
}, },
]} ]}
@@ -150,9 +150,9 @@ function CollectionGrid({
function EmptyView({ message }: { message: string }) { function EmptyView({ message }: { message: string }) {
return ( return (
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4"> <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
<StickerIcon <HugeiconsIcon
icon={HappyIcon}
className="text-muted-foreground size-10" className="text-muted-foreground size-10"
strokeWidth={1.5}
/> />
<div className="flex flex-col gap-2 text-center"> <div className="flex flex-col gap-2 text-center">
<p className="text-lg font-medium">No stickers found</p> <p className="text-lg font-medium">No stickers found</p>
@@ -334,7 +334,10 @@ function StickersContentView({ category }: { category: StickerCategory }) {
{recentStickers.length > 0 && viewMode === "browse" && ( {recentStickers.length > 0 && viewMode === "browse" && (
<div className="h-full"> <div className="h-full">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<Clock className="text-muted-foreground size-4" /> <HugeiconsIcon
icon={ClockIcon}
className="text-muted-foreground size-4"
/>
<span className="text-sm font-medium">Recent</span> <span className="text-sm font-medium">Recent</span>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@@ -344,7 +347,10 @@ function StickersContentView({ category }: { category: StickerCategory }) {
onClick={clearRecentStickers} onClick={clearRecentStickers}
className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0" className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0"
> >
<X className="text-muted-foreground size-3" /> <HugeiconsIcon
icon={MultiplicationSignIcon}
className="text-muted-foreground size-3"
/>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -366,7 +372,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
<div className="h-full"> <div className="h-full">
{isLoadingCollection ? ( {isLoadingCollection ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" /> <Spinner className="text-muted-foreground size-6" />
</div> </div>
) : showCollectionItems ? ( ) : showCollectionItems ? (
<StickerGrid <StickerGrid
@@ -376,7 +382,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
/> />
) : ( ) : (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" /> <Spinner className="text-muted-foreground size-6" />
</div> </div>
)} )}
</div> </div>
@@ -386,7 +392,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
<div className="h-full"> <div className="h-full">
{isSearching ? ( {isSearching ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" /> <Spinner className="text-muted-foreground size-6" />
</div> </div>
) : searchResults?.icons.length ? ( ) : searchResults?.icons.length ? (
<> <>
@@ -433,7 +439,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
<div className="h-full space-y-4"> <div className="h-full space-y-4">
{isLoadingCollections ? ( {isLoadingCollections ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" /> <Spinner className="text-muted-foreground size-6" />
</div> </div>
) : ( ) : (
<> <>
@@ -489,7 +495,7 @@ function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
<p className="font-medium">{title}</p> <p className="font-medium">{title}</p>
<p className="text-muted-foreground text-xs">{subtitle}</p> <p className="text-muted-foreground text-xs">{subtitle}</p>
</div> </div>
<ArrowRight /> <HugeiconsIcon icon={ArrowRightIcon} className="size-4" />
</Button> </Button>
); );
} }
@@ -593,7 +599,7 @@ function StickerItem({
/> />
{isAdding && ( {isAdding && (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60"> <div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
<Loader2 className="size-6 animate-spin text-white" /> <Spinner className="size-6 text-white" />
</div> </div>
)} )}
</div> </div>
@@ -52,7 +52,7 @@ export function DraggableItem({
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
const dragRef = useRef<HTMLDivElement>(null); const dragRef = useRef<HTMLDivElement>(null);
const editor = useEditor(); const editor = useEditor();
const highlightClassName = "ring-2 ring-primary rounded-sm bg-primary/10"; const highlightClassName = `ring-2 ring-primary bg-primary/10 ${isRounded ? "rounded-sm" : ""}`;
const handleAddToTimeline = () => { const handleAddToTimeline = () => {
onAddToTimeline?.({ currentTime: editor.playback.getCurrentTime() }); onAddToTimeline?.({ currentTime: editor.playback.getCurrentTime() });
@@ -111,7 +111,7 @@ export function DraggableItem({
ratio={aspectRatio} ratio={aspectRatio}
className={cn( className={cn(
"bg-panel-accent relative overflow-hidden", "bg-panel-accent relative overflow-hidden",
isRounded && "rounded-md", isRounded && "rounded-sm",
isDraggable && "[&::-webkit-drag-ghost]:opacity-0", isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
)} )}
draggable={isDraggable} draggable={isDraggable}
@@ -2,7 +2,6 @@
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { KeyboardShortcutsHelp } from "../keyboard-shortcuts-help";
import { useState } from "react"; import { useState } from "react";
import { import {
DropdownMenu, DropdownMenu,
@@ -21,7 +20,14 @@ import { ThemeToggle } from "../theme-toggle";
import { SOCIAL_LINKS } from "@/constants/site-constants"; import { SOCIAL_LINKS } from "@/constants/site-constants";
import { toast } from "sonner"; import { toast } from "sonner";
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import { OcLeftArrowIcon, OcPencilIcon, OcTrashIcon } from "@opencut/ui/icons"; import {
ArrowLeft02Icon,
Edit03Icon,
Delete02Icon,
Keyboard,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { ShortcutsDialog } from "./shortcuts-dialog";
export function EditorHeader() { export function EditorHeader() {
return ( return (
@@ -30,7 +36,6 @@ export function EditorHeader() {
<ProjectDropdown /> <ProjectDropdown />
</div> </div>
<nav className="flex items-center gap-2"> <nav className="flex items-center gap-2">
<KeyboardShortcutsHelp />
<ExportButton /> <ExportButton />
<ThemeToggle /> <ThemeToggle />
</nav> </nav>
@@ -39,8 +44,9 @@ export function EditorHeader() {
} }
function ProjectDropdown() { function ProjectDropdown() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [openDialog, setOpenDialog] = useState<
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); "delete" | "rename" | "shortcuts" | null
>(null);
const [isExiting, setIsExiting] = useState(false); const [isExiting, setIsExiting] = useState(false);
const router = useRouter(); const router = useRouter();
const editor = useEditor(); const editor = useEditor();
@@ -78,7 +84,7 @@ function ProjectDropdown() {
error instanceof Error ? error.message : "Please try again", error instanceof Error ? error.message : "Please try again",
}); });
} finally { } finally {
setIsRenameDialogOpen(false); setOpenDialog(null);
} }
} }
}; };
@@ -94,7 +100,7 @@ function ProjectDropdown() {
error instanceof Error ? error.message : "Please try again", error instanceof Error ? error.message : "Please try again",
}); });
} finally { } finally {
setIsDeleteDialogOpen(false); setOpenDialog(null);
} }
} }
}; };
@@ -119,25 +125,32 @@ function ProjectDropdown() {
onClick={handleExit} onClick={handleExit}
disabled={isExiting} disabled={isExiting}
> >
<OcLeftArrowIcon className="size-4" /> <HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
Projects Exit project
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="flex items-center gap-1.5" className="flex items-center gap-1.5"
onClick={() => setIsRenameDialogOpen(true)} onClick={() => setOpenDialog("rename")}
> >
<OcPencilIcon className="size-4" /> <HugeiconsIcon icon={Edit03Icon} className="size-4" />
Rename project Rename project
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
variant="destructive" variant="destructive"
className="flex items-center gap-1.5" className="flex items-center gap-1.5"
onClick={() => setIsDeleteDialogOpen(true)} onClick={() => setOpenDialog("delete")}
> >
<OcTrashIcon className="size-4" /> <HugeiconsIcon icon={Delete02Icon} className="size-4" />
Delete Project Delete project
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem
className="flex items-center gap-1.5"
onClick={() => setOpenDialog("shortcuts")}
>
<HugeiconsIcon icon={Keyboard} className="size-4" />
Keyboard shortcuts
</DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link
href={SOCIAL_LINKS.discord} href={SOCIAL_LINKS.discord}
@@ -152,17 +165,21 @@ function ProjectDropdown() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<RenameProjectDialog <RenameProjectDialog
isOpen={isRenameDialogOpen} isOpen={openDialog === "rename"}
onOpenChange={setIsRenameDialogOpen} onOpenChange={(isOpen) => setOpenDialog(isOpen ? "rename" : null)}
onConfirm={(newName) => handleSaveProjectName(newName)} onConfirm={(newName) => handleSaveProjectName(newName)}
projectName={activeProject?.metadata.name || ""} projectName={activeProject?.metadata.name || ""}
/> />
<DeleteProjectDialog <DeleteProjectDialog
isOpen={isDeleteDialogOpen} isOpen={openDialog === "delete"}
onOpenChange={setIsDeleteDialogOpen} onOpenChange={(isOpen) => setOpenDialog(isOpen ? "delete" : null)}
onConfirm={handleDeleteProject} onConfirm={handleDeleteProject}
projectName={activeProject?.metadata.name || ""} projectName={activeProject?.metadata.name || ""}
/> />
<ShortcutsDialog
isOpen={openDialog === "shortcuts"}
onOpenChange={(isOpen) => setOpenDialog(isOpen ? "shortcuts" : null)}
/>
</> </>
); );
} }
@@ -1,7 +1,8 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { OcTransitionUpIcon } from "@opencut/ui/icons"; import { TransitionTopIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
@@ -53,7 +54,7 @@ export function ExportButton() {
}} }}
> >
<div className="relative flex items-center gap-1.5 rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]"> <div className="relative flex items-center gap-1.5 rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
<OcTransitionUpIcon className="z-50" /> <HugeiconsIcon icon={TransitionTopIcon} className="z-50 size-4" />
<span className="z-50 text-[0.875rem]">Export</span> <span className="z-50 text-[0.875rem]">Export</span>
<div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.8rem] bg-linear-to-t from-white/0 to-white/50"> <div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.8rem] bg-linear-to-t from-white/0 to-white/50">
<div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div> <div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div>
@@ -4,7 +4,8 @@ import { ScrollArea } from "../../ui/scroll-area";
import { AudioProperties } from "./audio-properties"; import { AudioProperties } from "./audio-properties";
import { VideoProperties } from "./video-properties"; import { VideoProperties } from "./video-properties";
import { TextProperties } from "./text-properties"; import { TextProperties } from "./text-properties";
import { SquareSlashIcon } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react";
import { Settings05Icon } from "@hugeicons/core-free-icons";
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
@@ -29,12 +30,12 @@ export function PropertiesPanel() {
); );
} }
if (element.type === "audio") { if (element.type === "audio") {
return <AudioProperties key={element.id} element={element} />; return <AudioProperties key={element.id} _element={element} />;
} }
if (element.type === "video" || element.type === "image") { if (element.type === "video" || element.type === "image") {
return ( return (
<div key={element.id}> <div key={element.id}>
<VideoProperties element={element} /> <VideoProperties _element={element} />
</div> </div>
); );
} }
@@ -51,12 +52,13 @@ export function PropertiesPanel() {
function EmptyView() { function EmptyView() {
return ( return (
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4"> <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
<SquareSlashIcon <HugeiconsIcon
className="text-muted-foreground size-10" icon={Settings05Icon}
strokeWidth={1.5} className="text-muted-foreground/75 size-10"
strokeWidth={1}
/> />
<div className="flex flex-col gap-2 text-center"> <div className="flex flex-col gap-2 text-center">
<p className="text-lg font-medium">Its empty here</p> <p className="text-lg font-medium">It's empty here</p>
<p className="text-muted-foreground text-sm text-balance"> <p className="text-muted-foreground text-sm text-balance">
Click an element on the timeline to edit its properties Click an element on the timeline to edit its properties
</p> </p>
@@ -1,6 +1,7 @@
import { ChevronDown } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import { HugeiconsIcon } from "@hugeicons/react";
import { ArrowDownIcon } from "@hugeicons/core-free-icons";
interface PropertyItemProps { interface PropertyItemProps {
direction?: "row" | "column"; direction?: "row" | "column";
@@ -79,7 +80,10 @@ export function PropertyGroup({
<PropertyItemLabel className={cn("cursor-pointer", titleClassName)}> <PropertyItemLabel className={cn("cursor-pointer", titleClassName)}>
{title} {title}
</PropertyItemLabel> </PropertyItemLabel>
<ChevronDown className={cn("size-3", !isExpanded && "-rotate-90")} /> <HugeiconsIcon
icon={ArrowDownIcon}
className={cn("size-3", !isExpanded && "-rotate-90")}
/>
</button> </button>
{isExpanded && <PropertyItemValue>{children}</PropertyItemValue>} {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
</PropertyItem> </PropertyItem>
@@ -21,7 +21,8 @@ import { ColorPicker } from "@/components/ui/color-picker";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import { capitalizeFirstLetter, uppercase } from "@/utils/strings"; import { capitalizeFirstLetter, uppercase } from "@/utils/strings";
import { clamp } from "@/utils/math"; import { clamp } from "@/utils/math";
import { Grid2x2 } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react";
import { LayoutGridIcon } from "@hugeicons/core-free-icons";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -386,7 +387,7 @@ export function TextProperties({
} }
className="bg-panel-accent size-9 overflow-hidden rounded-full p-0" className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
> >
<Grid2x2 <HugeiconsIcon icon={LayoutGridIcon}
className={cn( className={cn(
"text-foreground", "text-foreground",
element.backgroundColor === "transparent" && element.backgroundColor === "transparent" &&
@@ -8,7 +8,7 @@ import {
useKeyboardShortcutsHelp, useKeyboardShortcutsHelp,
} from "@/hooks/use-keyboard-shortcuts-help"; } from "@/hooks/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/stores/keybindings-store"; import { useKeybindingsStore } from "@/stores/keybindings-store";
import { Button } from "./ui/button"; import { Button } from "../ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -16,11 +16,15 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, } from "../ui/dialog";
} from "./ui/dialog";
export function KeyboardShortcutsHelp() { export function ShortcutsDialog({
const [open, setOpen] = useState(false); isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) {
const [recordingShortcut, setRecordingShortcut] = const [recordingShortcut, setRecordingShortcut] =
useState<KeyboardShortcut | null>(null); useState<KeyboardShortcut | null>(null);
@@ -35,7 +39,6 @@ export function KeyboardShortcutsHelp() {
isRecording, isRecording,
} = useKeybindingsStore(); } = useKeybindingsStore();
// Get shortcuts from centralized hook
const { shortcuts } = useKeyboardShortcutsHelp(); const { shortcuts } = useKeyboardShortcutsHelp();
const categories = Array.from(new Set(shortcuts.map((s) => s.category))); const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
@@ -49,7 +52,6 @@ export function KeyboardShortcutsHelp() {
const keyString = getKeybindingString(e); const keyString = getKeybindingString(e);
if (keyString) { if (keyString) {
// Auto-save the new keybinding
const conflict = validateKeybinding( const conflict = validateKeybinding(
keyString, keyString,
recordingShortcut.action, recordingShortcut.action,
@@ -62,11 +64,11 @@ export function KeyboardShortcutsHelp() {
return; return;
} }
// Remove old keybindings for this action
const oldKeys = getKeybindingsForAction(recordingShortcut.action); const oldKeys = getKeybindingsForAction(recordingShortcut.action);
oldKeys.forEach((key) => removeKeybinding(key)); for (const key of oldKeys) {
removeKeybinding(key);
}
// Add new keybinding
updateKeybinding(keyString, recordingShortcut.action); updateKeybinding(keyString, recordingShortcut.action);
setIsRecording(false); setIsRecording(false);
@@ -103,18 +105,12 @@ export function KeyboardShortcutsHelp() {
}; };
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button variant="text" size="sm" className="gap-2">
<Keyboard />
Shortcuts
</Button>
</DialogTrigger>
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0"> <DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
<DialogHeader className="flex-shrink-0 p-6 pb-0"> <DialogHeader className="flex-shrink-0 gap-2 p-6 pb-0">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Keyboard className="size-5" /> <Keyboard className="size-5" />
Keyboard Shortcuts Keyboard shortcuts
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Speed up your video editing workflow with these keyboard shortcuts. Speed up your video editing workflow with these keyboard shortcuts.
@@ -123,13 +119,13 @@ export function KeyboardShortcutsHelp() {
</DialogHeader> </DialogHeader>
<div className="scrollbar-thin flex-grow overflow-y-auto"> <div className="scrollbar-thin flex-grow overflow-y-auto">
<div className="space-y-6 p-6 pt-2"> <div className="flex flex-col gap-6 p-6 pt-2">
{categories.map((category) => ( {categories.map((category) => (
<div key={category} className="flex flex-col gap-1"> <div key={category} className="flex flex-col gap-1">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase"> <h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
{category} {category}
</h3> </h3>
<div className="space-y-1"> <div className="flex flex-col gap-1">
{shortcuts {shortcuts
.filter((shortcut) => shortcut.category === category) .filter((shortcut) => shortcut.category === category)
.map((shortcut) => ( .map((shortcut) => (
@@ -139,7 +135,7 @@ export function KeyboardShortcutsHelp() {
isRecording={ isRecording={
shortcut.action === recordingShortcut?.action shortcut.action === recordingShortcut?.action
} }
onStartRecording={handleStartRecording} onStartRecording={() => handleStartRecording(shortcut)}
/> />
))} ))}
</div> </div>
@@ -164,9 +160,8 @@ function ShortcutItem({
}: { }: {
shortcut: KeyboardShortcut; shortcut: KeyboardShortcut;
isRecording: boolean; isRecording: boolean;
onStartRecording: (shortcut: KeyboardShortcut) => void; onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
}) { }) {
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
const displayKeys = shortcut.keys.filter((key: string) => { const displayKeys = shortcut.keys.filter((key: string) => {
if ( if (
key.includes("Cmd") && key.includes("Cmd") &&
@@ -195,7 +190,7 @@ function ShortcutItem({
<EditableShortcutKey <EditableShortcutKey
key={keyId} key={keyId}
isRecording={isRecording} isRecording={isRecording}
onStartRecording={() => onStartRecording(shortcut)} onStartRecording={() => onStartRecording({ shortcut })}
> >
{keyPart} {keyPart}
</EditableShortcutKey> </EditableShortcutKey>
@@ -1,7 +1,8 @@
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { Bookmark } from "lucide-react"; import { Bookmark02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
interface TimelineBookmarksRowProps { interface TimelineBookmarksRowProps {
zoomLevel: number; zoomLevel: number;
@@ -84,10 +85,10 @@ export function TimelineBookmark({
onClick={(event) => handleBookmarkClick({ event })} onClick={(event) => handleBookmarkClick({ event })}
> >
<div className="text-primary absolute top-[-1px] left-[-5px]"> <div className="text-primary absolute top-[-1px] left-[-5px]">
<Bookmark <HugeiconsIcon
icon={Bookmark02Icon}
aria-hidden="true" aria-hidden="true"
className="fill-primary size-3" className="fill-primary size-3"
focusable="false"
/> />
</div> </div>
</button> </button>
@@ -2,14 +2,13 @@
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { import {
Eye, Delete02Icon,
EyeOff, ViewIcon,
VolumeOff, ViewOffSlashIcon,
Volume2, VolumeHighIcon,
type LucideIcon, VolumeOffIcon,
EyeIcon, } from "@hugeicons/core-free-icons";
Trash2, import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
} from "lucide-react";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
@@ -133,6 +132,7 @@ export function Timeline() {
selectionBox, selectionBox,
handleMouseDown: handleSelectionMouseDown, handleMouseDown: handleSelectionMouseDown,
isSelecting, isSelecting,
shouldIgnoreClick,
} = useSelectionBox({ } = useSelectionBox({
containerRef: tracksContainerRef, containerRef: tracksContainerRef,
onSelectionComplete: (elements) => { onSelectionComplete: (elements) => {
@@ -267,17 +267,12 @@ export function Timeline() {
}} }}
> >
<div className="flex min-w-0 flex-1 items-center justify-end gap-2"> <div className="flex min-w-0 flex-1 items-center justify-end gap-2">
{/* Debug main track */}
{/* {isMainTrack(track) && (
<div className="size-2 rounded-full bg-red-500" />
)} */}
{canTracktHaveAudio(track) && ( {canTracktHaveAudio(track) && (
<TrackToggleIcon <TrackToggleIcon
isOff={track.muted} isOff={track.muted}
icons={{ icons={{
on: Volume2, on: VolumeHighIcon,
off: VolumeOff, off: VolumeOffIcon,
}} }}
onClick={() => onClick={() =>
editor.timeline.toggleTrackMute({ editor.timeline.toggleTrackMute({
@@ -291,8 +286,8 @@ export function Timeline() {
<TrackToggleIcon <TrackToggleIcon
isOff={track.hidden} isOff={track.hidden}
icons={{ icons={{
on: Eye, on: ViewIcon,
off: EyeOff, off: ViewOffSlashIcon,
}} }}
onClick={() => onClick={() =>
editor.timeline.toggleTrackVisibility({ editor.timeline.toggleTrackVisibility({
@@ -397,6 +392,7 @@ export function Timeline() {
onElementMouseDown={handleElementMouseDown} onElementMouseDown={handleElementMouseDown}
onElementClick={handleElementClick} onElementClick={handleElementClick}
onTrackMouseDown={handleSelectionMouseDown} onTrackMouseDown={handleSelectionMouseDown}
shouldIgnoreClick={shouldIgnoreClick}
/> />
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
@@ -409,7 +405,7 @@ export function Timeline() {
}); });
}} }}
> >
<Volume2 /> <HugeiconsIcon icon={VolumeHighIcon} />
<span> <span>
{canTracktHaveAudio(track) && track.muted {canTracktHaveAudio(track) && track.muted
? "Unmute track" ? "Unmute track"
@@ -424,7 +420,7 @@ export function Timeline() {
}); });
}} }}
> >
<EyeIcon /> <HugeiconsIcon icon={ViewIcon} />
<span> <span>
{canTrackBeHidden(track) && track.hidden {canTrackBeHidden(track) && track.hidden
? "Show track" ? "Show track"
@@ -440,7 +436,7 @@ export function Timeline() {
}} }}
variant="destructive" variant="destructive"
> >
<Trash2 /> <HugeiconsIcon icon={Delete02Icon} />
Delete track Delete track
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
@@ -467,20 +463,22 @@ function TrackToggleIcon({
}: { }: {
isOff: boolean; isOff: boolean;
icons: { icons: {
on: LucideIcon; on: IconSvgElement;
off: LucideIcon; off: IconSvgElement;
}; };
onClick: () => void; onClick: () => void;
}) { }) {
return ( return (
<> <>
{isOff ? ( {isOff ? (
<icons.off <HugeiconsIcon
icon={icons.off}
className="text-destructive size-4 cursor-pointer" className="text-destructive size-4 cursor-pointer"
onClick={onClick} onClick={onClick}
/> />
) : ( ) : (
<icons.on <HugeiconsIcon
icon={icons.on}
className="text-muted-foreground size-4 cursor-pointer" className="text-muted-foreground size-4 cursor-pointer"
onClick={onClick} onClick={onClick}
/> />
@@ -213,10 +213,6 @@ export function TimelineElement({
Duplicate {element.type === "text" ? "text" : "clip"} Duplicate {element.type === "text" ? "text" : "clip"}
</ContextMenuItem> </ContextMenuItem>
)} )}
<ContextMenuItem disabled>
<ArrowUpDown className="mr-2 size-4" />
Move to track (Coming soon)
</ContextMenuItem>
{selectedElements.length === 1 && hasMediaId(element) && ( {selectedElements.length === 1 && hasMediaId(element) && (
<> <>
<ContextMenuItem <ContextMenuItem
@@ -289,7 +285,6 @@ function ElementInner({
className="absolute inset-0 size-full cursor-pointer" className="absolute inset-0 size-full cursor-pointer"
onClick={(e) => onElementClick(e, element)} onClick={(e) => onElementClick(e, element)}
onMouseDown={(e) => onElementMouseDown(e, element)} onMouseDown={(e) => onElementMouseDown(e, element)}
onContextMenu={(e) => onElementMouseDown(e, element)}
> >
<div className="absolute inset-0 flex h-full items-center"> <div className="absolute inset-0 flex h-full items-center">
<ElementContent <ElementContent
@@ -300,7 +295,9 @@ function ElementInner({
/> />
</div> </div>
{(hasAudio ? isMuted : canElementBeHidden(element) && element.hidden) && ( {(hasAudio
? isMuted
: canElementBeHidden(element) && element.hidden) && (
<div className="bg-opacity-50 pointer-events-none absolute inset-0 flex items-center justify-center bg-black"> <div className="bg-opacity-50 pointer-events-none absolute inset-0 flex items-center justify-center bg-black">
{hasAudio ? ( {hasAudio ? (
<VolumeX className="size-6 text-white" /> <VolumeX className="size-6 text-white" />
@@ -7,21 +7,8 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Pause,
Play,
SkipBack, SkipBack,
Bookmark,
Magnet,
Link,
ZoomOut,
ZoomIn,
Copy,
Trash2,
Snowflake,
ArrowLeftToLine,
ArrowRightToLine,
SplitSquareHorizontal, SplitSquareHorizontal,
Scissors,
LayersIcon, LayersIcon,
} from "lucide-react"; } from "lucide-react";
import { import {
@@ -38,6 +25,28 @@ import { ScenesView } from "../scenes-view";
import { type TAction, invokeAction } from "@/lib/actions"; import { type TAction, invokeAction } from "@/lib/actions";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import { useTimelineStore } from "@/stores/timeline-store"; import { useTimelineStore } from "@/stores/timeline-store";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Bookmark02Icon,
Delete02Icon,
SnowIcon,
ScissorIcon,
MagnetIcon,
Link04Icon,
SearchAddIcon,
SearchMinusIcon,
PauseIcon,
PlayIcon,
Copy01Icon,
ArrowLeft03Icon,
ArrowRight03Icon,
LayoutAlignLeftIcon,
LayoutAlignRightIcon,
AlignLeftIcon,
AlignRightIcon,
Layers01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export function TimelineToolbar({ export function TimelineToolbar({
zoomLevel, zoomLevel,
@@ -60,6 +69,7 @@ export function TimelineToolbar({
}; };
return ( return (
<ScrollArea className="scrollbar-hidden">
<div className="flex h-10 items-center justify-between border-b px-2 py-1"> <div className="flex h-10 items-center justify-between border-b px-2 py-1">
<ToolbarLeftSection /> <ToolbarLeftSection />
@@ -72,6 +82,7 @@ export function TimelineToolbar({
onZoom={handleZoom} onZoom={handleZoom}
/> />
</div> </div>
</ScrollArea>
); );
} }
@@ -96,7 +107,13 @@ function ToolbarLeftSection() {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TooltipProvider delayDuration={500}> <TooltipProvider delayDuration={500}>
<ToolbarButton <ToolbarButton
icon={isPlaying ? <Pause /> : <Play />} icon={
isPlaying ? (
<HugeiconsIcon icon={PauseIcon} />
) : (
<HugeiconsIcon icon={PlayIcon} />
)
}
tooltip={isPlaying ? "Pause" : "Play"} tooltip={isPlaying ? "Pause" : "Play"}
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "toggle-play", event }) handleAction({ action: "toggle-play", event })
@@ -109,14 +126,14 @@ function ToolbarLeftSection() {
onClick={({ event }) => handleAction({ action: "goto-start", event })} onClick={({ event }) => handleAction({ action: "goto-start", event })}
/> />
<div className="bg-border mx-2 h-10 w-px" /> <div className="bg-border mx-1 h-6 w-px" />
<TimeDisplay /> <TimeDisplay />
<div className="bg-border mx-1 h-10 w-px" /> <div className="bg-border mx-1 h-6 w-px" />
<ToolbarButton <ToolbarButton
icon={<Scissors />} icon={<HugeiconsIcon icon={ScissorIcon} />}
tooltip="Split element" tooltip="Split element"
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "split-selected", event }) handleAction({ action: "split-selected", event })
@@ -124,18 +141,18 @@ function ToolbarLeftSection() {
/> />
<ToolbarButton <ToolbarButton
icon={<ArrowLeftToLine />} icon={<HugeiconsIcon icon={AlignLeftIcon} />}
tooltip="Split and keep left" tooltip="Split left"
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "split-selected-left", event }) handleAction({ action: "split-selected-right", event })
} }
/> />
<ToolbarButton <ToolbarButton
icon={<ArrowRightToLine />} icon={<HugeiconsIcon icon={AlignRightIcon} />}
tooltip="Split and keep right" tooltip="Split right"
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "split-selected-right", event }) handleAction({ action: "split-selected-left", event })
} }
/> />
@@ -147,7 +164,7 @@ function ToolbarLeftSection() {
/> />
<ToolbarButton <ToolbarButton
icon={<Copy />} icon={<HugeiconsIcon icon={Copy01Icon} />}
tooltip="Duplicate element" tooltip="Duplicate element"
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "duplicate-selected", event }) handleAction({ action: "duplicate-selected", event })
@@ -155,14 +172,14 @@ function ToolbarLeftSection() {
/> />
<ToolbarButton <ToolbarButton
icon={<Snowflake />} icon={<HugeiconsIcon icon={SnowIcon} />}
tooltip="Coming soon" /* freeze frame */ tooltip="Coming soon" /* freeze frame */
disabled={true} disabled={true}
onClick={({ event: _event }) => {}} onClick={({ event: _event }) => {}}
/> />
<ToolbarButton <ToolbarButton
icon={<Trash2 />} icon={<HugeiconsIcon icon={Delete02Icon} />}
tooltip="Delete element" tooltip="Delete element"
onClick={({ event }) => onClick={({ event }) =>
handleAction({ action: "delete-selected", event }) handleAction({ action: "delete-selected", event })
@@ -174,7 +191,8 @@ function ToolbarLeftSection() {
<Tooltip> <Tooltip>
<ToolbarButton <ToolbarButton
icon={ icon={
<Bookmark <HugeiconsIcon
icon={Bookmark02Icon}
className={currentBookmarked ? "fill-primary text-primary" : ""} className={currentBookmarked ? "fill-primary text-primary" : ""}
/> />
} }
@@ -228,7 +246,7 @@ function SceneSelector() {
<SplitButtonSeparator /> <SplitButtonSeparator />
<ScenesView> <ScenesView>
<SplitButtonRight onClick={() => {}} type="button"> <SplitButtonRight onClick={() => {}} type="button">
<LayersIcon className="size-4" /> <HugeiconsIcon icon={Layers01Icon} className="size-4" />
</SplitButtonRight> </SplitButtonRight>
</ScenesView> </ScenesView>
</SplitButton> </SplitButton>
@@ -259,7 +277,10 @@ function ToolbarRightSection({
<TooltipProvider delayDuration={500}> <TooltipProvider delayDuration={500}>
<ToolbarButton <ToolbarButton
icon={ icon={
<Magnet className={cn(snappingEnabled ? "text-primary" : "")} /> <HugeiconsIcon
icon={MagnetIcon}
className={cn(snappingEnabled ? "text-primary" : "")}
/>
} }
tooltip="Auto snapping" tooltip="Auto snapping"
onClick={() => toggleSnapping()} onClick={() => toggleSnapping()}
@@ -267,7 +288,13 @@ function ToolbarRightSection({
<ToolbarButton <ToolbarButton
icon={ icon={
<Link className={cn(rippleEditingEnabled ? "text-primary" : "")} /> <HugeiconsIcon
icon={Link04Icon}
className={cn(
rippleEditingEnabled ? "text-primary" : "",
"scale-110",
)}
/>
} }
tooltip="Ripple editing" tooltip="Ripple editing"
onClick={() => toggleRippleEditing()} onClick={() => toggleRippleEditing()}
@@ -283,7 +310,7 @@ function ToolbarRightSection({
type="button" type="button"
onClick={() => onZoom({ direction: "out" })} onClick={() => onZoom({ direction: "out" })}
> >
<ZoomOut /> <HugeiconsIcon icon={SearchAddIcon} />
</Button> </Button>
<Slider <Slider
className="w-24" className="w-24"
@@ -299,7 +326,7 @@ function ToolbarRightSection({
type="button" type="button"
onClick={() => onZoom({ direction: "in" })} onClick={() => onZoom({ direction: "in" })}
> >
<ZoomIn /> <HugeiconsIcon icon={SearchMinusIcon} />
</Button> </Button>
</div> </div>
</div> </div>
@@ -31,6 +31,7 @@ interface TimelineTrackContentProps {
track: TimelineTrack; track: TimelineTrack;
}) => void; }) => void;
onTrackMouseDown?: (event: React.MouseEvent) => void; onTrackMouseDown?: (event: React.MouseEvent) => void;
shouldIgnoreClick?: () => boolean;
} }
export function TimelineTrackContent({ export function TimelineTrackContent({
@@ -45,6 +46,7 @@ export function TimelineTrackContent({
onElementMouseDown, onElementMouseDown,
onElementClick, onElementClick,
onTrackMouseDown, onTrackMouseDown,
shouldIgnoreClick,
}: TimelineTrackContentProps) { }: TimelineTrackContentProps) {
const editor = useEditor(); const editor = useEditor();
const { isElementSelected, clearElementSelection } = useElementSelection(); const { isElementSelected, clearElementSelection } = useElementSelection();
@@ -66,7 +68,10 @@ export function TimelineTrackContent({
return ( return (
<button <button
className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")} className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
onClick={clearElementSelection} onClick={() => {
if (shouldIgnoreClick?.()) return;
clearElementSelection();
}}
onMouseDown={(event) => { onMouseDown={(event) => {
onTrackMouseDown?.(event); onTrackMouseDown?.(event);
}} }}
@@ -1,8 +1,11 @@
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { OcGithubIcon } from "@opencut/ui/icons";
import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { SOCIAL_LINKS } from "@/constants/site-constants"; import { SOCIAL_LINKS } from "@/constants/site-constants";
import {
GithubIcon,
Link04Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export function GitHubContributeSection({ export function GitHubContributeSection({
title, title,
@@ -24,7 +27,7 @@ export function GitHubContributeSection({
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Button className="w-full" size="lg"> <Button className="w-full" size="lg">
<OcGithubIcon className="size-4" /> <HugeiconsIcon icon={GithubIcon} />
Start contributing Start contributing
</Button> </Button>
</Link> </Link>
@@ -34,7 +37,7 @@ export function GitHubContributeSection({
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Button variant="outline" className="w-full" size="lg"> <Button variant="outline" className="w-full" size="lg">
<ExternalLink className="size-4" /> <HugeiconsIcon icon={Link04Icon} />
Report issues Report issues
</Button> </Button>
</Link> </Link>
+4 -3
View File
@@ -7,7 +7,8 @@ import { Button } from "./ui/button";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { ThemeToggle } from "./theme-toggle"; import { ThemeToggle } from "./theme-toggle";
import { OcGithubIcon, OcMenuIcon } from "@opencut/ui/icons"; import { GithubIcon, Menu02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants"; import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
@@ -62,13 +63,13 @@ export function Header() {
className="flex items-center justify-center p-0" className="flex items-center justify-center p-0"
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
> >
<OcMenuIcon size={30} /> <HugeiconsIcon icon={Menu02Icon} size={30} />
</Button> </Button>
</div> </div>
<div className="hidden items-center gap-3 md:flex"> <div className="hidden items-center gap-3 md:flex">
<Link href={SOCIAL_LINKS.github}> <Link href={SOCIAL_LINKS.github}>
<Button className="bg-background text-sm" variant="outline"> <Button className="bg-background text-sm" variant="outline">
<OcGithubIcon className="size-4" /> <HugeiconsIcon icon={GithubIcon} className="size-4" />
31k+ 31k+
</Button> </Button>
</Link> </Link>
+3 -2
View File
@@ -1,9 +1,10 @@
"use client"; "use client";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Sun } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { cn } from "@/utils/ui"; import { cn } from "@/utils/ui";
import { Sun03Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
interface ThemeToggleProps { interface ThemeToggleProps {
className?: string; className?: string;
@@ -28,7 +29,7 @@ export function ThemeToggle({
onToggle?.(e); onToggle?.(e);
}} }}
> >
<Sun className={cn("!size-[1.1rem]", iconClassName)} /> <HugeiconsIcon icon={Sun03Icon} className={cn("!size-[1.1rem]", iconClassName)} />
<span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span> <span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
</Button> </Button>
); );
+1 -1
View File
@@ -231,7 +231,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
}; };
return ( return (
<div className="relative"> <div className="relative flex-1">
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
+17
View File
@@ -0,0 +1,17 @@
import { HugeiconsIcon, type HugeiconsIconProps } from "@hugeicons/react";
import { Loading03Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/utils/ui";
function Spinner({ className, ...props }: Omit<HugeiconsIconProps, "icon">) {
return (
<HugeiconsIcon
icon={Loading03Icon}
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
}
export { Spinner }
+26 -5
View File
@@ -1,5 +1,11 @@
import type { TrackType } from "@/types/timeline"; import type { TrackType } from "@/types/timeline";
import { Video, TypeIcon, Music, Sticker } from "lucide-react"; import {
Happy01Icon,
MusicNote03Icon,
TextIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { OcVideoIcon } from "@opencut/ui/icons";
export const TRACK_COLORS: Record<TrackType, { background: string }> = { export const TRACK_COLORS: Record<TrackType, { background: string }> = {
video: { video: {
@@ -37,8 +43,23 @@ export const TIMELINE_CONSTANTS = {
} as const; } as const;
export const TRACK_ICONS: Record<TrackType, React.ReactNode> = { export const TRACK_ICONS: Record<TrackType, React.ReactNode> = {
video: <Video className="text-muted-foreground size-4 shrink-0" />, video: <OcVideoIcon className="text-muted-foreground size-4 shrink-0" />,
text: <TypeIcon className="text-muted-foreground size-4 shrink-0" />, text: (
audio: <Music className="text-muted-foreground size-4 shrink-0" />, <HugeiconsIcon
sticker: <Sticker className="text-muted-foreground size-4 shrink-0" />, icon={TextIcon}
className="text-muted-foreground size-4 shrink-0"
/>
),
audio: (
<HugeiconsIcon
icon={MusicNote03Icon}
className="text-muted-foreground size-4 shrink-0"
/>
),
sticker: (
<HugeiconsIcon
icon={Happy01Icon}
className="text-muted-foreground size-4 shrink-0"
/>
),
} as const; } as const;
@@ -510,13 +510,9 @@ export function useElementInteraction({
element: TimelineElement; element: TimelineElement;
track: TimelineTrack; track: TimelineTrack;
}) => { }) => {
event.stopPropagation();
mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
const isRightClick = event.button === 2; const isRightClick = event.button === 2;
const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
// right-click // right-click: don't stop propagation so ContextMenu can open
if (isRightClick) { if (isRightClick) {
const alreadySelected = isElementSelected({ const alreadySelected = isElementSelected({
trackId: track.id, trackId: track.id,
@@ -532,6 +528,12 @@ export function useElementInteraction({
return; return;
} }
// left-click: stop propagation for drag operations
event.stopPropagation();
mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
// multi-select: toggle selection // multi-select: toggle selection
if (isMultiSelect) { if (isMultiSelect) {
handleSelectionClick({ handleSelectionClick({
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline"; import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
import { useEditor } from "../use-editor"; import { useEditor } from "../use-editor";
@@ -98,6 +98,7 @@ export function useSelectionBox({
const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>( const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
null, null,
); );
const justFinishedSelectingRef = useRef(false);
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
({ clientX, clientY }: React.MouseEvent) => { ({ clientX, clientY }: React.MouseEvent) => {
@@ -195,6 +196,12 @@ export function useSelectionBox({
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (selectionBox?.isActive) {
justFinishedSelectingRef.current = true;
requestAnimationFrame(() => {
justFinishedSelectingRef.current = false;
});
}
setSelectionBox(null); setSelectionBox(null);
}; };
@@ -223,9 +230,14 @@ export function useSelectionBox({
}; };
}, [selectionBox, containerRef]); }, [selectionBox, containerRef]);
const shouldIgnoreClick = useCallback(() => {
return justFinishedSelectingRef.current;
}, []);
return { return {
selectionBox, selectionBox,
handleMouseDown, handleMouseDown,
isSelecting: selectionBox?.isActive || false, isSelecting: selectionBox?.isActive || false,
shouldIgnoreClick,
}; };
} }
@@ -1,17 +1,18 @@
import { import type { ElementType } from "react";
CaptionsIcon,
ArrowLeftRightIcon,
SparklesIcon,
StickerIcon,
MusicIcon,
VideoIcon,
BlendIcon,
SlidersHorizontalIcon,
type LucideIcon,
TypeIcon,
SettingsIcon,
} from "lucide-react";
import { create } from "zustand"; import { create } from "zustand";
import {
ArrowRightDoubleIcon,
ClosedCaptionIcon,
Folder03Icon,
Happy01Icon,
HeadphonesIcon,
MagicWand05Icon,
TextIcon,
Settings01Icon,
SlidersHorizontalIcon,
ColorsIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
export const TAB_KEYS = [ export const TAB_KEYS = [
"media", "media",
@@ -28,48 +29,57 @@ export const TAB_KEYS = [
export type Tab = (typeof TAB_KEYS)[number]; export type Tab = (typeof TAB_KEYS)[number];
const createHugeiconsIcon =
({ icon }: { icon: IconSvgElement }) =>
({ className }: { className?: string }) => (
<HugeiconsIcon icon={icon} className={className} />
);
export const tabs = { export const tabs = {
media: { media: {
icon: VideoIcon, icon: createHugeiconsIcon({ icon: Folder03Icon }),
label: "Media", label: "Media",
}, },
sounds: { sounds: {
icon: MusicIcon, icon: createHugeiconsIcon({ icon: HeadphonesIcon }),
label: "Sounds", label: "Sounds",
}, },
text: { text: {
icon: TypeIcon, icon: createHugeiconsIcon({ icon: TextIcon }),
label: "Text", label: "Text",
}, },
stickers: { stickers: {
icon: StickerIcon, icon: createHugeiconsIcon({ icon: Happy01Icon }),
label: "Stickers", label: "Stickers",
}, },
effects: { effects: {
icon: SparklesIcon, icon: createHugeiconsIcon({ icon: MagicWand05Icon }),
label: "Effects", label: "Effects",
}, },
transitions: { transitions: {
icon: ArrowLeftRightIcon, icon: createHugeiconsIcon({ icon: ArrowRightDoubleIcon }),
label: "Transitions", label: "Transitions",
}, },
captions: { captions: {
icon: CaptionsIcon, icon: createHugeiconsIcon({ icon: ClosedCaptionIcon }),
label: "Captions", label: "Captions",
}, },
filters: { filters: {
icon: BlendIcon, icon: createHugeiconsIcon({ icon: ColorsIcon }),
label: "Filters", label: "Filters",
}, },
adjustment: { adjustment: {
icon: SlidersHorizontalIcon, icon: createHugeiconsIcon({ icon: SlidersHorizontalIcon }),
label: "Adjustment", label: "Adjustment",
}, },
settings: { settings: {
icon: SettingsIcon, icon: createHugeiconsIcon({ icon: Settings01Icon }),
label: "Settings", label: "Settings",
}, },
} satisfies Record<Tab, { icon: LucideIcon; label: string }>; } satisfies Record<
Tab,
{ icon: ElementType<{ className?: string }>; label: string }
>;
type MediaViewMode = "grid" | "list"; type MediaViewMode = "grid" | "list";
+6
View File
@@ -21,6 +21,8 @@
"@ffmpeg/util": "^0.12.2", "@ffmpeg/util": "^0.12.2",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@huggingface/transformers": "^3.8.1", "@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*", "@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*", "@opencut/ui": "workspace:*",
@@ -218,6 +220,10 @@
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="], "@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
"@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@3.1.1", "", {}, "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg=="],
"@hugeicons/react": ["@hugeicons/react@1.1.4", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-gsc3eZyd2fGqRUThW9+lfjxxsOkz6KNVmRXRgJjP32GL0OnnLJnl3hytKt47CBbiQj2xE2kCw+rnP3UQCThcKw=="],
"@huggingface/jinja": ["@huggingface/jinja@0.5.3", "", {}, "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.3", "", {}, "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA=="],
"@huggingface/transformers": ["@huggingface/transformers@3.8.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA=="], "@huggingface/transformers": ["@huggingface/transformers@3.8.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA=="],
-42
View File
@@ -1,45 +1,3 @@
export function OcGoogleIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24">
<title>Google</title>
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
);
}
export function OcGithubIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
viewBox="0 -3.5 256 256"
preserveAspectRatio="xMinYMin meet"
>
<title>GitHub</title>
<g fill="currentColor">
<path d="M127.505 0C57.095 0 0 57.085 0 127.505c0 56.336 36.534 104.13 87.196 120.99 6.372 1.18 8.712-2.766 8.712-6.134 0-3.04-.119-13.085-.173-23.739-35.473 7.713-42.958-15.044-42.958-15.044-5.8-14.738-14.157-18.656-14.157-18.656-11.568-7.914.872-7.752.872-7.752 12.804.9 19.546 13.14 19.546 13.14 11.372 19.493 29.828 13.857 37.104 10.6 1.144-8.242 4.449-13.866 8.095-17.05-28.32-3.225-58.092-14.158-58.092-63.014 0-13.92 4.981-25.295 13.138-34.224-1.324-3.212-5.688-16.18 1.235-33.743 0 0 10.707-3.427 35.073 13.07 10.17-2.826 21.078-4.242 31.914-4.29 10.836.048 21.752 1.464 31.942 4.29 24.337-16.497 35.029-13.07 35.029-13.07 6.94 17.563 2.574 30.531 1.25 33.743 8.175 8.929 13.122 20.303 13.122 34.224 0 48.972-29.828 59.756-58.22 62.912 4.573 3.957 8.648 11.717 8.648 23.612 0 17.06-.148 30.791-.148 34.991 0 3.393 2.295 7.369 8.759 6.117 50.634-16.879 87.122-64.656 87.122-120.973C255.009 57.085 197.922 0 127.505 0" />
<path d="M47.755 181.634c-.28.633-1.278.823-2.185.389-.925-.416-1.445-1.28-1.145-1.916.275-.652 1.273-.834 2.196-.396.927.415 1.455 1.287 1.134 1.923M54.027 187.23c-.608.564-1.797.302-2.604-.589-.834-.889-.99-2.077-.373-2.65.627-.563 1.78-.3 2.616.59.834.899.996 2.08.36 2.65M58.33 194.39c-.782.543-2.06.034-2.849-1.1-.781-1.133-.781-2.493.017-3.038.792-.545 2.05-.055 2.85 1.07.78 1.153.78 2.513-.019 3.069M65.606 202.683c-.699.77-2.187.564-3.277-.488-1.114-1.028-1.425-2.487-.724-3.258.707-.772 2.204-.555 3.302.488 1.107 1.026 1.445 2.496.7 3.258M75.01 205.483c-.307.998-1.741 1.452-3.185 1.028-1.442-.437-2.386-1.607-2.095-2.616.3-1.005 1.74-1.478 3.195-1.024 1.44.435 2.386 1.596 2.086 2.612M85.714 206.67c.036 1.052-1.189 1.924-2.705 1.943-1.525.033-2.758-.818-2.774-1.852 0-1.062 1.197-1.926 2.721-1.951 1.516-.03 2.758.815 2.758 1.86M96.228 206.267c.182 1.026-.872 2.08-2.377 2.36-1.48.27-2.85-.363-3.039-1.38-.184-1.052.89-2.105 2.367-2.378 1.508-.262 2.857.355 3.049 1.398" />
</g>
</svg>
);
}
export function OcVercelIcon({ className }: { className?: string }) { export function OcVercelIcon({ className }: { className?: string }) {
return ( return (
<svg <svg
-165
View File
@@ -1,165 +0,0 @@
export function OcBackgroundIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="353"
height="353"
viewBox="0 0 353 353"
fill="none"
className={className}
>
<title>Background</title>
<g clipPath="url(#clip0_1_3)">
<rect
x="-241.816"
y="233.387"
width="592.187"
height="17.765"
transform="rotate(-37 -241.816 233.387)"
fill="white"
/>
<rect
x="-189.907"
y="306.804"
width="592.187"
height="17.765"
transform="rotate(-37 -189.907 306.804)"
fill="white"
/>
<rect
x="-146.928"
y="389.501"
width="592.187"
height="17.765"
transform="rotate(-37 -146.928 389.501)"
fill="white"
/>
<rect
x="-103.144"
y="477.904"
width="592.187"
height="17.765"
transform="rotate(-37 -103.144 477.904)"
fill="white"
/>
<rect
x="-57.169"
y="570.714"
width="592.187"
height="17.765"
transform="rotate(-37 -57.169 570.714)"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_1_3">
<rect width="353" height="353" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function OcSocialsIcon({
className = "",
size = 32,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 345 243"
fill="none"
className={className}
>
<title>Socials</title>
<g opacity="0.5">
<path
d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z"
fill="#FFFC00"
/>
<path
d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z"
fill="white"
/>
<path
d="M203.75 4H39.25C19.782 4 4 19.782 4 39.25V203.75C4 223.218 19.782 239 39.25 239H203.75C223.218 239 239 223.218 239 203.75V39.25C239 19.782 223.218 4 203.75 4Z"
stroke="black"
strokeWidth="7"
/>
<path
d="M97.1738 194.02C88.9121 188.053 82.4863 184.84 66.8809 187.594C64.5859 188.053 60.4551 189.43 59.9961 185.299C59.0781 182.086 59.0781 177.037 56.7832 176.578C42.5547 174.742 37.5059 171.988 35.2109 169.234C34.293 168.316 33.834 166.021 35.6699 165.562C59.9961 160.973 71.4707 137.564 73.7656 132.975C76.5195 126.09 71.9297 121.959 63.209 119.205C59.0781 117.828 52.1934 115.992 52.1934 111.402C52.1934 109.107 54.4883 107.73 56.7832 106.812C58.6191 106.354 60.4551 105.895 62.291 106.812C67.7988 109.107 72.8477 110.025 75.6016 107.73C75.6016 95.3379 72.3887 79.7324 77.4375 66.8809C83.4043 52.6523 98.0918 39.8008 121.5 39.8008C144.908 39.8008 159.596 52.6523 165.562 66.8809C170.611 79.7324 167.398 95.3379 167.398 107.73C170.152 110.025 175.201 109.107 180.709 106.812C182.545 105.895 184.381 106.354 186.217 106.812C188.512 107.73 190.807 109.107 190.807 111.402C190.807 115.992 183.922 117.828 179.791 119.205C171.07 121.959 166.48 126.09 169.234 132.975C171.529 137.564 183.004 160.973 207.33 165.562C209.166 166.021 208.707 168.316 207.789 169.234C205.494 171.988 200.445 174.742 186.217 176.578C183.922 177.037 183.922 182.086 183.004 185.299C182.545 189.43 178.414 188.053 176.119 187.594C160.514 184.84 154.088 188.053 145.826 194.02C139.065 199.872 130.442 203.126 121.5 203.199C111.861 203.658 104.059 199.527 97.1738 194.02Z"
stroke="black"
strokeWidth="7"
/>
</g>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M133.5 4H321.5C334.48 4 345 14.5205 345 27.5V215.5C345 228.48 334.48 239 321.5 239H133.5C120.52 239 110 228.48 110 215.5V27.5C110 14.5205 120.52 4 133.5 4Z"
fill="#010101"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M261.497 99.4086C271.784 106.758 284.388 111.083 297.999 111.083V84.9035C295.423 84.9045 292.854 84.6357 290.333 84.1016V104.709C276.723 104.709 264.121 100.384 253.831 93.0345V146.459C253.831 173.185 232.155 194.85 205.417 194.85C195.44 194.85 186.167 191.835 178.464 186.665C187.256 195.65 199.516 201.224 213.08 201.224C239.821 201.224 261.498 179.56 261.498 152.833L261.497 99.4086ZM270.953 72.9965C265.696 67.2559 262.244 59.8365 261.497 51.634V48.267H254.233C256.061 58.6916 262.298 67.5981 270.953 72.9965ZM195.376 166.157C192.438 162.308 190.851 157.598 190.858 152.756C190.858 140.533 200.773 130.622 213.005 130.622C215.285 130.621 217.551 130.97 219.723 131.659V104.894C217.185 104.547 214.622 104.399 212.061 104.454V125.286C209.887 124.597 207.62 124.247 205.34 124.249C193.107 124.249 183.193 134.159 183.193 146.384C183.193 155.027 188.149 162.512 195.376 166.157Z"
fill="#EE1D52"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M253.831 93.0345C264.121 100.384 276.723 104.709 290.333 104.709V84.1016C282.736 82.4848 276.011 78.5162 270.953 72.9965C262.298 67.5981 256.061 58.6916 254.233 48.267H235.152V152.832C235.108 165.022 225.21 174.892 213.004 174.892C205.811 174.892 199.422 171.465 195.376 166.157C188.149 162.512 183.193 155.027 183.193 146.384C183.193 134.159 193.107 124.249 205.34 124.249C207.683 124.249 209.942 124.614 212.061 125.286V104.454C185.792 104.996 164.665 126.449 164.665 152.833C164.665 166.003 169.926 177.942 178.464 186.665C186.167 191.835 195.44 194.85 205.417 194.85C232.155 194.85 253.831 173.185 253.831 146.459V93.0345Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M290.333 84.1016V78.5293C283.482 78.5396 276.766 76.6229 270.953 72.9965C276.099 78.6269 282.874 82.5087 290.333 84.1016ZM254.233 48.267C254.058 47.2708 253.924 46.2679 253.831 45.2608V41.8938H227.485V146.459C227.443 158.648 217.545 168.518 205.339 168.518C201.754 168.518 198.372 167.669 195.376 166.157C199.422 171.465 205.811 174.892 213.004 174.892C225.21 174.892 235.108 165.022 235.152 152.832V48.267H254.233ZM212.061 104.454L212.061 98.5212C209.859 98.2202 207.64 98.0698 205.418 98.071C178.676 98.071 157 119.736 157 146.459C157 163.214 165.518 177.979 178.464 186.665C169.926 177.942 164.666 166.002 164.666 152.832C164.666 126.449 185.791 104.996 212.061 104.454Z"
fill="#69C9D0"
/>
</svg>
);
}
export function OcTransitionUpIcon({
className = "",
size = 16,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
className={className}
>
<title>Transition Up</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.5 12.6667C2.5 13.4951 3.17157 14.1667 4 14.1667H12C12.8284 14.1667 13.5 13.4951 13.5 12.6667V11.3333C13.5 10.5049 12.8284 9.83333 12 9.83333H4C3.17157 9.83333 2.5 10.5049 2.5 11.3333V12.6667ZM4 15.1667C2.61929 15.1667 1.5 14.0474 1.5 12.6667V11.3333C1.5 9.95262 2.61929 8.83333 4 8.83333H12C13.3807 8.83333 14.5 9.95262 14.5 11.3333V12.6667C14.5 14.0474 13.3807 15.1667 12 15.1667H4Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 5.83333C1.72386 5.83333 1.5 5.60947 1.5 5.33333L1.5 4C1.5 2.2511 2.91777 0.833332 4.66667 0.833332L11.3333 0.833332C13.0822 0.833332 14.5 2.2511 14.5 4V5.33333C14.5 5.60947 14.2761 5.83333 14 5.83333C13.7239 5.83333 13.5 5.60947 13.5 5.33333V4C13.5 2.80338 12.53 1.83333 11.3333 1.83333L4.66667 1.83333C3.47005 1.83333 2.5 2.80338 2.5 4V5.33333C2.5 5.60947 2.27614 5.83333 2 5.83333Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.35355 3.64645C8.15829 3.45118 7.84171 3.45118 7.64645 3.64645L5.64645 5.64645C5.45118 5.84171 5.45118 6.15829 5.64645 6.35355C5.84171 6.54882 6.15829 6.54882 6.35355 6.35355L7.5 5.20711L7.5 9.33333C7.5 9.60948 7.72386 9.83333 8 9.83333C8.27614 9.83333 8.5 9.60948 8.5 9.33333V5.20711L9.64645 6.35355C9.84171 6.54882 10.1583 6.54882 10.3536 6.35355C10.5488 6.15829 10.5488 5.84171 10.3536 5.64645L8.35355 3.64645Z"
fill="white"
/>
</svg>
);
}
-1
View File
@@ -1,3 +1,2 @@
export * from "./brand"; export * from "./brand";
export * from "./editor";
export * from "./ui"; export * from "./ui";
+14 -90
View File
@@ -1,108 +1,32 @@
export function OcMenuIcon({ export function OcVideoIcon({
className = "", className = "",
size = 16, size = 32,
}: { }: {
className?: string; className?: string;
size?: number; size?: number;
}) { }) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
width={size} width={size}
height={size} height={size}
viewBox="0 0 141 111"
fill="none"
className={className} className={className}
> viewBox="0 0 29 24"
<title>Menu</title>
<rect y="19.5" width="141" height="12" rx="7.5" fill="currentColor" />
<rect y="67.5" width="141" height="12" rx="7.5" fill="currentColor" />
</svg>
);
}
export function OcPencilIcon({
className = "",
size = 24,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
color="currentColor"
fill="none" fill="none"
stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
>
<title>Video</title>
<path
d="M2.41667 11C2.41667 7.70017 2.41667 6.05025 3.65537 5.02513C4.89405 4 6.88771 4 10.875 4H12.0833C16.0706 4 18.0642 4 19.303 5.02513C20.5417 6.05025 20.5417 7.70017 20.5417 11V13C20.5417 16.2998 20.5417 17.9497 19.303 18.9749C18.0642 20 16.0706 20 12.0833 20H10.875C6.88771 20 4.89405 20 3.65537 18.9749C2.41667 17.9497 2.41667 16.2998 2.41667 13V11Z"
stroke="#141B34"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" />
className={className} <path
> d="M20.5417 8.90585L20.6938 8.80196C23.2504 7.05623 24.5287 6.18336 25.556 6.60482C26.5833 7.02628 26.5833 8.42355 26.5833 11.2181V12.7819C26.5833 15.5765 26.5833 16.9737 25.556 17.3952C24.5287 17.8166 23.2504 16.9438 20.6938 15.198L20.5417 15.0941"
<title>Edit</title> stroke="#141B34"
<path d="M14.0737 3.88545C14.8189 3.07808 15.1915 2.6744 15.5874 2.43893C16.5427 1.87076 17.7191 1.85309 18.6904 2.39232C19.0929 2.6158 19.4769 3.00812 20.245 3.79276C21.0131 4.5774 21.3972 4.96972 21.6159 5.38093C22.1438 6.37312 22.1265 7.57479 21.5703 8.5507C21.3398 8.95516 20.9446 9.33578 20.1543 10.097L10.7506 19.1543C9.25288 20.5969 8.504 21.3182 7.56806 21.6837C6.63212 22.0493 5.6032 22.0224 3.54536 21.9686L3.26538 21.9613C2.63891 21.9449 2.32567 21.9367 2.14359 21.73C1.9615 21.5234 1.98636 21.2043 2.03608 20.5662L2.06308 20.2197C2.20301 18.4235 2.27297 17.5255 2.62371 16.7182C2.97444 15.9109 3.57944 15.2555 4.78943 13.9445L14.0737 3.88545Z" />
<path d="M13 4L20 11" />
</svg>
);
}
export function OcLeftArrowIcon({
className = "",
size = 24,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
color="currentColor"
fill="none"
stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" />
className={className}
>
<title>Left Arrow</title>
<path d="M5.5 12.002H19" />
<path d="M10.9999 18.002C10.9999 18.002 4.99998 13.583 4.99997 12.0019C4.99996 10.4208 11 6.00195 11 6.00195" />
</svg>
);
}
export function OcTrashIcon({
className = "",
size = 24,
}: {
className?: string;
size?: number;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
color="currentColor"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<title>Trash</title>
<path d="M19.5 5.5L18.8803 15.5251C18.7219 18.0864 18.6428 19.3671 18.0008 20.2879C17.6833 20.7431 17.2747 21.1273 16.8007 21.416C15.8421 22 14.559 22 11.9927 22C9.42312 22 8.1383 22 7.17905 21.4149C6.7048 21.1257 6.296 20.7408 5.97868 20.2848C5.33688 19.3626 5.25945 18.0801 5.10461 15.5152L4.5 5.5" />
<path d="M3 5.5H21M16.0557 5.5L15.3731 4.09173C14.9196 3.15626 14.6928 2.68852 14.3017 2.39681C14.215 2.3321 14.1231 2.27454 14.027 2.2247C13.5939 2 13.0741 2 12.0345 2C10.9688 2 10.436 2 9.99568 2.23412C9.8981 2.28601 9.80498 2.3459 9.71729 2.41317C9.32164 2.7167 9.10063 3.20155 8.65861 4.17126L8.05292 5.5" />
<path d="M9.5 16.5L9.5 10.5" />
<path d="M14.5 16.5L14.5 10.5" />
</svg> </svg>
); );
} }