@patternfly/design-tokens 1.15.2 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/css/tokens-charts-dark.scss +4 -2
- package/build/css/tokens-charts-highcontrast-dark.scss +175 -0
- package/build/css/tokens-charts-highcontrast.scss +175 -0
- package/build/css/tokens-charts.scss +4 -2
- package/build/css/tokens-dark.scss +1 -1
- package/build/css/tokens-default.scss +1 -1
- package/build/css/tokens-felt-dark.scss +1 -1
- package/build/css/tokens-felt-glass-dark.scss +1 -1
- package/build/css/tokens-felt-glass.scss +1 -1
- package/build/css/tokens-felt-highcontrast-dark.scss +1 -1
- package/build/css/tokens-felt-highcontrast.scss +10 -2
- package/build/css/tokens-felt.scss +1 -1
- package/build/css/tokens-glass-dark.scss +1 -1
- package/build/css/tokens-glass.scss +1 -1
- package/build/css/tokens-palette.scss +1 -1
- package/build/css/tokens-redhat-highcontrast.scss +1 -1
- package/build.js +7 -1
- package/config.charts.dark.json +1 -1
- package/config.charts.highcontrast.dark.json +23 -0
- package/config.charts.highcontrast.json +23 -0
- package/config.charts.json +1 -1
- package/config.felt-highcontrast.json +2 -2
- package/config.layers.felt-highcontrast.json +2 -2
- package/config.layers.felt.json +2 -2
- package/package.json +1 -1
- package/patternfly-docs/content/token-layers-felt-dark.json +60 -16
- package/patternfly-docs/content/token-layers-felt-highcontrast.json +53027 -0
- package/patternfly-docs/content/token-layers-felt.json +65461 -0
- package/patternfly-docs/content/tokensTable.css +178 -0
- package/patternfly-docs/content/tokensTable.js +1078 -180
- package/patternfly-docs/content/tokensToolbar.js +240 -11
- package/patternfly-docs/generated/foundations-and-styles/design-tokens/all-design-tokens/design-tokens.js +37 -3
- package/patternfly-docs/generated/index.js +1 -0
- package/plugins/export-patternfly-tokens/dist/ui.html +9 -9
- package/plugins/export-patternfly-tokens/src/ui.tsx +8 -8
- package/tokens/default/dark/charts.dark.json +11 -3
- package/tokens/default/dark/charts.highcontrast.dark.json +11 -3
- package/tokens/default/light/charts.highcontrast.json +11 -3
- package/tokens/default/light/charts.json +11 -3
- package/tokens/felt/dark/felt.color.dark.json +41 -41
- package/tokens/felt/glass/felt.color.glass.json +9 -9
- package/tokens/felt/glass-dark/felt.color.glass.dark.json +41 -41
- package/tokens/felt/highcontrast/felt.color.highcontrast.json +9 -9
- package/tokens/felt/highcontrast-dark/felt.color.highcontrast.dark.json +9 -9
- package/tokens/felt/light/felt.color.json +9 -9
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
EmptyStateActions,
|
|
8
8
|
Flex,
|
|
9
9
|
FlexItem,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Pagination,
|
|
11
|
+
Popover,
|
|
12
12
|
Title,
|
|
13
|
+
Tooltip,
|
|
13
14
|
capitalize
|
|
14
15
|
} from '@patternfly/react-core';
|
|
15
16
|
import {
|
|
@@ -19,80 +20,74 @@ import {
|
|
|
19
20
|
Tr,
|
|
20
21
|
Tbody,
|
|
21
22
|
Td,
|
|
22
|
-
ExpandableRowContent,
|
|
23
23
|
OuterScrollContainer,
|
|
24
24
|
InnerScrollContainer
|
|
25
25
|
} from '@patternfly/react-table';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
TokensToolbar,
|
|
28
|
+
ThemeDisplayLabel,
|
|
29
|
+
ThemeLabelAbbrevContext,
|
|
30
|
+
getThemeDisplayName
|
|
31
|
+
} from './tokensToolbar';
|
|
27
32
|
import './tokensTable.css';
|
|
28
33
|
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
|
29
|
-
|
|
30
34
|
import c_expandable_section_m_display_lg_PaddingInlineStart from '@patternfly/react-tokens/dist/esm/c_expandable_section_m_display_lg_PaddingInlineStart';
|
|
31
35
|
import LevelUpAltIcon from '@patternfly/react-icons/dist/esm/icons/level-up-alt-icon';
|
|
32
36
|
|
|
33
37
|
{
|
|
34
38
|
/* Helper functions */
|
|
35
39
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
40
|
+
|
|
41
|
+
/** Keys are stored without CSS leading `--`; product usage uses `--pf-t--…` custom properties. */
|
|
42
|
+
const toCssCustomPropName = (name) => {
|
|
43
|
+
if (name == null || typeof name !== 'string') {
|
|
44
|
+
return name;
|
|
42
45
|
}
|
|
43
|
-
return
|
|
46
|
+
return name.startsWith('--') ? name : `--${name}`;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
const getTokensFromJson = (tokenJson) => {
|
|
47
|
-
|
|
50
|
+
if (!tokenJson || typeof tokenJson !== 'object') {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
const themesArr = Object.keys(tokenJson);
|
|
49
|
-
const
|
|
50
|
-
acc[cur] = JSON.parse(JSON.stringify(tokenJson[cur]));
|
|
51
|
-
return acc;
|
|
52
|
-
}, {});
|
|
53
|
-
return deepMerge(...Object.values(themesObj));
|
|
54
|
-
};
|
|
55
|
+
const allTokens = {};
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const mergeThemeData = (themeName, source, target) => {
|
|
58
|
+
if (!source || typeof source !== 'object') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Object.entries(source).forEach(([key, value]) => {
|
|
63
|
+
// Check for theme-specific value objects (e.g., { default: ... } or { dark: ... })
|
|
64
|
+
const hasThemeKey =
|
|
65
|
+
value &&
|
|
66
|
+
typeof value === 'object' &&
|
|
67
|
+
!Array.isArray(value) &&
|
|
68
|
+
(Object.hasOwn(value, 'default') || Object.hasOwn(value, 'dark')) &&
|
|
69
|
+
Object.keys(value).length === 1;
|
|
70
|
+
|
|
71
|
+
if (hasThemeKey) {
|
|
72
|
+
const themeValue = value.default ?? value.dark;
|
|
73
|
+
target[key] = target[key] || {};
|
|
74
|
+
target[key][themeName] = themeValue;
|
|
75
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
76
|
+
target[key] = target[key] || {};
|
|
77
|
+
mergeThemeData(themeName, value, target[key]);
|
|
67
78
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
target[key] = target[key] || {};
|
|
80
|
+
target[key][themeName] = value;
|
|
70
81
|
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return tokenChain;
|
|
74
|
-
};
|
|
82
|
+
});
|
|
83
|
+
};
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div
|
|
83
|
-
key={`${index}`}
|
|
84
|
-
style={{
|
|
85
|
-
padding: `4px 0 4px calc(${c_expandable_section_m_display_lg_PaddingInlineStart.value} * ${index})`
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
<LevelUpAltIcon style={{ transform: 'rotate(90deg)' }} />
|
|
89
|
-
<span style={{ paddingInlineStart: c_expandable_section_m_display_lg_PaddingInlineStart.value }}>
|
|
90
|
-
{nextValue}
|
|
91
|
-
</span>
|
|
92
|
-
</div>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
85
|
+
themesArr.forEach((themeName) => {
|
|
86
|
+
// Read-only merge into a fresh tree; no deep clone — theme JSON modules are treated as immutable.
|
|
87
|
+
mergeThemeData(themeName, tokenJson[themeName], allTokens);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return allTokens;
|
|
96
91
|
};
|
|
97
92
|
|
|
98
93
|
const isSearchMatch = (searchValue, tokenName, tokenData) => {
|
|
@@ -100,182 +95,1085 @@ const isSearchMatch = (searchValue, tokenName, tokenData) => {
|
|
|
100
95
|
if (searchValue === '') {
|
|
101
96
|
return true;
|
|
102
97
|
}
|
|
98
|
+
|
|
99
|
+
if (!tokenData || typeof tokenData !== 'object') {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
103
|
// match search term to token name, value, and description
|
|
104
104
|
searchValue = searchValue.toLowerCase();
|
|
105
105
|
return (
|
|
106
106
|
tokenName.toLowerCase().includes(searchValue) ||
|
|
107
|
-
Object.entries(tokenData).some(
|
|
108
|
-
(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
Object.entries(tokenData).some(([_themeName, themeData]) => {
|
|
108
|
+
if (!themeData || typeof themeData !== 'object') {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if value matches (convert to string first)
|
|
113
|
+
const valueMatch =
|
|
114
|
+
themeData.value !== undefined &&
|
|
115
|
+
themeData.value !== null &&
|
|
116
|
+
String(themeData.value).toLowerCase().includes(searchValue);
|
|
117
|
+
|
|
118
|
+
// Check if description matches (only if it's a string)
|
|
119
|
+
const descriptionMatch =
|
|
120
|
+
typeof themeData.description === 'string' &&
|
|
121
|
+
themeData.description.toLowerCase().includes(searchValue);
|
|
122
|
+
|
|
123
|
+
return valueMatch || descriptionMatch;
|
|
124
|
+
})
|
|
112
125
|
);
|
|
113
126
|
};
|
|
114
127
|
|
|
115
128
|
const getFilteredTokens = (tokensArr, searchVal) =>
|
|
116
129
|
tokensArr.filter(([tokenName, tokenData]) => isSearchMatch(searchVal, tokenName, tokenData));
|
|
117
130
|
|
|
118
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Build reverse reference map: which tokens use each base/palette token
|
|
133
|
+
* @returns {Object} - { tokenName: [{ category, tokenName }, ...] }
|
|
134
|
+
*/
|
|
135
|
+
const buildReferenceMap = (mergedTokens) => {
|
|
136
|
+
const referenceMap = {};
|
|
137
|
+
|
|
138
|
+
const searchTokens = (tokens, categoryName) => {
|
|
139
|
+
Object.entries(tokens).forEach(([key, value]) => {
|
|
140
|
+
if (!value || typeof value !== 'object') {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if this is a token with theme data
|
|
145
|
+
if (isTokenWithThemeData(value)) {
|
|
146
|
+
// This is a token - check its references
|
|
147
|
+
Object.values(value).forEach(themeData => {
|
|
148
|
+
if (themeData?.references) {
|
|
149
|
+
themeData.references.forEach(ref => {
|
|
150
|
+
if (ref.name) {
|
|
151
|
+
if (!referenceMap[ref.name]) {
|
|
152
|
+
referenceMap[ref.name] = [];
|
|
153
|
+
}
|
|
154
|
+
// Use just the token name (key), not the full path
|
|
155
|
+
// Avoid duplicates
|
|
156
|
+
if (!referenceMap[ref.name].some(r => r.category === categoryName && r.tokenName === key)) {
|
|
157
|
+
referenceMap[ref.name].push({
|
|
158
|
+
category: categoryName,
|
|
159
|
+
tokenName: key
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
// Recurse into nested structure
|
|
168
|
+
searchTokens(value, categoryName);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Search semantic, chart, and base categories for references
|
|
174
|
+
['semantic', 'chart', 'base'].forEach(category => {
|
|
175
|
+
if (mergedTokens[category]) {
|
|
176
|
+
searchTokens(mergedTokens[category], category);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return referenceMap;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Search across all categories and return results grouped by category
|
|
185
|
+
* @returns {Object} - { categoryName: [...tokens], ... } with only categories that have matches
|
|
186
|
+
*/
|
|
187
|
+
const getFilteredTokensByCategory = (mergedTokens, allCategoriesArr, searchVal) => {
|
|
188
|
+
if (!searchVal || searchVal.trim() === '') {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resultsByCategory = {};
|
|
193
|
+
|
|
194
|
+
allCategoriesArr.forEach((category) => {
|
|
195
|
+
const categoryTokens = mergedTokens[category];
|
|
196
|
+
if (!categoryTokens) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const categoryTokensArr = getCategoryTokensArr(category, categoryTokens);
|
|
201
|
+
const matches = getFilteredTokens(categoryTokensArr, searchVal);
|
|
202
|
+
|
|
203
|
+
if (matches.length > 0) {
|
|
204
|
+
resultsByCategory[category] = matches;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return resultsByCategory;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getIsColor = (value) => {
|
|
212
|
+
// Extract the actual value if it's nested in an object
|
|
213
|
+
const actualValue = (value && typeof value === 'object' && value.default) ? value.default : value;
|
|
214
|
+
return /^(#|rgb)/.test(actualValue);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if a value object represents a token with theme data (has references or value properties)
|
|
219
|
+
*/
|
|
220
|
+
const isTokenWithThemeData = (value) =>
|
|
221
|
+
value && typeof value === 'object' &&
|
|
222
|
+
Object.values(value).some(v => v && typeof v === 'object' && (v.references || v.value));
|
|
223
|
+
|
|
119
224
|
|
|
120
225
|
const getCategoryTokensArr = (selectedCategory, categoryTokens) => {
|
|
226
|
+
if (!categoryTokens || typeof categoryTokens !== 'object') {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
121
230
|
// Create array of all tokens/nested tokens in selectedCategory
|
|
122
231
|
let categoryTokensArr = [];
|
|
123
|
-
|
|
232
|
+
const NESTED_CATEGORIES = ['base', 'semantic'];
|
|
233
|
+
|
|
234
|
+
if (!NESTED_CATEGORIES.includes(selectedCategory)) {
|
|
124
235
|
categoryTokensArr = Object.entries(categoryTokens);
|
|
125
|
-
} else
|
|
236
|
+
} else {
|
|
126
237
|
// base/semantic combine nested subcategory tokens into flattened arr
|
|
127
|
-
for (
|
|
128
|
-
|
|
238
|
+
for (const subCategory in categoryTokens) {
|
|
239
|
+
if (Object.hasOwn(categoryTokens, subCategory)) {
|
|
240
|
+
const subCategoryTokens = categoryTokens[subCategory];
|
|
241
|
+
if (subCategoryTokens && typeof subCategoryTokens === 'object') {
|
|
242
|
+
categoryTokensArr.push(...Object.entries(subCategoryTokens));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
129
245
|
}
|
|
130
246
|
}
|
|
131
247
|
return categoryTokensArr;
|
|
132
248
|
};
|
|
133
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Theme select order and "All themes" value rows: Default family (Light: DC, HC, Gl; then Dark: …),
|
|
252
|
+
* then Unified family the same. Matches ThemeDisplayLabel column semantics.
|
|
253
|
+
*/
|
|
254
|
+
const THEME_OPTION_ORDER = [
|
|
255
|
+
'default',
|
|
256
|
+
'highcontrast',
|
|
257
|
+
'glass',
|
|
258
|
+
'dark',
|
|
259
|
+
'highcontrast-dark',
|
|
260
|
+
'glass-dark',
|
|
261
|
+
'redhat',
|
|
262
|
+
'redhat-highcontrast',
|
|
263
|
+
'redhat-glass',
|
|
264
|
+
'redhat-dark',
|
|
265
|
+
'redhat-highcontrast-dark',
|
|
266
|
+
'redhat-glass-dark'
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const THEME_OPTION_ORDER_INDEX = Object.fromEntries(THEME_OPTION_ORDER.map((name, index) => [name, index]));
|
|
270
|
+
|
|
271
|
+
const compareThemeNames = (a, b) => {
|
|
272
|
+
const ai = THEME_OPTION_ORDER_INDEX[a];
|
|
273
|
+
const bi = THEME_OPTION_ORDER_INDEX[b];
|
|
274
|
+
const aKnown = ai !== undefined;
|
|
275
|
+
const bKnown = bi !== undefined;
|
|
276
|
+
if (!aKnown && !bKnown) {
|
|
277
|
+
return a.localeCompare(b);
|
|
278
|
+
}
|
|
279
|
+
if (!aKnown) {
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
if (!bKnown) {
|
|
283
|
+
return -1;
|
|
284
|
+
}
|
|
285
|
+
return ai - bi;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const getThemeEntriesForDisplay = (tokenData, selectedTheme, exhibitsThemeVariantValues, allAvailableThemes) => {
|
|
289
|
+
if (!tokenData || typeof tokenData !== 'object') {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const normalizeThemeValue = (themeValue) => {
|
|
294
|
+
if (!themeValue) {
|
|
295
|
+
return themeValue;
|
|
296
|
+
}
|
|
297
|
+
// If it has a default property, use that (for some token structures)
|
|
298
|
+
if (Object.hasOwn(themeValue, 'default')) {
|
|
299
|
+
return themeValue.default;
|
|
300
|
+
}
|
|
301
|
+
// If it's an object with a value property, extract the value
|
|
302
|
+
if (typeof themeValue === 'object' && Object.hasOwn(themeValue, 'value')) {
|
|
303
|
+
return themeValue;
|
|
304
|
+
}
|
|
305
|
+
// Otherwise return as-is
|
|
306
|
+
return themeValue;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (selectedTheme === 'all') {
|
|
310
|
+
if (!exhibitsThemeVariantValues) {
|
|
311
|
+
// For palette tokens, show only one value since they don't vary by theme
|
|
312
|
+
const firstTheme = Object.keys(tokenData)[0];
|
|
313
|
+
if (!firstTheme) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
return [[firstTheme, normalizeThemeValue(tokenData[firstTheme])]];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Group themes by their value (don't sort first - group all matching values together)
|
|
320
|
+
const valueGroups = new Map();
|
|
321
|
+
const themesWithoutValue = [];
|
|
322
|
+
|
|
323
|
+
// Check ALL available themes, not just the ones in tokenData
|
|
324
|
+
const themesToCheck = allAvailableThemes || Object.keys(tokenData);
|
|
325
|
+
|
|
326
|
+
themesToCheck.forEach((themeName) => {
|
|
327
|
+
const themeValue = tokenData[themeName];
|
|
328
|
+
const normalizedValue = normalizeThemeValue(themeValue);
|
|
329
|
+
|
|
330
|
+
// Track themes that don't have a value
|
|
331
|
+
if (!normalizedValue || normalizedValue.value === undefined) {
|
|
332
|
+
themesWithoutValue.push(themeName);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create a key from the value for grouping - sort references to ensure consistent key
|
|
337
|
+
const valueKey = JSON.stringify({
|
|
338
|
+
value: normalizedValue?.value,
|
|
339
|
+
references: normalizedValue?.references?.map(r => r.name).sort()
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (!valueGroups.has(valueKey)) {
|
|
343
|
+
valueGroups.set(valueKey, {
|
|
344
|
+
themeNames: [],
|
|
345
|
+
themeValue: normalizedValue
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
valueGroups.get(valueKey).themeNames.push(themeName);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Build result with theme names sorted within each group
|
|
352
|
+
const result = Array.from(valueGroups.values()).map(({ themeNames, themeValue }) => [
|
|
353
|
+
themeNames.sort(compareThemeNames),
|
|
354
|
+
themeValue
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
// Add group for themes without values if any exist
|
|
358
|
+
if (themesWithoutValue.length > 0) {
|
|
359
|
+
result.push([themesWithoutValue.sort(compareThemeNames), null]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const themeValue = tokenData[selectedTheme];
|
|
366
|
+
|
|
367
|
+
// If theme doesn't exist for this token, return null to indicate no value
|
|
368
|
+
if (!themeValue) {
|
|
369
|
+
return [[selectedTheme, null]];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const tokenValue = normalizeThemeValue(themeValue) || {
|
|
373
|
+
value: '',
|
|
374
|
+
description: tokenData[Object.keys(tokenData)[0]]?.description || '',
|
|
375
|
+
references: []
|
|
376
|
+
};
|
|
377
|
+
return [[selectedTheme, tokenValue]];
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const getTokenChain = (themeTokenData) => {
|
|
381
|
+
if (!themeTokenData || typeof themeTokenData !== 'object') {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const tokenChain = [];
|
|
386
|
+
const MAX_CHAIN_DEPTH = 50; // Prevent infinite loops
|
|
387
|
+
let depth = 0;
|
|
388
|
+
|
|
389
|
+
if (!themeTokenData.references?.[0]) {
|
|
390
|
+
const value = themeTokenData.value;
|
|
391
|
+
if (value !== undefined && value !== null) {
|
|
392
|
+
tokenChain.push(value);
|
|
393
|
+
}
|
|
394
|
+
return tokenChain;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let referenceToken = themeTokenData.references[0];
|
|
398
|
+
while (referenceToken && depth < MAX_CHAIN_DEPTH) {
|
|
399
|
+
depth++;
|
|
400
|
+
|
|
401
|
+
if (referenceToken.name) {
|
|
402
|
+
tokenChain.push(referenceToken.name);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (referenceToken.references?.[0]) {
|
|
406
|
+
referenceToken = referenceToken.references[0];
|
|
407
|
+
} else {
|
|
408
|
+
if (referenceToken.value !== undefined && referenceToken.value !== null) {
|
|
409
|
+
tokenChain.push(referenceToken.value);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return tokenChain;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const findTokenCategory = (tokenName, mergedTokens) => {
|
|
419
|
+
// Search through categories to find which one contains this token
|
|
420
|
+
for (const category of ['semantic', 'chart', 'base', 'palette']) {
|
|
421
|
+
if (!mergedTokens[category]) continue;
|
|
422
|
+
|
|
423
|
+
const searchInCategory = (tokens) => {
|
|
424
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
425
|
+
if (key === tokenName) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
// Check if it's a nested structure
|
|
429
|
+
if (value && typeof value === 'object') {
|
|
430
|
+
if (!isTokenWithThemeData(value) && searchInCategory(value)) {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (searchInCategory(mergedTokens[category])) {
|
|
439
|
+
return category;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const showTokenChain = (themeTokenData, hasReferences, onNavigate, mergedTokens) => {
|
|
446
|
+
const tokenChain = hasReferences ? getTokenChain(themeTokenData) : [themeTokenData.value];
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div>
|
|
450
|
+
{tokenChain.map((nextValue, index) => {
|
|
451
|
+
// Check if this value is a token name (starts with pf-t--)
|
|
452
|
+
const isTokenReference = typeof nextValue === 'string' && nextValue.startsWith('pf-t--');
|
|
453
|
+
const category = isTokenReference && mergedTokens ? findTokenCategory(nextValue, mergedTokens) : null;
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<div
|
|
457
|
+
key={`${index}`}
|
|
458
|
+
className="ws-token-chain-item"
|
|
459
|
+
style={{
|
|
460
|
+
padding: `4px 0 4px calc(${c_expandable_section_m_display_lg_PaddingInlineStart.value} * ${index})`
|
|
461
|
+
}}
|
|
462
|
+
>
|
|
463
|
+
<LevelUpAltIcon style={{ transform: 'rotate(90deg)' }} />
|
|
464
|
+
<span
|
|
465
|
+
className="ws-token-chain-value"
|
|
466
|
+
style={{ paddingInlineStart: c_expandable_section_m_display_lg_PaddingInlineStart.value }}
|
|
467
|
+
>
|
|
468
|
+
{isTokenReference && category && onNavigate ? (
|
|
469
|
+
<Button
|
|
470
|
+
variant="link"
|
|
471
|
+
isInline
|
|
472
|
+
onClick={() => onNavigate(category, nextValue)}
|
|
473
|
+
className="ws-token-chain-button"
|
|
474
|
+
>
|
|
475
|
+
{capitalize(category)}: {toCssCustomPropName(nextValue)}
|
|
476
|
+
</Button>
|
|
477
|
+
) : (
|
|
478
|
+
nextValue
|
|
479
|
+
)}
|
|
480
|
+
</span>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
})}
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
};
|
|
487
|
+
|
|
134
488
|
{
|
|
135
489
|
/* Components */
|
|
136
490
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<ExpandableRowContent>
|
|
142
|
-
<Grid hasGutter>
|
|
143
|
-
{tokenThemesArr.map(([themeName, themeToken]) => (
|
|
144
|
-
<React.Fragment key={themeName}>
|
|
145
|
-
<GridItem span={2}>{capitalize(themeName)}:</GridItem>
|
|
146
|
-
<GridItem span={10}>{showTokenChain(themeToken)}</GridItem>
|
|
147
|
-
</React.Fragment>
|
|
148
|
-
))}
|
|
149
|
-
</Grid>
|
|
150
|
-
</ExpandableRowContent>
|
|
151
|
-
</Td>
|
|
152
|
-
</Tr>
|
|
153
|
-
);
|
|
491
|
+
const TokenDerivationPopoverBody = ({ themeName, themeToken, showThemeLabel, exhibitsThemeVariantValues, onNavigate, mergedTokens }) => {
|
|
492
|
+
const hasReferences = Boolean(themeToken?.references?.[0]);
|
|
493
|
+
const themeNames = Array.isArray(themeName) ? themeName : [themeName];
|
|
494
|
+
const isGrouped = themeNames.length > 1;
|
|
154
495
|
|
|
155
|
-
const TokenValue = ({ themeName, themeToken, tokenName }) => {
|
|
156
|
-
const isColor = getIsColor(themeToken.value);
|
|
157
496
|
return (
|
|
497
|
+
<ThemeLabelAbbrevContext.Provider value={false}>
|
|
498
|
+
<div className="ws-token-derivation-popover">
|
|
499
|
+
{exhibitsThemeVariantValues && showThemeLabel && (
|
|
500
|
+
<div className="ws-token-derivation-popover__theme">
|
|
501
|
+
{isGrouped ? (
|
|
502
|
+
<div>
|
|
503
|
+
<strong>{themeNames.length} themes:</strong>
|
|
504
|
+
<div style={{ marginBlockStart: 'var(--pf-t--global--spacer--xs)' }}>
|
|
505
|
+
{themeNames.map((name) => (
|
|
506
|
+
<div key={name}>
|
|
507
|
+
<ThemeDisplayLabel themeName={name} />
|
|
508
|
+
</div>
|
|
509
|
+
))}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
) : (
|
|
513
|
+
<ThemeDisplayLabel themeName={themeNames[0]} />
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
{showTokenChain(themeToken, hasReferences, onNavigate, mergedTokens)}
|
|
518
|
+
</div>
|
|
519
|
+
</ThemeLabelAbbrevContext.Provider>
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const TokenValue = ({
|
|
524
|
+
themeName,
|
|
525
|
+
themeToken,
|
|
526
|
+
tokenName,
|
|
527
|
+
showThemeLabel,
|
|
528
|
+
showDerivationPopover,
|
|
529
|
+
exhibitsThemeVariantValues,
|
|
530
|
+
selectedTheme,
|
|
531
|
+
onNavigate,
|
|
532
|
+
mergedTokens
|
|
533
|
+
}) => {
|
|
534
|
+
// Handle null themeToken (theme doesn't exist for this token)
|
|
535
|
+
if (themeToken === null) {
|
|
536
|
+
const themeNames = Array.isArray(themeName) ? themeName : [themeName];
|
|
537
|
+
const themeKey = themeNames.join('-');
|
|
538
|
+
const isGrouped = themeNames.length > 1;
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<div className="ws-token-value-line" key={`${themeKey}-${tokenName}`}>
|
|
542
|
+
<ThemeLabelAbbrevContext.Provider value={selectedTheme === 'all'}>
|
|
543
|
+
<Flex
|
|
544
|
+
className="ws-token-value-line-inner"
|
|
545
|
+
direction={{ default: 'row' }}
|
|
546
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
547
|
+
flexWrap={{ default: 'nowrap' }}
|
|
548
|
+
spaceItems={{ default: 'spaceItemsSm' }}
|
|
549
|
+
>
|
|
550
|
+
{showThemeLabel && (
|
|
551
|
+
<FlexItem className="ws-theme-label-inline">
|
|
552
|
+
{isGrouped ? (
|
|
553
|
+
<Tooltip
|
|
554
|
+
content={
|
|
555
|
+
<ThemeLabelAbbrevContext.Provider value={false}>
|
|
556
|
+
<div>
|
|
557
|
+
{themeNames.map((name) => (
|
|
558
|
+
<div key={name}>
|
|
559
|
+
<ThemeDisplayLabel themeName={name} />
|
|
560
|
+
</div>
|
|
561
|
+
))}
|
|
562
|
+
</div>
|
|
563
|
+
</ThemeLabelAbbrevContext.Provider>
|
|
564
|
+
}
|
|
565
|
+
position="top"
|
|
566
|
+
maxWidth="600px"
|
|
567
|
+
>
|
|
568
|
+
<Button variant="plain" className="ws-theme-group-label" tabIndex={-1}>
|
|
569
|
+
{themeNames.length} themes
|
|
570
|
+
</Button>
|
|
571
|
+
</Tooltip>
|
|
572
|
+
) : (
|
|
573
|
+
<ThemeDisplayLabel themeName={themeNames[0]} />
|
|
574
|
+
)}
|
|
575
|
+
</FlexItem>
|
|
576
|
+
)}
|
|
577
|
+
<FlexItem
|
|
578
|
+
className={
|
|
579
|
+
showThemeLabel ? 'ws-token-value-main ws-token-value-main--separated' : 'ws-token-value-main'
|
|
580
|
+
}
|
|
581
|
+
>
|
|
582
|
+
<span style={{ color: 'var(--pf-t--global--text--color--subtle)' }}>—</span>
|
|
583
|
+
</FlexItem>
|
|
584
|
+
</Flex>
|
|
585
|
+
</ThemeLabelAbbrevContext.Provider>
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!themeToken || typeof themeToken !== 'object') {
|
|
591
|
+
if (process.env.NODE_ENV === 'development') {
|
|
592
|
+
console.warn(`TokenValue: Invalid themeToken for ${tokenName}`);
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Extract the actual value from token.value if it's nested in an object
|
|
598
|
+
let displayValue = themeToken.value;
|
|
599
|
+
|
|
600
|
+
if (displayValue && typeof displayValue === 'object' && !Array.isArray(displayValue)) {
|
|
601
|
+
// If value is an object, try to extract a string value
|
|
602
|
+
const values = Object.values(displayValue);
|
|
603
|
+
if (values.length > 0 && typeof values[0] === 'string') {
|
|
604
|
+
displayValue = values[0];
|
|
605
|
+
} else {
|
|
606
|
+
// Cannot extract a valid string value
|
|
607
|
+
if (process.env.NODE_ENV === 'development') {
|
|
608
|
+
console.warn(`Unable to extract displayValue for token ${tokenName}:`, displayValue);
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Ensure displayValue is a primitive type suitable for rendering
|
|
615
|
+
if (typeof displayValue === 'object' || displayValue === undefined || displayValue === null) {
|
|
616
|
+
if (process.env.NODE_ENV === 'development') {
|
|
617
|
+
console.warn(`DisplayValue is invalid for ${tokenName}:`, typeof displayValue);
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Convert to string if needed
|
|
623
|
+
displayValue = String(displayValue);
|
|
624
|
+
|
|
625
|
+
const isColor = getIsColor(themeToken.value);
|
|
626
|
+
const valueMain = isColor ? (
|
|
158
627
|
<Flex
|
|
159
|
-
|
|
628
|
+
direction={{ default: 'row' }}
|
|
629
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
160
630
|
flexWrap={{ default: 'nowrap' }}
|
|
161
|
-
|
|
631
|
+
spaceItems={{ default: 'spaceItemsSm' }}
|
|
162
632
|
>
|
|
163
|
-
<FlexItem>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
633
|
+
<FlexItem className="ws-token-value-hex-wrap">
|
|
634
|
+
<span className="ws-token-value-hex">{displayValue}</span>
|
|
635
|
+
</FlexItem>
|
|
636
|
+
<FlexItem className="ws-token-swatch-wrap">
|
|
637
|
+
<span className="ws-token-swatch" style={{ backgroundColor: displayValue }} />
|
|
638
|
+
</FlexItem>
|
|
639
|
+
</Flex>
|
|
640
|
+
) : (
|
|
641
|
+
<span className="ws-token-value-plain-inline">{displayValue}</span>
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const valueFlexItem = (
|
|
645
|
+
<FlexItem
|
|
646
|
+
className={
|
|
647
|
+
showThemeLabel ? 'ws-token-value-main ws-token-value-main--separated' : 'ws-token-value-main'
|
|
648
|
+
}
|
|
649
|
+
>
|
|
650
|
+
{showDerivationPopover ? (
|
|
651
|
+
<Popover
|
|
652
|
+
aria-label={`How ${toCssCustomPropName(tokenName)} is derived for this theme`}
|
|
653
|
+
headerContent={toCssCustomPropName(tokenName)}
|
|
654
|
+
bodyContent={
|
|
655
|
+
<TokenDerivationPopoverBody
|
|
656
|
+
themeName={themeName}
|
|
657
|
+
themeToken={themeToken}
|
|
658
|
+
showThemeLabel={showThemeLabel}
|
|
659
|
+
exhibitsThemeVariantValues={exhibitsThemeVariantValues}
|
|
660
|
+
onNavigate={onNavigate}
|
|
661
|
+
mergedTokens={mergedTokens}
|
|
662
|
+
/>
|
|
663
|
+
}
|
|
664
|
+
position="bottom"
|
|
665
|
+
minWidth="400px"
|
|
666
|
+
>
|
|
667
|
+
<Button
|
|
668
|
+
variant="plain"
|
|
669
|
+
className="ws-token-value-popover-trigger"
|
|
670
|
+
aria-label={`${displayValue}. Show how ${toCssCustomPropName(tokenName)} is derived`}
|
|
671
|
+
aria-haspopup="dialog"
|
|
672
|
+
>
|
|
673
|
+
{valueMain}
|
|
674
|
+
</Button>
|
|
675
|
+
</Popover>
|
|
168
676
|
) : (
|
|
169
|
-
<
|
|
677
|
+
<span tabIndex={0}>{valueMain}</span>
|
|
170
678
|
)}
|
|
171
|
-
</
|
|
679
|
+
</FlexItem>
|
|
172
680
|
);
|
|
173
|
-
};
|
|
174
681
|
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
682
|
+
// Handle themeName being either a string or array of strings (when grouped by value)
|
|
683
|
+
const themeNames = Array.isArray(themeName) ? themeName : [themeName];
|
|
684
|
+
const themeKey = themeNames.join('-');
|
|
685
|
+
const isGrouped = themeNames.length > 1;
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<div className="ws-token-value-line" key={`${themeKey}-${tokenName}`}>
|
|
689
|
+
<ThemeLabelAbbrevContext.Provider value={selectedTheme === 'all'}>
|
|
690
|
+
<Flex
|
|
691
|
+
className="ws-token-value-line-inner"
|
|
692
|
+
direction={{ default: 'row' }}
|
|
693
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
694
|
+
flexWrap={{ default: 'nowrap' }}
|
|
695
|
+
spaceItems={{ default: 'spaceItemsSm' }}
|
|
696
|
+
>
|
|
697
|
+
{showThemeLabel && (
|
|
698
|
+
<FlexItem className="ws-theme-label-inline">
|
|
699
|
+
{isGrouped ? (
|
|
700
|
+
// Show count for grouped themes with tooltip showing full names
|
|
701
|
+
<Tooltip
|
|
702
|
+
content={
|
|
703
|
+
<ThemeLabelAbbrevContext.Provider value={false}>
|
|
704
|
+
<div>
|
|
705
|
+
{themeNames.map((name) => (
|
|
706
|
+
<div key={name}>
|
|
707
|
+
<ThemeDisplayLabel themeName={name} />
|
|
708
|
+
</div>
|
|
709
|
+
))}
|
|
710
|
+
</div>
|
|
711
|
+
</ThemeLabelAbbrevContext.Provider>
|
|
712
|
+
}
|
|
713
|
+
position="top"
|
|
714
|
+
minWidth="400px"
|
|
715
|
+
maxWidth="600px"
|
|
716
|
+
>
|
|
717
|
+
<Button variant="plain" className="ws-theme-group-label" tabIndex={-1}>
|
|
718
|
+
{themeNames.length} themes
|
|
719
|
+
</Button>
|
|
720
|
+
</Tooltip>
|
|
721
|
+
) : (
|
|
722
|
+
// ThemeDisplayLabel already handles tooltips when abbreviated
|
|
723
|
+
<ThemeDisplayLabel themeName={themeNames[0]} />
|
|
724
|
+
)}
|
|
725
|
+
</FlexItem>
|
|
726
|
+
)}
|
|
727
|
+
{valueFlexItem}
|
|
728
|
+
</Flex>
|
|
729
|
+
</ThemeLabelAbbrevContext.Provider>
|
|
730
|
+
</div>
|
|
180
731
|
);
|
|
181
|
-
|
|
182
|
-
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const OtherCategoryResults = ({ otherResults, onCategoryClick }) => {
|
|
735
|
+
if (Object.keys(otherResults).length === 0) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
183
738
|
|
|
184
739
|
return (
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
740
|
+
<div style={{
|
|
741
|
+
marginBlockStart: 'var(--pf-t--global--spacer--md)',
|
|
742
|
+
marginBlockEnd: 'var(--pf-t--global--spacer--md)',
|
|
743
|
+
paddingBlock: 'var(--pf-t--global--spacer--sm)',
|
|
744
|
+
paddingInline: 'var(--pf-t--global--spacer--sm)',
|
|
745
|
+
backgroundColor: 'var(--pf-t--global--background--color--secondary--default)',
|
|
746
|
+
borderRadius: 'var(--pf-t--global--border--radius--small)',
|
|
747
|
+
border: 'var(--pf-t--global--border--width--regular) solid var(--pf-t--global--border--color--default)'
|
|
748
|
+
}}>
|
|
749
|
+
<strong>Also found in: </strong>
|
|
750
|
+
{Object.entries(otherResults).map(([category, tokens], index) => (
|
|
751
|
+
<React.Fragment key={category}>
|
|
752
|
+
{index > 0 && ', '}
|
|
753
|
+
<Button
|
|
754
|
+
variant="link"
|
|
755
|
+
isInline
|
|
756
|
+
onClick={() => onCategoryClick(category)}
|
|
757
|
+
>
|
|
758
|
+
{capitalize(category)} ({tokens.length})
|
|
759
|
+
</Button>
|
|
760
|
+
</React.Fragment>
|
|
761
|
+
))}
|
|
762
|
+
</div>
|
|
763
|
+
);
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const UsedByCell = ({ tokenName, referenceMap, onNavigate }) => {
|
|
767
|
+
const references = referenceMap[tokenName] || [];
|
|
768
|
+
|
|
769
|
+
if (references.length === 0) {
|
|
770
|
+
return <Td>—</Td>;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (references.length <= 3) {
|
|
774
|
+
return (
|
|
775
|
+
<Td>
|
|
776
|
+
<Flex direction={{ default: 'column' }} rowGap={{ default: 'gapSm' }}>
|
|
777
|
+
{references.map((ref, index) => (
|
|
778
|
+
<FlexItem key={index}>
|
|
779
|
+
<Button
|
|
780
|
+
variant="link"
|
|
781
|
+
isInline
|
|
782
|
+
onClick={() => onNavigate(ref.category, ref.tokenName)}
|
|
783
|
+
>
|
|
784
|
+
{capitalize(ref.category)}: {toCssCustomPropName(ref.tokenName)}
|
|
785
|
+
</Button>
|
|
786
|
+
</FlexItem>
|
|
205
787
|
))}
|
|
206
|
-
</
|
|
788
|
+
</Flex>
|
|
789
|
+
</Td>
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 4+ references: show count with popover
|
|
794
|
+
return (
|
|
795
|
+
<Td>
|
|
796
|
+
<Popover
|
|
797
|
+
headerContent={`Used by ${references.length} tokens`}
|
|
798
|
+
bodyContent={
|
|
799
|
+
<Flex direction={{ default: 'column' }} rowGap={{ default: 'gapSm' }}>
|
|
800
|
+
{references.map((ref, index) => (
|
|
801
|
+
<FlexItem key={index}>
|
|
802
|
+
<Button
|
|
803
|
+
variant="link"
|
|
804
|
+
isInline
|
|
805
|
+
onClick={() => onNavigate(ref.category, ref.tokenName)}
|
|
806
|
+
>
|
|
807
|
+
{capitalize(ref.category)}: {toCssCustomPropName(ref.tokenName)}
|
|
808
|
+
</Button>
|
|
809
|
+
</FlexItem>
|
|
810
|
+
))}
|
|
811
|
+
</Flex>
|
|
812
|
+
}
|
|
813
|
+
position="left"
|
|
814
|
+
minWidth="400px"
|
|
815
|
+
>
|
|
816
|
+
<Button variant="link" isInline aria-haspopup="dialog">
|
|
817
|
+
{references.length} tokens
|
|
818
|
+
</Button>
|
|
819
|
+
</Popover>
|
|
820
|
+
</Td>
|
|
821
|
+
);
|
|
822
|
+
};
|
|
207
823
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
824
|
+
const TokensTableBody = ({
|
|
825
|
+
token,
|
|
826
|
+
isSemanticLayer,
|
|
827
|
+
isChartLayer,
|
|
828
|
+
isBaseLayer,
|
|
829
|
+
isPaletteLayer,
|
|
830
|
+
exhibitsThemeVariantValues,
|
|
831
|
+
selectedTheme,
|
|
832
|
+
referenceMap,
|
|
833
|
+
onNavigate,
|
|
834
|
+
mergedTokens,
|
|
835
|
+
allAvailableThemes
|
|
836
|
+
}) => {
|
|
837
|
+
const [tokenName, tokenData] = token;
|
|
838
|
+
const tokenThemesArr = getThemeEntriesForDisplay(tokenData, selectedTheme, exhibitsThemeVariantValues, allAvailableThemes);
|
|
839
|
+
const tokenDescription = tokenThemesArr[0]?.[1]?.description || '';
|
|
840
|
+
const showUsedBy = isBaseLayer || isPaletteLayer;
|
|
211
841
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
842
|
+
return (
|
|
843
|
+
<Tr>
|
|
844
|
+
<Td dataLabel="Name">
|
|
845
|
+
<code tabIndex={0}>{toCssCustomPropName(tokenName)}</code>
|
|
846
|
+
</Td>
|
|
847
|
+
<Td className="tokens-table-value-cell">
|
|
848
|
+
<Flex className="tokens-table-value-stack" direction={{ default: 'column' }} rowGap={{ default: 'gapMd' }}>
|
|
849
|
+
{tokenThemesArr.map(([themeName, themeToken], index) => {
|
|
850
|
+
// themeName can be a string or array of strings (when grouped by value)
|
|
851
|
+
const key = Array.isArray(themeName) ? themeName.join('-') : themeName;
|
|
852
|
+
return (
|
|
853
|
+
<FlexItem key={key}>
|
|
854
|
+
<TokenValue
|
|
855
|
+
themeName={themeName}
|
|
856
|
+
themeToken={themeToken}
|
|
857
|
+
tokenName={tokenName}
|
|
858
|
+
showThemeLabel={exhibitsThemeVariantValues && selectedTheme === 'all'}
|
|
859
|
+
exhibitsThemeVariantValues={exhibitsThemeVariantValues}
|
|
860
|
+
selectedTheme={selectedTheme}
|
|
861
|
+
showDerivationPopover={
|
|
862
|
+
(isSemanticLayer || isChartLayer || isBaseLayer) &&
|
|
863
|
+
(Boolean(themeToken?.references?.[0]) || getIsColor(themeToken?.value))
|
|
864
|
+
}
|
|
865
|
+
onNavigate={onNavigate}
|
|
866
|
+
mergedTokens={mergedTokens}
|
|
867
|
+
/>
|
|
868
|
+
</FlexItem>
|
|
869
|
+
);
|
|
870
|
+
})}
|
|
871
|
+
</Flex>
|
|
872
|
+
</Td>
|
|
873
|
+
{isSemanticLayer && <Td>{tokenDescription}</Td>}
|
|
874
|
+
{showUsedBy && <UsedByCell tokenName={tokenName} referenceMap={referenceMap} onNavigate={onNavigate} />}
|
|
875
|
+
</Tr>
|
|
215
876
|
);
|
|
216
877
|
};
|
|
217
878
|
|
|
218
879
|
export const TokensTable = ({ tokenJson }) => {
|
|
880
|
+
// Initialize state from URL params if present
|
|
881
|
+
const getInitialStateFromURL = () => {
|
|
882
|
+
if (typeof window === 'undefined') {
|
|
883
|
+
return { category: 'semantic', search: '' };
|
|
884
|
+
}
|
|
885
|
+
const params = new URLSearchParams(window.location.search);
|
|
886
|
+
return {
|
|
887
|
+
category: params.get('category') || 'semantic',
|
|
888
|
+
search: params.get('token') || ''
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const initialState = React.useMemo(getInitialStateFromURL, []);
|
|
893
|
+
|
|
894
|
+
// Track if we're currently handling browser navigation to avoid pushState loops
|
|
895
|
+
const isNavigatingRef = React.useRef(false);
|
|
896
|
+
|
|
219
897
|
// state variables
|
|
220
|
-
const [searchValue, setSearchValue] = React.useState(
|
|
221
|
-
const [
|
|
222
|
-
|
|
898
|
+
const [searchValue, setSearchValue] = React.useState(initialState.search);
|
|
899
|
+
const [selectedCategory, setSelectedCategory] = React.useState(initialState.category);
|
|
900
|
+
// All categories share the same theme selection except palette (which is always 'all')
|
|
901
|
+
const [sharedTheme, setSharedTheme] = React.useState('default');
|
|
902
|
+
const [page, setPage] = React.useState(1);
|
|
903
|
+
const [perPage, setPerPage] = React.useState(20);
|
|
904
|
+
|
|
905
|
+
// Palette always uses 'all', other categories use shared theme
|
|
906
|
+
const selectedTheme = selectedCategory === 'palette' ? 'all' : sharedTheme;
|
|
907
|
+
|
|
908
|
+
// Memoize sorted theme names to avoid creating new arrays on every render
|
|
909
|
+
const sortedThemeNames = React.useMemo(
|
|
910
|
+
() => Object.keys(tokenJson).sort(compareThemeNames),
|
|
911
|
+
[tokenJson]
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// Per-theme JSON module references are stable across MDX re-renders
|
|
915
|
+
const mergeDepList = React.useMemo(
|
|
916
|
+
() => sortedThemeNames.map((themeName) => tokenJson[themeName]),
|
|
917
|
+
[sortedThemeNames, tokenJson]
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const mergedTokens = React.useMemo(() => getTokensFromJson(tokenJson), mergeDepList);
|
|
921
|
+
|
|
922
|
+
const themeOptions = React.useMemo(() => ['all', ...sortedThemeNames], [sortedThemeNames]);
|
|
923
|
+
|
|
924
|
+
// List of all available themes (excluding 'all')
|
|
925
|
+
const allAvailableThemes = React.useMemo(() => sortedThemeNames, [sortedThemeNames]);
|
|
926
|
+
|
|
927
|
+
// Build reference map for "Used by" column
|
|
928
|
+
const referenceMap = React.useMemo(() => buildReferenceMap(mergedTokens), [mergedTokens]);
|
|
929
|
+
|
|
930
|
+
const deferredSearchValue = React.useDeferredValue(searchValue);
|
|
931
|
+
|
|
932
|
+
const tokenCategoryOrder = ['chart', 'semantic', 'base', 'palette'];
|
|
933
|
+
const allCategoryKeys = React.useMemo(
|
|
934
|
+
() => Object.keys(mergedTokens).filter((c) => c !== 'default'),
|
|
935
|
+
[mergedTokens]
|
|
936
|
+
);
|
|
937
|
+
const allCategoriesArr = React.useMemo(
|
|
938
|
+
() => [
|
|
939
|
+
...tokenCategoryOrder.filter((c) => allCategoryKeys.includes(c)),
|
|
940
|
+
...allCategoryKeys.filter((c) => !tokenCategoryOrder.includes(c)).sort()
|
|
941
|
+
],
|
|
942
|
+
[allCategoryKeys]
|
|
943
|
+
);
|
|
223
944
|
|
|
224
|
-
const allTokens = getTokensFromJson(tokenJson);
|
|
225
945
|
const isSemanticLayer = selectedCategory === 'semantic';
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
//
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
946
|
+
const isChartLayer = selectedCategory === 'chart';
|
|
947
|
+
const isBaseLayer = selectedCategory === 'base';
|
|
948
|
+
const isPaletteLayer = selectedCategory === 'palette';
|
|
949
|
+
// Base tokens can have theme variants (e.g., high contrast differs), only palette is truly invariant
|
|
950
|
+
const exhibitsThemeVariantValues = selectedCategory === 'semantic' || selectedCategory === 'chart' || selectedCategory === 'base';
|
|
951
|
+
const showUsedByColumn = isBaseLayer || isPaletteLayer;
|
|
952
|
+
const categoryTokens = mergedTokens[selectedCategory];
|
|
953
|
+
const categoryTokensArr = React.useMemo(
|
|
954
|
+
() => getCategoryTokensArr(selectedCategory, categoryTokens),
|
|
955
|
+
[selectedCategory, categoryTokens]
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
// When searching, search across all categories; otherwise just filter current category
|
|
959
|
+
const hasSearchTerm = deferredSearchValue && deferredSearchValue.trim() !== '';
|
|
960
|
+
const searchResultsByCategory = React.useMemo(
|
|
961
|
+
() => (hasSearchTerm ? getFilteredTokensByCategory(mergedTokens, allCategoriesArr, deferredSearchValue) : {}),
|
|
962
|
+
[mergedTokens, allCategoriesArr, deferredSearchValue, hasSearchTerm]
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Always show current category results
|
|
966
|
+
const searchResults = React.useMemo(
|
|
967
|
+
() => getFilteredTokens(categoryTokensArr, deferredSearchValue),
|
|
968
|
+
[categoryTokensArr, deferredSearchValue]
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Other categories with results (excluding current category)
|
|
972
|
+
const otherCategoryResults = React.useMemo(() => {
|
|
973
|
+
if (!hasSearchTerm) {
|
|
974
|
+
return {};
|
|
975
|
+
}
|
|
976
|
+
const others = { ...searchResultsByCategory };
|
|
977
|
+
delete others[selectedCategory];
|
|
978
|
+
return others;
|
|
979
|
+
}, [hasSearchTerm, searchResultsByCategory, selectedCategory]);
|
|
980
|
+
|
|
981
|
+
// Memoize results count to prevent SearchInput from losing focus
|
|
982
|
+
const resultsCount = React.useMemo(() => searchResults.length.toString(), [searchResults.length]);
|
|
983
|
+
|
|
984
|
+
// Pagination calculations
|
|
985
|
+
const totalItems = searchResults.length;
|
|
986
|
+
const startIndex = (page - 1) * perPage;
|
|
987
|
+
const endIndex = startIndex + perPage;
|
|
988
|
+
const paginatedResults = React.useMemo(
|
|
989
|
+
() => searchResults.slice(startIndex, endIndex),
|
|
990
|
+
[searchResults, startIndex, endIndex]
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
// Reset to page 1 when search or category changes
|
|
994
|
+
React.useEffect(() => {
|
|
995
|
+
setPage(1);
|
|
996
|
+
}, [deferredSearchValue, selectedCategory]);
|
|
997
|
+
|
|
998
|
+
// Sync URL with state (for shareable URLs and browser back/forward)
|
|
999
|
+
React.useEffect(() => {
|
|
1000
|
+
if (typeof window === 'undefined') {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Skip if we're handling browser navigation
|
|
1005
|
+
if (isNavigatingRef.current) {
|
|
1006
|
+
isNavigatingRef.current = false;
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const params = new URLSearchParams(window.location.search);
|
|
1011
|
+
// Normalize params the same way we do in popstate - missing params use defaults
|
|
1012
|
+
const currentCategory = params.get('category') || 'semantic';
|
|
1013
|
+
const currentToken = params.get('token') || '';
|
|
1014
|
+
|
|
1015
|
+
// Only update URL if state actually changed
|
|
1016
|
+
if (currentCategory === selectedCategory && currentToken === searchValue) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const newParams = new URLSearchParams();
|
|
1021
|
+
if (selectedCategory !== 'semantic') {
|
|
1022
|
+
newParams.set('category', selectedCategory);
|
|
1023
|
+
}
|
|
1024
|
+
if (searchValue) {
|
|
1025
|
+
newParams.set('token', searchValue);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const queryString = newParams.toString();
|
|
1029
|
+
const newUrl = queryString
|
|
1030
|
+
? `${window.location.pathname}?${queryString}${window.location.hash}`
|
|
1031
|
+
: `${window.location.pathname}${window.location.hash}`;
|
|
1032
|
+
|
|
1033
|
+
window.history.pushState(null, '', newUrl);
|
|
1034
|
+
}, [selectedCategory, searchValue]);
|
|
1035
|
+
|
|
1036
|
+
// Sync state when URL changes (browser back/forward)
|
|
1037
|
+
React.useEffect(() => {
|
|
1038
|
+
if (typeof window === 'undefined') {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const handlePopState = () => {
|
|
1043
|
+
const params = new URLSearchParams(window.location.search);
|
|
1044
|
+
const category = params.get('category') || 'semantic';
|
|
1045
|
+
const token = params.get('token') || '';
|
|
1046
|
+
|
|
1047
|
+
// Mark that we're handling browser navigation so pushState doesn't run
|
|
1048
|
+
isNavigatingRef.current = true;
|
|
1049
|
+
setSelectedCategory(category);
|
|
1050
|
+
setSearchValue(token);
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
window.addEventListener('popstate', handlePopState);
|
|
1054
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
1055
|
+
}, []);
|
|
1056
|
+
|
|
1057
|
+
const handleSetPage = (_event, newPage) => {
|
|
1058
|
+
setPage(newPage);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const handlePerPageSelect = (_event, newPerPage) => {
|
|
1062
|
+
setPerPage(newPerPage);
|
|
1063
|
+
setPage(1);
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
const handleCategoryChange = React.useCallback((category) => {
|
|
1067
|
+
setSelectedCategory(category);
|
|
1068
|
+
}, []);
|
|
1069
|
+
|
|
1070
|
+
// Memoize setters to prevent TokensToolbar from re-rendering unnecessarily
|
|
1071
|
+
const handleSearchChange = React.useCallback((value) => {
|
|
1072
|
+
setSearchValue(value);
|
|
1073
|
+
}, []);
|
|
1074
|
+
|
|
1075
|
+
const handleThemeChange = React.useCallback((theme) => {
|
|
1076
|
+
// Update shared theme (palette is always 'all' and selector is disabled, so this won't be called for palette)
|
|
1077
|
+
setSharedTheme(theme);
|
|
1078
|
+
}, []);
|
|
1079
|
+
|
|
1080
|
+
// Navigate to category and search for specific token
|
|
1081
|
+
const handleNavigateToToken = React.useCallback((category, tokenName) => {
|
|
1082
|
+
setSelectedCategory(category);
|
|
1083
|
+
setSearchValue(tokenName);
|
|
1084
|
+
setPage(1); // Reset to first page
|
|
1085
|
+
}, []);
|
|
241
1086
|
|
|
242
1087
|
return (
|
|
243
1088
|
<React.Fragment>
|
|
244
1089
|
<TokensToolbar
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
1090
|
+
searchValue={searchValue}
|
|
1091
|
+
setSearchValue={handleSearchChange}
|
|
1092
|
+
selectedCategory={selectedCategory}
|
|
1093
|
+
setSelectedCategory={handleCategoryChange}
|
|
1094
|
+
selectedTheme={selectedTheme}
|
|
1095
|
+
setSelectedTheme={handleThemeChange}
|
|
1096
|
+
themeOptions={themeOptions}
|
|
1097
|
+
resultsCount={resultsCount}
|
|
1098
|
+
categories={allCategoriesArr}
|
|
1099
|
+
/>
|
|
1100
|
+
<OuterScrollContainer className="tokens-table-outer-wrapper">
|
|
253
1101
|
<InnerScrollContainer>
|
|
254
|
-
<Title headingLevel="h2">
|
|
1102
|
+
<Title headingLevel="h2">
|
|
1103
|
+
{capitalize(selectedCategory)} tokens {selectedTheme !== 'all' && (
|
|
1104
|
+
<small className="ws-tokens-title-theme"> ({getThemeDisplayName(selectedTheme).trim()})</small>
|
|
1105
|
+
)}
|
|
1106
|
+
</Title>
|
|
255
1107
|
{searchResults.length > 0 ? (
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1108
|
+
<>
|
|
1109
|
+
<OtherCategoryResults
|
|
1110
|
+
otherResults={otherCategoryResults}
|
|
1111
|
+
onCategoryClick={handleCategoryChange}
|
|
1112
|
+
/>
|
|
1113
|
+
<Pagination
|
|
1114
|
+
itemCount={totalItems}
|
|
1115
|
+
perPage={perPage}
|
|
1116
|
+
page={page}
|
|
1117
|
+
onSetPage={handleSetPage}
|
|
1118
|
+
onPerPageSelect={handlePerPageSelect}
|
|
1119
|
+
perPageOptions={[
|
|
1120
|
+
{ title: '10', value: 10 },
|
|
1121
|
+
{ title: '20', value: 20 },
|
|
1122
|
+
{ title: '50', value: 50 },
|
|
1123
|
+
{ title: '100', value: 100 }
|
|
1124
|
+
]}
|
|
1125
|
+
variant="top"
|
|
1126
|
+
/>
|
|
1127
|
+
<Table
|
|
1128
|
+
variant="compact"
|
|
1129
|
+
className="tokens-table-fixed-layout"
|
|
1130
|
+
style={{ marginBlockEnd: `var(--pf-t--global--spacer--xl)` }}
|
|
1131
|
+
>
|
|
1132
|
+
<Thead>
|
|
1133
|
+
<Tr>
|
|
1134
|
+
<Th width={isSemanticLayer ? 33 : showUsedByColumn ? 30 : isChartLayer ? 40 : 52}>Name</Th>
|
|
1135
|
+
<Th modifier="breakWord" width={isSemanticLayer ? 46 : showUsedByColumn ? 40 : isChartLayer ? 60 : 48}>
|
|
1136
|
+
Value
|
|
1137
|
+
</Th>
|
|
1138
|
+
{isSemanticLayer && <Th width={21}>Description</Th>}
|
|
1139
|
+
{showUsedByColumn && <Th width={30}>Used by</Th>}
|
|
1140
|
+
</Tr>
|
|
1141
|
+
</Thead>
|
|
1142
|
+
|
|
1143
|
+
<Tbody>
|
|
1144
|
+
{paginatedResults.map((token) => (
|
|
1145
|
+
<TokensTableBody
|
|
1146
|
+
key={token[0]}
|
|
1147
|
+
token={token}
|
|
1148
|
+
isSemanticLayer={isSemanticLayer}
|
|
1149
|
+
isChartLayer={isChartLayer}
|
|
1150
|
+
isBaseLayer={isBaseLayer}
|
|
1151
|
+
isPaletteLayer={isPaletteLayer}
|
|
1152
|
+
exhibitsThemeVariantValues={exhibitsThemeVariantValues}
|
|
1153
|
+
selectedTheme={selectedTheme}
|
|
1154
|
+
referenceMap={referenceMap}
|
|
1155
|
+
onNavigate={handleNavigateToToken}
|
|
1156
|
+
mergedTokens={mergedTokens}
|
|
1157
|
+
allAvailableThemes={allAvailableThemes}
|
|
1158
|
+
/>
|
|
1159
|
+
))}
|
|
1160
|
+
</Tbody>
|
|
1161
|
+
</Table>
|
|
1162
|
+
<Pagination
|
|
1163
|
+
itemCount={totalItems}
|
|
1164
|
+
perPage={perPage}
|
|
1165
|
+
page={page}
|
|
1166
|
+
onSetPage={handleSetPage}
|
|
1167
|
+
onPerPageSelect={handlePerPageSelect}
|
|
1168
|
+
perPageOptions={[
|
|
1169
|
+
{ title: '10', value: 10 },
|
|
1170
|
+
{ title: '20', value: 20 },
|
|
1171
|
+
{ title: '50', value: 50 },
|
|
1172
|
+
{ title: '100', value: 100 }
|
|
1173
|
+
]}
|
|
1174
|
+
variant="bottom"
|
|
1175
|
+
/>
|
|
1176
|
+
</>
|
|
279
1177
|
) : (
|
|
280
1178
|
<EmptyState titleText="No results found" headingLevel="h4" icon={SearchIcon}>
|
|
281
1179
|
<EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>
|