diff --git a/.gitignore b/.gitignore index a00fdccc..f916afff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ # Don't add directory /dist/ to .gitignore, as it is used by the build script +dist/mushroom-strategy.js diff --git a/package-lock.json b/package-lock.json index 0497aa99..c40623ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,11 @@ "packages": { "": { "name": "mushroom-strategy", - "version": "2.3.4", + "version": "2.3.5", "license": "MIT", "dependencies": { - "deepmerge": "^4" + "deepmerge": "^4", + "lit": "^3.0.0" }, "devDependencies": { "@types/semver": "^7.7.0", @@ -408,6 +409,21 @@ "node": ">=10" } }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", + "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", + "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -569,6 +585,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", @@ -2755,6 +2777,37 @@ "uc.micro": "^2.0.0" } }, + "node_modules/lit": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz", + "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", + "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", + "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", diff --git a/package.json b/package.json index bf541e04..f86af3c6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "url": "https://github.com/DigiLive/mushroom-strategy" }, "dependencies": { - "deepmerge": "^4" + "deepmerge": "^4", + "lit": "^3.0.0" }, "devDependencies": { "@types/semver": "^7.7.0", diff --git a/src/editor/hui-mushroom-strategy-editor.ts b/src/editor/hui-mushroom-strategy-editor.ts new file mode 100644 index 00000000..0776af8d --- /dev/null +++ b/src/editor/hui-mushroom-strategy-editor.ts @@ -0,0 +1,428 @@ +import { LitElement, html, css, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { fireEvent } from '../utilities/fire-event'; +import { StrategyConfig } from '../types/strategy/strategy-generics'; + +export interface LovelaceStrategyEditor extends HTMLElement { + hass?: any; + config?: any; + setConfig(config: any): void; + configChanged?: (config: any) => void; +} + +@customElement("hui-mushroom-strategy-editor") +export class HuiMushroomStrategyEditor + extends LitElement + implements LovelaceStrategyEditor +{ + @property({ attribute: false }) public hass!: any; + @state() private _config?: any; + @state() private _selectedConfigArea?: string; + + static get styles() { + return css` + .card-config { + display: flex; + flex-direction: column; + gap: 16px; + } + + .section { + border: 1px solid var(--divider-color); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + } + + .section-header { + font-weight: bold; + margin-bottom: 12px; + color: var(--primary-text-color); + } + + .form-row { + display: flex; + align-items: center; + margin-bottom: 8px; + } + + .form-row label { + width: 200px; + color: var(--primary-text-color); + } + + ha-switch { + margin-left: auto; + } + + ha-textfield { + flex: 1; + margin-left: 16px; + } + + ha-select { + flex: 1; + margin-left: 16px; + } + `; + } + + public setConfig(config: any): void { + console.log('Editor setConfig called with:', config); + this._config = config || { type: 'custom:mushroom-strategy', options: {} }; + } + + public configChanged(config: any): void { + console.log('Editor configChanged called with:', config); + this._config = config || { type: 'custom:mushroom-strategy', options: {} }; + this.requestUpdate(); + } + + private _valueChanged(ev: CustomEvent): void { + if (!this._config) return; + + const target = ev.target as any; + const configPath = target.configPath; + const value = target.value; + + if (!configPath) return; + + const newConfig = JSON.parse(JSON.stringify(this._config)); + + // Ensure the options structure exists (flat structure for strategies) + if (!newConfig.options) { + newConfig.options = {}; + } + + // Navigate to the correct path and set the value + let current = newConfig.options; + const pathParts = configPath.split('.'); + + for (let i = 0; i < pathParts.length - 1; i++) { + if (!current[pathParts[i]]) { + current[pathParts[i]] = {}; + } + current = current[pathParts[i]]; + } + + current[pathParts[pathParts.length - 1]] = value; + + fireEvent(this, "config-changed", { config: newConfig }); + } + + protected render(): TemplateResult { + if (!this._config) { + // Initialize with default config if none provided + this._config = { + type: 'custom:mushroom-strategy', + options: {} + }; + } + + // For strategies, options are directly under the config + const options = this._config.options || {}; + + console.log('Editor rendering with config:', this._config); + console.log('Options:', options); + + return html` +
+
+ Mushroom Strategy Configuration +
+ ${this._renderGeneralSection(options)} + ${this._renderHomeViewSection(options)} + ${this._renderChipsSection(options)} + ${this._renderAreasSection(options)} + ${this._renderDomainsSection(options)} + ${this._renderViewsSection(options)} +
+ `; + } + + private _renderGeneralSection(options: StrategyConfig): TemplateResult { + return html` +
+
General Settings
+ +
+ + +
+
+ `; + } + + private _renderHomeViewSection(options: StrategyConfig): TemplateResult { + const homeView = options.home_view || { hidden: [], stack_count: { _: 2 } }; + + return html` +
+
Home View Settings
+ +
+ + +
+ + ${this._renderHiddenSections(homeView.hidden || [])} +
+ `; + } + + private _renderHiddenSections(hidden: string[]): TemplateResult { + const sections = ['areas', 'areasTitle', 'chips', 'greeting', 'persons']; + + return html` +
+ + ${sections.map(section => html` +
+ + +
+ `)} +
+ `; + } + + private _toggleHiddenSection(ev: CustomEvent): void { + const target = ev.target as any; + const section = target.section; + const isChecked = target.checked; + + if (!this._config) return; + + const newConfig = JSON.parse(JSON.stringify(this._config)); + + // Ensure the options structure exists (flat structure for strategies) + if (!newConfig.options) { + newConfig.options = {}; + } + + const hidden = newConfig.options.home_view?.hidden || []; + + if (isChecked && !hidden.includes(section)) { + hidden.push(section); + } else if (!isChecked) { + const index = hidden.indexOf(section); + if (index > -1) { + hidden.splice(index, 1); + } + } + + if (!newConfig.options.home_view) { + newConfig.options.home_view = {}; + } + newConfig.options.home_view.hidden = hidden; + + fireEvent(this, "config-changed", { config: newConfig }); + } + + private _renderChipsSection(options: StrategyConfig): TemplateResult { + const chips = options.chips || {}; + + return html` +
+
Chips Configuration
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ `; + } + + private _renderAreasSection(options: StrategyConfig): TemplateResult { + return html` +
+
Areas Configuration
+ +
+ + + Mushroom Area Card + Home Assistant Area Card + +
+
+ `; + } + + private _renderDomainsSection(options: StrategyConfig): TemplateResult { + const domains = options.domains || {}; + const supportedDomains = ['light', 'switch', 'fan', 'cover', 'climate', 'lock', 'camera', 'vacuum', 'scene'] as const; + + return html` +
+
Domain Configuration
+ + ${supportedDomains.map(domain => html` +
+ + +
+ `)} +
+ `; + } + + private _toggleDomainHidden(ev: CustomEvent): void { + const target = ev.target as any; + const configPath = target.configPath; + const isChecked = target.checked; + + if (!this._config) return; + + const newConfig = JSON.parse(JSON.stringify(this._config)); + + // Ensure the options structure exists (flat structure for strategies) + if (!newConfig.options) { + newConfig.options = {}; + } + + // Navigate to the correct path and set the hidden value (opposite of checked) + let current = newConfig.options; + const pathParts = configPath.split('.'); + + for (let i = 0; i < pathParts.length - 1; i++) { + if (!current[pathParts[i]]) { + current[pathParts[i]] = {}; + } + current = current[pathParts[i]]; + } + + current[pathParts[pathParts.length - 1]] = !isChecked; + + fireEvent(this, "config-changed", { config: newConfig }); + } + + private _renderViewsSection(options: StrategyConfig): TemplateResult { + const views = options.views || {}; + const supportedViews = ['home', 'light', 'switch', 'fan', 'cover', 'climate', 'lock', 'camera', 'vacuum', 'scene'] as const; + + return html` +
+
Views Configuration
+ + ${supportedViews.map(view => html` +
+ + +
+ `)} +
+ `; + } + + private _toggleViewHidden(ev: CustomEvent): void { + const target = ev.target as any; + const configPath = target.configPath; + const isChecked = target.checked; + + if (!this._config) return; + + const newConfig = JSON.parse(JSON.stringify(this._config)); + + // Ensure the options structure exists (flat structure for strategies) + if (!newConfig.options) { + newConfig.options = {}; + } + + // Navigate to the correct path and set the hidden value (opposite of checked) + let current = newConfig.options; + const pathParts = configPath.split('.'); + + for (let i = 0; i < pathParts.length - 1; i++) { + if (!current[pathParts[i]]) { + current[pathParts[i]] = {}; + } + current = current[pathParts[i]]; + } + + current[pathParts[pathParts.length - 1]] = !isChecked; + + fireEvent(this, "config-changed", { config: newConfig }); + } +} + +// Ensure custom element is registered +if (!customElements.get("hui-mushroom-strategy-editor")) { + customElements.define("hui-mushroom-strategy-editor", HuiMushroomStrategyEditor); +} \ No newline at end of file diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts index 5d425859..3244e3fc 100644 --- a/src/mushroom-strategy.ts +++ b/src/mushroom-strategy.ts @@ -1,6 +1,7 @@ import { HassServiceTarget } from 'home-assistant-js-websocket'; import HeaderCard from './cards/HeaderCard'; import SensorCard from './cards/SensorCard'; +import './editor/hui-mushroom-strategy-editor'; import { Registry } from './Registry'; import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card'; import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types'; @@ -268,8 +269,20 @@ class MushroomStrategy extends HTMLTemplateElement { logMessage(lvlError, 'Error while handling persistent notifications for Mushroom Strategy', e); } } + + static getConfigElement() { + return document.createElement("hui-mushroom-strategy-editor"); + } + + static getStubConfig() { + return { + type: 'custom:mushroom-strategy', + options: {} + }; + } } +// Register the strategy customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy); const STRATEGY_VERSION = 'v2.3.5'; @@ -278,3 +291,15 @@ console.info( 'color: white; background: coral; font-weight: 700;', 'color: coral; background: white; font-weight: 700;' ); + +// Debug: Test editor registration +setTimeout(() => { + const strategy = customElements.get('ll-strategy-mushroom-strategy'); + const editor = customElements.get('hui-mushroom-strategy-editor'); + console.log('Mushroom Strategy Debug:', { + strategyRegistered: !!strategy, + editorRegistered: !!editor, + hasGetConfigElement: !!(strategy as any)?.getConfigElement, + hasGetStubConfig: !!(strategy as any)?.getStubConfig + }); +}, 1000); diff --git a/src/utilities/fire-event.ts b/src/utilities/fire-event.ts new file mode 100644 index 00000000..3e8bc491 --- /dev/null +++ b/src/utilities/fire-event.ts @@ -0,0 +1,21 @@ +export const fireEvent = ( + node: HTMLElement, + type: string, + detail: any, + options?: { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + } +): Event => { + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new CustomEvent(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + detail, + }); + node.dispatchEvent(event); + return event; +}; diff --git a/tsconfig.json b/tsconfig.json index be86809c..5be25efb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,13 +15,16 @@ // 4. Interop and Imports "allowSyntheticDefaultImports": true, "esModuleInterop": true, - // 5. Type Checking and Strictness + // 5. Experimental Features + "experimentalDecorators": true, + "useDefineForClassFields": false, + // 6. Type Checking and Strictness "strict": true, "noImplicitAny": true, "noImplicitThis": true, "noImplicitReturns": true, "strictNullChecks": true, - // 6. Project Structure and Build Integrity + // 7. Project Structure and Build Integrity "forceConsistentCasingInFileNames": true, "skipLibCheck": true },