diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.test.tsx
new file mode 100644
index 000000000000..b1ecdc9b0980
--- /dev/null
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.test.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { Alert, AlertVariant } from './alert';
+
+describe('Alert theme semantics', () => {
+ it.each<[AlertVariant, string, string]>([
+ ['success', 'tw:bg-success-primary', 'tw:border-success-subtle'],
+ ['warning', 'tw:bg-warning-primary', 'tw:border-warning-subtle'],
+ ['error', 'tw:bg-error-primary', 'tw:border-error-subtle'],
+ ])(
+ 'uses semantic surface and border roles for the %s variant',
+ (variant, backgroundClass, borderClass) => {
+ render();
+
+ expect(screen.getByRole('alert')).toHaveClass(
+ backgroundClass,
+ borderClass
+ );
+ }
+ );
+});
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.tsx
index f6f5bdf13d71..87c15af2bc18 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/alert/alert.tsx
@@ -34,18 +34,20 @@ const variantStyles: Record<
defaultIcon: FC<{ className?: string }>;
}
> = {
+ // Severity roles preserve the intended contrast on each theme surface;
+ // palette steps invert independently and are reserved for data-bound color.
success: {
- root: 'tw:border-utility-success-300 tw:bg-success-primary',
+ root: 'tw:border-success-subtle tw:bg-success-primary',
iconColor: 'success',
defaultIcon: CheckCircle,
},
warning: {
- root: 'tw:border-utility-warning-300 tw:bg-warning-primary',
+ root: 'tw:border-warning-subtle tw:bg-warning-primary',
iconColor: 'warning',
defaultIcon: AlertTriangle,
},
error: {
- root: 'tw:border-utility-error-300 tw:bg-error-primary',
+ root: 'tw:border-error-subtle tw:bg-error-primary',
iconColor: 'error',
defaultIcon: AlertCircle,
},
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.test.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.test.tsx
new file mode 100644
index 000000000000..4a62bf319e44
--- /dev/null
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { TextAreaBase } from './textarea';
+
+describe('TextAreaBase theme semantics', () => {
+ it('colors the resize handle with the active semantic border token', () => {
+ render();
+
+ const textArea = screen.getByRole('textbox', { name: 'Description' });
+
+ expect(textArea).toHaveClass(
+ 'tw:[&::-webkit-resizer]:bg-border-primary',
+ 'tw:[&::-webkit-resizer]:mask-(image:--resize-handle-mask)'
+ );
+ expect(textArea.style.getPropertyValue('--resize-handle-mask')).toContain(
+ 'data:image/svg+xml;base64,'
+ );
+ expect(textArea.className).not.toContain('tw:dark:');
+ });
+});
diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.tsx
index bca3704fb8ae..98a2268253a0 100644
--- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.tsx
+++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/base/textarea/textarea.tsx
@@ -13,10 +13,10 @@ import { Label } from '@/components/base/input/label';
import { cx } from '@/utils/cx';
import { fontSizeClass } from '@/utils';
-// Creates a data URL for an SVG resize handle with a given color.
-const getResizeHandleBg = (color: string) => {
+// Masking keeps the resize glyph's shape fixed while its semantic color follows the active theme.
+const getResizeHandleMask = () => {
return `url(data:image/svg+xml;base64,${btoa(
- ``
+ ''
)})`;
};
@@ -40,8 +40,7 @@ export const TextAreaBase = ({
// gone — the outline IS the focus indicator here, as in input.tsx.
'tw:w-full tw:scroll-py-3 tw:rounded-lg tw:bg-primary tw:px-3.5 tw:py-3 tw:text-primary tw:shadow-xs tw:outline-1 tw:-outline-offset-1 tw:outline-primary tw:transition tw:duration-100 tw:ease-linear tw:placeholder:text-placeholder tw:autofill:rounded-lg tw:autofill:text-primary',
- // Resize handle
- 'tw:[&::-webkit-resizer]:bg-(image:--resize-handle-bg) tw:[&::-webkit-resizer]:bg-contain tw:dark:[&::-webkit-resizer]:bg-(image:--resize-handle-bg-dark)',
+ 'tw:[&::-webkit-resizer]:bg-border-primary tw:[&::-webkit-resizer]:mask-(image:--resize-handle-mask) tw:[&::-webkit-resizer]:mask-contain tw:[&::-webkit-resizer]:mask-no-repeat',
state.isFocused &&
!state.isDisabled &&
@@ -60,8 +59,7 @@ export const TextAreaBase = ({
}
style={
{
- '--resize-handle-bg': getResizeHandleBg('#D5D7DA'),
- '--resize-handle-bg-dark': getResizeHandleBg('#373A41'),
+ '--resize-handle-mask': getResizeHandleMask(),
} as React.CSSProperties
}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
index 0fcb464d752a..3f47befca49d 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-token-reference.js
@@ -67,19 +67,67 @@ function resolve(name, defs, seen = new Set()) {
}
const GROUPS = [
- { key: /^--om-space-/, title: 'Spacing', note: 'padding / margin / gap. See foundations/spacing.md.' },
- { key: /^--om-page-/, title: 'Layout', note: 'Shared viewport and shell-derived dimensions.' },
- { key: /^--om-radius-/, title: 'Radius', note: 'border-radius. See foundations/radius.md.' },
- { key: /^--om-font-size-/, title: 'Font size', note: 'font-size. See foundations/typography.md.' },
+ {
+ key: /^--om-space-/,
+ title: 'Spacing',
+ note: 'padding / margin / gap. See foundations/spacing.md.',
+ },
+ {
+ key: /^--om-page-/,
+ title: 'Layout',
+ note: 'Shared viewport and shell-derived dimensions.',
+ },
+ {
+ key: /^--om-radius-/,
+ title: 'Radius',
+ note: 'border-radius. See foundations/radius.md.',
+ },
+ {
+ key: /^--om-font-size-/,
+ title: 'Font size',
+ note: 'font-size. See foundations/typography.md.',
+ },
{ key: /^--om-font-weight-/, title: 'Font weight', note: 'font-weight.' },
- { key: /^--om-font-|^--om-line-height-/, title: 'Font family & line height', note: '' },
- { key: /^--om-shadow-/, title: 'Elevation', note: 'box-shadow. See foundations/elevation.md.' },
- { key: /^--om-z-/, title: 'z-index', note: 'stacking. Prefer the semantic ladder for new work.' },
- { key: /^--om-duration-|^--om-ease-/, title: 'Motion', note: 'transition / animation. See foundations/motion.md.' },
- { key: /^--om-color-(text|bg|border|fg|link|interactive|focus)/, title: 'Semantic colors', note: 'Prefer these — they adapt to dark mode.' },
- { key: /^--om-color-(white|black|transparent)/, title: 'Absolute colors', note: '' },
- { key: /^--om-color-/, title: 'Palette colors', note: 'Fixed swatches; do NOT adapt to dark mode. Prefer semantic tokens.' },
- { key: /^--om-legacy-color-/, title: 'Legacy colors', note: 'Exact migrated one-offs (migration debt). Do not use in new code; re-express with a semantic token.' },
+ {
+ key: /^--om-font-|^--om-line-height-|^--om-letter-spacing-/,
+ title: 'Font family, line height & letter spacing',
+ note: '',
+ },
+ {
+ key: /^--om-shadow-/,
+ title: 'Elevation',
+ note: 'box-shadow. See foundations/elevation.md.',
+ },
+ {
+ key: /^--om-z-/,
+ title: 'z-index',
+ note: 'stacking. Prefer the semantic ladder for new work.',
+ },
+ {
+ key: /^--om-duration-|^--om-ease-/,
+ title: 'Motion',
+ note: 'transition / animation. See foundations/motion.md.',
+ },
+ {
+ key: /^--om-color-(text|bg|border|fg|link|interactive|focus)/,
+ title: 'Semantic colors',
+ note: 'Prefer these — they adapt to dark mode.',
+ },
+ {
+ key: /^--om-color-(white|black|transparent)/,
+ title: 'Absolute colors',
+ note: '',
+ },
+ {
+ key: /^--om-color-/,
+ title: 'Palette colors',
+ note: 'Fixed swatches; do NOT adapt to dark mode. Prefer semantic tokens.',
+ },
+ {
+ key: /^--om-legacy-color-/,
+ title: 'Legacy colors',
+ note: 'Exact migrated one-offs (migration debt). Do not use in new code; re-express with a semantic token.',
+ },
];
function classify(name) {
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
index 70ef49e25f01..21c58cf6d3b5 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/gen-tokens.js
@@ -30,7 +30,13 @@ const map = require('./token-map');
const STYLES_ROOT = path.resolve(__dirname, '../../src');
const TOKENS_CSS = path.resolve(STYLES_ROOT, 'styles/tokens.css');
const SIDECAR = path.resolve(__dirname, 'legacy-colors.json');
-const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'target', 'output']);
+const SKIP_DIRS = new Set([
+ 'node_modules',
+ 'dist',
+ 'build',
+ 'target',
+ 'output',
+]);
const BEGIN = '/* @tokens:generated-begin';
const END = '/* @tokens:generated-end */';
const OM_USAGE_RE = /var\(\s*(--om-[\w-]+)/g;
@@ -147,17 +153,21 @@ function main() {
const r = map.registry;
process.stdout.write(
`tokens.css regenerated:\n` +
- ` palette (full upstream): ${Object.keys(map.constants.UPSTREAM).length}\n` +
+ ` palette passthrough: ${
+ Object.keys(map.constants.UPSTREAM).length -
+ map.constants.MANUAL_ABSOLUTE_COLOR_TOKENS.size
+ }\n` +
` legacy colors: ${r.legacyColors.size}\n` +
` extended spacing: ${
- [...r.spacing.keys()].filter((px) => !map.constants.CORE_SPACING.has(px))
- .length
+ [...r.spacing.keys()].filter(
+ (px) => !map.constants.CORE_SPACING.has(px)
+ ).length
}\n` +
` extended radius: ${
- [...r.radius.keys()].filter((k) => /^\d+$/.test(k)).length
+ [...r.radius.keys()].filter(map.isNumericSlug).length
}\n` +
` extended font-size: ${
- [...r.fontSize.keys()].filter((k) => /^\d+$/.test(k)).length
+ [...r.fontSize.keys()].filter(map.isNumericSlug).length
}\n` +
` z-index tokens: ${r.zIndex.size}\n` +
` duration tokens: ${r.duration.size}\n`
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/scanner.test.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/scanner.test.js
index 142a8dfe9511..e59b88fac74f 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/scanner.test.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/scanner.test.js
@@ -14,6 +14,7 @@
/* Focused round-trip checks for the token scanner. Run: node scanner.test.js */
const assert = require('assert');
const { processText } = require('./scanner');
+const tokenMap = require('./token-map');
let pass = 0;
function check(name, input, expected) {
@@ -22,7 +23,13 @@ function check(name, input, expected) {
assert.strictEqual(newText, expected);
pass++;
} catch (e) {
- process.stderr.write(`\nFAIL: ${name}\n in: ${JSON.stringify(input)}\n got: ${JSON.stringify(newText)}\n exp: ${JSON.stringify(expected)}\n`);
+ process.stderr.write(
+ `\nFAIL: ${name}\n in: ${JSON.stringify(
+ input
+ )}\n got: ${JSON.stringify(newText)}\n exp: ${JSON.stringify(
+ expected
+ )}\n`
+ );
process.exitCode = 1;
}
}
@@ -34,7 +41,11 @@ check(
'.a { color: var(--om-color-brand-500); }'
);
// color: 3-digit expands and matches white
-check('short hex white', '.a { color: #FFF; }', '.a { color: var(--om-color-white); }');
+check(
+ 'short hex white',
+ '.a { color: #FFF; }',
+ '.a { color: var(--om-color-white); }'
+);
// color: no upstream home -> legacy token, exact value preserved
check(
'legacy color',
@@ -80,9 +91,17 @@ check(
'.a { width: 100px; /* padding: 8px */ color: var(--om-color-white); }'
);
// width/top are out of scope -> untouched
-check('width out of scope', '.a { width: 20px; top: 8px; }', '.a { width: 20px; top: 8px; }');
+check(
+ 'width out of scope',
+ '.a { width: 20px; top: 8px; }',
+ '.a { width: 20px; top: 8px; }'
+);
// border-radius -> radius token
-check('radius', '.a { border-radius: 4px; }', '.a { border-radius: var(--om-radius-sm); }');
+check(
+ 'radius',
+ '.a { border-radius: 4px; }',
+ '.a { border-radius: var(--om-radius-sm); }'
+);
// font-size + font-weight
check(
'font-size and weight',
@@ -104,7 +123,11 @@ check(
'.a { box-shadow: 0px 2px 10px var(--om-legacy-color-0-0-0-0-12); }'
);
// LESS @variable usage untouched (not a literal)
-check('less var untouched', '.a { color: @grey-15; }', '.a { color: @grey-15; }');
+check(
+ 'less var untouched',
+ '.a { color: @grey-15; }',
+ '.a { color: @grey-15; }'
+);
// LESS color function: hex inside darken() must stay raw (LESS needs a literal)
check(
'less color fn untouched',
@@ -167,4 +190,27 @@ const once = processText(rich, 't').newText;
const twice = processText(once, 't').newText;
check('idempotent (all categories)', twice, once);
-process.stdout.write(`\n${pass} checks passed${process.exitCode ? ' (with failures above)' : ''}\n`);
+tokenMap.resetRegistry();
+for (const radius of [10, 16, 24, 1, 3.2]) {
+ tokenMap.resolveRadius(radius, 'px');
+}
+const generatedTokens = tokenMap.emitGeneratedBlocks();
+const generatedRadiusLines = generatedTokens
+ .split('\n')
+ .filter((line) => line.includes('--om-radius-'));
+assert.deepStrictEqual(generatedRadiusLines, [
+ ' --om-radius-1: 1px;',
+ ' --om-radius-3_2: 3.2px;',
+ ' --om-radius-10: 10px;',
+]);
+assert.deepStrictEqual(
+ generatedTokens
+ .split('\n')
+ .filter((line) => /--om-color-(?:black|white):/.test(line)),
+ []
+);
+pass++;
+
+process.stdout.write(
+ `\n${pass} checks passed${process.exitCode ? ' (with failures above)' : ''}\n`
+);
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
index ffc9c9cb6d1c..c2ed5619ba9c 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/theme-contract.test.js
@@ -258,6 +258,7 @@ test('exposes the approved roles through the legacy token bridge', () => {
'--om-color-bg-surface': 'var(--color-bg-surface, #ffffff)',
'--om-color-bg-raised': 'var(--color-bg-raised, #ffffff)',
'--om-color-bg-overlay-surface': 'var(--color-bg-overlay-surface, #ffffff)',
+ '--om-color-bg-secondary-hover': 'var(--color-bg-secondary_hover, #f5f5f5)',
'--om-color-border-subtle': 'var(--color-border-subtle, rgb(0 0 0 / 0.08))',
'--om-color-border-hover': 'var(--color-border-hover, #a4a7ae)',
'--om-color-border-brand-subtle':
diff --git a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/token-map.js b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/token-map.js
index 2a2600da9646..06d3e6554592 100644
--- a/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/token-map.js
+++ b/openmetadata-ui/src/main/resources/ui/scripts/design-tokens/token-map.js
@@ -31,6 +31,10 @@ const path = require('path');
const { canonicalizeColor, colorSlug } = require('./color-utils');
const UPSTREAM = require('./upstream-palette.json').palette;
+const MANUAL_ABSOLUTE_COLOR_TOKENS = new Set([
+ '--om-color-black',
+ '--om-color-white',
+]);
// ---------------------------------------------------------------------------
// Scales (Layer 1 primitive values)
@@ -373,6 +377,8 @@ function emitPaletteBlock() {
upstreamVar,
omName: omPaletteName(upstreamVar),
}))
+ // Absolute colors are declared in the manual token layer and must not be overridden later.
+ .filter(({ omName }) => !MANUAL_ABSOLUTE_COLOR_TOKENS.has(omName))
.sort((a, b) => a.omName.localeCompare(b.omName));
const lines = entries
.map((e) => ` ${e.omName}: var(${e.upstreamVar}, ${e.canon});`)
@@ -404,6 +410,11 @@ function slugToNum(slug) {
return Number(slug.replace('_', '.'));
}
+// A prefix check misclassifies semantic scale names such as `2xl` as off-scale values.
+function isNumericSlug(slug) {
+ return /^\d+(?:_\d+)?$/.test(slug);
+}
+
function emitSpacingExtBlock() {
const pxs = [...registry.spacing.keys()]
.filter((px) => !CORE_SPACING.has(px))
@@ -422,7 +433,7 @@ function emitSpacingExtBlock() {
}
function emitRadiusExtBlock() {
- const numeric = [...registry.radius.keys()].filter((k) => /^\d/.test(k));
+ const numeric = [...registry.radius.keys()].filter(isNumericSlug);
let block = null;
if (numeric.length) {
const lines = numeric
@@ -435,12 +446,14 @@ function emitRadiusExtBlock() {
}
function emitFontSizeExtBlock() {
- const numeric = [...registry.fontSize.keys()].filter((k) => /^\d/.test(k));
+ const numeric = [...registry.fontSize.keys()].filter(isNumericSlug);
let block = null;
if (numeric.length) {
const lines = numeric
.sort((a, b) => slugToNum(a) - slugToNum(b))
- .map((slug) => ` --om-font-size-${slug}: ${registry.fontSize.get(slug)};`)
+ .map(
+ (slug) => ` --om-font-size-${slug}: ${registry.fontSize.get(slug)};`
+ )
.join('\n');
block = ` /* Extended font sizes — off-scale values in use. */\n` + lines;
}
@@ -469,9 +482,7 @@ function emitDurationBlock() {
const ms = sortByNum(registry.duration);
let block = null;
if (ms.length) {
- const lines = ms
- .map((n) => ` --om-duration-${n}: ${n}ms;`)
- .join('\n');
+ const lines = ms.map((n) => ` --om-duration-${n}: ${n}ms;`).join('\n');
block = ` /* Motion durations in use. */\n` + lines;
}
return block;
@@ -515,8 +526,10 @@ module.exports = {
registry,
resetRegistry,
emitGeneratedBlocks,
+ isNumericSlug,
constants: {
CORE_SPACING,
+ MANUAL_ABSOLUTE_COLOR_TOKENS,
RADIUS_VALUE,
FONT_SIZE_VALUE,
FONT_WEIGHT_VALUE,
diff --git a/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md b/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
index fde2c468973f..d1435ef5278a 100644
--- a/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
+++ b/openmetadata-ui/src/main/resources/ui/specs/tokens/token-reference.md
@@ -4,7 +4,7 @@
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: **804**.
+Total project tokens: **807**.
## Spacing (62)
@@ -162,7 +162,7 @@ font-weight.
| `--om-font-weight-semibold` | `600` |
| `--om-font-weight-thin` | `100` |
-## Font family & line height (7)
+## Font family, line height & letter spacing (8)
| Token | Value |
| --- | --- |
@@ -170,6 +170,7 @@ font-weight.
Consolas, 'Liberation Mono', 'Courier New', monospace` |
| `--om-font-sans` | `'Inter', 'Poppins', -apple-system, 'Segoe UI', Roboto, Arial,
sans-serif` |
+| `--om-letter-spacing-wide` | `0.08em` |
| `--om-line-height-none` | `1` |
| `--om-line-height-normal` | `1.5` |
| `--om-line-height-relaxed` | `1.625` |
@@ -197,7 +198,7 @@ box-shadow. See foundations/elevation.md.
0px 8px 8px -4px rgba(10, 13, 18, 0.03)` |
| `--om-shadow-xs` | `0px 1px 2px rgba(10, 13, 18, 0.05)` |
-## z-index (31)
+## z-index (32)
stacking. Prefer the semantic ladder for new work.
@@ -229,6 +230,7 @@ stacking. Prefer the semantic ladder for new work.
| `--om-z-dropdown` | `1000` |
| `--om-z-max` | `9999` |
| `--om-z-modal` | `1500` |
+| `--om-z-n1` | `-1` |
| `--om-z-overlay` | `1050` |
| `--om-z-popover` | `2000` |
| `--om-z-raised` | `1` |
@@ -268,7 +270,7 @@ transition / animation. See foundations/motion.md.
| `--om-ease-out` | `cubic-bezier(0, 0, 0.2, 1)` |
| `--om-ease-standard` | `cubic-bezier(0.4, 0, 0.2, 1)` |
-## Semantic colors (52)
+## Semantic colors (53)
Prefer these — they adapt to dark mode.
@@ -287,6 +289,7 @@ Prefer these — they adapt to dark mode.
| `--om-color-bg-quaternary` | `#e9eaeb` |
| `--om-color-bg-raised` | `#ffffff` |
| `--om-color-bg-secondary` | `#fafafa` |
+| `--om-color-bg-secondary-hover` | `#f5f5f5` |
| `--om-color-bg-success` | `#ecfdf3` |
| `--om-color-bg-surface` | `#ffffff` |
| `--om-color-bg-tertiary` | `#f5f5f5` |
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.test.tsx
new file mode 100644
index 000000000000..526304b2cbda
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2026 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import ContextCenterSubNavSections from './ContextCenterSubNavSections';
+
+jest.mock('../../../../hooks/currentUserStore/useCurrentUserStore', () => ({
+ useCurrentUserPreferences: () => ({
+ preferences: { recentlyViewedQuickLinks: [] },
+ }),
+}));
+
+jest.mock('../../../../hooks/useApplicationStore', () => ({
+ useApplicationStore: () => ({ currentUser: undefined }),
+}));
+
+describe('ContextCenterSubNavSections', () => {
+ it('uses the semantic muted-text role for section headings', () => {
+ const { container } = render(
+
+
+
+ );
+ const heading = container.querySelector(
+ '.ask-sub-panel__section > .prose > span'
+ );
+
+ expect(heading).toHaveClass('tw:text-quaternary');
+ expect(heading).not.toHaveClass('tw:text-gray-500');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.tsx b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.tsx
index 6104e2e37390..3f873d7e030b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/ContextCenterSubNavSections.tsx
@@ -223,7 +223,10 @@ const ContextCenterSubNavSections: FC = ({
return (
<>
-
+
{t('label.quick-action-plural')}
@@ -275,7 +278,7 @@ const ContextCenterSubNavSections: FC = ({
{!isEmpty(recentlyViewed) && (
{t('label.recently-viewed')}
@@ -289,7 +292,7 @@ const ContextCenterSubNavSections: FC = ({
{!isEmpty(bookmarks) && (
{t('label.bookmark-plural')}
@@ -305,7 +308,7 @@ const ContextCenterSubNavSections: FC = ({
className="ask-sub-panel__section ask-sub-panel__section--with-header"
key={tagFqn}>
{startCase(tagFqn.split(FQN_SEPARATOR_CHAR)[1])}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/sidebar.less b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/sidebar.less
index 2ace10bf35d7..cec230f8f31c 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/sidebar.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/platform/ai-shell/Sidebar/sidebar.less
@@ -67,27 +67,24 @@
@ask-recent-chats-card-radius: 10px;
-// Colors reference Tailwind v4 theme tokens emitted at `:root` by
-// `@openmetadata/ui-core-components/globals.css`. The `--tw-` prefix
-// comes from this app's `tailwind.css` (`prefix(tw)`).
-// `@ask-bg-hover` stays literal — overlay convention, no semantic token.
+// Use shared roles already defined by Gate 0. The palette-backed page,
+// selected, brand, and badge roles remain pending #6564.
@ask-bg-page: var(--tw-color-utility-gray-blue-50);
-@ask-bg-card: var(--tw-color-bg-primary);
+@ask-bg-card: var(--om-color-bg-surface);
@ask-bg-active: var(--tw-color-gray-blue-100);
-@ask-bg-hover: rgba(0, 0, 0, 0.04);
-@ask-text-primary: var(--tw-color-text-primary);
-@ask-text-secondary: var(--tw-color-text-secondary);
-@ask-text-tertiary: var(--tw-color-text-tertiary);
-@ask-text-quaternary: var(--tw-color-text-quaternary);
+@ask-bg-hover: var(--om-color-bg-secondary-hover);
+@ask-text-primary: var(--om-color-text-primary);
+@ask-text-secondary: var(--om-color-text-secondary);
+@ask-text-tertiary: var(--om-color-text-tertiary);
+@ask-text-quaternary: var(--om-color-text-quaternary);
@ask-brand: var(--tw-color-brand-600);
@ask-brand-active: var(--tw-color-brand-800);
@ask-brand-50: var(--tw-color-brand-50);
-@ask-border-secondary: var(--tw-color-border-secondary);
+@ask-border-secondary: var(--om-color-border-secondary);
// Utility palette — used by the count badges on nav items.
@ask-util-gray-50: var(--tw-color-gray-50);
@ask-util-gray-200: var(--tw-color-gray-200);
-@ask-util-gray-600: var(--tw-color-gray-600);
@ask-util-gray-700: var(--tw-color-gray-700);
@ask-transition-duration: 200ms;
@@ -927,14 +924,14 @@
&:not(&--active):not(&--interactive-icon):not(&--recent-chats):hover {
background: @ask-bg-hover;
- color: @ask-util-gray-600;
+ color: @ask-text-tertiary;
svg {
width: 22px;
height: 22px;
* {
- stroke: @ask-util-gray-600;
+ stroke: @ask-text-tertiary;
}
}
}
@@ -1292,14 +1289,14 @@
&:not(&--active):hover {
background: @ask-bg-hover;
- color: @ask-util-gray-600;
+ color: @ask-text-tertiary;
svg {
width: 22px;
height: 22px;
* {
- stroke: @ask-util-gray-600;
+ stroke: @ask-text-tertiary;
}
}
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/tokens.css b/openmetadata-ui/src/main/resources/ui/src/styles/tokens.css
index 62b2fbd70068..63cab6ffac48 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/tokens.css
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/tokens.css
@@ -179,6 +179,7 @@
--om-color-bg: var(--color-bg-primary, #ffffff);
--om-color-bg-primary: var(--color-bg-primary, #ffffff);
--om-color-bg-secondary: var(--color-bg-secondary, #fafafa);
+ --om-color-bg-secondary-hover: var(--color-bg-secondary_hover, #f5f5f5);
--om-color-bg-tertiary: var(--color-bg-tertiary, #f5f5f5);
--om-color-bg-quaternary: var(--color-bg-quaternary, #e9eaeb);
--om-color-bg-disabled: var(--color-bg-disabled, #f5f5f5);
@@ -234,7 +235,6 @@
:root {
/* Palette passthrough — --om-* aliases reference the upstream palette in
globals.css with a raw fallback. Components reference these. */
- --om-color-black: var(--color-black, #000000);
--om-color-blue-dark-100: var(--color-blue-dark-100, #d1e0ff);
--om-color-blue-dark-200: var(--color-blue-dark-200, #b2ccff);
--om-color-blue-dark-25: var(--color-blue-dark-25, #f5f8ff);
@@ -547,7 +547,6 @@
--om-color-warning-800: var(--color-warning-800, #93370d);
--om-color-warning-900: var(--color-warning-900, #7a2e0e);
--om-color-warning-950: var(--color-warning-950, #4e1d09);
- --om-color-white: var(--color-white, #ffffff);
--om-color-yellow-100: var(--color-yellow-100, #fef7c3);
--om-color-yellow-200: var(--color-yellow-200, #feee95);
--om-color-yellow-25: var(--color-yellow-25, #fefdf0);
@@ -876,6 +875,7 @@
/* z-index — exact values preserved to keep stacking order identical.
For new work prefer the semantic ladder documented in specs. */
+ --om-z-n1: -1;
--om-z-0: 0;
--om-z-1: 1;
--om-z-2: 2;