From bf5c47ea6562790ce132170511db88f31480a1bf Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 22 May 2026 12:44:47 +0200 Subject: [PATCH 1/8] build: add one theme style dictionary --- build-tools/utils/themes.js | 2 +- style-dictionary/one-theme/colors.ts | 14 +++++++++++ style-dictionary/one-theme/index.ts | 33 ++++++++++++++++++++++++++ style-dictionary/one-theme/metadata.ts | 5 ++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 style-dictionary/one-theme/colors.ts create mode 100644 style-dictionary/one-theme/index.ts create mode 100644 style-dictionary/one-theme/metadata.ts diff --git a/build-tools/utils/themes.js b/build-tools/utils/themes.js index 5066477bbf..49d2387fa6 100644 --- a/build-tools/utils/themes.js +++ b/build-tools/utils/themes.js @@ -13,7 +13,7 @@ const themes = [ designTokensPackageJson: { name: '@cloudscape-design/design-tokens' }, outputPath: path.join(workspace.targetPath, 'components'), primaryThemePath: './classic/index.js', - secondaryThemePaths: ['./visual-refresh-secondary/index.js'], + secondaryThemePaths: ['./visual-refresh-secondary/index.js', './one-theme/index.js'], }, ]; diff --git a/style-dictionary/one-theme/colors.ts b/style-dictionary/one-theme/colors.ts new file mode 100644 index 0000000000..5db5671793 --- /dev/null +++ b/style-dictionary/one-theme/colors.ts @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expandColorDictionary } from '../utils/index.js'; +import { StyleDictionary } from '../utils/interfaces.js'; + +// Anything not listed here falls back to the visual-refresh value via ThemeBuilder.addTokens in ./index.ts. +const tokens: StyleDictionary.ColorsDictionary = { + colorBackgroundButtonPrimaryDefault: '{colorBlack}', +}; + +const expandedTokens: StyleDictionary.ExpandedColorScopeDictionary = expandColorDictionary(tokens); + +export { expandedTokens as tokens }; +export const mode: StyleDictionary.ModeIdentifier = 'color'; diff --git a/style-dictionary/one-theme/index.ts b/style-dictionary/one-theme/index.ts new file mode 100644 index 0000000000..71a56acf2e --- /dev/null +++ b/style-dictionary/one-theme/index.ts @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ThemeBuilder } from '@cloudscape-design/theming-build'; + +import { StyleDictionary } from '../utils/interfaces.js'; +import { createColorMode, createDensityMode, createMotionMode } from '../utils/modes.js'; +import { buildVisualRefresh } from '../visual-refresh/index.js'; + +const modes = [ + createColorMode('.awsui-dark-mode'), + createDensityMode('.awsui-compact-mode'), + createMotionMode('.awsui-motion-disabled'), +]; + +// One Theme starts from the full visual-refresh token set and layers overrides on top. +const overrides: Array = [await import('./colors.js')]; + +const builder = new ThemeBuilder('one-theme', '.awsui-one-theme', modes); + +// Layer 1: visual refresh baseline (full token set + contexts). +await buildVisualRefresh(builder); + +// Layer 2: One Theme overrides. +overrides.forEach(({ tokens, mode: modeId, referenceTokens }) => { + const mode = modes.find(m => m.id === modeId); + if (referenceTokens) { + builder.addReferenceTokens(referenceTokens, mode); + } + builder.addTokens(tokens, mode); +}); + +const theme = builder.build(); +export default theme; diff --git a/style-dictionary/one-theme/metadata.ts b/style-dictionary/one-theme/metadata.ts new file mode 100644 index 0000000000..41b3dceead --- /dev/null +++ b/style-dictionary/one-theme/metadata.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import metadata from '../visual-refresh/metadata/index.js'; + +export default metadata; From a061df28fb2f29873d887f6723a52d5807d39455 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 22 May 2026 13:16:36 +0200 Subject: [PATCH 2/8] chore: bump bundle size limit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fda995324a..5ff0ecaa0e 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "1285 kB", + "limit": "1400 kB", "ignore": "react-dom" } ], From da98aa69f8a5893410a3af1681b4f52ca8d71bfa Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 22 May 2026 15:16:07 +0200 Subject: [PATCH 3/8] feat: gate one theme behind env var --- build-tools/tasks/generate-environment.js | 2 ++ build-tools/utils/themes.js | 8 +++++++- package.json | 2 +- pages/app/app-context.tsx | 2 ++ pages/app/components/theme-switcher.tsx | 18 ++++++++++++++++-- pages/app/index.tsx | 10 +++++++++- 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/build-tools/tasks/generate-environment.js b/build-tools/tasks/generate-environment.js index 5bb2e80569..a7f2ab5b95 100644 --- a/build-tools/tasks/generate-environment.js +++ b/build-tools/tasks/generate-environment.js @@ -6,6 +6,7 @@ const themes = require('../utils/themes'); const workspace = require('../utils/workspace'); const ALWAYS_VISUAL_REFRESH = process.env.ALWAYS_VISUAL_REFRESH === 'true'; +const INCLUDE_ONE_THEME = process.env.NODE_ENV !== 'production'; function writeEnvironmentFile(theme) { const filepath = 'internal/environment'; @@ -16,6 +17,7 @@ function writeEnvironmentFile(theme) { THEME: theme.name, SYSTEM: 'core', ALWAYS_VISUAL_REFRESH: !!theme.alwaysVisualRefresh || ALWAYS_VISUAL_REFRESH, + INCLUDE_ONE_THEME: INCLUDE_ONE_THEME, }; const basePath = path.join(theme.outputPath, filepath); diff --git a/build-tools/utils/themes.js b/build-tools/utils/themes.js index 49d2387fa6..e44427ba78 100644 --- a/build-tools/utils/themes.js +++ b/build-tools/utils/themes.js @@ -3,6 +3,9 @@ const path = require('path'); const workspace = require('./workspace'); +// One Theme is gated on NODE_ENV so it does not ship in published packages yet. +const INCLUDE_ONE_THEME = process.env.NODE_ENV !== 'production'; + const themes = [ // This is the default Cloudscape theme, which is best used with Visual Refresh enabled (by default) { @@ -13,7 +16,10 @@ const themes = [ designTokensPackageJson: { name: '@cloudscape-design/design-tokens' }, outputPath: path.join(workspace.targetPath, 'components'), primaryThemePath: './classic/index.js', - secondaryThemePaths: ['./visual-refresh-secondary/index.js', './one-theme/index.js'], + secondaryThemePaths: [ + './visual-refresh-secondary/index.js', + ...(INCLUDE_ONE_THEME ? ['./one-theme/index.js'] : []), + ], }, ]; diff --git a/package.json b/package.json index 5ff0ecaa0e..fda995324a 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "1400 kB", + "limit": "1285 kB", "ignore": "react-dom" } ], diff --git a/pages/app/app-context.tsx b/pages/app/app-context.tsx index b0b2d48bd8..4807fdf5fe 100644 --- a/pages/app/app-context.tsx +++ b/pages/app/app-context.tsx @@ -12,6 +12,7 @@ interface AppUrlParams { density: Density; direction: 'ltr' | 'rtl'; visualRefresh: boolean; + oneTheme: boolean; motionDisabled: boolean; appLayoutWidget: boolean; mode?: Mode; @@ -32,6 +33,7 @@ const appContextDefaults: AppContextType = { density: Density.Comfortable, direction: 'ltr', visualRefresh: THEME === 'default', + oneTheme: false, motionDisabled: false, appLayoutWidget: false, }, diff --git a/pages/app/components/theme-switcher.tsx b/pages/app/components/theme-switcher.tsx index d6afde3416..86c0a4d65c 100644 --- a/pages/app/components/theme-switcher.tsx +++ b/pages/app/components/theme-switcher.tsx @@ -4,7 +4,7 @@ import React, { useContext } from 'react'; import { Density, Mode } from '@cloudscape-design/global-styles'; -import { ALWAYS_VISUAL_REFRESH } from '~components/internal/environment'; +import { ALWAYS_VISUAL_REFRESH, INCLUDE_ONE_THEME } from '~components/internal/environment'; import SpaceBetween from '~components/space-between'; import AppContext from '../app-context'; @@ -23,7 +23,7 @@ export default function ThemeSwitcher() { } else { vrSwitchProps.checked = urlParams.visualRefresh; vrSwitchProps.onChange = event => { - setUrlParams({ visualRefresh: event.target.checked }); + setUrlParams(event.target.checked ? { visualRefresh: true, oneTheme: false } : { visualRefresh: false }); window.location.reload(); }; } @@ -34,6 +34,20 @@ export default function ThemeSwitcher() { Visual refresh + {INCLUDE_ONE_THEME && ( + + )} diff --git a/pages/app/index.tsx b/pages/app/index.tsx index 02cbb25e59..a1d89e692e 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -100,7 +100,7 @@ const { direction, visualRefresh, oneTheme, appLayoutWidget, appLayoutToolbar, a ); // The VR class needs to be set before any React rendering occurs. -window[awsuiVisualRefreshFlag] = () => visualRefresh; +window[awsuiVisualRefreshFlag] = () => visualRefresh && !oneTheme; if (!window[awsuiGlobalFlagsSymbol]) { window[awsuiGlobalFlagsSymbol] = {}; } @@ -111,13 +111,10 @@ window[awsuiGlobalFlagsSymbol].appLayoutWidget = appLayoutWidget; window[awsuiGlobalFlagsSymbol].appLayoutToolbar = appLayoutToolbar; window[awsuiCustomFlagsSymbol].appLayoutDelayedWidget = appLayoutDelayedWidget; -// Removes .awsui-visual-refresh when one theme is active and vice verca -if (oneTheme) { - document.body.classList.add('awsui-one-theme'); - document.body.classList.remove('awsui-visual-refresh'); -} else { - document.body.classList.remove('awsui-one-theme'); -} +// Visual Refresh and One Theme are mutually exclusive — manage both classes here so they never coexist. +// useRuntimeVisualRefresh() detects .awsui-visual-refresh on body and short-circuits before its Symbol fallback. +document.body.classList.toggle('awsui-one-theme', oneTheme); +document.body.classList.toggle('awsui-visual-refresh', visualRefresh && !oneTheme); // Apply the direction value to the HTML element dir attribute document.documentElement.setAttribute('dir', direction); From 8caf07a3284ab9dac70412d49782c265ca766209 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 22 May 2026 15:49:06 +0200 Subject: [PATCH 7/8] refactor: remove comments --- build-tools/utils/themes.js | 1 - 1 file changed, 1 deletion(-) diff --git a/build-tools/utils/themes.js b/build-tools/utils/themes.js index 78e2e3ce8e..6564bf32d4 100644 --- a/build-tools/utils/themes.js +++ b/build-tools/utils/themes.js @@ -3,7 +3,6 @@ const path = require('path'); const workspace = require('./workspace'); -// One Theme is gated on this env var so it does not ship in published packages. const INCLUDE_ONE_THEME = process.env.INCLUDE_ONE_THEME === 'true'; const themes = [ From 20479ca47611ee9655e3d35285f9c71c1e6a17e3 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Fri, 22 May 2026 17:49:39 +0200 Subject: [PATCH 8/8] chore: remove redundant comment --- pages/app/components/theme-switcher.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/app/components/theme-switcher.tsx b/pages/app/components/theme-switcher.tsx index 1ab4cb55ce..26ade0902b 100644 --- a/pages/app/components/theme-switcher.tsx +++ b/pages/app/components/theme-switcher.tsx @@ -12,7 +12,6 @@ import AppContext from '../app-context'; export default function ThemeSwitcher() { const { mode, urlParams, setUrlParams, setMode } = useContext(AppContext); - // Three mutually-exclusive themes drive both URL params at once. function activateTheme(theme: 'visualRefresh' | 'oneTheme' | 'classic') { setUrlParams({ visualRefresh: theme === 'visualRefresh',