feat(workspace): logo upload + brand colors in workspace settings#941
Open
mdubel wants to merge 1 commit into
Open
feat(workspace): logo upload + brand colors in workspace settings#941mdubel wants to merge 1 commit into
mdubel wants to merge 1 commit into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
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, andbrandAccentColor, 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(), |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Gives organization workspaces a brand identity, configurable from Workspace settings → Profile → Branding (owners/admins only):
--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/:handlepath — no new routes. Empty string clears any field.data:URL on the workspace record, so it travels with the already-authenticatednamespaces.get/users.mepayloads 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.#rrggbbhex 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'.darktoken block.Full chain: schema → Postgres (migration
0029) + in-memory double → contract → handler → client/hook → settings UI + every display site →MeNamespaceso the sidebar sees it.Tests
pnpm typecheckclean; new/extended tests green:TEST_DATABASE_URL).hexToHslTriple/ contrast picker.Existing workspaces (no logo/colors) render byte-identically —
BrandThemereturnsnulland every logo site falls back to the current icon path.Notes for reviewers
db:migrate— migration0029addslogo,brand_primary_color,brand_accent_colortoworkspaces(all nullable).--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