Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RbcButton } from "@/components/ui/rbc-button";
import { exportBrandJson } from "@/features/brand-studio/exporters/export-brand-json";
import { useBrandStore } from "@/features/brand-studio/store/brand-store";
import { downloadFile } from "@/features/theme-engine/exporters/download-file";
import { LogoBuilderPanel } from "@/features/logo-builder/components/logo-builder-panel";

export function BrandStudioPanel() {
const brand = useBrandStore((state) => state.brand);
Expand Down Expand Up @@ -181,8 +182,10 @@ export function BrandStudioPanel() {
</div>

<RbcButton variant="primary" onClick={exportBrand} className="w-full">
Export Brand Kit JSON
</RbcButton>
Export Brand Kit JSON
</RbcButton>

<LogoBuilderPanel />
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { RbcBadge } from "@/components/ui/rbc-badge";
import { RbcButton } from "@/components/ui/rbc-button";
import { exportLogoSvg } from "@/features/logo-builder/exporters/export-logo-svg";
import { LogoControls } from "@/features/logo-builder/components/logo-controls";
import { LogoPreview } from "@/features/logo-builder/components/logo-preview";
import { useLogoStore } from "@/features/logo-builder/store/logo-store";
import { downloadFile } from "@/features/theme-engine/exporters/download-file";

export function LogoBuilderPanel() {
const logo = useLogoStore((state) => state.logo);
const resetLogo = useLogoStore((state) => state.resetLogo);

function handleExportSvg(): void {
downloadFile("rainbowcode-logo.svg", exportLogoSvg(logo), "image/svg+xml");
}

return (
<section className="overflow-hidden rounded-[28px] border border-white/70 bg-white/78 shadow-[0_18px_70px_rgba(15,23,42,0.08)] backdrop-blur-2xl dark:border-white/10 dark:bg-slate-950/76">
<div className="relative overflow-hidden border-b border-slate-200/70 p-4 dark:border-slate-800">
<div className="absolute right-0 top-0 h-24 w-24 rounded-full bg-fuchsia-400/20 blur-2xl" />

<div className="relative z-10 flex items-start justify-between gap-3">
<div>
<div className="flex flex-wrap gap-2">
<RbcBadge variant="info">Logo Builder</RbcBadge>
<RbcBadge variant="success">SVG Export</RbcBadge>
</div>

<h3 className="mt-3 text-lg font-black tracking-tight text-slate-950 dark:text-white">
Visual logo system
</h3>

<p className="mt-1 text-xs leading-5 text-slate-500">
Create a text-based brand mark with typography, gradient, preview,
and exportable SVG.
</p>
</div>

<RbcButton variant="ghost" onClick={resetLogo}>
Reset
</RbcButton>
</div>
</div>

<div className="grid gap-5 p-4 2xl:grid-cols-[minmax(0,1fr)_360px]">
<LogoPreview logo={logo} />

<div className="space-y-4">
<LogoControls />
<RbcButton variant="primary" onClick={handleExportSvg} className="w-full">
Export Logo SVG
</RbcButton>
</div>
</div>
</section>
);
}
248 changes: 248 additions & 0 deletions apps/web/src/features/logo-builder/components/logo-controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
"use client";

import type {
LogoFontFamily,
LogoGradientDirection,
} from "@/features/logo-builder/types/logo";
import { useLogoStore } from "@/features/logo-builder/store/logo-store";

const fontFamilies: readonly LogoFontFamily[] = [
"Inter",
"Poppins",
"Montserrat",
"Playfair Display",
"Space Grotesk",
];

const gradientDirections: readonly {
readonly label: string;
readonly value: LogoGradientDirection;
}[] = [
{ label: "Right", value: "to-right" },
{ label: "Bottom Right", value: "to-bottom-right" },
{ label: "Bottom", value: "to-bottom" },
{ label: "Top Right", value: "to-top-right" },
];

function fieldClass(): string {
return "mt-1 h-11 w-full rounded-2xl border border-slate-200 bg-white px-3 text-sm font-semibold text-slate-950 outline-none transition focus:border-indigo-300 focus:ring-4 focus:ring-indigo-500/10 dark:border-slate-800 dark:bg-slate-950 dark:text-white";
}

export function LogoControls() {
const logo = useLogoStore((state) => state.logo);
const updateText = useLogoStore((state) => state.updateText);
const updateTagline = useLogoStore((state) => state.updateTagline);
const updateFontFamily = useLogoStore((state) => state.updateFontFamily);
const updateFontWeight = useLogoStore((state) => state.updateFontWeight);
const updateLetterSpacing = useLogoStore(
(state) => state.updateLetterSpacing,
);
const updatePrimaryColor = useLogoStore((state) => state.updatePrimaryColor);
const updateSecondaryColor = useLogoStore(
(state) => state.updateSecondaryColor,
);
const updateBackgroundColor = useLogoStore(
(state) => state.updateBackgroundColor,
);
const updateGradientDirection = useLogoStore(
(state) => state.updateGradientDirection,
);
const updateRadius = useLogoStore((state) => state.updateRadius);

return (
<div className="grid gap-4">
<div>
<label
htmlFor="logo-text"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Logo Text
</label>
<input
id="logo-text"
type="text"
value={logo.text}
onChange={(event) => updateText(event.currentTarget.value)}
className={fieldClass()}
/>
</div>

<div>
<label
htmlFor="logo-tagline"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Tagline
</label>
<input
id="logo-tagline"
type="text"
value={logo.tagline}
onChange={(event) => updateTagline(event.currentTarget.value)}
className={fieldClass()}
/>
</div>

<div>
<label
htmlFor="logo-font"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Font
</label>
<select
id="logo-font"
value={logo.fontFamily}
onChange={(event) =>
updateFontFamily(event.currentTarget.value as LogoFontFamily)
}
className={fieldClass()}
>
{fontFamilies.map((fontFamily) => (
<option key={fontFamily} value={fontFamily}>
{fontFamily}
</option>
))}
</select>
</div>

<div className="grid grid-cols-2 gap-3">
<div>
<label
htmlFor="logo-weight"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Weight
</label>
<input
id="logo-weight"
type="number"
min={300}
max={900}
step={100}
value={logo.fontWeight}
onChange={(event) =>
updateFontWeight(Number(event.currentTarget.value))
}
className={fieldClass()}
/>
</div>

<div>
<label
htmlFor="logo-letter-spacing"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Spacing
</label>
<input
id="logo-letter-spacing"
type="number"
min={-8}
max={12}
value={logo.letterSpacing}
onChange={(event) =>
updateLetterSpacing(Number(event.currentTarget.value))
}
className={fieldClass()}
/>
</div>
</div>

<div>
<label
htmlFor="logo-gradient-direction"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Gradient
</label>
<select
id="logo-gradient-direction"
value={logo.gradientDirection}
onChange={(event) =>
updateGradientDirection(
event.currentTarget.value as LogoGradientDirection,
)
}
className={fieldClass()}
>
{gradientDirections.map((direction) => (
<option key={direction.value} value={direction.value}>
{direction.label}
</option>
))}
</select>
</div>

<div className="grid gap-3">
{[
{
id: "logo-primary-color",
label: "Primary",
value: logo.primaryColor,
onChange: updatePrimaryColor,
},
{
id: "logo-secondary-color",
label: "Secondary",
value: logo.secondaryColor,
onChange: updateSecondaryColor,
},
{
id: "logo-background-color",
label: "Background",
value: logo.backgroundColor,
onChange: updateBackgroundColor,
},
].map((field) => (
<div
key={field.id}
className="rounded-2xl border border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900/70"
>
<label
htmlFor={field.id}
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
{field.label}
</label>

<div className="mt-2 flex items-center gap-3">
<input
id={field.id}
type="color"
value={field.value}
onChange={(event) => field.onChange(event.currentTarget.value)}
className="size-11 cursor-pointer rounded-2xl border border-slate-200 bg-white p-1 dark:border-slate-800 dark:bg-slate-950"
/>
<input
type="text"
value={field.value}
aria-label={`${field.label} color value`}
onChange={(event) => field.onChange(event.currentTarget.value)}
className={fieldClass()}
/>
</div>
</div>
))}
</div>

<div>
<label
htmlFor="logo-radius"
className="text-xs font-black uppercase tracking-[0.14em] text-slate-500"
>
Background Radius
</label>
<input
id="logo-radius"
type="range"
min={0}
max={72}
value={logo.radius}
onChange={(event) => updateRadius(Number(event.currentTarget.value))}
className="mt-3 w-full accent-indigo-600"
/>
</div>
</div>
);
}
Loading
Loading