Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
869539e
added `Hds::ThemeSwitcher` component
didoo Sep 30, 2025
182eada
added `Hds::Theming` service
didoo Sep 30, 2025
d5ae427
added theming to the Showcase itself (and replaced hardcoded values w…
didoo Sep 30, 2025
fc9c4f1
added `Shw::ThemeSwitcher` component for showcase
didoo Sep 30, 2025
73a9df2
updated `Mock::App` and added new yielded sub-components
didoo Sep 30, 2025
6433a29
added `Shw:: ThemeSwitcher` to the Showcase page header
didoo Sep 30, 2025
0169455
added `foundations/theming` showcase page (and a frameless demo)
didoo Sep 30, 2025
3513fe6
refactored `hds-theming` service to align with the new themes/modes a…
didoo Oct 1, 2025
274d755
added `hdsTheming` initialization to main showcase app
didoo Oct 1, 2025
af827b4
removed compilation of components Scss and replaced it with static in…
didoo Oct 3, 2025
7adb4e3
added theming options via popover - part 1
didoo Oct 3, 2025
496aa6a
added theming options via popover - part 2
didoo Oct 3, 2025
62cd1d3
added theming options via popover - part 3
didoo Oct 3, 2025
c63bac5
added theming options via popover - part 4
didoo Oct 4, 2025
1b43974
added theming options via popover - part 5
didoo Oct 6, 2025
eb55281
big code refactoring for the theme selector, to streamline user selec…
didoo Oct 6, 2025
b88e665
updated logic that sets the theming for the showcase itself (without …
didoo Oct 7, 2025
33e2842
small fixes here and there for cleanup and linting
didoo Oct 10, 2025
de6b173
fixed issue with `pnpm lint:format` (missing newline at the end of `p…
didoo Oct 10, 2025
84de83f
fixed accessibility issue in `advanced-table` page, due to changes to…
didoo Oct 10, 2025
df9cec8
fixed typescript error due to new mock page being added
didoo Oct 10, 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
3 changes: 3 additions & 0 deletions showcase/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@
ember-cli-update.json
*.html
*.scss

# temporary CSS files for theming
/public/assets/styles/@hashicorp/
3 changes: 3 additions & 0 deletions showcase/.stylelintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

# compiled output
/dist/

# temporary CSS files for theming
/public/assets/styles/@hashicorp/
Loading
Loading