Skip to content

feat(workspace): logo upload + brand colors in workspace settings#941

Open
mdubel wants to merge 1 commit into
mainfrom
feat/workspace-branding
Open

feat(workspace): logo upload + brand colors in workspace settings#941
mdubel wants to merge 1 commit into
mainfrom
feat/workspace-branding

Conversation

@mdubel

@mdubel mdubel commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Gives organization workspaces a brand identity, configurable from Workspace settings → Profile → Branding (owners/admins only):

  • Logo upload — PNG/JPG/SVG/WebP/GIF up to ~384 KB. The logo replaces the Lucide icon everywhere the workspace is shown (sidebar switcher, workspace home header, workspace picker, profile "Workspaces" list) and falls back to the icon when unset.
  • Main + auxiliary brand colors — two color pickers with a live preview, Save, and Reset-to-default. The main color drives --primary (buttons, links, active nav, focus rings), the auxiliary drives --accent, recoloring the UI in both light and dark mode with an auto-contrast foreground.

How

Both ride the existing PATCH /api/namespaces/:handle path — no new routes. Empty string clears any field.

  • Logo is stored inline as a size-capped base64 data: URL on the workspace record, so it travels with the already-authenticated namespaces.get / users.me payloads and renders via a plain <img src> — no auth-in-<img> problem, no blob-store serving route, no proxy carve-out. The base64 blob is stripped from the audit snapshot.
  • Colors are #rrggbb hex on the same record. A small client component (brand-theme.tsx) converts them to HSL triples (brand-color.ts) and injects a <style> overriding --primary/--accent (+ --ring/--sidebar-* mirrors). It emits both :root{} and .dark{} so the override wins over next-themes' .dark token block.

Full chain: schema → Postgres (migration 0029) + in-memory double → contract → handler → client/hook → settings UI + every display site → MeNamespace so the sidebar sees it.

Tests

pnpm typecheck clean; new/extended tests green:

  • Contract: brand-color hex + logo data-URL accept/reject/clear.
  • Handler: logo + colors mapping, clearing, and that the base64 logo stays out of the audit snapshot.
  • Repo parity: new columns round-trip (in-memory; Postgres branch runs with TEST_DATABASE_URL).
  • Unit: hexToHslTriple / contrast picker.

Existing workspaces (no logo/colors) render byte-identically — BrandTheme returns null and every logo site falls back to the current icon path.

Notes for reviewers

  • Deploy needs db:migrate — migration 0029 adds logo, brand_primary_color, brand_accent_color to workspaces (all nullable).
  • The auxiliary color maps to --accent, which shadcn also uses for subtle hover surfaces (hover:bg-accent) — so a very saturated auxiliary color tints hover states app-wide, especially in dark mode. This matches the "colors used everywhere" intent; happy to scope it to badges/highlights only if preferred (one-line change to which tokens it overrides).

🤖 Generated with Claude Code

Organization workspaces can now set a brand identity from Workspace
settings: an uploadable logo image and a main + auxiliary brand color.

- Logo: stored inline as a size-capped base64 data URL on the namespace
  record (no new upload/serving route — it travels with the existing
  authenticated namespaces.get / users.me payloads and renders via a
  plain <img>). Replaces the Lucide icon in the sidebar switcher,
  workspace header, and workspace picker; falls back to the icon when
  unset.
- Colors: #rrggbb hex on the namespace record, converted to HSL triples
  and injected as a <style> that overrides the --primary / --accent
  design tokens app-wide (plus --ring / --sidebar-* mirrors, with a
  contrast-picked foreground), applied in both light and dark.

All three fields flow through the existing PATCH /api/namespaces/:handle
(empty string clears). The base64 logo is kept out of the audit snapshot.

Covers schema -> Postgres (migration 0029) + in-memory double ->
contract -> handler -> client/hook -> settings UI + every display site,
plus MeNamespace so the sidebar sees it. Tests: contract, handler,
repo parity, and a hex->HSL unit test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mediforce Ignored Ignored Jul 17, 2026 8:37pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds workspace branding (organization workspaces) by extending the existing namespace/workspace record with an inline logo (data: URL) plus primary/accent brand colors, and wiring that through the API, repositories, and UI so the logo/colors render consistently across workspace surfaces.

Changes:

  • Extend workspace/namespace schema + persistence (Postgres + repo) to store logo, brandPrimaryColor, and brandAccentColor, including a migration.
  • Update API contracts/handlers and UI mutation hooks to accept/patch/echo the new fields (and keep the audit snapshot small for logos).
  • Add UI rendering + settings controls: logo upload/removal, color pickers, and runtime token overrides via BrandTheme.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/platform-ui/src/lib/brand-color.ts Pure helpers to convert #rrggbb to HSL triples and choose readable foregrounds.
packages/platform-ui/src/lib/tests/brand-color.test.ts Unit tests for hex→HSL conversion and foreground selection.
packages/platform-ui/src/hooks/use-namespace-mutations.ts Optimistic updates + cache updates for logo and brand colors.
packages/platform-ui/src/components/brand-theme.tsx Client-side token override injector for --primary/--accent (+ mirrors).
packages/platform-ui/src/components/app-shell.tsx Apply BrandTheme and render logos in the namespace switcher.
packages/platform-ui/src/app/workspace-selection/page.tsx Render org logo in workspace selection cards when present.
packages/platform-ui/src/app/(app)/[handle]/settings/page.tsx Add branding section (logo upload + color pickers) in workspace settings.
packages/platform-ui/src/app/(app)/[handle]/page.tsx Render workspace logos in profile/workspace listings and header.
packages/platform-infra/src/postgres/schema/workspace.ts Add new columns to Drizzle workspace table schema.
packages/platform-infra/src/postgres/repositories/namespace-repository.ts Map new fields to/from DB rows and update mutations.
packages/platform-infra/src/postgres/migrations/meta/_journal.json Register migration 0029_workspace_branding.
packages/platform-infra/src/postgres/migrations/0029_workspace_branding.sql Add logo, brand_primary_color, brand_accent_color columns.
packages/platform-infra/src/postgres/tests/namespace-parity.test.ts Ensure repo parity/round-trip includes branding fields.
packages/platform-core/src/schemas/namespace.ts Add Zod schemas for logo + brand colors and extend NamespaceSchema.
packages/platform-core/src/schemas/index.ts Re-export branding schemas/constants.
packages/platform-core/src/interfaces/namespace-repository.ts Extend NamespaceUpdates to include branding fields.
packages/platform-core/src/index.ts Re-export branding schemas/constants at package root.
packages/platform-api/src/handlers/users/get-me.ts Include branding fields in users.me namespace list output.
packages/platform-api/src/handlers/namespaces/namespace-mutations.ts Accept branding updates; omit base64 logo from audit snapshot.
packages/platform-api/src/handlers/namespaces/tests/namespace-mutations.test.ts Tests for updating/clearing branding and audit snapshot behavior.
packages/platform-api/src/contract/users.ts Extend MeNamespaceSchema to include branding fields.
packages/platform-api/src/contract/namespaces.ts Extend update schema to accept logo + brand color fields.
packages/platform-api/src/contract/tests/namespaces.test.ts Contract tests for accepting/rejecting branding inputs.
CHANGELOG.md Add Unreleased entry documenting workspace branding feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +22
/**
* A workspace logo, stored inline as a base64 `data:` image URL (or `""` to
* clear). Kept on the workspace record rather than the blob store so it travels
* with the already-authenticated `namespaces.get` / `users.me` payloads and
* renders via a plain `<img src>` — no separate authenticated fetch. The
* ~256 KiB char cap keeps that payload small; logos should be optimised
* (SVG/PNG) assets, not photos.
*/
Comment on lines +315 to +325
const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
try {
await updateNamespace.mutateAsync({ handle, logo: dataUrl });
} catch {
setLogoError('Failed to upload logo.');
}
Comment on lines +57 to +60
const block = declarations.join(';');
const css = `:root{${block}}\n.dark{${block}}`;

return <style dangerouslySetInnerHTML={{ __html: css }} />;
Comment on lines 25 to +29
avatarUrl: z.string().url().optional(),
icon: z.string().optional(),
logo: z.string().optional(),
brandPrimaryColor: z.string().optional(),
brandAccentColor: z.string().optional(),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants