fix(theme): align avatar gradients to Crewli brand teal + diverse palette
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) <noreply@anthropic.com>
This commit is contained in:
@@ -382,7 +382,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
: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<MenuItem[]>(() => [
|
||||
<Avatar
|
||||
:label="userInitials"
|
||||
shape="circle"
|
||||
:pt="{ root: { style: 'background: linear-gradient(135deg, #f472b6, var(--p-primary-500, #0d9488)); color: #fff; width:40px; height:40px; font-size:14px;' } }"
|
||||
:pt="{ root: { style: 'background: linear-gradient(135deg, #f472b6, var(--p-primary-500, #0D9394)); color: #fff; width:40px; height:40px; font-size:14px;' } }"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-[13.5px] font-semibold text-[var(--p-text-color)]">
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user