# Source: alpha/uib-theme-changer.js

    // @ts-nocheck
    /** Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
     *
     * TODO:
     *   - Compact layout
     *   - Other attribs for controlling scheme, etc
     *   - Settings for accessibility
     *   - Reset button
     *   - Move event handlers to fns and remove them on disconnect
     *   - Add inbound uibuilder msg handler
     *   - Add hidden attrib to allow for just msg handling
     *   - Update documentation
     *
     * version See the class code
     *
     * References:
     *   - https://web.dev/building-a-color-scheme/
     *
     * See https://github.com/runem/web-component-analyzer#-how-to-document-your-components-using-jsdoc on how to document
     * Use `npx web-component-analyzer ./components/button-send.js` to create/update the documentation
     *     or paste into https://runem.github.io/web-component-analyzer/
     * Use `npx web-component-analyzer ./components/*.js --format vscode --outFile ./vscode-descriptors/ti-web-components.html-data.json`
     *     to generate/update vscode custom data files. See https://github.com/microsoft/vscode-custom-data/tree/main/samples/webcomponents
     *
     */
    /** Copyright (c) 2022-2025 Julian Knight (Totally Information)
     * https://it.knightnet.org.uk, https://github.com/TotallyInformation
     *
     * Licensed under the Apache License, Version 2.0 (the 'License');
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     * http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an 'AS IS' BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    const componentName = 'uib-theme-changer'
    const className = 'UibThemeChanger'

    const template = document.createElement('template')
    template.innerHTML = /*html*/`
        <style>
            :host {
                --size: 2rem;
                --w: 4rem;
                /* display: flex; */
                display: inline-block; /* default is inline */
                contain: content; /* performance boost */
                position: sticky;
                top: 0;
                background-color: var(--surface1);
                border: 1px solid var(--text3);
                border-radius: 0.5rem;
                margin: 0.2rem;
                padding: 0.5rem;
                color: var(--text2);
                background-clip: border-box;
                box-sizing: border-box;
                box-shadow: var(--shadow2);
                width: 100%;
            }
            * {
                vertical-align: top;
            }
            div {
                display: inline-block;
            }
            form {
                margin: .2rem 1rem;
                /* flex: 1 0 auto; */
                display: inline-block;
            }
            form > div {
                display: block;
            }
            .s-and-m {
                aspect-ratio: 1;
                background: none;
                color: var(--text3);
                border: none;
                padding: 0;
                inline-size: var(--size);
                block-size: var(--size);

                border-radius: 50%;

                cursor: pointer;
                touch-action: manipulation;
                -webkit-tap-highlight-color: transparent;
                outline-offset: 5px;
            }
            input[type=submit] {
                inline-size: var(--w);
                block-size: var(--size);
                margin-left: 1rem;
            }
            .sun-and-moon {
                inline-size: 100%;
                block-size: 100%;
                stroke-linecap: round;
            }
            .moon, .sun {
                fill: var(--text2);
            }
            .moon:hover, .sun:hover {
                fill: var(--text1);
            }

            /* @media (hover: none) {
                :host {
                    --size: 48px;
                    --w: 48px;
                }
            } */
        </style>
        <div id="s-and-m" name="color-scheme-toggle" onclick="this.getRootNode().host.evtClickToggle(event)"
                title="Toggles between light & dark color schemes"
                aria-label="auto" aria-live="polite">
            <svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
                <svg name="sun" class="sun" viewbox="0 0 20 20">
                    <path
                        fill-rule="evenodd"
                        d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
                        clip-rule="evenodd"
                    ></path>
                </svg>
                <svg name="moon" class="moon" viewbox="0 0 20 20">
                    <path
                        d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"
                    ></path>
                </svg>
                <svg class="divider" width='100%' height='100%' viewBox='0 0 20 20' preserveAspectRatio='none'>
                    <line x1="5" y1="5" x2="15" y2="15" vector-effect="non-scaling-stroke"
                        stroke-width="10%" stroke="currentColor" />
                </svg>
            </svg>
        </div>
        <input type="submit" value="Reset" onclick="this.getRootNode().host.evtClickReset(event)">
        <form>
            <div name="color-scheme-choose" onclick="this.getRootNode().host.evtClickChooser(event)">
                <b>Color scheme:</b>
                <label><input type="radio" name="input-color-scheme-choose" value="auto">&nbsp;auto</label>
                <label><input type="radio" name="input-color-scheme-choose" value="light">&nbsp;light</label>
                <label><input type="radio" name="input-color-scheme-choose" value="dark">&nbsp;dark</label>
            </div>

            <div onclick="this.getRootNode().host.evtClickContrast(event)">
                <b>Contrast:</b>
                <label><input type="radio" name="contrast" value="standard">&nbsp;standard</label>
                <label><input type="radio" name="contrast" value="more">&nbsp;more</label>
                <label><input type="radio" name="contrast" value="less">&nbsp;less</label>
            </div>

            <div>
                <label><b>Brand Hue angle:</b>
                <input name="brand-hue" type="range" min="0" max="360" step="1" value="200">
                <output>200</output></label>
            </div>

            <div title="30=Split Complementary, 60=Triadic, 150=Complementary">
                <label><b>Accent offset angle</b>
                <input name="accent-offset" type="range" min="0" max="360" step="1" value="30">
                <output>30</output></label>
            </div>
        </form>
    `

    /** Namespace
     * @namespace Alpha
     */

    /** A uibuilder for Node-RED Theme Changer component
     * @class
     * @augments HTMLElement
     * @description Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
     *
     * @element component-template
     * @memberOf Alpha
     *
     *
     * @element uib-theme-changer
     *
     * fires uib-theme-changer:construction - Document object event. evt.details contains the data
     * fires uib-theme-changer:connected - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
     * fires uib-theme-changer:disconnected - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
     * fires uib-theme-changer:attribChanged - When a watched attribute changes. `evt.details` contains the details of the change.
     * NOTE that listeners can be attached either to the `document` or to the specific element instance.
     *
     * @property {string} name - Optional. Will be used to synthesize an ID if no ID is provided.
     * attr {string} data-* - Optional. All data-* attributes are returned in the _meta prop as a _meta.data object.
     *
     * slot Container contents
     *
     * @csspart ??? - Uses the uib-styles.css uibuilder master for variables where available.
     */
    class UibThemeChanger extends HTMLElement {
        /** Component version */
        static version = '2024-09-21'

        //#region ---- Class Variables ----

        /** Is UIBUILDER for Node-RED loaded? */
        uib = !!window['uibuilder']
        /** Mini jQuery-like shadow dom selector (see constructor)
         * @type {function(string): Element}
         * @param {string} selector - A CSS selector to match the element within the shadow DOM.
         * @returns {Element} The first element that matches the specified selector.
         */
        $
        /** Mini jQuery-like shadow dom multi-selector (see constructor)
         * @type {function(string): NodeList}
         * @param {string} selector - A CSS selector to match the element within the shadow DOM.
         * @returns {NodeList} A STATIC list of all shadow dom elements that match the selector.
         */
        $$
        /** Holds a count of how many instances of this component are on the page that don't have their own id
         * Used to ensure a unique id if needing to add one dynamically
         */
        static _iCount = 0

        /** Runtime configuration settings */
        opts = {}

        /** Makes HTML attribute change watched
         * @returns {Array<string>} List of all of the html attribs (props) listened to
         */
        static get observedAttributes() {
            return [
                // Standard watched attributes:
                'inherit-style',
                // Other watched attributes:
            ]
        }

        /** Report the current component version string
         *  @returns {string} The component version string
         */
        get version() {
            return UibThemeChanger.version
        }

        /** Holds the uib theme settings for all pages in this domain (from/to localStorage) */
        uibThemeSettings = {}

        /** Standard _ui object to include in msgs */
        _ui = {
            type: componentName,
            event: undefined,
            id: undefined,
            name: undefined,
            data: undefined, // All of the data-* attributes as an object
        }

        /** What is the current scheme? 'light', 'dark' or 'auto' */
        scheme = undefined

        //#endregion ---- ---- ---- ----

        /** NB: Attributes not available here - use connectedCallback to reference */
        constructor() {
            super()

            this.attachShadow({ mode: 'open', delegatesFocus: true, })
                // Only append the template if code and style isolation is needed
                .append(template.content.cloneNode(true))

            // jQuery-like selectors but for the shadow. NB: Returns are STATIC not dynamic lists
            this.$ = this.shadowRoot?.querySelector.bind(this.shadowRoot)
            this.$$ = this.shadowRoot?.querySelectorAll.bind(this.shadowRoot)
        }

        /** Runs when an instance is added to the DOM */
        connectedCallback() {
            // Make sure instance has an ID. Create an id from name or calculation if needed
            if (!this.id) {
                if (!this.name) this.name = this.getAttribute('name')
                if (this.name) this.id = this.name.toLowerCase().replace(/\s/g, '_')
                else this.id = `uib-meta-${++UibThemeChanger._iCount}`
            }

            // Check again if UIBUILDER for Node-RED is loaded
            this.uib = !!window['uibuilder']

            // Apply parent styles from a stylesheet if required - only required if using an applied template
            if (this.hasAttribute('inherit-style')) {
                const styleUrl = this.getAttribute('inherit-style')
                this.doInheritStyles(styleUrl)
            }

            if ( !getComputedStyle(this).getPropertyValue('--uib-css').includes('uib-brand') ) { // eslint-disable-line @stylistic/newline-per-chained-call
                console.warn('[uib-theme-changer] WARNING: It appears that you are not using uibuilder\'s uib-brand.css stylesheet. This component may not work as expected.')
            }

            // Try to retrieve theme settings for this page
            try {
                this.uibThemeSettings = JSON.parse(localStorage.getItem('uibThemeSettings')) || this.uibThemeSettings
            } catch (e) {}
            if ( !this.uibThemeSettings[window.location.pathname] ) this.uibThemeSettings[window.location.pathname] = {}

            const docRoot = document.documentElement

            // TODO: Replace fns with named fns so that listeners can be removed
            this.$('[name=brand-hue]').addEventListener('change', function(evt) {
                docRoot.style.setProperty('--brand-hue', evt.target.value)
            })
            this.$('[name=brand-hue]').addEventListener('input', function(evt) {
                this.nextElementSibling.value = this.value
            })

            this.$('[name=accent-offset]').addEventListener('change', function(evt) {
                docRoot.style.setProperty('--accent-offset', evt.target.value)
            })
            this.$('[name=accent-offset]').addEventListener('input', function(evt) {
                this.nextElementSibling.value = this.value
            })

            //#region --- set contrast ---

            // If contrast is manually set, remove saved setting for this page (not needed)
            if ( docRoot.classList.contains('standard') || docRoot.classList.contains('more')  || docRoot.classList.contains('less')) {
                try {
                    delete this.uibThemeSettings[window.location.pathname].contrast
                    localStorage.setItem('uibThemeSettings', JSON.stringify(this.uibThemeSettings))
                } catch (e) {}
                this.shadowRoot.querySelector(`input[value=${docRoot.classList[0]}][name=contrast]`).checked = true
            } else if ( this.uibThemeSettings[window.location.pathname] && this.uibThemeSettings[window.location.pathname].contrast ) {
                // not manually set but does have a saved page setting
                this.shadowRoot.querySelector(`input[value=${this.uibThemeSettings[window.location.pathname].contrast}][name=contrast]`).checked = true
                this.evtClickContrast({ target: { name: 'contrast', value: this.uibThemeSettings[window.location.pathname].contrast, }, })
            } else {
                this.shadowRoot.querySelector('input[value=standard][name=contrast]').checked = true
            }

            //#endregion --- --- ---

            //#region --- set theme ---

            // If theme is manually set, remove saved setting for this page (not needed)
            if ( docRoot.classList.contains('light') || docRoot.classList.contains('dark')  || docRoot.classList.contains('auto')) {
                try {
                    delete this.uibThemeSettings[window.location.pathname].theme
                    localStorage.setItem('uibThemeSettings', JSON.stringify(this.uibThemeSettings))
                } catch (e) {}
                this.shadowRoot.querySelector(`input[value=${docRoot.classList[0]}][name=input-color-scheme-choose]`).checked = true
            } else if ( this.uibThemeSettings[window.location.pathname] && this.uibThemeSettings[window.location.pathname].theme ) {
                // not manually set but does have a saved page setting
                this.shadowRoot.querySelector(`input[value=${this.uibThemeSettings[window.location.pathname].theme}][name=input-color-scheme-choose]`).checked = true
                this.setTheme(this.uibThemeSettings[window.location.pathname].theme)
            } else {
                this.shadowRoot.querySelector('input[value=auto][name=input-color-scheme-choose]').checked = true
            }

            //#endregion --- --- ---

            // OPTIONAL. Listen for a uibuilder msg that is targetted at this instance of the component
            if (this.uib) document.addEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler.bind(this) )

            // Keep at end. Let everyone know that a new instance of the component has been connected
            this.dispatchEvent(new CustomEvent('uib-theme-changer:connected', {
                bubbles: true,
                composed: true,
                detail: {
                    id: this.id,
                    name: this.name,
                },
            } ) )
        }

        /** Runs when an instance is removed from the DOM */
        disconnectedCallback() {
            // @ts-ignore Remove optional uibuilder event listener
            document.removeEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler )

            // Keep at end. Let everyone know that an instance of the component has been disconnected
            this.dispatchEvent(new CustomEvent('uib-theme-changer:disconnected', {
                bubbles: true,
                composed: true,
                detail: {
                    id: this.id,
                    name: this.name,
                },
            } ) )
        }

        /** Runs when an observed attribute changes - Note: values are always strings
         * @param {string} attrib Name of watched attribute that has changed
         * @param {string} oldVal The previous attribute value
         * @param {string} newVal The new attribute value
         */
        attributeChangedCallback(attrib, oldVal, newVal) {
            // Don't bother if the new value same as old
            if (oldVal === newVal) return
            // Create a property from the value - WARN: Be careful with name clashes
            this[attrib] = newVal

            // Add other dynamic attribute processing here.
            // If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle

            // Keep at end. Let everyone know that an attribute has changed for this instance of the component
            document.dispatchEvent(new CustomEvent('uib-theme-changer:attribChanged', {
                bubbles: true,
                composed: true,
                detail: {
                    id: this.id,
                    name: this.name,
                    attribute: attrib,
                    newVal: newVal,
                    oldVal: oldVal,
                },
            }))
        }

        /** Optionally apply an external linked style sheet (called from connectedCallback)
         * @param {*} url The URL for the linked style sheet
         */
        async doInheritStyles(url) {
            if (!url) url = './index.css'

            const linkEl = document.createElement('link')
            linkEl.setAttribute('type', 'text/css')
            linkEl.setAttribute('rel', 'stylesheet')
            linkEl.setAttribute('href', url)
            // @ts-ignore
            this.shadowRoot.appendChild(linkEl)

            console.info(`[state-timeline] Inherit style requested. Loading: "${url}"`)
        }

        /** OPTIONAL. Update runtime configuration, return complete config
         * @param {object|undefined} config If present, partial or full set of options. If undefined, fn returns the current full option settings
         * @returns {object} The current full option settings
         */
        config(config) {
            // Merge config but ensure that default states always present
            // if (config) this.opts = { ...this.opts, ...config }
            if (config) this.opts = UibThemeChanger.deepAssign(this.opts, config)
            return this.opts
        }

        /** Utility object deep merge fn
         * @param {object} target Merge target object
         * @param  {...object} sources 1 or more source objects to merge
         * @returns {object} Deep merged object
         */
        static deepAssign(target, ...sources) {
            for (let source of sources) { // eslint-disable-line prefer-const
                for (let k in source) { // eslint-disable-line prefer-const
                    const vs = source[k]
                    const vt = target[k]
                    if (Object(vs) == vs && Object(vt) === vt) {
                        target[k] = UibThemeChanger.deepAssign(vt, vs)
                        continue
                    }
                    target[k] = source[k]
                }
            }
            return target
        }

        /** Handle a `uibuilder:msg:_ui:update:${this.id}` custom event
         * @param {CustomEvent} evt uibuilder `uibuilder:msg:_ui:update:${this.id}` custom event evt.details contains the data
         */
        _uibMsgHandler(evt) {
            // If there is a payload, we want to replace the slot - easiest done from the light DOM
            // if ( evt['detail'].payload ) {
            //     const el = document.getElementById(this.id)
            //     el.innerHTML = evt['detail'].payload
            // }
            // If there is a payload, we want to replace the VALUE
            // if ( evt['detail'].payload ) {
            //     const el = this.shadowRoot.getElementById('value')
            //     el.innerHTML = evt['detail'].payload
            // }
        }

        /**
         *
         * @param {*} theme _
         * @returns {string} _
         */
        setTheme(theme) {
            const $ = this.shadowRoot.querySelector.bind(this.shadowRoot)
            const docRoot = document.documentElement
            switch (theme) {
                case 'dark': {
                    this.scheme = 'dark'
                    docRoot.classList.remove('light')
                    docRoot.classList.add('dark')
                    $('.sun').style.opacity = 0
                    $('.moon').style.opacity = 1
                    $('.divider').style.opacity = 0
                    break
                }
                case 'light': {
                    this.scheme = 'light'
                    docRoot.classList.remove('dark')
                    docRoot.classList.add('light')
                    $('.sun').style.opacity = 1
                    $('.moon').style.opacity = 0
                    $('.divider').style.opacity = 0
                    break
                }
                case 'auto': {
                    this.scheme = 'auto'
                    docRoot.classList.remove('light')
                    docRoot.classList.remove('dark')
                    $('.sun').style.opacity = 1
                    $('.moon').style.opacity = 1
                    $('.divider').style.opacity = 1
                    break
                }
                case 'none':
                default: {
                    this.scheme = undefined
                    docRoot.classList.remove('light')
                    docRoot.classList.remove('dark')
                    docRoot.classList.remove('auto')
                    $('.sun').style.opacity = 1
                    $('.moon').style.opacity = 1
                    $('.divider').style.opacity = 1
                    break
                }
            }
            return theme
        }

        /** TODO Handle the icon
         * @param {MouseEvent} evt _
         */
        evtClickToggle(evt) {
            console.log('icon click: ', evt.target.tagName)
        }

        /** Handle the light/dark theme chooser. Override contrast css variables and set appropriate class on html
         * @param {MouseEvent} evt _
         */
        evtClickChooser(evt) {
            if (evt.target.name !== 'input-color-scheme-choose') return

            try {
                this.uibThemeSettings[window.location.pathname].theme = evt.target.value
                localStorage.setItem('uibThemeSettings', JSON.stringify(this.uibThemeSettings))
            } catch (e) {}

            // TODO: Consider moving to a getter/setter
            this.setTheme(evt.target.value)
        }

        /** Handle reset button. Override contrast css variables and set appropriate class on html
         * @param {MouseEvent} evt _
         */
        evtClickReset(evt) {
            this.setTheme('none')
            const els = this.shadowRoot.querySelectorAll('input[name=input-color-scheme-choose]')
            for (let i = 0; i < els.length; i++) {
                els[i].checked = false
            }

            this.evtClickContrast({ target: { name: 'contrast', value: 'standard', }, })
            const els1 = this.shadowRoot.querySelectorAll('input[name=contrast]')
            for (let i = 0; i < els1.length; i++) {
                els1[i].checked = false
            }

            delete this.uibThemeSettings[window.location.pathname]
            localStorage.setItem('uibThemeSettings', JSON.stringify(this.uibThemeSettings))
        }

        /** Handle contrast click. Override contrast css variables and set appropriate class on html
         * @param {MouseEvent} evt Click event
         */
        evtClickContrast(evt) {
            if (evt.target.name !== 'contrast') return

            const docRoot = document.documentElement

            if ( evt.target.value === 'more' ) {
                docRoot.style.setProperty('--text-bias', '1')
                docRoot.style.setProperty('--surfaces-bias', '1')
                docRoot.style.setProperty('--saturation-bias', '1')
                docRoot.classList.remove('standard')
                docRoot.classList.remove('less')
                docRoot.classList.add('more')
            } else if ( evt.target.value === 'less' ) {
                docRoot.style.setProperty('--text-bias', '-.1')
                docRoot.style.setProperty('--surfaces-bias', '-.05')
                docRoot.style.setProperty('--saturation-bias', '-.05')
                docRoot.classList.remove('standard')
                docRoot.classList.remove('more')
                docRoot.classList.add('less')
            } else {
                docRoot.style.removeProperty('--text-bias')
                docRoot.style.removeProperty('--surfaces-bias')
                docRoot.style.removeProperty('--saturation-bias')
                docRoot.classList.remove('standard')
                docRoot.classList.remove('more')
                docRoot.classList.remove('less')

            }

            try {
                this.uibThemeSettings[window.location.pathname].contrast = evt.target.value
                localStorage.setItem('uibThemeSettings', JSON.stringify(this.uibThemeSettings))
            } catch (e) {}
        }
    } // ---- end of Class ---- //

    // Make the class the default export so it can be used elsewhere
    export default UibThemeChanger

    /** Self register the class to global
     * Enables new data lists to be dynamically added via JS
     * and lets the static methods be called
     */
    window['UibThemeChanger'] = UibThemeChanger

    // Self-register the HTML tag
    customElements.define('uib-theme-changer', UibThemeChanger)