Skip to content

Commit e20862a

Browse files
committed
feat: Add opt-in dark-mode-prefers.css for prefers-color-scheme support
Generate a separate CSS file that enables automatic dark mode based on the user's system preference (prefers-color-scheme: dark). This file is opt-in to avoid bloating the default bundle. Consumers who need SSR-compatible automatic dark mode can import it: import '@cloudscape-design/design-tokens/dark-mode-prefers.css'; Then apply the class to their root element: <div className="awsui-dark-mode-if-preferred">...</div> The CSS file extracts all .awsui-dark-mode rules from the generated styles and transforms them to use .awsui-dark-mode-if-preferred wrapped in @media (prefers-color-scheme: dark).
1 parent 36ebfc0 commit e20862a

File tree

2 files changed

+117
-3
lines changed

2 files changed

+117
-3
lines changed

build-tools/tasks/styles.js

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
const { parallel, series } = require('gulp');
4-
const { readFileSync } = require('fs');
4+
const { readFileSync, existsSync } = require('fs');
55
const { createHash } = require('crypto');
66
const { join } = require('path');
77
const { buildThemedComponentsInternal } = require('@cloudscape-design/theming-build/internal');
@@ -36,6 +36,112 @@ function compileStyleDictionary() {
3636
});
3737
}
3838

39+
/**
40+
* Extracts dark mode CSS rules from the generated styles and creates a separate
41+
* CSS file that applies dark mode styles based on prefers-color-scheme media query.
42+
*
43+
* This allows consumers to opt-in to automatic dark mode without bundling the
44+
* duplicate styles by default.
45+
*/
46+
function generateDarkModePrefersCss(theme) {
47+
return task(`dark-mode-prefers:${theme.name}`, () => {
48+
const baseStylesPath = join(theme.outputPath, 'internal/base-component/styles.scoped.css');
49+
50+
if (!existsSync(baseStylesPath)) {
51+
console.log(` Base component styles not found at ${baseStylesPath}, skipping dark-mode-prefers CSS`);
52+
return Promise.resolve();
53+
}
54+
55+
const cssContent = readFileSync(baseStylesPath, 'utf-8');
56+
const darkModeRules = extractDarkModeRules(cssContent);
57+
58+
if (darkModeRules.length === 0) {
59+
console.log(' No dark mode rules found, skipping dark-mode-prefers CSS');
60+
return Promise.resolve();
61+
}
62+
63+
// Transform .awsui-dark-mode to .awsui-dark-mode-if-preferred
64+
const transformedRules = darkModeRules.map(rule =>
65+
rule.replace(/\.awsui-dark-mode/g, '.awsui-dark-mode-if-preferred')
66+
);
67+
68+
const outputCss = `/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */
69+
/* SPDX-License-Identifier: Apache-2.0 */
70+
71+
/*
72+
* This file provides automatic dark mode support based on the user's system preference.
73+
* Import this file and apply the .awsui-dark-mode-if-preferred class to enable automatic
74+
* dark mode switching based on prefers-color-scheme.
75+
*
76+
* Usage:
77+
* import darkModeStyles from '@cloudscape-design/design-tokens/dark-mode-prefers.css';
78+
* <div className={darkModeStyles.awsuiDarkModeIfPreferred}>...</div>
79+
*
80+
* Or without CSS modules:
81+
* import '@cloudscape-design/design-tokens/dark-mode-prefers.css';
82+
* <div className="awsui-dark-mode-if-preferred">...</div>
83+
*/
84+
85+
@media (prefers-color-scheme: dark) {
86+
${transformedRules.join('\n\n')}
87+
}
88+
`;
89+
90+
const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
91+
writeFile(join(designTokensOutputDir, 'dark-mode-prefers.css'), outputCss);
92+
console.log(` Generated dark-mode-prefers.css (${transformedRules.length} rules)`);
93+
94+
return Promise.resolve();
95+
});
96+
}
97+
98+
/**
99+
* Extracts all .awsui-dark-mode CSS rule blocks from the stylesheet content.
100+
*/
101+
function extractDarkModeRules(cssContent) {
102+
const lines = cssContent.split('\n');
103+
const darkModeRules = [];
104+
105+
let i = 0;
106+
while (i < lines.length) {
107+
const line = lines[i];
108+
109+
// Look for dark mode selectors (not inside @media blocks)
110+
if (line.includes('.awsui-dark-mode') && line.includes('{') && !line.trim().startsWith('@media')) {
111+
// Extract the full rule block
112+
let braceCount = 0;
113+
const ruleLines = [];
114+
let j = i;
115+
116+
while (j < lines.length) {
117+
const currentLine = lines[j];
118+
ruleLines.push(currentLine);
119+
120+
for (const char of currentLine) {
121+
if (char === '{') {
122+
braceCount++;
123+
}
124+
if (char === '}') {
125+
braceCount--;
126+
}
127+
}
128+
129+
if (braceCount === 0 && ruleLines.length > 0) {
130+
break;
131+
}
132+
j++;
133+
}
134+
135+
darkModeRules.push(ruleLines.join('\n'));
136+
i = j + 1;
137+
} else {
138+
i++;
139+
}
140+
}
141+
142+
return darkModeRules;
143+
}
144+
39145
function stylesTask(theme) {
40146
return task(`styles:${theme.name}`, async () => {
41147
const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
@@ -88,5 +194,5 @@ function stylesTask(theme) {
88194
module.exports = series(
89195
generateEnvironment(),
90196
compileStyleDictionary(),
91-
parallel(themes.map(theme => stylesTask(theme)))
197+
parallel(themes.map(theme => series(stylesTask(theme), generateDarkModePrefersCss(theme))))
92198
);

build-tools/utils/themes.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ const themes = [
1010
packageJson: { name: '@cloudscape-design/components' },
1111
designTokensOutput: 'index',
1212
designTokensDir: 'design-tokens',
13-
designTokensPackageJson: { name: '@cloudscape-design/design-tokens' },
13+
designTokensPackageJson: {
14+
name: '@cloudscape-design/design-tokens',
15+
exports: {
16+
'.': './index.js',
17+
'./index.js': './index.js',
18+
'./index.scss': './index.scss',
19+
'./dark-mode-prefers.css': './dark-mode-prefers.css',
20+
},
21+
},
1422
outputPath: path.join(workspace.targetPath, 'components'),
1523
primaryThemePath: './classic/index.js',
1624
secondaryThemePaths: ['./visual-refresh-secondary/index.js'],

0 commit comments

Comments
 (0)