Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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