├── .devcontainer ├── Dockerfile ├── configuration.yaml ├── devcontainer.json └── ui-lovelace.yaml ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── docs └── Example.png ├── hacs.json ├── package.json ├── rollup.config.js ├── src ├── stack-in-card.ts └── types.ts ├── tsconfig.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ludeeus/container:monster 2 | 3 | RUN apk add jq 4 | RUN adduser -Du 1000 vscode 5 | -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | lovelace: 3 | mode: yaml 4 | demo: 5 | 6 | frontend: 7 | themes: 8 | test: 9 | background-color: "#F7F8F9" 10 | border-color: "#E8E8E8" 11 | ha-card-box-shadow: "inset 0px 0px 0px 1px var(--border-color)" 12 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Stack In Card Development", 4 | "dockerFile": "Dockerfile", 5 | "context": "..", 6 | "remoteUser": "vscode", 7 | "appPort": [ 8 | "5000:5000", 9 | "9123:8123" 10 | ], 11 | "postCreateCommand": "yarn 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 | ], 27 | "settings": { 28 | "files.eol": "\n", 29 | "editor.tabSize": 2, 30 | "terminal.integrated.shell.linux": "/bin/bash", 31 | "editor.formatOnPaste": false, 32 | "editor.formatOnSave": true, 33 | "editor.formatOnType": true, 34 | "files.trimTrailingWhitespace": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.devcontainer/ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - url: http://127.0.0.1:5000/stack-in-card.js 3 | type: module 4 | - url: https://cdn.jsdelivr.net/gh/thomasloven/lovelace-card-mod@master/card-mod.js 5 | type: module 6 | - url: http://127.0.0.1:5000/button-card.js 7 | type: module 8 | views: 9 | - cards: 10 | - type: custom:stack-in-card 11 | mode: horizontal 12 | keep: 13 | box_shadow: true 14 | margin: true 15 | border_radius: true 16 | background: true 17 | outer_padding: true 18 | title: test 19 | cards: 20 | - type: custom:stack-in-card 21 | keep: 22 | margin: true 23 | outer_padding: false 24 | border_radius: false 25 | cards: 26 | - type: custom:button-card 27 | entity: switch.decorative_lights 28 | color_type: card 29 | # color: white 30 | # styles: 31 | # card: 32 | # - --keep-background: "true" 33 | - type: custom:button-card 34 | entity: sun.sun 35 | color_type: card 36 | color: white 37 | # styles: 38 | # card: 39 | # - --keep-background: "true" 40 | - type: vertical-stack 41 | cards: 42 | - type: custom:button-card 43 | entity: sun.sun 44 | color_type: card 45 | color: white 46 | styles: 47 | card: 48 | - --keep-background: "true" 49 | - type: custom:button-card 50 | entity: sun.sun 51 | color_type: card 52 | color: yellow 53 | color_off: rgb(245, 245, 245) 54 | - type: entities 55 | style: | 56 | ha-card { 57 | --ha-card-background: rgb(0,100,182); 58 | color: rgb(217,242,251); 59 | --keep-background: true; 60 | } 61 | entities: 62 | - type: divider 63 | - type: custom:stack-in-card 64 | title: My Stack In Card 65 | mode: vertical 66 | cards: 67 | - type: horizontal-stack 68 | cards: 69 | - type: button 70 | entity: sun.sun 71 | - type: button 72 | entity: sun.sun 73 | - type: entities 74 | style: | 75 | ha-card { 76 | --ha-card-background: rgb(255,100,182); 77 | color: rgb(217,242,251); 78 | --keep-background: true; 79 | } 80 | entities: 81 | - type: divider 82 | - type: vertical-stack 83 | cards: 84 | - type: entities 85 | entities: 86 | - sun.sun 87 | - type: custom:stack-in-card 88 | title: My Stack In Card 89 | mode: vertical 90 | cards: 91 | - type: horizontal-stack 92 | cards: 93 | - type: button 94 | entity: sun.sun 95 | - type: button 96 | entity: sun.sun 97 | - type: entities 98 | style: | 99 | ha-card { 100 | --ha-card-background: rgb(255,100,182); 101 | color: rgb(217,242,251); 102 | --keep-background: true; 103 | } 104 | entities: 105 | - type: divider 106 | - type: vertical-stack 107 | cards: 108 | - type: entities 109 | entities: 110 | - sun.sun 111 | 112 | - type: custom:stack-in-card 113 | title: My Stack In Card 114 | mode: vertical 115 | cards: 116 | - type: horizontal-stack 117 | cards: 118 | - type: button 119 | entity: sun.sun 120 | - type: button 121 | entity: sun.sun 122 | - type: entities 123 | style: | 124 | ha-card { 125 | --ha-card-background: rgb(255,100,182); 126 | color: rgb(217,242,251); 127 | --keep-background: true; 128 | } 129 | entities: 130 | - type: divider 131 | - type: vertical-stack 132 | cards: 133 | - type: entities 134 | entities: 135 | - sun.sun 136 | 137 | - type: custom:stack-in-card 138 | title: My Stack In Card 139 | mode: vertical 140 | cards: 141 | - type: horizontal-stack 142 | cards: 143 | - type: button 144 | entity: sun.sun 145 | - type: button 146 | entity: sun.sun 147 | - type: entities 148 | style: | 149 | ha-card { 150 | --ha-card-background: rgb(255,100,182); 151 | color: rgb(217,242,251); 152 | --keep-background: true; 153 | } 154 | entities: 155 | - type: divider 156 | - type: vertical-stack 157 | cards: 158 | - type: entities 159 | entities: 160 | - sun.sun 161 | 162 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [RomRider] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Build 18 | run: | 19 | yarn install 20 | npm run build 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | # Build 15 | - name: Build the file 16 | run: | 17 | cd /home/runner/work/stack-in-card/stack-in-card 18 | yarn install 19 | npm run build 20 | 21 | # Upload build file to the releas as an asset. 22 | - name: Upload zip to release 23 | uses: svenstaro/upload-release-action@v1-release 24 | 25 | with: 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} 27 | file: /home/runner/work/stack-in-card/stack-in-card/dist/stack-in-card.js 28 | asset_name: stack-in-card.js 29 | tag: ${{ github.ref }} 30 | overwrite: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.rpt2_cache/ 3 | package-lock.json 4 | /dist 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version=false 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /.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 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Chrome Localhost", 8 | "url": "http://localhost:8123", 9 | "webRoot": "${workspaceFolder}/dist", 10 | "sourceMaps": true 11 | // "runtimeExecutable": "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" 12 | }, 13 | { 14 | "type": "browser-preview", 15 | "request": "attach", 16 | "name": "Browser Preview: Attach", 17 | "webRoot": "${workspaceFolder}/dist", 18 | "pathMapping": { 19 | "http://127.0.0.1:5000/": "${workspaceFolder}/dist" 20 | }, 21 | // "sourceMapPathOverrides": { 22 | // "http://127.0.0.1:5000": "${workspaceFolder}/dist" 23 | // }, 24 | }, 25 | { 26 | "type": "browser-preview", 27 | "request": "launch", 28 | "name": "Browser Preview: Launch", 29 | "url": "http://localhost:9123", 30 | "webRoot": "${workspaceFolder}/dist", 31 | // "sourceMapPathOverrides": { 32 | // "http://127.0.0.1:5000": "${workspaceFolder}/dist" 33 | // }, 34 | // "pathMapping": { 35 | // "http://127.0.0.1:5000": "${workspaceFolder}/dist" 36 | // }, 37 | // "trace": true 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 9123", 6 | "type": "shell", 7 | "command": "dc start", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "Run Home Assistant configuration against /config", 12 | "type": "shell", 13 | "command": "dc check", 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "label": "Upgrade Home Assistant to latest dev", 18 | "type": "shell", 19 | "command": "dc install", 20 | "problemMatcher": [] 21 | }, 22 | { 23 | "label": "Install a specific version of Home Assistant", 24 | "type": "shell", 25 | "command": "dc set-version", 26 | "problemMatcher": [] 27 | }, 28 | { 29 | "label": "Watch for file change and service the card on :5000", 30 | "type": "npm", 31 | "script": "start", 32 | "problemMatcher": [] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Custom cards for Home Assistant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack In Card by [@RomRider](https://www.github.com/RomRider) 2 | 3 | A replacement for [vertical-stack-in-card](https://github.com/ofekashery/vertical-stack-in-card) and `horizontal-stack-in-card` 4 | 5 | It allows to group multiple cards into one card without the borders. By default, it will stack everything vertically. 6 | 7 | [![GitHub Release][releases-shield]][releases] 8 | [![License][license-shield]](LICENSE.md) 9 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) 10 | 11 | ![Project Maintenance][maintenance-shield] 12 | [![GitHub Activity][commits-shield]][commits] 13 | 14 | [![Discord][discord-shield]][discord] 15 | [![Community Forum][forum-shield]][forum] 16 | 17 | ## Options 18 | 19 | If a card inside the stack has the `--keep-background` CSS style defined, it will not replace the background. This is usefull for [button-card](https://github.com/custom-cards/button-card) for example. You can also define this CSS variable by using [card-mod](https://github.com/thomasloven/lovelace-card-mod). 20 | 21 | | Name | Type | Requirement | Description | Default | 22 | | ---- | ---- | ----------- | ----------- | ------- | 23 | | `type` | string | **Required** | `custom:stack-in-card` | | 24 | | `title` | string | **Optional** | Header of the card | | 25 | | `mode` | string | **Optional** | `vertical` or `horizontal` stack | `vertical` | 26 | | `cards` | object | **Required** | The cards you want to embed | `none` | 27 | | `keep` | object | **Optional** | See [keep object](#keep-object) | | 28 | 29 | ### `keep` object 30 | 31 | | Name | Type | Requirement | Description | Default | 32 | | ---- | ---- | ----------- | ----------- | ------- | 33 | | `background` | boolean | **Optional** | Will keep the background on **all** the child cards. To keep the background on specific cards only, assign the CSS variable `--keep-background: 'true'` on the card where you want to keep the background. | `false` | 34 | | `box_shadow` | boolean | **Optional** | Will keep the `box-shadow` on **all** the child cards | `false` | 35 | | `margin` | boolean | **Optional** | Will keep the `margin` between **all** the child cards | `false` | 36 | | `outer_padding` | boolean | **Optional** | Will add a `padding` of `8px` to the card if `margin` is `true` | `true` if `margin` is `true`, else false | 37 | | `border_radius` | boolean | **Optional** | Will keep the `border-radius` on **all** the child cards | `false` | 38 | 39 | ## Example 40 | 41 | ### Simple Example 42 | 43 | ![example](docs/Example.png) 44 | 45 | ```yaml 46 | - type: custom:stack-in-card 47 | title: My Stack In Card 48 | mode: vertical 49 | cards: 50 | - type: horizontal-stack 51 | cards: 52 | - type: button 53 | entity: sun.sun 54 | - type: button 55 | entity: sun.sun 56 | - type: vertical-stack 57 | cards: 58 | - type: entities 59 | entities: 60 | - sun.sun 61 | ``` 62 | 63 | ### Example with button-card to keep the background 64 | 65 | This will keep the background of the button even if stacked: 66 | 67 | ```yaml 68 | - type: custom:stack-in-card 69 | title: My Stack In Card 70 | mode: vertical 71 | cards: 72 | - type: custom:button-card 73 | entity: sun.sun 74 | color_type: card 75 | styles: 76 | card: 77 | - --keep-background: 'true' 78 | ``` 79 | 80 | ## Installation 81 | 82 | Use [HACS](https://hacs.xyz) or follow this [guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins) 83 | 84 | ```yaml 85 | resources: 86 | url: /local/stack-in-card.js 87 | type: module 88 | ``` 89 | 90 | [commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/stack-in-card.svg?style=for-the-badge 91 | [commits]: https://github.com/custom-cards/stack-in-card/commits/master 92 | [devcontainer]: https://code.visualstudio.com/docs/remote/containers 93 | [discord]: https://discord.gg/5e9yvq 94 | [discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge 95 | [forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge 96 | [forum]: https://community.home-assistant.io/t/stack-in-card-drop-in-replacement-for-vertical-stack-in-card/180072 97 | [license-shield]: https://img.shields.io/github/license/custom-cards/stack-in-card.svg?style=for-the-badge 98 | [maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg?style=for-the-badge 99 | [releases-shield]: https://img.shields.io/github/release/custom-cards/stack-in-card.svg?style=for-the-badge 100 | [releases]: https://github.com/custom-cards/stack-in-card/releases 101 | -------------------------------------------------------------------------------- /docs/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/custom-cards/stack-in-card/6d8401dd2c90f25565bfc5e913dff262ddd331ef/docs/Example.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stack In Card", 3 | "render_readme": true, 4 | "filename": "stack-in-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-in-card", 3 | "version": "0.2.0", 4 | "description": "Stack in Card for Home Assistant's Lovelace UI", 5 | "main": "dist/stack-in-card.js", 6 | "pre-commit": [ 7 | "pre-commit" 8 | ], 9 | "scripts": { 10 | "build": "npm run lint && npm run rollup", 11 | "rollup": "rollup -c", 12 | "lint": "eslint src/*.ts", 13 | "start": "rollup -c --watch" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/custom-cards/stack-in-card.git" 18 | }, 19 | "keywords": [ 20 | "home-assistant", 21 | "homeassistant", 22 | "hass", 23 | "automation", 24 | "lovelace", 25 | "custom-cards" 26 | ], 27 | "author": "Jérôme Wiedemann ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/custom-cards/stack-in-card/issues" 31 | }, 32 | "homepage": "https://github.com/custom-cards/stack-in-card#readme", 33 | "devDependencies": { 34 | "@babel/core": "^7.8.7", 35 | "@babel/plugin-proposal-class-properties": "^7.8.3", 36 | "@babel/plugin-proposal-decorators": "^7.8.3", 37 | "@rollup/plugin-babel": "^5.2.1", 38 | "@rollup/plugin-commonjs": "^16.0.0", 39 | "@rollup/plugin-json": "^4.0.2", 40 | "@rollup/plugin-node-resolve": "^10.0.0", 41 | "@typescript-eslint/eslint-plugin": "^4.6.1", 42 | "@typescript-eslint/parser": "^4.6.1", 43 | "eslint": "^7.13.0", 44 | "eslint-config-airbnb-base": "^14.1.0", 45 | "eslint-config-prettier": "^6.10.0", 46 | "eslint-plugin-import": "^2.20.1", 47 | "eslint-plugin-prettier": "^3.1.2", 48 | "npm": "^6.14.3", 49 | "prettier": "^2.1.2", 50 | "prettier-eslint": "^11.0.0", 51 | "rollup": "^2.1.0", 52 | "rollup-plugin-serve": "^1.0.1", 53 | "rollup-plugin-terser": "^7.0.2", 54 | "rollup-plugin-typescript2": "^0.29.0", 55 | "ts-lit-plugin": "^1.1.10", 56 | "typescript": "^4.0.5", 57 | "typescript-styled-plugin": "^0.15.0" 58 | }, 59 | "dependencies": { 60 | "custom-card-helpers": "^1.6.6", 61 | "home-assistant-js-websocket": "^5.7.0", 62 | "lit-element": "^2.4.0", 63 | "lit-html": "^1.3.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | const dev = process.env.ROLLUP_WATCH; 10 | 11 | const serveopts = { 12 | contentBase: ['./dist'], 13 | host: '0.0.0.0', 14 | port: 5000, 15 | allowCrossOrigin: true, 16 | headers: { 17 | 'Access-Control-Allow-Origin': '*', 18 | }, 19 | }; 20 | 21 | const plugins = [ 22 | nodeResolve({}), 23 | commonjs(), 24 | typescript(), 25 | json(), 26 | babel({ 27 | exclude: 'node_modules/**', 28 | babelHelpers: 'bundled', 29 | }), 30 | dev && serve(serveopts), 31 | !dev && terser(), 32 | ]; 33 | 34 | export default [ 35 | { 36 | input: 'src/stack-in-card.ts', 37 | output: { 38 | dir: './dist', 39 | format: 'es', 40 | sourcemap: dev ? true : false, 41 | }, 42 | plugins: [...plugins], 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /src/stack-in-card.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, customElement, property, TemplateResult, html, css, CSSResult, PropertyValues } from 'lit-element'; 2 | import { ifDefined } from 'lit-html/directives/if-defined'; 3 | import { HomeAssistant, LovelaceCardConfig, createThing, LovelaceCard } from 'custom-card-helpers'; 4 | import { StackInCardConfig } from './types'; 5 | import * as pjson from '../package.json'; 6 | 7 | console.info( 8 | `%c STACK-IN-CARD \n%c Version ${pjson.version} `, 9 | 'color: orange; font-weight: bold; background: black', 10 | 'color: white; font-weight: bold; background: dimgray', 11 | ); 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | const HELPERS = (window as any).loadCardHelpers ? (window as any).loadCardHelpers() : undefined; 15 | 16 | @customElement('stack-in-card') 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | class StackInCard extends LitElement implements LovelaceCard { 19 | @property() protected _card?: LovelaceCard; 20 | 21 | @property() private _config?: StackInCardConfig; 22 | 23 | private _hass?: HomeAssistant; 24 | 25 | private _cardPromise: Promise | undefined; 26 | 27 | set hass(hass: HomeAssistant) { 28 | this._hass = hass; 29 | if (this._card) { 30 | this._card.hass = hass; 31 | } 32 | } 33 | 34 | static get styles(): CSSResult { 35 | return css` 36 | ha-card { 37 | overflow: hidden; 38 | } 39 | `; 40 | } 41 | 42 | public setConfig(config: StackInCardConfig): void { 43 | if (!config.cards) { 44 | throw new Error(`There is no cards parameter defined`); 45 | } 46 | this._config = { 47 | mode: 'vertical', 48 | ...config, 49 | keep: { 50 | background: false, 51 | margin: false, 52 | box_shadow: false, 53 | border_radius: false, 54 | ...config.keep, 55 | }, 56 | }; 57 | if (this._config.keep?.margin && this._config.keep?.outer_padding === undefined) 58 | this._config.keep.outer_padding = true; 59 | this._createStack(); 60 | } 61 | 62 | protected updated(changedProperties: PropertyValues): void { 63 | super.updated(changedProperties); 64 | if (!this._card) return; 65 | this._waitForChildren(this._card, false); 66 | window.setTimeout(() => { 67 | if (!this._config?.keep?.background) this._waitForChildren(this._card, true); 68 | if (this._config?.keep?.outer_padding && this._card?.shadowRoot) { 69 | const stackRoot = this._card.shadowRoot.getElementById('root'); 70 | if (stackRoot) stackRoot.style.padding = '8px'; 71 | } 72 | }, 500); 73 | } 74 | 75 | private async _createStack() { 76 | this._cardPromise = this._createCard({ 77 | type: `${this._config!.mode}-stack`, 78 | cards: this._config!.cards, 79 | }); 80 | 81 | this._card = await this._cardPromise; 82 | } 83 | 84 | protected render(): TemplateResult { 85 | if (!this._hass || !this._config) { 86 | return html``; 87 | } 88 | 89 | return html` 90 | 91 |
${this._card}
92 |
93 | `; 94 | } 95 | 96 | private _updateStyle(e: LovelaceCard | null, withBg: boolean): void { 97 | if (!e) return; 98 | if (!this._config?.keep?.box_shadow) e.style.boxShadow = 'none'; 99 | if ( 100 | !this._config?.keep?.background && 101 | withBg && 102 | getComputedStyle(e).getPropertyValue('--keep-background').trim() !== 'true' 103 | ) { 104 | e.style.background = 'transparent'; 105 | } 106 | if (!this._config?.keep?.border_radius) e.style.borderRadius = '0'; 107 | } 108 | 109 | private _loopChildren(e: LovelaceCard, withBg: boolean): void { 110 | const searchElements = e.childNodes; 111 | searchElements.forEach((childE) => { 112 | if ((childE as Element).tagName === 'STACK-IN-CARD') return; 113 | if (!this._config?.keep?.margin && (childE as LovelaceCard).style) { 114 | (childE as LovelaceCard).style.margin = '0px'; 115 | } 116 | this._waitForChildren(childE as LovelaceCard, withBg); 117 | }); 118 | } 119 | 120 | private _updateChildren(element: LovelaceCard | undefined, withBg: boolean): void { 121 | if (!element) return; 122 | if (element.shadowRoot) { 123 | const card = element.shadowRoot.querySelector('ha-card') as LovelaceCard; 124 | if (!card) { 125 | // if (element.shadowRoot.querySelector('stack-in-card')) return; 126 | const searchEles = element.shadowRoot.getElementById('root') || element.shadowRoot.getElementById('card'); 127 | if (!searchEles) return; 128 | this._loopChildren(searchEles as LovelaceCard, withBg); 129 | } else { 130 | this._updateStyle(card, withBg); 131 | } 132 | } else { 133 | if (typeof element.querySelector === 'function' && element.querySelector('ha-card')) { 134 | this._updateStyle(element.querySelector('ha-card'), withBg); 135 | } 136 | this._loopChildren(element as LovelaceCard, withBg); 137 | } 138 | } 139 | 140 | private _waitForChildren(element: LovelaceCard | undefined, withBg: boolean): void { 141 | if (((element as unknown) as LitElement).updateComplete) { 142 | ((element as unknown) as LitElement).updateComplete.then(() => { 143 | this._updateChildren(element, withBg); 144 | }); 145 | } else { 146 | this._updateChildren(element, withBg); 147 | } 148 | } 149 | 150 | private async _createCard(config: LovelaceCardConfig): Promise { 151 | let element: LovelaceCard; 152 | if (HELPERS) { 153 | element = (await HELPERS).createCardElement(config); 154 | } else { 155 | element = createThing(config); 156 | } 157 | if (this._hass) { 158 | element.hass = this._hass; 159 | } 160 | if (element) { 161 | element.addEventListener( 162 | 'll-rebuild', 163 | (ev) => { 164 | ev.stopPropagation(); 165 | this._rebuildCard(element, config); 166 | }, 167 | { once: true }, 168 | ); 169 | } 170 | return element; 171 | } 172 | 173 | private async _rebuildCard(element: LovelaceCard, config: LovelaceCardConfig): Promise { 174 | const newCard = await this._createCard(config); 175 | element.replaceWith(newCard); 176 | this._card = newCard; 177 | window.setTimeout(() => { 178 | if (!this._config?.keep?.background) this._waitForChildren(this._card, true); 179 | if (this._config?.keep?.outer_padding && this._card?.shadowRoot) { 180 | const stackRoot = this._card.shadowRoot.getElementById('root'); 181 | if (stackRoot) stackRoot.style.padding = '8px'; 182 | } 183 | }, 500); 184 | return newCard; 185 | } 186 | 187 | public async getCardSize(): Promise { 188 | await this._cardPromise; 189 | if (!this._card) { 190 | return 0; 191 | } 192 | return await this._computeCardSize(this._card); 193 | } 194 | 195 | private _computeCardSize(card: LovelaceCard): number | Promise { 196 | if (typeof card.getCardSize === 'function') { 197 | return card.getCardSize(); 198 | } 199 | if (customElements.get(card.localName)) { 200 | return 1; 201 | } 202 | return customElements.whenDefined(card.localName).then(() => this._computeCardSize(card)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { LovelaceCardConfig } from 'custom-card-helpers'; 2 | 3 | export interface StackInCardConfig { 4 | type: string; 5 | mode?: 'horizontal' | 'vertical'; 6 | cards: LovelaceCardConfig[]; 7 | title?: string; 8 | keep?: KeepConfig; 9 | } 10 | 11 | export interface KeepConfig { 12 | margin?: boolean; 13 | background?: boolean; 14 | box_shadow?: boolean; 15 | border_radius?: boolean; 16 | outer_padding?: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /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 | "noEmit": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "experimentalDecorators": true, 20 | "sourceMap": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------