feat(diagram): accurate text color for tags with custom colors#2978
Conversation
Custom-colored tags (e.g. `tag deprecated { color #FF0000 }`) now
get a high-contrast text color derived from the background via
the APCA contrast algorithm, instead of the previous CSS-filter
hack (`autoTextColor` variant).
- `getContrastedColorsAPCA` is now exported from `@likec4/core/styles`.
- `TagStylesProvider` emits `--colors-likec4-tag-text` for every tag,
including custom-hex ones. Named (Radix) tags are unchanged.
- `autoTextColor` is removed from the `likec4tag` recipe and from
`ElementTags`.
Resolves likec4#2143
🦋 Changeset detectedLatest commit: 56f8cb9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 22 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughThis PR switches tag text color derivation to APCA contrast: core styles export the APCA helper and validation, TagStylesContext computes and emits --colors-likec4-tag-text (with bg/bg-hover), ElementTag stops using spec-driven autoTextColor, and the likec4tag recipe moves text styles into base rules. Tests validate hex, black/white, named radices, light-scale exceptions, unknowns, and malformed inputs. ChangesTag text color APCA contrast derivation
Sequence Diagram(s)sequenceDiagram
participant Spec as TagSpecification
participant Context as TagStylesContext.generateColorVars
participant Core as core/styles.getContrastedColorsAPCA
participant CSS as EmittedCSSVars
participant Component as ElementTag
Spec->>Context: provide color (hex or radix)
Context->>Core: isValidColor? / getContrastedColorsAPCA(color)
Core-->>Context: hiContrast or radix mapping
Context->>CSS: emit --colors-likec4-tag-bg, --colors-likec4-tag-bg-hover, --colors-likec4-tag-text
Component->>CSS: read CSS vars for rendering
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/diagram/src/context/TagStylesContext.tsx (1)
12-37: ⚡ Quick winConsider adding JSDoc to the now-exported function.
Now that
generateColorVarsis exported, it forms part of the public API. Per coding guidelines, public methods should have JSDoc documentation describing parameters, return values, and behavior.📝 Suggested JSDoc
+/** + * Generates CSS custom property declarations for tag styling. + * + * For custom hex colors, derives a high-contrast text color using APCA. + * For known Radix color names, emits Radix-based CSS variable references. + * + * `@param` spec - Tag specification containing the color property + * `@returns` CSS string with custom property declarations, or empty string for unrecognized colors + */ export const generateColorVars = (spec: TagSpecification): string => {As per coding guidelines: Use JSDoc to document public classes and methods.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/diagram/src/context/TagStylesContext.tsx` around lines 12 - 37, Add a JSDoc block above the exported function generateColorVars that documents the function purpose, the parameter spec (type TagSpecification and what fields are used, e.g. spec.color), possible control flow (when isTagColorSpecified(spec) is true vs when color is in radixColors), return type (string containing CSS custom property declarations or empty string) and any important details (which CSS variables are produced, the use of APCA contrasted color via getContrastedColorsAPCA, and the special-case mapping to 'textcolor' for mint/grass/lime/yellow/amber). Keep the description concise and include `@param` and `@returns` tags referencing TagSpecification and string respectively so this exported API is properly documented.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/diagram/src/context/TagStylesContext.tsx`:
- Around line 16-22: The current branch calls getContrastedColorsAPCA(color)
when isTagColorSpecified(spec) is true but isTagColorSpecified only checks
prefixes and can allow unparseable values; update the logic in the
generateColorVars (the block that references isTagColorSpecified and calls
getContrastedColorsAPCA) to validate the color first (e.g., call
chroma.valid(color) or pass the value through the existing
computeFrom/computeColorValues path) and only call getContrastedColorsAPCA when
validation succeeds; if validation fails, fall back to the default tag color
path so malformed strings cannot reach chroma(refColor) and throw.
---
Nitpick comments:
In `@packages/diagram/src/context/TagStylesContext.tsx`:
- Around line 12-37: Add a JSDoc block above the exported function
generateColorVars that documents the function purpose, the parameter spec (type
TagSpecification and what fields are used, e.g. spec.color), possible control
flow (when isTagColorSpecified(spec) is true vs when color is in radixColors),
return type (string containing CSS custom property declarations or empty string)
and any important details (which CSS variables are produced, the use of APCA
contrasted color via getContrastedColorsAPCA, and the special-case mapping to
'textcolor' for mint/grass/lime/yellow/amber). Keep the description concise and
include `@param` and `@returns` tags referencing TagSpecification and string
respectively so this exported API is properly documented.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de822fad-b95c-4f13-925c-2146429bda75
📒 Files selected for processing (6)
.changeset/tag-text-contrast.mdpackages/core/src/styles/index.tspackages/diagram/src/base-primitives/element/ElementTags.tsxpackages/diagram/src/context/TagStylesContext.spec.tspackages/diagram/src/context/TagStylesContext.tsxstyled-system/preset/src/recipes/likec4tag.ts
isTagColorSpecified is only a prefix check (accepts `#zzz`, `rgb(garbage)`, etc.), so a malformed color in a tag spec would reach getContrastedColorsAPCA and throw from chroma(refColor), crashing the diagram render. Validate the value with chroma.valid via a new isValidColor helper exported from @likec4/core/styles, and fall through to the default tag styling when it fails. New regression test in TagStylesContext.spec.ts covers `#zzzzzz`, `likec4#12`, and `rgb(garbage)`. Addresses CodeRabbit feedback on likec4#2978.
|
Pushed 56f8cb9 addressing the CodeRabbit finding on validation. Verified the concern against current code:
Fix:
Diff: 4 files, +25 / -3. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/diagram/src/context/TagStylesContext.spec.ts (1)
59-68: 💤 Low valueConsider eliminating the redundant call in the malformed input test.
Each malformed input is passed to
generateColorVarstwice (lines 63 and 65). You can call once and assert both that it doesn't throw and that the result is empty.♻️ More efficient pattern
- for (const malformed of ['`#zzzzzz`', '`#12`', 'rgb(garbage)']) { - expect(() => generateColorVars({ color: malformed as TagSpecification['color'] })) - .not.toThrow() - const css = generateColorVars({ color: malformed as TagSpecification['color'] }) - expect(css).toBe('') - } + for (const malformed of ['`#zzzzzz`', '`#12`', 'rgb(garbage)']) { + let css: string + expect(() => { + css = generateColorVars({ color: malformed as TagSpecification['color'] }) + }).not.toThrow() + expect(css!).toBe('') + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/diagram/src/context/TagStylesContext.spec.ts` around lines 59 - 68, The test in TagStylesContext.spec.ts redundantly calls generateColorVars twice for each malformed color; change the loop to call generateColorVars once per malformed value, storing the result in a local variable, then assert that the call did not throw and that the returned css string is empty. Update the test that iterates over malformed inputs (referencing generateColorVars and TagSpecification['color']) to use a single invocation per iteration and assert both expectations on the stored result.packages/diagram/src/context/TagStylesContext.tsx (1)
12-12: 💤 Low valueConsider adding JSDoc for the exported function.
Since
generateColorVarsis now exported, adding JSDoc would help consumers understand its purpose and contract (input format, return value structure, validation behavior).📝 Suggested JSDoc
+/** + * Generates CSS custom property declarations for a tag specification. + * For custom colors (hex/rgb), derives APCA-based high-contrast text color. + * For named Radix colors, emits standard Radix scale variables. + * Returns empty string for invalid or unknown colors. + */ export const generateColorVars = (spec: TagSpecification): string => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/diagram/src/context/TagStylesContext.tsx` at line 12, Add JSDoc for the exported function generateColorVars describing its purpose, the TagSpecification parameter shape (required fields, types and any optional properties or validation behavior), and the return value (string format and what CSS variables it contains); include examples of accepted input and note any edge cases (e.g., missing colors or defaults applied) and mention that the function may throw or sanitize invalid values so consumers know expected behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/diagram/src/context/TagStylesContext.spec.ts`:
- Around line 59-68: The test in TagStylesContext.spec.ts redundantly calls
generateColorVars twice for each malformed color; change the loop to call
generateColorVars once per malformed value, storing the result in a local
variable, then assert that the call did not throw and that the returned css
string is empty. Update the test that iterates over malformed inputs
(referencing generateColorVars and TagSpecification['color']) to use a single
invocation per iteration and assert both expectations on the stored result.
In `@packages/diagram/src/context/TagStylesContext.tsx`:
- Line 12: Add JSDoc for the exported function generateColorVars describing its
purpose, the TagSpecification parameter shape (required fields, types and any
optional properties or validation behavior), and the return value (string format
and what CSS variables it contains); include examples of accepted input and note
any edge cases (e.g., missing colors or defaults applied) and mention that the
function may throw or sanitize invalid values so consumers know expected
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4407a1ee-f792-4305-a682-94e373e19c70
📒 Files selected for processing (4)
packages/core/src/styles/compute-color-values.tspackages/core/src/styles/index.tspackages/diagram/src/context/TagStylesContext.spec.tspackages/diagram/src/context/TagStylesContext.tsx
Checklist
mainbefore creating this PR.pnpm typecheckandpnpm test.Summary
Resolves #2143.
Tags with a custom color in the specification (e.g.
tag deprecated { color #FF0000 }) now get an accurate, accessible text color derived from the background via the APCA algorithm, instead of relying on the previous CSS-filter workaround (autoTextColorvariant in thelikec4tagrecipe).This is the cleanup the issue describes: emit
--colors-likec4-tag-textfromTagStylesProviderwhenever a custom color is set, and drop theautoTextColorrecipe variant.Implementation
@likec4/core/styles— exportgetContrastedColorsAPCA(already used internally bycomputeColorValuesfor element colors since #2105). Reuses the existing chroma-js + APCA pipeline so we don't introduce a second contrast implementation.@likec4/diagram—TagStylesContext.tsx— ingenerateColorVars, the custom-color branch now computesgetContrastedColorsAPCA(color).hiContrastand emits--colors-likec4-tag-textalongside--colors-likec4-tag-bgand--colors-likec4-tag-bg-hover. Radix-named tags are unchanged.@likec4/style-preset—likec4tag.ts— theautoTextColorvariant (including thestaticCssentry,defaultVariants, and CSS-filter trick) is removed. Inner-span color is set unconditionally tolikec4.tag.textinbase.ElementTags.tsx— drops theautoTextColor: isTagColorSpecified(spec)prop on thelikec4tag()call. TheuseTagSpecificationimport is removed (no longer needed in this file).Test plan
New unit tests in
packages/diagram/src/context/TagStylesContext.spec.ts:#000000→ text channels > 0x99)#ffffff→ text channels < 0x66)tomato)dark-2text for radix backgrounds that are light at scale 9 (grass, lime, yellow, amber)''for an unrecognised colorLocal checks:
pnpm test packages/diagram/src/context/TagStylesContext.spec.ts→ 6/6 passing,pnpm --filter @likec4/core --filter @likec4/diagram --filter @likec4/style-preset typecheck→ clean,pnpm lint:errors-only→ 0 errors.Generated panda artifacts (
packages/react/styled-system/recipes/likec4tag.{mjs,d.mts}and the workspacestyled-system/styles/dist/) are git-ignored per AGENTS.md, so CI'spnpm generatestep will produce the updated outputs.Thanks to @Kiiv for the APCA contrast work in #2105 — this PR reuses that infrastructure.