Skip to content

[Rebrand] Migrate site header and footer to DSCO semantic tokens#73985

Merged
stephenliang merged 6 commits into
stagingfrom
stephen/tokenize-header-footer
Jul 22, 2026
Merged

[Rebrand] Migrate site header and footer to DSCO semantic tokens#73985
stephenliang merged 6 commits into
stagingfrom
stephen/tokenize-header-footer

Conversation

@stephenliang

@stephenliang stephenliang commented Jul 20, 2026

Copy link
Copy Markdown
Member

Migrates the legacy site header and footer from hardcoded SCSS colors to DSCO semantic tokens, following the established [Rebrand] in-place tokenization pattern (exemplar: PhoneFrame, #73055). The chrome now responds to the brand router: unchanged (modulo the documented drift below) under the default codeai brand, CADS colors (purple #4c42cf bar, near-black footer) under codeai-next, and pink under codeai-audit — which is the point: the header/footer were among the last audit-opaque surfaces.

Tokenization

One commit per surface, each carrying its full hex-mapping table in the commit message: header + user menu + hamburger (fdf92c79f72), full footer (28738f2184e), LessonProgress vignette gradient (6ea14067e00). Color-value swaps only — zero selector, markup, or layout changes, so the UI-test selector contract (#header_user_menu, .headerlinks, #hamburger-icon, …) is untouched.

Headline exact mappings (no default-brand pixel change): header bar $contrastTealHeader #00818f--background-brand-teal-primary, footer $darkest_gray #292F36--background-neutral-primary-inverse, all whites → --text-neutral-white-fixed / --background-neutral-primary, menu text #292F36--text-neutral-primary.

Documented nearest-token drift on the default brand (per-commit tables have the full list):

Surface Old New
Dropdown/hamburger borders + dividers #D1D4D8 #d4dae1
Menu hover background #e7e8ea #dfe3e9
Hamburger create-account purple #8C52BA #9657c7
Signed-in .display_name chip — no-op: the swapped rule (.user_options .display_name) is pre-existing dead CSS whose selector never matches the HAML (user_header.haml:73 puts .display_name outside .user_options); the visible chip background is .header_user (row above) #5b6770 #69788a
.header_user chip teal (now matches the bar) #0093a4 #00818f
Project save-error red #d62911 #e02d16
Header separator #c6cacd #b7c1cb
Menu shadows (alpha primitives; elevation token TBD in CADS) #0000001a/#0000000f #292f361a/#292f360d
Legacy dropdown text/hover #000/#8c52ba #292f36/#9657c7

New token: --borders-neutral-white-fixed

Design ruled the white-fixed family should be complete across background/text/borders, so the border member is added through the whole bridge (7840c22d255): legacy colors.css (light+dark), canonical colors_codeAi.css + regenerated brandCodeAiNext.css (generator --check green), brandLegacyShim.css, brandCodeAiNextAliases.css, and brandCodeAiAudit.css (pinked #fff8fc, matching siblings). The header buttons' white borders adopt it.

⚠️ Follow-up for design: colors_codeAi.css is a CADS Sandbox export ("do not hand-edit"). The token must also be added to the sandbox source, or the next re-export silently drops it — the generator --check would still pass and only codeai-next would lose the header-button borders (default and audit resolve via other files).

Design rulings applied

The accent-orange header-link/button hovers are deliberately NOT tokenized (de0aff56fb3) — design says that hover does not carry over to the new brand (a marketing-style hover replaces it in the nav redesign), so painting it CADS orange under codeai-next would be wrong. The two sites keep $orange with an explanatory comment and will honestly show non-pink under audit. The small footer (light and dark share variants) is out of scope entirely — no mockup exists; its hexes are untouched, so all footer.feature Eyes baselines stay green. Levelbuilder/local env header colors and shared/css/color.scss are also untouched (note: $header_text in color.scss is now orphaned — its last four consumers were tokenized here; left in place since the shared palette file was fenced off).

Bug fix: left vignette gradient

headerVignetteLeft's gradient string had been missing its closing paren since 2020 (68b79ff4565), so the CSSOM dropped it and the left-edge fade never rendered in the left-anchored scroll state. Fixed (d98d5b6213e) — the left edge now fades like the right edge always has. This is the branch's only behavior change; an Eyes baseline capturing that scroll state would need a one-time re-accept.

Token resolution per brand

flowchart LR
    A["var(--background-brand-teal-primary)<br/>in application.scss"] --> B{data-brand}
    B -->|"code / codeai"| C["colors.css<br/>brand-teal-60 = #00818f<br/>(pixel-identical)"]
    B -->|"codeai-next"| D["brandCodeAiNextAliases.css<br/>→ brandCodeAiNext.css<br/>brand-purple-50 = #4c42cf"]
    B -->|"codeai-audit"| E["brandCodeAiAudit.css<br/>pink"]
Loading

Screenshots

Links

Testing story

./tools/hooks/pre-commit green after every commit batch. generateBrandCodeAiNext.mjs --check green (also the package's npm test). yarn typecheck shows pre-existing errors only (missing generated build artifacts, unrelated). Header unit-test run blocked by the known missing-yarn build locale-artifact issue — the change is CSS-only, lint + generator check are the material gates. Manual browser matrix on local dev (level page, project page, signed-out home): default brand pixel-stable except the documented drift; ?brand=codeai-next renders the purple bar / near-black footer; ?brand=codeai-audit renders the migrated chrome pink. An xhigh multi-agent code review (10 finder angles + adversarial verification) ran over the branch; all confirmed findings are addressed in commits or documented above.

Eyes impact: footer.feature baselines are all small-footer snapshots — untouched. Page-level snapshots containing a signed-in header may show the .header_user chip drift (#0093a4#00818f) and need re-accepts.

🤖 Generated with Claude Code

stephenliang and others added 6 commits July 20, 2026 11:38
…tic tokens

Part of the header/footer chrome-migration train (see PhoneFrame,
c90bb3a, for the prior exemplar). In-place tokenization only: no
data-brand forks, no structural restyle. Replace legacy color.scss
vars/hexes in the header, user menu, and hamburger with legacy DSCO
semantic tokens (plural --borders-*, brand-family names) so the
surfaces recolor under the CADS alias bridge.

Mapping (verified by hex):

dashboard/app/assets/stylesheets/application.scss (.header, .headerlinks,
.header_button, #header_user_create_account, .project-save-error,
#sign_in_or_user, .header_separator, .user_options/.help_contents):
  $contrastTealHeader (#00818f)   -> --background-brand-teal-primary   (#00818f)
  $header_text/$white (#fff)     -> --text-neutral-white-fixed        (#ffffff)
  $orange (#ffa400, hover underline) -> --background-accent-orange-primary (#ffb42e) [drift]
  $orange (#ffa400, button hover bg) -> --background-accent-orange-primary (#ffb42e) [drift]
  $white (border, .header_button)    -> --background-neutral-white-fixed  (#ffffff)
  $light_white (#fff)             -> --background-neutral-primary      (#ffffff)
  $light_primary_500 (#0093a4)    -> --text-brand-teal-primary          (#0093a4)
  $dark_red (#d62911)             -> --background-error-primary         (#e02d16) [drift]
  $teal (#0093a4, .header_user chip) -> --background-brand-teal-primary (#00818f) [drift; unifies chip with the bar]
  $inset_color (#c6cacd, .header_separator) -> --borders-neutral-strong (#b7c1cb) [drift]
  $black (#000)                   -> --text-neutral-primary             (#292f36) [drift]
  $dark_color (#8c52ba, a:hover/:active bg) -> --background-brand-purple-primary (#9657c7) [drift]
  border 1px solid $black         -> --borders-neutral-solid            (#292f36) [drift]

shared/css/user-menu.scss (.user_options/.create_options/.help_contents panels,
.display_name, .help_icon, create-menu text):
  $white (panel bg)          -> --background-neutral-primary   (#ffffff)
  $light_gray_200 (#D1D4D8)  -> --borders-neutral-primary       (#d4dae1) [drift, imperceptible]
  #0000001a, #0000000f (box-shadow) -> --neutral-black-alpha-10, --neutral-black-alpha-5 [drift; alpha primitives, no elevation token exists]
  $light_black (#292F36)     -> --text-neutral-primary          (#292f36)
  $lightest_gray (#e7e8ea)   -> --background-neutral-tertiary    (#dfe3e9) [drift, nearest]
  $charcoal (#5b6770)        -> --background-neutral-octonary    (#69788a) [drift; same mapping PhoneFrame used for $charcoal]
  $white (.display_name/.help_icon text) -> --text-neutral-white-fixed (#ffffff)

shared/css/hamburger.scss (#hamburger-contents, .item, .divider, #hamburger-icon,
.button-signin, .button-create-account):
  $light_white               -> --background-neutral-primary   (#ffffff)
  $light_gray_200            -> --borders-neutral-primary       (#d4dae1) [drift]
  box-shadow alphas          -> --neutral-black-alpha-10/-5 (as above) [drift]
  $lightest_gray             -> --background-neutral-tertiary    (#dfe3e9) [drift]
  $light_black               -> --text-neutral-primary           (#292f36)
  $white (burger bars)       -> --background-neutral-white-fixed (#ffffff)
  $light_black (.button-signin color/border) -> --text-neutral-primary / --borders-neutral-solid (#292f36)
  $light_secondary_500 (#8C52BA, .button-create-account) -> --background-brand-purple-primary / --borders-brand-purple-primary (#9657c7) [drift]
  $white (.button-create-account text) -> --text-neutral-white-fixed (#ffffff)

Two workaround sites are flagged inline: --background-neutral-white-fixed
used as a border color (no border-white token exists), and
--background-accent-orange-primary used for the orange hover underline
(no borders-accent-orange token exists).

$contrastTealHeader was a local application.scss var with no other
callers after this change; removed. All other now-unreferenced
variables live in shared/css/color.scss, which is out of scope for
this change and left untouched.

Out of scope, untouched: the entire SmallFooter (light/dark variants,
application.scss ~689-978, and SmallFooter.tsx), levelbuilder/local
header env colors (application.scss:423-433), shared/css/color.scss
definitions, and the Figma structural reskin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continues the header/footer chrome-migration train (see the header/
user-menu/hamburger commit immediately prior, and PhoneFrame,
c90bb3a, for the original exemplar). In-place tokenization only,
scoped to the full-site .footer block; the SmallFooter utility strip
(.small-footer-base, #more-menu, dark/phone variants,
SmallFooter.tsx) is untouched, per the user's decision that it has no
Figma design of its own.

Mapping (verified by hex):

dashboard/app/assets/stylesheets/application.scss (.footer):
  $darkest_gray (#292F36)          -> --background-neutral-primary-inverse (#292f36)
  $header_text/$white (#fff)      -> --text-neutral-inverse               (#ffffff)

Both are EXACT (inverse tokens pair with the inverse background; no
visual change on any brand).

$darkest_gray and $header_text now have no other callers in the repo,
but both live in shared/css/color.scss, which is out of scope for
this change and left untouched.

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

Continues the header/footer chrome-migration train (see the two
commits immediately prior, and PhoneFrame, c90bb3a, for the
original exemplar).

The header vignette's fade-to-edge gradient stops hardcoded
rgba(231, 232, 234, a) — the old $lightest_gray hex — while the
container it sits on top of already reads
var(--background-neutral-secondary) (#f0f2f5), migrated ahead of this
change. The vignette hadn't kept up: it was fading to a color that no
longer matched its own background. rgba() can't reference a CSS
variable directly, so each stop becomes a color-mix() call that reads
the same token at the stop's original alpha:

  rgba(231, 232, 234, 1) -> color-mix(in srgb, var(--background-neutral-secondary) 100%, transparent)
  rgba(231, 232, 234, 0) -> color-mix(in srgb, var(--background-neutral-secondary) 0%, transparent)

Small drift (#e7e8ea-based fade -> #f0f2f5-based fade) that now makes
the vignette consistent with the surface it fades into, on every
brand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design ruling (2026-07-20): the accent-orange hover states on
.headerlink and .header_button don't carry over to the rebrand — this
hover will eventually be replaced by a marketing-site-style "tag"
hover in the nav redesign, not yet designed. Tokenizing it would
incorrectly paint it CADS orange under codeai-next. Revert both sites
to the original $orange SCSS variable and note why they're
intentionally excluded from tokenization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design ruling (2026-07-20): complete the white-fixed family so
background, text, and borders have a consistent member set across
both token schemas — legacy plural --borders-neutral-white-fixed and
CADS singular --border-neutral-white-fixed, both resolving to white
in every theme and brand.

Adds the token to colors.css (legacy) and colors_codeAi.css (CADS
canonical export), regenerates brandCodeAiNext.css from the canonical
source via scripts/generateBrandCodeAiNext.mjs, and wires the bridge
files (brandLegacyShim.css, brandCodeAiNextAliases.css,
brandCodeAiAudit.css) so the new name resolves under every brand and
theme. Adopts it in the two .header_button border sites in
application.scss that previously reused --background-neutral-white-fixed
as a workaround, removing the workaround comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The left-edge vignette's linear-gradient string has been missing its
closing paren since 68b79ff (2020), so the CSSOM dropped the
invalid background and the left fade never rendered in the
left-anchored scroll state. Surfaced while reviewing the rgba ->
color-mix rewrite, which had preserved the truncation byte-for-byte.

Behavior change: the left-edge fade now renders, matching what
headerVignetteRight has always done. Any Eyes baseline capturing a
left-anchored progress header will need a one-time re-accept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@stephenliang
stephenliang marked this pull request as ready for review July 20, 2026 21:14
@stephenliang
stephenliang requested a review from a team as a code owner July 20, 2026 21:14
@github-actions

Copy link
Copy Markdown

🖼️ Storybook Visual Comparison Report

✅ No Storybook eyes differences detected!

@moshebaricdo moshebaricdo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice, thank you!

@stephenliang
stephenliang merged commit de1e704 into staging Jul 22, 2026
18 checks passed
@stephenliang
stephenliang deleted the stephen/tokenize-header-footer branch July 22, 2026 17:47
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.

3 participants