Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a71ff07
added `Hds::ThemeSwitcher` component
didoo Sep 30, 2025
aef9405
added `Hds::Theming` service
didoo Sep 30, 2025
3481977
added theming to the Showcase itself (and replaced hardcoded values w…
didoo Sep 30, 2025
1b2d9f8
added `Shw::ThemeSwitcher` component for showcase
didoo Sep 30, 2025
77668b3
updated `Mock::App` and added new yielded sub-components
didoo Sep 30, 2025
f9a00bf
added `Shw:: ThemeSwitcher` to the Showcase page header
didoo Sep 30, 2025
a034fea
added `foundations/theming` showcase page (and a frameless demo)
didoo Sep 30, 2025
2862b85
refactored `hds-theming` service to align with the new themes/modes a…
didoo Oct 1, 2025
06dd075
added `hdsTheming` initialization to main showcase app
didoo Oct 1, 2025
16973ea
removed compilation of components Scss and replaced it with static in…
didoo Oct 3, 2025
b37b334
added theming options via popover - part 1
didoo Oct 3, 2025
5ccd9bb
added theming options via popover - part 2
didoo Oct 3, 2025
64cada9
added theming options via popover - part 3
didoo Oct 3, 2025
78de122
added theming options via popover - part 4
didoo Oct 4, 2025
7e28e82
added theming options via popover - part 5
didoo Oct 6, 2025
112a73b
big code refactoring for the theme selector, to streamline user selec…
didoo Oct 6, 2025
2d530dd
updated logic that sets the theming for the showcase itself (without …
didoo Oct 7, 2025
785cbe9
small fixes here and there for cleanup and linting
didoo Oct 10, 2025
7b3b5b8
fixed issue with `pnpm lint:format` (missing newline at the end of `p…
didoo Oct 10, 2025
5bd74f6
fixed accessibility issue in `advanced-table` page, due to changes to…
didoo Oct 10, 2025
0b4be89
fixed typescript error due to new mock page being added
didoo Oct 10, 2025
fb50663
added fix for tests failing
didoo Oct 13, 2025
954e916
basic implementation of `<ShwCarbonizationComparisonGrid>`
didoo Sep 23, 2025
e3f0463
refactored implementation of `<ShwCarbonizationComparisonGrid>`
didoo Sep 23, 2025
863e0a5
implemented β€œCarbonization” section for `Badge` page
didoo Sep 23, 2025
89378e1
implemented β€œCarbonization” section for `BadgeCount` page
didoo Sep 23, 2025
788b3bd
implemented β€œCarbonization” section for `Button` page
didoo Sep 23, 2025
99eac5a
implemented β€œCarbonization” section for `Focus Ring` page
didoo Sep 24, 2025
d97f2aa
implemented β€œCarbonization” section for `Typography` page
didoo Sep 24, 2025
e7ebfa2
implemented β€œCarbonization” section for (new) `Color` page
didoo Sep 24, 2025
745c41f
added `@carbon/web-components` as devDependency to Showcase app
didoo Sep 24, 2025
bdbfd82
added `@carbon/[themes|layout|grid|type]` + `@ibm/plex` as devDepende…
didoo Sep 25, 2025
4c6616e
refactored `ComparisonGrid` component to add themed background color …
didoo Sep 25, 2025
824f9b2
added temporary support for IBM Plex font-family
didoo Sep 25, 2025
77409b8
refactored code to support theming for reference web components, base…
didoo Sep 25, 2025
6a30e14
small refactoring
didoo Sep 25, 2025
1ac78eb
small refactorings
didoo Sep 25, 2025
0eb81ed
refactored content organization to have the carbonization pages as st…
didoo Sep 25, 2025
a292b3c
added carbon web components for comparison on `BadgeCount` page
didoo Oct 7, 2025
383748c
added carbon web components for comparison on `Button` page
didoo Oct 7, 2025
bba228c
more small fixes for cleanup and linting
didoo Oct 10, 2025
9ade8d9
implemented β€œCarbonization” section for `Form::TextInput` page
didoo Oct 13, 2025
83feb83
implemented β€œCarbonization” section for `SegmentedGroup` page
didoo Oct 14, 2025
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
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@
"./components/hds/text/code.js": "./dist/_app_/components/hds/text/code.js",
"./components/hds/text/display.js": "./dist/_app_/components/hds/text/display.js",
"./components/hds/text.js": "./dist/_app_/components/hds/text.js",
"./components/hds/theme-switcher.js": "./dist/_app_/components/hds/theme-switcher.js",
"./components/hds/time.js": "./dist/_app_/components/hds/time.js",
"./components/hds/time/range.js": "./dist/_app_/components/hds/time/range.js",
"./components/hds/time/single.js": "./dist/_app_/components/hds/time/single.js",
Expand Down Expand Up @@ -394,6 +395,7 @@
"./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js",
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js",
"./services/hds-intl.js": "./dist/_app_/services/hds-intl.js",
"./services/hds-theming.js": "./dist/_app_/services/hds-theming.js",
"./services/hds-time.js": "./dist/_app_/services/hds-time.js"
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ export { default as HdsTextCode } from './components/hds/text/code.ts';
export { default as HdsTextDisplay } from './components/hds/text/display.ts';
export * from './components/hds/text/types.ts';

// Theme Switcher
export { default as HdsThemeSwitcher } from './components/hds/theme-switcher/index.ts';

// Time
export { default as HdsTime } from './components/hds/time/index.ts';
export { default as HdsTimeSingle } from './components/hds/time/single.ts';
Expand Down
23 changes: 23 additions & 0 deletions packages/components/src/components/hds/theme-switcher/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

<Hds::Dropdown
@enableCollisionDetection={{true}}
@matchToggleWidth={{@toggleIsFullWidth}}
class="hds-theme-switcher-control"
...attributes
as |D|
>
<D.ToggleButton
@color="secondary"
@size={{this.toggleSize}}
@isFullWidth={{@toggleIsFullWidth}}
@text={{this.toggleContent.label}}
@icon={{this.toggleContent.icon}}
/>
{{#each-in this._options as |key data|}}
<D.Interactive @icon={{data.icon}} {{on "click" (fn this.setTheme data.theme)}}>{{data.label}}</D.Interactive>
{{/each-in}}
</Hds::Dropdown>
89 changes: 89 additions & 0 deletions packages/components/src/components/hds/theme-switcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

import type { HdsDropdownSignature } from '../dropdown/index.ts';
import type { HdsDropdownToggleButtonSignature } from '../dropdown/toggle/button.ts';
import type { HdsIconSignature } from '../icon/index.ts';
import type HdsThemingService from '../../../services/hds-theming.ts';
import { type HdsThemes } from '../../../services/hds-theming.ts';

type ThemeOptionKey = 'system' | 'light' | 'dark'; // | 'none';

interface ThemeOption {
theme: HdsThemes;
icon: HdsIconSignature['Args']['name'];
label: string;
}

export const OPTIONS: Record<ThemeOptionKey, ThemeOption> = {
system: { theme: 'system', icon: 'monitor', label: 'System' },
light: { theme: 'light', icon: 'sun', label: 'Light' },
dark: { theme: 'dark', icon: 'moon', label: 'Dark' },
// none: { theme: undefined, icon: 'minus', label: 'None' },
};

export interface HdsThemeSwitcherSignature {
Args: {
toggleSize?: HdsDropdownToggleButtonSignature['Args']['size'];
toggleIsFullWidth?: boolean;
hasSystemOption?: boolean;
// hasNoThemeOption?: boolean;
};
Element: HdsDropdownSignature['Element'];
}

export default class HdsThemeSwitcher extends Component<HdsThemeSwitcherSignature> {
@service declare readonly hdsTheming: HdsThemingService;

get _options() {
const options: Partial<typeof OPTIONS> = { ...OPTIONS };
const hasSystemOption = this.args.hasSystemOption ?? true;
// const hasNoThemeOption = this.args.hasNoThemeOption ?? false;

if (!hasSystemOption) {
delete options.system;
}

// if (!hasNoThemeOption) {
// delete options.none;
// }

return options;
}

get toggleSize() {
return this.args.toggleSize ?? 'small';
}

get toggleContent() {
switch (this.currentTheme) {
case 'system':
case 'light':
case 'dark':
return {
label: OPTIONS[this.currentTheme].label,
icon: OPTIONS[this.currentTheme].icon,
};
case undefined:
default:
return { label: 'Theme', icon: undefined };
}
}

get currentTheme() {
// we get the theme from the global service
return this.hdsTheming.currentTheme;
}

@action
setTheme(theme: HdsThemes): void {
// we set the theme in the global service
this.hdsTheming.setTheme(theme);
}
}
2 changes: 2 additions & 0 deletions packages/components/src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
*/

// This file is used to expose public services

export * from './services/hds-theming.ts';
160 changes: 160 additions & 0 deletions packages/components/src/services/hds-theming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';

import type Owner from '@ember/owner';

export enum HdsThemeValues {
// system settings (prefers-color-scheme)
System = 'system',
// user settings for dark/light
Light = 'light',
Dark = 'dark',
}

enum HdsModesBaseValues {
Hds = 'hds', // TODO understand if it should be `default`
}

export enum HdsModesLightValues {
CdsG0 = 'cds-g0',
CdsG10 = 'cds-g10',
}

export enum HdsModesDarkValues {
CdsG90 = 'cds-g90',
CdsG100 = 'cds-g100',
}

export type HdsModeValues =
| HdsModesBaseValues
| HdsModesLightValues
| HdsModesDarkValues;

export enum HdsCssSelectorsValues {
Data = 'data',
Class = 'class',
}

export type HdsThemes = `${HdsThemeValues}` | undefined;
export type HdsModes = `${HdsModeValues}` | undefined;
export type HdsModesLight = `${HdsModesLightValues}`;
export type HdsModesDark = `${HdsModesDarkValues}`;
export type HdsCssSelectors = `${HdsCssSelectorsValues}`;

export const THEMES: HdsThemes[] = Object.values(HdsThemeValues);
export const MODES_LIGHT: HdsModesLight[] = Object.values(HdsModesLightValues);
export const MODES_DARK: HdsModesDark[] = Object.values(HdsModesDarkValues);
export const MODES: HdsModes[] = [
...Object.values(HdsModesBaseValues),
...MODES_LIGHT,
...MODES_DARK,
];

export const CSS_SELECTORS: HdsCssSelectors[] = Object.values(
HdsCssSelectorsValues
);

export const HDS_THEMING_DATA_SELECTOR = 'data-hds-theme';
export const HDS_THEMING_CLASS_SELECTOR_PREFIX = 'hds-theme';
export const HDS_THEMING_CLASS_SELECTORS_LIST = [
...MODES_LIGHT,
...MODES_DARK,
].map((mode) => `${HDS_THEMING_CLASS_SELECTOR_PREFIX}-${mode}`);
export const HDS_THEMING_LOCALSTORAGE_KEY = 'hds-current-theming-preferences';

export type HdsThemingServiceOptions = {
themeMap: {
[HdsThemeValues.Light]: HdsModesLight | undefined;
[HdsThemeValues.Dark]: HdsModesDark | undefined;
};
cssSelector: HdsCssSelectors | undefined;
};

export const DEFAULT_THEMING_OPTIONS: HdsThemingServiceOptions = {
themeMap: {
[HdsThemeValues.Light]: HdsModesLightValues.CdsG0,
[HdsThemeValues.Dark]: HdsModesDarkValues.CdsG100,
},
cssSelector: 'data',
};
export default class HdsThemingService extends Service {
@tracked isInitialized: boolean = false;
@tracked currentTheme: HdsThemes = undefined;
@tracked currentMode: HdsModes = undefined;
@tracked currentThemingServiceOptions: HdsThemingServiceOptions =
DEFAULT_THEMING_OPTIONS;

constructor(owner: Owner) {
super(owner);
console.log('HdsThemingService constructor');
this.initializeTheme();
}

initializeTheme() {
if (this.isInitialized) {
return;
}
console.log('HdsThemingService > initializeTheme');
const storedTheme = localStorage.getItem(
HDS_THEMING_LOCALSTORAGE_KEY
) as HdsThemes;
if (storedTheme) {
this.setTheme(storedTheme);
}
this.isInitialized = true;
}

getTheme(): HdsThemes {
return this.currentTheme;
}

setTheme(theme: HdsThemes) {
console.log('setTheme invoked', `theme=${theme}`);

// IMPORTANT: for this to work, it needs to be the HTML tag (it's the `:root` in CSS)
const rootElement = document.querySelector('html');

if (!rootElement) {
return;
}

// set `currentTheme` and `currentMode`
if (
theme === undefined || // standard (no theming)
theme === HdsThemeValues.System || // system (prefers-color-scheme)
!THEMES.includes(theme) // handle possible errors
) {
this.currentTheme = undefined;
this.currentMode = undefined;
} else {
this.currentTheme = theme;
this.currentMode =
this.currentThemingServiceOptions.themeMap[this.currentTheme];
}

// remove or update the CSS selectors applied to the root element (depending on the `theme` argument)
rootElement.removeAttribute(HDS_THEMING_DATA_SELECTOR);
rootElement.classList.remove(...HDS_THEMING_CLASS_SELECTORS_LIST);
if (this.currentMode !== undefined) {
if (this.currentThemingServiceOptions.cssSelector === 'data') {
rootElement.setAttribute(HDS_THEMING_DATA_SELECTOR, this.currentMode);
} else if (this.currentThemingServiceOptions.cssSelector === 'class') {
rootElement.classList.add(
`${HDS_THEMING_CLASS_SELECTOR_PREFIX}-${this.currentMode}`
);
}
}

// store the current theme in local storage (unless undefined)
if (this.currentTheme) {
localStorage.setItem(HDS_THEMING_LOCALSTORAGE_KEY, this.currentTheme);
} else {
localStorage.removeItem(HDS_THEMING_LOCALSTORAGE_KEY);
}
}

// this is used for the HDS Showcase and for consumers that want to customize how they apply theming
setThemingServiceOptions(customOptions: HdsThemingServiceOptions) {
this.currentThemingServiceOptions = customOptions;
}
}
5 changes: 5 additions & 0 deletions packages/components/src/template-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ import type HdsTagComponent from './components/hds/tag';
import type HdsTooltipButtonComponent from './components/hds/tooltip-button';
import type HdsToastComponent from './components/hds/toast';
import type HdsTextCodeComponent from './components/hds/text/code';
import type HdsThemeSwitcherComponent from './components/hds/theme-switcher';
import type HdsTimeComponent from './components/hds/time';
import type HdsTimeSingleComponent from './components/hds/time/single';
import type HdsTimeRangeComponent from './components/hds/time/range';
Expand Down Expand Up @@ -1021,6 +1022,10 @@ export default interface HdsComponentsRegistry {
'Hds::Toast': typeof HdsToastComponent;
'hds/toast': typeof HdsToastComponent;

// ThemeSwitcher
'Hds::ThemeSwitcher': typeof HdsThemeSwitcherComponent;
'hds/theme-switcher': typeof HdsThemeSwitcherComponent;

// Time
'Hds::Time': typeof HdsTimeComponent;
'hds/time': typeof HdsTimeComponent;
Expand Down
Loading