Skip to content

Refactor base color ramp to fixed-lightness OKLCH with mode-adaptive interactions - #456

Merged
jackgranatowski merged 6 commits into
mainfrom
claude/pr455-token-display-mb0imj
Jun 30, 2026
Merged

jackgranatowski merged 6 commits into
mainfrom
claude/pr455-token-display-mb0imj

Conversation

@jackgranatowski

Copy link
Copy Markdown
Contributor

Summary

Migrates the --sf-color-base-* family from a mode-dependent V-shaped ramp (mixing toward text/surface) to a fixed-lightness OKLCH ramp that is mode-independent and monotonic. The numbered scale (50–950) now uses absolute lightness values (0.97 → 0.06), while hover and active states become mode-adaptive via --sf-is-dark calculations.

Key Changes

  • Base ramp definition: Replaced color-mix(in oklab, ...) expressions with oklch(from var(--sf-color-base) L c h) syntax, where L is fixed per step (50=0.97, 100=0.94, ..., 950=0.06)

    • Preserves hue and chroma from the source --sf-color-base while fixing lightness
    • Ensures 50 is always near-white and 950 is always near-black, regardless of light/dark mode or source color position
    • Aligns with Tailwind and Radix design patterns
  • Interaction states: --sf-color-base--hover and --sf-color-base--active now use mode-aware formulas:

    • --hover: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.04 + var(--sf-is-dark) * 0.08), 0.99) c h)
    • --active: oklch(from var(--sf-color-base) clamp(0.01, calc(l - 0.08 + var(--sf-is-dark) * 0.16), 0.99) c h)
    • These darken in light mode and lighten in dark mode, replacing the previous fixed aliases to base-darker and base-xdark
  • Documentation: Updated token comments to reflect the new fixed-lightness approach and removed semantic alias mappings (superlight, xlight, lighter, darker, xdark, superdark) from the main ramp definition

  • Generated artifacts: Regenerated docs/api-index.json, docs/api-index.md, docs/token-index.json, docs/token-index.md, and docs/tokens.md to reflect new token values

  • Demo generation: Updated demos/generate.mjs to:

    • Filter classes by bundle membership (exclude empty bundles)
    • Mark tokens that inherit contextually (--sf-color-mark-text, --sf-color-selection-text) with data-inherit attribute
    • Display "inherit (contextual)" for empty inherited tokens instead of "(empty)"
    • Updated CDN reference to use slashed.optimal-components.css

Implementation Details

The new approach uses CSS relative color syntax (oklch(from ...)) to derive all steps from a single source token while maintaining perceptual consistency. The fixed lightness scale is independent of the source color's original position, making the ramp predictable and composable across light/dark modes without requiring separate palette-mix variables.

https://claude.ai/code/session_017TkkKpQTutqD8LcaYC2wtN

claude added 4 commits June 30, 2026 07:19
F1: switch full-api-demo pages from slashed.optimal.css to slashed.full.css
so all 691 tokens (including 6 btn/field component tokens) resolve correctly.

F2: mark --sf-color-mark-text and --sf-color-selection-text with data-inherit
in token tiles; refresh() now shows "inherit (contextual)" instead of "(empty)"
so users understand these are fallback-only override hooks, not broken tokens.

F3: document base family exception in llm-guide.md — the 50=Lightest/950=Darkest
contract holds for the five mid-tone brand families but not for base (near-white
source), which is a surface-elevation ramp; direct users to semantic aliases.

F4: exclude classes with bundles:[] from demo tiles — theme-transition lives
only in optional/theme-example.css (copy-and-customise, not shipped); its tile
was inert and misleading. .sf-theme-transition (shipped in motion.css) remains.
slashed.full.css includes utilities.css and components.css which are both
STAGED (all class definitions commented out). optimal-components is the
correct bundle: adds only tokens.components.css (the 6 active btn/field
tokens) without pulling in dead staged code.
…ch steps

The old formula (color-mix text↔base for steps 50-400, base↔text for
600-950) produced a V-shaped ramp for the near-white base source: steps
50→400 darkened, step 500 spiked back to near-white, steps 600→950
re-darkened. This also made base-lighter (step 400) visually darker
than base-darker (step 600) — semantically inverted aliases.

New approach: fixed OKLCH L steps (0.97 at 50 → 0.06 at 950) preserving
the base source hue and chroma, same strategy as Tailwind/Radix. The ramp
is now strictly monotonic and mode-independent. Aliases are semantically
correct: lighter (0.65) is lighter than darker (0.37).

base--hover and base--active are switched from step aliases to relative
oklch offsets (l - 0.04 / l - 0.08), so hover/active states adapt
correctly to both light mode (base≈0.96) and dark mode (base≈0.22).

The --sf-palette-mix-* knobs are unchanged and still govern the five
mid-tone brand families (primary/secondary/tertiary/action/neutral).

docs/llm-guide.md updated: base now fully honours the 50=Lightest
contract; the palette-mix knobs note clarifies they do not affect base.
l - 0.04 darkens in both modes, which is wrong for dark mode (base ≈ 0.22
going to 0.18 is barely perceptible and in the wrong direction).

Use --sf-is-dark flag (0 in light, 1 in dark) to flip the direction:
  light: l - 0.04        = 0.92  (darkens near-white surface, visible)
  dark:  l - 0.04 + 0.08 = 0.26  (lightens near-dark surface, visible)
Same logic for --active with a larger delta (0.08 / 0.30).
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jackgranatowski, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3e34a0-a5c5-454e-8065-e432eeac88fe

📥 Commits

Reviewing files that changed from the base of the PR and between 4202fa2 and d608f42.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • badges/badge-optimal.json
  • configurator/src/data/api-index.generated.json
  • configurator/src/data/token-registry.generated.json
  • core/tokens.css
  • demos/full-api-demo-with-overrides.html
  • demos/full-api-demo.html
  • demos/generate.mjs
  • docs/api-index.json
  • docs/api-index.md
  • docs/llm-guide.md
  • docs/migration.md
  • docs/registry.json
  • docs/token-annotations.json
  • docs/token-index.json
  • docs/token-index.md
  • docs/tokens.md
  • tests/token-api.snapshot.json
  • token-registry.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/pr455-token-display-mb0imj

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Refactor base color ramp to fixed-lightness OKLCH with mode-adaptive states

✨ Enhancement 🐞 Bug fix 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Replace the --sf-color-base-* numeric ramp with fixed-lightness OKLCH steps (50–950).
• Make --sf-color-base--hover/--active mode-adaptive via --sf-is-dark lightness offsets.
• Fix and clarify the Full API demo (bundle selection, inherited tokens, exclude unshipped classes).
Diagram

graph TD
  A["--sf-color-base"] --> B["core/tokens.css"] --> C["Base ramp 50–950"] --> D["Hover/Active (mode-adaptive)"]
  B --> E(["docs index generation"]) --> F[("docs/*.json, *.md")]
  B --> G(["configurator sync"]) --> H[("configurator api-index")]
  B --> I(["demos/generate.mjs"]) --> J["Full API demo pages"]

  subgraph Legend
    direction LR
    _tok["Token"] ~~~ _file["CSS/HTML file"] ~~~ _script(["Script"]) ~~~ _gen[("Generated artifact")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep `color-mix()` ramp with updated mix knobs
  • ➕ Likely broader browser support than relative color syntax in older environments
  • ➕ Keeps ramp behavior aligned with existing --sf-palette-mix-* mental model
  • ➖ Still mode-dependent and harder to guarantee monotonic/absolute lightness for near-white/near-black base sources
  • ➖ Harder to enforce the ‘50 lightest / 950 darkest’ contract across themes and inputs
2. Precompute light/dark ramps during build (emit explicit values)
  • ➕ Deterministic output per mode; no runtime --sf-is-dark math
  • ➕ Can generate fallback-friendly values for older CSS engines
  • ➖ Doubles token surface area (light + dark variants) or requires additional indirection
  • ➖ Adds build complexity and makes ad-hoc token overrides less composable
3. Retain hover/active as aliases to fixed steps (e.g., 600/800)
  • ➕ Simpler mental model and fewer formulas
  • ➕ Consistent with other families’ alias patterns
  • ➖ Does not invert behavior in dark mode (hover/active may reduce contrast)
  • ➖ Couples interaction affordances to the numeric scale rather than intent (state change)

Recommendation: The PR’s approach (fixed-lightness OKLCH ramp + mode-adaptive hover/active) is the best fit for a ‘predictable, composable’ base scale: it enforces monotonic steps regardless of mode, while preserving interaction intent by inverting the lightness delta based on --sf-is-dark. The main tradeoff to validate is CSS relative color syntax support in your supported browser matrix; if that’s a concern, the build-time precompute alternative is the safest fallback.

Files changed (11) +146 / -160

Bug fix (3) +20 / -24
full-api-demo-with-overrides.htmlFix demo CSS bundle reference and remove unshipped theme class tile +5/-9

Fix demo CSS bundle reference and remove unshipped theme class tile

• Switches the CDN stylesheet from 'slashed.optimal.css' to 'slashed.optimal-components.css' and updates the subtitle to match. Removes the 'theme-transition' tile that is not present in shipped bundles, leaving only 'sf-theme-transition'.

demos/full-api-demo-with-overrides.html

full-api-demo.htmlFix demo CSS bundle reference and remove unshipped theme class tile +5/-9

Fix demo CSS bundle reference and remove unshipped theme class tile

• Switches the CDN stylesheet from 'slashed.optimal.css' to 'slashed.optimal-components.css' and updates the subtitle to match. Removes the 'theme-transition' tile that is not present in shipped bundles, leaving only 'sf-theme-transition'.

demos/full-api-demo.html

generate.mjsImprove demo generation: bundle filtering and contextual inherit tokens +10/-6

Improve demo generation: bundle filtering and contextual inherit tokens

• Updates the CDN target to 'slashed.optimal-components.css' and filters class tiles to only those that belong to at least one bundle. Marks contextual/fallback-only tokens ('--sf-color-mark-text', '--sf-color-selection-text') with a 'data-inherit' attribute so the UI displays “inherit (contextual)” instead of “(empty)”, and improves generation logging to report excluded example-only classes.

demos/generate.mjs

Refactor (1) +38 / -50
tokens.cssReplace base ramp with fixed-lightness OKLCH and mode-adaptive states +38/-50

Replace base ramp with fixed-lightness OKLCH and mode-adaptive states

• Replaces the '--sf-color-base-{50..950}' V-shaped 'color-mix()' ramp with a monotonic, mode-independent set of fixed OKLCH lightness steps using 'oklch(from var(--sf-color-base) L c h)'. Updates '--sf-color-base--hover' and '--sf-color-base--active' from step aliases to relative lightness offsets that darken in light mode and lighten in dark mode using '--sf-is-dark', and revises the explanatory comments accordingly.

core/tokens.css

Documentation (6) +71 / -69
api-index.jsonRegenerate API index JSON for new base ramp and interaction values +16/-16

Regenerate API index JSON for new base ramp and interaction values

• Updates '--sf-color-base-{50..950}' values from 'color-mix()' expressions to fixed-lightness 'oklch(from ...)' values. Updates '--sf-color-base--hover/--active' from aliases to mode-adaptive relative OKLCH formulas and clears their 'aliasOf' fields.

docs/api-index.json

api-index.mdRegenerate API index table for updated base tokens +13/-13

Regenerate API index table for updated base tokens

• Updates the rendered token table rows for base ramp steps and hover/active to reflect the new OKLCH definitions. Removes references to the old alias-based hover/active values in the published markdown table.

docs/api-index.md

llm-guide.mdDocument base-family exception and fixed-lightness generation model +3/-1

Document base-family exception and fixed-lightness generation model

• Clarifies that '--sf-palette-mix-*' describes ramp generation for the five mid-tone brand families, while 'base' uses fixed OKLCH lightness steps. Adds guidance to use surface tokens or the new mode-adaptive base hover/active tokens for mode-safe adjustments.

docs/llm-guide.md

token-index.jsonRegenerate token index JSON for base ramp and interaction tokens +13/-13

Regenerate token index JSON for base ramp and interaction tokens

• Updates the declared values for base ramp steps and '--sf-color-base--hover/--active' to the new fixed/relative OKLCH expressions. Keeps file provenance pointing back to 'core/tokens.css'.

docs/token-index.json

token-index.mdRegenerate token index markdown for updated base token values +13/-13

Regenerate token index markdown for updated base token values

• Updates the token listing for the base family to show fixed-lightness OKLCH steps and the new mode-adaptive hover/active formulas. Keeps the rest of the index unchanged aside from regenerated values.

docs/token-index.md

tokens.mdRegenerate tokens reference table for base family values +13/-13

Regenerate tokens reference table for base family values

• Updates the base token rows in the compact token reference to reflect the new OKLCH ramp and hover/active computations. This keeps the public token documentation aligned with 'core/tokens.css'.

docs/tokens.md

Other (1) +17 / -17
api-index.generated.jsonRegenerate configurator API index for new base token values +17/-17

Regenerate configurator API index for new base token values

• Updates the generated token catalog hash and replaces base ramp/interaction token values to match the new relative OKLCH definitions. Removes alias relationships for '--sf-color-base--hover/--active' since they are now computed expressions.

configurator/src/data/api-index.generated.json

@qodo-code-review

qodo-code-review Bot commented Jun 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 6 rules

Grey Divider


Remediation recommended

1. Demo class count wrong ✓ Resolved 🐞 Bug ≡ Correctness
Description
demos/generate.mjs now excludes classes with empty bundles from rendered sections, but the demo
header still displays ${classes.length}. This makes demos/full-api-demo*.html report 239 classes
even though one class (theme-transition) is excluded from rendering due to having "bundles": [].
Code

demos/generate.mjs[658]

+    <p style="color:var(--sf-color-text--muted)">v${VERSION} · optimal-components bundle from jsDelivr CDN · ${classes.length} classes · ${tokens.length} tokens (${knobs.length} configurable)</p>
Relevance

⭐⭐⭐ High

Team accepted similar count/summary desync fixes (token-aware header count) in PR #402; likely
accept demo count fix.

PR-#402
PR-#454

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The generator filters rendered classes to those with non-empty bundle membership, but the header
still uses the unfiltered classes.length. The API index contains a concrete class
(theme-transition) with an empty bundles array, proving there is at least one excluded class while
the header count remains unchanged.

demos/generate.mjs[358-365]
demos/generate.mjs[654-660]
docs/api-index.json[25381-25400]
demos/full-api-demo.html[122-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The full API demo header prints `${classes.length} classes`, but the page rendering now filters out classes with `bundles.length === 0`. This causes the displayed count to be incorrect and confusing.

### Issue Context
The PR intentionally excludes “example-only” classes (those not shipped in any bundle). The generator already computes `bundledClassCount` for logging, but it does not use it in the demo header.

### Fix Focus Areas
- demos/generate.mjs[358-366]
- demos/generate.mjs[654-660]

### Suggested fix
- Compute `bundledClassCount` once (e.g. near `classes`/`tokens` definitions or inside `buildDemo`) and use it in the header string instead of `classes.length`.
- Optionally show both numbers, e.g. `238 bundled classes (1 example-only excluded)`.
- Regenerate `demos/full-api-demo.html` and `demos/full-api-demo-with-overrides.html` after updating the generator.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

claude added 2 commits June 30, 2026 08:05
The demo header was displaying classes.length (239) while the page itself
only renders bundled classes (238), hiding the theme-transition example-only class.
Compute bundledClassCount once at module level and use it in the subtitle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TkkKpQTutqD8LcaYC2wtN
… family

base is an absolute lightness scale (step 50 always near-white, 950 always
near-black), so direction-named aliases lied depending on where the source
sits — e.g. against a near-white base in light mode --sf-color-base-lighter
resolved *darker* than --sf-color-base. Remove the six relative aliases
(superlight/xlight/lighter/darker/xdark/superdark).

The numeric ramp (base-50…950) covers fixed shades; the surface tokens
(bg/inset/raised) cover mode-adaptive elevation; hover/active cover
interaction states. Each concern now has exactly one home. The relative
aliases remain on the five mid-tone brand families, where the source sits
mid-ramp and the names are honest.

Also drop the comparative reference from the base ramp comment and the
llm-guide. Regenerate docs/api-index/token-index/registry/configurator,
update the token API snapshot, and document the removal in migration.md.

BREAKING CHANGE: --sf-color-base-{superlight,xlight,lighter,darker,xdark,superdark}
removed. See docs/migration.md for the replacement map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TkkKpQTutqD8LcaYC2wtN
@jackgranatowski
jackgranatowski merged commit 1e873ff into main Jun 30, 2026
13 checks passed
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.

2 participants