diff --git a/packages/module/build/css/tokens-dark.scss b/packages/module/build/css/tokens-dark.scss
index cf687ff..8573a71 100644
--- a/packages/module/build/css/tokens-dark.scss
+++ b/packages/module/build/css/tokens-dark.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:18 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
@mixin pf-v6-tokens {
--pf-t--global--background--color--action--plain--default: rgba(0, 0, 0, 0.0000);
diff --git a/packages/module/build/css/tokens-default.scss b/packages/module/build/css/tokens-default.scss
index b99f457..226a765 100644
--- a/packages/module/build/css/tokens-default.scss
+++ b/packages/module/build/css/tokens-default.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:18 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
@mixin pf-v6-tokens {
--pf-t--global--background--color--500: rgba(21, 21, 21, 0.4000);
diff --git a/packages/module/build/css/tokens-felt-dark.scss b/packages/module/build/css/tokens-felt-dark.scss
index f4c7f1c..7dc8a8e 100644
--- a/packages/module/build/css/tokens-felt-dark.scss
+++ b/packages/module/build/css/tokens-felt-dark.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (14 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-felt-glass-dark.scss b/packages/module/build/css/tokens-felt-glass-dark.scss
index c3b65c2..75182fe 100644
--- a/packages/module/build/css/tokens-felt-glass-dark.scss
+++ b/packages/module/build/css/tokens-felt-glass-dark.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (33 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-felt-glass.scss b/packages/module/build/css/tokens-felt-glass.scss
index 1a79489..008d69d 100644
--- a/packages/module/build/css/tokens-felt-glass.scss
+++ b/packages/module/build/css/tokens-felt-glass.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (23 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-felt-highcontrast-dark.scss b/packages/module/build/css/tokens-felt-highcontrast-dark.scss
index 7c27aaa..01ba09b 100644
--- a/packages/module/build/css/tokens-felt-highcontrast-dark.scss
+++ b/packages/module/build/css/tokens-felt-highcontrast-dark.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (43 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-felt.scss b/packages/module/build/css/tokens-felt.scss
index 8ad5044..bf7d371 100644
--- a/packages/module/build/css/tokens-felt.scss
+++ b/packages/module/build/css/tokens-felt.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (8 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-glass-dark.scss b/packages/module/build/css/tokens-glass-dark.scss
index 26df460..89446c6 100644
--- a/packages/module/build/css/tokens-glass-dark.scss
+++ b/packages/module/build/css/tokens-glass-dark.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (19 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-glass.scss b/packages/module/build/css/tokens-glass.scss
index 0fe964f..585dd26 100644
--- a/packages/module/build/css/tokens-glass.scss
+++ b/packages/module/build/css/tokens-glass.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
// Only tokens that differ from base theme (15 tokens)
@mixin pf-v6-tokens {
diff --git a/packages/module/build/css/tokens-palette.scss b/packages/module/build/css/tokens-palette.scss
index 7d63a63..09ae0e6 100644
--- a/packages/module/build/css/tokens-palette.scss
+++ b/packages/module/build/css/tokens-palette.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:18 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
@mixin pf-v6-tokens {
--pf-t--color--black: #000000;
diff --git a/packages/module/build/css/tokens-redhat-highcontrast.scss b/packages/module/build/css/tokens-redhat-highcontrast.scss
index 7a38461..7268d98 100644
--- a/packages/module/build/css/tokens-redhat-highcontrast.scss
+++ b/packages/module/build/css/tokens-redhat-highcontrast.scss
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Thu, 30 Apr 2026 21:31:19 GMT
+// Generated on Mon, 04 May 2026 19:53:21 GMT
@mixin pf-v6-tokens {
--pf-t--global--background--color--500: rgba(21, 21, 21, 0.4000);
diff --git a/packages/module/patternfly-docs/content/tokensTable.css b/packages/module/patternfly-docs/content/tokensTable.css
index 255c142..a9f4f4a 100644
--- a/packages/module/patternfly-docs/content/tokensTable.css
+++ b/packages/module/patternfly-docs/content/tokensTable.css
@@ -1,3 +1,175 @@
+/* Keep column widths stable when Value cell content changes (theme switch, all vs one theme) */
+.tokens-table-fixed-layout {
+ table-layout: fixed;
+ width: 100%;
+}
+
+.tokens-table-fixed-layout td {
+ vertical-align: middle;
+}
+
+.tokens-table-fixed-layout td:not(.tokens-table-value-cell) {
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+/* Value column: one line per theme row; scroll horizontally if needed */
+.tokens-table-fixed-layout td.tokens-table-value-cell {
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+ overflow-wrap: normal;
+ word-break: normal;
+ /**
+ * max-width: 0 is a CSS trick that works with table-layout: fixed.
+ * It forces the cell to respect percentage widths from
and enables horizontal scrolling
+ * when content exceeds the allocated width, instead of expanding the table.
+ */
+ max-width: 0;
+}
+
+.tokens-table-value-stack {
+ min-width: 0;
+}
+
+.ws-token-value-line {
+ display: block;
+ min-width: min-content;
+}
+
+.ws-token-value-line-inner {
+ flex-wrap: nowrap !important;
+ width: max-content;
+ max-width: none;
+}
+
+.ws-theme-label-inline {
+ flex: 0 0 auto;
+}
+
+.ws-theme-label-inline .ws-theme-display-label {
+ white-space: pre;
+}
+
+.ws-token-value-main {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.ws-token-value-main--separated {
+ margin-inline-start: var(--pf-t--global--spacer--md);
+ padding-inline-start: var(--pf-t--global--spacer--md);
+ border-inline-start: var(--pf-t--global--border--width--regular) solid
+ var(--pf-t--global--border--color--default);
+}
+
+.ws-token-value-hex-wrap {
+ flex: 0 1 auto;
+ min-width: 0;
+}
+
+.ws-token-value-hex {
+ display: inline-block;
+ white-space: nowrap;
+ font-family: var(--pf-t--global--font--family--mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.ws-token-swatch-wrap {
+ flex: 0 0 auto;
+}
+
+.ws-token-value-plain-inline {
+ flex: 1 1 auto;
+ min-width: 0;
+ white-space: nowrap;
+}
+
+.ws-token-value-popover-trigger {
+ text-decoration: underline dotted;
+ text-decoration-color: var(--pf-t--global--border--color--default);
+ text-underline-offset: 0.12em;
+}
+
+.ws-token-value-popover-trigger:hover {
+ text-decoration-color: var(--pf-t--global--border--color--clicked);
+}
+
+.ws-token-derivation-popover__theme {
+ margin-block-end: var(--pf-t--global--spacer--md);
+ padding-block-end: var(--pf-t--global--spacer--sm);
+ border-block-end: var(--pf-t--global--border--width--regular) solid
+ var(--pf-t--global--border--color--default);
+}
+
+.ws-token-chain-item {
+ font-family: var(--pf-t--global--font--family--mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.ws-token-chain-value {
+ font-family: var(--pf-t--global--font--family--mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.ws-token-chain-value .pf-v6-c-button.pf-m-link.pf-m-inline {
+ text-decoration: underline;
+ text-decoration-style: dotted;
+ text-underline-offset: 0.15em;
+}
+
+.ws-token-chain-value .pf-v6-c-button.pf-m-link.pf-m-inline:hover {
+ text-decoration-style: solid;
+}
+
+.ws-token-chain-button {
+ font-family: inherit;
+ font-size: inherit;
+ padding: 0;
+ vertical-align: baseline;
+}
+
+.ws-theme-group-label {
+ font-size: var(--pf-t--global--font--size--body--sm, 0.875rem);
+ font-weight: var(--pf-t--global--font--weight--body--default);
+ color: var(--pf-t--global--text--color--subtle);
+ cursor: help;
+ border-radius: var(--pf-t--global--border--radius--small);
+ padding-inline: var(--pf-t--global--spacer--xs);
+ background-color: var(--pf-t--global--background--color--secondary--default);
+ display: inline-block;
+}
+
+.ws-theme-group-label:hover {
+ color: var(--pf-t--global--text--color--subtle);
+ background-color: var(--pf-t--global--background--color--secondary--default);
+}
+
+.ws-theme-group-label:focus-visible {
+ outline: 2px solid var(--pf-t--global--border--color--clicked);
+ outline-offset: 2px;
+}
+
+.ws-theme-display-label {
+ font-family: var(--pf-t--global--font--family--mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: var(--pf-t--global--font--size--body--sm, 0.875rem);
+ line-height: var(--pf-t--global--font--line-height--body--sm, 1.4);
+ white-space: pre;
+}
+
+.ws-theme-abbr-trigger {
+ cursor: help;
+ border-radius: var(--pf-t--global--border--radius--small);
+ display: inline;
+ padding: 0;
+}
+
+.ws-theme-abbr-trigger:hover {
+ background-color: transparent;
+}
+
+.ws-theme-abbr-trigger:focus-visible {
+ outline: 2px solid var(--pf-t--global--border--color--clicked);
+ outline-offset: 2px;
+}
+
.ws-token-swatch {
height: 1em;
display: inline-block;
@@ -7,6 +179,12 @@
box-shadow: var(--pf-t--global--box-shadow--sm);
}
+.ws-tokens-title-theme {
+ font-size: 0.65em;
+ font-weight: var(--pf-t--global--font--weight--body--default);
+ color: var(--pf-t--global--text--color--subtle);
+}
+
@media (min-width: 1200px) {
.tokens-table-outer-wrapper {
/* Allow table to break out of artificial max-width, fill available space */
diff --git a/packages/module/patternfly-docs/content/tokensTable.js b/packages/module/patternfly-docs/content/tokensTable.js
index ed86320..920bc4b 100644
--- a/packages/module/patternfly-docs/content/tokensTable.js
+++ b/packages/module/patternfly-docs/content/tokensTable.js
@@ -7,9 +7,10 @@ import {
EmptyStateActions,
Flex,
FlexItem,
- Grid,
- GridItem,
+ Pagination,
+ Popover,
Title,
+ Tooltip,
capitalize
} from '@patternfly/react-core';
import {
@@ -19,80 +20,74 @@ import {
Tr,
Tbody,
Td,
- ExpandableRowContent,
OuterScrollContainer,
InnerScrollContainer
} from '@patternfly/react-table';
-import { TokensToolbar } from './tokensToolbar';
+import {
+ TokensToolbar,
+ ThemeDisplayLabel,
+ ThemeLabelAbbrevContext,
+ getThemeDisplayName
+} from './tokensToolbar';
import './tokensTable.css';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
-
import c_expandable_section_m_display_lg_PaddingInlineStart from '@patternfly/react-tokens/dist/esm/c_expandable_section_m_display_lg_PaddingInlineStart';
import LevelUpAltIcon from '@patternfly/react-icons/dist/esm/icons/level-up-alt-icon';
{
/* Helper functions */
}
-// Used to combine data grouped by theme under each token name
-const deepMerge = (target, source) => {
- for (const key in source) {
- if (source[key] instanceof Object && key in target) {
- Object.assign(source[key], deepMerge(target[key], source[key]));
- }
+
+/** Keys are stored without CSS leading `--`; product usage uses `--pf-t--…` custom properties. */
+const toCssCustomPropName = (name) => {
+ if (name == null || typeof name !== 'string') {
+ return name;
}
- return Object.assign(target || {}, source);
+ return name.startsWith('--') ? name : `--${name}`;
};
const getTokensFromJson = (tokenJson) => {
- // parse tokens from json, convert from modules, merge into single allTokens obj
+ if (!tokenJson || typeof tokenJson !== 'object') {
+ return {};
+ }
+
const themesArr = Object.keys(tokenJson);
- const themesObj = themesArr.reduce((acc, cur) => {
- acc[cur] = JSON.parse(JSON.stringify(tokenJson[cur]));
- return acc;
- }, {});
- return deepMerge(...Object.values(themesObj));
-};
+ const allTokens = {};
-const getTokenChain = (themeTokenData) => {
- let tokenChain = [];
- // Palette & some color tokens don't have references but we still want to still show their values
- if (!themeTokenData?.references?.[0]) {
- tokenChain.push(themeTokenData.value);
- } else {
- let referenceToken = themeTokenData?.references?.[0];
- while (referenceToken && referenceToken !== undefined) {
- tokenChain = [...tokenChain, referenceToken.name];
- if (referenceToken?.references?.[0]) {
- referenceToken = referenceToken?.references?.[0];
+ const mergeThemeData = (themeName, source, target) => {
+ if (!source || typeof source !== 'object') {
+ return;
+ }
+
+ Object.entries(source).forEach(([key, value]) => {
+ // Check for theme-specific value objects (e.g., { default: ... } or { dark: ... })
+ const hasThemeKey =
+ value &&
+ typeof value === 'object' &&
+ !Array.isArray(value) &&
+ (Object.hasOwn(value, 'default') || Object.hasOwn(value, 'dark')) &&
+ Object.keys(value).length === 1;
+
+ if (hasThemeKey) {
+ const themeValue = value.default ?? value.dark;
+ target[key] = target[key] || {};
+ target[key][themeName] = themeValue;
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
+ target[key] = target[key] || {};
+ mergeThemeData(themeName, value, target[key]);
} else {
- tokenChain.push(referenceToken.value);
- break;
+ target[key] = target[key] || {};
+ target[key][themeName] = value;
}
- }
- }
- return tokenChain;
-};
+ });
+ };
-const showTokenChain = (themeTokenData, hasReferences) => {
- // Show final value if isColorToken but no references - otherwise color value not displayed in table
- const tokenChain = hasReferences ? getTokenChain(themeTokenData) : [themeTokenData.value];
- return (
-
- {tokenChain.map((nextValue, index) => (
-
-
-
- {nextValue}
-
-
- ))}
-
- );
+ themesArr.forEach((themeName) => {
+ // Read-only merge into a fresh tree; no deep clone — theme JSON modules are treated as immutable.
+ mergeThemeData(themeName, tokenJson[themeName], allTokens);
+ });
+
+ return allTokens;
};
const isSearchMatch = (searchValue, tokenName, tokenData) => {
@@ -100,182 +95,1085 @@ const isSearchMatch = (searchValue, tokenName, tokenData) => {
if (searchValue === '') {
return true;
}
+
+ if (!tokenData || typeof tokenData !== 'object') {
+ return false;
+ }
+
// match search term to token name, value, and description
searchValue = searchValue.toLowerCase();
return (
tokenName.toLowerCase().includes(searchValue) ||
- Object.entries(tokenData).some(
- ([_themeName, themeData]) =>
- themeData?.value?.toString().toLowerCase().includes(searchValue) ||
- themeData?.description?.toLowerCase().includes(searchValue)
- )
+ Object.entries(tokenData).some(([_themeName, themeData]) => {
+ if (!themeData || typeof themeData !== 'object') {
+ return false;
+ }
+
+ // Check if value matches (convert to string first)
+ const valueMatch =
+ themeData.value !== undefined &&
+ themeData.value !== null &&
+ String(themeData.value).toLowerCase().includes(searchValue);
+
+ // Check if description matches (only if it's a string)
+ const descriptionMatch =
+ typeof themeData.description === 'string' &&
+ themeData.description.toLowerCase().includes(searchValue);
+
+ return valueMatch || descriptionMatch;
+ })
);
};
const getFilteredTokens = (tokensArr, searchVal) =>
tokensArr.filter(([tokenName, tokenData]) => isSearchMatch(searchVal, tokenName, tokenData));
-const getIsColor = (value) => /^(#|rgb)/.test(value);
+/**
+ * Build reverse reference map: which tokens use each base/palette token
+ * @returns {Object} - { tokenName: [{ category, tokenName }, ...] }
+ */
+const buildReferenceMap = (mergedTokens) => {
+ const referenceMap = {};
+
+ const searchTokens = (tokens, categoryName) => {
+ Object.entries(tokens).forEach(([key, value]) => {
+ if (!value || typeof value !== 'object') {
+ return;
+ }
+
+ // Check if this is a token with theme data
+ if (isTokenWithThemeData(value)) {
+ // This is a token - check its references
+ Object.values(value).forEach(themeData => {
+ if (themeData?.references) {
+ themeData.references.forEach(ref => {
+ if (ref.name) {
+ if (!referenceMap[ref.name]) {
+ referenceMap[ref.name] = [];
+ }
+ // Use just the token name (key), not the full path
+ // Avoid duplicates
+ if (!referenceMap[ref.name].some(r => r.category === categoryName && r.tokenName === key)) {
+ referenceMap[ref.name].push({
+ category: categoryName,
+ tokenName: key
+ });
+ }
+ }
+ });
+ }
+ });
+ } else {
+ // Recurse into nested structure
+ searchTokens(value, categoryName);
+ }
+ });
+ };
+
+ // Search semantic, chart, and base categories for references
+ ['semantic', 'chart', 'base'].forEach(category => {
+ if (mergedTokens[category]) {
+ searchTokens(mergedTokens[category], category);
+ }
+ });
+
+ return referenceMap;
+};
+
+/**
+ * Search across all categories and return results grouped by category
+ * @returns {Object} - { categoryName: [...tokens], ... } with only categories that have matches
+ */
+const getFilteredTokensByCategory = (mergedTokens, allCategoriesArr, searchVal) => {
+ if (!searchVal || searchVal.trim() === '') {
+ return {};
+ }
+
+ const resultsByCategory = {};
+
+ allCategoriesArr.forEach((category) => {
+ const categoryTokens = mergedTokens[category];
+ if (!categoryTokens) {
+ return;
+ }
+
+ const categoryTokensArr = getCategoryTokensArr(category, categoryTokens);
+ const matches = getFilteredTokens(categoryTokensArr, searchVal);
+
+ if (matches.length > 0) {
+ resultsByCategory[category] = matches;
+ }
+ });
+
+ return resultsByCategory;
+};
+
+const getIsColor = (value) => {
+ // Extract the actual value if it's nested in an object
+ const actualValue = (value && typeof value === 'object' && value.default) ? value.default : value;
+ return /^(#|rgb)/.test(actualValue);
+};
+
+/**
+ * Check if a value object represents a token with theme data (has references or value properties)
+ */
+const isTokenWithThemeData = (value) =>
+ value && typeof value === 'object' &&
+ Object.values(value).some(v => v && typeof v === 'object' && (v.references || v.value));
+
const getCategoryTokensArr = (selectedCategory, categoryTokens) => {
+ if (!categoryTokens || typeof categoryTokens !== 'object') {
+ return [];
+ }
+
// Create array of all tokens/nested tokens in selectedCategory
let categoryTokensArr = [];
- if (!['base', 'semantic'].includes(selectedCategory)) {
+ const NESTED_CATEGORIES = ['base', 'semantic'];
+
+ if (!NESTED_CATEGORIES.includes(selectedCategory)) {
categoryTokensArr = Object.entries(categoryTokens);
- } else if (['base', 'semantic'].includes(selectedCategory)) {
+ } else {
// base/semantic combine nested subcategory tokens into flattened arr
- for (var subCategory in categoryTokens) {
- categoryTokensArr.push(...Object.entries(categoryTokens[subCategory]));
+ for (const subCategory in categoryTokens) {
+ if (Object.hasOwn(categoryTokens, subCategory)) {
+ const subCategoryTokens = categoryTokens[subCategory];
+ if (subCategoryTokens && typeof subCategoryTokens === 'object') {
+ categoryTokensArr.push(...Object.entries(subCategoryTokens));
+ }
+ }
}
}
return categoryTokensArr;
};
+/**
+ * Theme select order and "All themes" value rows: Default family (Light: DC, HC, Gl; then Dark: …),
+ * then Unified family the same. Matches ThemeDisplayLabel column semantics.
+ */
+const THEME_OPTION_ORDER = [
+ 'default',
+ 'highcontrast',
+ 'glass',
+ 'dark',
+ 'highcontrast-dark',
+ 'glass-dark',
+ 'redhat',
+ 'redhat-highcontrast',
+ 'redhat-glass',
+ 'redhat-dark',
+ 'redhat-highcontrast-dark',
+ 'redhat-glass-dark'
+];
+
+const THEME_OPTION_ORDER_INDEX = Object.fromEntries(THEME_OPTION_ORDER.map((name, index) => [name, index]));
+
+const compareThemeNames = (a, b) => {
+ const ai = THEME_OPTION_ORDER_INDEX[a];
+ const bi = THEME_OPTION_ORDER_INDEX[b];
+ const aKnown = ai !== undefined;
+ const bKnown = bi !== undefined;
+ if (!aKnown && !bKnown) {
+ return a.localeCompare(b);
+ }
+ if (!aKnown) {
+ return 1;
+ }
+ if (!bKnown) {
+ return -1;
+ }
+ return ai - bi;
+};
+
+const getThemeEntriesForDisplay = (tokenData, selectedTheme, exhibitsThemeVariantValues, allAvailableThemes) => {
+ if (!tokenData || typeof tokenData !== 'object') {
+ return [];
+ }
+
+ const normalizeThemeValue = (themeValue) => {
+ if (!themeValue) {
+ return themeValue;
+ }
+ // If it has a default property, use that (for some token structures)
+ if (Object.hasOwn(themeValue, 'default')) {
+ return themeValue.default;
+ }
+ // If it's an object with a value property, extract the value
+ if (typeof themeValue === 'object' && Object.hasOwn(themeValue, 'value')) {
+ return themeValue;
+ }
+ // Otherwise return as-is
+ return themeValue;
+ };
+
+ if (selectedTheme === 'all') {
+ if (!exhibitsThemeVariantValues) {
+ // For palette tokens, show only one value since they don't vary by theme
+ const firstTheme = Object.keys(tokenData)[0];
+ if (!firstTheme) {
+ return [];
+ }
+ return [[firstTheme, normalizeThemeValue(tokenData[firstTheme])]];
+ }
+
+ // Group themes by their value (don't sort first - group all matching values together)
+ const valueGroups = new Map();
+ const themesWithoutValue = [];
+
+ // Check ALL available themes, not just the ones in tokenData
+ const themesToCheck = allAvailableThemes || Object.keys(tokenData);
+
+ themesToCheck.forEach((themeName) => {
+ const themeValue = tokenData[themeName];
+ const normalizedValue = normalizeThemeValue(themeValue);
+
+ // Track themes that don't have a value
+ if (!normalizedValue || normalizedValue.value === undefined) {
+ themesWithoutValue.push(themeName);
+ return;
+ }
+
+ // Create a key from the value for grouping - sort references to ensure consistent key
+ const valueKey = JSON.stringify({
+ value: normalizedValue?.value,
+ references: normalizedValue?.references?.map(r => r.name).sort()
+ });
+
+ if (!valueGroups.has(valueKey)) {
+ valueGroups.set(valueKey, {
+ themeNames: [],
+ themeValue: normalizedValue
+ });
+ }
+ valueGroups.get(valueKey).themeNames.push(themeName);
+ });
+
+ // Build result with theme names sorted within each group
+ const result = Array.from(valueGroups.values()).map(({ themeNames, themeValue }) => [
+ themeNames.sort(compareThemeNames),
+ themeValue
+ ]);
+
+ // Add group for themes without values if any exist
+ if (themesWithoutValue.length > 0) {
+ result.push([themesWithoutValue.sort(compareThemeNames), null]);
+ }
+
+ return result;
+ }
+
+ const themeValue = tokenData[selectedTheme];
+
+ // If theme doesn't exist for this token, return null to indicate no value
+ if (!themeValue) {
+ return [[selectedTheme, null]];
+ }
+
+ const tokenValue = normalizeThemeValue(themeValue) || {
+ value: '',
+ description: tokenData[Object.keys(tokenData)[0]]?.description || '',
+ references: []
+ };
+ return [[selectedTheme, tokenValue]];
+};
+
+const getTokenChain = (themeTokenData) => {
+ if (!themeTokenData || typeof themeTokenData !== 'object') {
+ return [];
+ }
+
+ const tokenChain = [];
+ const MAX_CHAIN_DEPTH = 50; // Prevent infinite loops
+ let depth = 0;
+
+ if (!themeTokenData.references?.[0]) {
+ const value = themeTokenData.value;
+ if (value !== undefined && value !== null) {
+ tokenChain.push(value);
+ }
+ return tokenChain;
+ }
+
+ let referenceToken = themeTokenData.references[0];
+ while (referenceToken && depth < MAX_CHAIN_DEPTH) {
+ depth++;
+
+ if (referenceToken.name) {
+ tokenChain.push(referenceToken.name);
+ }
+
+ if (referenceToken.references?.[0]) {
+ referenceToken = referenceToken.references[0];
+ } else {
+ if (referenceToken.value !== undefined && referenceToken.value !== null) {
+ tokenChain.push(referenceToken.value);
+ }
+ break;
+ }
+ }
+
+ return tokenChain;
+};
+
+const findTokenCategory = (tokenName, mergedTokens) => {
+ // Search through categories to find which one contains this token
+ for (const category of ['semantic', 'chart', 'base', 'palette']) {
+ if (!mergedTokens[category]) continue;
+
+ const searchInCategory = (tokens) => {
+ for (const [key, value] of Object.entries(tokens)) {
+ if (key === tokenName) {
+ return true;
+ }
+ // Check if it's a nested structure
+ if (value && typeof value === 'object') {
+ if (!isTokenWithThemeData(value) && searchInCategory(value)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ if (searchInCategory(mergedTokens[category])) {
+ return category;
+ }
+ }
+ return null;
+};
+
+const showTokenChain = (themeTokenData, hasReferences, onNavigate, mergedTokens) => {
+ const tokenChain = hasReferences ? getTokenChain(themeTokenData) : [themeTokenData.value];
+
+ return (
+
+ {tokenChain.map((nextValue, index) => {
+ // Check if this value is a token name (starts with pf-t--)
+ const isTokenReference = typeof nextValue === 'string' && nextValue.startsWith('pf-t--');
+ const category = isTokenReference && mergedTokens ? findTokenCategory(nextValue, mergedTokens) : null;
+
+ return (
+
+
+
+ {isTokenReference && category && onNavigate ? (
+
+ ) : (
+ nextValue
+ )}
+
+
+ );
+ })}
+
+ );
+};
+
{
/* Components */
}
-const TokenChain = ({ tokenThemesArr }) => (
- |
- |
-
-
-
- {tokenThemesArr.map(([themeName, themeToken]) => (
-
- {capitalize(themeName)}:
- {showTokenChain(themeToken)}
-
- ))}
-
-
- |
-
-);
+const TokenDerivationPopoverBody = ({ themeName, themeToken, showThemeLabel, exhibitsThemeVariantValues, onNavigate, mergedTokens }) => {
+ const hasReferences = Boolean(themeToken?.references?.[0]);
+ const themeNames = Array.isArray(themeName) ? themeName : [themeName];
+ const isGrouped = themeNames.length > 1;
-const TokenValue = ({ themeName, themeToken, tokenName }) => {
- const isColor = getIsColor(themeToken.value);
return (
+
+
+ {exhibitsThemeVariantValues && showThemeLabel && (
+
+ {isGrouped ? (
+
+
{themeNames.length} themes:
+
+ {themeNames.map((name) => (
+
+
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+ )}
+ {showTokenChain(themeToken, hasReferences, onNavigate, mergedTokens)}
+
+
+ );
+};
+
+const TokenValue = ({
+ themeName,
+ themeToken,
+ tokenName,
+ showThemeLabel,
+ showDerivationPopover,
+ exhibitsThemeVariantValues,
+ selectedTheme,
+ onNavigate,
+ mergedTokens
+}) => {
+ // Handle null themeToken (theme doesn't exist for this token)
+ if (themeToken === null) {
+ const themeNames = Array.isArray(themeName) ? themeName : [themeName];
+ const themeKey = themeNames.join('-');
+ const isGrouped = themeNames.length > 1;
+
+ return (
+
+
+
+ {showThemeLabel && (
+
+ {isGrouped ? (
+
+
+ {themeNames.map((name) => (
+
+
+
+ ))}
+
+
+ }
+ position="top"
+ maxWidth="600px"
+ >
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ —
+
+
+
+
+ );
+ }
+
+ if (!themeToken || typeof themeToken !== 'object') {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(`TokenValue: Invalid themeToken for ${tokenName}`);
+ }
+ return null;
+ }
+
+ // Extract the actual value from token.value if it's nested in an object
+ let displayValue = themeToken.value;
+
+ if (displayValue && typeof displayValue === 'object' && !Array.isArray(displayValue)) {
+ // If value is an object, try to extract a string value
+ const values = Object.values(displayValue);
+ if (values.length > 0 && typeof values[0] === 'string') {
+ displayValue = values[0];
+ } else {
+ // Cannot extract a valid string value
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(`Unable to extract displayValue for token ${tokenName}:`, displayValue);
+ }
+ return null;
+ }
+ }
+
+ // Ensure displayValue is a primitive type suitable for rendering
+ if (typeof displayValue === 'object' || displayValue === undefined || displayValue === null) {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(`DisplayValue is invalid for ${tokenName}:`, typeof displayValue);
+ }
+ return null;
+ }
+
+ // Convert to string if needed
+ displayValue = String(displayValue);
+
+ const isColor = getIsColor(themeToken.value);
+ const valueMain = isColor ? (
- {capitalize(themeName)}:
- {isColor ? (
-
-
-
+
+ {displayValue}
+
+
+
+
+
+ ) : (
+ {displayValue}
+ );
+
+ const valueFlexItem = (
+
+ {showDerivationPopover ? (
+
+ }
+ position="bottom"
+ minWidth="400px"
+ >
+
+
) : (
- {themeToken.value}
+ {valueMain}
)}
-
+
);
-};
-const TokensTableBody = ({ token, expandedTokens, setExpanded, isSemanticLayer, rowIndex }) => {
- const [tokenName, tokenData] = token;
- const tokenThemesArr = Object.entries(tokenData);
- const isExpandable = tokenThemesArr.some(
- ([_themeName, themeToken]) => themeToken.hasOwnProperty('references') || getIsColor(themeToken.value)
+ // Handle themeName being either a string or array of strings (when grouped by value)
+ const themeNames = Array.isArray(themeName) ? themeName : [themeName];
+ const themeKey = themeNames.join('-');
+ const isGrouped = themeNames.length > 1;
+
+ return (
+
+
+
+ {showThemeLabel && (
+
+ {isGrouped ? (
+ // Show count for grouped themes with tooltip showing full names
+
+
+ {themeNames.map((name) => (
+
+
+
+ ))}
+
+
+ }
+ position="top"
+ minWidth="400px"
+ maxWidth="600px"
+ >
+
+
+ ) : (
+ // ThemeDisplayLabel already handles tooltips when abbreviated
+
+ )}
+
+ )}
+ {valueFlexItem}
+
+
+
);
- const isTokenExpanded = expandedTokens.includes(tokenName);
- const tokenDescription = tokenThemesArr[0][1].description;
+};
+
+const OtherCategoryResults = ({ otherResults, onCategoryClick }) => {
+ if (Object.keys(otherResults).length === 0) {
+ return null;
+ }
return (
-
-
- {/* Expandable Row icon */}
- | setExpanded(tokenName, !isTokenExpanded),
- expandId: `${tokenName}-expandable-toggle`
- }
- }
- />
- |
- {tokenName}
- |
- {/* Token values for each theme */}
-
- {tokenThemesArr.map(([themeName, themeToken]) => (
-
+
+ Also found in:
+ {Object.entries(otherResults).map(([category, tokens], index) => (
+
+ {index > 0 && ', '}
+
+
+ ))}
+
+ );
+};
+
+const UsedByCell = ({ tokenName, referenceMap, onNavigate }) => {
+ const references = referenceMap[tokenName] || [];
+
+ if (references.length === 0) {
+ return | — | ;
+ }
+
+ if (references.length <= 3) {
+ return (
+
+
+ {references.map((ref, index) => (
+
+
+
))}
- |
+
+
+ );
+ }
+
+ // 4+ references: show count with popover
+ return (
+
+
+ {references.map((ref, index) => (
+
+
+
+ ))}
+
+ }
+ position="left"
+ minWidth="400px"
+ >
+
+
+ |
+ );
+};
- {/* Description - only for semantic tokens */}
- {isSemanticLayer && {tokenDescription} | }
-
+const TokensTableBody = ({
+ token,
+ isSemanticLayer,
+ isChartLayer,
+ isBaseLayer,
+ isPaletteLayer,
+ exhibitsThemeVariantValues,
+ selectedTheme,
+ referenceMap,
+ onNavigate,
+ mergedTokens,
+ allAvailableThemes
+}) => {
+ const [tokenName, tokenData] = token;
+ const tokenThemesArr = getThemeEntriesForDisplay(tokenData, selectedTheme, exhibitsThemeVariantValues, allAvailableThemes);
+ const tokenDescription = tokenThemesArr[0]?.[1]?.description || '';
+ const showUsedBy = isBaseLayer || isPaletteLayer;
- {/* Expandable token chain */}
- {isTokenExpanded && }
-
+ return (
+
+
+ {toCssCustomPropName(tokenName)}
+ |
+
+
+ {tokenThemesArr.map(([themeName, themeToken], index) => {
+ // themeName can be a string or array of strings (when grouped by value)
+ const key = Array.isArray(themeName) ? themeName.join('-') : themeName;
+ return (
+
+
+
+ );
+ })}
+
+ |
+ {isSemanticLayer && {tokenDescription} | }
+ {showUsedBy && }
+
);
};
export const TokensTable = ({ tokenJson }) => {
+ // Initialize state from URL params if present
+ const getInitialStateFromURL = () => {
+ if (typeof window === 'undefined') {
+ return { category: 'semantic', search: '' };
+ }
+ const params = new URLSearchParams(window.location.search);
+ return {
+ category: params.get('category') || 'semantic',
+ search: params.get('token') || ''
+ };
+ };
+
+ const initialState = React.useMemo(getInitialStateFromURL, []);
+
+ // Track if we're currently handling browser navigation to avoid pushState loops
+ const isNavigatingRef = React.useRef(false);
+
// state variables
- const [searchValue, setSearchValue] = React.useState('');
- const [expandedTokens, setExpandedTokens] = React.useState([]);
- const [selectedCategory, setSelectedCategory] = React.useState('semantic');
+ const [searchValue, setSearchValue] = React.useState(initialState.search);
+ const [selectedCategory, setSelectedCategory] = React.useState(initialState.category);
+ // All categories share the same theme selection except palette (which is always 'all')
+ const [sharedTheme, setSharedTheme] = React.useState('default');
+ const [page, setPage] = React.useState(1);
+ const [perPage, setPerPage] = React.useState(20);
+
+ // Palette always uses 'all', other categories use shared theme
+ const selectedTheme = selectedCategory === 'palette' ? 'all' : sharedTheme;
+
+ // Memoize sorted theme names to avoid creating new arrays on every render
+ const sortedThemeNames = React.useMemo(
+ () => Object.keys(tokenJson).sort(compareThemeNames),
+ [tokenJson]
+ );
+
+ // Per-theme JSON module references are stable across MDX re-renders
+ const mergeDepList = React.useMemo(
+ () => sortedThemeNames.map((themeName) => tokenJson[themeName]),
+ [sortedThemeNames, tokenJson]
+ );
+
+ const mergedTokens = React.useMemo(() => getTokensFromJson(tokenJson), mergeDepList);
+
+ const themeOptions = React.useMemo(() => ['all', ...sortedThemeNames], [sortedThemeNames]);
+
+ // List of all available themes (excluding 'all')
+ const allAvailableThemes = React.useMemo(() => sortedThemeNames, [sortedThemeNames]);
+
+ // Build reference map for "Used by" column
+ const referenceMap = React.useMemo(() => buildReferenceMap(mergedTokens), [mergedTokens]);
+
+ const deferredSearchValue = React.useDeferredValue(searchValue);
+
+ const tokenCategoryOrder = ['chart', 'semantic', 'base', 'palette'];
+ const allCategoryKeys = React.useMemo(
+ () => Object.keys(mergedTokens).filter((c) => c !== 'default'),
+ [mergedTokens]
+ );
+ const allCategoriesArr = React.useMemo(
+ () => [
+ ...tokenCategoryOrder.filter((c) => allCategoryKeys.includes(c)),
+ ...allCategoryKeys.filter((c) => !tokenCategoryOrder.includes(c)).sort()
+ ],
+ [allCategoryKeys]
+ );
- const allTokens = getTokensFromJson(tokenJson);
const isSemanticLayer = selectedCategory === 'semantic';
- const categoryTokens = allTokens[selectedCategory];
- // get tokens for selected category
- const categoryTokensArr = getCategoryTokensArr(selectedCategory, categoryTokens);
- // filter selected category tokens based on search term
- const searchResults = getFilteredTokens(categoryTokensArr, searchValue);
- // remove extra 'default' category
- delete allTokens.default;
- const allCategoriesArr = Object.keys(allTokens);
-
- // helper funcs
- const setExpanded = (tokenName, isExpanding = true) =>
- setExpandedTokens((prevExpanded) => {
- const otherExpandedTokens = prevExpanded.filter((n) => n !== tokenName);
- return isExpanding ? [...otherExpandedTokens, tokenName] : otherExpandedTokens;
- });
+ const isChartLayer = selectedCategory === 'chart';
+ const isBaseLayer = selectedCategory === 'base';
+ const isPaletteLayer = selectedCategory === 'palette';
+ // Base tokens can have theme variants (e.g., high contrast differs), only palette is truly invariant
+ const exhibitsThemeVariantValues = selectedCategory === 'semantic' || selectedCategory === 'chart' || selectedCategory === 'base';
+ const showUsedByColumn = isBaseLayer || isPaletteLayer;
+ const categoryTokens = mergedTokens[selectedCategory];
+ const categoryTokensArr = React.useMemo(
+ () => getCategoryTokensArr(selectedCategory, categoryTokens),
+ [selectedCategory, categoryTokens]
+ );
+
+ // When searching, search across all categories; otherwise just filter current category
+ const hasSearchTerm = deferredSearchValue && deferredSearchValue.trim() !== '';
+ const searchResultsByCategory = React.useMemo(
+ () => (hasSearchTerm ? getFilteredTokensByCategory(mergedTokens, allCategoriesArr, deferredSearchValue) : {}),
+ [mergedTokens, allCategoriesArr, deferredSearchValue, hasSearchTerm]
+ );
+
+ // Always show current category results
+ const searchResults = React.useMemo(
+ () => getFilteredTokens(categoryTokensArr, deferredSearchValue),
+ [categoryTokensArr, deferredSearchValue]
+ );
+
+ // Other categories with results (excluding current category)
+ const otherCategoryResults = React.useMemo(() => {
+ if (!hasSearchTerm) {
+ return {};
+ }
+ const others = { ...searchResultsByCategory };
+ delete others[selectedCategory];
+ return others;
+ }, [hasSearchTerm, searchResultsByCategory, selectedCategory]);
+
+ // Memoize results count to prevent SearchInput from losing focus
+ const resultsCount = React.useMemo(() => searchResults.length.toString(), [searchResults.length]);
+
+ // Pagination calculations
+ const totalItems = searchResults.length;
+ const startIndex = (page - 1) * perPage;
+ const endIndex = startIndex + perPage;
+ const paginatedResults = React.useMemo(
+ () => searchResults.slice(startIndex, endIndex),
+ [searchResults, startIndex, endIndex]
+ );
+
+ // Reset to page 1 when search or category changes
+ React.useEffect(() => {
+ setPage(1);
+ }, [deferredSearchValue, selectedCategory]);
+
+ // Sync URL with state (for shareable URLs and browser back/forward)
+ React.useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ // Skip if we're handling browser navigation
+ if (isNavigatingRef.current) {
+ isNavigatingRef.current = false;
+ return;
+ }
+
+ const params = new URLSearchParams(window.location.search);
+ // Normalize params the same way we do in popstate - missing params use defaults
+ const currentCategory = params.get('category') || 'semantic';
+ const currentToken = params.get('token') || '';
+
+ // Only update URL if state actually changed
+ if (currentCategory === selectedCategory && currentToken === searchValue) {
+ return;
+ }
+
+ const newParams = new URLSearchParams();
+ if (selectedCategory !== 'semantic') {
+ newParams.set('category', selectedCategory);
+ }
+ if (searchValue) {
+ newParams.set('token', searchValue);
+ }
+
+ const queryString = newParams.toString();
+ const newUrl = queryString
+ ? `${window.location.pathname}?${queryString}${window.location.hash}`
+ : `${window.location.pathname}${window.location.hash}`;
+
+ window.history.pushState(null, '', newUrl);
+ }, [selectedCategory, searchValue]);
+
+ // Sync state when URL changes (browser back/forward)
+ React.useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const handlePopState = () => {
+ const params = new URLSearchParams(window.location.search);
+ const category = params.get('category') || 'semantic';
+ const token = params.get('token') || '';
+
+ // Mark that we're handling browser navigation so pushState doesn't run
+ isNavigatingRef.current = true;
+ setSelectedCategory(category);
+ setSearchValue(token);
+ };
+
+ window.addEventListener('popstate', handlePopState);
+ return () => window.removeEventListener('popstate', handlePopState);
+ }, []);
+
+ const handleSetPage = (_event, newPage) => {
+ setPage(newPage);
+ };
+
+ const handlePerPageSelect = (_event, newPerPage) => {
+ setPerPage(newPerPage);
+ setPage(1);
+ };
+
+ const handleCategoryChange = React.useCallback((category) => {
+ setSelectedCategory(category);
+ }, []);
+
+ // Memoize setters to prevent TokensToolbar from re-rendering unnecessarily
+ const handleSearchChange = React.useCallback((value) => {
+ setSearchValue(value);
+ }, []);
+
+ const handleThemeChange = React.useCallback((theme) => {
+ // Update shared theme (palette is always 'all' and selector is disabled, so this won't be called for palette)
+ setSharedTheme(theme);
+ }, []);
+
+ // Navigate to category and search for specific token
+ const handleNavigateToToken = React.useCallback((category, tokenName) => {
+ setSelectedCategory(category);
+ setSearchValue(tokenName);
+ setPage(1); // Reset to first page
+ }, []);
return (
-
+ searchValue={searchValue}
+ setSearchValue={handleSearchChange}
+ selectedCategory={selectedCategory}
+ setSelectedCategory={handleCategoryChange}
+ selectedTheme={selectedTheme}
+ setSelectedTheme={handleThemeChange}
+ themeOptions={themeOptions}
+ resultsCount={resultsCount}
+ categories={allCategoriesArr}
+ />
+
- {capitalize(selectedCategory)} tokens
+
+ {capitalize(selectedCategory)} tokens {selectedTheme !== 'all' && (
+ ({getThemeDisplayName(selectedTheme).trim()})
+ )}
+
{searchResults.length > 0 ? (
-
-
-
- {/* Only semantic tokens have description, adjust columns accordingly */}
- |
- Name |
- Value |
- {isSemanticLayer && Description | }
-
-
-
- {/* Loop through row for each token in current layer */}
- {searchResults.map((token, rowIndex) => (
-
- ))}
-
+ <>
+
+
+
+
+
+ | Name |
+
+ Value
+ |
+ {isSemanticLayer && Description | }
+ {showUsedByColumn && Used by | }
+
+
+
+
+ {paginatedResults.map((token) => (
+
+ ))}
+
+
+
+ >
) : (
No results match the filter criteria. Clear all filters and try again.
diff --git a/packages/module/patternfly-docs/content/tokensToolbar.js b/packages/module/patternfly-docs/content/tokensToolbar.js
index 95b0915..7b1962f 100644
--- a/packages/module/patternfly-docs/content/tokensToolbar.js
+++ b/packages/module/patternfly-docs/content/tokensToolbar.js
@@ -1,17 +1,148 @@
import React from 'react';
import {
+ Button,
+ Divider,
MenuToggle,
SearchInput,
Select,
+ SelectGroup,
SelectList,
SelectOption,
Toolbar,
ToolbarItem,
ToolbarContent,
+ Tooltip,
capitalize
} from '@patternfly/react-core';
import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
+/**
+ * Theme configuration constants
+ * Structure: [theme family, light/dark mode, contrast variant]
+ */
+const THEME_LABEL_PARTS = {
+ default: ['Default theme', 'Light', 'Default contrast'],
+ dark: ['Default theme', 'Dark', 'Default contrast'],
+ glass: ['Default theme', 'Light', 'Glass'],
+ 'glass-dark': ['Default theme', 'Dark', 'Glass'],
+ highcontrast: ['Default theme', 'Light', 'High contrast'],
+ 'highcontrast-dark': ['Default theme', 'Dark', 'High contrast'],
+ redhat: ['Unified theme', 'Light', 'Default contrast'],
+ 'redhat-dark': ['Unified theme', 'Dark', 'Default contrast'],
+ 'redhat-glass': ['Unified theme', 'Light', 'Glass'],
+ 'redhat-glass-dark': ['Unified theme', 'Dark', 'Glass'],
+ 'redhat-highcontrast': ['Unified theme', 'Light', 'High contrast'],
+ 'redhat-highcontrast-dark': ['Unified theme', 'Dark', 'High contrast']
+};
+
+/** Full segment → acronym mapping */
+const THEME_SEGMENT_ABBREV = {
+ 'Default theme': 'DT',
+ 'Unified theme': 'UT',
+ Light: 'Lt',
+ Dark: 'Dk',
+ 'Default contrast': 'DC',
+ 'High contrast': 'HC',
+ Glass: 'Gl'
+};
+
+// Pre-compute static values to avoid recalculating on every render
+const THEME_LABEL_ROWS = Object.values(THEME_LABEL_PARTS);
+const THEME_LABEL_COL_WIDTHS = (() => {
+ const rows = THEME_LABEL_ROWS;
+ return [
+ Math.max(...rows.map((r) => r[0].length)),
+ Math.max(...rows.map((r) => r[1].length)),
+ Math.max(...rows.map((r) => r[2].length))
+ ];
+})();
+
+const toAbbrevParts = (fullParts) => fullParts.map((p) => THEME_SEGMENT_ABBREV[p] || p);
+
+const THEME_ACRONYM_PARTS = Object.fromEntries(
+ Object.entries(THEME_LABEL_PARTS).map(([key, parts]) => [key, toAbbrevParts(parts)])
+);
+
+const THEME_ACRONYM_ROWS = Object.values(THEME_ACRONYM_PARTS);
+const THEME_ACRONYM_COL_WIDTHS = (() => {
+ const rows = THEME_ACRONYM_ROWS;
+ return [
+ Math.max(...rows.map((r) => r[0].length)),
+ Math.max(...rows.map((r) => r[1].length)),
+ Math.max(...rows.map((r) => r[2].length))
+ ];
+})();
+
+const padThemeCol = (s, w) => s.padEnd(w, ' ');
+
+const formatAlignedThemeParts = (parts) => {
+ const [w0, w1, w2] = THEME_LABEL_COL_WIDTHS;
+ return `${padThemeCol(parts[0], w0)} | ${padThemeCol(parts[1], w1)} | ${padThemeCol(parts[2], w2)}`;
+};
+
+export const getThemeDisplayName = (themeName) => {
+ if (themeName === 'all') {
+ return 'All themes';
+ }
+ const parts = THEME_LABEL_PARTS[themeName];
+ if (parts) {
+ return formatAlignedThemeParts(parts);
+ }
+ return themeName.split('-').map((part) => capitalize(part)).join(' ');
+};
+
+export const ThemeLabelAbbrevContext = React.createContext(false);
+
+/**
+ * Abbreviated theme row with single tooltip showing full theme name.
+ * Improves accessibility by avoiding multiple tab stops.
+ */
+const ThemeAbbrevSegments = ({ themeName }) => {
+ const abbrParts = THEME_ACRONYM_PARTS[themeName];
+ const fullParts = THEME_LABEL_PARTS[themeName];
+ const [w0, w1, w2] = THEME_ACRONYM_COL_WIDTHS;
+ const paddedCols = [
+ padThemeCol(abbrParts[0], w0),
+ padThemeCol(abbrParts[1], w1),
+ padThemeCol(abbrParts[2], w2)
+ ];
+
+ const fullLabel = formatAlignedThemeParts(fullParts);
+ const abbreviatedText = `${paddedCols[0]} | ${paddedCols[1]} | ${paddedCols[2]}`;
+
+ return (
+
+
+
+ );
+};
+
+/** Renders theme text; known themes use monospace + padded spaces so `|` columns line up */
+export const ThemeDisplayLabel = ({ themeName, fullLabel = false }) => {
+ const contextAbbrev = React.useContext(ThemeLabelAbbrevContext);
+ const abbreviated = fullLabel ? false : contextAbbrev;
+ const parts = THEME_LABEL_PARTS[themeName];
+
+ if (themeName === 'all') {
+ return All themes;
+ }
+ if (parts) {
+ if (abbreviated) {
+ return ;
+ }
+ return {formatAlignedThemeParts(parts)};
+ }
+ return {themeName.split('-').map((p) => capitalize(p)).join(' ')};
+};
+
+
+// Category configuration constants
+const TOKEN_CATEGORY_GROUP_PRIMARY = new Set(['chart', 'semantic']);
+// Only palette tokens are truly theme-invariant (base tokens CAN vary, e.g., high contrast)
+const THEME_INVARIANT_CATEGORIES = new Set(['palette']);
+
const TokensToolbarSelect = ({ selectedCategory, setSelectedCategory, categories }) => {
const [isSelectOpen, setIsSelectOpen] = React.useState(false);
const handleSelect = (_e, category) => {
@@ -21,6 +152,10 @@ const TokensToolbarSelect = ({ selectedCategory, setSelectedCategory, categories
setIsSelectOpen(!isSelectOpen);
};
+ const primaryCategories = categories.filter((c) => TOKEN_CATEGORY_GROUP_PRIMARY.has(c));
+ const secondaryCategories = categories.filter((c) => !TOKEN_CATEGORY_GROUP_PRIMARY.has(c));
+ const showGroupDivider = primaryCategories.length > 0 && secondaryCategories.length > 0;
+
const SelectToggle = (toggleRef) => (
}
@@ -32,6 +167,13 @@ const TokensToolbarSelect = ({ selectedCategory, setSelectedCategory, categories
);
+ const renderCategoryOptions = (list) =>
+ list.map((category, idx) => (
+
+ {capitalize(category)} tokens
+
+ ));
+
return (
);
};
-export const TokensToolbar = ({
+const TokensThemeSelect = ({ selectedTheme, setSelectedTheme, themeOptions, selectedCategory }) => {
+ const [isSelectOpen, setIsSelectOpen] = React.useState(false);
+ const handleSelect = (_e, themeName) => {
+ if (!(selectedTheme === themeName)) {
+ setSelectedTheme(themeName);
+ }
+ setIsSelectOpen(!isSelectOpen);
+ };
+
+ // Theme selector is disabled for categories that don't have theme variants
+ const isDisabled = THEME_INVARIANT_CATEGORIES.has(selectedCategory);
+
+ const ariaSelectedTheme = getThemeDisplayName(selectedTheme)
+ .replace(/\|/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ const SelectToggle = (toggleRef) => (
+ !isDisabled && setIsSelectOpen(!isSelectOpen)}
+ isExpanded={isSelectOpen}
+ isDisabled={isDisabled}
+ >
+
+
+ );
+
+ const themesAfterAll = themeOptions.slice(1);
+ const defaultThemeOptions = themesAfterAll.filter((name) => !name.startsWith('redhat'));
+ const unifiedThemeOptions = themesAfterAll.filter((name) => name.startsWith('redhat'));
+ const showUnifiedDivider = defaultThemeOptions.length > 0 && unifiedThemeOptions.length > 0;
+
+ const renderThemeOption = (themeName) => (
+
+
+
+ );
+
+ return (
+
+ );
+};
+
+const TokensToolbarComponent = ({
selectedCategory,
setSelectedCategory,
+ selectedTheme,
+ setSelectedTheme,
searchValue,
setSearchValue,
resultsCount,
- categories
+ categories,
+ themeOptions
}) => {
return (
setSearchValue(value)}
onClear={() => setSearchValue('')}
@@ -74,6 +291,15 @@ export const TokensToolbar = ({
/>
+
+
+
+
);
};
+
+// Memoize the toolbar to prevent unnecessary re-renders that cause SearchInput to lose focus
+export const TokensToolbar = React.memo(TokensToolbarComponent);
diff --git a/packages/module/patternfly-docs/generated/foundations-and-styles/design-tokens/all-design-tokens/design-tokens.js b/packages/module/patternfly-docs/generated/foundations-and-styles/design-tokens/all-design-tokens/design-tokens.js
index ecdc813..5074899 100644
--- a/packages/module/patternfly-docs/generated/foundations-and-styles/design-tokens/all-design-tokens/design-tokens.js
+++ b/packages/module/patternfly-docs/generated/foundations-and-styles/design-tokens/all-design-tokens/design-tokens.js
@@ -2,6 +2,16 @@ import React from 'react';
import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components';
import * as defaultTokens from '../../../../content/./token-layers-default.json';
import * as darkTokens from '../../../../content/./token-layers-dark.json';
+import * as glassTokens from '../../../../content/./token-layers-glass.json';
+import * as glassDarkTokens from '../../../../content/./token-layers-glass-dark.json';
+import * as highcontrastTokens from '../../../../content/./token-layers-highcontrast.json';
+import * as highcontrastDarkTokens from '../../../../content/./token-layers-highcontrast-dark.json';
+import * as redhatTokens from '../../../../content/./token-layers-redhat.json';
+import * as redhatDarkTokens from '../../../../content/./token-layers-redhat-dark.json';
+import * as redhatGlassTokens from '../../../../content/./token-layers-redhat-glass.json';
+import * as redhatGlassDarkTokens from '../../../../content/./token-layers-redhat-glass-dark.json';
+import * as redhatHighcontrastTokens from '../../../../content/./token-layers-redhat-highcontrast.json';
+import * as redhatHighcontrastDarkTokens from '../../../../content/./token-layers-redhat-highcontrast-dark.json';
import { TokensTable } from '../../../../content/./tokensTable.js';
const pageData = {
"id": "All design tokens",
@@ -16,21 +26,45 @@ const pageData = {
"tabName": null,
"slug": "/foundations-and-styles/design-tokens/all-design-tokens/design-tokens",
"sourceLink": "https://github.com/patternfly/patternfly-org/blob/main/packages/module/patternfly-docs/content/all-patternfly-tokens.md",
- "relPath": "packages/module/patternfly-docs/content/all-patternfly-tokens.md"
+ "relPath": "packages/module/patternfly-docs/content/all-patternfly-tokens.md",
+ "sortValue": 5
};
pageData.liveContext = {
defaultTokens,
darkTokens,
+ glassTokens,
+ glassDarkTokens,
+ highcontrastTokens,
+ highcontrastDarkTokens,
+ redhatTokens,
+ redhatDarkTokens,
+ redhatGlassTokens,
+ redhatGlassDarkTokens,
+ redhatHighcontrastTokens,
+ redhatHighcontrastDarkTokens,
TokensTable
};
-pageData.relativeImports = "import * as defaultTokens from 'content/./token-layers-default.json';,import * as darkTokens from 'content/./token-layers-dark.json';,import { TokensTable } from 'content/./tokensTable.js';"
+pageData.relativeImports = "import * as defaultTokens from 'content/./token-layers-default.json';,import * as darkTokens from 'content/./token-layers-dark.json';,import * as glassTokens from 'content/./token-layers-glass.json';,import * as glassDarkTokens from 'content/./token-layers-glass-dark.json';,import * as highcontrastTokens from 'content/./token-layers-highcontrast.json';,import * as highcontrastDarkTokens from 'content/./token-layers-highcontrast-dark.json';,import * as redhatTokens from 'content/./token-layers-redhat.json';,import * as redhatDarkTokens from 'content/./token-layers-redhat-dark.json';,import * as redhatGlassTokens from 'content/./token-layers-redhat-glass.json';,import * as redhatGlassDarkTokens from 'content/./token-layers-redhat-glass-dark.json';,import * as redhatHighcontrastTokens from 'content/./token-layers-redhat-highcontrast.json';,import * as redhatHighcontrastDarkTokens from 'content/./token-layers-redhat-highcontrast-dark.json';,import { TokensTable } from 'content/./tokensTable.js';"
pageData.examples = {
};
const Component = () => (
-
+
);
Component.displayName = 'FoundationsAndStylesDesignTokensAllDesignTokensDesignTokensDocs';
diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js
index 593e06c..02a0a74 100644
--- a/packages/module/patternfly-docs/generated/index.js
+++ b/packages/module/patternfly-docs/generated/index.js
@@ -7,6 +7,7 @@ module.exports = {
subsection: "design-tokens",
source: "design-tokens",
tabName: null,
+ sortValue: 5,
Component: () => import(/* webpackChunkName: "foundations-and-styles/design-tokens/all-design-tokens/design-tokens/index" */ './foundations-and-styles/design-tokens/all-design-tokens/design-tokens')
}
};
\ No newline at end of file