From 9d50aeaeab4e73327a952920ce26385f2229deb6 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 10 Sep 2026 13:18:56 -0400 Subject: [PATCH] Unify brand palette across site emails previews and sign defaults --- .../events/[id]/sign/design-controls.tsx | 15 ++--- .../app/dashboard/events/[id]/sign/studio.tsx | 3 +- apps/web/src/app/globals.css | 62 +++++++++---------- apps/web/src/app/layout.tsx | 3 + apps/web/src/lib/brand.test.ts | 19 ++++++ apps/web/src/lib/guest-sign.ts | 9 +-- apps/web/src/server/social-image.tsx | 13 ++-- packages/contracts/src/brand.ts | 14 +++++ packages/contracts/src/index.ts | 1 + packages/email/src/index.ts | 25 ++++---- packages/email/src/logo.ts | 6 +- 11 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 apps/web/src/lib/brand.test.ts create mode 100644 packages/contracts/src/brand.ts diff --git a/apps/web/src/app/dashboard/events/[id]/sign/design-controls.tsx b/apps/web/src/app/dashboard/events/[id]/sign/design-controls.tsx index 99d59f6..f4c285c 100644 --- a/apps/web/src/app/dashboard/events/[id]/sign/design-controls.tsx +++ b/apps/web/src/app/dashboard/events/[id]/sign/design-controls.tsx @@ -1,4 +1,5 @@ "use client"; +import { brandPalette } from "@album/contracts"; import { useRef, useState } from "react"; import { toast } from "sonner"; @@ -9,11 +10,11 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import type { SignDesign } from "@/lib/guest-sign"; const presets = { - linenCard: { layout: "compact", paper: "card6x4", background: "#f1eadf", accent: "#473a30", decoration: "minimal", font: "funnel" }, - indigo: { layout: "typography", background: "#4055b5", accent: "#ffffff", decoration: "minimal", font: "funnel" }, - linen: { layout: "photo", background: "#f1eadf", accent: "#473a30", decoration: "minimal", font: "funnel" }, + linenCard: { layout: "compact", paper: "card6x4", background: brandPalette.background, accent: brandPalette.ink, decoration: "minimal", font: "funnel" }, + indigo: { layout: "typography", background: brandPalette.primary, accent: "#ffffff", decoration: "minimal", font: "funnel" }, + linen: { layout: "photo", background: brandPalette.background, accent: brandPalette.ink, decoration: "minimal", font: "funnel" }, forest: { layout: "photo", background: "#153e35", accent: "#ffffff", decoration: "minimal", font: "funnel" }, - mono: { layout: "typography", background: "#f4f2ec", accent: "#171717", decoration: "minimal", font: "funnel" }, + mono: { layout: "typography", background: brandPalette.background, accent: brandPalette.ink, decoration: "minimal", font: "funnel" }, } as const; export function SignDesignControls({ design, onChange, onBusy }: { design: SignDesign; onChange: (patch: Partial) => void; onBusy: (busy: boolean) => void }) { @@ -52,12 +53,12 @@ export function SignDesignControls({ design, onChange, onBusy }: { design: SignD
{(["customWidth", "customHeight"] as const).map((key, i) => {i === 0 ? "Width" : "Height"} onChange({ [key]: e.target.value === "" ? 0 : Number(e.target.value) })} />)}
Portrait, square, or landscape. 3–48 inches per edge. Small signs should be scanned up close. : null} - Design preset + Design preset Heading font {design.layout === "compact" ? "The Linen text-and-QR design without a photo. Starts at 6 × 4 inches; choose any paper size above." : design.layout === "photo" ? "A large event photo with a compact QR section. Add a photo below to enable sign export." : "Bold typography, a full-color background, and a prominent QR. Your uploaded photo is kept for photo presets."}
- Background onChange({ background: e.target.value })} /> - Accent onChange({ accent: e.target.value })} />Text adapts for contrast against the background. + Background onChange({ background: e.target.value })} /> + Accent onChange({ accent: e.target.value })} />Text adapts for contrast against the background.
{([{ key: "backgroundImage", label: "Background photo" }, { key: "logoImage", label: "Your logo" }] as const).map(field => {field.label} { const file = e.target.files?.[0]; e.target.value = ""; if (file) void upload(file, field.key); }} /> diff --git a/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx b/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx index 97420e6..f77d636 100644 --- a/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx +++ b/apps/web/src/app/dashboard/events/[id]/sign/studio.tsx @@ -1,4 +1,5 @@ "use client"; +import { brandPalette } from "@album/contracts"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; @@ -23,7 +24,7 @@ export function GuestSignStudio({ eventId, title, guestUrl, qr, warning, savedUr eventId: string; title: string; guestUrl: string; qr: SignQr; warning: string | null; savedUrl: string | null; savedRevision: string | null; canSave: boolean; }) { - const [design, setDesign] = useState({ title, headline: "Share your favorite moments", message: "Help us see the day through your eyes. Add your photos to our shared album.", paper: "letter", ink: "indigo", font: "funnel", layout: "typography", background: "#4055b5", accent: "#ffffff", decoration: "minimal", showBrand: true, customWidth: 8.5, customHeight: 11, unit: "in" }); + const [design, setDesign] = useState({ title, headline: "Share your favorite moments", message: "Help us see the day through your eyes. Add your photos to our shared album.", paper: "letter", ink: "indigo", font: "funnel", layout: "typography", background: brandPalette.primary, accent: "#ffffff", decoration: "minimal", showBrand: true, customWidth: 8.5, customHeight: 11, unit: "in" }); const [fontError, setFontError] = useState(false); const [assetBusy, setAssetBusy] = useState(false); const [loadingDesign, setLoadingDesign] = useState(Boolean(savedUrl)); diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index ce816b8..cdf8d74 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -51,52 +51,52 @@ :root { color-scheme: light; - --background: #f4f1ea; - --foreground: #171716; - --card: #fffefa; - --card-foreground: #171716; - --popover: #fffefa; - --popover-foreground: #171716; - --primary: #3156c8; + --background: var(--brand-background); + --foreground: var(--brand-ink); + --card: var(--brand-surface); + --card-foreground: var(--brand-ink); + --popover: var(--brand-surface); + --popover-foreground: var(--brand-ink); + --primary: var(--brand-primary); --primary-foreground: #ffffff; --secondary: #e7e4dc; --secondary-foreground: #252526; --muted: #e9e5dc; - --muted-foreground: #5e5a52; - --accent: #e4e9f8; + --muted-foreground: var(--brand-muted); + --accent: var(--brand-accent); --accent-foreground: #203b90; --destructive: oklch(0.58 0.22 27); - --border: #c9c3b7; + --border: var(--brand-border); --input: #c7c1b6; - --ring: #3156c8; - --chart-1: #3156c8; + --ring: var(--brand-primary); + --chart-1: var(--brand-primary); --chart-2: oklch(0.64 0.12 210); --chart-3: oklch(0.7 0.13 145); --chart-4: oklch(0.72 0.13 75); --chart-5: oklch(0.56 0.12 300); --radius: 0.75rem; - --sidebar: #fffefa; - --sidebar-foreground: #171716; - --sidebar-primary: #3156c8; + --sidebar: var(--brand-surface); + --sidebar-foreground: var(--brand-ink); + --sidebar-primary: var(--brand-primary); --sidebar-primary-foreground: #ffffff; - --sidebar-accent: #e4e9f8; + --sidebar-accent: var(--brand-accent); --sidebar-accent-foreground: #203b90; - --sidebar-border: #c9c3b7; - --sidebar-ring: #3156c8; + --sidebar-border: var(--brand-border); + --sidebar-ring: var(--brand-primary); } .dark { color-scheme: dark; - --background: #0d1018; - --foreground: #f4f1ea; - --card: #171b26; - --card-foreground: #f4f1ea; + --background: var(--brand-darkBackground); + --foreground: var(--brand-background); + --card: var(--brand-darkSurface); + --card-foreground: var(--brand-background); --popover: #1b202d; - --popover-foreground: #f4f1ea; - --primary: #7f9cff; + --popover-foreground: var(--brand-background); + --primary: var(--brand-darkPrimary); --primary-foreground: #0c1634; --secondary: #262b37; - --secondary-foreground: #f4f1ea; + --secondary-foreground: var(--brand-background); --muted: #242936; --muted-foreground: #bbb8b1; --accent: #202b50; @@ -104,20 +104,20 @@ --destructive: oklch(0.7 0.18 25); --border: #3b4150; --input: #4a5161; - --ring: #7f9cff; - --chart-1: #7f9cff; + --ring: var(--brand-darkPrimary); + --chart-1: var(--brand-darkPrimary); --chart-2: oklch(0.7 0.12 210); --chart-3: oklch(0.72 0.13 145); --chart-4: oklch(0.75 0.13 75); --chart-5: oklch(0.68 0.12 300); - --sidebar: #171b26; - --sidebar-foreground: #f4f1ea; - --sidebar-primary: #7f9cff; + --sidebar: var(--brand-darkSurface); + --sidebar-foreground: var(--brand-background); + --sidebar-primary: var(--brand-darkPrimary); --sidebar-primary-foreground: #0c1634; --sidebar-accent: #202b50; --sidebar-accent-foreground: #b9c7ff; --sidebar-border: #3b4150; - --sidebar-ring: #7f9cff; + --sidebar-ring: var(--brand-darkPrimary); } @keyframes reveal-up { diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index a0e3d48..e02a0ed 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,4 +1,6 @@ import type { Metadata, Viewport } from "next"; +import type { CSSProperties } from "react"; +import { brandPalette } from "@album/contracts"; import { Funnel_Display, Geologica, Inter } from "next/font/google"; import { TRPCReactProvider } from "@/trpc/react"; import { ThemeProvider } from "@/components/theme-provider"; @@ -52,6 +54,7 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { return ( [`--brand-${name}`, value])) as CSSProperties} lang="en" className={`${inter.variable} ${funnelDisplay.variable} ${geologica.variable} font-sans`} suppressHydrationWarning diff --git a/apps/web/src/lib/brand.test.ts b/apps/web/src/lib/brand.test.ts new file mode 100644 index 0000000..b7052bf --- /dev/null +++ b/apps/web/src/lib/brand.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test"; +import { brandPalette } from "@album/contracts"; +import { chrome } from "@album/email"; +import { EMAIL_LOGO_BASE64 } from "../../../../packages/email/src/logo"; +import sharp from "sharp"; + +test("email chrome shares the site palette and embeds a matching monochrome logo", async () => { + const html = chrome("

Welcome

"); + for (const color of [brandPalette.background, brandPalette.surface, brandPalette.ink, brandPalette.primary, brandPalette.border]) expect(html).toContain(color); + expect(html).not.toContain("#8b5a4a"); + const { data } = await sharp(Buffer.from(EMAIL_LOGO_BASE64, "base64")).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); + const expected = [23, 23, 22]; + let opaque = 0; + for (let i = 0; i < data.length; i += 4) if (data[i + 3] === 255) { + expect([...data.subarray(i, i + 3)]).toEqual(expected); + opaque++; + } + expect(opaque).toBeGreaterThan(0); +}); diff --git a/apps/web/src/lib/guest-sign.ts b/apps/web/src/lib/guest-sign.ts index 6acb579..9088740 100644 --- a/apps/web/src/lib/guest-sign.ts +++ b/apps/web/src/lib/guest-sign.ts @@ -1,4 +1,5 @@ import { SIGN_PAPERS, type SavedSign } from "@album/contracts"; +import { brandPalette } from "@album/contracts"; export { SIGN_PAPERS }; export type SignPaper = keyof typeof SIGN_PAPERS | "custom"; export type SignDesign = SavedSign & { @@ -49,16 +50,16 @@ export function signLines(value: string, limit: number) { export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) { const paper = signPaper(design); const color = (value: string | undefined, fallback: string) => /^#[\da-f]{6}$/i.test(value ?? "") ? value! : fallback; - const ink = color(design.accent, design.ink === "black" ? "#171717" : "#4055b5"); + const ink = color(design.accent, design.ink === "black" ? brandPalette.ink : brandPalette.primary); const luminance = (hex: string) => { const channels = [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16) / 255).map(c => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4); return channels[0]! * .2126 + channels[1]! * .7152 + channels[2]! * .0722; }; - const background = color(design.background, "#eeece5"); + const background = color(design.background, brandPalette.background); const raster = (value?: string) => /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(value ?? "") ? value : undefined; const photo = raster(design.backgroundImage), logo = raster(design.logoImage); const surfaceLuminance = luminance(background); - const bodyInk = photo || surfaceLuminance < .18 ? "#ffffff" : "#171717"; + const bodyInk = photo || surfaceLuminance < .18 ? "#ffffff" : brandPalette.ink; const accentContrast = (Math.max(luminance(ink), surfaceLuminance) + .05) / (Math.min(luminance(ink), surfaceLuminance) + .05); const headingInk = !photo && accentContrast >= 4.5 ? ink : bodyInk; const landscape = paper.viewHeight < 850; @@ -75,7 +76,7 @@ export function guestSignSvg(design: SignDesign, guestUrl: string, qr: SignQr) { if (design.layout) { const compact = design.layout === "compact"; const photoLed = design.layout === "photo"; - const foreground = surfaceLuminance < .18 ? "#ffffff" : "#171717"; + const foreground = surfaceLuminance < .18 ? "#ffffff" : brandPalette.ink; const headingColor = accentContrast >= 4.5 ? ink : foreground; const left = landscape && photoLed ? 650 : 70; const top = photoLed ? (landscape ? 80 : 600) : 85; diff --git a/apps/web/src/server/social-image.tsx b/apps/web/src/server/social-image.tsx index 4e1d5c8..f7cdc0e 100644 --- a/apps/web/src/server/social-image.tsx +++ b/apps/web/src/server/social-image.tsx @@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { ImageResponse } from "next/og"; import sharp from "sharp"; +import { brandPalette as palette } from "@album/contracts"; import { BRAND_NAME, BRAND_TAGLINE } from "@/lib/brand"; export async function renderSocialImage(event?: { title: string; bannerUrl: string | null }) { @@ -10,18 +11,20 @@ export async function renderSocialImage(event?: { title: string; bannerUrl: stri readFile(path.join(process.cwd(), "public/fonts/geologica-semibold.ttf")), event?.bannerUrl ? loadBanner(event.bannerUrl) : null, ]); + const ink = banner ? palette.onPrimary : palette.ink; const title = event?.title ?? BRAND_TAGLINE; return new ImageResponse( -
+
{banner ? : null} -
+
+
65 ? 48 : 64, lineHeight: 1.08, maxWidth: 1060, marginBottom: 30 }}>{title.length > 140 ? `${title.slice(0, 137)}…` : title}
- - - + + +
{BRAND_NAME}
diff --git a/packages/contracts/src/brand.ts b/packages/contracts/src/brand.ts new file mode 100644 index 0000000..9e97b5c --- /dev/null +++ b/packages/contracts/src/brand.ts @@ -0,0 +1,14 @@ +/** Shared brand colors for web, transactional mail, social previews and signs. */ +export const brandPalette = { + background: "#f4f1ea", + surface: "#fffefa", + ink: "#171716", + muted: "#5e5a52", + border: "#d8d2c7", + primary: "#3156c8", + onPrimary: "#ffffff", + accent: "#e4e9f8", + darkBackground: "#0d1018", + darkSurface: "#171b26", + darkPrimary: "#7f9cff", +} as const; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 741f753..3e85a9c 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +export { brandPalette } from "./brand"; export { SIGN_PAPERS, savedSignSchema, type SavedSign } from "./sign"; export const eventStatusSchema = z.enum(["draft", "published", "closed"]); diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 3bc696b..7ca6930 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -4,7 +4,8 @@ import { Resend } from "resend"; import { EMAIL_LOGO_CID, emailLogoAttachment } from "./logo"; export { emailBrowserPreview } from "./logo"; -const PRIMARY = "#8b5a4a"; +import { brandPalette as palette } from "@album/contracts"; +const PRIMARY = palette.primary; function escapeHtml(value: string) { return value @@ -27,22 +28,22 @@ export function chrome(bodyHtml: string) { - -
-
+ +
+
- +
ManyanglesManyangles
-

Every angle. One shared album.

+

Every angle. One shared album.

-
+
${bodyHtml}
-
- Manyangles -

Keep the moment.

-

Hadlock Technologies LLC

+
+ Manyangles +

Keep the moment.

+

Hadlock Technologies LLC

@@ -179,7 +180,7 @@ export async function sendPasswordResetEmail(message: { Reset password

-

+

If you did not request this, you can ignore this message.

`), diff --git a/packages/email/src/logo.ts b/packages/email/src/logo.ts index 305fbed..204473c 100644 --- a/packages/email/src/logo.ts +++ b/packages/email/src/logo.ts @@ -1,6 +1,6 @@ -// 3x PNG rendering of BrandMark. Keep v1 immutable for queued email retries. -export const EMAIL_LOGO_CID = "manyangles-mark-v1"; -export const EMAIL_LOGO_BASE64 = "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"; +// 3x PNG rendering of BrandMark in brandPalette.ink. Queued messages retain their embedded image. +export const EMAIL_LOGO_CID = "manyangles-mark-v2"; +export const EMAIL_LOGO_BASE64 = "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"; export const emailLogoAttachment = { filename: "manyangles-logo.png", content: EMAIL_LOGO_BASE64, contentId: EMAIL_LOGO_CID }; export function emailBrowserPreview(html: string) { return html.replaceAll(`cid:${EMAIL_LOGO_CID}`, `data:image/png;base64,${EMAIL_LOGO_BASE64}`);