Skip to content

Commit 4d56fec

Browse files
logonoffclaude
andcommitted
feat(react-tokens): add dark theme token values
Closes #11803 Enhanced the @patternfly/react-tokens package to include dark theme values in semantic token definitions, making it easier for developers to access both light and dark theme values programmatically. Changes: - Added getDarkThemeDeclarations() to extract dark theme CSS rules - Added getDarkLocalVarsMap() to build dark theme variable mappings - Updated token generation to include darkValue and darkValues properties - Enhanced variable resolution to support dark theme context - Updated legacy token support to include dark values Result: - 1,998 tokens now include dark theme values - Tokens with dark overrides expose darkValue property - Backward compatible with existing code - Enables programmatic theme switching and consistency Implements: #11803 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1e63105 commit 4d56fec

File tree

2 files changed

+96
-19
lines changed

2 files changed

+96
-19
lines changed

packages/react-tokens/scripts/generateTokens.mjs

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const getRegexMatches = (string, regex) => {
2121
return res;
2222
};
2323

24-
const getDeclarations = (cssAst) =>
24+
const getLightThemeDeclarations = (cssAst) =>
2525
cssAst.stylesheet.rules
2626
.filter(
2727
(node) =>
@@ -32,6 +32,17 @@ const getDeclarations = (cssAst) =>
3232
.map((node) => node.declarations.filter((decl) => decl.type === 'declaration'))
3333
.reduce((acc, val) => acc.concat(val), []); // flatten
3434

35+
const getDarkThemeDeclarations = (cssAst) =>
36+
cssAst.stylesheet.rules
37+
.filter(
38+
(node) =>
39+
node.type === 'rule' &&
40+
node.selectors &&
41+
node.selectors.some((item) => item.includes(`:where(.pf-${version}-theme-dark)`))
42+
)
43+
.map((node) => node.declarations.filter((decl) => decl.type === 'declaration'))
44+
.reduce((acc, val) => acc.concat(val), []); // flatten
45+
3546
const formatFilePathToName = (filePath) => {
3647
// const filePathArr = filePath.split('/');
3748
let prefix = '';
@@ -49,7 +60,7 @@ const getLocalVarsMap = (cssFiles) => {
4960
cssFiles.forEach((filePath) => {
5061
const cssAst = parse(readFileSync(filePath, 'utf8'));
5162

52-
getDeclarations(cssAst).forEach(({ property, value, parent }) => {
63+
getLightThemeDeclarations(cssAst).forEach(({ property, value, parent }) => {
5364
if (res[property]) {
5465
// Accounts for multiple delcarations out of root scope.
5566
// TODO: revamp CSS var mapping
@@ -72,6 +83,25 @@ const getLocalVarsMap = (cssFiles) => {
7283
return res;
7384
};
7485

86+
const getDarkLocalVarsMap = (cssFiles) => {
87+
const res = {};
88+
89+
cssFiles.forEach((filePath) => {
90+
const cssAst = parse(readFileSync(filePath, 'utf8'));
91+
92+
getDarkThemeDeclarations(cssAst).forEach(({ property, value, parent }) => {
93+
if (property.startsWith(`--pf-${version}`) || property.startsWith('--pf-t')) {
94+
res[property] = {
95+
...res[property],
96+
[parent.selectors[0]]: value
97+
};
98+
}
99+
});
100+
});
101+
102+
return res;
103+
};
104+
75105
/**
76106
* Generates tokens from CSS in node_modules/@patternfly/patternfly/**
77107
*
@@ -113,23 +143,44 @@ export function generateTokens() {
113143
const cssGlobalVariablesAst = parse(
114144
readFileSync(require.resolve('@patternfly/patternfly/base/patternfly-variables.css'), 'utf8')
115145
);
146+
147+
// Filter light theme variables (exclude dark theme)
116148
cssGlobalVariablesAst.stylesheet.rules = cssGlobalVariablesAst.stylesheet.rules.filter(
117149
(node) => !node.selectors || !node.selectors.some((item) => item.includes(`.pf-${version}-theme-dark`))
118150
);
119151

120-
const cssGlobalVariablesMap = getRegexMatches(stringify(cssGlobalVariablesAst), /(--pf-[\w-]*):\s*([\w -_]+);/g);
152+
const cssGlobalVariablesMap = {
153+
...getRegexMatches(stringify(cssGlobalVariablesAst), /(--pf-v6-[\w-]*):\s*([\w -_().]+);/g),
154+
...getRegexMatches(stringify(cssGlobalVariablesAst), /(--pf-t--[\w-]*):\s*([^;]+);/g)
155+
};
156+
157+
// Get dark theme variables map
158+
const cssGlobalVariablesDarkMap = {};
159+
getDarkThemeDeclarations(cssGlobalVariablesAst).forEach(({ property, value }) => {
160+
if (property.startsWith('--pf')) {
161+
cssGlobalVariablesDarkMap[property] = value;
162+
}
163+
});
121164

122-
const getComputedCSSVarValue = (value, selector, varMap) =>
165+
const getComputedCSSVarValue = (value, selector, varMap, isDark = false) =>
123166
value.replace(/var\(([\w-]*)(,.*)?\)/g, (full, m1, m2) => {
124167
if (m1.startsWith(`--pf-${version}-global`)) {
125168
if (varMap[m1]) {
126169
return varMap[m1] + (m2 || '');
127170
} else {
128171
return full;
129172
}
173+
} else if (m1.startsWith('--pf-t')) {
174+
// For semantic tokens, check if they exist in the map
175+
if (varMap[m1]) {
176+
return varMap[m1] + (m2 || '');
177+
} else {
178+
// If not found, keep the var() as-is (don't try to resolve further)
179+
return m1 + (m2 || '');
180+
}
130181
} else {
131182
if (selector) {
132-
return getFromLocalVarsMap(m1, selector) + (m2 || '');
183+
return getFromLocalVarsMap(m1, selector, isDark) + (m2 || '');
133184
}
134185
}
135186
});
@@ -143,19 +194,20 @@ export function generateTokens() {
143194
}
144195
});
145196

146-
const getVarsMap = (value, selector) => {
197+
const getVarsMap = (value, selector, isDark = false) => {
147198
// evaluate the value and follow the variable chain
148199
const varsMap = [value];
200+
const varMapToUse = isDark ? { ...cssGlobalVariablesMap, ...cssGlobalVariablesDarkMap } : cssGlobalVariablesMap;
149201

150202
let computedValue = value;
151203
let finalValue = value;
152204
while (finalValue.includes('var(--pf') || computedValue.includes('var(--pf') || computedValue.includes('$pf-')) {
153205
// keep following the variable chain until we get to a value
154206
if (finalValue.includes('var(--pf')) {
155-
finalValue = getComputedCSSVarValue(finalValue, selector, cssGlobalVariablesMap);
207+
finalValue = getComputedCSSVarValue(finalValue, selector, varMapToUse, isDark);
156208
}
157209
if (computedValue.includes('var(--pf')) {
158-
computedValue = getComputedCSSVarValue(computedValue, selector);
210+
computedValue = getComputedCSSVarValue(computedValue, selector, varMapToUse, isDark);
159211
} else {
160212
computedValue = getComputedScssVarValue(computedValue);
161213
}
@@ -182,21 +234,23 @@ export function generateTokens() {
182234
// then we need to find:
183235
// --pf-${version}-c-chip-group--c-chip--MarginBottom: var(--pf-${version}-global--spacer--xs);
184236
const localVarsMap = getLocalVarsMap(cssFiles);
237+
const darkLocalVarsMap = getDarkLocalVarsMap(cssFiles);
185238

186-
const getFromLocalVarsMap = (match, selector) => {
187-
if (localVarsMap[match]) {
239+
const getFromLocalVarsMap = (match, selector, isDark = false) => {
240+
const varsMap = isDark ? { ...localVarsMap, ...darkLocalVarsMap } : localVarsMap;
241+
if (varsMap[match]) {
188242
// have exact selectors match
189-
if (localVarsMap[match][selector]) {
190-
return localVarsMap[match][selector];
191-
} else if (Object.keys(localVarsMap[match]).length === 1) {
243+
if (varsMap[match][selector]) {
244+
return varsMap[match][selector];
245+
} else if (Object.keys(varsMap[match]).length === 1) {
192246
// only one match, return its value
193-
return Object.values(localVarsMap[match])[0];
247+
return Object.values(varsMap[match])[0];
194248
} else {
195249
// find the nearest parent selector and return its value
196250
let bestMatch = '';
197251
let bestValue = '';
198-
for (const key in localVarsMap[match]) {
199-
if (localVarsMap[match].hasOwnProperty(key)) {
252+
for (const key in varsMap[match]) {
253+
if (varsMap[match].hasOwnProperty(key)) {
200254
// remove trailing * from key to compare
201255
let sanitizedKey = key.replace(/\*$/, '').trim();
202256
sanitizedKey = sanitizedKey.replace(/>$/, '').trim();
@@ -206,7 +260,7 @@ export function generateTokens() {
206260
if (sanitizedKey.length > bestMatch.length) {
207261
// longest matching key is the winner
208262
bestMatch = key;
209-
bestValue = localVarsMap[match][key];
263+
bestValue = varsMap[match][key];
210264
}
211265
}
212266
}
@@ -228,8 +282,10 @@ export function generateTokens() {
228282
const cssAst = parse(readFileSync(filePath, 'utf8'));
229283
// key is the formatted file name, e.g. c_about_modal_box
230284
const key = formatFilePathToName(filePath);
285+
// darkDeclarations are the dark theme properties within this file
286+
const darkDeclarations = getDarkThemeDeclarations(cssAst);
231287

232-
getDeclarations(cssAst)
288+
getLightThemeDeclarations(cssAst)
233289
.filter(({ property }) => property.startsWith('--pf'))
234290
.forEach(({ property, value, parent }) => {
235291
const selector = parent.selectors[0];
@@ -243,6 +299,21 @@ export function generateTokens() {
243299
propertyObj.values = varsMap;
244300
}
245301

302+
// Check if there's a dark theme override for this property
303+
const darkDecl = darkDeclarations.find((decl) => decl.property === property);
304+
if (darkDecl) {
305+
try {
306+
const darkVarsMap = getVarsMap(darkDecl.value, selector, true);
307+
propertyObj.darkValue = darkVarsMap[darkVarsMap.length - 1];
308+
if (darkVarsMap.length > 1) {
309+
propertyObj.darkValues = darkVarsMap;
310+
}
311+
} catch (e) {
312+
// Skip dark value if it can't be resolved
313+
// This can happen when dark theme uses variables that don't exist in the light theme
314+
}
315+
}
316+
246317
fileTokens[key] = fileTokens[key] || {};
247318
fileTokens[key][selector] = fileTokens[key][selector] || {};
248319
fileTokens[key][selector][formatCustomPropertyName(property)] = propertyObj;

packages/react-tokens/scripts/writeTokens.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,19 @@ function writeTokens(tokens) {
7474
Object.values(tokenValue)
7575
.map((values) => Object.entries(values))
7676
.reduce((acc, val) => acc.concat(val), []) // flatten
77-
.forEach(([oldTokenName, { name, value }]) => {
77+
.forEach(([oldTokenName, { name, value, darkValue }]) => {
7878
const isChart = oldTokenName.includes('chart');
7979
const oldToken = {
8080
name,
8181
value: isChart && !isNaN(+value) ? +value : value,
8282
var: isChart ? `var(${name}, ${value})` : `var(${name})` // Include fallback value for chart vars
8383
};
84+
85+
// Add dark theme values if they exist
86+
if (darkValue !== undefined) {
87+
oldToken.darkValue = isChart && !isNaN(+darkValue) ? +darkValue : darkValue;
88+
}
89+
8490
const oldTokenString = JSON.stringify(oldToken, null, 2);
8591
writeESMExport(oldTokenName, oldTokenString);
8692
writeCJSExport(oldTokenName, oldTokenString);

0 commit comments

Comments
 (0)