-
Notifications
You must be signed in to change notification settings - Fork 51
[WIP-04] [Project Solar / Phase 1 / Showcase] Add support for theming and theme-switching to the showcase #3240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: project-solar/phase-1/HDS-5505_components/modes-css-compilation
Are you sure you want to change the base?
Changes from all commits
30c7358
b010313
6e7c212
e34eec2
5334488
886991c
fe43287
ea7d04f
405e985
cc8f42b
047d063
88fa64d
ef9c402
8b74fa2
6c54ffa
e203f2a
b779edf
d353214
28bd8d3
5c09ae0
d0d9929
fae141d
ba04696
f227d85
b39fc16
7c1b1d9
d60ad02
2ca46d3
c0b0a55
d916092
e68a6d8
b07fd14
96fdc4a
a291556
590d0fc
326f0e0
3132fe2
be76fe3
b6ad465
8c6da52
25c5e29
4d45460
c3a9268
7800278
85beb6f
502312e
fd95fc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| {{! | ||
| Copyright (c) HashiCorp, Inc. | ||
| SPDX-License-Identifier: MPL-2.0 | ||
| }} | ||
|
|
||
| {{! | ||
| ------------------------------------------------------------------------------------------ | ||
| IMPORTANT: this is a temporary implementation, while we wait for the design specifications | ||
| ------------------------------------------------------------------------------------------ | ||
| }} | ||
|
|
||
| <Hds::Dropdown | ||
| @enableCollisionDetection={{true}} | ||
| @matchToggleWidth={{@toggleIsFullWidth}} | ||
| class="hds-theme-switcher-control" | ||
| ...attributes | ||
| as |D| | ||
| > | ||
| <D.ToggleButton | ||
| @color="secondary" | ||
| @size={{this.toggleSize}} | ||
| @isFullWidth={{this.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.onSelectTheme data.theme)}}>{{data.label}}</D.Interactive> | ||
| {{/each-in}} | ||
| </Hds::Dropdown> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /** | ||
| * Copyright (c) HashiCorp, Inc. | ||
| * SPDX-License-Identifier: MPL-2.0 | ||
| */ | ||
|
|
||
| // ------------------------------------------------------------------------------------------ | ||
| // IMPORTANT: this is a temporary implementation, while we wait for the design specifications | ||
| // ------------------------------------------------------------------------------------------ | ||
|
|
||
| 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, | ||
| OnSetThemeCallback, | ||
| } from '../../../services/hds-theming.ts'; | ||
|
|
||
| interface ThemeOption { | ||
| theme: HdsThemes | undefined; | ||
| icon: HdsIconSignature['Args']['name']; | ||
| label: string; | ||
| } | ||
|
|
||
| const OPTIONS: Record<HdsThemes, ThemeOption> = { | ||
| system: { theme: 'system', icon: 'monitor', label: 'System' }, | ||
| light: { theme: 'light', icon: 'sun', label: 'Light' }, | ||
| dark: { theme: 'dark', icon: 'moon', label: 'Dark' }, | ||
| }; | ||
|
|
||
| interface HdsThemeSwitcherSignature { | ||
| Args: { | ||
| toggleSize?: HdsDropdownToggleButtonSignature['Args']['size']; | ||
| toggleIsFullWidth?: HdsDropdownToggleButtonSignature['Args']['isFullWidth']; | ||
| hasSystemOption?: boolean; | ||
| onSetTheme?: OnSetThemeCallback; | ||
| }; | ||
| Element: HdsDropdownSignature['Element']; | ||
| } | ||
|
|
||
| export default class HdsThemeSwitcher extends Component<HdsThemeSwitcherSignature> { | ||
| @service declare readonly hdsTheming: HdsThemingService; | ||
|
|
||
| get toggleSize() { | ||
| return this.args.toggleSize ?? 'small'; | ||
| } | ||
|
|
||
| get toggleIsFullWidth() { | ||
| return this.args.toggleIsFullWidth ?? false; | ||
| } | ||
|
|
||
| get toggleContent() { | ||
| if ( | ||
| (this.currentTheme === 'system' && this.hasSystemOption) || | ||
| this.currentTheme === 'light' || | ||
| this.currentTheme === 'dark' | ||
| ) { | ||
| return { | ||
| label: OPTIONS[this.currentTheme].label, | ||
| icon: OPTIONS[this.currentTheme].icon, | ||
| }; | ||
| } else { | ||
| return { label: 'Theme', icon: undefined }; | ||
| } | ||
| } | ||
|
|
||
| get hasSystemOption() { | ||
| return this.args.hasSystemOption ?? true; | ||
| } | ||
|
|
||
| get _options() { | ||
| const options: Partial<typeof OPTIONS> = { ...OPTIONS }; | ||
|
|
||
| if (!this.hasSystemOption) { | ||
| delete options.system; | ||
| } | ||
|
|
||
| return options; | ||
| } | ||
|
|
||
| get currentTheme() { | ||
| // we get the theme from the global service | ||
| return this.hdsTheming.currentTheme; | ||
| } | ||
|
|
||
| @action | ||
| onSelectTheme(theme: HdsThemes | undefined): void { | ||
| // we set the theme in the global service (and provide an optional user-defined callback) | ||
| this.hdsTheming.setTheme({ theme, onSetTheme: this.args.onSetTheme }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,3 +4,5 @@ | |
| */ | ||
|
|
||
| // This file is used to expose public services | ||
|
|
||
| export * from './services/hds-theming.ts'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| import Service from '@ember/service'; | ||
| import { tracked } from '@glimmer/tracking'; | ||
|
|
||
| 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` | ||
didoo marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If consumers don't do anything, the "Hds" theme is the one they should get so that would be a "default". Is that what you mean? |
||
| } | ||
|
|
||
| enum HdsModesLightValues { | ||
| CdsG0 = 'cds-g0', | ||
| CdsG10 = 'cds-g10', | ||
| } | ||
|
|
||
| enum HdsModesDarkValues { | ||
| CdsG90 = 'cds-g90', | ||
| CdsG100 = 'cds-g100', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't really understand the purpose of having multiple Light & Dark value options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll explain in today's sync |
||
| } | ||
|
|
||
| export enum HdsCssSelectorsValues { | ||
| Data = 'data', | ||
| Class = 'class', | ||
| } | ||
|
|
||
| export type HdsThemes = `${HdsThemeValues}`; | ||
| export type HdsModes = | ||
| | `${HdsModesBaseValues}` | ||
| | `${HdsModesLightValues}` | ||
| | `${HdsModesDarkValues}`; | ||
| export type HdsModesLight = `${HdsModesLightValues}`; | ||
| export type HdsModesDark = `${HdsModesDarkValues}`; | ||
| export type HdsCssSelectors = `${HdsCssSelectorsValues}`; | ||
|
|
||
| type HdsThemingOptions = { | ||
| lightTheme: HdsModesLight; | ||
| darkTheme: HdsModesDark; | ||
| cssSelector: HdsCssSelectors; | ||
| }; | ||
|
|
||
| type SetThemeArgs = { | ||
| theme: HdsThemes | undefined; | ||
| options?: HdsThemingOptions; | ||
| onSetTheme?: OnSetThemeCallback; | ||
| }; | ||
|
|
||
| export type OnSetThemeCallbackArgs = { | ||
| currentTheme: HdsThemes | undefined; | ||
| currentMode: HdsModes | undefined; | ||
| }; | ||
|
|
||
| export type OnSetThemeCallback = (args: OnSetThemeCallbackArgs) => void; | ||
|
|
||
| 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 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_DATA = 'hds-theming-data'; | ||
|
|
||
| export const DEFAULT_THEMING_OPTION_LIGHT_THEME = HdsModesLightValues.CdsG0; | ||
| export const DEFAULT_THEMING_OPTION_DARK_THEME = HdsModesDarkValues.CdsG100; | ||
| export const DEFAULT_THEMING_OPTION_CSS_SELECTOR = 'data'; | ||
|
|
||
| export default class HdsThemingService extends Service { | ||
| @tracked _isInitialized: boolean = false; | ||
| @tracked _currentTheme: HdsThemes | undefined = undefined; | ||
| @tracked _currentMode: HdsModes | undefined = undefined; | ||
| @tracked _currentLightTheme: HdsModesLight = | ||
| DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| @tracked _currentDarkTheme: HdsModesDark = DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| @tracked _currentCssSelector: HdsCssSelectors = | ||
| DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| @tracked globalOnSetTheme: OnSetThemeCallback | undefined; | ||
|
|
||
| initializeTheme() { | ||
| if (this._isInitialized) { | ||
| return; | ||
| } | ||
|
|
||
| const rawStoredThemingData = localStorage.getItem( | ||
| HDS_THEMING_LOCALSTORAGE_DATA | ||
| ); | ||
| if (rawStoredThemingData !== null) { | ||
| const storedThemingData: unknown = JSON.parse(rawStoredThemingData); | ||
| if (storedThemingData) { | ||
| const { theme, options } = storedThemingData as { | ||
| theme: HdsThemes | undefined; | ||
| options: HdsThemingOptions; | ||
| }; | ||
| this.setTheme({ | ||
| theme, | ||
| options, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| this._isInitialized = true; | ||
| } | ||
|
|
||
| setTheme({ theme, options, onSetTheme }: SetThemeArgs) { | ||
| if (options !== undefined) { | ||
| // if we have new options, we override the current ones (`lightTheme` / `darkTheme` / `cssSelector`) | ||
| // these options can be used by consumers that want to customize how they apply theming | ||
| // (and used by the showcase for the custom theming / theme switching logic) | ||
| if ( | ||
| Object.hasOwn(options, 'lightTheme') && | ||
| Object.hasOwn(options, 'darkTheme') && | ||
| Object.hasOwn(options, 'cssSelector') | ||
| ) { | ||
| const { lightTheme, darkTheme, cssSelector } = options; | ||
|
|
||
| this._currentLightTheme = lightTheme; | ||
| this._currentDarkTheme = darkTheme; | ||
| this._currentCssSelector = cssSelector; | ||
| } else { | ||
| // fallback if something goes wrong | ||
| this._currentLightTheme = DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| this._currentDarkTheme = DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| this._currentCssSelector = DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| } | ||
| } | ||
|
|
||
| // set the current theme/mode (`currentTheme` / `currentMode`) | ||
| if ( | ||
| theme === undefined || // standard (no theming) | ||
| !THEMES.includes(theme) // handle possible errors | ||
| ) { | ||
| this._currentTheme = undefined; | ||
| this._currentMode = undefined; | ||
| } else if ( | ||
| theme === HdsThemeValues.System // system (prefers-color-scheme) | ||
| ) { | ||
| this._currentTheme = HdsThemeValues.System; | ||
| this._currentMode = undefined; | ||
| } else { | ||
| this._currentTheme = theme; | ||
| if (this._currentTheme === HdsThemeValues.Light) { | ||
| this._currentMode = this._currentLightTheme; | ||
| } | ||
| if (this._currentTheme === HdsThemeValues.Dark) { | ||
| this._currentMode = this._currentDarkTheme; | ||
| } | ||
| } | ||
|
|
||
| // 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; | ||
| } | ||
| // 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._currentCssSelector === 'data') { | ||
| rootElement.setAttribute(HDS_THEMING_DATA_SELECTOR, this._currentMode); | ||
| } else if (this._currentCssSelector === 'class') { | ||
| rootElement.classList.add( | ||
| `${HDS_THEMING_CLASS_SELECTOR_PREFIX}-${this._currentMode}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // store the current theme and theming options in local storage (unless undefined) | ||
| localStorage.setItem( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] There could be use cases where a consumer wants to handle on their own how they keep track of a user's theme. They could want to handle it through their own cookies or storage or some other means. It would be useful to have an argument like A consumer could then use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because of my point above I think it would be useful to keep the callback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the current implementation, what they could do would be to use the The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This was the main use case I was thinking to keep it for. It's good to have some way for consumers to listen for theme changes and |
||
| HDS_THEMING_LOCALSTORAGE_DATA, | ||
| JSON.stringify({ | ||
| theme: this._currentTheme, | ||
| options: { | ||
| lightTheme: this._currentLightTheme, | ||
| darkTheme: this._currentDarkTheme, | ||
| cssSelector: this._currentCssSelector, | ||
| }, | ||
| }) | ||
| ); | ||
|
|
||
| // this is a general callback that can be defined globally (by extending the service) | ||
| if (this.globalOnSetTheme) { | ||
| this.globalOnSetTheme({ | ||
| currentTheme: this._currentTheme, | ||
| currentMode: this._currentMode, | ||
| }); | ||
| } | ||
|
|
||
| // this is a "local" callback that can be defined "locally" (eg. in a theme switcher) | ||
| if (onSetTheme) { | ||
| onSetTheme({ | ||
| currentTheme: this._currentTheme, | ||
| currentMode: this._currentMode, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // getters used for reactivity in the components/services using this service | ||
|
|
||
| get currentTheme(): HdsThemes | undefined { | ||
| return this._currentTheme; | ||
| } | ||
|
|
||
| get currentMode(): HdsModes | undefined { | ||
| return this._currentMode; | ||
| } | ||
|
|
||
| get currentLightTheme(): HdsModesLight { | ||
| return this._currentLightTheme ?? DEFAULT_THEMING_OPTION_LIGHT_THEME; | ||
| } | ||
|
|
||
| get currentDarkTheme(): HdsModesDark { | ||
| return this._currentDarkTheme ?? DEFAULT_THEMING_OPTION_DARK_THEME; | ||
| } | ||
|
|
||
| get currentCssSelector(): HdsCssSelectors { | ||
| return this._currentCssSelector ?? DEFAULT_THEMING_OPTION_CSS_SELECTOR; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.