diff --git a/src/packages/tour/option.ts b/src/packages/tour/option.ts index f24c904f2..45743866a 100644 --- a/src/packages/tour/option.ts +++ b/src/packages/tour/option.ts @@ -1,5 +1,6 @@ import { TooltipPosition } from "../../packages/tooltip"; import { TourStep, ScrollTo } from "./steps"; +import { ThemeType } from "./theme"; export interface TourOptions { steps: Partial[]; @@ -74,6 +75,8 @@ export interface TourOptions { progressBarAdditionalClass: string; /* Optional property to determine if content should be rendered as HTML */ tooltipRenderAsHtml?: boolean; + /* Theme for the tour - light, dark, auto */ + theme?: ThemeType; } export function getDefaultTourOptions(): TourOptions { @@ -116,5 +119,6 @@ export function getDefaultTourOptions(): TourOptions { buttonClass: "introjs-button", progressBarAdditionalClass: "", tooltipRenderAsHtml: true, + theme: "auto", }; } diff --git a/src/packages/tour/theme.ts b/src/packages/tour/theme.ts new file mode 100644 index 000000000..9c6e98c69 --- /dev/null +++ b/src/packages/tour/theme.ts @@ -0,0 +1,113 @@ +// theme.ts +export type ThemeType = "light" | "dark" | "auto"; + +export interface ThemeOptions { + theme?: ThemeType; + root?: HTMLElement; +} + +export class Theme { + private _theme: "light" | "dark"; + private _root: HTMLElement; + private mqlDark: MediaQueryList | null = null; + private boundHandleSystemThemeChange: () => void; + private themeType: ThemeType; + + constructor(options: ThemeOptions = {}) { + this._root = options.root ?? document.documentElement; + this.themeType = options.theme ?? "auto"; + + this.boundHandleSystemThemeChange = this.handleSystemThemeChange.bind(this); + + this._theme = this.resolveTheme(this.themeType); + this.applyToRoot(); + + if (this.themeType === "auto") { + this.mqlDark = window.matchMedia("(prefers-color-scheme: dark)"); + if ("addEventListener" in this.mqlDark) { + this.mqlDark.addEventListener( + "change", + this.boundHandleSystemThemeChange + ); + } else { + (this.mqlDark as any).addListener(this.boundHandleSystemThemeChange); + } + } + } + + private resolveTheme(theme: ThemeType): "light" | "dark" { + if (theme === "auto") { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + return theme; + } + + private handleSystemThemeChange() { + if (this.themeType === "auto") { + this._theme = this.resolveTheme("auto"); + this.applyToRoot(); + } + } + + private applyToRoot() { + this._root.classList.remove("introjs-light", "introjs-dark"); + this._root.classList.add(`introjs-${this._theme}`); + } + + /** Change theme manually */ + public setTheme(themeType: ThemeType) { + this.themeType = themeType; + this._theme = this.resolveTheme(themeType); + this.applyToRoot(); + } + + /** Update the root element (useful for tour reopen) */ + public setRoot(root: HTMLElement) { + if (this._root !== root) { + this._root = root; + this.applyToRoot(); + } + } + + /** Optional cleanup */ + public destroy() { + if (this.mqlDark) { + if ("removeEventListener" in this.mqlDark) { + this.mqlDark.removeEventListener( + "change", + this.boundHandleSystemThemeChange + ); + } else { + (this.mqlDark as any).removeListener(this.boundHandleSystemThemeChange); + } + this.mqlDark = null; + } + } + + public get value(): "light" | "dark" { + return this._theme; + } +} + +let currentTheme: Theme | null = null; + +export function getTheme(): Theme { + if (!currentTheme) { + currentTheme = new Theme({ theme: "auto" }); + } + return currentTheme; +} + +export function applyTheme(options: ThemeOptions = {}) { + if (!currentTheme) { + currentTheme = new Theme(options); + } else { + currentTheme.setTheme(options.theme ?? "auto"); + if (options.root) { + currentTheme.setRoot(options.root); + } + } + return currentTheme; +} diff --git a/src/packages/tour/tour.ts b/src/packages/tour/tour.ts index d1b6f1d76..699f7e5f7 100644 --- a/src/packages/tour/tour.ts +++ b/src/packages/tour/tour.ts @@ -22,6 +22,7 @@ import onKeyDown from "./onKeyDown"; import dom from "../dom"; import { TourRoot } from "./components/TourRoot"; import { FloatingElement } from "./components/FloatingElement"; +import { applyTheme } from "./theme"; /** * Intro.js Tour class @@ -446,6 +447,7 @@ export class Tour implements Package { async start() { if (await start(this)) { this.createRoot(); + this.initTheme(); this.enableKeyboardNavigation(); this.enableRefreshOnResize(); } @@ -453,6 +455,12 @@ export class Tour implements Package { return this; } + private initTheme() { + if (!this._root || !(this._root instanceof HTMLElement)) return; + + applyTheme({ root: this._root, theme: this._options.theme }); + } + /** * Exit the tour * @param {boolean} force whether to force exit the tour diff --git a/src/styles/introjs.scss b/src/styles/introjs.scss index 087aefe05..1d4912637 100644 --- a/src/styles/introjs.scss +++ b/src/styles/introjs.scss @@ -8,11 +8,49 @@ $black200: #eeeeee; $black100: #f4f4f4; $white: #ffffff; -$font_family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", - Helvetica, Arial, sans-serif; +$font_family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif; $background_color_9: #08c; $background_color_10: rgba(136, 136, 136, 0.24); +/* =================== THEME CLASSES =================== */ +.introjs-light { + --introjs-btn-bg: #{$black100}; + --introjs-btn-color: #{$black800}; + --introjs-btn-border: #{$black400}; + --introjs-btn-hover-bg: #{$black300}; + --introjs-btn-hover-color: #{$black900}; + --introjs-btn-focus-bg: #{$black200}; + --introjs-btn-focus-color: #{$black800}; + --introjs-btn-active-bg: #{$black300}; + --introjs-btn-active-color: #{$black900}; + --introjs-btn-active-border: #{$black500}; + --introjs-btn-textShadow: #{$white}; + + --introjs-arrow-color: #{$white}; + --introjs-overlay-bg: #{$black100}; + --introjs-bg: #{$white}; + --introjs-color: #{$black900}; +} + +.introjs-dark { + --introjs-btn-bg: #{$black600}; + --introjs-btn-color: #{$white}; + --introjs-btn-border: #{$white}; + --introjs-btn-hover-bg: #{$white}; + --introjs-btn-hover-color: #{$black800}; + --introjs-btn-focus-bg: #{$black500}; + --introjs-btn-focus-color: #{$white}; + --introjs-btn-active-bg: #{$black600}; + --introjs-btn-active-color: #{$white}; + --introjs-btn-active-border: #{$black400}; + --introjs-btn-textShadow: #{$black800}; + + --introjs-arrow-color: #{$black800}; + --introjs-overlay-bg: #{$black800}; + --introjs-bg: #{$black800}; + --introjs-color: #{$white}; +} + .introjs-tour { transition: all 0.3s ease-out; } @@ -104,70 +142,70 @@ tr.introjs-showElement { .introjs-arrow.top { top: -10px; left: 10px; - border-bottom-color: $white; + border-bottom-color: var(--introjs-arrow-color); } .introjs-arrow.top-right { top: -10px; right: 10px; - border-bottom-color: $white; + border-bottom-color: var(--introjs-arrow-color); } .introjs-arrow.top-middle { top: -10px; left: 50%; margin-left: -5px; - border-bottom-color: $white; + border-bottom-color: var(--introjs-arrow-color); } .introjs-arrow.right { right: -10px; top: 10px; - border-left-color: $white; + border-left-color: var(--introjs-arrow-color); } .introjs-arrow.right-bottom { bottom: 10px; right: -10px; - border-left-color: $white; + border-left-color: var(--introjs-arrow-color); } .introjs-arrow.bottom { bottom: -10px; left: 10px; - border-top-color: $white; + border-top-color: var(--introjs-arrow-color); } .introjs-arrow.bottom-right { bottom: -10px; right: 10px; - border-top-color: $white; + border-top-color: var(--introjs-arrow-color); } .introjs-arrow.bottom-middle { bottom: -10px; left: 50%; margin-left: -5px; - border-top-color: $white; + border-top-color: var(--introjs-arrow-color); } .introjs-arrow.left { left: -10px; top: 10px; - border-right-color: $white; + border-right-color: var(--introjs-arrow-color); } .introjs-arrow.left-bottom { left: -10px; bottom: 10px; - border-right-color: $white; + border-right-color: var(--introjs-arrow-color); } .introjs-tooltip { box-sizing: content-box; position: absolute; visibility: visible; - background-color: $white; + background-color: var(--introjs-bg); min-width: 250px; max-width: 300px; border-radius: 5px; @@ -177,6 +215,7 @@ tr.introjs-showElement { .introjs-tooltiptext { padding: 20px; + color: var(--introjs-color); } .introjs-dontShowAgain { @@ -199,7 +238,7 @@ tr.introjs-showElement { font-weight: normal; margin: 0 0 0 5px; padding: 0; - background-color: $white; + background-color: var(--introjs-bg); color: $black600; user-select: none; } @@ -212,6 +251,7 @@ tr.introjs-showElement { padding: 0; font-weight: 700; line-height: 1.5; + color: var(--introjs-color); } .introjs-tooltip-header { @@ -220,6 +260,7 @@ tr.introjs-showElement { padding-right: 20px; padding-top: 10px; min-height: 1.5em; + color: var(--introjs-color); } .introjs-tooltipbuttons { @@ -246,13 +287,13 @@ tr.introjs-showElement { border: 1px solid $black400; text-decoration: none; - text-shadow: 1px 1px 0 $white; + text-shadow: 1px 1px 0 var(--introjs-btn-textShadow); font-size: 14px; - color: $black800; + color: var(--introjs-btn-color); white-space: nowrap; cursor: pointer; outline: none; - background-color: $black100; + background-color: var(--introjs-btn-bg); border-radius: 0.2em; zoom: 1; display: inline; @@ -260,26 +301,26 @@ tr.introjs-showElement { &:hover { outline: none; text-decoration: none; - border-color: $black500; - background-color: $black300; - color: $black900; + border-color: var(--introjs-btn-border); + background-color: var(--introjs-btn-hover-bg); + color: var(--introjs-btn-hover-color);; } &:focus { outline: none; text-decoration: none; - background-color: $black200; + background-color: var(--introjs-btn-focus-bg); box-shadow: 0 0 0 0.2rem rgba($black500, 0.5); - border: 1px solid $black600; - color: $black900; + border: 1px solid var(--introjs-btn-border); + color: var(--introjs-btn-focus-color); } &:active { outline: none; text-decoration: none; - background-color: $black300; - border-color: $black500; - color: $black900; + background-color: var(--introjs-btn-active-bg); + border-color: var(--introjs-btn-active-border); + color: var(--introjs-btn-active-color); } &::-moz-focus-inner { @@ -298,7 +339,7 @@ tr.introjs-showElement { height: 45px; line-height: 45px; - color: $black600; + color: var(--introjs-btn-color); font-size: 22px; cursor: pointer; font-weight: bold; @@ -307,7 +348,7 @@ tr.introjs-showElement { &:hover, &:focus { - color: $black900; + color: var(--introjs-btn-focus-color); outline: none; text-decoration: none; } @@ -315,28 +356,30 @@ tr.introjs-showElement { .introjs-prevbutton { float: left; + color: var(--introjs-btn-color); } .introjs-nextbutton { float: right; + color: var(--introjs-btn-color); } .introjs-disabled { - color: $black500; - border-color: $black400; + color: var(--introjs-btn-color); + border-color: var(--introjs-btn-border); box-shadow: none; cursor: default; - background-color: $black100; + background-color: var(--introjs-btn-bg); background-image: none; text-decoration: none; &:hover, &:focus { - color: $black500; - border-color: $black400; + color: var(--introjs-btn-focus-color); + border-color: var(--introjs-btn-border); box-shadow: none; cursor: default; - background-color: $black100; + background-color: var(--introjs-btn-focus-bg); background-image: none; text-decoration: none; }