From cd118bd165e660b8410adf438db18e2f9b754086 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 21 May 2026 02:38:27 +0200 Subject: [PATCH] fix(theme): align avatar gradients to Crewli brand teal + diverse palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P7 token audit (8330e93f §10) found two off-brand color bugs: - utils/v2/gradient.ts GRADIENT_PALETTE used Tailwind blue-green anchors (teal-500/600 #0d9488, cyan, emerald, sky) clustered in a single hue family. Two problems: the brand-anchor slot used Tailwind teal #0d9488, NOT Crewli's #0D9394, AND orgs in multi-workspace views all rendered as similar teal/green squares (poor distinguishability). Replaced with the crewli-starter SoT palette: [0] #0D9394 → #075F60 (Crewli teal — brand anchor) [1] #7C3AED → #4C1D95 (purple) [2] #EA580C → #9A3412 (orange) [3] #16A34A → #14532D (green) [4] #F59E0B → #92400E (amber) [5] #EC4899 → #9D174D (pink) [6] #4F46E5 → #312E81 (indigo — added for org-distinguishability) [7] #E11D48 → #881337 (rose — added for org-distinguishability) Palette stays 8-entry; only the values change. Indexing logic (djb2 hash % 8) unchanged. Per-org avatar colors are not persisted pre-launch, so the slot reshuffle is safe. - AppTopbar.vue user-avatar gradient (two sites: the trigger Avatar + the user-menu header Avatar). Fallback in the CSS var was #0d9488 (Tailwind teal-600), NOT Crewli #0D9394 — if the var ever fails to resolve, the chrome would render off-brand. Fixed to #0D9394. The hardcoded pink #f472b6 in the gradient's from-color was kept intentionally: it matches the crewli-starter SoT (user avatars are a pink/purple gradient distinct from workspace chrome's teal — the visual contrast between "your account" and "your workspace" is by design). Regression locks: - gradient.spec.ts +2 specs: brand-anchor slot is #0D9394 (and defensively, #0d9488 must not appear anywhere in the palette); palette spans diverse hue families (purple + orange present beyond the teal anchor). Suite delta: 564 → 566 (+2). vue-tsc clean. Scoped ESLint clean (0 errors, pre-existing warnings only). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components-v2/layout/AppTopbar.vue | 4 +-- .../src/utils/v2/__tests__/gradient.spec.ts | 26 +++++++++++++++ apps/app/src/utils/v2/gradient.ts | 33 +++++++++++-------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/apps/app/src/components-v2/layout/AppTopbar.vue b/apps/app/src/components-v2/layout/AppTopbar.vue index 16f84803..898dc5fc 100644 --- a/apps/app/src/components-v2/layout/AppTopbar.vue +++ b/apps/app/src/components-v2/layout/AppTopbar.vue @@ -382,7 +382,7 @@ const userMenuItems = computed(() => [ :label="userInitials" shape="circle" class="cursor-pointer" - :pt="{ root: { style: 'background: linear-gradient(135deg, #f472b6, var(--p-primary-500, #0d9488)); color: #fff;' } }" + :pt="{ root: { style: 'background: linear-gradient(135deg, #f472b6, var(--p-primary-500, #0D9394)); color: #fff;' } }" aria-label="User menu" @click="toggleUserMenu" /> @@ -397,7 +397,7 @@ const userMenuItems = computed(() => [
diff --git a/apps/app/src/utils/v2/__tests__/gradient.spec.ts b/apps/app/src/utils/v2/__tests__/gradient.spec.ts index 8a7d3de4..e041efbd 100644 --- a/apps/app/src/utils/v2/__tests__/gradient.spec.ts +++ b/apps/app/src/utils/v2/__tests__/gradient.spec.ts @@ -62,4 +62,30 @@ describe('computeOrgGradient', () => { expect(matchingEntry).toBeDefined() }) + + // P7-followup-gradient-brand — the brand-anchor slot (index 0) must be + // Crewli's actual brand teal (#0D9394), NOT the Tailwind teal-600 + // (#0d9488) the palette had drifted to. The two are visually similar + // (rgb 13,147,148 vs 13,148,136) but #0D9394 is the brand identity. + // Spec-level lock against re-drift. + it('brand-anchor slot uses Crewli teal #0D9394 (not Tailwind teal #0d9488)', () => { + expect(GRADIENT_PALETTE[0][0].toUpperCase()).toBe('#0D9394') + expect(GRADIENT_PALETTE[0][1].toUpperCase()).toBe('#075F60') + + // Defensive: the wrong Tailwind teal must not appear anywhere in the + // palette as a from-color either. + const allFromColors = GRADIENT_PALETTE.map(([from]) => from.toLowerCase()) + + expect(allFromColors).not.toContain('#0d9488') + }) + + // Org-distinguishability: the palette spans hue families so users with + // multiple workspaces see distinct avatars rather than a wall of teals. + // Light assertion — checks a couple of known-diverse slots are present. + it('palette includes diverse hue families (purple + orange beyond the teal anchor)', () => { + const flat = GRADIENT_PALETTE.flat().map(c => c.toUpperCase()) + + expect(flat).toContain('#7C3AED') // purple + expect(flat).toContain('#EA580C') // orange + }) }) diff --git a/apps/app/src/utils/v2/gradient.ts b/apps/app/src/utils/v2/gradient.ts index 391b6748..b0f1443b 100644 --- a/apps/app/src/utils/v2/gradient.ts +++ b/apps/app/src/utils/v2/gradient.ts @@ -1,25 +1,30 @@ /** * computeOrgGradient — deterministic gradient pair for an organisation. * - * Maps an org `id` string to one of 8 teal-adjacent colour pairs via a - * simple djb2-style character-code hash. Same id always returns the - * same pair; no external dependencies. + * Maps an org `id` string to one of 8 colour pairs via a simple djb2-style + * character-code hash. Same id always returns the same pair; no external + * dependencies. * - * Palette rationale: teal / cyan / emerald / sea-green family to stay - * on-brand with Crewli's teal primary. Each tuple is [from, to] for a - * 135° linear gradient (darker "to" keeps depth on the logo square). + * Palette rationale (P7-followup-gradient-brand): the diverse crewli-starter + * SoT palette. Slot 1 is Crewli's brand teal (`#0D9394` → `#075F60`) so the + * brand-anchor org renders in true Crewli teal — NOT Tailwind teal-600 + * (`#0d9488`), which had drifted in. Remaining slots span hue families + * (purple / orange / green / amber / pink / indigo / rose) so multi-org + * users see clearly distinct avatars rather than a wall of similar teals. + * Each tuple is `[from, to]` for a 135° linear gradient (darker "to" + * keeps depth on the logo square). */ /** Exported so tests can assert membership without hard-coding values. */ export const GRADIENT_PALETTE: [string, string][] = [ - ['#0d9488', '#0f766e'], // teal-600 → teal-700 - ['#0891b2', '#0e7490'], // cyan-600 → cyan-700 - ['#059669', '#047857'], // emerald-600 → emerald-700 - ['#10b981', '#059669'], // emerald-500 → emerald-600 - ['#0284c7', '#0369a1'], // sky-600 → sky-700 - ['#14b8a6', '#0d9488'], // teal-500 → teal-600 - ['#06b6d4', '#0891b2'], // cyan-500 → cyan-600 - ['#34d399', '#10b981'], // emerald-400 → emerald-500 + ['#0D9394', '#075F60'], // Crewli teal (brand anchor) + ['#7C3AED', '#4C1D95'], // purple + ['#EA580C', '#9A3412'], // orange + ['#16A34A', '#14532D'], // green + ['#F59E0B', '#92400E'], // amber + ['#EC4899', '#9D174D'], // pink + ['#4F46E5', '#312E81'], // indigo (added for org-distinguishability) + ['#E11D48', '#881337'], // rose (added for org-distinguishability) ] /**