Repository: ljmerza/light-entity-card Branch: master Commit: ac7a40642c3a Files: 29 Total size: 134.2 KB Directory structure: gitextract_6su_67oe/ ├── .babelrc ├── .eslintrc.yaml ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── deploy.yaml │ └── validate.yaml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── babel.config.js ├── dist/ │ ├── light-entity-card.js │ └── light-entity-card.js.LICENSE.txt ├── hacs.json ├── package.json ├── rollup-plugins/ │ └── ignore.js ├── src/ │ ├── buildElementDefinitions.js │ ├── color-wheel/ │ │ ├── color-picker.js │ │ ├── convert-color.js │ │ ├── events-mixin.js │ │ └── fire_event.js │ ├── defaults.js │ ├── globalElementLoader.js │ ├── index-editor.js │ ├── index.js │ ├── style-editor.js │ └── style.js └── webpack/ ├── config.common.js ├── config.dev.js └── config.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "modules": false, "targets": { "esmodules": true } } ], ["minify"] ], "comments": false, "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ], [ "@babel/plugin-proposal-class-properties", { "loose": true } ], ["@babel/plugin-transform-template-literals"], ["iife-wrap"] ] } ================================================ FILE: .eslintrc.yaml ================================================ extends: airbnb-base rules: no-else-return: 0 no-underscore-dangle: 0 nonblock-statement-body-position: 0 curly: 0 no-return-assign: 0 consistent-return: 0 no-mixed-operators: 0 class-methods-use-this: 0 no-nested-ternary: 0 camelcase: 0 no-bitwise: 0 max-len: 0 no-useless-constructor: 0 no-restricted-globals: 0 globals: window: true Event: true customElements: true ================================================ FILE: .gitattributes ================================================ *.js text eol=lf ================================================ FILE: .github/workflows/deploy.yaml ================================================ name: Build and Release on Tag on: workflow_dispatch: push: tags: - '*' jobs: build-and-release: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '16' - name: Install Dependencies run: npm install - name: Run Build run: npm run build - name: Commit dist folder run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add dist/ git commit -m "Add dist folder" - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} force: true directory: dist - name: Get current and previous tags id: tags run: | current_tag=$(git describe --tags --abbrev=0) previous_tag=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1)) echo ::set-output name=current_tag::${current_tag} echo ::set-output name=previous_tag::${previous_tag} - name: Generate release notes id: releasenotes run: | echo "Release Notes:" > release_notes.md git log ${previous_tag}..${current_tag} --pretty=format:"%h - %s" >> release_notes.md echo ::set-output name=notes::$(cat release_notes.md) - name: Create Release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.tags.outputs.current_tag }} release_name: Release ${{ steps.tags.outputs.current_tag }} body: ${{ steps.releasenotes.outputs.notes }} draft: false prerelease: false - name: Upload light-entity-card.js to Release uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./dist/light-entity-card.js asset_name: light-entity-card.js asset_content_type: application/javascript ================================================ FILE: .github/workflows/validate.yaml ================================================ name: Validate on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: validate: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v2" - name: HACS validation uses: "hacs/action@main" with: category: "plugin" ================================================ FILE: .gitignore ================================================ /node_modules/ ================================================ FILE: .prettierrc.json ================================================ { "trailingComma": "es5", "singleQuote": true, "printWidth": 120, "arrowParens": "avoid" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Leonardo Merza Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Light Entity Card Control any light/switch entity through lovelace ## Support For help, visit the light entity support thread [here](https://community.home-assistant.io/t/light-entity-card/96146) [![GitHub Release][releases-shield]][releases] [![License][license-shield]](LICENSE.md) ![Project Maintenance][maintenance-shield] [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ## Features * Works on any light and switch based entity * Toggle on/off * HS Color wheel * Color temperature and white value support * Support for configured language * Compact card support for grouped entities * use `persist_features: true` to always show entity features * use `effects_list` to add custom effects list or use `input_select` entity * always show or hide header and each input ## Installation Add through [HACS](https://github.com/custom-components/hacs) Issues with the installation should be asked in the [Home Assistant forums](https://community.home-assistant.io/t/light-entity-card/96146) ## Configurations ```yaml type: custom:light-entity-card entity: light.downstairs ``` ```yaml type: custom:light-entity-card entity: light.downstairs effects_list: - effect1 - effect2 ``` ```yaml type: custom:light-entity-card entity: light.downstairs effects_list: input_select.custom_effect_list ``` ```yaml type: custom:light-entity-card entity: light.downstairs group: true ``` ## Options | Name | Type | Requirement | `Default value` Description | | -------------------- | ------------------- | ------------ | --------------------------------------------------------------------------- | | type | string | **Required** | `custom:light-entity-card` | | entity | string | **Required** | The entity name of the light entity to control | | shorten_cards | boolean | **Optional** | `false` show a compact version of the card | | consolidate_entities | boolean | **Optional** | `false` if entity is a group you can consolidate all entities into one | | persist_features | boolean | **Optional** | `false` always show entity features | | effects_list | list/string/boolean | **Optional** | custom list of effects, an input_select entity, or set false to always hide | | header | string | **Optional** | custom header name | | hide_header | boolean | **Optional** | `false` hides the entity header of the card including toggle | | show_header_icon | boolean | **Optional** | `false` shows the entity icon of the card including toggle | | brightness | boolean | **Optional** | `true` show brightness slider if available | | color_temp | boolean | **Optional** | `true` show color temp slider if available | | white_value | boolean | **Optional** | `true` show white value slider if available | | color_picker | boolean | **Optional** | `true` show color picker wheel if available | | speed | boolean | **Optional** | `false` show speed slider if available | | intensity | boolean | **Optional** | `false` show intensity slider if available | | force_features | boolean | **Optional** | `false` force showing all features in card | | full_width_sliders | boolean | **Optional** | `false` makes slider the full width of card | | brightness_icon | string | **Optional** | `weather-sunny` change the brightness slider icon | | white_icon | string | **Optional** | `file-word-box` change the white slider icon | | temperature_icon | string | **Optional** | `thermometer` change the temperature slider icon | | speed_icon | string | **Optional** | `speedometer` change the speed slider icon | | intensity_icon | string | **Optional** | `transit-connection-horizontal` change the intensity slider icon | | show_slider_percent | boolean | **Optional** | `false` show percent next to sliders | | child_card | boolean | **Optional** | `false` remove padding/margin to make this card within another card | --- Enjoy my card? Help me out for a couple of :beers: or a :coffee:! [![coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/JMISm06AD) [commits-shield]: https://img.shields.io/github/commit-activity/y/ljmerza/light-entity-card.svg?style=for-the-badge [commits]: https://github.com/ljmerza/light-entity-card/commits/master [license-shield]: https://img.shields.io/github/license/ljmerza/light-entity-card.svg?style=for-the-badge [maintenance-shield]: https://img.shields.io/badge/maintainer-Leonardo%20Merza%20%40ljmerza-blue.svg?style=for-the-badge [releases-shield]: https://img.shields.io/github/release/ljmerza/light-entity-card.svg?style=for-the-badge [releases]: https://github.com/ljmerza/light-entity-card/releases ================================================ FILE: babel.config.js ================================================ module.exports = { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "debug": true, "targets": "> 0.25%, not dead", "shippedProposals": true } ] ] } ================================================ FILE: dist/light-entity-card.js ================================================ /*! For license information please see light-entity-card.js.LICENSE.txt */ (()=>{"use strict";const t=window,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),n=new WeakMap;class r{constructor(t,e,n){if(this._$cssResult$=!0,n!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const i=this.t;if(e&&void 0===t){const e=void 0!==i&&1===i.length;e&&(t=n.get(i)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&n.set(i,t))}return t}toString(){return this.cssText}}const o=(t,...e)=>{const n=1===t.length?t[0]:e.reduce((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1],t[0]);return new r(n,t,i)},s=(i,n)=>{e?i.adoptedStyleSheets=n.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet):n.forEach(e=>{const n=document.createElement("style"),r=t.litNonce;void 0!==r&&n.setAttribute("nonce",r),n.textContent=e.cssText,i.appendChild(n)})},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new r("string"==typeof t?t:t+"",void 0,i))(e)})(t):t;var c;const h=window,l=h.trustedTypes,d=l?l.emptyScript:"",u=h.reactiveElementPolyfillSupport,f={toAttribute(t,e){switch(e){case Boolean:t=t?d:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},p=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:f,reflect:!1,hasChanged:p},_="finalized";class v extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this._$Eu()}static addInitializer(t){var e;this.finalize(),(null!==(e=this.h)&&void 0!==e?e:this.h=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach((e,i)=>{const n=this._$Ep(i,e);void 0!==n&&(this._$Ev.set(n,i),t.push(n))}),t}static createProperty(t,e=g){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const r=this[t];this[e]=n,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}static finalize(){if(this.hasOwnProperty(_))return!1;this[_]=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Ep(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}_$Eu(){var t;this._$E_=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach(t=>t(this))}addController(t){var e,i;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$ES)||void 0===e||e.splice(this._$ES.indexOf(t)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach((t,e)=>{this.hasOwnProperty(e)&&(this._$Ei.set(e,this[e]),delete this[e])})}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return s(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$EO(t,e,i=g){var n;const r=this.constructor._$Ep(t,i);if(void 0!==r&&!0===i.reflect){const o=(void 0!==(null===(n=i.converter)||void 0===n?void 0:n.toAttribute)?i.converter:f).toAttribute(e,i.type);this._$El=t,null==o?this.removeAttribute(r):this.setAttribute(r,o),this._$El=null}}_$AK(t,e){var i;const n=this.constructor,r=n._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=n.getPropertyOptions(r),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(i=t.converter)||void 0===i?void 0:i.fromAttribute)?t.converter:f;this._$El=r,this[r]=o.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||p)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach((t,e)=>this[e]=t),this._$Ei=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$ES)||void 0===t||t.forEach(t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)}),this.update(i)):this._$Ek()}catch(t){throw e=!1,this._$Ek(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$ES)||void 0===e||e.forEach(t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)}),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach((t,e)=>this._$EO(e,this[e],t)),this._$EC=void 0),this._$Ek()}updated(t){}firstUpdated(t){}}function y(t,e,i){return e=b(e),function(t,e){if(e&&("object"==O(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t)}(t,m()?Reflect.construct(e,i||[],b(t).constructor):e.apply(t,i))}function m(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(t){}return(m=function(){return!!t})()}function b(t){return b=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},b(t)}function $(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&w(t,e)}function w(t,e){return w=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},w(t,e)}function k(t,e){var i="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!i){if(Array.isArray(t)||(i=S(t))||e&&t&&"number"==typeof t.length){i&&(t=i);var n=0,r=function(){};return{s:r,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){i=i.call(t)},n:function(){var t=i.next();return s=t.done,t},e:function(t){a=!0,o=t},f:function(){try{s||null==i.return||i.return()}finally{if(a)throw o}}}}function S(t,e){if(t){if("string"==typeof t)return x(t,e);var i={}.toString.call(t).slice(8,-1);return"Object"===i&&t.constructor&&(i=t.constructor.name),"Map"===i||"Set"===i?Array.from(t):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?x(t,e):void 0}}function x(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,n=Array(e);i"),M=document,W=function(){return M.createComment("")},L=function(t){return null===t||"object"!=O(t)&&"function"!=typeof t},B=Array.isArray,D="[ \t\n\f\r]",F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,z=/-->/g,q=/>/g,K=RegExp(">|".concat(D,"(?:([^\\s\"'>=/]+)(").concat(D,"*=").concat(D,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),J=/'/g,Z=/"/g,G=/^(?:script|style|textarea|title)$/i,Q=function(t){return function(e){for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r":"",s=F,a=0;a"===l[0]?(s=null!=i?i:F,d=-1):void 0===l[1]?d=-2:(d=s.lastIndex-l[2].length,h=l[1],s=void 0===l[3]?K:'"'===l[3]?Z:J):s===Z||s===J?s=K:s===z||s===q?s=F:(s=K,i=void 0);var f=s===K&&t[a+1].startsWith("/>")?" ":"";o+=s===F?c+V:d>=0?(r.push(h),c.slice(0,d)+N+c.slice(d)+R+f):c+R+(-2===d?(r.push(void 0),a):f)}return[nt(t,o+(t[n]||"")+(2===e?"":"")),r]},ot=function(){return E(function t(e,i){var n,r=e.strings,o=e._$litType$;C(this,t),this.parts=[];var s=0,a=0,c=r.length-1,h=this.parts,l=function(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var i=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=i){var n,r,o,s,a=[],c=!0,h=!1;try{if(o=(i=i.call(t)).next,0===e){if(Object(i)!==i)return;c=!1}else for(;!(c=(n=o.call(i)).done)&&(a.push(n.value),a.length!==e);c=!0);}catch(t){h=!0,r=t}finally{try{if(!c&&null!=i.return&&(s=i.return(),Object(s)!==s))return}finally{if(h)throw r}}return a}}(t,e)||S(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(rt(r,o),2),d=l[0],u=l[1];if(this.el=t.createElement(d,i),it.currentNode=this.el.content,2===o){var f=this.el.content,p=f.firstChild;p.remove(),f.append.apply(f,function(t){return function(t){if(Array.isArray(t))return x(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||S(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(p.childNodes))}for(;null!==(n=it.nextNode())&&h.length0){n.textContent=U?U.emptyScript:"";for(var T=0;T2&&void 0!==arguments[2]?arguments[2]:t,a=arguments.length>3?arguments[3]:void 0;if(e===Y)return e;var c=void 0!==a?null===(i=s._$Co)||void 0===i?void 0:i[a]:s._$Cl,h=L(e)?void 0:e._$litDirective$;return(null==c?void 0:c.constructor)!==h&&(null===(n=null==c?void 0:c._$AO)||void 0===n||n.call(c,!1),void 0===h?c=void 0:(c=new h(t))._$AT(t,s,a),void 0!==a?(null!==(r=(o=s)._$Co)&&void 0!==r?r:o._$Co=[])[a]=c:s._$Cl=c),void 0!==c&&(e=st(t,c._$AS(t,e.values),c,a)),e}var at,ct,ht=function(){return E(function t(e,i){C(this,t),this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=i},[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(t){var e,i=this._$AD,n=i.el.content,r=i.parts,o=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:M).importNode(n,!0);it.currentNode=o;for(var s=it.nextNode(),a=0,c=0,h=r[0];void 0!==h;){if(a===h.index){var l=void 0;2===h.type?l=new lt(s,s.nextSibling,this,t):1===h.type?l=new h.ctor(s,h.name,h.strings,this,t):6===h.type&&(l=new _t(s,this,t)),this._$AV.push(l),h=r[++c]}a!==(null==h?void 0:h.index)&&(s=it.nextNode(),a++)}return it.currentNode=M,o}},{key:"v",value:function(t){var e,i=0,n=k(this._$AV);try{for(n.s();!(e=n.n()).done;){var r=e.value;void 0!==r&&(void 0!==r.strings?(r._$AI(t,r,i),i+=r.strings.length-2):r._$AI(t[i])),i++}}catch(t){n.e(t)}finally{n.f()}}}])}(),lt=function(){function t(e,i,n,r){var o;C(this,t),this.type=2,this._$AH=tt,this._$AN=void 0,this._$AA=e,this._$AB=i,this._$AM=n,this.options=r,this._$Cp=null===(o=null==r?void 0:r.isConnected)||void 0===o||o}return E(t,[{key:"_$AU",get:function(){var t,e;return null!==(e=null===(t=this._$AM)||void 0===t?void 0:t._$AU)&&void 0!==e?e:this._$Cp}},{key:"parentNode",get:function(){var t=this._$AA.parentNode,e=this._$AM;return void 0!==e&&11===(null==t?void 0:t.nodeType)&&(t=e.parentNode),t}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){t=st(this,t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),L(t)?t===tt||null==t||""===t?(this._$AH!==tt&&this._$AR(),this._$AH=tt):t!==this._$AH&&t!==Y&&this._(t):void 0!==t._$litType$?this.g(t):void 0!==t.nodeType?this.$(t):function(t){return B(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])}(t)?this.T(t):this._(t)}},{key:"k",value:function(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}},{key:"$",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.k(t))}},{key:"_",value:function(t){this._$AH!==tt&&L(this._$AH)?this._$AA.nextSibling.data=t:this.$(M.createTextNode(t)),this._$AH=t}},{key:"g",value:function(t){var e,i=t.values,n=t._$litType$,r="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=ot.createElement(nt(n.h,n.h[0]),this.options)),n);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.v(i);else{var o=new ht(r,this),s=o.u(this.options);o.v(i),this.$(s),this._$AH=o}}},{key:"_$AC",value:function(t){var e=et.get(t.strings);return void 0===e&&et.set(t.strings,e=new ot(t)),e}},{key:"T",value:function(e){B(this._$AH)||(this._$AH=[],this._$AR());var i,n,r=this._$AH,o=0,s=k(e);try{for(s.s();!(n=s.n()).done;){var a=n.value;o===r.length?r.push(i=new t(this.k(W()),this.k(W()),this,this.options)):i=r[o],i._$AI(a),o++}}catch(t){s.e(t)}finally{s.f()}o0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,i=arguments.length>1?arguments[1]:void 0;for(null===(t=this._$AP)||void 0===t||t.call(this,!1,!0,i);e&&e!==this._$AB;){var n=e.nextSibling;e.remove(),e=n}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cp=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}])}(),dt=function(){return E(function t(e,i,n,r,o){C(this,t),this.type=1,this._$AH=tt,this._$AN=void 0,this.element=e,this.name=i,this._$AM=r,this.options=o,n.length>2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=tt},[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,i=arguments.length>2?arguments[2]:void 0,n=arguments.length>3?arguments[3]:void 0,r=this.strings,o=!1;if(void 0===r)t=st(this,t,e,0),(o=!L(t)||t!==this._$AH&&t!==Y)&&(this._$AH=t);else{var s,a,c=t;for(t=r[0],s=0;s1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==e?e:tt)!==Y){var i=this._$AH,n=t===tt&&i!==tt||t.capture!==i.capture||t.once!==i.once||t.passive!==i.passive,r=t!==tt&&(i===tt||n);n&&this.element.removeEventListener(this.name,this,i),r&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,i;"function"==typeof this._$AH?this._$AH.call(null!==(i=null===(e=this.options)||void 0===e?void 0:e.host)&&void 0!==i?i:this.element,t):this._$AH.handleEvent(t)}}])}(dt),_t=function(){return E(function t(e,i,n){C(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=i,this.options=n},[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){st(this,t)}}])}(),vt=j.litHtmlPolyfillSupport;function yt(t){return yt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},yt(t)}function mt(t,e){for(var i=0;it.registry.define(e,i)));const r=this.renderOptions.creationScope=this.attachShadow({...n,customElements:t.registry});return((t,e)=>{if(Ot)t.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of e){const e=document.createElement("style"),n=Pt.litNonce;void 0!==n&&e.setAttribute("nonce",n),e.textContent=i.cssText,t.appendChild(e)}})(r,this.constructor.elementStyles),r}}}Symbol(),new WeakMap;const jt=o` .light-entity-card { padding: 16px; } .light-entity-child-card { box-shadow: none !important; padding: 0 !important; } .light-entity-card.group { padding-bottom: 5; padding-top: 0; } .ha-slider-full-width ha-slider { width: 100%; } .percent-slider { color: var(--primary-text-color); display: flex; justify-content: center; align-items: center; margin-left: 8px; min-width: 40px; text-align: right; } .light-entity-card__header { display: flex; justify-content: space-between; @apply --paper-font-headline; line-height: 40px; color: var(--primary-text-color); } .group .light-entity-card__header { } .light-entity-card-sliders > div { margin-top: 10px; } .group .light-entity-card-sliders > div { margin-top: 0px; } .light-entity-card__toggle { display: flex; cursor: pointer; } .light-entity-card__color-picker { display: flex; justify-content: center; margin-top: 10px; } .light-entity-card__color-picker ha-hs-color-picker { max-width: 300px; width: 100%; } .light-entity-card-color_temp { background-image: var(--ha-slider-background, linear-gradient(to right, #a6d1ff, #ffb74d)); border-radius: 4px; } .light-entity-card-color_temp--kelvin { background-image: var(--ha-slider-background, linear-gradient(to right, #ffb74d, #a6d1ff)); } .light-entity-card-effectlist { padding-top: 10px; padding-bottom: 10px; } .group .light-entity-card-effectlist { padding-bottom: 20px; } .light-entity-card-center { display: flex; justify-content: center; align-items: center; cursor: pointer; } .hidden { display: none; } .icon-container { display: flex; justify-content: center; align-items: center; margin-right: 8px; } `,Ut={shorten_cards:!1,consolidate_entities:!1,child_card:!1,hide_header:!1,show_header_icon:!1,header:"",persist_features:!1,brightness:!0,color_temp:!0,white_value:!0,warm_white_value:!0,color_picker:!0,effects_list:!0,speed:!0,intensity:!0,force_features:!1,show_slider_percent:!1,full_width_sliders:!1,color_temp_in_kelvin:!1,transition:0,brightness_icon:"weather-sunny",white_icon:"file-word-box",warm_white_icon:"weather-sunset",temperature_icon:"thermometer",speed_icon:"speedometer",intensity_icon:"transit-connection-horizontal"},It=o` .entities { padding-top: 10px; padding-bottom: 10px; display: flex; } .entities ha-formfield { display: block; margin-bottom: 10px; margin-left: 10px; } .checkbox-options { display: flex; } ha-entity-picker { width: 100%; } .checkbox-options ha-formfield, .entities mwc-switch, .entities ha-form-string { padding-right: 2%; width: 48%; } .checkbox-options ha-formfield { margin-top: 10px; } .overall-config { margin-bottom: 20px; } `,Nt=(t,e)=>t.reduce((t,i)=>(i.defineId?t[i.defineId]=i:i.promise.then(t=>{void 0===e.registry.get(i.name)&&e.registry.define(i.name,t)}),t),{}),Rt=t=>({name:t,promise:customElements.whenDefined(t).then(()=>customElements.get(t))}),Ht=(t,e,i={},n={})=>{const r=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,t.dispatchEvent(r),r};class Vt extends(Tt(At)){static get elementDefinitions(){return Nt([Rt("ha-checkbox"),Rt("ha-formfield"),Rt("ha-form-string"),Rt("ha-entity-picker")],Vt)}static get styles(){return It}static get properties(){return{hass:{},_config:{}}}setConfig(t){this._config={...Ut,...t}}get entityDomains(){return["switch","light","group"]}firstUpdated(){this._firstRendered=!0}render(){if(!this.hass||!this._config)return X``;let{header:t}=this._config;if(!t&&this._config.entity){let e=this._config.entity.split(".")[1]||"";e&&(e=e.charAt(0).toUpperCase()+e.slice(1),t=e)}return X`
`}entityChanged(t){if(!this._config||!this.hass||!this._firstRendered)return;const e=t.detail.value;e!==this._config.entity&&(this._config={...this._config,entity:e},Ht(this,"config-changed",{config:this._config}))}configChanged(t){if(!this._config||!this.hass||!this._firstRendered)return;const{target:{configValue:e,value:i},detail:{value:n}}=t;this._config=null!=n?{...this._config,[e]:n}:{...this._config,[e]:i},Ht(this,"config-changed",{config:this._config})}checkboxConfigChanged(t){if(!this._config||!this.hass||!this._firstRendered)return;const{target:{value:e,checked:i}}=t;this._config={...this._config,[e]:i},Ht(this,"config-changed",{config:this._config})}}const Mt="light-entity-card-editor";customElements.define(Mt,Vt),console.info("light-entity-card v6.3.1");class Wt extends(Tt(At)){static get elementDefinitions(){return Nt([Rt("ha-card"),Rt("more-info-light"),Rt("ha-switch"),Rt("ha-icon"),Rt("state-badge"),Rt("ha-slider"),Rt("ha-hs-color-picker"),Rt("ha-select"),Rt("mwc-list-item")],Wt)}static get properties(){return{hass:{},config:{},_colorPickerValues:{state:!0}}}async firstUpdated(){if(this._firstUpdate=!0,!1!==this.config.color_picker&&this.config.entity.startsWith("light.")&&!customElements.get("ha-hs-color-picker")){const t=document.querySelector("home-assistant");if(t){const e=document.createElement("style");e.textContent="ha-more-info-dialog { display: none !important; }",t.shadowRoot.appendChild(e);const i=new CustomEvent("hass-more-info",{detail:{entityId:this.config.entity},bubbles:!0,composed:!0});t.dispatchEvent(i);try{await Promise.race([customElements.whenDefined("ha-hs-color-picker"),new Promise((t,e)=>setTimeout(()=>e(new Error("timeout")),5e3))])}catch(t){}finally{const i=new CustomEvent("hass-more-info",{detail:{entityId:""},bubbles:!0,composed:!0});t.dispatchEvent(i),e.remove()}}this.requestUpdate()}}setConfig(t){if(!t.entity)throw Error("entity required.");this.config={...Ut,...t}}static async getConfigElement(){return document.createElement(Mt)}static get featureNames(){return{brightness:1,colorTemp:2,effectList:4,color:16,whiteValue:128}}static get cmdToggle(){return{on:"turn_on",off:"turn_off"}}static get entityLength(){return{light:10,switch:1}}getCardSize(){if(!this.config||!this.__hass||!this.__hass.states[this.config.entity])return 1;let t=0;const e=this.__hass.states[this.config.entity];return Array.isArray(e.attributes.entity_id)?e.attributes.entity_id.forEach(e=>t+=this.getEntityLength(e)):t+=this.getEntityLength(e.attributes.entity_id),this.config.group&&(t*=.8),parseInt(t,10)}getEntityLength(t){return/^light\./.test(t)?Wt.entityLength.light:/^switch\./.test(t)?Wt.entityLength.switch:0}get styles(){return jt}isEntityOn(t){return"on"===t.state}render(){const t=this.hass.states[this.config.entity];if(!t)return X` ${`Invalid entity: ${this.config.entity}`} `;this._stateObjects=this.getEntitiesToShow(t),this.config.consolidate_entities?this._shownStateObjects=[t]:this._shownStateObjects=[...this._stateObjects];const e=this._shownStateObjects.reduce((t,e)=>X`${t}${this.createEntityTemplate(e)}`,""),i=`light-entity-card ${this.config.shorten_cards?" group":""} ${this.config.child_card?" light-entity-child-card":""}`;return X` ${e} `}getEntitiesToShow(t){return t.attributes.entity_id&&Array.isArray(t.attributes.entity_id)?t.attributes.entity_id.map(t=>this.hass.states[t]).filter(Boolean):[t]}createEntityTemplate(t){const e=this.config.full_width_sliders?"ha-slider-full-width":"";return X` ${this.createHeader(t)}
${this.createBrightnessSlider(t)} ${this.createSpeedSlider(t)} ${this.createIntensitySlider(t)} ${this.createColorTemperature(t)} ${this.createWhiteValue(t)} ${this.createWarmWhiteValue(t)}
${this.createColorPicker(t)} ${this.createEffectList(t)} `}createHeader(t){if(this.config.hide_header)return X``;const e=this.config.header||t.attributes.friendly_name||t.entity_id;return X`
${this.showHeaderIcon(t)}
${e}
this.setToggle(e,t)}>
`}showHeaderIcon(t){return this.config.show_header_icon?X`
`:X``}createBrightnessSlider(t){return!1===this.config.brightness||this.dontShowFeature("brightness",t)?X``:X`
${this.showPercent(t.attributes.brightness,0,254,"brightness")}
`}createSpeedSlider(t){return!1===this.config.speed||this.dontShowFeature("speed",t)?X``:X`
${this.showPercent(t.attributes.speed,0,254)}
`}createIntensitySlider(t){return!1===this.config.intensity||this.dontShowFeature("intensity",t)?X``:X`
${this.showPercent(t.attributes.intensity,0,254)}
`}showPercent(t,e,i,n){if("brightness"===n&&void 0!==this.config.show_brightness_percent){if(!this.config.show_brightness_percent)return X``}else if("color_temp"===n&&void 0!==this.config.show_color_temp_percent){if(!this.config.show_color_temp_percent)return X``}else if("color_temp"===n&&this.config.color_temp_in_kelvin);else if(!this.config.show_slider_percent)return X``;if("color_temp"===n&&this.config.color_temp_in_kelvin)return X`
${t}K
`;let r=parseInt(100*(t-e)/(i-e),10);return isNaN(r)&&(r=0),X`
${r}%
`}createColorTemperature(t){if(!1===this.config.color_temp)return X``;if(this.dontShowFeature("colorTemp",t))return X``;const e=void 0!==t.attributes.min_color_temp_kelvin;let i,n,r,o,s,a;if(this.config.color_temp_in_kelvin){let o,s,a;if(e?(a=t.attributes.color_temp_kelvin,o=t.attributes.min_color_temp_kelvin,s=t.attributes.max_color_temp_kelvin):(a=t.attributes.color_temp?Math.round(1e6/t.attributes.color_temp):null,o=t.attributes.max_mireds?Math.round(1e6/t.attributes.max_mireds):null,s=t.attributes.min_mireds?Math.round(1e6/t.attributes.min_mireds):null),!o||!s)return X``;const c=Math.round((o+s)/2);i="number"==typeof a&&Number.isFinite(a)&&a>0?a:null,n=o,r=s;const h=i||c,l=this.showPercent(h,n,r,"color_temp");return X`
${l}
`}if(e){const e=t.attributes.color_temp_kelvin,i=t.attributes.min_color_temp_kelvin,n=t.attributes.max_color_temp_kelvin;if(!i||!n)return X``;a="number"==typeof e&&Number.isFinite(e)&&e>0?Math.round(1e6/e):null,o=Math.round(1e6/n),s=Math.round(1e6/i)}else if(a=t.attributes.color_temp,o=t.attributes.min_mireds,s=t.attributes.max_mireds,!o||!s)return X``;const c=s&&o?s-o:0,h=c>0&&null!=a?Math.round((a-o)/c*100):50,l=this.showPercent(h,0,100,"color_temp");return X`
${l}
`}getWhiteValue(t,e=3){const i=t.attributes.rgbw_color,n=t.attributes.rgbww_color;return i&&3===e?i[3]||0:n&&e
${this.showPercent(e,0,254)} `}createWarmWhiteValue(t){if(!1===this.config.warm_white_value)return X``;if(this.dontShowFeature("warmWhiteValue",t))return X``;const e=this.getWhiteValue(t,4);return X`
${this.showPercent(e,0,254)}
`}_setWhiteValue(t,e,i){const n=parseInt(t.target.value,10);if(isNaN(n))return;const r=e.attributes.supported_color_modes||[],o=e.attributes.rgbw_color,s=e.attributes.rgbww_color;if(r.includes("rgbw")&&3===i){const t=o?o.slice(0,3):e.attributes.rgb_color||[255,255,255];this.callEntityService({rgbw_color:[t[0],t[1],t[2],n]},e)}else if(r.includes("rgbww")){let t;if(s)t=s;else if(o)t=[o[0],o[1],o[2],o[3],0];else{const i=e.attributes.rgb_color||[255,255,255];t=[i[0],i[1],i[2],0,0]}const r=[...t];r[i]=n,this.callEntityService({rgbww_color:r},e)}else this.callEntityService({white_value:n},e)}createEffectList(t){if(!1===this.config.effects_list)return X``;if(!this.config.persist_features&&!this.isEntityOn(t))return X``;let e=t.attributes.effect_list||[];if(this.config.effects_list&&Array.isArray(this.config.effects_list))e=this.config.effects_list;else if(this.config.effects_list&&this.hass.states[this.config.effects_list]){const t=this.hass.states[this.config.effects_list];e=t.attributes&&t.attributes.options||[]}else if(this.dontShowFeature("effectList",t))return X``;const i=e.map(e=>this.createListItem(t,e)),n=this.hass.localize("ui.card.light.effect")||"Effect";return X`
this.setEffect(e,t)} label="${n}" > ${i}
`}createListItem(t,e){return X`${e}`}createColorPicker(t){if(!1===this.config.color_picker)return X``;if(this.dontShowFeature("color",t))return X``;const e=t.attributes.hs_color||[0,0],i=this._colorPickerValues&&this._colorPickerValues[t.entity_id]||[e[0],e[1]/100];return X`
{this._colorPickerValues={...this._colorPickerValues,[t.entity_id]:e.detail.value}}} @value-changed=${e=>this._onColorPickerChanged(e.detail.value,t)} >
`}dontShowFeature(t,e){if(this.config.force_features)return!1;if("speed"===t&&"speed"in e.attributes)return!1;if("intensity"===t&&"intensity"in e.attributes)return!1;let i=Wt.featureNames[t]&e.attributes.supported_features;const n=e.attributes.supported_color_modes||[];if(!i)switch(t){case"brightness":if(i=Object.prototype.hasOwnProperty.call(e.attributes,"brightness"),!i){const t=["hs","rgb","rgbw","rgbww","white","brightness","color_temp","xy"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0}break;case"colorTemp":if(n){const t=["color_temp"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0}break;case"effectList":i=e.attributes.effect_list&&e.attributes.effect_list.length;break;case"color":{const t=["hs","rgb","rgbw","rgbww","xy"];i=[...new Set(n.filter(e=>t.includes(e)))].length>0;break}case"whiteValue":if(i=Object.prototype.hasOwnProperty.call(e.attributes,"white_value"),!i){const t=["rgbw","rgbww"];i=n.some(e=>t.includes(e))}break;case"warmWhiteValue":{const t=["rgbww"];i=n.some(e=>t.includes(e));break}default:i=!1}return!i||!this.config.persist_features&&!this.isEntityOn(e)}_onColorPickerChanged(t,e){if(this._colorPickerValues){const{[e.entity_id]:t,...i}=this._colorPickerValues;this._colorPickerValues=i}this.setColorPicker(t,e)}setColorPicker(t,e){t&&this.callEntityService({hs_color:[t[0],100*t[1]]},e)}_setValue(t,e,i){const n=parseInt(t.target.value,10);isNaN(n)||parseInt(e.attributes[i],10)===n||this.callEntityService({[i]:n},e)}_setColorTemp(t,e,i,n,r,o){const s=parseInt(t.target.value,10);if(!isNaN(s))if(n)if(i){if(s===parseInt(e.attributes.color_temp_kelvin,10))return;this.callEntityService({color_temp_kelvin:s},e)}else{const t=Math.round(1e6/s);if(t===parseInt(e.attributes.color_temp,10))return;this.callEntityService({color_temp:t},e)}else{if(!Number.isFinite(r)||!Number.isFinite(o)||o<=r)return;const t=Math.round(r+s/100*(o-r));if(i){const i=Math.round(1e6/t);if(i===parseInt(e.attributes.color_temp_kelvin,10))return;this.callEntityService({color_temp_kelvin:i},e)}else{if(t===parseInt(e.attributes.color_temp,10))return;this.callEntityService({color_temp:t},e)}}}setToggle(t,e){const i=this.isEntityOn(e)?Wt.cmdToggle.off:Wt.cmdToggle.on;this.callEntityService({},e,i)}setEffect(t,e){t.target.value&&this.callEntityService({effect:t.target.value},e)}callEntityService(t,e,i){if(!this._firstUpdate)return;let n=e.entity_id.split(".")[0];"group"===n&&(n="homeassistant");const r=parseFloat(this.config.transition)||0;r>0&&"light"===n&&(t={...t,transition:r}),this.hass.callService(n,i||Wt.cmdToggle.on,{entity_id:e.entity_id,...t})}}customElements.define("light-entity-card",Wt),window.customCards=window.customCards||[],window.customCards.push({type:"light-entity-card",name:"Light Entity Card",description:"Control lights and switches"})})(); ================================================ FILE: dist/light-entity-card.js.LICENSE.txt ================================================ /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ /** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ ================================================ FILE: hacs.json ================================================ { "name": "Light Entity Card", "render_readme": true, "filename": "light-entity-card.js" } ================================================ FILE: package.json ================================================ { "name": "light-entity-card", "version": "6.3.1", "description": "A light-entity card for Home Assistant Lovelace UI", "keywords": [ "home-assistant", "homeassistant", "hass", "automation", "lovelace", "custom-cards", "light-entity" ], "repository": "git@github.com:ljmerza/light-entity-card.git", "author": "Leonardo Merza ", "license": "MIT", "dependencies": { "@babel/polyfill": "^7.4.4", "@lit-labs/scoped-registry-mixin": "^1.0.0", "@material/mwc-icon": "^0.25.3", "@material/mwc-list": "^0.25.3", "@material/mwc-menu": "^0.25.3", "@material/mwc-notched-outline": "^0.25.3", "@material/mwc-select": "^0.25.3", "core-js": "^2.6.5", "lit": "^2.1.2", "lit-element": "^2.2.1" }, "devDependencies": { "@babel/cli": "^7.5.5", "@babel/core": "^7.5.5", "@babel/preset-env": "^7.5.5", "babel-loader": "^8.0.6", "eslint": "^6.1.0", "webpack": "^5.95.0", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1" }, "scripts": { "lint": "eslint --fix ./src", "start": "webpack --watch --config webpack/config.dev.js", "build": "webpack --config webpack/config.prod.js" } } ================================================ FILE: rollup-plugins/ignore.js ================================================ export default function (userOptions = {}) { // Files need to be absolute paths. // This only works if the file has no exports // and only is imported for its side effects const files = userOptions.files || []; if (files.length === 0) { return { name: 'ignore', }; } return { name: 'ignore', load(id) { return files.some(toIgnorePath => id.startsWith(toIgnorePath)) ? { code: '', } : null; }, }; } ================================================ FILE: src/buildElementDefinitions.js ================================================ const buildElementDefinitions = (elements, constructor) => elements.reduce((aggregate, element) => { if (element.defineId) { // eslint-disable-next-line no-param-reassign aggregate[element.defineId] = element; } else { element.promise.then((clazz) => { if (constructor.registry.get(element.name) === undefined) { constructor.registry.define(element.name, clazz); } }); } return aggregate; }, {}); export default buildElementDefinitions; ================================================ FILE: src/color-wheel/color-picker.js ================================================ import { LitElement, html } from 'lit'; import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; import { hs2rgb, rgb2hs } from "./convert-color"; class LmeColorPicker extends ScopedRegistryHost(LitElement) { static get properties() { return { hsColor: { type: Object, }, // use these properties to update the state via attributes desiredHsColor: { type: Object, observer: "applyHsColor", }, // use these properties to update the state via attributes desiredRgbColor: { type: Object, observer: "applyRgbColor", }, // width, height and radius apply to the coordinates of // of the canvas. // border width are relative to these numbers // the onscreen displayed size should be controlled with css // and should be the same or smaller width: { type: Number, value: 500, }, height: { type: Number, value: 500, }, radius: { type: Number, value: 225, }, // the amount segments for the hue // 0 = continuous gradient // other than 0 gives 'pie-pieces' hueSegments: { type: Number, value: 0, observer: "segmentationChange", }, // the amount segments for the hue // 0 = continuous gradient // 1 = only fully saturated // > 1 = segments from white to fully saturated saturationSegments: { type: Number, value: 0, observer: "segmentationChange", }, // set to true to make the segments purely esthetical // this allows selection off all collors, also // interpolated between the segments ignoreSegments: { type: Boolean, value: false, }, // throttle te amount of 'colorselected' events fired // value is timeout in milliseconds throttle: { type: Number, value: 500, }, }; } render() { console.log('test') return html`
`; } firstUpdated() { super.firstUpdated(); this.setupLayers(); this.drawColorWheel(); this.drawMarker(); if (this.desiredHsColor) { this.applyHsColor(this.desiredHsColor); } if (this.desiredRgbColor) { this.applyRgbColor(this.desiredRgbColor); } this.interactionLayer.addEventListener("mousedown", (ev) => this.onMouseDown(ev) ); this.interactionLayer.addEventListener("touchstart", (ev) => this.onTouchStart(ev) ); } // converts browser coordinates to canvas canvas coordinates // origin is wheel center // returns {x: X, y: Y} object convertToCanvasCoordinates(clientX, clientY) { const svgPoint = this.interactionLayer.createSVGPoint(); svgPoint.x = clientX; svgPoint.y = clientY; const cc = svgPoint.matrixTransform( this.interactionLayer.getScreenCTM().inverse() ); return { x: cc.x, y: cc.y }; } // Mouse events onMouseDown(ev) { const cc = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); // return if we're not on the wheel if (!this.isInWheel(cc.x, cc.y)) { return; } // a mousedown in wheel is always a color select action this.onMouseSelect(ev); // allow dragging this.canvas.classList.add("mouse", "dragging"); this.addEventListener("mousemove", this.onMouseSelect); this.addEventListener("mouseup", this.onMouseUp); } onMouseUp() { this.canvas.classList.remove("mouse", "dragging"); this.removeEventListener("mousemove", this.onMouseSelect); } onMouseSelect(ev) { requestAnimationFrame(() => this.processUserSelect(ev)); } // Touch events onTouchStart(ev) { const touch = ev.changedTouches[0]; const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY); // return if we're not on the wheel if (!this.isInWheel(cc.x, cc.y)) { return; } if (ev.target === this.marker) { // drag marker ev.preventDefault(); this.canvas.classList.add("touch", "dragging"); this.addEventListener("touchmove", this.onTouchSelect); this.addEventListener("touchend", this.onTouchEnd); return; } // don't fire color selection immediately, // wait for touchend and invalidate when we scroll this.tapBecameScroll = false; this.addEventListener("touchend", this.onTap); this.addEventListener( "touchmove", () => { this.tapBecameScroll = true; }, { passive: true } ); } onTap(ev) { if (this.tapBecameScroll) { return; } ev.preventDefault(); this.onTouchSelect(ev); } onTouchEnd() { this.canvas.classList.remove("touch", "dragging"); this.removeEventListener("touchmove", this.onTouchSelect); } onTouchSelect(ev) { requestAnimationFrame(() => this.processUserSelect(ev.changedTouches[0])); } /* * General event/selection handling */ // Process user input to color processUserSelect(ev) { const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); const hs = this.getColor(canvasXY.x, canvasXY.y); let rgb; if (!this.isInWheel(canvasXY.x, canvasXY.y)) { const [r, g, b] = hs2rgb([hs.h, hs.s]); rgb = { r, g, b }; } else { rgb = this.getRgbColor(canvasXY.x, canvasXY.y); } this.onColorSelect(hs, rgb); } // apply color to marker position and canvas onColorSelect(hs, rgb) { this.setMarkerOnColor(hs); // marker always follows mouse 'raw' hs value (= mouse position) if (!this.ignoreSegments) { // apply segments if needed hs = this.applySegmentFilter(hs); } // always apply the new color to the interface / canvas this.applyColorToCanvas(hs); // throttling is applied to updating the exposed colors (properties) // and firing of events if (this.colorSelectIsThrottled) { // make sure we apply the last selected color // eventually after throttle limit has passed clearTimeout(this.ensureFinalSelect); this.ensureFinalSelect = setTimeout(() => { this.fireColorSelected(hs, rgb); // do it for the final time }, this.throttle); return; } this.fireColorSelected(hs, rgb); // do it this.colorSelectIsThrottled = true; setTimeout(() => { this.colorSelectIsThrottled = false; }, this.throttle); } // set color values and fire colorselected event fireColorSelected(hs, rgb) { this.hsColor = hs; this.fire("colorselected", { hs, rgb }); } /* * Interface updating */ // set marker position to the given color setMarkerOnColor(hs) { if (!this.marker || !this.tooltip) { return; } const dist = hs.s * this.radius; const theta = ((hs.h - 180) / 180) * Math.PI; const markerdX = -dist * Math.cos(theta); const markerdY = -dist * Math.sin(theta); const translateString = `translate(${markerdX},${markerdY})`; this.marker.setAttribute("transform", translateString); this.tooltip.setAttribute("transform", translateString); } // apply given color to interface elements applyColorToCanvas(hs) { if (!this.interactionLayer) { return; } // we're not really converting hs to hsl here, but we keep it cheap // setting the color on the interactionLayer, the svg elements can inherit this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ 100 - hs.s * 50 }%)`; } applyHsColor(hs) { // do nothing is we already have the same color if (this.hsColor && this.hsColor.h === hs.h && this.hsColor.s === hs.s) { return; } this.setMarkerOnColor(hs); // marker is always set on 'raw' hs position if (!this.ignoreSegments) { // apply segments if needed hs = this.applySegmentFilter(hs); } this.hsColor = hs; // always apply the new color to the interface / canvas this.applyColorToCanvas(hs); } applyRgbColor(rgb) { const [h, s] = rgb2hs(rgb); this.applyHsColor({ h, s }); } /* * input processing helpers */ // get angle (degrees) getAngle(dX, dY) { const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right return angle; } // returns true when coordinates are in the colorwheel isInWheel(x, y) { return this.getDistance(x, y) <= 1; } // returns distance from wheel center, 0 = center, 1 = edge, >1 = outside getDistance(dX, dY) { return Math.sqrt(dX * dX + dY * dY) / this.radius; } /* * Getting colors */ getColor(x, y) { const hue = this.getAngle(x, y); // degrees, clockwise from right const relativeDistance = this.getDistance(x, y); // edge of radius = 1 const sat = Math.min(relativeDistance, 1); // Distance from center return { h: hue, s: sat }; } getRgbColor(x, y) { // get current pixel const imageData = this.backgroundLayer .getContext("2d") .getImageData(x + 250, y + 250, 1, 1); const pixel = imageData.data; return { r: pixel[0], g: pixel[1], b: pixel[2] }; } applySegmentFilter(hs) { // apply hue segment steps if (this.hueSegments) { const angleStep = 360 / this.hueSegments; const halfAngleStep = angleStep / 2; hs.h -= halfAngleStep; // take the 'centered segemnts' into account if (hs.h < 0) { hs.h += 360; } // don't end up below 0 const rest = hs.h % angleStep; hs.h -= rest - angleStep; } // apply saturation segment steps if (this.saturationSegments) { if (this.saturationSegments === 1) { hs.s = 1; } else { const segmentSize = 1 / this.saturationSegments; const saturationStep = 1 / (this.saturationSegments - 1); const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep; hs.s = Math.min(calculatedSat, 1); } } return hs; } /* * Drawing related stuff */ setupLayers() { this.canvas = this.$.canvas; this.backgroundLayer = this.$.backgroundLayer; this.interactionLayer = this.$.interactionLayer; // coordinate origin position (center of the wheel) this.originX = this.width / 2; this.originY = this.originX; // synchronise width/height coordinates this.backgroundLayer.width = this.width; this.backgroundLayer.height = this.height; this.interactionLayer.setAttribute( "viewBox", `${-this.originX} ${-this.originY} ${this.width} ${this.height}` ); } drawColorWheel() { /* * Setting up all paremeters */ let shadowColor; let shadowOffsetX; let shadowOffsetY; let shadowBlur; const context = this.backgroundLayer.getContext("2d"); // postioning and sizing const cX = this.originX; const cY = this.originY; const radius = this.radius; const counterClockwise = false; // styling of the wheel const wheelStyle = window.getComputedStyle(this.backgroundLayer, null); const borderWidth = parseInt( wheelStyle.getPropertyValue("--wheel-borderwidth"), 10 ); const borderColor = wheelStyle .getPropertyValue("--wheel-bordercolor") .trim(); const wheelShadow = wheelStyle.getPropertyValue("--wheel-shadow").trim(); // extract shadow properties from CSS variable // the shadow should be defined as: "10px 5px 5px 0px COLOR" if (wheelShadow !== "none") { const values = wheelShadow.split("px "); shadowColor = values.pop(); shadowOffsetX = parseInt(values[0], 10); shadowOffsetY = parseInt(values[1], 10); shadowBlur = parseInt(values[2], 10) || 0; } const borderRadius = radius + borderWidth / 2; const wheelRadius = radius; const shadowRadius = radius + borderWidth; /* * Drawing functions */ function drawCircle(hueSegments, saturationSegments) { hueSegments = hueSegments || 360; // reset 0 segments to 360 const angleStep = 360 / hueSegments; const halfAngleStep = angleStep / 2; // center segments on color for (let angle = 0; angle <= 360; angle += angleStep) { const startAngle = (angle - halfAngleStep) * (Math.PI / 180); const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180); context.beginPath(); context.moveTo(cX, cY); context.arc( cX, cY, wheelRadius, startAngle, endAngle, counterClockwise ); context.closePath(); // gradient const gradient = context.createRadialGradient( cX, cY, 0, cX, cY, wheelRadius ); let lightness = 100; // first gradient stop gradient.addColorStop(0, `hsl(${angle}, 100%, ${lightness}%)`); // segment gradient stops if (saturationSegments > 0) { const ratioStep = 1 / saturationSegments; let ratio = 0; for (let stop = 1; stop < saturationSegments; stop += 1) { const prevLighness = lightness; ratio = stop * ratioStep; lightness = 100 - 50 * ratio; gradient.addColorStop( ratio, `hsl(${angle}, 100%, ${prevLighness}%)` ); gradient.addColorStop(ratio, `hsl(${angle}, 100%, ${lightness}%)`); } gradient.addColorStop(ratio, `hsl(${angle}, 100%, 50%)`); } // last gradient stop gradient.addColorStop(1, `hsl(${angle}, 100%, 50%)`); context.fillStyle = gradient; context.fill(); } } function drawShadow() { context.save(); context.beginPath(); context.arc(cX, cY, shadowRadius, 0, 2 * Math.PI, false); context.shadowColor = shadowColor; context.shadowOffsetX = shadowOffsetX; context.shadowOffsetY = shadowOffsetY; context.shadowBlur = shadowBlur; context.fillStyle = "white"; context.fill(); context.restore(); } function drawBorder() { context.beginPath(); context.arc(cX, cY, borderRadius, 0, 2 * Math.PI, false); context.lineWidth = borderWidth; context.strokeStyle = borderColor; context.stroke(); } /* * Call the drawing functions * draws the shadow, wheel and border */ if (wheelStyle.shadow !== "none") { drawShadow(); } drawCircle(this.hueSegments, this.saturationSegments); if (borderWidth > 0) { drawBorder(); } } /* * Draw the (draggable) marker and tooltip * on the interactionLayer) */ drawMarker() { const svgElement = this.interactionLayer; const markerradius = this.radius * 0.08; const tooltipradius = this.radius * 0.15; const TooltipOffsetY = -(tooltipradius * 3); const TooltipOffsetX = 0; svgElement.marker = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); svgElement.marker.setAttribute("id", "marker"); svgElement.marker.setAttribute("r", markerradius); this.marker = svgElement.marker; svgElement.appendChild(svgElement.marker); svgElement.tooltip = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); svgElement.tooltip.setAttribute("id", "colorTooltip"); svgElement.tooltip.setAttribute("r", tooltipradius); svgElement.tooltip.setAttribute("cx", TooltipOffsetX); svgElement.tooltip.setAttribute("cy", TooltipOffsetY); this.tooltip = svgElement.tooltip; svgElement.appendChild(svgElement.tooltip); } segmentationChange() { if (this.backgroundLayer) { this.drawColorWheel(); } } } export default LmeColorPicker; ================================================ FILE: src/color-wheel/convert-color.js ================================================ export const rgb2hsv = (rgb) => { const [r, g, b] = rgb; const v = Math.max(r, g, b); const c = v - Math.min(r, g, b); const h = c && (v === r ? (g - b) / c : v === g ? 2 + (b - r) / c : 4 + (r - g) / c); return [60 * (h < 0 ? h + 6 : h), v && c / v, v]; }; export const hsv2rgb = (hsv) => { const [h, s, v] = hsv; const f = (n) => { const k = (n + h / 60) % 6; return v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); }; return [f(5), f(3), f(1)]; }; export const rgb2hs = (rgb) => rgb2hsv(rgb).slice(0, 2); export const hs2rgb = (hs) => hsv2rgb([hs[0], hs[1], 255]); ================================================ FILE: src/color-wheel/events-mixin.js ================================================ import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin"; import { fireEvent } from "./fire_event"; // Polymer legacy event helpers used courtesy of the Polymer project. // // Copyright (c) 2017 The Polymer Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /* @polymerMixin */ export const EventsMixin = dedupingMixin( (superClass) => class extends superClass { /** * Dispatches a custom event with an optional detail value. * * @param {string} type Name of event type. * @param {*=} detail Detail value containing event-specific * payload. * @param {{ bubbles: (boolean|undefined), cancelable: (boolean|undefined), composed: (boolean|undefined) }=} * options Object specifying options. These may include: * `bubbles` (boolean, defaults to `true`), * `cancelable` (boolean, defaults to false), and * `node` on which to fire the event (HTMLElement, defaults to `this`). * @return {Event} The new event that was fired. */ fire(type, detail, options) { options = options || {}; return fireEvent(options.node || this, type, detail, options); } } ); ================================================ FILE: src/color-wheel/fire_event.js ================================================ // Polymer legacy event helpers used courtesy of the Polymer project. // // Copyright (c) 2017 The Polymer Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** * Dispatches a custom event with an optional detail value. * * @param {string} type Name of event type. * @param {*=} detail Detail value containing event-specific * payload. * @param {{ bubbles: (boolean|undefined), * cancelable: (boolean|undefined), * composed: (boolean|undefined) }=} * options Object specifying options. These may include: * `bubbles` (boolean, defaults to `true`), * `cancelable` (boolean, defaults to false), and * `node` on which to fire the event (HTMLElement, defaults to `this`). * @return {Event} The new event that was fired. */ export const fireEvent = ( node, type, detail, options ) => { options = options || {}; detail = detail === null || detail === undefined ? {} : detail; const event = new Event(type, { bubbles: options.bubbles === undefined ? true : options.bubbles, cancelable: Boolean(options.cancelable), composed: options.composed === undefined ? true : options.composed, }); event.detail = detail; node.dispatchEvent(event); return event; }; ================================================ FILE: src/defaults.js ================================================ export default { shorten_cards: false, consolidate_entities: false, child_card: false, hide_header: false, show_header_icon: false, header: '', persist_features: false, brightness: true, color_temp: true, white_value: true, warm_white_value: true, color_picker: true, effects_list: true, speed: true, intensity: true, force_features: false, show_slider_percent: false, full_width_sliders: false, color_temp_in_kelvin: false, transition: 0, brightness_icon: 'weather-sunny', white_icon: 'file-word-box', warm_white_icon: 'weather-sunset', temperature_icon: 'thermometer', speed_icon: 'speedometer', intensity_icon: 'transit-connection-horizontal', }; ================================================ FILE: src/globalElementLoader.js ================================================ const globalElementLoader = name => ({ name, promise: customElements.whenDefined(name).then(() => customElements.get(name)), }); export default globalElementLoader; ================================================ FILE: src/index-editor.js ================================================ import { LitElement, html } from 'lit'; import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; import style from './style-editor'; import defaultConfig from './defaults'; import buildElementDefinitions from './buildElementDefinitions'; import globalElementLoader from './globalElementLoader'; export const fireEvent = (node, type, detail = {}, options = {}) => { const event = new Event(type, { bubbles: options.bubbles === undefined ? true : options.bubbles, cancelable: Boolean(options.cancelable), composed: options.composed === undefined ? true : options.composed, }); event.detail = detail; node.dispatchEvent(event); return event; }; export default class LightEntityCardEditor extends ScopedRegistryHost(LitElement) { static get elementDefinitions() { return buildElementDefinitions([ globalElementLoader('ha-checkbox'), globalElementLoader('ha-formfield'), globalElementLoader('ha-form-string'), globalElementLoader('ha-entity-picker'), ], LightEntityCardEditor); } static get styles() { return style; } static get properties() { return { hass: {}, _config: {} }; } setConfig(config) { this._config = { ...defaultConfig, ...config, }; } get entityDomains() { return ['switch', 'light', 'group']; } firstUpdated() { this._firstRendered = true; } render() { if (!this.hass || !this._config) { return html``; } // get header name let { header } = this._config; if (!header && this._config.entity) { let name = this._config.entity.split('.')[1] || ''; if (name) { name = name.charAt(0).toUpperCase() + name.slice(1); header = name; } } return html`
`; } entityChanged(ev) { if (!this._config || !this.hass || !this._firstRendered) return; const entity = ev.detail.value; if (entity === this._config.entity) return; this._config = { ...this._config, entity }; fireEvent(this, 'config-changed', { config: this._config }); } configChanged(ev) { if (!this._config || !this.hass || !this._firstRendered) return; const { target: { configValue, value }, detail: { value: checkedValue }, } = ev; if (checkedValue !== undefined && checkedValue !== null) { this._config = { ...this._config, [configValue]: checkedValue }; } else { this._config = { ...this._config, [configValue]: value }; } fireEvent(this, 'config-changed', { config: this._config }); } checkboxConfigChanged(ev) { if (!this._config || !this.hass || !this._firstRendered) return; const { target: { value, checked }, } = ev; this._config = { ...this._config, [value]: checked }; fireEvent(this, 'config-changed', { config: this._config }); } } ================================================ FILE: src/index.js ================================================ import { LitElement, html } from 'lit'; import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; import style from './style'; import defaultConfig from './defaults'; import LightEntityCardEditor from './index-editor'; import packageJson from '../package.json'; import buildElementDefinitions from './buildElementDefinitions'; import globalElementLoader from './globalElementLoader'; const editorName = 'light-entity-card-editor'; customElements.define(editorName, LightEntityCardEditor); console.info(`light-entity-card v${packageJson.version}`); class LightEntityCard extends ScopedRegistryHost(LitElement) { static get elementDefinitions() { return buildElementDefinitions( [ globalElementLoader('ha-card'), globalElementLoader('more-info-light'), globalElementLoader('ha-switch'), globalElementLoader('ha-icon'), globalElementLoader('state-badge'), globalElementLoader('ha-slider'), globalElementLoader('ha-hs-color-picker'), globalElementLoader('ha-select'), globalElementLoader('mwc-list-item'), ], LightEntityCard ); } static get properties() { return { hass: {}, config: {}, _colorPickerValues: { state: true }, }; } async firstUpdated() { this._firstUpdate = true; // ha-hs-color-picker is lazy-loaded by HA (only when more-info dialog opens). // Force it to load by opening a more-info dialog hidden via CSS, then closing it. const needsColorPicker = this.config.color_picker !== false && this.config.entity.startsWith('light.'); if (needsColorPicker && !customElements.get('ha-hs-color-picker')) { const ha = document.querySelector('home-assistant'); if (ha) { // Hide the dialog to prevent visible flash const hideStyle = document.createElement('style'); hideStyle.textContent = 'ha-more-info-dialog { display: none !important; }'; ha.shadowRoot.appendChild(hideStyle); const ev = new CustomEvent('hass-more-info', { detail: { entityId: this.config.entity }, bubbles: true, composed: true, }); ha.dispatchEvent(ev); try { await Promise.race([ customElements.whenDefined('ha-hs-color-picker'), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)), ]); } catch (e) { // Timed out — color picker element not available, continue gracefully } finally { // Always close the hidden dialog and remove the style const closeEv = new CustomEvent('hass-more-info', { detail: { entityId: '' }, bubbles: true, composed: true, }); ha.dispatchEvent(closeEv); hideStyle.remove(); } } this.requestUpdate(); } } /** * checks and saves config of entity * @param {*} config */ setConfig(config) { if (!config.entity) throw Error('entity required.'); this.config = { ...defaultConfig, ...config, }; } static async getConfigElement() { // eslint-disable-next-line no-undef return document.createElement(editorName); } static get featureNames() { return { brightness: 1, colorTemp: 2, effectList: 4, color: 16, whiteValue: 128, }; } static get cmdToggle() { return { on: 'turn_on', off: 'turn_off', }; } static get entityLength() { return { light: 10, switch: 1, }; } /** * get the current size of the card * @return {Number} */ getCardSize() { if (!this.config || !this.__hass || !this.__hass.states[this.config.entity]) { return 1; } let cardLength = 0; const entities = this.__hass.states[this.config.entity]; // if given a group entity then sum length of each entity by type // else just get the sible entity length if (Array.isArray(entities.attributes.entity_id)) { entities.attributes.entity_id.forEach(entity_id => (cardLength += this.getEntityLength(entity_id))); } else { cardLength += this.getEntityLength(entities.attributes.entity_id); } // if we are compacting the card account for that if (this.config.group) { cardLength *= 0.8; } return parseInt(cardLength, 10); } /** * determines the UI length of an entity * @param {string} entity_id */ getEntityLength(entity_id) { if (/^light\./.test(entity_id)) { return LightEntityCard.entityLength.light; } else if (/^switch\./.test(entity_id)) { return LightEntityCard.entityLength.switch; } else { return 0; } } /** * generates the CSS styles for this card * @return {TemplateResult} */ get styles() { return style; } /** * check if the given entity is on or off * @param {LightEntity} stateObj * @return {Boolean} */ isEntityOn(stateObj) { return stateObj.state === 'on'; } /** * generates a card for each given entiy in the config * @return {TemplateResult} */ render() { const entity = this.hass.states[this.config.entity]; if (!entity) { return html` ${`Invalid entity: ${this.config.entity}`} `; } this._stateObjects = this.getEntitiesToShow(entity); // need to find what state objects are actually going to be shown if (this.config.consolidate_entities) { this._shownStateObjects = [entity]; } else { this._shownStateObjects = [...this._stateObjects]; } const templates = this._shownStateObjects.reduce( (htmlTemplate, stateObj) => html`${htmlTemplate}${this.createEntityTemplate(stateObj)}`, // eslint-disable-next-line comma-dangle '' ); const css = `light-entity-card ${this.config.shorten_cards ? ' group' : ''} ${ this.config.child_card ? ' light-entity-child-card' : '' }`; return html` ${templates} `; } /** * gets all the entities we need to build this card for * @param {LightEntity|GroupEntity} entities * @return {Array} */ getEntitiesToShow(entities) { if (entities.attributes.entity_id && Array.isArray(entities.attributes.entity_id)) return entities.attributes.entity_id.map(entity_id => this.hass.states[entity_id]).filter(Boolean); return [entities]; } /** * creates an entity's template * @param {LightEntity} stateObj * @return {TemplateResult} */ createEntityTemplate(stateObj) { const sliderClass = this.config.full_width_sliders ? 'ha-slider-full-width' : ''; return html` ${this.createHeader(stateObj)}
${this.createBrightnessSlider(stateObj)} ${this.createSpeedSlider(stateObj)} ${this.createIntensitySlider(stateObj)} ${this.createColorTemperature(stateObj)} ${this.createWhiteValue(stateObj)} ${this.createWarmWhiteValue(stateObj)}
${this.createColorPicker(stateObj)} ${this.createEffectList(stateObj)} `; } /** * creates card header with state toggle for a given entity * @param {LightEntity} stateObj * @return {TemplateResult} */ createHeader(stateObj) { if (this.config.hide_header) return html``; const title = this.config.header || stateObj.attributes.friendly_name || stateObj.entity_id; return html`
${this.showHeaderIcon(stateObj)}
${title}
this.setToggle(e, stateObj)}>
`; } showHeaderIcon(stateObj) { if (!this.config.show_header_icon) return html``; return html`
`; } /** * creates brightness slider * @param {LightEntity} stateObj * @return {TemplateResult} */ createBrightnessSlider(stateObj) { if (this.config.brightness === false) return html``; if (this.dontShowFeature('brightness', stateObj)) return html``; return html`
${this.showPercent(stateObj.attributes.brightness, 0, 254, 'brightness')}
`; } /** * creates speed slider * @param {LightEntity} stateObj * @return {TemplateResult} */ createSpeedSlider(stateObj) { if (this.config.speed === false) return html``; if (this.dontShowFeature('speed', stateObj)) return html``; return html`
${this.showPercent(stateObj.attributes.speed, 0, 254)}
`; } /** * creates intensity slider * @param {LightEntity} stateObj * @return {TemplateResult} */ createIntensitySlider(stateObj) { if (this.config.intensity === false) return html``; if (this.dontShowFeature('intensity', stateObj)) return html``; return html`
${this.showPercent(stateObj.attributes.intensity, 0, 254)}
`; } /** * shows slider value label if config is set * @param {number} value * @param {number} min * @param {number} max * @param {string} [sliderType] - 'brightness' or 'color_temp' for per-slider overrides * @return {TemplateResult} */ showPercent(value, min, max, sliderType) { // Per-slider visibility overrides if (sliderType === 'brightness' && this.config.show_brightness_percent !== undefined) { if (!this.config.show_brightness_percent) return html``; } else if (sliderType === 'color_temp' && this.config.show_color_temp_percent !== undefined) { if (!this.config.show_color_temp_percent) return html``; } else if (sliderType === 'color_temp' && this.config.color_temp_in_kelvin) { // color_temp_in_kelvin implies showing the value label } else if (!this.config.show_slider_percent) { return html``; } // Show kelvin for color temp if configured (slider value is already in kelvin) if (sliderType === 'color_temp' && this.config.color_temp_in_kelvin) { return html`
${value}K
`; } let percent = parseInt(((value - min) * 100) / (max - min), 10); if (isNaN(percent)) percent = 0; return html`
${percent}%
`; } /** * creates color temperature slider for a given entity * @param {LightEntity} stateObj * @return {TemplateResult} */ createColorTemperature(stateObj) { if (this.config.color_temp === false) return html``; if (this.dontShowFeature('colorTemp', stateObj)) return html``; // HA 2026.3+ uses kelvin-based attributes; fall back to mireds for older HA const usesKelvin = stateObj.attributes.min_color_temp_kelvin !== undefined; const showInKelvin = this.config.color_temp_in_kelvin; let currentTemp, minTemp, maxTemp; if (showInKelvin) { // Slider works in kelvin so the native popup shows kelvin values. // Kelvin: low=warm, high=cool — slider left=warm, right=cool. let minK, maxK, kelvin; if (usesKelvin) { kelvin = stateObj.attributes.color_temp_kelvin; minK = stateObj.attributes.min_color_temp_kelvin; maxK = stateObj.attributes.max_color_temp_kelvin; } else { kelvin = stateObj.attributes.color_temp ? Math.round(1000000 / stateObj.attributes.color_temp) : null; minK = stateObj.attributes.max_mireds ? Math.round(1000000 / stateObj.attributes.max_mireds) : null; maxK = stateObj.attributes.min_mireds ? Math.round(1000000 / stateObj.attributes.min_mireds) : null; } if (!minK || !maxK) return html``; const midpoint = Math.round((minK + maxK) / 2); currentTemp = (typeof kelvin === 'number' && Number.isFinite(kelvin) && kelvin > 0) ? kelvin : null; minTemp = minK; maxTemp = maxK; const sliderValue = currentTemp || midpoint; const label = this.showPercent(sliderValue, minTemp, maxTemp, 'color_temp'); return html`
${label}
`; } // Compute mired range for percentage-based slider. // Slider works in 0–100 so the native popup shows a percentage value. let minMired, maxMired, currentMired; if (usesKelvin) { const kelvin = stateObj.attributes.color_temp_kelvin; const minK = stateObj.attributes.min_color_temp_kelvin; const maxK = stateObj.attributes.max_color_temp_kelvin; if (!minK || !maxK) return html``; currentMired = (typeof kelvin === 'number' && Number.isFinite(kelvin) && kelvin > 0) ? Math.round(1000000 / kelvin) : null; minMired = Math.round(1000000 / maxK); maxMired = Math.round(1000000 / minK); } else { currentMired = stateObj.attributes.color_temp; minMired = stateObj.attributes.min_mireds; maxMired = stateObj.attributes.max_mireds; if (!minMired || !maxMired) return html``; } const miredRange = (maxMired && minMired) ? maxMired - minMired : 0; const percentValue = (miredRange > 0 && currentMired != null) ? Math.round(((currentMired - minMired) / miredRange) * 100) : 50; const label = this.showPercent(percentValue, 0, 100, 'color_temp'); return html`
${label}
`; } /** * gets the current white value from entity state * supports modern rgbw_color/rgbww_color and legacy white_value * @param {LightEntity} stateObj * @param {number} index - index in the color tuple (3 for white/cool white, 4 for warm white) * @return {number} */ getWhiteValue(stateObj, index = 3) { const rgbwColor = stateObj.attributes.rgbw_color; const rgbwwColor = stateObj.attributes.rgbww_color; if (rgbwColor && index === 3) return rgbwColor[3] || 0; if (rgbwwColor && index < rgbwwColor.length) return rgbwwColor[index] || 0; // Legacy fallback if (index === 3 && stateObj.attributes.white_value !== undefined) { return stateObj.attributes.white_value ?? 0; } return 0; } /** * creates white value slider for a given entity * supports modern RGBW/RGBWW color modes and legacy white_value * @param {LightEntity} stateObj * @return {TemplateResult} */ createWhiteValue(stateObj) { if (this.config.white_value === false) return html``; if (this.dontShowFeature('whiteValue', stateObj)) return html``; const whiteValue = this.getWhiteValue(stateObj, 3); return html`
${this.showPercent(whiteValue, 0, 254)}
`; } /** * creates warm white value slider for RGBWW entities * @param {LightEntity} stateObj * @return {TemplateResult} */ createWarmWhiteValue(stateObj) { if (this.config.warm_white_value === false) return html``; if (this.dontShowFeature('warmWhiteValue', stateObj)) return html``; const warmWhiteValue = this.getWhiteValue(stateObj, 4); return html`
${this.showPercent(warmWhiteValue, 0, 254)}
`; } /** * sets the white value for RGBW/RGBWW lights using modern color mode API * falls back to legacy white_value for older HA installations * @param {CustomEvent} event * @param {LightEntity} stateObj * @param {number} index - 3 for white/cool white, 4 for warm white */ _setWhiteValue(event, stateObj, index) { const newValue = parseInt(event.target.value, 10); if (isNaN(newValue)) return; const colorModes = stateObj.attributes.supported_color_modes || []; const rgbwColor = stateObj.attributes.rgbw_color; const rgbwwColor = stateObj.attributes.rgbww_color; if (colorModes.includes('rgbw') && index === 3) { const rgb = rgbwColor ? rgbwColor.slice(0, 3) : (stateObj.attributes.rgb_color || [255, 255, 255]); this.callEntityService({ rgbw_color: [rgb[0], rgb[1], rgb[2], newValue] }, stateObj); } else if (colorModes.includes('rgbww')) { let base; if (rgbwwColor) { base = rgbwwColor; } else if (rgbwColor) { base = [rgbwColor[0], rgbwColor[1], rgbwColor[2], rgbwColor[3], 0]; } else { const rgb = stateObj.attributes.rgb_color || [255, 255, 255]; base = [rgb[0], rgb[1], rgb[2], 0, 0]; } const newColor = [...base]; newColor[index] = newValue; this.callEntityService({ rgbww_color: newColor }, stateObj); } else { // Legacy fallback this.callEntityService({ white_value: newValue }, stateObj); } } /** * creates effect list dropdown for a given entity * @param {LightEntity} stateObj * @return {TemplateResult} */ createEffectList(stateObj) { // do we disable effect list always? if (this.config.effects_list === false) return html``; // need to check state and persist_features here because if given custom effect list we may // want to sho that even if the feature doesn't exist so dont check that part to move forward just persist_features/state if (!this.config.persist_features && !this.isEntityOn(stateObj)) return html``; let effect_list = stateObj.attributes.effect_list || []; // if we were given a custom list then use that if (this.config.effects_list && Array.isArray(this.config.effects_list)) { effect_list = this.config.effects_list; } else if (this.config.effects_list && this.hass.states[this.config.effects_list]) { // else if given an input_select entity use that as effect list const inputSelect = this.hass.states[this.config.effects_list]; effect_list = (inputSelect.attributes && inputSelect.attributes.options) || []; } else if (this.dontShowFeature('effectList', stateObj)) { // finally if no custom list nor feature exists then dont show effect list return html``; } const listItems = effect_list.map(effect => this.createListItem(stateObj, effect)); const caption = this.hass.localize('ui.card.light.effect') || 'Effect'; return html`
this.setEffect(e, stateObj)} label="${caption}" > ${listItems}
`; } createListItem(stateObj, effect) { return html`${effect}`; } /** * creates color picker wheel for a given entity * @param {LightEntity} stateObj * @return {TemplateResult} */ createColorPicker(stateObj) { if (this.config.color_picker === false) return html``; if (this.dontShowFeature('color', stateObj)) return html``; // ha-hs-color-picker uses saturation 0-1, HA uses 0-100 const haHs = stateObj.attributes.hs_color || [0, 0]; const pickerValue = (this._colorPickerValues && this._colorPickerValues[stateObj.entity_id]) || [haHs[0], haHs[1] / 100]; return html`
{ this._colorPickerValues = { ...this._colorPickerValues, [stateObj.entity_id]: e.detail.value }; }} @value-changed=${(e) => this._onColorPickerChanged(e.detail.value, stateObj)} >
`; } /** * do we show a feature or not? * @param {string} featureName * @param {LightEntity} stateObj * @return {boolean} */ dontShowFeature(featureName, stateObj) { // show all feature if this is set to true if (this.config.force_features) return false; // WLED support if (featureName === 'speed' && 'speed' in stateObj.attributes) return false; if (featureName === 'intensity' && 'intensity' in stateObj.attributes) return false; // old deprecated way to seeing if supported feature let featureSupported = LightEntityCard.featureNames[featureName] & stateObj.attributes.supported_features; // support new color modes https://developers.home-assistant.io/docs/core/entity/light/#color-modes const colorModes = stateObj.attributes.supported_color_modes || []; if (!featureSupported) { switch (featureName) { case 'brightness': featureSupported = Object.prototype.hasOwnProperty.call(stateObj.attributes, 'brightness'); if (!featureSupported) { const supportedModes = ['hs', 'rgb', 'rgbw', 'rgbww', 'white', 'brightness', 'color_temp', 'xy']; featureSupported = [...new Set(colorModes.filter(mode => supportedModes.includes(mode)))].length > 0; } break; case 'colorTemp': if (colorModes) { const supportedModes = ['color_temp']; featureSupported = [...new Set(colorModes.filter(mode => supportedModes.includes(mode)))].length > 0; } break; case 'effectList': featureSupported = stateObj.attributes.effect_list && stateObj.attributes.effect_list.length; break; case 'color': { const supportedModes = ['hs', 'rgb', 'rgbw', 'rgbww', 'xy']; featureSupported = [...new Set(colorModes.filter(mode => supportedModes.includes(mode)))].length > 0; break; } case 'whiteValue': featureSupported = Object.prototype.hasOwnProperty.call(stateObj.attributes, 'white_value'); if (!featureSupported) { const supportedModes = ['rgbw', 'rgbww']; featureSupported = colorModes.some(mode => supportedModes.includes(mode)); } break; case 'warmWhiteValue': { const supportedModes = ['rgbww']; featureSupported = colorModes.some(mode => supportedModes.includes(mode)); break; } default: featureSupported = false; break; } } if (!featureSupported) return true; if (!this.config.persist_features && !this.isEntityOn(stateObj)) return true; return false; } /** * change to hs color for a given entity * @param {HSV} hsv * @param {LightEntity} stateObj */ _onColorPickerChanged(value, stateObj) { if (this._colorPickerValues) { const { [stateObj.entity_id]: _, ...rest } = this._colorPickerValues; this._colorPickerValues = rest; } this.setColorPicker(value, stateObj); } setColorPicker(value, stateObj) { if (!value) return; // Convert saturation back from 0-1 to 0-100 for HA this.callEntityService({ hs_color: [value[0], value[1] * 100] }, stateObj); } _setValue(event, stateObj, valueName) { const newValue = parseInt(event.target.value, 10); if (isNaN(newValue) || parseInt(stateObj.attributes[valueName], 10) === newValue) return; this.callEntityService({ [valueName]: newValue }, stateObj); } /** * handles color temperature slider changes, converting to the correct unit for HA * @param {CustomEvent} event * @param {LightEntity} stateObj * @param {boolean} usesKelvin - whether the HA entity uses kelvin-based attributes * @param {boolean} sliderInKelvin - whether the slider value is in kelvin * @param {number} [minMired] - min mired value (needed for percentage→mired conversion) * @param {number} [maxMired] - max mired value (needed for percentage→mired conversion) */ _setColorTemp(event, stateObj, usesKelvin, sliderInKelvin, minMired, maxMired) { const rawValue = parseInt(event.target.value, 10); if (isNaN(rawValue)) return; if (sliderInKelvin) { // Slider value is kelvin if (usesKelvin) { if (rawValue === parseInt(stateObj.attributes.color_temp_kelvin, 10)) return; this.callEntityService({ color_temp_kelvin: rawValue }, stateObj); } else { const miredValue = Math.round(1000000 / rawValue); if (miredValue === parseInt(stateObj.attributes.color_temp, 10)) return; this.callEntityService({ color_temp: miredValue }, stateObj); } } else { // Slider value is percentage (0–100), convert back to mireds if (!Number.isFinite(minMired) || !Number.isFinite(maxMired) || maxMired <= minMired) return; const miredValue = Math.round(minMired + (rawValue / 100) * (maxMired - minMired)); if (usesKelvin) { const kelvinValue = Math.round(1000000 / miredValue); if (kelvinValue === parseInt(stateObj.attributes.color_temp_kelvin, 10)) return; this.callEntityService({ color_temp_kelvin: kelvinValue }, stateObj); } else { if (miredValue === parseInt(stateObj.attributes.color_temp, 10)) return; this.callEntityService({ color_temp: miredValue }, stateObj); } } } /** * sets the toggle state based on the given entity state * @param {CustomEvent} event * @param {LightEntity} stateObj */ setToggle(event, stateObj) { const newState = this.isEntityOn(stateObj) ? LightEntityCard.cmdToggle.off : LightEntityCard.cmdToggle.on; this.callEntityService({}, stateObj, newState); } /** * sets the current effect selected for an entity * @param {CustomEvent} event * @param {LightEntity} entity */ setEffect(event, stateObj) { if(!event.target.value ) return; this.callEntityService({ effect: event.target.value }, stateObj); } /** * call light service to update a state of an entity * @param {Object} payload * @param {LightEntity} entity * @param {String} state */ callEntityService(payload, stateObj, state) { if(!this._firstUpdate) return; let entityType = stateObj.entity_id.split('.')[0]; if (entityType === 'group') entityType = 'homeassistant'; const transition = parseFloat(this.config.transition) || 0; if (transition > 0 && entityType === 'light') { payload = { ...payload, transition }; } this.hass.callService(entityType, state || LightEntityCard.cmdToggle.on, { entity_id: stateObj.entity_id, ...payload, }); } } customElements.define('light-entity-card', LightEntityCard); window.customCards = window.customCards || []; window.customCards.push({ type: 'light-entity-card', name: 'Light Entity Card', description: 'Control lights and switches', }); ================================================ FILE: src/style-editor.js ================================================ import { css } from 'lit'; const style = css` .entities { padding-top: 10px; padding-bottom: 10px; display: flex; } .entities ha-formfield { display: block; margin-bottom: 10px; margin-left: 10px; } .checkbox-options { display: flex; } ha-entity-picker { width: 100%; } .checkbox-options ha-formfield, .entities mwc-switch, .entities ha-form-string { padding-right: 2%; width: 48%; } .checkbox-options ha-formfield { margin-top: 10px; } .overall-config { margin-bottom: 20px; } `; export default style; ================================================ FILE: src/style.js ================================================ import { css } from 'lit'; const style = css` .light-entity-card { padding: 16px; } .light-entity-child-card { box-shadow: none !important; padding: 0 !important; } .light-entity-card.group { padding-bottom: 5; padding-top: 0; } .ha-slider-full-width ha-slider { width: 100%; } .percent-slider { color: var(--primary-text-color); display: flex; justify-content: center; align-items: center; margin-left: 8px; min-width: 40px; text-align: right; } .light-entity-card__header { display: flex; justify-content: space-between; @apply --paper-font-headline; line-height: 40px; color: var(--primary-text-color); } .group .light-entity-card__header { } .light-entity-card-sliders > div { margin-top: 10px; } .group .light-entity-card-sliders > div { margin-top: 0px; } .light-entity-card__toggle { display: flex; cursor: pointer; } .light-entity-card__color-picker { display: flex; justify-content: center; margin-top: 10px; } .light-entity-card__color-picker ha-hs-color-picker { max-width: 300px; width: 100%; } .light-entity-card-color_temp { background-image: var(--ha-slider-background, linear-gradient(to right, #a6d1ff, #ffb74d)); border-radius: 4px; } .light-entity-card-color_temp--kelvin { background-image: var(--ha-slider-background, linear-gradient(to right, #ffb74d, #a6d1ff)); } .light-entity-card-effectlist { padding-top: 10px; padding-bottom: 10px; } .group .light-entity-card-effectlist { padding-bottom: 20px; } .light-entity-card-center { display: flex; justify-content: center; align-items: center; cursor: pointer; } .hidden { display: none; } .icon-container { display: flex; justify-content: center; align-items: center; margin-right: 8px; } `; export default style; ================================================ FILE: webpack/config.common.js ================================================ const path = require("path"); module.exports = { entry: "./src/index.js", devtool: "source-map", output: { filename: "light-entity-card.js", path: path.resolve(__dirname, "../dist"), }, module: { rules: [ { test: /\.js$/, include: [/node_modules(?:\/|\\)lit-element|lit-html/], use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"], }, }, }, ], }, }; ================================================ FILE: webpack/config.dev.js ================================================ const { merge } = require('webpack-merge'); const commonConfig = require('./config.common'); module.exports = merge(commonConfig, { mode: 'development' }); ================================================ FILE: webpack/config.prod.js ================================================ const { merge } = require('webpack-merge'); const commonConfig = require('./config.common'); module.exports = merge(commonConfig, { mode: 'production', devtool: false, optimization: { minimize: true }, output: { publicPath: '/local/' }, });