From a43f87c636b8d09581ac60039f46e031d651e588 Mon Sep 17 00:00:00 2001 From: SunilKumarKV Date: Thu, 11 Jun 2026 16:40:38 +0530 Subject: [PATCH] feat(brand-studio): add typography system --- .../components/brand-studio-panel.tsx | 2 + .../components/typography-system-panel.tsx | 320 ++++++++++++++++++ .../exporters/export-typography-css.ts | 22 ++ .../brand-studio/store/typography-store.ts | 117 +++++++ .../tests/export-typography-css.test.ts | 14 + .../tests/typography-store.test.ts | 56 +++ .../features/brand-studio/types/typography.ts | 28 ++ 7 files changed, 559 insertions(+) create mode 100644 apps/web/src/features/brand-studio/components/typography-system-panel.tsx create mode 100644 apps/web/src/features/brand-studio/exporters/export-typography-css.ts create mode 100644 apps/web/src/features/brand-studio/store/typography-store.ts create mode 100644 apps/web/src/features/brand-studio/tests/export-typography-css.test.ts create mode 100644 apps/web/src/features/brand-studio/tests/typography-store.test.ts create mode 100644 apps/web/src/features/brand-studio/types/typography.ts 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 760150d..1fbbd0b 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 @@ -6,6 +6,7 @@ import { exportBrandJson } from "@/features/brand-studio/exporters/export-brand- 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"; +import { TypographySystemPanel } from "@/features/brand-studio/components/typography-system-panel"; export function BrandStudioPanel() { const brand = useBrandStore((state) => state.brand); @@ -186,6 +187,7 @@ export function BrandStudioPanel() { + ); diff --git a/apps/web/src/features/brand-studio/components/typography-system-panel.tsx b/apps/web/src/features/brand-studio/components/typography-system-panel.tsx new file mode 100644 index 0000000..d1f8dbc --- /dev/null +++ b/apps/web/src/features/brand-studio/components/typography-system-panel.tsx @@ -0,0 +1,320 @@ +"use client"; + +import { RbcBadge } from "@/components/ui/rbc-badge"; +import { RbcButton } from "@/components/ui/rbc-button"; +import { exportTypographyCss } from "@/features/brand-studio/exporters/export-typography-css"; +import { useTypographyStore } from "@/features/brand-studio/store/typography-store"; +import type { + BrandFontFamily, + BrandTypographySystem, +} from "@/features/brand-studio/types/typography"; +import { downloadFile } from "@/features/theme-engine/exporters/download-file"; + +const fonts: readonly BrandFontFamily[] = [ + "Inter", + "Poppins", + "Montserrat", + "Playfair Display", + "Space Grotesk", + "System UI", +]; + +const scaleTokens: readonly (keyof BrandTypographySystem["scale"])[] = [ + "xs", + "sm", + "base", + "lg", + "xl", + "2xl", + "3xl", + "4xl", +]; + +const fieldClass = + "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 TypographySystemPanel() { + const typography = useTypographyStore((state) => state.typography); + const updateHeadingFont = useTypographyStore( + (state) => state.updateHeadingFont, + ); + const updateBodyFont = useTypographyStore((state) => state.updateBodyFont); + const updateHeadingWeight = useTypographyStore( + (state) => state.updateHeadingWeight, + ); + const updateBodyWeight = useTypographyStore( + (state) => state.updateBodyWeight, + ); + const updateLineHeight = useTypographyStore( + (state) => state.updateLineHeight, + ); + const updateLetterSpacing = useTypographyStore( + (state) => state.updateLetterSpacing, + ); + const updateScaleToken = useTypographyStore( + (state) => state.updateScaleToken, + ); + const resetTypography = useTypographyStore( + (state) => state.resetTypography, + ); + + function handleExportCss(): void { + downloadFile( + "rainbowcode-typography.css", + exportTypographyCss(typography), + "text/css", + ); + } + + return ( +
+
+
+ +
+
+
+ Typography + CSS Export +
+ +

+ Brand typography system +

+ +

+ Define the font system used by brand kits, themes, components, + and generated code. +

+
+ + + Reset + +
+
+ +
+
+

+ Preview +

+ +

+ Build beautiful interfaces faster. +

+ +

+ RainbowCode typography tokens keep brand, theme, component, canvas, + and code output visually consistent. +

+ +
+ {scaleTokens.map((token) => ( +
+

{token}

+

+ Aa +

+
+ ))} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + + updateHeadingWeight(Number(event.currentTarget.value)) + } + className={fieldClass} + /> +
+ +
+ + + updateBodyWeight(Number(event.currentTarget.value)) + } + className={fieldClass} + /> +
+
+ +
+
+ + + updateLineHeight(Number(event.currentTarget.value)) + } + className={fieldClass} + /> +
+ +
+ + + updateLetterSpacing(Number(event.currentTarget.value)) + } + className={fieldClass} + /> +
+
+ +
+

+ Type Scale +

+ + {scaleTokens.map((token) => ( +
+ + + updateScaleToken(token, event.currentTarget.value) + } + className={fieldClass} + /> +
+ ))} +
+ + + Export Typography CSS + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/features/brand-studio/exporters/export-typography-css.ts b/apps/web/src/features/brand-studio/exporters/export-typography-css.ts new file mode 100644 index 0000000..b63bea5 --- /dev/null +++ b/apps/web/src/features/brand-studio/exporters/export-typography-css.ts @@ -0,0 +1,22 @@ +import type { BrandTypographySystem } from "@/features/brand-studio/types/typography"; + +export function exportTypographyCss(typography: BrandTypographySystem): string { + return `:root { + --rbc-font-heading: "${typography.headingFont}", system-ui, sans-serif; + --rbc-font-body: "${typography.bodyFont}", system-ui, sans-serif; + --rbc-font-heading-weight: ${typography.headingWeight}; + --rbc-font-body-weight: ${typography.bodyWeight}; + --rbc-line-height: ${typography.lineHeight}; + --rbc-letter-spacing: ${typography.letterSpacing}em; + + --rbc-text-xs: ${typography.scale.xs}; + --rbc-text-sm: ${typography.scale.sm}; + --rbc-text-base: ${typography.scale.base}; + --rbc-text-lg: ${typography.scale.lg}; + --rbc-text-xl: ${typography.scale.xl}; + --rbc-text-2xl: ${typography.scale["2xl"]}; + --rbc-text-3xl: ${typography.scale["3xl"]}; + --rbc-text-4xl: ${typography.scale["4xl"]}; +} +`; +} \ No newline at end of file diff --git a/apps/web/src/features/brand-studio/store/typography-store.ts b/apps/web/src/features/brand-studio/store/typography-store.ts new file mode 100644 index 0000000..cfffe4c --- /dev/null +++ b/apps/web/src/features/brand-studio/store/typography-store.ts @@ -0,0 +1,117 @@ +"use client"; + +import { create } from "zustand"; +import type { + BrandFontFamily, + BrandTypographySystem, +} from "@/features/brand-studio/types/typography"; + +export const defaultTypographySystem: BrandTypographySystem = { + headingFont: "Inter", + bodyFont: "Inter", + headingWeight: 800, + bodyWeight: 500, + lineHeight: 1.5, + letterSpacing: -0.02, + scale: { + xs: "0.75rem", + sm: "0.875rem", + base: "1rem", + lg: "1.125rem", + xl: "1.25rem", + "2xl": "1.5rem", + "3xl": "1.875rem", + "4xl": "2.25rem", + }, +}; + +type TypographyStoreState = { + readonly typography: BrandTypographySystem; + readonly updateHeadingFont: (headingFont: BrandFontFamily) => void; + readonly updateBodyFont: (bodyFont: BrandFontFamily) => void; + readonly updateHeadingWeight: (headingWeight: number) => void; + readonly updateBodyWeight: (bodyWeight: number) => void; + readonly updateLineHeight: (lineHeight: number) => void; + readonly updateLetterSpacing: (letterSpacing: number) => void; + readonly updateScaleToken: ( + token: keyof BrandTypographySystem["scale"], + value: string, + ) => void; + readonly resetTypography: () => void; +}; + +export const useTypographyStore = create((set) => ({ + typography: defaultTypographySystem, + + updateHeadingFont: (headingFont) => { + set((state) => ({ + typography: { + ...state.typography, + headingFont, + }, + })); + }, + + updateBodyFont: (bodyFont) => { + set((state) => ({ + typography: { + ...state.typography, + bodyFont, + }, + })); + }, + + updateHeadingWeight: (headingWeight) => { + set((state) => ({ + typography: { + ...state.typography, + headingWeight, + }, + })); + }, + + updateBodyWeight: (bodyWeight) => { + set((state) => ({ + typography: { + ...state.typography, + bodyWeight, + }, + })); + }, + + updateLineHeight: (lineHeight) => { + set((state) => ({ + typography: { + ...state.typography, + lineHeight, + }, + })); + }, + + updateLetterSpacing: (letterSpacing) => { + set((state) => ({ + typography: { + ...state.typography, + letterSpacing, + }, + })); + }, + + updateScaleToken: (token, value) => { + set((state) => ({ + typography: { + ...state.typography, + scale: { + ...state.typography.scale, + [token]: value, + }, + }, + })); + }, + + resetTypography: () => { + set({ + typography: defaultTypographySystem, + }); + }, +})); \ No newline at end of file diff --git a/apps/web/src/features/brand-studio/tests/export-typography-css.test.ts b/apps/web/src/features/brand-studio/tests/export-typography-css.test.ts new file mode 100644 index 0000000..aebd00d --- /dev/null +++ b/apps/web/src/features/brand-studio/tests/export-typography-css.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { exportTypographyCss } from "@/features/brand-studio/exporters/export-typography-css"; +import { defaultTypographySystem } from "@/features/brand-studio/store/typography-store"; + +describe("exportTypographyCss", () => { + it("exports typography css variables", () => { + const css = exportTypographyCss(defaultTypographySystem); + + expect(css).toContain("--rbc-font-heading"); + expect(css).toContain("--rbc-font-body"); + expect(css).toContain("--rbc-text-4xl"); + expect(css).toContain("Inter"); + }); +}); \ No newline at end of file diff --git a/apps/web/src/features/brand-studio/tests/typography-store.test.ts b/apps/web/src/features/brand-studio/tests/typography-store.test.ts new file mode 100644 index 0000000..ee763fa --- /dev/null +++ b/apps/web/src/features/brand-studio/tests/typography-store.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + defaultTypographySystem, + useTypographyStore, +} from "@/features/brand-studio/store/typography-store"; + +describe("useTypographyStore", () => { + beforeEach(() => { + useTypographyStore.getState().resetTypography(); + }); + + it("starts with default typography system", () => { + expect(useTypographyStore.getState().typography).toEqual( + defaultTypographySystem, + ); + }); + + it("updates font families", () => { + useTypographyStore.getState().updateHeadingFont("Poppins"); + useTypographyStore.getState().updateBodyFont("Montserrat"); + + expect(useTypographyStore.getState().typography.headingFont).toBe( + "Poppins", + ); + expect(useTypographyStore.getState().typography.bodyFont).toBe( + "Montserrat", + ); + }); + + it("updates typography metrics", () => { + useTypographyStore.getState().updateHeadingWeight(700); + useTypographyStore.getState().updateBodyWeight(400); + useTypographyStore.getState().updateLineHeight(1.6); + useTypographyStore.getState().updateLetterSpacing(-0.04); + + expect(useTypographyStore.getState().typography.headingWeight).toBe(700); + expect(useTypographyStore.getState().typography.bodyWeight).toBe(400); + expect(useTypographyStore.getState().typography.lineHeight).toBe(1.6); + expect(useTypographyStore.getState().typography.letterSpacing).toBe(-0.04); + }); + + it("updates scale token", () => { + useTypographyStore.getState().updateScaleToken("4xl", "3rem"); + + expect(useTypographyStore.getState().typography.scale["4xl"]).toBe("3rem"); + }); + + it("resets typography", () => { + useTypographyStore.getState().updateHeadingFont("Poppins"); + useTypographyStore.getState().resetTypography(); + + expect(useTypographyStore.getState().typography).toEqual( + defaultTypographySystem, + ); + }); +}); \ No newline at end of file diff --git a/apps/web/src/features/brand-studio/types/typography.ts b/apps/web/src/features/brand-studio/types/typography.ts new file mode 100644 index 0000000..3333ab5 --- /dev/null +++ b/apps/web/src/features/brand-studio/types/typography.ts @@ -0,0 +1,28 @@ +export type BrandFontFamily = + | "Inter" + | "Poppins" + | "Montserrat" + | "Playfair Display" + | "Space Grotesk" + | "System UI"; + +export type BrandTypographyScale = { + readonly xs: string; + readonly sm: string; + readonly base: string; + readonly lg: string; + readonly xl: string; + readonly "2xl": string; + readonly "3xl": string; + readonly "4xl": string; +}; + +export type BrandTypographySystem = { + readonly headingFont: BrandFontFamily; + readonly bodyFont: BrandFontFamily; + readonly headingWeight: number; + readonly bodyWeight: number; + readonly lineHeight: number; + readonly letterSpacing: number; + readonly scale: BrandTypographyScale; +}; \ No newline at end of file