Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/rules/frontend-styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ Light mode is frozen: reach dark with tokens that adapt, never by changing light
[`docs/colors.md`](../../openmetadata-ui/src/main/resources/ui/docs/colors.md); dark values live under
`.dark-mode` in `globals.css`.

- **One file per token layer:** core tokens (`--color-*`, `--tw-*`) are declared only in core
`globals.css`; project `--om-*` tokens only in `tokens.css`, light on `:root` and dark on
`:root.dark-mode` side by side. Never declare an `--om-*` in `app.less` or a component file —
cross-file import order decides the winner. `variables.less` fallbacks equal the `tokens.css`
light value. Guard: `theme-contract.test.js` (ADR:2026-10-13-om-tokens-have-one-source-file).

- **Scope:** dark mode is AI mode on core/Tailwind surfaces only. Classic mode gets no dark theme
and antd-only surfaces (`.ant-*`) are not themed for dark — migrate them to core instead
(ADR:2026-10-12-dark-mode-targets-ai-mode-core-surfaces-only).
Expand Down
40 changes: 40 additions & 0 deletions docs/decisions/2026-10-13-om-tokens-have-one-source-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Every --om-* token is declared in tokens.css, light and dark side by side

- **Status:** Accepted
- **Revisions:** v1 2026-10-13 (initial)
- **Deciders:** Harshit Shah
- **Guard:** `openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js` (run by `yarn token-drift` and the `token-drift` pre-commit hook)
- **Related:** open-metadata/OpenMetadata#34936, open-metadata/OpenMetadata#35271, ADR:2026-10-12-dark-mode-targets-ai-mode-core-surfaces-only

## Context
Each legacy LESS palette var (`@grey-15`, `@blue-29`, the status tints, …) was defined in three
files: the LESS var and its fallback hex in `variables.less`, the light `--om-*` value on `:root`
in `tokens.css`, and the dark `--om-*` value under `.dark-mode` in `app.less`. #34936 added the
light values to `tokens.css`. That file is imported after `app.less`, and `:root` has the same
specificity as `.dark-mode` on `<html>`, so the light values won and every bridged var
(the app-wide `@grey-15` border, `@grey-1/2/5/6/9/26`, `@blue-29/30`, the status tints) stayed
light in dark mode. #35271 patched the selector to `:root.dark-mode`, but the split itself, where
correctness depends on import order across files, remained. The same split had already let
`variables.less` carry two `@grey-25` definitions with different fallbacks.

## Decision
- Core tokens (`--color-*`, `--tw-*`, and the other Tailwind theme namespaces) are declared only in
core `globals.css`, light in `@theme` and dark in `.dark-mode`.
- Project `--om-*` tokens are declared only in `openmetadata-ui/.../src/styles/tokens.css`. A
token's light value goes on `:root` and its dark value on `:root.dark-mode` in the same file.
`:root.dark-mode` out-ranks `:root` regardless of order.
- No other stylesheet (`app.less`, component `.less`/`.css`) declares an `--om-*` property. The one
exception is `--om-page-height`, a layout value each shell re-declares so its `calc()` resolves
against that shell's navbar height. It is not a themed token.
- Every dark `--om-*` override has a light value in `tokens.css`.
- A `variables.less` bridge (`@x: var(--om-x, <fallback>)`) is defined once, and its fallback
equals the `tokens.css` light value.
- `theme-contract.test.js` enforces all of the above.

## Consequences
Adding or changing a themed legacy colour is a one-file edit, and the guard fails the
`token-drift` hook if a second file starts declaring `--om-*`. Overrides that must beat inline
custom properties on `<html>` (the `.dark-mode body` brand-accent block in `app.less`) target the
core `--tw-*` layer, not `--om-*`, so they are outside this rule. They stay where they are until
the brand injection stops writing inline vars. Revisit if `tokens.css` is split into per-theme
files, in which case the guard must check the import order instead.
8 changes: 4 additions & 4 deletions openmetadata-ui/src/main/resources/ui/docs/colors.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Use background tokens for surfaces, containers, cards, modals, and page-level ba

| Tailwind Class | CSS Variable | Light | Dark | When to Use |
|---|---|---|---|---|
| `tw:bg-brand-primary` | `--color-bg-brand-primary` | `#eff8ff` | Brand 500 at 14% | Light brand tint surface |
| `tw:bg-brand-primary` | `--color-bg-brand-primary` | `#eff8ff` | Brand 600 at 20% | Light brand tint surface |
| `tw:bg-brand-primary_alt` | `--color-bg-brand-primary_alt` | `#eff8ff` | `#191919` | Alternate brand tint |
| `tw:bg-brand-secondary` | `--color-bg-brand-secondary` | `#d1e9ff` | `#1570ef` | Stronger brand tint |
| `tw:bg-brand-solid` | `--color-bg-brand-solid` | `#1570ef` | `#1570ef` | Primary CTA button fill |
Expand Down Expand Up @@ -195,7 +195,7 @@ Use border tokens for input outlines, card dividers, separators, and table borde
| `tw:border-secondary` | `--color-border-secondary` | `#e9eaeb` (gray-200) | `#222222` (gray-800) | Existing subtle border |
| `tw:border-secondary_alt` | `--color-border-secondary_alt` | `rgba(0,0,0,0.08)` | White at 8% | Existing alpha-transparent border |
| `tw:border-tertiary` | `--color-border-tertiary` | `#f5f5f5` (gray-100) | `#222222` | Very subtle dividers |
| `tw:border-brand` | `--color-border-brand` | `#2e90fa` (brand-500) | `#53b1fd` (brand-400) | Focused inputs, selected states |
| `tw:border-brand` | `--color-border-brand` | `#2e90fa` (brand-500) | `#1570ef` (brand-600) | Focused inputs, selected states |
| `tw:border-brand_alt` | `--color-border-brand_alt` | `#1570ef` (brand-600) | `#2e2e2e` | Alternate brand border |
| `tw:border-brand-subtle` | `--color-border-brand-subtle` | `#84caff` | Brand 400 at 35% | Selected-state and info borders |
| `tw:border-error` | `--color-border-error` | `#f04438` (error-500) | `#f97066` (error-400) | Error state input/container border |
Expand Down Expand Up @@ -429,14 +429,14 @@ Brand tokens are for CTAs, highlighted navigation, brand-accented UI, and sectio
|---|---|---|---|
| `tw:bg-brand-solid` | `#1570ef` | `#1570ef` | Primary action button fill |
| `tw:bg-brand-solid_hover` | `#175cd3` | `#2e90fa` | CTA button hover state |
| `tw:bg-brand-primary` | `#eff8ff` | `#2e90fa` | Brand-tinted content area |
| `tw:bg-brand-primary` | `#eff8ff` | Brand 600 at 20% | Brand-tinted content area |
| `tw:bg-brand-secondary` | `#d1e9ff` | `#1570ef` | Stronger brand-tinted area |
| `tw:bg-brand-section` | `#1849a9` | `#191919` | Full-width brand promo section |
| `tw:bg-brand-section_subtle` | `#175cd3` | `#141414` | Subtle brand promo section |
| `tw:text-brand-primary` | `#194185` | `#f0f0f0` | Strong brand text |
| `tw:text-brand-secondary` | `#175cd3` | `#bfbfbf` | Brand links |
| `tw:text-brand-tertiary` | `#1570ef` | `#a0a0a0` | Lighter brand text |
| `tw:border-brand` | `#2e90fa` | `#53b1fd` | Focused/selected border |
| `tw:border-brand` | `#2e90fa` | `#1570ef` | Focused/selected border |
| `tw:text-fg-brand-primary` | `#1570ef` | `#2e90fa` | Brand icons |

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,71 @@ test('documents every approved go-forward utility', () => {
assert.match(reference, new RegExp(`\\b${utility}\\b`));
}
});

// ADR:2026-10-13-om-tokens-have-one-source-file
const SRC_DIR = path.resolve(__dirname, '../../src');
// Layout height re-declared per shell so calc() re-resolves against that
// shell's navbar height; not a themed token.
const OM_DECLARATION_ALLOWLIST = new Set(['--om-page-height']);

function styleFiles(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
return entry.name === 'node_modules' ? [] : styleFiles(full);
}

return /\.(less|css)$/.test(entry.name) ? [full] : [];
});
}

test('declares --om-* tokens only in tokens.css', () => {
const offenders = styleFiles(SRC_DIR)
.filter((file) => file !== TOKENS_FILE)
.flatMap((file) =>
[...fs.readFileSync(file, 'utf8').matchAll(/(--om-[\w-]+)\s*:/g)]
.map((match) => match[1])
.filter((token) => !OM_DECLARATION_ALLOWLIST.has(token))
.map((token) => `${path.relative(SRC_DIR, file)}: ${token}`)
);

// A second file declaring --om-* splits light and dark across stylesheets,
// so the winner depends on import order (light won and dark mode broke).
assert.deepEqual(offenders, []);
});

test('gives every dark --om-* override a light value in tokens.css', () => {
const css = fs.readFileSync(TOKENS_FILE, 'utf8');
const dark = declarations(extractBlock(css, ':root.dark-mode'));
const light = declarations(extractBlock(css, ':root {'));

assert.ok(dark.size > 0);
for (const token of dark.keys()) {
assert.ok(light.has(token), `${token} has a dark value but no light one`);
}
});
Comment on lines +378 to +387

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Dark-override guard parses the light :root block, so it always passes

extractBlock(css, ':root.dark-mode') uses indexOf. The first match is the header comment this PR adds in tokens.css at line 32 (:root.dark-mode — both here…), not the rule at line 386. The next { after that comment opens the light :root { block at line 45, so dark and light end up as the same map. The test 'gives every dark --om-* override a light value' checks the light block against itself and can never fail. A dark-only token added at line 386 would get through, even though the PR says it tested exactly that case. Fix: anchor the marker to a real rule, e.g. ' :root.dark-mode {' or a regex like /^:root\.dark-mode\s*\{/m. Also assert that the dark map contains a known dark token such as --om-grey-15 with value var(--color-border-subtle).

Anchor the marker to the rule, not the comment, and check that the right block was parsed:

  const dark = declarations(extractBlock(css, '
:root.dark-mode {'));
  const light = declarations(extractBlock(css, '
:root {'));

  assert.equal(dark.get('--om-grey-15'), 'var(--color-border-subtle)');
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎


test('keeps variables.less fallbacks equal to the tokens.css light values', () => {
const css = fs.readFileSync(TOKENS_FILE, 'utf8');
const light = declarations(extractBlock(css, ':root {'));
const less = fs.readFileSync(
path.resolve(SRC_DIR, 'styles/variables.less'),
'utf8'
);
const bridged = [
...less.matchAll(/^(@[\w-]+):\s*var\((--om-[\w-]+),\s*(.+)\);$/gm),
];
const names = bridged.map((match) => match[1]);

// LESS keeps the last of two definitions, so an earlier one is dead code
// whose fallback silently disagrees with tokens.css.
assert.deepEqual(
names.filter((name, index) => names.indexOf(name) !== index),
[]
);
for (const [, , token, fallback] of bridged) {
if (light.has(token)) {
assert.equal(fallback, light.get(token), `${token} fallback drifted`);
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| **Name** | Card |
| **Category** | Base / container |
| **Status** | Stable |
| **Styles** | [`src/styles/components/card.less`](../../src/styles/components/card.less) |
| **Styles** | [`core card.tsx`](../../../../../../../openmetadata-ui-core-components/src/main/resources/ui/src/components/base/card/card.tsx) |
| **Component** | `@openmetadata/ui-core-components` → `Card` (`Card.Header` / `Card.Content` / `Card.Footer`) for new work; Ant Design `Card` (legacy) |

## Overview
Expand Down Expand Up @@ -35,8 +35,8 @@ Parts: **root** (surface, border, radius, optional shadow), **header**

## Tokens used

Real `--om-*` tokens in `card.less` are the medium weight and the expand
transition; surface/border/radius map through the modern core-components Card.
Surface, border and radius come from the core-components Card; the medium
weight and expand transition below are the matching `--om-*` tokens.

| Part | Token |
| --- | --- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

Master map of every **project (`--om-*`) token** — the tokens components reference. Each references the matching upstream `globals.css` token (or holds a raw value) and resolves to the value shown. Full layering: [../README.md](../README.md).

Total project tokens: **832**.
Total project tokens: **816**.

## Spacing (57)
## Spacing (53)

padding / margin / gap. See foundations/spacing.md.

Expand All @@ -31,12 +31,10 @@ padding / margin / gap. See foundations/spacing.md.
| `--om-space-14` | `14px` |
| `--om-space-15` | `15px` |
| `--om-space-16` | `16px` |
| `--om-space-17` | `17px` |
| `--om-space-18` | `18px` |
| `--om-space-19` | `19px` |
| `--om-space-20` | `20px` |
| `--om-space-22` | `22px` |
| `--om-space-23` | `23px` |
| `--om-space-24` | `24px` |
| `--om-space-26` | `26px` |
| `--om-space-28` | `28px` |
Expand All @@ -55,7 +53,6 @@ padding / margin / gap. See foundations/spacing.md.
| `--om-space-56` | `56px` |
| `--om-space-60` | `60px` |
| `--om-space-64` | `64px` |
| `--om-space-70` | `70px` |
| `--om-space-75` | `75px` |
| `--om-space-80` | `80px` |
| `--om-space-84` | `84px` |
Expand All @@ -67,7 +64,6 @@ padding / margin / gap. See foundations/spacing.md.
| `--om-space-176` | `176px` |
| `--om-space-192` | `192px` |
| `--om-space-208` | `208px` |
| `--om-space-220` | `220px` |
| `--om-space-290` | `290px` |

## Layout (1)
Expand Down Expand Up @@ -704,11 +700,11 @@ Fixed swatches; do NOT adapt to dark mode. Prefer semantic tokens.

</details>

## Legacy colors (191)
## Legacy colors (179)

Exact migrated one-offs (migration debt). Do not use in new code; re-express with a semantic token.

<details><summary>Show all 191</summary>
<details><summary>Show all 179</summary>

| Token | Value |
| --- | --- |
Expand All @@ -723,7 +719,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-0-0-0-0-8` | `rgba(0, 0, 0, 0.8)` |
| `--om-legacy-color-0-0-0-0-9` | `rgba(0, 0, 0, 0.9)` |
| `--om-legacy-color-0-0-0-0-12` | `rgba(0, 0, 0, 0.12)` |
| `--om-legacy-color-0-0-0-0-13` | `rgba(0, 0, 0, 0.13)` |
| `--om-legacy-color-0-0-0-0-15` | `rgba(0, 0, 0, 0.15)` |
| `--om-legacy-color-0-0-0-0-25` | `rgba(0, 0, 0, 0.25)` |
| `--om-legacy-color-0-0-0-0-102` | `#0000001a` |
Expand All @@ -746,7 +741,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-10-13-18-0-04` | `rgba(10, 13, 18, 0.04)` |
| `--om-legacy-color-10-13-18-0-05` | `rgba(10, 13, 18, 0.05)` |
| `--om-legacy-color-10-13-18-0-08` | `rgba(10, 13, 18, 0.08)` |
| `--om-legacy-color-10-13-18-0-18` | `rgba(10, 13, 18, 0.18)` |
| `--om-legacy-color-10-13-18-0-051` | `#0a0d120d` |
| `--om-legacy-color-10-13-18-0-059` | `#0a0d120f` |
| `--om-legacy-color-10-13-18-0-102` | `#0a0d121a` |
Expand All @@ -770,15 +764,13 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-52c41a` | `#52c41a` |
| `--om-legacy-color-0056b3` | `#0056b3` |
| `--om-legacy-color-0066cc` | `#06c` |
| `--om-legacy-color-82-196-26-0-05` | `rgba(82, 196, 26, 0.05)` |
| `--om-legacy-color-85-155-238-0-2` | `rgba(85, 155, 238, 0.2)` |
| `--om-legacy-color-96-167-251-0-2` | `rgba(96, 167, 251, 0.2)` |
| `--om-legacy-color-102a72` | `#102a72` |
| `--om-legacy-color-107-114-128-0-15` | `rgba(107, 114, 128, 0.15)` |
| `--om-legacy-color-113-71-232-0-6` | `#7147e899` |
| `--om-legacy-color-113-71-232-0-35` | `rgba(113, 71, 232, 0.35)` |
| `--om-legacy-color-113-71-232-0-102` | `#7147e81a` |
| `--om-legacy-color-117-117-117-0-3` | `rgba(117, 117, 117, 0.3)` |
| `--om-legacy-color-203-36-49-0-059` | `#cb24310f` |
| `--om-legacy-color-209-236-241-0-867` | `#d1ecf1dd` |
| `--om-legacy-color-212-237-218-0-867` | `#d4eddadd` |
Expand All @@ -802,7 +794,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-356fe7` | `rgb(53, 111, 231)` |
| `--om-legacy-color-373e44` | `#373e44` |
| `--om-legacy-color-721c24` | `#721c24` |
| `--om-legacy-color-0950c5` | `#0950c5` |
| `--om-legacy-color-1677ff` | `#1677ff` |
| `--om-legacy-color-1890ff` | `#1890ff` |
| `--om-legacy-color-2563eb` | `#2563eb` |
Expand All @@ -813,7 +804,6 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-52525b` | `#52525b` |
| `--om-legacy-color-52607a` | `#52607a` |
| `--om-legacy-color-76746f` | `#76746f` |
| `--om-legacy-color-101828` | `#101828` |
| `--om-legacy-color-111827` | `#111827` |
| `--om-legacy-color-155724` | `#155724` |
| `--om-legacy-color-344054` | `#344054` |
Expand All @@ -833,9 +823,7 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-b93815` | `#b93815` |
| `--om-legacy-color-bee5eb` | `#bee5eb` |
| `--om-legacy-color-c3e6cb` | `#c3e6cb` |
| `--om-legacy-color-c6b5f6` | `#c6b5f6` |
| `--om-legacy-color-c6c6c6` | `#c6c6c6` |
| `--om-legacy-color-c9d7ff` | `#c9d7ff` |
| `--om-legacy-color-c45296` | `#c45296` |
| `--om-legacy-color-cb2431` | `#cb2431` |
| `--om-legacy-color-cccccc` | `#cccccc` |
Expand All @@ -861,15 +849,13 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-e8e8e9` | `#e8e8e9` |
| `--om-legacy-color-e8ecf5` | `#e8ecf5` |
| `--om-legacy-color-e60000` | `#e60000` |
| `--om-legacy-color-eaecf0` | `#eaecf0` |
| `--om-legacy-color-ebeef1` | `#ebeef1` |
| `--om-legacy-color-ecedee` | `#ecedee` |
| `--om-legacy-color-eeeeee` | `#eeeeee` |
| `--om-legacy-color-ef6820` | `#ef6820` |
| `--om-legacy-color-efbdc1` | `#efbdc1` |
| `--om-legacy-color-f0f0f0` | `#f0f0f0` |
| `--om-legacy-color-f0f5ff` | `#f0f5ff` |
| `--om-legacy-color-f1edfd` | `#f1edfd` |
| `--om-legacy-color-f1f1f1` | `#f1f1f1` |
| `--om-legacy-color-f1f5ff` | `#f1f5ff` |
| `--om-legacy-color-f3f7fd` | `#f3f7fd` |
Expand All @@ -885,10 +871,8 @@ Exact migrated one-offs (migration debt). Do not use in new code; re-express wit
| `--om-legacy-color-f7f9fc` | `#f7f9fc` |
| `--om-legacy-color-f7fbf9` | `#f7fbf9` |
| `--om-legacy-color-f8f8f8` | `#f8f8f8` |
| `--om-legacy-color-f8f9fa` | `#f8f9fa` |
| `--om-legacy-color-f8faff` | `#f8faff` |
| `--om-legacy-color-f9dbaf` | `#f9dbaf` |
| `--om-legacy-color-f9f5ff` | `#f9f5ff` |
| `--om-legacy-color-f9816c` | `#f9816c` |
| `--om-legacy-color-f44336` | `#f44336` |
| `--om-legacy-color-fbf8f8` | `#fbf8f8` |
Expand Down
Loading
Loading