Skip to content

feat(diagram): accurate text color for tags with custom colors#2978

Merged
davydkov merged 2 commits into
likec4:mainfrom
farhan523:feat/tag-text-contrast
May 23, 2026
Merged

feat(diagram): accurate text color for tags with custom colors#2978
davydkov merged 2 commits into
likec4:mainfrom
farhan523:feat/tag-text-contrast

Conversation

@farhan523

Copy link
Copy Markdown
Collaborator

Checklist

  • I've thoroughly read the latest contribution guidelines.
  • I've rebased my branch onto main before creating this PR.
  • I've added tests to cover my changes (if applicable).
  • I've verified pnpm typecheck and pnpm test.
  • I've added changesets.
  • My change requires documentation updates. (no — internal styling change, no public DSL/CLI surface)
  • I've updated the documentation accordingly. (N/A)

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 (autoTextColor variant in the likec4tag recipe).

This is the cleanup the issue describes: emit --colors-likec4-tag-text from TagStylesProvider whenever a custom color is set, and drop the autoTextColor recipe variant.

Implementation

  • @likec4/core/styles — export getContrastedColorsAPCA (already used internally by computeColorValues for element colors since #2105). Reuses the existing chroma-js + APCA pipeline so we don't introduce a second contrast implementation.
  • @likec4/diagramTagStylesContext.tsx — in generateColorVars, the custom-color branch now computes getContrastedColorsAPCA(color).hiContrast and emits --colors-likec4-tag-text alongside --colors-likec4-tag-bg and --colors-likec4-tag-bg-hover. Radix-named tags are unchanged.
  • @likec4/style-presetlikec4tag.ts — the autoTextColor variant (including the staticCss entry, defaultVariants, and CSS-filter trick) is removed. Inner-span color is set unconditionally to likec4.tag.text in base.
  • ElementTags.tsx — drops the autoTextColor: isTagColorSpecified(spec) prop on the likec4tag() call. The useTagSpecification import is removed (no longer needed in this file).

Test plan

New unit tests in packages/diagram/src/context/TagStylesContext.spec.ts:

  • Emits all three CSS vars (bg, bg-hover, text) for a custom hex color, with a derived text color
  • Derives a light text color for dark custom backgrounds (#000000 → text channels > 0x99)
  • Derives a dark text color for light custom backgrounds (#ffffff → text channels < 0x66)
  • Still emits radix-based vars (including text) for a named tag color (tomato)
  • Uses dark-2 text for radix backgrounds that are light at scale 9 (grass, lime, yellow, amber)
  • Returns '' for an unrecognised color

Local 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 workspace styled-system/styles/dist/) are git-ignored per AGENTS.md, so CI's pnpm generate step will produce the updated outputs.

Thanks to @Kiiv for the APCA contrast work in #2105 — this PR reuses that infrastructure.

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-bot

changeset-bot Bot commented May 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 56f8cb9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
@likec4/core Patch
@likec4/diagram Patch
@likec4/style-preset Patch
@likec4/playground Patch
@likec4/config Patch
@likec4/generators Patch
@likec4/language-server Patch
@likec4/language-services Patch
@likec4/layouts Patch
@likec4/leanix-bridge Patch
@likec4/spa Patch
likec4 Patch
@likec4/mcp Patch
@likec4/react Patch
@likec4/vite-plugin Patch
@likec4/vscode-preview Patch
likec4-vscode Patch
@likec4/styles Patch
@likec4/docs-astro Patch
@likec4/lsp Patch
@likec4/log Patch
@likec4/tsconfig Patch

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

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Tag text color APCA contrast derivation

Layer / File(s) Summary
Export APCA contrast and add color validation
packages/core/src/styles/index.ts, packages/core/src/styles/compute-color-values.ts
Core styles re-exports getContrastedColorsAPCA and adds isValidColor for safe color validation.
Generate tag color variables with APCA contrast
packages/diagram/src/context/TagStylesContext.tsx, packages/diagram/src/context/TagStylesContext.spec.ts
generateColorVars is exported and uses isValidColor + getContrastedColorsAPCA to derive --colors-likec4-tag-text for explicit colors and emits bg/bg-hover/text vars; test suite expanded to cover hex, #000/#fff, named radices, light-scale exceptions, unknowns, and malformed inputs.
Remove spec-driven styling from ElementTag
packages/diagram/src/base-primitives/element/ElementTags.tsx
ElementTag no longer reads tag specification for auto text-color and renders likec4tag() unconditionally, relying on emitted CSS variables.
Remove autoTextColor variant from likec4tag recipe
styled-system/preset/src/recipes/likec4tag.ts
Text & > span color and first-child opacity moved into base styles; autoTextColor variant and related staticCss removed.
Document APCA contrast implementation
.changeset/tag-text-contrast.md
Changelog entry describes APCA-based tag text color derivation, unified CSS var emission, and removal of autoTextColor across packages.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • likec4/likec4#2912: Related changes to getContrastedColorsAPCA and core color-contrast logic that this PR re-exports and consumes.

Suggested reviewers

  • davydkov
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing APCA-based text color derivation for tags with custom colors, replacing the CSS-filter workaround.
Description check ✅ Passed The PR description comprehensively covers the checklist, summarizes the change, details implementation across all affected packages, and includes test verification results.
Linked Issues check ✅ Passed All objectives from issue #2143 are met: APCA-based text color derivation is implemented, --colors-likec4-tag-text is emitted from TagStylesProvider, the autoTextColor variant is removed, and proper color validation is added.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing APCA-based tag text colors: exports, context generation, recipe updates, component changes, and tests are all aligned with issue #2143 objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/diagram/src/context/TagStylesContext.tsx (1)

12-37: ⚡ Quick win

Consider adding JSDoc to the now-exported function.

Now that generateColorVars is 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

📥 Commits

Reviewing files that changed from the base of the PR and between f692562 and 66ef7dc.

📒 Files selected for processing (6)
  • .changeset/tag-text-contrast.md
  • packages/core/src/styles/index.ts
  • packages/diagram/src/base-primitives/element/ElementTags.tsx
  • packages/diagram/src/context/TagStylesContext.spec.ts
  • packages/diagram/src/context/TagStylesContext.tsx
  • styled-system/preset/src/recipes/likec4tag.ts

Comment thread packages/diagram/src/context/TagStylesContext.tsx Outdated
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.
@farhan523

Copy link
Copy Markdown
Collaborator Author

Pushed 56f8cb9 addressing the CodeRabbit finding on validation.

Verified the concern against current code:

  • isTagColorSpecified is a prefix check only (accepts #zzz, rgb(garbage), …).
  • getContrastedColorsAPCA calls chroma(refColor) with no guard — it'd throw on malformed input and crash the diagram render. Real bug.

Fix:

  • Added a tiny isValidColor helper in @likec4/core/styles (wraps chroma.valid).
  • generateColorVars now requires both isTagColorSpecified and isValidColor before invoking APCA. Malformed values fall through to the default tag styling.
  • New regression test in TagStylesContext.spec.ts covers #zzzzzz, #12, rgb(garbage)expect(() => ...).not.toThrow() + returns ''. All 7 tests passing.

Diff: 4 files, +25 / -3.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
packages/diagram/src/context/TagStylesContext.spec.ts (1)

59-68: 💤 Low value

Consider eliminating the redundant call in the malformed input test.

Each malformed input is passed to generateColorVars twice (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 value

Consider adding JSDoc for the exported function.

Since generateColorVars is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 66ef7dc and 56f8cb9.

📒 Files selected for processing (4)
  • packages/core/src/styles/compute-color-values.ts
  • packages/core/src/styles/index.ts
  • packages/diagram/src/context/TagStylesContext.spec.ts
  • packages/diagram/src/context/TagStylesContext.tsx

@davydkov davydkov enabled auto-merge (squash) May 23, 2026 09:46
@davydkov davydkov merged commit 75e1510 into likec4:main May 23, 2026
14 checks passed
@likec4-ci likec4-ci Bot mentioned this pull request May 23, 2026
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.

[feature request] accurate text color for tags

2 participants