diff --git a/apps/web/src/features/brand-studio/components/brand-studio-panel.tsx b/apps/web/src/features/brand-studio/components/brand-studio-panel.tsx
index 0db6860..760150d 100644
--- a/apps/web/src/features/brand-studio/components/brand-studio-panel.tsx
+++ b/apps/web/src/features/brand-studio/components/brand-studio-panel.tsx
@@ -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);
@@ -181,8 +182,10 @@ export function BrandStudioPanel() {
- Export Brand Kit JSON
-
+ Export Brand Kit JSON
+
+
+
);
diff --git a/apps/web/src/features/logo-builder/components/logo-builder-panel.tsx b/apps/web/src/features/logo-builder/components/logo-builder-panel.tsx
new file mode 100644
index 0000000..08bde40
--- /dev/null
+++ b/apps/web/src/features/logo-builder/components/logo-builder-panel.tsx
@@ -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 (
+
+
+
+
+
+
+
+ Logo Builder
+ SVG Export
+
+
+
+ Visual logo system
+
+
+
+ Create a text-based brand mark with typography, gradient, preview,
+ and exportable SVG.
+
+
+
+
+ Reset
+
+
+
+
+
+
+
+
+
+
+ Export Logo SVG
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/features/logo-builder/components/logo-controls.tsx b/apps/web/src/features/logo-builder/components/logo-controls.tsx
new file mode 100644
index 0000000..80423b8
--- /dev/null
+++ b/apps/web/src/features/logo-builder/components/logo-controls.tsx
@@ -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 (
+
+
+
+ updateText(event.currentTarget.value)}
+ className={fieldClass()}
+ />
+
+
+
+
+ updateTagline(event.currentTarget.value)}
+ className={fieldClass()}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[
+ {
+ 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) => (
+
+ ))}
+
+
+
+
+ updateRadius(Number(event.currentTarget.value))}
+ className="mt-3 w-full accent-indigo-600"
+ />
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/features/logo-builder/components/logo-preview.tsx b/apps/web/src/features/logo-builder/components/logo-preview.tsx
new file mode 100644
index 0000000..25b4898
--- /dev/null
+++ b/apps/web/src/features/logo-builder/components/logo-preview.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import type { LogoConfig } from "@/features/logo-builder/types/logo";
+
+function getGradientClass(direction: LogoConfig["gradientDirection"]): string {
+ if (direction === "to-right") {
+ return "bg-gradient-to-r";
+ }
+
+ if (direction === "to-bottom") {
+ return "bg-gradient-to-b";
+ }
+
+ if (direction === "to-top-right") {
+ return "bg-gradient-to-tr";
+ }
+
+ return "bg-gradient-to-br";
+}
+
+type LogoPreviewProps = {
+ readonly logo: LogoConfig;
+};
+
+export function LogoPreview({ logo }: LogoPreviewProps) {
+ const initials = (logo.text.trim() || "RBC").slice(0, 3).toUpperCase();
+
+ return (
+
+
+
+
+
+
+
+ {initials}
+
+
+
+
+ {logo.text || "Logo"}
+
+
+
+ {logo.tagline}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/features/logo-builder/exporters/export-logo-svg.ts b/apps/web/src/features/logo-builder/exporters/export-logo-svg.ts
new file mode 100644
index 0000000..cd31a98
--- /dev/null
+++ b/apps/web/src/features/logo-builder/exporters/export-logo-svg.ts
@@ -0,0 +1,51 @@
+import type { LogoConfig } from "@/features/logo-builder/types/logo";
+
+function escapeSvgText(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function getGradientCoordinates(direction: LogoConfig["gradientDirection"]): {
+ readonly x1: string;
+ readonly y1: string;
+ readonly x2: string;
+ readonly y2: string;
+} {
+ if (direction === "to-right") {
+ return { x1: "0%", y1: "50%", x2: "100%", y2: "50%" };
+ }
+
+ if (direction === "to-bottom") {
+ return { x1: "50%", y1: "0%", x2: "50%", y2: "100%" };
+ }
+
+ if (direction === "to-top-right") {
+ return { x1: "0%", y1: "100%", x2: "100%", y2: "0%" };
+ }
+
+ return { x1: "0%", y1: "0%", x2: "100%", y2: "100%" };
+}
+
+export function exportLogoSvg(logo: LogoConfig): string {
+ const gradient = getGradientCoordinates(logo.gradientDirection);
+ const safeText = escapeSvgText(logo.text.trim() || "Logo");
+ const safeTagline = escapeSvgText(logo.tagline.trim());
+
+ return ``;
+}
\ No newline at end of file
diff --git a/apps/web/src/features/logo-builder/store/logo-store.ts b/apps/web/src/features/logo-builder/store/logo-store.ts
new file mode 100644
index 0000000..928ff76
--- /dev/null
+++ b/apps/web/src/features/logo-builder/store/logo-store.ts
@@ -0,0 +1,138 @@
+"use client";
+
+import { create } from "zustand";
+import type {
+ LogoConfig,
+ LogoFontFamily,
+ LogoGradientDirection,
+} from "@/features/logo-builder/types/logo";
+
+export const defaultLogoConfig: LogoConfig = {
+ text: "RainbowCode",
+ tagline: "Design visually. Ship clean code.",
+ fontFamily: "Inter",
+ fontWeight: 800,
+ letterSpacing: -2,
+ primaryColor: "#4f46e5",
+ secondaryColor: "#db2777",
+ backgroundColor: "#020617",
+ gradientDirection: "to-bottom-right",
+ radius: 32,
+};
+
+type LogoStoreState = {
+ readonly logo: LogoConfig;
+ readonly updateText: (text: string) => void;
+ readonly updateTagline: (tagline: string) => void;
+ readonly updateFontFamily: (fontFamily: LogoFontFamily) => void;
+ readonly updateFontWeight: (fontWeight: number) => void;
+ readonly updateLetterSpacing: (letterSpacing: number) => void;
+ readonly updatePrimaryColor: (primaryColor: string) => void;
+ readonly updateSecondaryColor: (secondaryColor: string) => void;
+ readonly updateBackgroundColor: (backgroundColor: string) => void;
+ readonly updateGradientDirection: (
+ gradientDirection: LogoGradientDirection,
+ ) => void;
+ readonly updateRadius: (radius: number) => void;
+ readonly resetLogo: () => void;
+};
+
+export const useLogoStore = create((set) => ({
+ logo: defaultLogoConfig,
+
+ updateText: (text) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ text,
+ },
+ }));
+ },
+
+ updateTagline: (tagline) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ tagline,
+ },
+ }));
+ },
+
+ updateFontFamily: (fontFamily) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ fontFamily,
+ },
+ }));
+ },
+
+ updateFontWeight: (fontWeight) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ fontWeight,
+ },
+ }));
+ },
+
+ updateLetterSpacing: (letterSpacing) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ letterSpacing,
+ },
+ }));
+ },
+
+ updatePrimaryColor: (primaryColor) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ primaryColor,
+ },
+ }));
+ },
+
+ updateSecondaryColor: (secondaryColor) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ secondaryColor,
+ },
+ }));
+ },
+
+ updateBackgroundColor: (backgroundColor) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ backgroundColor,
+ },
+ }));
+ },
+
+ updateGradientDirection: (gradientDirection) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ gradientDirection,
+ },
+ }));
+ },
+
+ updateRadius: (radius) => {
+ set((state) => ({
+ logo: {
+ ...state.logo,
+ radius,
+ },
+ }));
+ },
+
+ resetLogo: () => {
+ set({
+ logo: defaultLogoConfig,
+ });
+ },
+}));
\ No newline at end of file
diff --git a/apps/web/src/features/logo-builder/tests/export-logo-svg.test.ts b/apps/web/src/features/logo-builder/tests/export-logo-svg.test.ts
new file mode 100644
index 0000000..130b493
--- /dev/null
+++ b/apps/web/src/features/logo-builder/tests/export-logo-svg.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it } from "vitest";
+import { exportLogoSvg } from "@/features/logo-builder/exporters/export-logo-svg";
+import { defaultLogoConfig } from "@/features/logo-builder/store/logo-store";
+
+describe("exportLogoSvg", () => {
+ it("exports a valid svg string", () => {
+ const svg = exportLogoSvg(defaultLogoConfig);
+
+ expect(svg).toContain("