diff --git a/packages/components/package.json b/packages/components/package.json index eff7de50090..de38d70625b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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", @@ -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" } }, diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts index 400dc061324..adb7400d800 100644 --- a/packages/components/src/components.ts +++ b/packages/components/src/components.ts @@ -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'; diff --git a/packages/components/src/components/hds/theme-switcher/index.hbs b/packages/components/src/components/hds/theme-switcher/index.hbs new file mode 100644 index 00000000000..50e2f699a8d --- /dev/null +++ b/packages/components/src/components/hds/theme-switcher/index.hbs @@ -0,0 +1,23 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + {{#each-in this._options as |key data|}} + {{data.label}} + {{/each-in}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/theme-switcher/index.ts b/packages/components/src/components/hds/theme-switcher/index.ts new file mode 100644 index 00000000000..7040e2d3fb9 --- /dev/null +++ b/packages/components/src/components/hds/theme-switcher/index.ts @@ -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 = { + 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 { + @service declare readonly hdsTheming: HdsThemingService; + + get _options() { + const options: Partial = { ...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); + } +} diff --git a/packages/components/src/services.ts b/packages/components/src/services.ts index 2650e722d57..9f61ec10978 100644 --- a/packages/components/src/services.ts +++ b/packages/components/src/services.ts @@ -4,3 +4,5 @@ */ // This file is used to expose public services + +export * from './services/hds-theming.ts'; diff --git a/packages/components/src/services/hds-theming.ts b/packages/components/src/services/hds-theming.ts new file mode 100644 index 00000000000..748bb173417 --- /dev/null +++ b/packages/components/src/services/hds-theming.ts @@ -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; + } +} diff --git a/packages/components/src/template-registry.ts b/packages/components/src/template-registry.ts index 66183aa31c6..25f4575d028 100644 --- a/packages/components/src/template-registry.ts +++ b/packages/components/src/template-registry.ts @@ -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'; @@ -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; diff --git a/showcase/.prettierignore b/showcase/.prettierignore index 6b95cb7bf32..63e7e2dc3d7 100644 --- a/showcase/.prettierignore +++ b/showcase/.prettierignore @@ -12,3 +12,6 @@ ember-cli-update.json *.html *.scss + +# temporary CSS files for theming +/public/assets/styles/@hashicorp/ diff --git a/showcase/.stylelintignore b/showcase/.stylelintignore index fc178a0b910..6c722d9246d 100644 --- a/showcase/.stylelintignore +++ b/showcase/.stylelintignore @@ -3,3 +3,6 @@ # compiled output /dist/ + +# temporary CSS files for theming +/public/assets/styles/@hashicorp/ diff --git a/showcase/app/components/mock/app/index.gts b/showcase/app/components/mock/app/index.gts index e8ffe853e0b..5e8f983f4d5 100644 --- a/showcase/app/components/mock/app/index.gts +++ b/showcase/app/components/mock/app/index.gts @@ -11,10 +11,15 @@ import MockAppSidebarOldSideNav from './sidebar/side-nav'; import MockAppMainPageHeader from './main/page-header'; import MockAppMainGenericTextContent from './main/generic-text-content'; import MockAppMainGenericAdvancedTable from './main/generic-advanced-table'; +import MockAppMainFormComplex from './main/form-complex'; +import MockAppMainTableComplex from './main/table-complex'; import MockAppFooterAppFooter from './footer/app-footer'; // HDS components -import { HdsAppFrame } from '@hashicorp/design-system-components/components'; +import { + HdsAlert, + HdsAppFrame, +} from '@hashicorp/design-system-components/components'; // types import type { ComponentLike } from '@glint/template'; @@ -25,10 +30,14 @@ import type { MockAppSidebarOldSideNavSignature } from './sidebar/side-nav'; import type { MockAppMainPageHeaderSignature } from './main/page-header'; import type { MockAppMainGenericTextContentSignature } from './main/generic-text-content'; import type { MockAppMainGenericAdvancedTableSignature } from './main/generic-advanced-table'; +import type { MockAppMainFormComplexSignature } from './main/form-complex'; +import type { MockAppMainTableComplexSignature } from './main/table-complex'; +import type { MockAppMainPaginationSignature } from './main/pagination'; import type { MockAppFooterAppFooterSignature } from './footer/app-footer'; export interface MockAppSignature { Args: { + hasPageAlert?: boolean; hasHeader?: HdsAppFrameSignature['Args']['hasHeader']; hasSidebar?: HdsAppFrameSignature['Args']['hasSidebar']; hasOldSidebar?: boolean; @@ -42,9 +51,8 @@ export interface MockAppSignature { ]; sidebar?: [ { - SideNav?: - | ComponentLike - | ComponentLike; + AppSideNav?: ComponentLike; + SideNav?: ComponentLike; }, ]; main?: [ @@ -52,6 +60,9 @@ export interface MockAppSignature { PageHeader?: ComponentLike; GenericTextContent?: ComponentLike; GenericAdvancedTable?: ComponentLike; + FormComplex?: ComponentLike; + TableComplex?: ComponentLike; + Pagination?: ComponentLike; }, ]; footer?: [ @@ -82,7 +93,13 @@ export default class MockApp extends Component { {{#if (has-block "sidebar")}} - {{yield (hash SideNav=MockAppSidebarAppSideNav) to="sidebar"}} + {{yield + (hash + AppSideNav=MockAppSidebarAppSideNav + SideNav=MockAppSidebarOldSideNav + ) + to="sidebar" + }} {{else}} {{#if @hasOldSidebar}} @@ -92,12 +109,20 @@ export default class MockApp extends Component { {{/if}} + {{#if @hasPageAlert}} + + Lorem ipsum + Lorem ipsum dolor sit amet. + + {{/if}}
{{yield (hash PageHeader=MockAppMainPageHeader GenericTextContent=MockAppMainGenericTextContent GenericAdvancedTable=MockAppMainGenericAdvancedTable + FormComplex=MockAppMainFormComplex + TableComplex=MockAppMainTableComplex ) to="main" }} diff --git a/showcase/app/components/mock/app/main/form-complex.gts b/showcase/app/components/mock/app/main/form-complex.gts new file mode 100644 index 00000000000..4d966dc64c3 --- /dev/null +++ b/showcase/app/components/mock/app/main/form-complex.gts @@ -0,0 +1,354 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import style from 'ember-style-modifier/modifiers/style'; + +// HDS components +import { + HdsButton, + HdsButtonSet, + HdsFormCheckboxGroup, + HdsFormFileInputField, + HdsFormMaskedInputField, + HdsFormRadioGroup, + HdsFormRadioCardGroup, + HdsFormSelectField, + HdsFormSuperSelectSingleField, + HdsFormSuperSelectMultipleField, + HdsFormTextInputField, + HdsFormTextareaField, + HdsFormToggleField, + HdsLinkInline, + HdsSeparator, + HdsTextBody, + HdsTextDisplay, +} from '@hashicorp/design-system-components/components'; + +const RADIOCARDS = [ + { + value: '1', + label: 'Radio card label 1', + badge: 'Badge', + checked: true, + description: 'Radio card description 1', + generic: 'Radio card custom content 1', + }, + { + value: '2', + label: 'Radio card label 2', + badge: 'Badge', + description: 'Radio card description 2', + generic: 'Radio card custom content 2', + }, + { + value: '3', + label: 'Radio card label 3', + badge: 'Badge', + description: 'Radio card description 3', + generic: 'Radio card custom content 3', + }, +]; + +const SUPERSELECT1_OPTIONS = [ + { + size: 'Extra Small', + description: '2 vCPU | 1 GiB RAM', + price: '$0.02', + }, + { + size: 'Small', + description: '2 vCPU | 2 GiB RAM', + price: '$0.04', + disabled: true, + }, + { + size: 'Medium', + description: '4 vCPU | 4 GiB RAM', + price: '$0.08', + disabled: true, + }, + { size: 'Large', description: '8 vCPU | 8 GiB RAM', price: '$0.16' }, + { + size: 'Extra Large', + description: '16 vCPU | 16 GiB RAM', + price: '$0.32', + }, +]; +const SELECTED_SUPERSELECT1_OPTION = SUPERSELECT1_OPTIONS[1]; + +const SUPERSELECT2_OPTIONS = ['Option 1', 'Option 2', 'Option 3']; +const SELECTED_SUPERSELECT2_OPTIONS = [ + SUPERSELECT2_OPTIONS[0], + SUPERSELECT2_OPTIONS[1], +]; + +const noop = () => {}; + +export interface MockAppMainFormComplexSignature { + Args: { + showAll?: boolean; + showErrors?: boolean; + showIntro?: boolean; + showCheckbox?: boolean; + showFileInput?: boolean; + showMaskedInput?: boolean; + showRadio?: boolean; + showRadioCard?: boolean; + showSelect?: boolean; + showSuperSelect?: boolean; + showTextarea?: boolean; + showTextInput?: boolean; + showToggle?: boolean; + showButtons?: boolean; + }; + Element: HTMLDivElement; +} + +export default class MockAppMainFormComplex extends Component { + _showIntro; + _showCheckbox; + _showFileInput; + _showMaskedInput; + _showRadio; + _showRadioCard; + _showSelect; + _showSuperSelect; + _showTextarea; + _showTextInput; + _showToggle; + _showButtons; + _showErrors; + + constructor(owner: unknown, args: MockAppMainFormComplexSignature['Args']) { + super(owner, args); + this._showIntro = this.args.showIntro ?? this.args.showAll ?? false; + this._showCheckbox = this.args.showCheckbox ?? this.args.showAll ?? false; + this._showFileInput = this.args.showFileInput ?? this.args.showAll ?? false; + this._showMaskedInput = + this.args.showMaskedInput ?? this.args.showAll ?? false; + this._showRadio = this.args.showRadio ?? this.args.showAll ?? false; + this._showRadioCard = this.args.showRadioCard ?? this.args.showAll ?? false; + this._showSelect = this.args.showSelect ?? this.args.showAll ?? false; + this._showSuperSelect = + this.args.showSuperSelect ?? this.args.showAll ?? false; + this._showTextarea = this.args.showTextarea ?? this.args.showAll ?? false; + this._showToggle = this.args.showToggle ?? this.args.showAll ?? false; + this._showErrors = this.args.showErrors ?? this.args.showAll ?? false; + // we want at least something to be visible by default + this._showTextInput = this.args.showTextInput ?? this.args.showAll ?? true; + this._showButtons = this.args.showButtons ?? this.args.showAll ?? true; + } + + +} diff --git a/showcase/app/components/mock/app/main/generic-text-content.gts b/showcase/app/components/mock/app/main/generic-text-content.gts index 94c54345959..0fd674d4f35 100644 --- a/showcase/app/components/mock/app/main/generic-text-content.gts +++ b/showcase/app/components/mock/app/main/generic-text-content.gts @@ -8,16 +8,23 @@ import type { TemplateOnlyComponent } from '@ember/component/template-only'; // HDS components import { HdsLinkInline, + HdsTextDisplay, HdsTextBody, } from '@hashicorp/design-system-components/components'; export interface MockAppMainGenericTextContentSignature { + Args: { + showHeadings?: boolean; + }; Element: HTMLDivElement; } const MockAppMainGenericTextContent: TemplateOnlyComponent = diff --git a/showcase/app/components/mock/app/sidebar/side-nav.gts b/showcase/app/components/mock/app/sidebar/side-nav.gts index c5f61f41237..c2329a4d04e 100644 --- a/showcase/app/components/mock/app/sidebar/side-nav.gts +++ b/showcase/app/components/mock/app/sidebar/side-nav.gts @@ -27,6 +27,10 @@ export interface MockAppSidebarOldSideNavSignature { showHeader?: boolean; showFooter?: boolean; }; + Blocks: { + extraBodyAfter: []; + extraFooterBefore: []; + }; Element: HdsSideNavSignature['Element']; } @@ -153,9 +157,11 @@ export default class MockAppSidebarOldSideNav extends Component + {{yield to="extraBodyAfter"}} <:footer> {{#if this.showFooter}} + {{yield to="extraFooterBefore"}} = + ; + +export default CodeFragmentWithThemingBasicContainer; diff --git a/showcase/app/components/page-foundations/theming/index.gts b/showcase/app/components/page-foundations/theming/index.gts new file mode 100644 index 00000000000..d0fc53f4c87 --- /dev/null +++ b/showcase/app/components/page-foundations/theming/index.gts @@ -0,0 +1,32 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import { pageTitle } from 'ember-page-title'; + +import ShwTextH1 from 'showcase/components/shw/text/h1'; +import ShwDivider from 'showcase/components/shw/divider'; + +import SubSectionThemeSwitcher from 'showcase/components/page-foundations/theming/sub-sections/theme-switcher'; +import SubSectionContexts from 'showcase/components/page-foundations/theming/sub-sections/contexts'; +import SubSectionComponents from 'showcase/components/page-foundations/theming/sub-sections/components'; +import SubSectionDemo from 'showcase/components/page-foundations/theming/sub-sections/demo'; + +const ThemingIndex: TemplateOnlyComponent = ; + +export default ThemingIndex; diff --git a/showcase/app/components/page-foundations/theming/sub-sections/components.gts b/showcase/app/components/page-foundations/theming/sub-sections/components.gts new file mode 100644 index 00000000000..0d8cf4e23f7 --- /dev/null +++ b/showcase/app/components/page-foundations/theming/sub-sections/components.gts @@ -0,0 +1,107 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; +import { on } from '@ember/modifier'; + +import ShwDivider from 'showcase/components/shw/divider'; +import ShwFlex from 'showcase/components/shw/flex'; +import ShwTextH2 from 'showcase/components/shw/text/h2'; +import ShwTextH4 from 'showcase/components/shw/text/h4'; + +import { + HdsAppFooter, + HdsDropdown, + HdsCodeBlock, +} from '@hashicorp/design-system-components/components'; + +const SubSectionComponents: TemplateOnlyComponent = ; + +export default SubSectionComponents; diff --git a/showcase/app/components/page-foundations/theming/sub-sections/contexts.gts b/showcase/app/components/page-foundations/theming/sub-sections/contexts.gts new file mode 100644 index 00000000000..6fcd9fd8dbe --- /dev/null +++ b/showcase/app/components/page-foundations/theming/sub-sections/contexts.gts @@ -0,0 +1,89 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import ShwDivider from 'showcase/components/shw/divider'; +import ShwFlex from 'showcase/components/shw/flex'; +import ShwTextH2 from 'showcase/components/shw/text/h2'; +import ShwTextH4 from 'showcase/components/shw/text/h4'; +import CodeFragmentWithThemingBasicContainer from '../code-fragments/with-theming-basic-container'; + +const SubSectionContexts: TemplateOnlyComponent = ; + +export default SubSectionContexts; diff --git a/showcase/app/components/page-foundations/theming/sub-sections/demo.gts b/showcase/app/components/page-foundations/theming/sub-sections/demo.gts new file mode 100644 index 00000000000..3001c4b7e03 --- /dev/null +++ b/showcase/app/components/page-foundations/theming/sub-sections/demo.gts @@ -0,0 +1,17 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import ShwTextH2 from 'showcase/components/shw/text/h2'; +import ShwFrame from 'showcase/components/shw/frame'; + +const SubSectionDemo: TemplateOnlyComponent = ; + +export default SubSectionDemo; diff --git a/showcase/app/components/page-foundations/theming/sub-sections/theme-switcher.gts b/showcase/app/components/page-foundations/theming/sub-sections/theme-switcher.gts new file mode 100644 index 00000000000..9f1eca7d4c3 --- /dev/null +++ b/showcase/app/components/page-foundations/theming/sub-sections/theme-switcher.gts @@ -0,0 +1,49 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; +import style from 'ember-style-modifier'; + +import ShwFlex from 'showcase/components/shw/flex'; +import ShwTextH2 from 'showcase/components/shw/text/h2'; +import ShwTextH4 from 'showcase/components/shw/text/h4'; +import ShwDivider from 'showcase/components/shw/divider'; + +import { HdsThemeSwitcher } from '@hashicorp/design-system-components/components'; + +const SubSectionThemeSwitcher: TemplateOnlyComponent = ; + +export default SubSectionThemeSwitcher; diff --git a/showcase/app/components/shw/theme-switcher/control/select.gts b/showcase/app/components/shw/theme-switcher/control/select.gts new file mode 100644 index 00000000000..c6e400f0b5e --- /dev/null +++ b/showcase/app/components/shw/theme-switcher/control/select.gts @@ -0,0 +1,67 @@ +import Component from '@glimmer/component'; +import { guidFor } from '@ember/object/internals'; +import { on } from '@ember/modifier'; +import { eq } from 'ember-truth-helpers'; + +interface ShwThemeSwitcherControlSelectSignature { + Args: { + label: string; + values?: string[] | Record; + selectedValue?: string; + onChange?: (event: Event) => void; + }; + Blocks: { + default: []; + }; +} + +export default class ShwThemeSwitcherControlSelect extends Component { + selectId = `shw-theme-switcher-select-${guidFor(this)}`; + + get options() { + if (Array.isArray(this.args.values)) { + // Convert array to an object where keys and values are the same + return this.args.values.reduce( + (acc, value) => { + acc[value] = value; + return acc; + }, + {} as Record, + ); + } else { + // If values is already an object, return it directly + return this.args.values; + } + } + + onChange = (event: Event) => { + if (this.args.onChange) { + this.args.onChange(event); + } + }; + + +} diff --git a/showcase/app/components/shw/theme-switcher/index.gts b/showcase/app/components/shw/theme-switcher/index.gts new file mode 100644 index 00000000000..72964641168 --- /dev/null +++ b/showcase/app/components/shw/theme-switcher/index.gts @@ -0,0 +1,161 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { service } from '@ember/service'; +import { guidFor } from '@ember/object/internals'; + +import ShwThemeSwitcherPopover from './popover'; +import ShwThemeSwitcherSelector from './selector'; +import type { OnApplyArgs } from './popover'; +import type { OnSelectThemeArgs } from './selector'; + +import config from 'showcase/config/environment'; +import { HdsIcon } from '@hashicorp/design-system-components/components'; +import { + HdsThemeValues, + HdsModesLightValues, + HdsModesDarkValues, + HdsCssSelectorsValues, + // DEFAULT_THEMING_OPTIONS, +} from '@hashicorp/design-system-components/services/hds-theming'; +import type HdsThemingService from '@hashicorp/design-system-components/services/hds-theming'; +import type { + HdsThemes, + HdsModesLight, + HdsModesDark, + HdsCssSelectors, + // HdsThemingServiceOptions, +} from '@hashicorp/design-system-components/services/hds-theming'; + +const updatePageStylesheet = (currentStylesheet: string) => { + let newStylesheet; + switch (currentStylesheet) { + case 'prefers-color-scheme': + // themed CSS where theming is applied via `@media(prefers-color-scheme)` + newStylesheet = + 'assets/styles/@hashicorp/design-system-components-theming-with-prefers-color-scheme.css'; + break; + case 'css-selectors': + // themed CSS where theming is applied via CSS selectors + newStylesheet = + 'assets/styles/@hashicorp/design-system-components-theming-with-css-selectors.css'; + break; + case 'combined-strategies': + // this is used for local testing purposes + newStylesheet = + 'assets/styles/@hashicorp/design-system-components-theming-with-combined-strategies.css'; + break; + default: + // this is the standard CSS for HDS components, without any theming + newStylesheet = 'assets/styles/@hashicorp/design-system-components.css'; + break; + } + + // re-assign the stylesheet `href` attribute + const hdsComponentsStylesheet = document.getElementById( + 'hds-components-stylesheet', + ); + if (hdsComponentsStylesheet) { + hdsComponentsStylesheet.setAttribute( + 'href', + `${config.rootURL}${newStylesheet}`, + ); + } +}; + +export default class ShwThemeSwitcher extends Component { + @service declare readonly hdsTheming: HdsThemingService; + + @tracked currentStylesheet = 'standard'; + @tracked currentTheme: HdsThemes = undefined; + @tracked currentLightTheme: HdsModesLight = HdsModesLightValues.CdsG0; + @tracked currentDarkTheme: HdsModesDark = HdsModesDarkValues.CdsG100; + @tracked currentCssSelector: HdsCssSelectors = HdsCssSelectorsValues.Data; + + popoverId = `shw-theming-options-popover-${guidFor(this)}`; + + onSelectPageTheme = (args: OnSelectThemeArgs) => { + const { currentStylesheet, currentTheme } = args; + + console.log( + 'onSelectPageTheme invoked', + `currentStylesheet=${currentStylesheet}`, + `currentTheme=${currentTheme}`, + ); + + // update the theming preferences + this.currentStylesheet = currentStylesheet; + this.currentTheme = currentTheme; + + // update the page's stylesheet + updatePageStylesheet(this.currentStylesheet); + + // we set the theme in the global service + this.hdsTheming.setTheme(this.currentTheme); + + // we set the theme for the showcase itself + const rootElement = document.querySelector('html'); + if (rootElement) { + if (this.currentTheme) { + rootElement.setAttribute('data-shw-theme', this.currentTheme); + } else { + rootElement.removeAttribute('data-shw-theme'); + } + } + }; + + onApplyAdvancedThemingPreferences = (args: OnApplyArgs) => { + const { currentLightTheme, currentDarkTheme, currentCssSelector } = args; + + console.log( + 'onApplyAdvancedThemingPreferences invoked', + `currentLightTheme=${currentLightTheme}`, + `currentDarkTheme=${currentDarkTheme}`, + `currentCssSelector=${currentCssSelector}`, + ); + + // update the theming preferences + this.currentLightTheme = currentLightTheme; + this.currentDarkTheme = currentDarkTheme; + this.currentCssSelector = currentCssSelector; + + // update the theming options in the global service + this.hdsTheming.setThemingServiceOptions({ + themeMap: { + [HdsThemeValues.Light]: this.currentLightTheme, + [HdsThemeValues.Dark]: this.currentDarkTheme, + }, + cssSelector: this.currentCssSelector, + }); + }; + + +} diff --git a/showcase/app/components/shw/theme-switcher/popover.gts b/showcase/app/components/shw/theme-switcher/popover.gts new file mode 100644 index 00000000000..5e2ca63551b --- /dev/null +++ b/showcase/app/components/shw/theme-switcher/popover.gts @@ -0,0 +1,135 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; +import { hash, fn } from '@ember/helper'; +import type Owner from '@ember/owner'; + +import ShwThemeSwitcherControlSelect from './control/select'; + +import { + MODES_LIGHT, + MODES_DARK, +} from '@hashicorp/design-system-components/services/hds-theming'; +import type { + HdsModesLight, + HdsModesDark, + HdsCssSelectors, +} from '@hashicorp/design-system-components/services/hds-theming'; + +export interface OnApplyArgs { + currentLightTheme: HdsModesLight; + currentDarkTheme: HdsModesDark; + currentCssSelector: HdsCssSelectors; +} + +export interface ShwThemeSwitcherPopoverSignature { + Args: { + popoverId: string; + currentLightTheme: HdsModesLight; + currentDarkTheme: HdsModesDark; + currentCssSelector: HdsCssSelectors; + onApply: (args: OnApplyArgs) => void; + }; + Element: HTMLDivElement; +} + +export default class ShwThemeSwitcherPopover extends Component { + @tracked selectedLightTheme; + @tracked selectedDarkTheme; + @tracked selectedCssSelector; + + constructor(owner: Owner, args: ShwThemeSwitcherPopoverSignature['Args']) { + super(owner, args); + this.selectedLightTheme = this.args.currentLightTheme; + this.selectedDarkTheme = this.args.currentDarkTheme; + this.selectedCssSelector = this.args.currentCssSelector; + } + + onChangeAdvancedOption = (optionName: string, event: Event) => { + const select = event.target as HTMLSelectElement; + switch (optionName) { + case 'light-theme': + this.selectedLightTheme = select.value as HdsModesLight; + break; + case 'dark-theme': + this.selectedDarkTheme = select.value as HdsModesDark; + break; + case 'css-selector': + this.selectedCssSelector = select.value as HdsCssSelectors; + break; + } + }; + + onApplyThemingPreferences = () => { + if (typeof this.args.onApply === 'function') { + this.args.onApply({ + currentLightTheme: this.selectedLightTheme, + currentDarkTheme: this.selectedDarkTheme, + currentCssSelector: this.selectedCssSelector, + }); + } + + // programmatically close the popover + const popoverElement = document.getElementById(this.args.popoverId); + if (popoverElement && 'hidePopover' in popoverElement) { + popoverElement.hidePopover(); + } + }; + + +} diff --git a/showcase/app/components/shw/theme-switcher/selector.gts b/showcase/app/components/shw/theme-switcher/selector.gts new file mode 100644 index 00000000000..3b40e0c0faa --- /dev/null +++ b/showcase/app/components/shw/theme-switcher/selector.gts @@ -0,0 +1,116 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { eq } from 'ember-truth-helpers'; +import type Owner from '@ember/owner'; + +import ShwThemeSwitcherControlSelect from './control/select'; + +import type { + HdsModesLight, + HdsModesDark, + HdsThemes, +} from '@hashicorp/design-system-components/services/hds-theming'; + +export interface ShwThemeSwitcherSelectorSignature { + Args: { + currentStylesheet: string; + currentTheme: HdsThemes; + currentLightTheme: HdsModesLight; + currentDarkTheme: HdsModesDark; + onSelectTheme?: (args: OnSelectThemeArgs) => void; + }; + Element: HTMLDivElement; +} + +export interface OnSelectThemeArgs { + currentStylesheet: string; + currentTheme: HdsThemes; +} + +export default class ShwThemeSwitcherSelector extends Component { + @tracked selectedStylesheet; + @tracked selectedTheme; + + constructor(owner: Owner, args: ShwThemeSwitcherSelectorSignature['Args']) { + super(owner, args); + this.selectedStylesheet = this.args.currentStylesheet; + this.selectedTheme = this.args.currentTheme; + } + + get gLight() { + return this.args.currentLightTheme.replace('cds-', ''); + } + + get gDark() { + return this.args.currentDarkTheme.replace('cds-', ''); + } + + get selectedOption() { + return `${this.selectedStylesheet}|${this.selectedTheme}`; + } + + get themingOptions(): Record> { + return { + 'No theming': { + 'standard|no-theming': 'HDS / Standard', + }, + 'Theming via prefers-color-scheme': { + 'prefers-color-scheme|system': 'Carbon / System', + }, + 'Theming via CSS selectors': { + 'css-selectors|standard': 'HDS / Default', + 'css-selectors|light': `Carbon / Light (${this.gLight})`, + 'css-selectors|dark': `Carbon / Dark (${this.gDark})`, + }, + 'Theming via combined strategies': { + 'combined-strategies|standard': 'HDS / Default', + 'combined-strategies|system': 'Carbon / System', + 'combined-strategies|light': `Carbon / Light (${this.gLight})`, + 'combined-strategies|dark': `Carbon / Dark (${this.gDark})`, + }, + }; + } + + onSelectTheme = (event: Event) => { + const select = event.target as HTMLSelectElement; + const selectValue = select.value; + + const [selectedStylesheet, selectedTheme] = selectValue.split('|') as [ + string, + HdsThemes, + ]; + + this.selectedStylesheet = selectedStylesheet; + this.selectedTheme = selectedTheme; + + if (typeof this.args.onSelectTheme === 'function') { + this.args.onSelectTheme({ + currentStylesheet: this.selectedStylesheet, + currentTheme: this.selectedTheme, + }); + } + }; + + +} diff --git a/showcase/app/controllers/application.ts b/showcase/app/controllers/application.ts index 79d6ff8ac42..e3d3992fb75 100644 --- a/showcase/app/controllers/application.ts +++ b/showcase/app/controllers/application.ts @@ -10,14 +10,18 @@ import { tracked } from '@glimmer/tracking'; import type RouterService from '@ember/routing/router-service'; import type Owner from '@ember/owner'; +import type HdsThemingService from '@hashicorp/design-system-components/services/hds-theming'; + export default class ApplicationController extends Controller { @service declare readonly router: RouterService; + @service declare readonly hdsTheming: HdsThemingService; @tracked isFrameless = false; constructor(owner: Owner) { super(owner); this.router.on('routeDidChange', this.routeDidChange.bind(this)); + this.hdsTheming.initializeTheme(); } routeDidChange() { diff --git a/showcase/app/index.html b/showcase/app/index.html index 116b91e14f9..29005fe403a 100644 --- a/showcase/app/index.html +++ b/showcase/app/index.html @@ -5,28 +5,39 @@ --> - - - HDS components showcase - - - - - {{content-for "head"}} - - - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - + + + + HDS components showcase + + + + + {{content-for "head"}} + + + + + + + + + + + + + {{content-for "head-footer"}} + + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/showcase/app/router.ts b/showcase/app/router.ts index cbc23642057..9fd96b42b8d 100644 --- a/showcase/app/router.ts +++ b/showcase/app/router.ts @@ -23,6 +23,11 @@ Router.map(function () { this.route('demo-viewport-breakpoints-page-padding'); }); }); + this.route('theming', function () { + this.route('frameless', function () { + this.route('demo-application-with-theme-switcher'); + }); + }); }); this.route('page-components', { path: 'components' }, function () { this.route('accordion'); diff --git a/showcase/app/styles/_globals.scss b/showcase/app/styles/_globals.scss index 1447a120e7e..76695a082fc 100644 --- a/showcase/app/styles/_globals.scss +++ b/showcase/app/styles/_globals.scss @@ -19,6 +19,7 @@ body { min-height: 100vh; margin: 0; padding: 0; + color: var(--shw-color-black); background: var(--shw-color-white); } @@ -32,7 +33,7 @@ body { height: 68px; padding: 0 24px; color: var(--shw-color-black); - border-bottom: 1px solid #eaeaea; + border-bottom: 1px solid var(--shw-color-gray-500); } .shw-page-header__logo { @@ -68,6 +69,10 @@ body { line-height: 1; } +.shw-page-header__theme-toggle { + margin-left: auto; +} + .shw-page-aside { padding: 1rem; diff --git a/showcase/app/styles/_tokens.scss b/showcase/app/styles/_tokens.scss deleted file mode 100644 index fa11ede62f2..00000000000 --- a/showcase/app/styles/_tokens.scss +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -// TOKENS (CSS PROPS) - -:root { - // COLORS - --shw-color-white: #fff; - --shw-color-gray-600: #f2f2f3; - --shw-color-gray-500: #dbdbdc; - --shw-color-gray-400: #bfbfc0; - --shw-color-gray-300: #727374; - --shw-color-gray-200: #343536; - --shw-color-gray-100: #1d1e1f; - --shw-color-black: #000; - --shw-color-link-on-black: #4294ff; - --shw-color-link-on-white: #2264d6; - --shw-color-feedback-information-100: #0d44cc; - --shw-color-feedback-information-200: #1563ff; - --shw-color-feedback-information-300: #d0e0ff; - --shw-color-feedback-information-400: #eff5ff; - --shw-color-feedback-success-100: #007854; - --shw-color-feedback-success-200: #00bc7f; - --shw-color-feedback-success-300: #c1f1e0; - --shw-color-feedback-success-400: #ebfdf7; - --shw-color-feedback-warning-100: #975b06; - --shw-color-feedback-warning-200: #eaaa32; - --shw-color-feedback-warning-300: #f9eacd; - --shw-color-feedback-warning-400: #fcf6ea; - --shw-color-feedback-critical-100: #ba2226; - --shw-color-feedback-critical-200: #f25054; - --shw-color-feedback-critical-300: #ffd4d6; - --shw-color-feedback-critical-400: #fcf0f2; - --shw-color-action-active-foreground: #00f; // HTML "blue" - --shw-color-action-active-border: #00f; // HTML "blue" - --shw-color-action-active-background: #f0f8ff; // HTML "aliceblue" - // "FLEX/GRID" COMPONENTS - --shw-layout-gap-base: 1rem; -} diff --git a/showcase/app/styles/_typography.scss b/showcase/app/styles/_typography.scss index d94b4c80230..ab1d9d2d4f8 100644 --- a/showcase/app/styles/_typography.scss +++ b/showcase/app/styles/_typography.scss @@ -86,6 +86,7 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; @mixin shw-font-style-h1() { @include shw-font-family("gilmer"); + color: var(--shw-color-black); font-weight: 700; font-size: 3rem; line-height: 1.3; @@ -95,6 +96,7 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; @mixin shw-font-style-h2() { @include shw-font-family("gilmer"); + color: var(--shw-color-black); font-weight: 400; font-size: 1.8rem; line-height: 1.3; @@ -104,6 +106,7 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; @mixin shw-font-style-h3() { @include shw-font-family("gilmer"); + color: var(--shw-color-black); font-weight: 400; font-size: 1.4rem; line-height: 1.3; @@ -113,7 +116,7 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; @mixin shw-font-style-h4() { @include shw-font-family("gilmer"); - color: #666; // equivalent to `opacity: 0.5` + color: var(--shw-color-gray-300); font-weight: 500; font-size: 1.2rem; line-height: 1.3; @@ -123,6 +126,7 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; @mixin shw-font-style-body { @include shw-font-family("gilmer"); + color: var(--shw-color-black); font-size: 1rem; line-height: 1.4; } @@ -170,6 +174,14 @@ $show-font-family-mono: ui-monospace, menlo, consolas, monospace; } } +// we need to increase the contrast for accessibility, for the `h4` style (lighter than the others) +.shw-text-h4 { + code { + color: var(--shw-color-gray-200); + } +} + + .shw-text-body code { font-size: 0.85em; } // more visually balanced // ALIGNMENT diff --git a/showcase/app/styles/app.scss b/showcase/app/styles/app.scss index 4df2b448959..82c6ef179c5 100644 --- a/showcase/app/styles/app.scss +++ b/showcase/app/styles/app.scss @@ -3,12 +3,13 @@ * SPDX-License-Identifier: MPL-2.0 */ -@use "@hashicorp/design-system-components"; -@use "@hashicorp/design-system-power-select-overrides"; +// @use "@hashicorp/design-system-components"; +// @use "@hashicorp/design-system-power-select-overrides"; // global declarations -@use "./tokens"; +@use "./showcase-theming/light"; +@use "./showcase-theming/dark"; @use "./layout"; @use "./typography"; @use "./globals"; @@ -23,6 +24,7 @@ @use "./showcase-components/label"; @use "./showcase-components/outliner"; @use "./showcase-components/placeholder"; +@use "./showcase-components/theme-switcher"; @use "./mock-components/app"; @use "./mock-components/demo/breakpoints"; @@ -81,6 +83,7 @@ @use "./showcase-pages/tabs" as showcase-tabs; @use "./showcase-pages/tag" as showcase-tag; @use "./showcase-pages/text" as showcase-text; +@use "./showcase-pages/theming" as showcase-theming; @use "./showcase-pages/tooltip" as showcase-tooltip; @use "./showcase-pages/typography" as showcase-typography; // END COMPONENT PAGES IMPORTS diff --git a/showcase/app/styles/showcase-components/divider.scss b/showcase/app/styles/showcase-components/divider.scss index 51f63676a67..19718e76ab3 100644 --- a/showcase/app/styles/showcase-components/divider.scss +++ b/showcase/app/styles/showcase-components/divider.scss @@ -6,9 +6,9 @@ .shw-divider { margin: 3rem 0; border: none; - border-top: 2px solid #ccc; + border-top: 2px solid var(--shw-color-gray-500); } .shw-divider--level-2 { - border-top: 2px dotted #ddd; + border-top-style: dotted; } diff --git a/showcase/app/styles/showcase-components/flex.scss b/showcase/app/styles/showcase-components/flex.scss index d5779511a0a..32808edb42f 100644 --- a/showcase/app/styles/showcase-components/flex.scss +++ b/showcase/app/styles/showcase-components/flex.scss @@ -8,13 +8,13 @@ .shw-flex { & + &, & + .shw-grid { - margin-top: var(--shw-layout-gap-base); + margin-top: 1rem; } } .shw-flex__items { display: flex; - gap: var(--shw-layout-gap-base); + gap: 1rem; .shw-flex--direction-row > & { flex-direction: row; diff --git a/showcase/app/styles/showcase-components/frame.scss b/showcase/app/styles/showcase-components/frame.scss index cf57890321f..c99a40ee31a 100644 --- a/showcase/app/styles/showcase-components/frame.scss +++ b/showcase/app/styles/showcase-components/frame.scss @@ -15,7 +15,7 @@ $shw-frame-navigation-bar-height: 48px; max-width: 100%; height: calc(var(--iframe-height) + #{$shw-frame-navigation-bar-height}); max-height: 100%; - outline: 1px solid #e4e4d4; + outline: 1px solid var(--shw-color-gray-500); } .shw-frame__browser-navigation { @@ -25,12 +25,12 @@ $shw-frame-navigation-bar-height: 48px; height: $shw-frame-navigation-bar-height; // safe area for the dots padding: 8px 24px 8px 120px; - background-color: #fafafa; + background-color: var(--shw-frame-browser-navigation-background); background-image: url('data:image/svg+xml,'); background-repeat: no-repeat; background-position: 24px 50%; background-size: 56px 14px; - border-bottom: 1px solid #e4e4d4; + border-bottom: 1px solid var(--shw-color-gray-500); } .shw-frame__open-link { diff --git a/showcase/app/styles/showcase-components/grid.scss b/showcase/app/styles/showcase-components/grid.scss index 313db6e024b..9d2b9d92d23 100644 --- a/showcase/app/styles/showcase-components/grid.scss +++ b/showcase/app/styles/showcase-components/grid.scss @@ -8,7 +8,7 @@ .shw-grid { & + &, & + .shw-flex { - margin-top: var(--shw-layout-gap-base); + margin-top: 1rem; } } @@ -17,7 +17,7 @@ flex-wrap: wrap; // this will be set via JS grid-template-columns: repeat(var(--shw-grid-columns), 1fr); - gap: var(--shw-layout-gap-base); + gap: 1rem; } .shw-grid__item--grow { diff --git a/showcase/app/styles/showcase-components/label.scss b/showcase/app/styles/showcase-components/label.scss index 2340e936091..bdee3f9b876 100644 --- a/showcase/app/styles/showcase-components/label.scss +++ b/showcase/app/styles/showcase-components/label.scss @@ -10,7 +10,7 @@ .shw-label { @include shw-font-family("rubik"); margin: 0 0 10px 0; - color: #545454; + color: var(--shw-label-text-color); font-size: 0.8rem; line-height: 1.2; } diff --git a/showcase/app/styles/showcase-components/placeholder.scss b/showcase/app/styles/showcase-components/placeholder.scss index df37357fc9b..664ff2e37d4 100644 --- a/showcase/app/styles/showcase-components/placeholder.scss +++ b/showcase/app/styles/showcase-components/placeholder.scss @@ -11,18 +11,18 @@ display: flex; align-items: center; justify-content: center; - color: #6b6b6b; // if background is #EEE then this has the appropriate color contrast (4.59:1) + color: var(--shw-placeholder-text-color); font-weight: bold; font-size: 10px; font-family: monaco, Consolas, "Lucida Console", monospace; line-height: 1.2; text-align: center; - text-shadow: 0 0 5px #fff; - background-color: #eee; + text-shadow: 0 0 5px var(--shw-color-white); + background-color: var(--shw-placeholder-background-color); a, a > & { - color: #333; + color: var(--shw-placeholder-link-color); text-decoration: underline; } } diff --git a/showcase/app/styles/showcase-components/theme-switcher.scss b/showcase/app/styles/showcase-components/theme-switcher.scss new file mode 100644 index 00000000000..3e207a2237f --- /dev/null +++ b/showcase/app/styles/showcase-components/theme-switcher.scss @@ -0,0 +1,164 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +@use "../typography" as *; + +.shw-theme-switcher { + position: relative; + display: flex; + gap: 8px; + align-items: center; + color: var(--shw-color-black); +} + + +// POPOVER + +.shw-theme-switcher-popover { + position: absolute; + top: 50px; + right: 24px; + left: auto; + width: 350px; + max-width: calc(100vw - 48px); + margin: initial; + margin-left: 24px; + padding: 16px; + background-color: var(--shw-color-white); + border: 1px solid var(--shw-color-gray-400); + border-radius: 4px; + box-shadow: 2px 4px 8px rgba(0, 0, 0, 15%); + + &::backdrop { + background-color: var(--shw-color-white); + opacity: 0.8; + } +} + +.shw-theme-switcher-popover__title { + @include shw-font-family("gilmer"); + + margin: 0 0 8px; + color: var(--shw-color-gray-300); + font-weight: 500; + font-size: 1.2rem; + line-height: 1.3; +} + +.shw-theme-switcher-popover__description { + @include shw-font-family("gilmer"); + + margin: 4px 0 12px; + color: var(--shw-color-black); + font-weight: 400; + font-size: 0.9rem; + line-height: 1.3; +} + +.shw-theme-switcher-popover__advanced-options { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; +} + +.shw-theme-switcher-popover__actions { + display: flex; + flex-direction: row; + gap: 12px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--shw-color-gray-600); +} + +.shw-theme-switcher-popover__button { + @include shw-font-family('gilmer'); + + min-width: 72px; + height: 32px; + padding: 2px 12px 2px 12px; + font-size: 0.9rem; + border-style: solid; + border-width: 1px; + border-radius: 3px; + appearance: none; +} + +.shw-theme-switcher-popover__button--primary { + color: var(--shw-color-action-active-foreground); + background-color: var(--shw-color-action-active-background); + border-color: var(--shw-color-action-active-border); +} + +.shw-theme-switcher-popover__button--secondary { + color: var(--shw-color-gray-100); + background-color: transparent; + border-color: var(--shw-color-gray-300); +} + + +// CONTROLS + +.shw-theme-switcher-popover__control-item { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +} + +.shw-theme-switcher__control-label { + @include shw-font-family('gilmer'); + + margin: 0; + font-size: 0.9rem; + line-height: 20px; +} + +.shw-theme-switcher__control-select { + @include shw-font-family('gilmer'); + + height: 24px; + padding: 2px 24px 2px 8px; + color: var(--shw-color-gray-100); + font-size: 0.8rem; + background-color: var(--shw-color-gray-600); + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.34572 7H20.6543C21.8517 7 22.4504 8.4463 21.6028 9.29391L12.9519 17.9515C12.4272 18.4763 11.5728 18.4763 11.0481 17.9515L2.39722 9.29391C1.54961 8.4463 2.14832 7 3.34572 7Z' fill='%23808080'/%3E%3C/svg%3E"); // notice: the 'caret' color is hardcoded here! + background-repeat: no-repeat; + background-position: right 6px top 4px; + background-size: 12px 12px; + border: 1px solid var(--shw-color-gray-400); + border-radius: 3px; + appearance: none; + + &:hover { + background-color: var(--shw-color-gray-500); + } + + &:where(:focus-visible) { + outline: 2px dashed var(--shw-color-action-active-foreground); + } +} + + +// OPTIONS BUTTON + +.shw-theme-switcher__options-button { + height: 24px; + padding: 2px 8px; + color: var(--shw-color-gray-100); + background-color: var(--shw-color-gray-600); + border: 1px solid var(--shw-color-gray-400); + border-radius: 3px; + cursor: pointer; + appearance: none; + + &:hover { + background-color: var(--shw-color-gray-500); + } + + &:where(:focus-visible) { + outline: 2px dashed var(--shw-color-action-active-foreground); + } +} diff --git a/showcase/app/styles/showcase-pages/theming.scss b/showcase/app/styles/showcase-pages/theming.scss new file mode 100644 index 00000000000..579a77e206f --- /dev/null +++ b/showcase/app/styles/showcase-pages/theming.scss @@ -0,0 +1,34 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// THEMING + +body.page-foundations-theming { + .shw-foundation-theming-light-background { + padding: 8px; + background: var(--shw-color-white); + } + + .shw-foundation-theming-dark-background { + padding: 8px; + background: var(--shw-color-black); + } + + .shw-foundation-theming-basic-container { + display: flex; + align-items: center; + justify-content: center; + width: fit-content; + min-width: 120px; + min-height: 120px; + padding: 50px; + color: var(--token-color-foreground-strong); + font-weight: bold; + font-size: 10px; + font-family: monaco, Consolas, "Lucida Console", monospace; + line-height: 1.2; + background-color: var(--token-color-surface-strong); + } +} diff --git a/showcase/app/styles/showcase-pages/typography.scss b/showcase/app/styles/showcase-pages/typography.scss index aa44b080a01..6ec00b2e738 100644 --- a/showcase/app/styles/showcase-pages/typography.scss +++ b/showcase/app/styles/showcase-pages/typography.scss @@ -7,11 +7,12 @@ body.page-foundations-typography { .shw-label { - color: #999; + color: var(--shw-color-gray-300); } p[class^="hds-"] { margin: 0; padding: 0; + color: var(--shw-color-black); } } diff --git a/showcase/app/styles/showcase-theming/dark.scss b/showcase/app/styles/showcase-theming/dark.scss new file mode 100644 index 00000000000..d9b2fdcfee7 --- /dev/null +++ b/showcase/app/styles/showcase-theming/dark.scss @@ -0,0 +1,32 @@ +// SHOWCASE COLORS > DARK THEME + +@mixin shw-theme-color-variables-dark() { + // SEMANTIC PALETTE + --shw-color-white: #1a1a1a; + --shw-color-gray-600: #222225; + --shw-color-gray-500: #353537; + --shw-color-gray-400: #4c4c4d; + --shw-color-gray-300: #89898a; + --shw-color-gray-200: #babbbc; + --shw-color-gray-100: #cccdcf; + --shw-color-black: #e5e5e5; + --shw-color-action-active-foreground: #1a1ae5; + --shw-color-action-active-border: #1a1ae5; + --shw-color-action-active-background: #062139; + // COMPONENTS + --shw-frame-browser-navigation-background: #050505; + --shw-label-text-color: #c4c4c4; + --shw-placeholder-text-color: #949494; + --shw-placeholder-background-color: #121212; + --shw-placeholder-link-color: #ccc; +} + +@media (prefers-color-scheme: dark) { + :root[data-shw-theme="system"] { + @include shw-theme-color-variables-dark(); + } +} + +[data-shw-theme="dark"] { + @include shw-theme-color-variables-dark(); +} diff --git a/showcase/app/styles/showcase-theming/light.scss b/showcase/app/styles/showcase-theming/light.scss new file mode 100644 index 00000000000..351e38d93d1 --- /dev/null +++ b/showcase/app/styles/showcase-theming/light.scss @@ -0,0 +1,36 @@ +// SHOWCASE COLORS > LIGHT THEME + +@mixin shw-theme-color-variables-light() { + // SEMANTIC PALETTE + --shw-color-white: #fff; + --shw-color-gray-600: #f2f2f3; + --shw-color-gray-500: #dbdbdc; + --shw-color-gray-400: #bfbfc0; + --shw-color-gray-300: #727374; + --shw-color-gray-200: #343536; + --shw-color-gray-100: #1d1e1f; + --shw-color-black: #000; + --shw-color-action-active-foreground: #00f; // HTML "blue" + --shw-color-action-active-border: #00f; // HTML "blue" + --shw-color-action-active-background: #f0f8ff; // HTML "aliceblue" + // COMPONENTS + --shw-frame-browser-navigation-background: #fafafa; + --shw-label-text-color: #545454; + --shw-placeholder-text-color: #6b6b6b; // if background is #EEE then this has the appropriate color contrast (4.59:1) + --shw-placeholder-background-color: #eee; + --shw-placeholder-link-color: #333; +} + +:root { + @include shw-theme-color-variables-light(); +} + +@media (prefers-color-scheme: light) { + :root[data-shw-theme="system"] { + @include shw-theme-color-variables-light(); + } +} + +[data-shw="light"] { + @include shw-theme-color-variables-light(); +} diff --git a/showcase/app/templates/application.hbs b/showcase/app/templates/application.hbs index ff9e1cbdd64..f0bd45abb47 100644 --- a/showcase/app/templates/application.hbs +++ b/showcase/app/templates/application.hbs @@ -13,6 +13,9 @@
Components showcase
+
+ +