Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions build-tools/tasks/styles.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const { parallel, series } = require('gulp');
const { readFileSync } = require('fs');
const { readFileSync, existsSync } = require('fs');
const { createHash } = require('crypto');
const { join } = require('path');
const { buildThemedComponentsInternal } = require('@cloudscape-design/theming-build/internal');
Expand Down Expand Up @@ -36,6 +36,112 @@ function compileStyleDictionary() {
});
}

/**
* Extracts dark mode CSS rules from the generated styles and creates a separate
* CSS file that applies dark mode styles based on prefers-color-scheme media query.
*
* This allows consumers to opt-in to automatic dark mode without bundling the
* duplicate styles by default.
*/
function generateDarkModePrefersCss(theme) {
return task(`dark-mode-prefers:${theme.name}`, () => {
const baseStylesPath = join(theme.outputPath, 'internal/base-component/styles.scoped.css');

if (!existsSync(baseStylesPath)) {
console.log(` Base component styles not found at ${baseStylesPath}, skipping dark-mode-prefers CSS`);
return Promise.resolve();
}

const cssContent = readFileSync(baseStylesPath, 'utf-8');
const darkModeRules = extractDarkModeRules(cssContent);

if (darkModeRules.length === 0) {
console.log(' No dark mode rules found, skipping dark-mode-prefers CSS');
return Promise.resolve();
}

// Transform .awsui-dark-mode to .awsui-dark-mode-if-preferred
const transformedRules = darkModeRules.map(rule =>
rule.replace(/\.awsui-dark-mode/g, '.awsui-dark-mode-if-preferred')
);

const outputCss = `/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */
/* SPDX-License-Identifier: Apache-2.0 */

/*
* This file provides automatic dark mode support based on the user's system preference.
* Import this file and apply the .awsui-dark-mode-if-preferred class to enable automatic
* dark mode switching based on prefers-color-scheme.
*
* Usage:
* import darkModeStyles from '@cloudscape-design/design-tokens/dark-mode-prefers.css';
* <div className={darkModeStyles.awsuiDarkModeIfPreferred}>...</div>
*
* Or without CSS modules:
* import '@cloudscape-design/design-tokens/dark-mode-prefers.css';
* <div className="awsui-dark-mode-if-preferred">...</div>
*/

@media (prefers-color-scheme: dark) {
${transformedRules.join('\n\n')}
}
`;

const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
writeFile(join(designTokensOutputDir, 'dark-mode-prefers.css'), outputCss);
console.log(` Generated dark-mode-prefers.css (${transformedRules.length} rules)`);

return Promise.resolve();
});
}

/**
* Extracts all .awsui-dark-mode CSS rule blocks from the stylesheet content.
*/
function extractDarkModeRules(cssContent) {
const lines = cssContent.split('\n');
const darkModeRules = [];

let i = 0;
while (i < lines.length) {
const line = lines[i];

// Look for dark mode selectors (not inside @media blocks)
if (line.includes('.awsui-dark-mode') && line.includes('{') && !line.trim().startsWith('@media')) {
// Extract the full rule block
let braceCount = 0;
const ruleLines = [];
let j = i;

while (j < lines.length) {
const currentLine = lines[j];
ruleLines.push(currentLine);

for (const char of currentLine) {
if (char === '{') {
braceCount++;
}
if (char === '}') {
braceCount--;
}
}

if (braceCount === 0 && ruleLines.length > 0) {
break;
}
j++;
}

darkModeRules.push(ruleLines.join('\n'));
i = j + 1;
} else {
i++;
}
}

return darkModeRules;
}

function stylesTask(theme) {
return task(`styles:${theme.name}`, async () => {
const designTokensOutputDir = join(workspace.targetPath, theme.designTokensDir);
Expand Down Expand Up @@ -88,5 +194,5 @@ function stylesTask(theme) {
module.exports = series(
generateEnvironment(),
compileStyleDictionary(),
parallel(themes.map(theme => stylesTask(theme)))
parallel(themes.map(theme => series(stylesTask(theme), generateDarkModePrefersCss(theme))))
);
10 changes: 9 additions & 1 deletion build-tools/utils/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ const themes = [
packageJson: { name: '@cloudscape-design/components' },
designTokensOutput: 'index',
designTokensDir: 'design-tokens',
designTokensPackageJson: { name: '@cloudscape-design/design-tokens' },
designTokensPackageJson: {
name: '@cloudscape-design/design-tokens',
exports: {
'.': './index.js',
'./index.js': './index.js',
'./index.scss': './index.scss',
'./dark-mode-prefers.css': './dark-mode-prefers.css',
},
},
outputPath: path.join(workspace.targetPath, 'components'),
primaryThemePath: './classic/index.js',
secondaryThemePaths: ['./visual-refresh-secondary/index.js'],
Expand Down