From 9510f06ddb9dbe7015b7c911b7c504003139e385 Mon Sep 17 00:00:00 2001 From: Cristiano Rastelli Date: Tue, 30 Sep 2025 20:52:07 +0100 Subject: [PATCH 001/147] added `Hds::ThemeSwitcher` component --- packages/components/package.json | 3 +- packages/components/src/components.ts | 3 + .../components/hds/theme-switcher/index.hbs | 11 ++++ .../components/hds/theme-switcher/index.ts | 63 +++++++++++++++++++ packages/components/src/template-registry.ts | 5 ++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 packages/components/src/components/hds/theme-switcher/index.hbs create mode 100644 packages/components/src/components/hds/theme-switcher/index.ts diff --git a/packages/components/package.json b/packages/components/package.json index a2dfcfe6ba7..136ab3b6824 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -369,6 +369,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", @@ -427,4 +428,4 @@ "engines": { "node": ">= 18" } -} +} \ No newline at end of file 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..4486348faff --- /dev/null +++ b/packages/components/src/components/hds/theme-switcher/index.hbs @@ -0,0 +1,11 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + {{#each-in this._options as |key data|}} + {{data.label}} + {{/each-in}} + 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..4f6ec8fb882 --- /dev/null +++ b/packages/components/src/components/hds/theme-switcher/index.ts @@ -0,0 +1,63 @@ +/** + * 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 HdsThemingService from '../../../services/hds-theming.ts'; +import { type HdsThemes } from '../../../services/hds-theming.ts'; + +export const OPTIONS = { + none: { theme: undefined, icon: 'minus', label: 'None' }, + system: { theme: 'system', icon: 'monitor', label: 'System' }, + light: { theme: 'light', icon: 'sun', label: 'Light' }, + dark: { theme: 'dark', icon: 'moon', label: 'Dark' }, +} as const; + +export interface HdsThemeSwitcherSignature { + Args: { + toggleSize?: HdsDropdownToggleButtonSignature['Args']['size']; + toggleIsFullWidth?: boolean; + }; + Element: HdsDropdownSignature['Element']; +} + +export default class HdsThemeSwitcher extends Component { + @service declare readonly hdsTheming: HdsThemingService; + + _options = 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() { + 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/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; From af06a91aeb3f88cce940e9f818cca44dfc31e95f Mon Sep 17 00:00:00 2001 From: Cristiano Rastelli Date: Tue, 30 Sep 2025 20:53:01 +0100 Subject: [PATCH 002/147] added `Hds::Theming` service --- packages/components/package.json | 1 + packages/components/src/services.ts | 2 + .../components/src/services/hds-theming.ts | 63 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 packages/components/src/services/hds-theming.ts diff --git a/packages/components/package.json b/packages/components/package.json index 136ab3b6824..d96dde0bf1a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -398,6 +398,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/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..cb508ddc167 --- /dev/null +++ b/packages/components/src/services/hds-theming.ts @@ -0,0 +1,63 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +import type Owner from '@ember/owner'; + +export const LOCALSTORAGE_KEY = 'hds-current-theme'; + +export enum HdsThemeValues { + System = 'system', + Light = 'light', + Dark = 'dark', +} + +export type HdsThemes = `${HdsThemeValues}` | undefined; + +export const THEMES: string[] = Object.values(HdsThemeValues); + +export default class HdsThemingService extends Service { + @tracked currentTheme: HdsThemes = undefined; + + constructor(owner: Owner) { + super(owner); + this.initializeTheme(); + } + + initializeTheme() { + const _initialTheme = localStorage.getItem(LOCALSTORAGE_KEY); + if ( + _initialTheme === 'system' || + _initialTheme === 'light' || + _initialTheme === 'dark' + ) { + this.setTheme(_initialTheme); + } + } + + getTheme(): HdsThemes { + return this.currentTheme; + } + + setTheme(theme: HdsThemes) { + // console.log('setting HDS theme', theme); + + if (theme === undefined) { + localStorage.removeItem(LOCALSTORAGE_KEY); + } else { + localStorage.setItem(LOCALSTORAGE_KEY, 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) { + if (theme === undefined) { + rootElement.removeAttribute('data-hds-theme'); + this.currentTheme = undefined; + } else { + rootElement.setAttribute('data-hds-theme', theme); + this.currentTheme = theme; + } + } + } +} From 147b1ebafcc2e97f00e6cc1688b088da23f533a2 Mon Sep 17 00:00:00 2001 From: Cristiano Rastelli Date: Tue, 30 Sep 2025 21:10:42 +0100 Subject: [PATCH 003/147] =?UTF-8?q?added=20theming=20to=20the=20Showcase?= =?UTF-8?q?=20itself=20(and=20replaced=20hardcoded=20values=20with=20`?= =?UTF-8?q?=E2=80=94shw`=20CSS=20variables)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- showcase/app/styles/_globals.scss | 7 +++- showcase/app/styles/_tokens.scss | 41 ------------------- showcase/app/styles/_typography.scss | 6 ++- showcase/app/styles/app.scss | 3 +- .../styles/showcase-components/divider.scss | 4 +- .../app/styles/showcase-components/flex.scss | 4 +- .../app/styles/showcase-components/frame.scss | 6 +-- .../app/styles/showcase-components/grid.scss | 4 +- .../app/styles/showcase-components/label.scss | 2 +- .../showcase-components/placeholder.scss | 8 ++-- .../app/styles/showcase-pages/typography.scss | 3 +- .../app/styles/showcase-theming/dark.scss | 33 +++++++++++++++ .../app/styles/showcase-theming/light.scss | 37 +++++++++++++++++ 13 files changed, 99 insertions(+), 59 deletions(-) delete mode 100644 showcase/app/styles/_tokens.scss create mode 100644 showcase/app/styles/showcase-theming/dark.scss create mode 100644 showcase/app/styles/showcase-theming/light.scss 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..cad0d0452bf 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; } diff --git a/showcase/app/styles/app.scss b/showcase/app/styles/app.scss index 3f80ec0b870..a8808eedabd 100644 --- a/showcase/app/styles/app.scss +++ b/showcase/app/styles/app.scss @@ -8,7 +8,8 @@ // global declarations -@use "./tokens"; +@use "./showcase-theming/light"; +@use "./showcase-theming/dark"; @use "./layout"; @use "./typography"; @use "./globals"; 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 3f06392e9a3..08c40f5d49a 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-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..a00d5ccb79d --- /dev/null +++ b/showcase/app/styles/showcase-theming/dark.scss @@ -0,0 +1,33 @@ +// 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-hds-theme="auto"] { + @include shw-theme-color-variables-dark(); + } +} + +.hds-theme-dark, +[data-hds-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..5ad549848fa --- /dev/null +++ b/showcase/app/styles/showcase-theming/light.scss @@ -0,0 +1,37 @@ +// 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="auto"] { + @include shw-theme-color-variables-light(); + } +} + +.hds-theme-light, +[data-hds-theme="light"] { + @include shw-theme-color-variables-light(); +} From 5ce786c9917870e38d1f051c4c38bd79716f2f8c Mon Sep 17 00:00:00 2001 From: Cristiano Rastelli Date: Tue, 30 Sep 2025 20:17:51 +0100 Subject: [PATCH 004/147] added `Shw::ThemeSwitcher` component for showcase --- .../components/shw/theme-switcher/index.gts | 57 +++++++++++++++++++ showcase/app/styles/app.scss | 1 + .../showcase-components/theme-switcher.scss | 32 +++++++++++ 3 files changed, 90 insertions(+) create mode 100644 showcase/app/components/shw/theme-switcher/index.gts create mode 100644 showcase/app/styles/showcase-components/theme-switcher.scss 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..d2c3c37504b --- /dev/null +++ b/showcase/app/components/shw/theme-switcher/index.gts @@ -0,0 +1,57 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { eq } from 'ember-truth-helpers'; +// import { tracked } from '@glimmer/tracking'; + +import type HdsThemingService from '@hashicorp/design-system-components/services/hds-theming.ts'; + +interface ShwThemeSwitcherSignature { + Element: HTMLDivElement; +} + +const options = { + none: 'None (No theming)', + system: 'System (prefers-color-scheme)', + light: 'Light (data-attribute)', + dark: 'Dark (data-attribute)', +}; + +export default class ShwThemeSwitcher extends Component { + @service declare readonly hdsTheming: HdsThemingService; + + @action + onChangePageTheme(event: Event) { + const select = event.target as HTMLSelectElement; + + // we set the theme in the global service + this.hdsTheming.setTheme(select.value); + } + + +} diff --git a/showcase/app/styles/app.scss b/showcase/app/styles/app.scss index a8808eedabd..ece7e869bb8 100644 --- a/showcase/app/styles/app.scss +++ b/showcase/app/styles/app.scss @@ -24,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"; 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..f61937282bc --- /dev/null +++ b/showcase/app/styles/showcase-components/theme-switcher.scss @@ -0,0 +1,32 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.shw-theme-switcher { + display: flex; + gap: 8px; + align-items: center; +} + +.shw-theme-switcher__label { + color: var(--shw-color-black); + font-size: 0.75rem; + font-family: monaco, Consolas, "Lucida Console", monospace; +} + +.shw-theme-switcher__control { + height: 24px; + padding: 2px 24px 2px 8px; + color: var(--shw-color-gray-100); + font-size: 0.75rem; + font-family: monaco, Consolas, "Lucida Console", monospace; + 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; +} From 884a821895ca1fc249e7faf4c875b334ceeb5368 Mon Sep 17 00:00:00 2001 From: Cristiano Rastelli Date: Tue, 30 Sep 2025 20:24:05 +0100 Subject: [PATCH 005/147] updated `Mock::App` and added new yielded sub-components --- showcase/app/components/mock/app/index.gts | 22 +- .../components/mock/app/main/form-complex.gts | 357 ++++++++++++++++++ .../mock/app/main/generic-text-content.gts | 13 + .../components/mock/app/main/pagination.gts | 31 ++ .../mock/app/main/table-complex.gts | 164 ++++++++ .../mock/app/sidebar/app-side-nav.gts | 9 + .../components/mock/app/sidebar/side-nav.gts | 2 + 7 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 showcase/app/components/mock/app/main/form-complex.gts create mode 100644 showcase/app/components/mock/app/main/pagination.gts create mode 100644 showcase/app/components/mock/app/main/table-complex.gts diff --git a/showcase/app/components/mock/app/index.gts b/showcase/app/components/mock/app/index.gts index e8ffe853e0b..22d4933ec5e 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; @@ -52,6 +61,9 @@ export interface MockAppSignature { PageHeader?: ComponentLike; GenericTextContent?: ComponentLike; GenericAdvancedTable?: ComponentLike; + FormComplex?: ComponentLike; + TableComplex?: ComponentLike; + Pagination?: ComponentLike; }, ]; footer?: [ @@ -92,12 +104,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..0b32daaedc3 --- /dev/null +++ b/showcase/app/components/mock/app/main/form-complex.gts @@ -0,0 +1,357 @@ +/** + * 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..2bfad93852e 100644 --- a/showcase/app/components/mock/app/sidebar/side-nav.gts +++ b/showcase/app/components/mock/app/sidebar/side-nav.gts @@ -153,9 +153,11 @@ export default class MockAppSidebarOldSideNav extends Component + {{yield to="extraBodyAfter"}} <:footer> {{#if this.showFooter}} + {{yield to="extraFooterBefore"}} Date: Tue, 30 Sep 2025 21:20:41 +0100 Subject: [PATCH 006/147] added `Shw:: ThemeSwitcher` to the Showcase page header --- showcase/app/templates/application.gts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/showcase/app/templates/application.gts b/showcase/app/templates/application.gts index f7617b77a00..44bb388835e 100644 --- a/showcase/app/templates/application.gts +++ b/showcase/app/templates/application.gts @@ -14,6 +14,7 @@ import { modifier } from 'ember-modifier'; import { HdsIcon } from '@hashicorp/design-system-components/components'; import ShwLogoDesignSystem from 'showcase/components/shw/logo/design-system'; +import ShwThemeSwitcher from 'showcase/components/shw/theme-switcher'; export default class Application extends Component { @service declare readonly router: RouterService; @@ -68,6 +69,9 @@ export default class Application extends Component {
Components showcase
+
+ +