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' && ( + <small className="ws-tokens-title-theme"> ({getThemeDisplayName(selectedTheme).trim()})</small> + )} + {searchResults.length > 0 ? ( - - - - {/* Only semantic tokens have description, adjust columns accordingly */} - - - - {isSemanticLayer && } - - - - {/* Loop through row for each token in current layer */} - {searchResults.map((token, rowIndex) => ( - - ))} -
NameValueDescription
+ <> + + + + + + + + {isSemanticLayer && } + {showUsedByColumn && } + + + + + {paginatedResults.map((token) => ( + + ))} + +
Name + Value + DescriptionUsed by
+ + ) : ( 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