├── .npmrc ├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── release.yml │ └── build.yml ├── .gitignore ├── .devcontainer ├── configuration.yaml ├── devcontainer.json └── ui-lovelace.yaml ├── hacs.json ├── .prettierrc.js ├── CHANGELOG.md ├── src ├── types.ts ├── utils.ts ├── deep-replace.ts └── decluttering-card.ts ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── release.config.js ├── rollup.config.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version=false 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [RomRider] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | yarn.lock 3 | .rpt2_cache/ 4 | /dist/** 5 | -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | lovelace: 3 | mode: yaml 4 | demo: -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Decluttering Card", 3 | "render_readme": true, 4 | "filename": "decluttering-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (2023-04-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * card broken with HA 2023.4 beta and up ([0ccd5b0](https://github.com/custom-cards/decluttering-card/commit/0ccd5b05a99202c80de21606df1b8e94ea9ee668)), closes [#63](https://github.com/custom-cards/decluttering-card/issues/63) 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | open-pull-requests-limit: 0 8 | target-branch: 'dev' 9 | - package-ecosystem: 'github-actions' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | target-branch: 'dev' -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export interface DeclutteringCardConfig { 3 | variables?: VariablesConfig[]; 4 | template: string; 5 | } 6 | 7 | export interface VariablesConfig { 8 | [key: string]: any; 9 | } 10 | 11 | export interface TemplateConfig { 12 | default: VariablesConfig[]; 13 | card?: any; 14 | element?: any; 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.vscode-pull-request-github", 4 | "eamodio.gitlens", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "bierner.lit-html", 8 | "runem.lit-plugin", 9 | "auchenberg.vscode-browser-preview", 10 | "davidanson.vscode-markdownlint", 11 | "redhat.vscode-yaml", 12 | "msjsdiag.debugger-for-chrome", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Chrome Localhost", 11 | "url": "http://localhost:8123", 12 | "webRoot": "${workspaceFolder}/dist", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2017", 8 | "dom", 9 | "dom.iterable" 10 | ], 11 | "plugins": [ 12 | { 13 | "name": "ts-lit-plugin" 14 | } 15 | ], 16 | "noEmit": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "strict": true, 21 | "noImplicitAny": false, 22 | "skipLibCheck": true, 23 | "resolveJsonModule": true, 24 | "experimentalDecorators": true, 25 | "sourceMap": true, 26 | }, 27 | "include": [ 28 | "src/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [{ 4 | "label": "Run Home Assistant on port 9123", 5 | "type": "shell", 6 | "command": "dc start", 7 | "problemMatcher": [] 8 | }, 9 | { 10 | "label": "Run Home Assistant configuration against /config", 11 | "type": "shell", 12 | "command": "dc check", 13 | "problemMatcher": [] 14 | }, 15 | { 16 | "label": "Upgrade Home Assistant to latest dev", 17 | "type": "shell", 18 | "command": "dc install", 19 | "problemMatcher": [] 20 | }, 21 | { 22 | "label": "Install a spesific version of Home Assistant", 23 | "type": "shell", 24 | "command": "dc set-version", 25 | "problemMatcher": [] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | experimentalDecorators: true, 12 | }, 13 | rules: { 14 | "@typescript-eslint/camelcase": 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2018, Alexandre Garcia 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release-bundle: 7 | runs-on: ubuntu-latest 8 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' 9 | 10 | outputs: 11 | new_release_published: ${{ steps.semantic.outputs.new_release_published }} 12 | new_release_version: ${{ steps.semantic.outputs.new_release_version }} 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js 18.x 17 | uses: actions/setup-node@v3.6.0 18 | with: 19 | node-version: 18.x 20 | - name: Cache Node.js modules 21 | uses: actions/cache@v3.3.1 22 | with: 23 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 24 | key: ${{ runner.OS }}-node-18.x-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.OS }}-node-18.x 27 | ${{ runner.OS }}- 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Release 31 | run: npx semantic-release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Decluttering Card Development", 4 | "image": "ghcr.io/ludeeus/devcontainer/generic:stable", 5 | "context": "..", 6 | "remoteUser": "vscode", 7 | "appPort": [ 8 | "5000:5000", 9 | "9123:8123" 10 | ], 11 | "postCreateCommand": "npm install", 12 | "runArgs": [ 13 | "-v", 14 | "${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" // This is added so you can push from inside the container 15 | ], 16 | "extensions": [ 17 | "github.vscode-pull-request-github", 18 | "eamodio.gitlens", 19 | "dbaeumer.vscode-eslint", 20 | "esbenp.prettier-vscode", 21 | "bierner.lit-html", 22 | "runem.lit-plugin", 23 | "auchenberg.vscode-browser-preview", 24 | "davidanson.vscode-markdownlint", 25 | "redhat.vscode-yaml", 26 | "msjsdiag.debugger-for-chrome", 27 | ], 28 | "settings": { 29 | "files.eol": "\n", 30 | "editor.tabSize": 2, 31 | "terminal.integrated.shell.linux": "/bin/bash", 32 | "editor.formatOnPaste": false, 33 | "editor.formatOnSave": true, 34 | "editor.formatOnType": true, 35 | "files.trimTrailingWhitespace": true 36 | } 37 | } -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | module.exports = { 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | [ 6 | '@semantic-release/release-notes-generator', 7 | { 8 | preset: 'conventionalcommits', 9 | presetConfig: { 10 | types: [ 11 | { type: 'feat', section: 'Features' }, 12 | { type: 'fix', section: 'Bug Fixes' }, 13 | { type: 'doc', hidden: false, section: 'Documentation' }, 14 | { type: 'docs', hidden: false, section: 'Documentation' }, 15 | { type: 'chore', hidden: true, section: 'Chores' }, 16 | ], 17 | }, 18 | }, 19 | ], 20 | '@semantic-release/changelog', 21 | [ 22 | '@semantic-release/npm', 23 | { 24 | npmPublish: false, 25 | }, 26 | ], 27 | [ 28 | '@semantic-release/git', 29 | { 30 | assets: ['CHANGELOG.md', 'README.md', 'package.json', 'yarn.lock'], 31 | }, 32 | ], 33 | [ 34 | '@semantic-release/github', 35 | { 36 | assets: 'dist/*.js', 37 | }, 38 | ], 39 | ], 40 | preset: 'conventionalcommits', 41 | branches: [{ name: 'master' }, { name: 'dev', channel: 'beta', prerelease: true }], 42 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | **Checklist:** 16 | 17 | - [ ] I updated to the latest version available 18 | - [ ] I cleared the cache of my browser 19 | 20 | **Release with the issue:** 21 | 22 | **Last working release (if known):** 23 | 24 | **Browser and Operating System:** 25 | 26 | 29 | 30 | **Description of problem:** 31 | 32 | 35 | 36 | **Javascript errors shown in the web inspector (if applicable):** 37 | 38 | ``` 39 | 40 | ``` 41 | 42 | **Additional information:** 43 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { LovelaceConfig } from 'custom-card-helpers'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export function getLovelaceCast(): any { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | let root: any = document.querySelector('hc-main'); 7 | root = root && root.shadowRoot; 8 | root = root && root.querySelector('hc-lovelace'); 9 | root = root && root.shadowRoot; 10 | root = root && root.querySelector('hui-view'); 11 | if (root) { 12 | const ll = root.lovelace; 13 | ll.current_view = root.___curView; 14 | return ll; 15 | } 16 | return null; 17 | } 18 | 19 | export function getLovelace(): LovelaceConfig | null { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | let root: any = document.querySelector('home-assistant'); 22 | root = root && root.shadowRoot; 23 | root = root && root.querySelector('home-assistant-main'); 24 | root = root && root.shadowRoot; 25 | root = root && root.querySelector('app-drawer-layout partial-panel-resolver, ha-drawer partial-panel-resolver'); 26 | root = (root && root.shadowRoot) || root; 27 | root = root && root.querySelector('ha-panel-lovelace'); 28 | root = root && root.shadowRoot; 29 | root = root && root.querySelector('hui-root'); 30 | if (root) { 31 | const ll = root.lovelace; 32 | ll.current_view = root.___curView; 33 | return ll; 34 | } 35 | return null; 36 | } 37 | -------------------------------------------------------------------------------- /src/deep-replace.ts: -------------------------------------------------------------------------------- 1 | import { VariablesConfig, TemplateConfig } from './types'; 2 | import { LovelaceCardConfig } from 'custom-card-helpers'; 3 | 4 | export default (variables: VariablesConfig[] | undefined, templateConfig: TemplateConfig): LovelaceCardConfig => { 5 | if (!variables && !templateConfig.default) { 6 | return templateConfig.card; 7 | } 8 | let variableArray: VariablesConfig[] = []; 9 | if (variables) { 10 | variableArray = variables.slice(0); 11 | } 12 | if (templateConfig.default) { 13 | variableArray = variableArray.concat(templateConfig.default); 14 | } 15 | let jsonConfig = templateConfig.card ? JSON.stringify(templateConfig.card) : JSON.stringify(templateConfig.element); 16 | variableArray.forEach(variable => { 17 | const key = Object.keys(variable)[0]; 18 | const value = Object.values(variable)[0]; 19 | if (typeof value === 'number' || typeof value === 'boolean') { 20 | const rxp2 = new RegExp(`"\\[\\[${key}\\]\\]"`, 'gm'); 21 | jsonConfig = jsonConfig.replace(rxp2, (value as unknown) as string); 22 | } 23 | if (typeof value === 'object') { 24 | const rxp2 = new RegExp(`"\\[\\[${key}\\]\\]"`, 'gm'); 25 | const valueString = JSON.stringify(value); 26 | jsonConfig = jsonConfig.replace(rxp2, valueString); 27 | } else { 28 | const rxp = new RegExp(`\\[\\[${key}\\]\\]`, 'gm'); 29 | jsonConfig = jsonConfig.replace(rxp, value); 30 | } 31 | }); 32 | return JSON.parse(jsonConfig); 33 | }; 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import babel from '@rollup/plugin-babel'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import serve from 'rollup-plugin-serve'; 7 | import json from '@rollup/plugin-json'; 8 | 9 | // eslint-disable-next-line no-undef 10 | const dev = process.env.ROLLUP_WATCH; 11 | 12 | const serveopts = { 13 | contentBase: ['./dist'], 14 | host: '0.0.0.0', 15 | port: 5000, 16 | allowCrossOrigin: true, 17 | headers: { 18 | 'Access-Control-Allow-Origin': '*', 19 | }, 20 | }; 21 | 22 | const plugins = [ 23 | nodeResolve({}), 24 | commonjs(), 25 | typescript(), 26 | json(), 27 | babel({ 28 | exclude: 'node_modules/**', 29 | babelHelpers: 'bundled', 30 | babelrc: false, 31 | presets: [ 32 | '@babel/preset-env', 33 | { 34 | useBuiltIns: 'entry', 35 | targets: '> 0.25%, not dead', 36 | }, 37 | ], 38 | }), 39 | dev && serve(serveopts), 40 | !dev && 41 | terser({ 42 | // format: { 43 | // comments: false, 44 | // }, 45 | mangle: { 46 | safari10: true, 47 | }, 48 | }), 49 | ]; 50 | 51 | export default [ 52 | { 53 | input: 'src/decluttering-card.ts', 54 | output: { 55 | dir: './dist', 56 | format: 'es', 57 | sourcemap: dev ? true : false, 58 | }, 59 | plugins: [...plugins], 60 | watch: { 61 | exclude: 'node_modules/**', 62 | }, 63 | }, 64 | ]; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js 18.x 17 | uses: actions/setup-node@v3.6.0 18 | with: 19 | node-version: 18.x 20 | - name: Cache Node.js modules 21 | uses: actions/cache@v3.3.1 22 | with: 23 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 24 | key: ${{ runner.OS }}-node-18.x-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.OS }}-node-18.x 27 | ${{ runner.OS }}- 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Lint the code 31 | run: npm run lint --if-present 32 | build: 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | node-version: [18.x] 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v3.6.0 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | - name: Cache Node.js modules 46 | uses: actions/cache@v3.3.1 47 | with: 48 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 49 | key: ${{ runner.OS }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 50 | restore-keys: | 51 | ${{ runner.OS }}-node-${{ matrix.node-version }} 52 | ${{ runner.OS }}- 53 | - name: Install dependencies 54 | run: npm ci 55 | - name: Build the project 56 | run: npm run build --if-present -------------------------------------------------------------------------------- /.devcontainer/ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - url: http://127.0.0.1:5000/decluttering-card.js 3 | type: module 4 | - url: https://cdn.jsdelivr.net/gh/thomasloven/lovelace-card-mod@master/card-mod.js 5 | type: module 6 | 7 | decluttering_templates: 8 | my_first_template: # This is the name of a template 9 | default: 10 | - icon: fire 11 | card: 12 | type: button 13 | name: '[[name]]' 14 | icon: 'mdi:[[icon]]' 15 | large_divider: 16 | default: 17 | - opacity: 0.25 18 | card: 19 | type: divider 20 | style: 21 | background-color: var(--secondary-text-color) 22 | height: 1px 23 | margin: 15px auto 24 | opacity: '[[opacity]]' 25 | demo_icon: 26 | element: 27 | type: icon 28 | icon: mdi:alert-circle 29 | title: Problem detected! 30 | entity: '[[entity]]' 31 | tap_action: 32 | action: more-info 33 | style: 34 | right: 50% 35 | top: 50% 36 | color: var(--google-red-500) 37 | filter: drop-shadow(black 0 0 1px) 38 | test_hidden: 39 | card: 40 | type: conditional 41 | conditions: 42 | - entity: light.bed_light 43 | state: 'on' 44 | card: 45 | type: entities 46 | entities: 47 | - sun.sun 48 | 49 | views: 50 | - cards: 51 | - type: custom:decluttering-card 52 | template: my_first_template 53 | variables: 54 | - name: This is a test 55 | - type: entities 56 | entities: 57 | - light.bed_light 58 | - type: custom:decluttering-card 59 | template: large_divider 60 | - sun.sun 61 | - type: picture-elements 62 | image: https://www.w3schools.com/w3css/img_lights.jpg 63 | elements: 64 | - type: custom:decluttering-card 65 | template: demo_icon 66 | variables: 67 | - entity: sun.sun 68 | - type: horizontal-stack 69 | cards: 70 | - type: custom:decluttering-card 71 | template: my_first_template 72 | variables: 73 | - name: Full width when light Off 74 | - type: custom:decluttering-card 75 | template: test_hidden 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decluttering-card", 3 | "version": "1.0.0", 4 | "description": "Decluttering Card for Lovelace", 5 | "main": "dist/decluttering-card.js", 6 | "scripts": { 7 | "build": "npm run lint && npm run rollup", 8 | "rollup": "rollup -c", 9 | "babel": "babel dist/decluttering-card.js --out-file dist/decluttering-card.js", 10 | "lint": "eslint src/*.ts", 11 | "postversion": "npm run build", 12 | "start": "rollup -c --watch" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/custom-cards/decluttering-card.git" 17 | }, 18 | "keywords": [ 19 | "lovelace" 20 | ], 21 | "author": "RomRider", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/custom-cards/decluttering-card/issues" 25 | }, 26 | "homepage": "https://github.com/custom-cards/decluttering-card/README.md", 27 | "devDependencies": { 28 | "@babel/core": "^7.21.4", 29 | "@babel/plugin-proposal-class-properties": "^7.8.3", 30 | "@babel/plugin-proposal-decorators": "^7.8.3", 31 | "@rollup/plugin-babel": "^6.0.3", 32 | "@rollup/plugin-commonjs": "^24.0.1", 33 | "@rollup/plugin-json": "^6.0.0", 34 | "@rollup/plugin-node-resolve": "^15.0.1", 35 | "@semantic-release/changelog": "^6.0.3", 36 | "@semantic-release/commit-analyzer": "^9.0.2", 37 | "@semantic-release/exec": "^6.0.3", 38 | "@semantic-release/git": "^10.0.1", 39 | "@semantic-release/npm": "^10.0.2", 40 | "@semantic-release/release-notes-generator": "^10.0.3", 41 | "@typescript-eslint/eslint-plugin": "^2.24.0", 42 | "@typescript-eslint/parser": "^2.24.0", 43 | "babel-cli": "^6.26.0", 44 | "conventional-changelog-conventionalcommits": "^5.0.0", 45 | "eslint": "^6.8.0", 46 | "eslint-config-airbnb-base": "^14.1.0", 47 | "eslint-config-prettier": "^6.10.0", 48 | "eslint-plugin-import": "^2.20.1", 49 | "eslint-plugin-prettier": "^3.1.2", 50 | "npm": "^9.6.3", 51 | "prettier": "^1.19.0", 52 | "prettier-eslint": "^9.0.1", 53 | "rollup-plugin-serve": "^1.0.1", 54 | "rollup-plugin-terser": "^5.3.0", 55 | "rollup-plugin-typescript2": "^0.26.0", 56 | "ts-lit-plugin": "^1.2.1", 57 | "typescript": "^3.8.3", 58 | "typescript-styled-plugin": "^0.15.0" 59 | }, 60 | "dependencies": { 61 | "custom-card-helpers": "^1.5.0", 62 | "home-assistant-js-websocket": "^5.1.0", 63 | "lit-element": "^2.3.0", 64 | "resize-observer": "^1.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/decluttering-card.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, customElement, property, TemplateResult, css, CSSResult } from 'lit-element'; 2 | import { HomeAssistant, createThing, LovelaceCardConfig, LovelaceCard } from 'custom-card-helpers'; 3 | import { DeclutteringCardConfig, TemplateConfig } from './types'; 4 | import deepReplace from './deep-replace'; 5 | import { getLovelace, getLovelaceCast } from './utils'; 6 | import { ResizeObserver } from 'resize-observer'; 7 | import * as pjson from '../package.json'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const HELPERS = (window as any).loadCardHelpers ? (window as any).loadCardHelpers() : undefined; 11 | 12 | console.info( 13 | `%c DECLUTTERING-CARD \n%c Version ${pjson.version} `, 14 | 'color: orange; font-weight: bold; background: black', 15 | 'color: white; font-weight: bold; background: dimgray', 16 | ); 17 | 18 | @customElement('decluttering-card') 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | class DeclutteringCard extends LitElement { 21 | @property() protected _card?: LovelaceCard; 22 | 23 | @property() private _config?: LovelaceCardConfig; 24 | 25 | private _ro?: ResizeObserver; 26 | 27 | private _hass?: HomeAssistant; 28 | 29 | private _type?: 'element' | 'card'; 30 | 31 | set hass(hass: HomeAssistant) { 32 | if (!hass) return; 33 | if (!this._hass && hass) { 34 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 35 | this._createCard(this._config!, this._type!).then(card => { 36 | this._card = card; 37 | this._card && this._ro?.observe(this._card); 38 | return this._card; 39 | }); 40 | } 41 | this._hass = hass; 42 | if (this._card) { 43 | this._card.hass = hass; 44 | } 45 | } 46 | 47 | static get styles(): CSSResult { 48 | return css` 49 | :host(.child-card-hidden) { 50 | display: none; 51 | } 52 | `; 53 | } 54 | 55 | protected firstUpdated(): void { 56 | this.updateComplete.then(() => { 57 | this._displayHidden(); 58 | }); 59 | } 60 | 61 | protected _displayHidden(): void { 62 | if (this._card?.style.display === 'none') { 63 | this.classList.add('child-card-hidden'); 64 | } else if (this.classList.contains('child-card-hidden')) { 65 | this.classList.remove('child-card-hidden'); 66 | } 67 | } 68 | 69 | public setConfig(config: DeclutteringCardConfig): void { 70 | if (!config.template) { 71 | throw new Error('Missing template object in your config'); 72 | } 73 | const ll = getLovelace() || getLovelaceCast(); 74 | if (!ll.config && !ll.config.decluttering_templates) { 75 | throw new Error("The object decluttering_templates doesn't exist in your main lovelace config."); 76 | } 77 | const templateConfig = ll.config.decluttering_templates[config.template] as TemplateConfig; 78 | if (!templateConfig) { 79 | throw new Error(`The template "${config.template}" doesn't exist in decluttering_templates`); 80 | } else if (!(templateConfig.card || templateConfig.element)) { 81 | throw new Error('You shoud define either a card or an element in the template'); 82 | } else if (templateConfig.card && templateConfig.element) { 83 | throw new Error('You can define a card and an element in the template'); 84 | } 85 | this._ro = new ResizeObserver(() => { 86 | this._displayHidden(); 87 | }); 88 | this._config = deepReplace(config.variables, templateConfig); 89 | this._type = templateConfig.card ? 'card' : 'element'; 90 | } 91 | 92 | protected render(): TemplateResult | void { 93 | if (!this._hass || !this._card || !this._config) return html``; 94 | 95 | return html` 96 |
${this._card}
97 | `; 98 | } 99 | 100 | private async _createCard(config: LovelaceCardConfig, type: 'element' | 'card'): Promise { 101 | let element: LovelaceCard; 102 | if (HELPERS) { 103 | if (type === 'card') { 104 | if (config.type === 'divider') element = (await HELPERS).createRowElement(config); 105 | else element = (await HELPERS).createCardElement(config); 106 | // fireEvent(element, 'll-rebuild'); 107 | } else { 108 | element = (await HELPERS).createHuiElement(config); 109 | if (config.style) { 110 | Object.keys(config.style).forEach(prop => { 111 | this.style.setProperty(prop, config.style[prop]); 112 | }); 113 | } 114 | } 115 | } else { 116 | element = createThing(config); 117 | } 118 | if (this._hass) { 119 | element.hass = this._hass; 120 | } 121 | element.addEventListener( 122 | 'll-rebuild', 123 | ev => { 124 | ev.stopPropagation(); 125 | this._rebuildCard(element, config, type); 126 | }, 127 | { once: true }, 128 | ); 129 | element.id = 'declutter-child'; 130 | return element; 131 | } 132 | 133 | private async _rebuildCard( 134 | element: LovelaceCard, 135 | config: LovelaceCardConfig, 136 | type: 'element' | 'card', 137 | ): Promise { 138 | const newCard = await this._createCard(config, type); 139 | element.replaceWith(newCard); 140 | this._card = newCard; 141 | this._ro?.observe(this._card); 142 | return; 143 | } 144 | 145 | public getCardSize(): Promise | number { 146 | return this._card && typeof this._card.getCardSize === 'function' ? this._card.getCardSize() : 1; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decluttering Card 2 | 📝 Reuse multiple times the same card configuration with variables to declutter your config. 3 | 4 | [![GitHub Release][releases-shield]][releases] 5 | [![GitHub Activity][commits-shield]][commits] 6 | [![custom_updater][customupdaterbadge]][customupdater] 7 | [![License][license-shield]](LICENSE.md) 8 | 9 | [![Project Maintenance][maintenance-shield]][maintainer] 10 | 11 | [![Discord][discord-shield]][discord] 12 | [![Community Forum][forum-shield]][forum] 13 | 14 | [![Github][github]][maintainer] 15 | 16 | This card is for [Lovelace](https://www.home-assistant.io/lovelace) on [Home Assistant](https://www.home-assistant.io/). 17 | 18 | We all use multiple times the same block of configuration across our lovelace configuration and we don't want to change the same things in a hundred places across our configuration each time we want to modify something. 19 | 20 | `decluttering-card` to the rescue!! This card allows you to reuse multiple times the same configuration in your lovelace configuration to avoid repetition and supports variables and default values. 21 | 22 | ## Configuration 23 | 24 | ### Defining your templates 25 | 26 | First, you need to define your templates. 27 | 28 | The templates are defined in an object at the root of your lovelace configuration. This object needs to be named `decluttering_templates`. 29 | 30 | This object needs to contains your templates declaration, each template has a name and can contain variables. A variable needs to be enclosed in double square brackets `[[variable_name]]`. It will later be replaced by a real value when you instanciate a card which uses this template. If a variable is alone on it's line, enclose it in single quotes: `'[[variable_name]]'`. 31 | 32 | You can also define default values for your variables in the `default` object. 33 | 34 | For a card: 35 | 36 | ```yaml 37 | decluttering_templates: 38 | 39 | default: # This is optional 40 | - : 41 | - : 42 | [...] 43 | card: # This is where you put your card config (it can be a card embedding other cards) 44 | type: custom:my-super-card 45 | [...] 46 | ``` 47 | 48 | For a Picture-Element: 49 | 50 | ```yaml 51 | decluttering_templates: 52 | 53 | default: # This is optional 54 | - : 55 | - : 56 | [...] 57 | element: # This is where you put your element config 58 | type: icon 59 | [...] 60 | ``` 61 | 62 | Example in your `lovelace-ui.yaml`: 63 | ```yaml 64 | resources: 65 | - url: /local/decluttering-card.js 66 | type: module 67 | 68 | decluttering_templates: 69 | my_first_template: # This is the name of a template 70 | default: 71 | - icon: fire 72 | card: 73 | type: custom:button-card 74 | name: '[[name]]' 75 | icon: 'mdi:[[icon]]' 76 | 77 | my_second_template: # This is the name of another template 78 | card: 79 | type: custom:vertical-stack-in-card 80 | cards: 81 | - type: horizontal-stack 82 | cards: 83 | - type: custom:button-card 84 | entity: '[[entity_1]]' 85 | - type: custom:button-card 86 | entity: '[[entity_2]]' 87 | ``` 88 | 89 | ### Using the card 90 | 91 | | Name | Type | Requirement | Description 92 | | ---- | ---- | ------- | ----------- 93 | | type | string | **Required** | `custom:decluttering-card` 94 | | template | object | **Required** | The template to use from `decluttering_templates` 95 | | variables | list | **Optional** | List of variables and their value to replace in the `template` 96 | 97 | Example which references the previous templates: 98 | ```yaml 99 | - type: custom:decluttering-card 100 | template: my_first_template 101 | variables: 102 | - name: Test Button 103 | - icon: arrow-up 104 | 105 | - type: custom:decluttering-card 106 | template: my_first_template 107 | variables: Default Icon Button 108 | 109 | - type: custom:decluttering-card 110 | template: my_second_template 111 | variables: 112 | - entity_1: switch.my_switch 113 | - entity_2: light.my_light 114 | ``` 115 | 116 | 117 | ## Installation 118 | 119 | ### Step 1 120 | 121 | Save [decluttering-card](https://github.com/custom-cards/decluttering-card/releases/download/latest/decluttering-card.js) to `/www/decluttering-card.js` on your Home Assistant instanse. 122 | 123 | **Example:** 124 | 125 | ```bash 126 | wget https://raw.githubusercontent.com/custom-cards/decluttering-card/master/dist/decluttering-card.js 127 | mv decluttering-card.js /config/www/ 128 | ``` 129 | 130 | ### Step 2 131 | 132 | Link `decluttering-card` inside your `ui-lovelace.yaml` or Raw Editor in the UI Editor 133 | 134 | ```yaml 135 | resources: 136 | - url: /local/decluttering-card.js 137 | type: module 138 | ``` 139 | 140 | ### Step 3 141 | 142 | Add a custom element in your `ui-lovelace.yaml` or in the UI Editor as a Manual Card 143 | 144 | ## Troubleshooting 145 | 146 | See this guide: [Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins) 147 | 148 | ## Developers 149 | Fork and then clone the repo to your local machine. From the cloned directory run 150 | 151 | `npm install && npm run build` 152 | 153 | 154 | [commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/decluttering-card.svg?style=for-the-badge 155 | [commits]: https://github.com/custom-cards/decluttering-card/commits/master 156 | [customupdater]: https://github.com/custom-components/custom_updater 157 | [customupdaterbadge]: https://img.shields.io/badge/custom__updater-true-success.svg?style=for-the-badge 158 | [discord]: https://discord.gg/Qa5fW2R 159 | [discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge 160 | [forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge 161 | [forum]: https://community.home-assistant.io/t/lovelace-decluttering-card/118625 162 | [license-shield]: https://img.shields.io/github/license/custom-cards/decluttering-card.svg?style=for-the-badge 163 | [maintenance-shield]: https://img.shields.io/badge/maintainer-RomRider-blue.svg?style=for-the-badge 164 | [maintainer]: https://github.com/RomRider 165 | [releases-shield]: https://img.shields.io/github/release/custom-cards/decluttering-card.svg?style=for-the-badge 166 | [releases]: https://github.com/custom-cards/decluttering-card/releases 167 | [github]: https://img.shields.io/github/followers/RomRider.svg?style=social 168 | --------------------------------------------------------------------------------