From 46e55674dee7061973e155375dc4f01fb4f13c00 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 4 Mar 2026 01:48:39 +0100 Subject: [PATCH] feat: branding page --- apps/web/src/app/base-page.tsx | 5 +- apps/web/src/app/branding/page.tsx | 243 +++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/branding/page.tsx diff --git a/apps/web/src/app/base-page.tsx b/apps/web/src/app/base-page.tsx index a3d56fe5..1841a1ee 100644 --- a/apps/web/src/app/base-page.tsx +++ b/apps/web/src/app/base-page.tsx @@ -8,7 +8,8 @@ interface BasePageProps { mainClassName?: string; maxWidth?: "3xl" | "6xl" | "full"; title?: string; - description?: string; + description?: React.ReactNode; + action?: React.ReactNode; } export function BasePage({ @@ -18,6 +19,7 @@ export function BasePage({ maxWidth = "3xl", title, description, + action, }: BasePageProps) { const maxWidthClass = { "3xl": "max-w-3xl", @@ -43,6 +45,7 @@ export function BasePage({

{description}

+ {action} )} {children} diff --git a/apps/web/src/app/branding/page.tsx b/apps/web/src/app/branding/page.tsx new file mode 100644 index 00000000..4ebef644 --- /dev/null +++ b/apps/web/src/app/branding/page.tsx @@ -0,0 +1,243 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { Check, Copy, Download } from "lucide-react"; +import { useState } from "react"; +import { BasePage } from "@/app/base-page"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/utils/ui"; + +function downloadAsset(src: string) { + const filename = src.split("/").pop() ?? "asset.svg"; + const a = document.createElement("a"); + a.href = src; + a.download = filename; + a.click(); +} + +async function copyAsset(src: string) { + const res = await fetch(src); + const text = await res.text(); + await navigator.clipboard.writeText(text); +} + +const ALL_ASSETS = () => + ASSET_SECTIONS.flatMap((s) => s.assets); + +type AssetTheme = "dark" | "light" | "icon"; + +interface AssetVariant { + src: string; + theme: AssetTheme; + label: string; + width: number; + height: number; +} + +interface AssetSection { + title: string; + description: string; + cols: "1" | "2"; + assets: AssetVariant[]; +} + +const ASSET_SECTIONS: AssetSection[] = [ + { + title: "Symbol", + description: + "Use the symbol on its own when the OpenCut name is already present nearby or space is limited.", + cols: "2", + assets: [ + { + src: "/logos/opencut/symbol.svg", + theme: "dark", + label: "Symbol", + width: 400, + height: 400, + }, + { + src: "/logos/opencut/symbol-light.svg", + theme: "light", + label: "Symbol", + width: 400, + height: 400, + }, + ], + }, + { + title: "Lockup", + description: + "The full lockup combines the symbol and wordmark. Prefer this in most contexts where you have enough horizontal space.", + cols: "2", + assets: [ + { + src: "/logos/opencut/logo.svg", + theme: "dark", + label: "Logo", + width: 1809, + height: 400, + }, + { + src: "/logos/opencut/logo-light.svg", + theme: "light", + label: "Logo", + width: 1809, + height: 400, + }, + { + src: "/logos/opencut/text.svg", + theme: "dark", + label: "Text", + width: 1760, + height: 400, + }, + { + src: "/logos/opencut/text-light.svg", + theme: "light", + label: "Text", + width: 1760, + height: 400, + }, + ], + }, +]; + +export default function BrandingPage() { + return ( + + Download OpenCut brand assets for use in your projects.{" "} + + Read the brand guidelines. + + + } + action={ + + } + > +
+ {ASSET_SECTIONS.map((section) => ( +
+
+

{section.title}

+

+ {section.description} +

+
+
+ {section.assets.map((variant) => ( + + ))} +
+
+ ))} +
+ + + +
+
+

Usage

+

+ OpenCut is open source — the code is free to use under its license. + That license does not cover the name or logo. You can say you use + OpenCut, that your project integrates with OpenCut, or that it was + built on top of OpenCut. You cannot name your product OpenCut, imply + we made or endorse your product, or use the marks commercially + without asking first. For anything unclear, reach out at{" "} + + brand@opencut.app + + . +

+
+ +
+

What's not allowed

+
    + {[ + "Using OpenCut in the name of your product, service, or domain.", + "Implying that OpenCut made, sponsors, or endorses your work.", + "Using the logo or name on merchandise or commercial marketing.", + "Modifying the marks.", + ].map((item) => ( +
  • + - + {item} +
  • + ))} +
+
+
+
+ ); +} + +function AssetCard({ variant }: { variant: AssetVariant }) { + const [copied, setCopied] = useState(false); + + async function handleCopy() { + await copyAsset(variant.src); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + return ( + +
+ {variant.label} +
+ + +
+ ); +}