Skip to content

[ColorSync] Convert color values to semantic tokens in label-image#3435

Open
Myranae wants to merge 17 commits intomainfrom
tb/testing-font-color-conversion
Open

[ColorSync] Convert color values to semantic tokens in label-image#3435
Myranae wants to merge 17 commits intomainfrom
tb/testing-font-color-conversion

Conversation

@Myranae
Copy link
Copy Markdown
Contributor

@Myranae Myranae commented Mar 31, 2026

Summary:

As the first step of our reignited Color Sync project, we're converting label image's color values to semantic tokens.

  • Replaced three hardcoded hex colors (#00880b, #ECF3FE) and one rgba value (rgba(33, 36, 44, 0.32)) in answer-pill.tsx, marker.tsx, and label-image.tsx with semantic tokens from @khanacademy/wonder-blocks-tokens
  • Added Chromatic visual regression stories (label-image-initial-state-regression.stories.tsx and label-image-interactions-regression.stories.tsx) to establish a baseline before conversion and capture diffs after

Issue: LEMS-3994

Test plan:

  • Chromatic diffs for the color changes have been reviewed and approved against the regression story baseline
  • pnpm lint, pnpm tsc, and pnpm test all pass
  • Storybook play functions verified: all interaction stories (MarkerOpened, AnswerSelected, CorrectAnswerGraded, IncorrectAnswerWithPill, MathChoicesVisible) complete without errors — Interactions tab shows green checkmarks
  • Visual states confirmed in Storybook:
    • Unanswered: marker pulsates, no selection
    • Answer selected: marker filled (solid blue, instructive.default)
    • Correct graded: marker and pill render green (success.strong)
    • Incorrect: marker and pill render neutral gray (neutral.default)
    • Instructions with choices: separator dots visible between choice items

@Myranae Myranae self-assigned this Mar 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

Size Change: -7 B (0%)

Total Size: 495 kB

Filename Size Change
packages/perseus/dist/es/index.js 193 kB -7 B (0%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.5 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.21 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 11.9 kB
packages/perseus-core/dist/es/index.js 25.1 kB
packages/perseus-editor/dist/es/index.js 101 kB
packages/perseus-linter/dist/es/index.js 9.3 kB
packages/perseus-score/dist/es/index.js 9.66 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/strings.js 8.09 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (9097781) and published it to npm. You
can install it using the tag PR3435.

Example:

pnpm add @khanacademy/perseus@PR3435

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3435

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3435

Myranae added 14 commits March 31, 2026 10:24
Though these colors are not touched during this conversion, we still want regression stories that cover all states of the widget
…n vs figma color

The Figma design was based on using only one point in all the widgets, so it is the interactive graph point. This project does not have a redesign goal, so the Figma does not reflect how the widget should look.
@Myranae Myranae requested a review from mark-fitzgerald April 1, 2026 16:41
@Myranae Myranae changed the title [WIP] Convert color values to semantic tokens in label-image [ColorSync] Convert color values to semantic tokens in label-image Apr 1, 2026
@Myranae Myranae marked this pull request as ready for review April 1, 2026 21:22
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@Myranae
Copy link
Copy Markdown
Contributor Author

Myranae commented Apr 1, 2026

@claude review once

Comment on lines 863 to +866
marginLeft: 5,
marginRight: 5,

background: "rgba(33, 36, 44, 0.32)",
background: semanticColor.core.border.neutral.default,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 The separator dot pseudo-element in instructionsChoice uses semanticColor.core.border.neutral.default for its CSS background property, but border tokens are semantically distinct from background tokens and may resolve to different values in non-default themes (dark mode, high-contrast). The correct token is semanticColor.core.background.neutral.default, consistent with markerIncorrect in marker.tsx and the incorrect pill in answer-pill.tsx.

Extended reasoning...

Wrong semantic token category used for separator dot background

What the bug is: In label-image.tsx line 866, the instructionsChoice style block uses semanticColor.core.border.neutral.default as the value of the CSS background property for the ::after pseudo-element that renders separator dots between instruction choices. Border tokens and background tokens are distinct categories in a design token system and are not interchangeable — they exist for different semantic purposes and can resolve to different hues, opacities, or color values across themes.

The specific code path: The instructionsChoice style defines a ::after pseudo-element that creates a 2×2px filled circle (via borderRadius: 2) used as a visual separator dot between each non-last choice item. This is purely a filled background element. The PR replaces the original rgba(33, 36, 44, 0.32) with semanticColor.core.border.neutral.default — a token that belongs to the border category, not the background category.

Why existing code does not prevent this: There is no type-level enforcement preventing a border token from being passed to a background CSS property; both are plain string values at runtime. The bug is a semantic/intent mismatch, not a type error.

Impact: In the default light theme, border and background neutral tokens may happen to be visually similar, so the bug could go unnoticed. However, in dark mode or high-contrast themes, these tokens are likely to diverge. Since the stated purpose of this PR is improved theme support via semantic token conversion, using the wrong token category directly undermines that goal. The separator dots could render with incorrect color or opacity in non-default themes.

Proof by example: Consider the pattern established within this same PR:

  • marker.tsx:286markerIncorrect style: background: semanticColor.core.background.neutral.default
  • answer-pill.tsx:91incorrect style: backgroundColor: semanticColor.core.background.neutral.default
  • marker.tsx:213markerIcon border: border: \2px solid ${semanticColor.core.border.neutral.default}`` ✅ (correct: border token for an actual CSS border)
  • answer-pill.tsx:12 — box shadow: boxShadow: \0 8px 8px ${semanticColor.core.border.neutral.default}`` ✅ (correct: border token for a shadow outline)
  • label-image.tsx:866 — separator dot: background: semanticColor.core.border.neutral.default ❌ (wrong: border token for a fill/background)

All three neutral-colored filled elements use background.neutral.default; the only other uses of border.neutral.default are for actual border and boxShadow properties. Line 866 breaks this consistent pattern.

Fix: Replace semanticColor.core.border.neutral.default with semanticColor.core.background.neutral.default at label-image.tsx:866 to match the pattern used for all other neutral-colored background elements in this widget.

"@khanacademy/perseus": patch
---

Convert font and colors to semantic tokens for label image
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The changeset description says "Convert font and colors to semantic tokens for label image", but this PR only converts color properties — no font-related tokens are changed anywhere. This will produce a misleading CHANGELOG entry; the description should be updated to "Convert hardcoded color values to semantic tokens for label image".

Extended reasoning...

The changeset file at .changeset/nervous-moons-roll.md (line 5) reads: Convert font and colors to semantic tokens for label image. However, examining every modified file in this PR reveals that only background color and border color properties were converted to semantic tokens — no font-related properties (font-size, font-weight, font-family, text color, etc.) were touched.

The specific color conversions made in this PR are:

  • answer-pill.tsx: backgroundColor: "#00880b"semanticColor.core.background.success.strong
  • label-image.tsx: background: "rgba(33, 36, 44, 0.32)"semanticColor.core.border.neutral.default
  • marker.tsx: backgroundColor: "#ECF3FE"semanticColor.core.background.instructive.subtle and background: "#00880b"semanticColor.core.background.success.strong

All four converted values are background or border color properties. None of them are font or typography tokens. The PR title itself confirms this scope: "[ColorSync] Convert color values to semantic tokens in label-image" — no mention of fonts.

The git branch name tb/testing-font-color-conversion and the git log entries (e.g., "Change background to foreground color", "Revert to straight token conversion vs figma color") suggest that font/foreground color conversion was originally planned or experimented with during development, but was ultimately not included in this PR. The changeset description was likely written when font conversion was still planned and never updated when that work was removed.

The impact is purely a documentation inaccuracy: the generated CHANGELOG entry for @khanacademy/perseus will claim that font tokens were converted when they were not. This could confuse consumers reviewing the changelog trying to understand the scope of this patch.

To fix this, the changeset description should be updated to accurately reflect what was done: Convert hardcoded color values to semantic tokens for label image.

Comment on lines +41 to +44
// including TeX fraction choices and the rgba(33, 36, 44, 0.32) separator dots
// that appear between each choice.
export const WithChoicesInInstructions: Story = {
args: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The WithChoicesInInstructions story comment on line 42 still reads "the rgba(33, 36, 44, 0.32) separator dots" — the exact hardcoded value this PR replaces with semanticColor.core.border.neutral.default. Update the comment to reference the semantic token instead.

Extended reasoning...

This PR replaces rgba(33, 36, 44, 0.32) with semanticColor.core.border.neutral.default in label-image.tsx (line 863). The new story file label-image-initial-state-regression.stories.tsx was written as part of this same PR to serve as a visual regression baseline, but the comment on the WithChoicesInInstructions story was never updated to reflect the token-based implementation.

The stale reference appears at lines 41-42 of the new file:

// including TeX fraction choices and the rgba(33, 36, 44, 0.32) separator dots
// that appear between each choice.

This is immediately contradicted by label-image.tsx where the instructionsChoice::after pseudo-element now uses background: semanticColor.core.border.neutral.default instead of the raw RGBA value.

The comment was almost certainly written before the color conversion was made and was not revisited. Since the file is brand new (added in this PR), there is no pre-existing history to blame — the inaccuracy was introduced the moment this PR was created.

Step-by-step proof: A developer reads WithChoicesInInstructions to understand what it tests. The comment says the separator dots use rgba(33, 36, 44, 0.32). They search the codebase for that string and find nothing, because this PR deleted it. They are now misled about what color value is used. The actual color is resolved at runtime from semanticColor.core.border.neutral.default, a value that may differ across themes.

The fix is a one-line comment update: replace rgba(33, 36, 44, 0.32) with semanticColor.core.border.neutral.default in the story comment. No functional code is affected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants