From 0a2aab86b6cc034d2565737d0188c44d9e86ac43 Mon Sep 17 00:00:00 2001 From: Mahdi Hosseinzadeh Date: Wed, 9 Mar 2022 21:21:04 +0330 Subject: [PATCH] Extract a function --- test/theme-switch.test.js | 24 ++++++++++++++++++++++++ theme-switch.js | 24 ++++++++++++++---------- theme-switch.min.js | 2 +- theme-switch.min.js.map | 2 +- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/test/theme-switch.test.js b/test/theme-switch.test.js index 9b67b9a..073ba7f 100644 --- a/test/theme-switch.test.js +++ b/test/theme-switch.test.js @@ -189,6 +189,30 @@ test(`When user selected theme is auto, getInitialStateForIcon should return cor expect(getInitialStateForIcon()).toEqual([10, 0, 33, 0]); }); +test(`When current theme is light and new theme is dark, createEvent should create event with proper details`, () => { + const instance = new themeSwitchClass(); + const result = instance.createEvent(THEME_LIGHT, THEME_DARK); + expect(result.detail.originId).toBe(instance.identifier); + expect(result.detail.oldState).toBe(THEME_LIGHT); + expect(result.detail.newState).toBe(THEME_DARK); +}); + +test(`When current theme is dark and new theme is auto, createEvent should create event with proper details`, () => { + const instance = new themeSwitchClass(); + const result = instance.createEvent(THEME_DARK, THEME_AUTO); + expect(result.detail.originId).toBe(instance.identifier); + expect(result.detail.oldState).toBe(THEME_DARK); + expect(result.detail.newState).toBe(THEME_AUTO); +}); + +test(`When current theme is auto and new theme is light, createEvent should create event with proper details`, () => { + const instance = new themeSwitchClass(); + const result = instance.createEvent(THEME_AUTO, THEME_LIGHT); + expect(result.detail.originId).toBe(instance.identifier); + expect(result.detail.oldState).toBe(THEME_AUTO); + expect(result.detail.newState).toBe(THEME_LIGHT); +}); + describe("Screenshot tests", () => { // Increase the timeout of executing all the test suit from // the default 5000 to a greater value to run fine on CI diff --git a/theme-switch.js b/theme-switch.js index a65f4c0..29057a4 100644 --- a/theme-switch.js +++ b/theme-switch.js @@ -133,16 +133,7 @@ class ThemeSwitchElement extends HTMLElement { this.toggleTheme(oldTheme); const newTheme = getUserThemeSelection(); // See https://stackoverflow.com/a/53804106/8583692 - const event = new CustomEvent(CUSTOM_EVENT_NAME, { - detail: { - originId: this.identifier, - oldState: oldTheme, - newState: newTheme - }, - bubbles: true, - composed: true, - cancelable: false - }); + const event = this.createEvent(oldTheme, newTheme); this.dispatchEvent(event); }); @@ -160,6 +151,19 @@ class ThemeSwitchElement extends HTMLElement { this.shadowRoot.append(style); } + createEvent(oldTheme, newTheme) { + return new CustomEvent(CUSTOM_EVENT_NAME, { + detail: { + originId: this.identifier, + oldState: oldTheme, + newState: newTheme + }, + bubbles: true, + composed: true, + cancelable: false + }); + } + // See https://stackoverflow.com/q/48316611 toggleTheme(currentTheme) { if (currentTheme === THEME_AUTO) { diff --git a/theme-switch.min.js b/theme-switch.min.js index 1f16b02..9eed9bf 100644 --- a/theme-switch.min.js +++ b/theme-switch.min.js @@ -1,4 +1,4 @@ -;(function(){"use strict";function b(d,a,b){if(a in d){Object.defineProperty(d,a,{value:b,enumerable:true,configurable:true,writable:true})}else{d[a]=b}return d}const c="theme-switch";const d=24;const e="#000";const f="theme";const g="auto";const h="dark";const i="light";const j=[g,h,i];const k=i;const l="data-theme";const m="(prefers-color-scheme: dark)";const n="themeToggle";const o=[10,0,33,0];const p=[10,0,20,1];const q=[5,1,33,1];class r extends HTMLElement{constructor(){super();b(this,"shadowRoot",void 0);b(this,"identifier",r.counter++);this.shadowRoot=this.attachShadow({mode:"open"});this.shadowRoot.innerHTML=s(...x());this.shadowRoot.host.addEventListener("click",()=>{const d=v();this.toggleTheme(d);const a=v();const b=new CustomEvent(n,{detail:{originId:this.identifier,oldState:d,newState:a},bubbles:true,composed:true,cancelable:false});this.dispatchEvent(b)});document.addEventListener(n,b=>{if(b.detail.originId!==this.identifier){this.adaptToTheme()}});const c=document.createElement("style");c.textContent=t();this.shadowRoot.append(c)}toggleTheme(b){if(b===g){localStorage.setItem(f,i);this.animateThemeButtonIconToLight()}else if(b===h){localStorage.setItem(f,g);this.animateThemeButtonIconToAuto()}else{localStorage.setItem(f,h);this.animateThemeButtonIconToDark()}u()}adaptToTheme(){const b=v();if(b===g){this.animateThemeButtonIconToAuto()}else if(b===h){this.animateThemeButtonIconToDark()}else{this.animateThemeButtonIconToLight()}}animateThemeButtonIconToLight(){this.shadowRoot.getElementById("letter-anim-hide").beginElement();this.shadowRoot.getElementById("core-anim-shrink").beginElement();this.shadowRoot.getElementById("rays-anim-rotate").beginElement();this.shadowRoot.getElementById("rays-anim-show").beginElement()}animateThemeButtonIconToAuto(){this.shadowRoot.getElementById("eclipse-anim-go").beginElement();this.shadowRoot.getElementById("letter-anim-show").beginElement()}animateThemeButtonIconToDark(){this.shadowRoot.getElementById("rays-anim-hide").beginElement();this.shadowRoot.getElementById("core-anim-enlarge").beginElement();this.shadowRoot.getElementById("eclipse-anim-come").beginElement()}}b(r,"counter",0);function s(e,a,b,c){return` +;(function(){"use strict";function b(d,a,b){if(a in d){Object.defineProperty(d,a,{value:b,enumerable:true,configurable:true,writable:true})}else{d[a]=b}return d}const c="theme-switch";const d=24;const e="#000";const f="theme";const g="auto";const h="dark";const i="light";const j=[g,h,i];const k=i;const l="data-theme";const m="(prefers-color-scheme: dark)";const n="themeToggle";const o=[10,0,33,0];const p=[10,0,20,1];const q=[5,1,33,1];class r extends HTMLElement{constructor(){super();b(this,"shadowRoot",void 0);b(this,"identifier",r.counter++);this.shadowRoot=this.attachShadow({mode:"open"});this.shadowRoot.innerHTML=s(...x());this.shadowRoot.host.addEventListener("click",()=>{const d=v();this.toggleTheme(d);const a=v();const b=this.createEvent(d,a);this.dispatchEvent(b)});document.addEventListener(n,b=>{if(b.detail.originId!==this.identifier){this.adaptToTheme()}});const c=document.createElement("style");c.textContent=t();this.shadowRoot.append(c)}createEvent(c,a){return new CustomEvent(n,{detail:{originId:this.identifier,oldState:c,newState:a},bubbles:true,composed:true,cancelable:false})}toggleTheme(b){if(b===g){localStorage.setItem(f,i);this.animateThemeButtonIconToLight()}else if(b===h){localStorage.setItem(f,g);this.animateThemeButtonIconToAuto()}else{localStorage.setItem(f,h);this.animateThemeButtonIconToDark()}u()}adaptToTheme(){const b=v();if(b===g){this.animateThemeButtonIconToAuto()}else if(b===h){this.animateThemeButtonIconToDark()}else{this.animateThemeButtonIconToLight()}}animateThemeButtonIconToLight(){this.shadowRoot.getElementById("letter-anim-hide").beginElement();this.shadowRoot.getElementById("core-anim-shrink").beginElement();this.shadowRoot.getElementById("rays-anim-rotate").beginElement();this.shadowRoot.getElementById("rays-anim-show").beginElement()}animateThemeButtonIconToAuto(){this.shadowRoot.getElementById("eclipse-anim-go").beginElement();this.shadowRoot.getElementById("letter-anim-show").beginElement()}animateThemeButtonIconToDark(){this.shadowRoot.getElementById("rays-anim-hide").beginElement();this.shadowRoot.getElementById("core-anim-enlarge").beginElement();this.shadowRoot.getElementById("eclipse-anim-come").beginElement()}}b(r,"counter",0);function s(e,a,b,c){return` \r\n `;\r\n}\r\n\r\n// language=css\r\nfunction generateStyle() {\r\n return `\r\n /* :host === the host element of the shadow === */\r\n /* See https://developer.mozilla.org/en-US/docs/Web/CSS/:host */\r\n :host {\r\n display: flex;\r\n width: ${ICON_SIZE}px;\r\n aspect-ratio: 1 / 1;\r\n /* This is for when the element has padding */\r\n cursor: pointer;\r\n }\r\n\r\n :host([hidden]) { display: none; }\r\n\r\n button {\r\n padding: 0;\r\n border: none;\r\n background: transparent;\r\n display: flex;\r\n /* The host element also has its cursor set */\r\n cursor: pointer;\r\n }\r\n\r\n #circle { fill: var(--theme-switch-icon-color, ${ICON_COLOR}); }\r\n\r\n #rays { stroke: var(--theme-switch-icon-color, ${ICON_COLOR}); }\r\n `;\r\n}\r\n\r\nupdateTheme();\r\nwindow.customElements.define(ELEMENT_NAME, ThemeSwitchElement);\r\nwindow\r\n .matchMedia(COLOR_SCHEME_DARK)\r\n .addEventListener(\"change\", updateTheme);\r\n\r\nfunction updateTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) theme = getSystemTheme();\r\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\r\n}\r\n\r\nfunction getUserThemeSelection() {\r\n const userSelection = localStorage.getItem(THEME_KEY);\r\n return THEME_VALUES.includes(userSelection) ? userSelection : THEME_DEFAULT;\r\n}\r\n\r\nfunction getSystemTheme() {\r\n const isDark = window.matchMedia(COLOR_SCHEME_DARK).matches;\r\n return isDark ? THEME_DARK : THEME_LIGHT;\r\n}\r\n\r\nfunction getInitialStateForIcon() {\r\n const theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n return ICON_INITIAL_STATE_FOR_AUTO;\r\n } else if (theme === THEME_DARK) {\r\n return ICON_INITIAL_STATE_FOR_DARK;\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n return ICON_INITIAL_STATE_FOR_LIGHT;\r\n }\r\n}\r\n\r\n// Export for tests run by npm (no longer needed; kept for future reference)\r\n// See https://stackoverflow.com/q/63752210/8583692\r\n// and https://stackoverflow.com/a/54680602/8583692\r\n// and https://stackoverflow.com/q/43042889/8583692\r\n// and https://stackoverflow.com/a/1984728/8583692\r\n// if (typeof module !== \"undefined\") {\r\n// module.exports = {\r\n// updateTheme,\r\n// toggleTheme,\r\n// getSystemTheme,\r\n// getInitialStateForIcon,\r\n// animateThemeButtonIconToAuto,\r\n// animateThemeButtonIconToDark,\r\n// animateThemeButtonIconToLight\r\n// };\r\n// }\r\n"]} \ No newline at end of file +{"version":3,"sources":["theme-switch.js"],"names":[],"mappings":"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","file":"theme-switch.min.js","sourcesContent":["/*\r\n* NOTE: Do not use this script as an ES6 module.\r\n* ES6 modules are deferred and we don't want that because\r\n* we want the user previous theme selection to be applied\r\n* as soon as possible (before the page is rendered).\r\n* */\r\n\r\n/*\r\n* There are two types of modules mostly used in JavaScript.\r\n* One is created by Node.js and is used inside the Node environment\r\n* and has been available for a long time. It is called CommonJS.\r\n* Another is the standard native JavaScript modules introduced in ES6.\r\n*\r\n* The Node variant (CommonJS) uses `module.exports` (or simply `exports`) and\r\n* `require()` to export and import scripts, functions, variables, etc.\r\n* Browsers do not know about `exports` or `require` functions and throw error\r\n* because they are objects and functions created just in Node environment and set globally.\r\n* If you want to use this type of module in browsers, you should bundle the files\r\n* (merge all of them into a single JS file which eliminates the need for exports and require)\r\n* with tools like babel, webpack, rollup, etc.\r\n*\r\n* ES6 modules use `export` and `import` keywords for the same purpose.\r\n*\r\n* Example Node modules:\r\n*\r\n* my-calculator.js\r\n* const PI = 3.14;\r\n* function calculate() {}\r\n* modules.exports.calculate = calculate;\r\n* modules.exports.PI = PI;\r\n*\r\n* main.js\r\n* const calculator = require(\"my-calculator\");\r\n* const perimeter = 2 * calculator.PI;\r\n* const result = calculator.calculate();\r\n*\r\n* Example ES6 modules:\r\n*\r\n* my-calculator.js\r\n* export const PI = 3.14;\r\n* export function calculate() {}\r\n*\r\n* main.js\r\n* import { calculate, PI } from \"my-calculator.js\";\r\n* const perimeter = 2 * PI;\r\n* const result = calculate();\r\n*\r\n* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules\r\n* and https://stackoverflow.com/a/9901097/8583692\r\n* */\r\n\r\n/*\r\n* Minify the script either through command line with babel:\r\n* - babel theme-switch.js --source-maps --out-file result.min.js\r\n* or with babel-minify (also has an alias called minify) which is useful if\r\n* you don't already use babel (as a preset) or want to run minification standalone.\r\n* Note that it does not take into account babel.config.json settings.\r\n* - babel-minify (or minify) theme-switch.js --mangle --no-comments --out-file result.min.js`\r\n* Or automate it with IntelliJ file watcher\r\n* - babel\r\n* + program: $ProjectFileDir$\\node_modules\\.bin\\babel\r\n* + arguments: $FilePathRelativeToProjectRoot$ --out-file $FileNameWithoutExtension$.min.js --source-maps\r\n* Note that setting \"sourceMaps\": \"true\" in babel.config.json does not work because of\r\n* this bug: https://github.com/babel/babel/issues/5261 (\"sourceMaps\": \"inline\" works, however)\r\n* - UglifyJS\r\n* - YUI compressor (seems to be deprecated and removed in newer versions of IntelliJ)\r\n*\r\n* Babel preset-env inserts a semicolon at the start of the minified file.\r\n* See why: https://stackoverflow.com/q/1873983/8583692\r\n* */\r\n\r\n// TODO: extract Jest configuration to a jest.config.js file\r\n// TODO: Add an attribute so the user can define key name stored in localstorage\r\n\r\n/*\r\n* NOTE: To avoid name collisions if another script declares variables or functions with the same name\r\n* as ours (i.e. defining them in the global scope) and browsers complaining about identifiers\r\n* being redeclared, we wrap all our code in a closure or IIFE (sort of creating a namespace for it).\r\n* ES6 now supports block scope as well (simply wrapping the whole code in {}).\r\n* I am now using the babel-plugin-iife-wrap plugin to wrap the whole result (minified)\r\n* code in an IIFE.\r\n* For examples, see these libraries:\r\n* - https://github.com/highlightjs/highlight.js/blob/main/src/highlight.js\r\n* - https://github.com/jashkenas/underscore/blob/master/underscore.js\r\n* See\r\n* - https://www.w3schools.com/js/js_scope.asp\r\n* - https://github.com/jhnns/rewire/issues/136#issuecomment-380829197\r\n* - https://stackoverflow.com/a/32750216/8583692\r\n* - https://stackoverflow.com/q/8228281/8583692\r\n* - https://stackoverflow.com/q/881515/8583692\r\n* - https://stackoverflow.com/q/39388777/8583692\r\n* - https://stackoverflow.com/a/47207686/8583692\r\n* We could also do something like these libraries:\r\n* - https://github.com/juliangarnier/anime/blob/master/build.js\r\n* - https://github.com/mrdoob/three.js/\r\n* - https://github.com/moment/moment\r\n* - https://github.com/floating-ui/floating-ui\r\n* */\r\n\r\nconst ELEMENT_NAME = \"theme-switch\";\r\nconst ICON_SIZE = 24 /* px */;\r\nconst ICON_COLOR = \"#000\";\r\nconst THEME_KEY = \"theme\";\r\nconst THEME_AUTO = \"auto\";\r\nconst THEME_DARK = \"dark\";\r\nconst THEME_LIGHT = \"light\";\r\nconst THEME_VALUES = [THEME_AUTO, THEME_DARK, THEME_LIGHT];\r\nconst THEME_DEFAULT = THEME_LIGHT;\r\nconst THEME_ATTRIBUTE = \"data-theme\";\r\nconst COLOR_SCHEME_DARK = \"(prefers-color-scheme: dark)\";\r\nconst CUSTOM_EVENT_NAME = \"themeToggle\";\r\n// circleRadius, raysOpacity, eclipseCenterX, letterOffset\r\nconst ICON_INITIAL_STATE_FOR_AUTO = [10, 0, 33, 0];\r\nconst ICON_INITIAL_STATE_FOR_DARK = [10, 0, 20, 1];\r\nconst ICON_INITIAL_STATE_FOR_LIGHT = [5, 1, 33, 1];\r\n\r\nclass ThemeSwitchElement extends HTMLElement {\r\n shadowRoot;\r\n static counter = 0; // See https://stackoverflow.com/a/43116254/8583692\r\n identifier = ThemeSwitchElement.counter++;\r\n\r\n constructor() {\r\n super();\r\n\r\n // See https://stackoverflow.com/q/2305654/8583692\r\n this.shadowRoot = this.attachShadow({ mode: \"open\" });\r\n this.shadowRoot.innerHTML = generateIcon(...getInitialStateForIcon());\r\n\r\n // Add the click listener to the top-most parent (the custom element itself)\r\n // so the padding etc. on the element be also clickable\r\n this.shadowRoot.host.addEventListener(\"click\", () => {\r\n const oldTheme = getUserThemeSelection();\r\n this.toggleTheme(oldTheme);\r\n const newTheme = getUserThemeSelection();\r\n // See https://stackoverflow.com/a/53804106/8583692\r\n const event = this.createEvent(oldTheme, newTheme);\r\n this.dispatchEvent(event);\r\n });\r\n\r\n // If another theme switch in page toggled, update my icon too\r\n document.addEventListener(CUSTOM_EVENT_NAME, event => {\r\n if (event.detail.originId !== this.identifier) {\r\n this.adaptToTheme();\r\n }\r\n });\r\n\r\n // Create some CSS to apply to the shadow DOM\r\n // See https://css-tricks.com/styling-a-web-component/\r\n const style = document.createElement(\"style\");\r\n style.textContent = generateStyle();\r\n this.shadowRoot.append(style);\r\n\r\n // console.log(\"%cThis is a sample message\", `background: #234; padding: 8px;`);\r\n // console.time(\"label\");\r\n // console.timeEnd(\"label\");\r\n }\r\n\r\n createEvent(oldTheme, newTheme) {\r\n return new CustomEvent(CUSTOM_EVENT_NAME, {\r\n detail: {\r\n originId: this.identifier,\r\n oldState: oldTheme,\r\n newState: newTheme\r\n },\r\n bubbles: true,\r\n composed: true,\r\n cancelable: false\r\n });\r\n }\r\n\r\n // See https://stackoverflow.com/q/48316611\r\n toggleTheme(currentTheme) {\r\n if (currentTheme === THEME_AUTO) {\r\n localStorage.setItem(THEME_KEY, THEME_LIGHT);\r\n this.animateThemeButtonIconToLight();\r\n } else if (currentTheme === THEME_DARK) {\r\n localStorage.setItem(THEME_KEY, THEME_AUTO);\r\n this.animateThemeButtonIconToAuto();\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n localStorage.setItem(THEME_KEY, THEME_DARK);\r\n this.animateThemeButtonIconToDark();\r\n }\r\n updateTheme();\r\n }\r\n\r\n adaptToTheme() {\r\n const theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n this.animateThemeButtonIconToAuto();\r\n } else if (theme === THEME_DARK) {\r\n this.animateThemeButtonIconToDark();\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n this.animateThemeButtonIconToLight();\r\n }\r\n }\r\n\r\n animateThemeButtonIconToLight() {\r\n this.shadowRoot.getElementById(\"letter-anim-hide\").beginElement();\r\n this.shadowRoot.getElementById(\"core-anim-shrink\").beginElement();\r\n this.shadowRoot.getElementById(\"rays-anim-rotate\").beginElement();\r\n this.shadowRoot.getElementById(\"rays-anim-show\").beginElement();\r\n }\r\n\r\n animateThemeButtonIconToAuto() {\r\n this.shadowRoot.getElementById(\"eclipse-anim-go\").beginElement();\r\n this.shadowRoot.getElementById(\"letter-anim-show\").beginElement();\r\n }\r\n\r\n animateThemeButtonIconToDark() {\r\n this.shadowRoot.getElementById(\"rays-anim-hide\").beginElement();\r\n this.shadowRoot.getElementById(\"core-anim-enlarge\").beginElement();\r\n this.shadowRoot.getElementById(\"eclipse-anim-come\").beginElement();\r\n }\r\n}\r\n\r\n// language=html\r\nfunction generateIcon(circleRadius, raysOpacity, eclipseCenterX, letterOffset) {\r\n return `\r\n \r\n \r\n `;\r\n}\r\n\r\n// language=css\r\nfunction generateStyle() {\r\n return `\r\n /* :host === the host element of the shadow === */\r\n /* See https://developer.mozilla.org/en-US/docs/Web/CSS/:host */\r\n :host {\r\n display: flex;\r\n width: ${ICON_SIZE}px;\r\n aspect-ratio: 1 / 1;\r\n /* This is for when the element has padding */\r\n cursor: pointer;\r\n }\r\n\r\n :host([hidden]) { display: none; }\r\n\r\n button {\r\n padding: 0;\r\n border: none;\r\n background: transparent;\r\n display: flex;\r\n /* The host element also has its cursor set */\r\n cursor: pointer;\r\n }\r\n\r\n #circle { fill: var(--theme-switch-icon-color, ${ICON_COLOR}); }\r\n\r\n #rays { stroke: var(--theme-switch-icon-color, ${ICON_COLOR}); }\r\n `;\r\n}\r\n\r\nupdateTheme();\r\nwindow.customElements.define(ELEMENT_NAME, ThemeSwitchElement);\r\nwindow\r\n .matchMedia(COLOR_SCHEME_DARK)\r\n .addEventListener(\"change\", updateTheme);\r\n\r\nfunction updateTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) theme = getSystemTheme();\r\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\r\n}\r\n\r\nfunction getUserThemeSelection() {\r\n const userSelection = localStorage.getItem(THEME_KEY);\r\n return THEME_VALUES.includes(userSelection) ? userSelection : THEME_DEFAULT;\r\n}\r\n\r\nfunction getSystemTheme() {\r\n const isDark = window.matchMedia(COLOR_SCHEME_DARK).matches;\r\n return isDark ? THEME_DARK : THEME_LIGHT;\r\n}\r\n\r\nfunction getInitialStateForIcon() {\r\n const theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n return ICON_INITIAL_STATE_FOR_AUTO;\r\n } else if (theme === THEME_DARK) {\r\n return ICON_INITIAL_STATE_FOR_DARK;\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n return ICON_INITIAL_STATE_FOR_LIGHT;\r\n }\r\n}\r\n\r\n// Export for tests run by npm (no longer needed; kept for future reference)\r\n// See https://stackoverflow.com/q/63752210/8583692\r\n// and https://stackoverflow.com/a/54680602/8583692\r\n// and https://stackoverflow.com/q/43042889/8583692\r\n// and https://stackoverflow.com/a/1984728/8583692\r\n// if (typeof module !== \"undefined\") {\r\n// module.exports = {\r\n// updateTheme,\r\n// toggleTheme,\r\n// getSystemTheme,\r\n// getInitialStateForIcon,\r\n// animateThemeButtonIconToAuto,\r\n// animateThemeButtonIconToDark,\r\n// animateThemeButtonIconToLight\r\n// };\r\n// }\r\n"]} \ No newline at end of file