├── .npmrc ├── examples ├── editor.png ├── nested.png ├── example.gif ├── styling1.png ├── styling2.png ├── styling3.png ├── clear_router.png └── lights_overlay_button.png ├── docs └── images │ └── pplogo.png ├── .github ├── FUNDING.yml ├── labeler.yml ├── workflows │ ├── validate.yml │ ├── autoClose.yml │ ├── build.yml │ └── release-workflow.yml ├── release.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── hacs.json ├── .gitignore ├── tsconfig.json ├── src ├── helpers │ ├── forward-haptic.ts │ ├── compute-card-size.ts │ ├── promise-timeout.ts │ └── templates.ts ├── types.ts ├── index.ts ├── configtype.ts ├── ExpanderCardEditor.ts ├── Card.svelte ├── editortype.ts └── ExpanderCard.svelte ├── svelte.config.js ├── vite.config.js ├── package.json ├── rollup.config.mjs ├── eslint.config.mjs ├── license.txt └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /examples/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/editor.png -------------------------------------------------------------------------------- /examples/nested.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/nested.png -------------------------------------------------------------------------------- /examples/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/example.gif -------------------------------------------------------------------------------- /examples/styling1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/styling1.png -------------------------------------------------------------------------------- /examples/styling2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/styling2.png -------------------------------------------------------------------------------- /examples/styling3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/styling3.png -------------------------------------------------------------------------------- /docs/images/pplogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/docs/images/pplogo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [MelleD] 2 | buy_me_a_coffee: melled 3 | custom: ["https://www.paypal.me/MelleDennis"] 4 | -------------------------------------------------------------------------------- /examples/clear_router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/clear_router.png -------------------------------------------------------------------------------- /examples/lights_overlay_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelleD/lovelace-expander-card/HEAD/examples/lights_overlay_button.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expander-card", 3 | "content_in_root": false, 4 | "render_readme": true, 5 | "filename": "expander-card.js", 6 | "homeassistant": "2025.10.0b0" 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | /dist 12 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add/remove labels to PR 2 | breaking-change: 3 | - '(breaking-change|major-update)' 4 | bugfix: 5 | - '(fix)' 6 | documentation: 7 | - '(doc|docs)' 8 | miscellaneous: 9 | - '(misc)' 10 | feature: 11 | - '(feat)' -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | validate-hacs: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "plugin" 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "types": ["svelte"], 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "lib": ["es2019", "dom"] 15 | }, 16 | "include": ["src/**/*"], 17 | } 18 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: ⚠️ Breaking Changes 4 | labels: 5 | - major-update 6 | - breaking-change 7 | - title: 🐞 Bug Fixes 8 | labels: 9 | - bugfix 10 | - title: 📦 Dependency Upgrades 11 | labels: 12 | - dependencies 13 | - title: 📔 Documentation 14 | labels: 15 | - documentation 16 | - title: ⚙️ Miscellaneous 17 | labels: 18 | - miscellaneous 19 | - title: ⭐ New Features 20 | labels: 21 | - "*" 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | # Enable version updates for npm 9 | - package-ecosystem: "npm" 10 | # Look for `package.json` and `lock` files in the `root` directory 11 | directory: "/" 12 | # Check the npm registry for updates every day (weekdays) 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /src/helpers/forward-haptic.ts: -------------------------------------------------------------------------------- 1 | // Allowed types are from iOS HIG. 2 | // https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics 3 | // Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible. 4 | export type HapticType = 'success' | 'warning' | 'failure' | 'light' | 'medium' | 'heavy' | 'selection' | 'none'; 5 | 6 | declare global { 7 | // for fire event 8 | interface HASSDomEvents { 9 | haptic: HapticType; 10 | } 11 | } 12 | 13 | export const forwardHaptic = (node: HTMLElement, hapticType: HapticType) => { 14 | node.dispatchEvent?.( 15 | new CustomEvent('haptic', 16 | { detail: hapticType, bubbles: true, composed: true } 17 | ) 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/autoClose.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | workflow_dispatch: 6 | 7 | jobs: 8 | close-issues: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v10 15 | with: 16 | days-before-issue-stale: 30 17 | days-before-issue-close: 14 18 | stale-issue-label: "stale" 19 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 20 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 21 | days-before-pr-stale: -1 22 | days-before-pr-close: -1 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | any-of-labels: "wait-for-user-input,wait-for-mre" 25 | -------------------------------------------------------------------------------- /.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 | Please start by describing the problem that you are trying to solve. There may already 11 | be a solution, or there may be a way to solve it that you hadn't considered. 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | 23 | 24 | TIP: You can always edit your issue if it isn't formatted correctly. 25 | See https://guides.github.com/features/mastering-markdown 26 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess({ 9 | sourceMap: false, 10 | postcss: true, 11 | script: true 12 | }), 13 | compilerOptions: { 14 | // runes: true, 15 | customElement: true, 16 | enableSourcemap: true, 17 | }, 18 | 19 | kit: { 20 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 21 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 22 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 23 | adapter: adapter(), 24 | alias: { 25 | '$': 'src', 26 | '$components': 'src/lib/components', 27 | '$assets': 'src/assets', 28 | 29 | }, 30 | }, 31 | }; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /src/helpers/compute-card-size.ts: -------------------------------------------------------------------------------- 1 | import { HuiCard } from '../types'; 2 | import { TimeoutError } from './promise-timeout'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export const promiseTimeout = (ms: number, promise: Promise | any) => { // NOSONAR 6 | const timeout = new Promise((_resolve, reject) => { 7 | setTimeout(() => { 8 | reject(new TimeoutError(ms)); 9 | }, ms); 10 | }); 11 | 12 | // Returns a race between our timeout and the passed in promise 13 | return Promise.race([promise, timeout]); 14 | }; 15 | 16 | export const computeCardSize = ( 17 | card: HuiCard 18 | ): number | Promise => { 19 | if (typeof card.getCardSize === 'function') { 20 | try { 21 | return promiseTimeout(500, card.getCardSize()).catch( 22 | () => 1 23 | ) as Promise; 24 | } catch { 25 | return 1; 26 | } 27 | } 28 | if (customElements.get(card.localName)) { 29 | return 1; 30 | } 31 | return customElements 32 | .whenDefined(card.localName) 33 | .then(() => computeCardSize(card)); 34 | }; 35 | -------------------------------------------------------------------------------- /src/helpers/promise-timeout.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export class TimeoutError extends Error { 3 | public timeout: number; 4 | 5 | // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility 6 | constructor(timeout: number, ...params: undefined[]) { 7 | super(...params); 8 | 9 | // Maintains proper stack trace for where our error was thrown (only available on V8) 10 | if ((Error as any).captureStackTrace) { 11 | (Error as any).captureStackTrace(this, TimeoutError); 12 | } 13 | 14 | this.name = 'TimeoutError'; 15 | // Custom debugging information 16 | this.timeout = timeout; 17 | this.message = `Timed out in ${timeout} ms.`; 18 | } 19 | } 20 | 21 | export const promiseTimeout = (ms: number, promise: Promise | any) => { // NOSONAR 22 | const timeout = new Promise((_resolve, reject) => { 23 | setTimeout(() => { 24 | reject(new TimeoutError(ms)); 25 | }, ms); 26 | }); 27 | 28 | // Returns a race between our timeout and the passed in promise 29 | return Promise.race([promise, timeout]); 30 | }; 31 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | svelte({ 8 | preprocess: vitePreprocess({ 9 | postcss: true, 10 | script: true 11 | }) 12 | }) 13 | ], 14 | build: { 15 | sourcemap: true, 16 | lib: { 17 | entry: 'src/index.ts', 18 | name: 'ExpanderCard', // Name der globalen Variable für UMD/IIFE 19 | fileName: 'expander-card', // Basisname der generierten Datei 20 | } 21 | }, 22 | rollupOptions: { 23 | output: { 24 | entryFileNames: 'expander-card.js' // Dateiname der generierten Haupt-JS-Datei 25 | 26 | } 27 | } 28 | }); 29 | 30 | process 31 | .on('unhandledRejection', (reason, p) => { 32 | console.error(reason, 'Unhandled Rejection at Promise', p); 33 | process.exit(1); 34 | }) 35 | .on('uncaughtException', (err) => { 36 | console.error(err, 'Uncaught Exception thrown'); 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type AnimationState = 'opening' | 'closing' | 'idle'; 2 | 3 | export interface HomeAssistantUser { 4 | id: string; 5 | name: string; 6 | is_admin: boolean; 7 | is_owner: boolean; 8 | system_generated: boolean; 9 | } 10 | export interface HomeAssistant { 11 | user?: HomeAssistantUser; 12 | [key: string]: unknown; 13 | } 14 | 15 | export interface LovelaceCardConfig { 16 | index?: number; 17 | view_index?: number; 18 | type: string; 19 | disabled?: boolean; 20 | [key: string]: unknown; 21 | } 22 | 23 | export interface LovelaceCard extends HTMLElement { 24 | hass?: HomeAssistant; 25 | isPanel?: boolean; 26 | preview?: boolean; 27 | getCardSize(): number | Promise; 28 | config?: LovelaceCardConfig; 29 | } 30 | 31 | export interface HuiCard extends LovelaceCard { 32 | load(): void; 33 | _element?: LovelaceCard; 34 | } 35 | 36 | export interface HaRipple extends HTMLElement { 37 | disabled?: boolean; 38 | startPressAnimation(event?: Event): void; 39 | endPressAnimation(): void; 40 | } 41 | 42 | export interface ExpanderCardDomEventDetail { 43 | 'expander-card-id'?: string; 44 | action?: 'open' | 'close' | 'toggle'; 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 Peter Repukat - FlatspotSoftware 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | export { default } from './ExpanderCard.svelte'; 14 | import { version } from '../package.json'; 15 | declare global { 16 | interface Window { 17 | customCards?: { 18 | type: string; 19 | name: string; 20 | preview: boolean; 21 | description: string; 22 | }[]; 23 | } 24 | } 25 | 26 | /* eslint no-console: 0 */ 27 | console.info( 28 | `%c Expander-Card \n%c Version ${version}`, 29 | 'color: orange; font-weight: bold; background: black', 30 | 'color: white; font-weight: bold; background: dimgray' 31 | ); 32 | 33 | window.customCards = window.customCards || []; 34 | window.customCards.push(...[ 35 | { 36 | type: 'expander-card', 37 | name: 'Expander Card', 38 | preview: true, 39 | description: 'Expander card' 40 | } 41 | ]); 42 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | issues: write 17 | name: Build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: github/issue-labeler@v3.4 21 | if: github.event_name == 'pull_request_target' 22 | with: 23 | configuration-path: .github/labeler.yml 24 | include-title: 1 25 | include-body: 0 26 | enable-versioned-regex: 0 27 | repo-token: ${{ secrets.GITHUB_TOKEN }} 28 | - uses: actions/checkout@v4 29 | - name: Setup Git 30 | run: | 31 | git config user.name github-actions 32 | git config user.email github-actions@github.com 33 | 34 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda 35 | name: Install pnpm 36 | with: 37 | version: 9 38 | run_install: false 39 | - uses: actions/setup-node@v4 40 | name: Install Node.js 41 | with: 42 | node-version: 20 43 | cache: 'pnpm' 44 | - name: Install dependencies 45 | run: pnpm install 46 | - name: Build 47 | run: pnpm run build 48 | - name: Archive production artifacts 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: expander-card 52 | path: | 53 | dist 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lovelace-expander-card", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "watch": { 6 | "buildAndStart": { 7 | "patterns": [ 8 | "src" 9 | ], 10 | "extensions": "svelte" 11 | } 12 | }, 13 | "scripts": { 14 | "build": "eslint . && vite build", 15 | "srv": "pnpx serve -l 5000 --cors dist", 16 | "lint": "eslint ." 17 | }, 18 | "devDependencies": { 19 | "@eslint/eslintrc": "^3.3.3", 20 | "@eslint/js": "^9.39.1", 21 | "@stylistic/eslint-plugin": "^5.6.1", 22 | "@sveltejs/adapter-node": "^5.4.0", 23 | "@sveltejs/enhanced-img": "^0.6.1", 24 | "@sveltejs/vite-plugin-svelte": "5.1.1", 25 | "@types/eslint": "9.6.1", 26 | "eslint": "^9.39.1", 27 | "eslint-config-prettier": "^10.1.8", 28 | "eslint-plugin-prefer-arrow": "^1.2.3", 29 | "eslint-plugin-prettier": "^5.5.4", 30 | "eslint-plugin-svelte": "^3.13.1", 31 | "globals": "^16.5.0", 32 | "postcss-preset-env": "^10.5.0", 33 | "prettier": "^3.7.4", 34 | "prettier-plugin-svelte": "^3.4.1", 35 | "svelte": "5.46.0", 36 | "svelte-check": "^4.3.5", 37 | "svelte-eslint-parser": "^1.4.1", 38 | "tslib": "^2.8.1", 39 | "typescript": "^5.9.3", 40 | "typescript-eslint": "8.50.1", 41 | "vite": "^6.4.1" 42 | }, 43 | "dependencies": { 44 | "home-assistant-javascript-templates": "^6.0.0", 45 | "svelte": "5.46.0" 46 | }, 47 | "pnpm": { 48 | "ignoredBuiltDependencies": [ 49 | "@sveltejs/kit" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.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 | READ THIS FIRST: 10 | Thanks for raising a expander card issue. Please take the time to review the following 11 | categories as some of them do not apply here. 12 | 13 | 🙅 "Please DO NOT Raise an Issue" Cases 14 | - Question 15 | STOP!! Please ask questions about how to use something, or to understand why something isn't 16 | working as you expect it to Github discussion or on forum https://community.home-assistant.io/t/expander-accordion-collapsible-card/738817/4. 17 | - Managed Dependency Upgrade 18 | You DO NOT need to raise an issue for a managed dependency version upgrade as there's a semi-automatic process for checking managed dependencies for new versions before a release. BUT pull requests for upgrades that are more involved than just a version property change are still most welcome. 19 | - With an Immediate Pull Request 20 | An issue will be closed as a duplicate of the immediate pull request, so you don't have to raise an issue if you plan to create a pull request immediately. 21 | 22 | 🐞 Bug report (please don't include this emoji/text, just add your details) 23 | Please provide details of the problem, including the version of expander card and your Brwoser that you 24 | are using. If possible, please provide a test case or sample application that reproduces 25 | the problem. This makes it much easier for us to diagnose the problem and to verify that 26 | we have fixed it 27 | For quick troubleshooting, prepare a [minimally reproducible example](https://en.wikipedia.org/wiki/Minimal_reproducible_example). 28 | 29 | !!!Please check your Browser console for Javascript errors!!! 30 | 31 | 32 | 33 | TIP: You can always edit your issue if it isn't formatted correctly. 34 | See https://guides.github.com/features/mastering-markdown 35 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import replace from '@rollup/plugin-replace'; 3 | import svelte from 'rollup-plugin-svelte'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import terser from '@rollup/plugin-terser'; 7 | import sveltePreprocess from 'svelte-preprocess'; 8 | import json from '@rollup/plugin-json'; 9 | 10 | 11 | const MAIN_COMPONENT_NAME = 'ExpanderCard'; 12 | const TAG_NAME = 'expander-card'; 13 | const CONTAINER_TAG_NAME ='expander-child-card'; 14 | const FILE_NAME = `${TAG_NAME}.js`; 15 | 16 | 17 | export default (commandlineargs) => { 18 | console.log('commandlineargs: ', commandlineargs); 19 | return ({ 20 | input: 'src/index.ts', 21 | output: { 22 | sourcemap: true, 23 | format: 'umd', 24 | name: MAIN_COMPONENT_NAME, 25 | file: `dist/${FILE_NAME}` 26 | }, 27 | plugins: [ 28 | replace({ 29 | 'tag-name': TAG_NAME, 30 | 'container-tag-name': CONTAINER_TAG_NAME, 31 | 'preventAssignment': true 32 | }), 33 | svelte({ 34 | preprocess: 35 | sveltePreprocess({ 36 | sourceMap: true 37 | }), 38 | compilerOptions: { 39 | customElement: true, 40 | hydratable: true, 41 | dev: true 42 | }, 43 | emitCss: true 44 | }), 45 | resolve({ 46 | browser: true, 47 | dedupe: ['svelte'] 48 | }), 49 | commonjs(), 50 | json(), 51 | typescript({ 52 | sourceMap: true, 53 | inlineSources: !production 54 | }), 55 | production && terser() 56 | ], 57 | watch: { 58 | clearScreen: false 59 | } 60 | }); }; 61 | -------------------------------------------------------------------------------- /src/configtype.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 Peter Repukat - FlatspotSoftware 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | import type { LovelaceCardConfig } from './types'; 14 | export interface ExpanderCardVariables { 15 | variable: string; 16 | value_template: unknown; 17 | } 18 | 19 | export interface ExpanderCardTemplates { 20 | template: string; 21 | value_template: unknown; 22 | } 23 | export interface ExpanderConfig { 24 | clear?: boolean; 25 | 'clear-children'?: boolean; 26 | cards?: { type: string }[]; 27 | gap?: string; 28 | 'expanded-gap'?: string; 29 | padding?: string; 30 | title?: string; 31 | 'title-card'?: LovelaceCardConfig; 32 | 'title-card-padding'?: string; 33 | 'title-card-button-overlay'?: false; 34 | 'title-card-clickable'?: boolean; 35 | 'overlay-margin'?: string; 36 | 'child-padding'?: string; 37 | 'child-margin-top'?: string; 38 | expanded?: boolean; 39 | 'expander-card-background'?: string; 40 | 'expander-card-background-expanded'?: string; 41 | 'header-color'?: string; 42 | 'button-background'?: string; 43 | 'arrow-color'?: string; 44 | 'expander-card-display'?: string; 45 | 'min-width-expanded'?: number; 46 | 'max-width-expanded'?: number; 47 | icon?: string; 48 | 'storage-id'?: string; 49 | 'icon-rotate-degree'?: string; 50 | 'show-button-users'?: string[]; 51 | 'start-expanded-users'?: string[]; 52 | animation?: boolean; 53 | 'expander-card-id'?: string; 54 | style?: string; 55 | variables?: Record; 56 | templates?: Record; 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/release-workflow.yml: -------------------------------------------------------------------------------- 1 | name: "Create Tagged Release" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_version: 7 | description: 'Version number of the release' 8 | required: true 9 | 10 | jobs: 11 | gh_tagged_release: 12 | name: Create tagged release 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout project 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Git 20 | run: | 21 | git config user.name github-actions 22 | git config user.email github-actions@github.com 23 | 24 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda 25 | name: Install pnpm 26 | with: 27 | version: 9 28 | run_install: false 29 | - name: Install Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | cache: 'pnpm' 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Set app version (Unix) 38 | run: npm version ${{ github.event.inputs.release_version }} --no-git-tag-version 39 | 40 | - name: Build Project 41 | run: pnpm run build 42 | 43 | - name: "Create Github release (full)" 44 | if: ${{ !contains( github.event.inputs.release_version, '-rc' ) }} 45 | uses: softprops/action-gh-release@v2 46 | id: expander_card_release 47 | with: 48 | body: "Release version ${{ github.event.inputs.release_version }}." 49 | tag_name: ${{ github.event.inputs.release_version }} 50 | target_commitish: "main" 51 | draft: false 52 | prerelease: false 53 | files: dist/* 54 | generate_release_notes: true 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: "Notify issues of release their fix is contained in" 59 | uses: apexskier/github-release-commenter@3bd413ad5e1d603bfe2282f9f06f2bdcec079327 # v1.3.6 60 | with: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | comment-template: | 63 | Release {release_link} addresses this. 64 | -------------------------------------------------------------------------------- /src/helpers/templates.ts: -------------------------------------------------------------------------------- 1 | import HomeAssistantJavaScriptTemplates, { HomeAssistant, HomeAssistantJavaScriptTemplatesRenderer } from 'home-assistant-javascript-templates'; 2 | 3 | export function getJSTemplateRenderer(variables: Record = {}, refs: Record = {}): Promise { 4 | return new HomeAssistantJavaScriptTemplates( 5 | document.querySelector('home-assistant') as HomeAssistant, 6 | { 7 | autoReturn: false, 8 | variables, 9 | refs, 10 | refsVariableName: 'variables' 11 | } 12 | ).getRenderer(); 13 | } 14 | 15 | export function isJSTemplate(template: unknown): boolean { 16 | if (!template || typeof template !== 'string') return false; 17 | return String(template).trim().startsWith('[[[') && String(template).trim().endsWith(']]]'); 18 | } 19 | 20 | export function renderJSTemplate( 21 | templatesRenderer: Promise, 22 | template: string, 23 | variables: Record = {}) { 24 | if (!isJSTemplate(template)) { 25 | throw new Error('Not a valid JS template'); 26 | } 27 | template = String(template).trim().slice(3, -3); 28 | void templatesRenderer.then((renderer) => renderer.renderTemplate(template, { variables } )); 29 | } 30 | 31 | export function trackJSTemplate( 32 | templatesRenderer: Promise, 33 | callback: (result: unknown) => void, 34 | template: string, 35 | variables: Record = {}): Promise<(() => void)> { 36 | if (!isJSTemplate(template)) { 37 | throw new Error('Not a valid JS template'); 38 | } 39 | template = String(template).trim().slice(3, -3); 40 | return templatesRenderer.then((renderer) => renderer.trackTemplate(template, callback, { variables })); 41 | } 42 | 43 | export function setJSTemplateRef( 44 | templatesRenderer: Promise, 45 | refName: string, 46 | refValue: unknown) { 47 | void templatesRenderer.then((renderer) => { 48 | renderer.refs[refName] = refValue; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/ExpanderCardEditor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | import { ExpanderCardEditorNulls, ExpanderCardEditorSchema, expanderCardEditorTemplates } from './editortype'; 5 | import { HomeAssistantUser } from './types'; 6 | 7 | const wdw = window; // NOSONAR es2019 8 | 9 | let helpers = (wdw as any).cardHelpers; 10 | const helperPromise = new Promise((resolve) => { 11 | if (helpers) resolve(); 12 | if ((wdw as any).loadCardHelpers) { 13 | (wdw as any).loadCardHelpers().then((loadedHelpers: any) => { 14 | helpers = loadedHelpers; 15 | (wdw as any).cardHelpers = helpers; 16 | resolve(); 17 | }); 18 | } 19 | }); 20 | 21 | async function fetchUsers(): Promise { 22 | const el = document.querySelector('home-assistant'); 23 | const hass = (el as any)?.hass; 24 | if (!hass) return; 25 | const users = await hass.callWS({ type: 'config/auth/list' }); 26 | return users 27 | .filter((user: HomeAssistantUser) => !user.system_generated) 28 | .map((user: HomeAssistantUser) => user.name); 29 | } 30 | 31 | const loader = async (): Promise => { 32 | // create a temporary vertical-stack card to inherit from 33 | const verticalStackCard = await helperPromise.then(() => 34 | helpers.createCardElement({ type: 'vertical-stack', cards: [] })); 35 | // get its editor class 36 | const verticalStackEditor = await verticalStackCard.constructor.getConfigElement(); 37 | // fetch users 38 | const users = await fetchUsers(); 39 | // return a new class that extends the vertical-stack editor 40 | return class ExpanderCardEditor extends verticalStackEditor.constructor { 41 | public constructor() { 42 | super(); 43 | this._users = users; 44 | } 45 | 46 | // override setConfig to store config only and not assert stack editor config 47 | // we also upgrade any old config here if needed 48 | public setConfig(config: any): void { 49 | this._config = config; 50 | } 51 | 52 | // define _schema getter to return our own schema 53 | public get _schema(): any { 54 | const schema = ExpanderCardEditorSchema; 55 | const schemaJSON = JSON.stringify(schema); 56 | const usersEscaped = this._users 57 | .map((u: string) => u.replace(/\\/g, '\\\\').replace(/"/g, '\\"')) // NOSONAR es2019 58 | .join('","'); 59 | let populatedSchemaJSON = schemaJSON.replace(/\[\[users\]\]/g, usersEscaped); // NOSONAR es2019 60 | // populate templates options, but only those not already in config 61 | populatedSchemaJSON = populatedSchemaJSON.replace(/\[\[templates\]\]/g, // NOSONAR es2019 62 | expanderCardEditorTemplates 63 | .filter((t: any) => !this._config.templates?.some((ct: any) => ct.template === t)) 64 | .join('","')); 65 | const populatedSchema = JSON.parse(populatedSchemaJSON); 66 | return populatedSchema; 67 | } 68 | 69 | // _schema setter does nothing as we want to use our own schema 70 | public set _schema(_) { 71 | // do nothing 72 | } 73 | 74 | // override _computeLabelCallback to show label or name 75 | public _computeLabelCallback = (item: any): string => item.label ?? item.name ?? ''; 76 | 77 | // override _valueChanged to remove null values from config before storing and firing event 78 | public _valueChanged = (ev: CustomEvent): void => { 79 | const config = ev.detail.value; 80 | const entries = Object.entries(ExpanderCardEditorNulls); 81 | for (const [key, value] of entries) { 82 | if (typeof value === 'object' && Array.isArray(value) && Array.isArray(config[key])) { 83 | if (JSON.stringify(config[key]) === JSON.stringify(value)) { 84 | delete config[key]; 85 | } 86 | continue; 87 | } 88 | if (config[key] === value) { 89 | delete config[key]; 90 | } 91 | } 92 | this._config = config; 93 | this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this._config } })); 94 | }; 95 | }; 96 | }; 97 | 98 | export const loadExpanderCardEditor = (async () => { 99 | // Wait for scoped customElements registry to be set up 100 | while (customElements.get('home-assistant') === undefined) 101 | await new Promise((resolve) => wdw.setTimeout(resolve, 100)); 102 | 103 | if (!customElements.get('expander-card-editor')) { 104 | const expanderCardEditor = await loader(); 105 | customElements.define('expander-card-editor', expanderCardEditor); 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import stylistic from '@stylistic/eslint-plugin' 5 | 6 | 7 | // mimic CommonJS variables -- not needed if using CommonJS 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname 13 | }); 14 | 15 | import js from '@eslint/js'; 16 | import tseslint from 'typescript-eslint'; 17 | import eslintPluginSvelte from 'eslint-plugin-svelte'; 18 | import svelteEslintParser from 'svelte-eslint-parser'; 19 | import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'; 20 | import globals from 'globals'; 21 | 22 | 23 | export default tseslint.config( 24 | eslintPluginPrettier, 25 | js.configs.recommended, 26 | ...tseslint.configs.recommended, 27 | ...eslintPluginSvelte.configs['flat/recommended'], 28 | { 29 | ignores: ['eslint.config.mjs','rollup.config.mjs','dist/**/*','vite.config.js', 'svelte.config.js', 'eslint.config.js', '.svelte-kit/**/*', 'build/**/*'], 30 | }, 31 | { 32 | plugins: { 33 | '@typescript-eslint': tseslint.plugin, 34 | '@stylistic': stylistic 35 | }, 36 | languageOptions: { 37 | parser: tseslint.parser, 38 | globals: { 39 | ...globals.browser, 40 | ...globals.node, 41 | ...globals.es2021, 42 | }, 43 | parserOptions: { 44 | sourceType: 'module', 45 | ecmaVersion: 2020, 46 | project: './tsconfig.json', 47 | extraFileExtensions: ['.svelte'], 48 | }, 49 | }, 50 | rules: { 51 | 'prettier/prettier': 'off', 52 | '@typescript-eslint/no-unused-expressions': 'off', 53 | '@typescript-eslint/no-namespace': 'off', 54 | '@typescript-eslint/adjacent-overload-signatures': 'error', 55 | '@typescript-eslint/array-type': 'error', 56 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 57 | '@typescript-eslint/no-inferrable-types': 'error', 58 | '@typescript-eslint/no-misused-new': 'error', 59 | '@typescript-eslint/no-this-alias': 'error', 60 | '@typescript-eslint/prefer-for-of': 'error', 61 | '@typescript-eslint/prefer-function-type': 'error', 62 | '@typescript-eslint/prefer-namespace-keyword': 'error', 63 | 'no-inner-declarations': 'off', 64 | '@typescript-eslint/triple-slash-reference': 'error', 65 | '@stylistic/type-annotation-spacing': 'error', 66 | '@typescript-eslint/unified-signatures': 'error', 67 | '@typescript-eslint/no-explicit-any': 'error', 68 | '@typescript-eslint/no-unused-vars': 'error', 69 | '@typescript-eslint/unbound-method': 'warn', 70 | '@typescript-eslint/no-floating-promises': 'error', 71 | '@typescript-eslint/no-unnecessary-type-assertion': 'error', 72 | 'object-curly-spacing': ['error', 'always'], 73 | '@stylistic/semi': [ 74 | 'error', 75 | 'always' 76 | ], 77 | '@stylistic/quotes': [ 78 | 'warn', 79 | 'single' 80 | ], 81 | '@stylistic/member-delimiter-style': [ 82 | 'error', 83 | { 84 | multiline: { 85 | delimiter: 'semi', 86 | requireLast: true 87 | }, 88 | singleline: { 89 | delimiter: 'semi', 90 | requireLast: false 91 | } 92 | } 93 | ], 94 | '@stylistic/indent': [ 95 | 'warn', 96 | 4, 97 | { 98 | FunctionDeclaration: { 99 | parameters: 'first' 100 | }, 101 | FunctionExpression: { 102 | parameters: 'first' 103 | }, 104 | SwitchCase: 1 105 | } 106 | ], 107 | 108 | '@typescript-eslint/explicit-member-accessibility': [ 109 | 'error', 110 | { 111 | accessibility: 'explicit' 112 | } 113 | ], 114 | '@typescript-eslint/no-use-before-define': ['error', { functions: false }], 115 | 'no-console': 'warn', 116 | 'no-return-await': 'error', 117 | 'arrow-body-style': 'error', 118 | 'arrow-parens': [ 119 | 'error', 120 | 'always' 121 | ], 122 | 'comma-dangle': [ 123 | 'error', 124 | { 125 | objects: 'never', 126 | arrays: 'never', 127 | functions: 'never' 128 | } 129 | ], 130 | 'prefer-arrow-callback': 'error', 131 | 'prefer-const': 'error', 132 | 'quote-props': [ 133 | 'error', 134 | 'consistent-as-needed' 135 | ], 136 | 'no-var': 'error', 137 | 'new-parens': 'error', 138 | 'no-caller': 'error', 139 | 'no-cond-assign': 'error', 140 | 'no-debugger': 'error', 141 | 'no-empty': 'error', 142 | 'no-eval': 'error', 143 | 'no-multiple-empty-lines': 'warn', 144 | 'no-new-wrappers': 'error', 145 | 'no-redeclare': 'error', 146 | 'no-shadow': [ 147 | 'error', 148 | { 149 | hoist: 'all' 150 | } 151 | ], 152 | 'no-throw-literal': 'error', 153 | 'no-trailing-spaces': 'error', 154 | 'no-undef-init': 'error', 155 | 'no-underscore-dangle': 'error', 156 | 'no-unsafe-finally': 'error', 157 | 'no-unused-labels': 'error', 158 | 'spaced-comment': 'error', 159 | 'use-isnan': 'error', 160 | 'max-len': [ 161 | 'warn', 162 | { 163 | code: 180 164 | } 165 | ], 166 | 'dot-notation': 'error', 167 | 'eqeqeq': 'error', 168 | 'eol-last': 'error', 169 | 'linebreak-style': ['error', 'unix'], 170 | 'block-spacing': ['error', 'always'], 171 | 'tsdoc/syntax': 'off' 172 | }, 173 | }, 174 | { 175 | files: ['*.svelte', '**/*.svelte'], 176 | languageOptions: { 177 | parser: svelteEslintParser, 178 | parserOptions: { 179 | parser: tseslint.parser, 180 | } 181 | }, 182 | rules: { 183 | 'prettier/prettier': ['warn', { 184 | 'svelteStrictMode': true, 185 | 'svelteBracketNewLine': false, 186 | 'svelteAllowShorthand': false, 187 | 'svelteIndentScriptAndStyle': false, 188 | 'tabWidth': 4, 189 | 'bracketSpacing': true, 190 | 'trailingComma': 'none', 191 | 'arrowParens': 'always', 192 | 'semi': true, 193 | 'singleQuote': true, 194 | 'printWidth': 110, 195 | 'proseWrap': 'preserve', 196 | 'plugins': ['prettier-plugin-svelte'], 197 | }], 198 | } 199 | } 200 | ); 201 | -------------------------------------------------------------------------------- /src/Card.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 142 | 143 |
146 | {#if loading} 147 | Loading... 148 | {/if} 149 |
150 | 151 | 152 | 218 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/editortype.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | import { ExpanderConfig } from './configtype'; 3 | 4 | export const ExpanderCardEditorNulls: ExpanderConfig = { 5 | icon: '', 6 | 'arrow-color': '', 7 | 'icon-rotate-degree': '', 8 | 'header-color': '', 9 | 'button-background': '', 10 | 'min-width-expanded': 0, 11 | 'max-width-expanded': 0, 12 | 'storage-id': '', 13 | 'expander-card-id': '', 14 | 'show-button-users': [], 15 | 'start-expanded-users': [], 16 | 'expander-card-background': '', 17 | 'expander-card-background-expanded': '', 18 | 'expander-card-display': '', 19 | gap: '', 20 | padding: '', 21 | 'expanded-gap': '', 22 | 'child-padding': '', 23 | 'child-margin-top': '', 24 | 'overlay-margin': '', 25 | 'title-card-padding': '', 26 | 'style': '' 27 | }; 28 | 29 | export const expanderCardEditorTemplates = [ 30 | 'expanded', 31 | 'style' 32 | ]; 33 | 34 | const iconSelector = { icon: {} }; 35 | const textSelector = { text: {} }; 36 | const multilineTextSelector = { text: { multiline: true } }; 37 | const booleanSelector = { boolean: {} }; 38 | const objectSelector = { object: {} }; 39 | const numberSelector = (unit_of_measurement: string) => ({ 40 | number: { 41 | unit_of_measurement 42 | } 43 | }); 44 | 45 | const iconField = (name: string, label: string) => ({ 46 | name, 47 | label, 48 | selector: iconSelector 49 | }); 50 | 51 | const textField = (name: string, label: string) => ({ 52 | name, 53 | label, 54 | selector: textSelector 55 | }); 56 | 57 | const multilineTextField = (name: string, label: string) => ({ 58 | name, 59 | label, 60 | selector: multilineTextSelector 61 | }); 62 | 63 | const booleanField = (name: string, label: string) => ({ 64 | name, 65 | label, 66 | selector: booleanSelector 67 | }); 68 | 69 | const objectField = (name: string, label: string) => ({ 70 | name, 71 | label, 72 | selector: objectSelector 73 | }); 74 | 75 | const numberField = (name: string, label: string, unit_of_measurement: string) => ({ 76 | name, 77 | label, 78 | selector: numberSelector(unit_of_measurement) 79 | }); 80 | 81 | const labelField = (label: string) => ({ 82 | label, 83 | type: 'constant' 84 | }); 85 | 86 | // See https://www.home-assistant.io/docs/blueprint/selectors 87 | export const ExpanderCardEditorSchema = [ 88 | { 89 | type: 'expandable', 90 | label: 'Expander Card Settings', 91 | icon: 'mdi:arrow-down-bold-box-outline', 92 | schema: [ 93 | { 94 | ...textField('title', 'Title') 95 | }, 96 | { 97 | ...iconField('icon', 'Icon') 98 | }, 99 | { 100 | type: 'expandable', 101 | label: 'Expander control', 102 | icon: 'mdi:cog-outline', 103 | schema: [ 104 | { 105 | type: 'grid', 106 | schema: [ 107 | { 108 | ...booleanField('expanded', 'Start expanded') 109 | }, 110 | { 111 | ...booleanField('animation', 'Enable animation') 112 | }, 113 | { 114 | ...numberField('min-width-expanded', 'Min width expanded', 'px') 115 | }, 116 | { 117 | ...numberField('max-width-expanded', 'Max width expanded', 'px') 118 | }, 119 | { 120 | ...textField('storage-id', 'Storage ID') 121 | }, 122 | { 123 | ...textField('expander-card-id', 'Expander card ID') 124 | } 125 | ] 126 | } 127 | ] 128 | }, 129 | { 130 | type: 'expandable', 131 | label: 'Expander styling', 132 | icon: 'mdi:palette-swatch', 133 | schema: [ 134 | { 135 | type: 'grid', 136 | schema: [ 137 | { 138 | ...textField('arrow-color', 'Icon color') 139 | }, 140 | { 141 | ...textField('icon-rotate-degree', 'Icon rotate degree') 142 | }, 143 | { 144 | ...textField('header-color', 'Header color') 145 | }, 146 | { 147 | ...textField('button-background', 'Button background color') 148 | }, 149 | { 150 | ...textField('expander-card-background', 'Background') 151 | }, 152 | { 153 | ...textField('expander-card-background-expanded', 'Background when expanded') 154 | }, 155 | { 156 | ...textField('expander-card-display', 'Expander card display') 157 | }, 158 | { 159 | ...booleanField('clear', 'Clear border and background') 160 | }, 161 | { 162 | ...textField('gap', 'Gap') 163 | }, 164 | { 165 | ...textField('padding', 'Padding') 166 | } 167 | ] 168 | } 169 | ] 170 | }, 171 | { 172 | type: 'expandable', 173 | label: 'Card styling', 174 | icon: 'mdi:palette-swatch-outline', 175 | schema: [ 176 | { 177 | type: 'grid', 178 | schema: [ 179 | { 180 | ...textField('expanded-gap', 'Card gap') 181 | }, 182 | { 183 | ...textField('child-padding', 'Card padding') 184 | }, 185 | { 186 | ...textField('child-margin-top', 'Card margin top') 187 | }, 188 | { 189 | ...booleanField('clear-children', 'Clear card border and background') 190 | } 191 | ] 192 | } 193 | ] 194 | }, 195 | { 196 | type: 'expandable', 197 | label: 'Title card', 198 | icon: 'mdi:subtitles-outline', 199 | schema: [ 200 | { 201 | ...labelField('Use YAML to specify a title card to replace the expander title') 202 | }, 203 | { 204 | ...objectField('title-card', '') 205 | }, 206 | { 207 | type: 'grid', 208 | schema: [ 209 | { 210 | ...booleanField('title-card-clickable', 'Make title card clickable to expand/collapse') 211 | }, 212 | { 213 | ...booleanField('title-card-button-overlay', 'Overlay expand button on title card') 214 | }, 215 | { 216 | ...textField('overlay-margin', 'Overlay margin') 217 | }, 218 | { 219 | ...textField('title-card-padding', 'Title card padding') 220 | } 221 | ] 222 | } 223 | ] 224 | }, 225 | { 226 | type: 'expandable', 227 | label: 'User settings', 228 | icon: 'mdi:account-multiple-outline', 229 | schema: [ 230 | { 231 | type: 'grid', 232 | schema: [ 233 | { 234 | name: 'show-button-users', 235 | label: 'Show button users', 236 | selector: { 237 | select: { 238 | multiple: true, 239 | mode: 'dropdown', 240 | custom: true, // to allow for unknown users 241 | options: ['[[users]]'] // to be populated dynamically 242 | } 243 | } 244 | }, 245 | { 246 | name: 'start-expanded-users', 247 | label: 'Start expanded users', 248 | selector: { 249 | select: { 250 | multiple: true, 251 | mode: 'dropdown', 252 | custom: true, // to allow for unknown users 253 | options: ['[[users]]'] // to be populated dynamically 254 | } 255 | } 256 | } 257 | ] 258 | } 259 | ] 260 | }, 261 | { 262 | type: 'expandable', 263 | label: 'Advanced styling', 264 | icon: 'mdi:brush-outline', 265 | schema: [ 266 | { 267 | ...multilineTextField('style', 'Custom CSS style') 268 | } 269 | ] 270 | }, 271 | { 272 | type: 'expandable', 273 | label: 'Advanced templates', 274 | icon: 'mdi:code-brackets', 275 | schema: [ 276 | { 277 | type: 'expandable', 278 | label: 'Variables', 279 | icon: 'mdi:variable', 280 | schema: [ 281 | { 282 | name: 'variables', 283 | label: 'Variables', 284 | selector: { 285 | object: { 286 | label_field: 'variable', 287 | multiple: true, 288 | fields: { 289 | variable: { 290 | label: 'Variable name', 291 | required: true, 292 | selector: { text: {} } 293 | }, 294 | value_template: { 295 | label: 'Value template', 296 | required: true, 297 | selector: { text: { multiline: true } } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | ] 304 | }, 305 | { 306 | type: 'expandable', 307 | label: 'Templates', 308 | icon: 'mdi:code-brackets', 309 | schema: [ 310 | { 311 | name: 'templates', 312 | label: 'Templates', 313 | selector: { 314 | object: { 315 | label_field: 'template', 316 | multiple: true, 317 | fields: { 318 | template: { 319 | label: 'Config item', 320 | required: true, 321 | selector: { 322 | select: { 323 | mode: 'dropdown', 324 | custom_value: true, // to allow for current templates not in dropdown 325 | sort: true, 326 | options: ['[[templates]]'] // to be populated dynamically 327 | } 328 | } 329 | }, 330 | value_template: { 331 | label: 'Value template', 332 | required: true, 333 | selector: { text: { multiline: true } } 334 | } 335 | } 336 | } 337 | } 338 | } 339 | ] 340 | } 341 | ] 342 | } 343 | ] 344 | } 345 | ]; 346 | -------------------------------------------------------------------------------- /src/ExpanderCard.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | class extends customElementConstructor { 31 | // re-declare props used in customClass. 32 | public config!: ExpanderConfig; 33 | 34 | public static async getConfigElement() { 35 | await loadExpanderCardEditor(); 36 | return document.createElement('expander-card-editor'); 37 | } 38 | 39 | public static getStubConfig() { 40 | return { 41 | type: 'custom:expander-card', 42 | title: 'Expander Card', 43 | cards: [] 44 | }; 45 | } 46 | 47 | public setConfig(conf = {}) { 48 | this.config = { ...defaults, ...conf }; 49 | }; 50 | } 51 | }}/> 52 | 53 | 412 | 413 | 426 | {#if config['title-card']} 427 |
434 |
437 | 445 |
446 | {#if showButtonUsers} 447 | 465 | {/if} 466 | {#if config['title-card-clickable'] && !config['title-card-button-overlay'] } 467 | 468 | {/if} 469 |
470 | {:else} 471 | {#if showButtonUsers} 472 | 484 | {/if} 485 | {/if} 486 | {#if config.cards} 487 |
488 |
494 | {#each config.cards as card (card)} 495 | 504 | {/each} 505 |
506 |
507 | {/if} 508 | {#if userStyleTemplateOrConfig} 509 | 510 | {@html userStyleTemplateOrConfig} 511 | {/if} 512 |
513 | 514 | 611 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expander Card for HomeAssistant 2 | 3 | [![release][release-badge]][release-url] 4 | ![downloads][downloads-badge] 5 | ![build][build-badge] 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MelleD_lovelace-expander-card&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=MelleD_lovelace-expander-card) 7 | [![PayPal.Me][paypal-me-badge]][paypal-me-url] 8 | [![BuyMeCoffee][buy-me-a-coffee-shield]][buy-me-a-coffee-url] 9 | 10 | Expander/Collapsible card for HomeAssistant 11 | 12 | ## Introduction 13 | 14 | First a few words to start with. A big thank you goes to [@Alia5](https://github.com/Alia5/lovelace-expander-card), who initially launched the card. I forked this card for my own HomeAssistant to make a few improvements. I give no guarantee for the functionality and no promise of lifelong maintenance, as I do the whole thing in my free time. Of course, I am happy about every contribution and PR 15 | 16 | ## Demo 17 | 18 | ![Sample gif](examples/example.gif) 19 | 20 | --- 21 | 22 | Expand button as overlay: 23 | ![Sample lights overlay](examples/lights_overlay_button.png) 24 | 25 | --- 26 | 27 | You can even nest expanders! 28 | 29 | ![Sample nesting](examples/nested.png) 30 | 31 | --- 32 | 33 | Clear Background (default theme): 34 | 35 | ![Sample clear router](examples/clear_router.png) 36 | 37 | ## Options 38 | 39 | All options are available for editing in Graphical config editor. Title card config is in YAML at this time. 40 | 41 | Yaml Options: 42 | 43 | | Name | Type | Default | Supported options | Description | 44 | | ------------------------- | -------- | ------------- | ---------------------- | ----------------------------------------------------- | 45 | | type | string | **Required** | `custom:expander-card` | Type of the card. | 46 | | title | string | Empty | * | Title (Not displayed if using Title-Card) | 47 | | icon | string | mdi:chevron-down | mdi icon shortcut | Icon in button | 48 | | expanded | boolean | _false_ | true\|false | Start expanded | 49 | | animation | boolean | _true_ | true\|false | Should the opening/closing of expander be animated? | 50 | | min-width-expanded | number | 0 | number | Min screen width (px) to be expanded on start (use with start expanded above) | 51 | | max-width-expanded | number | 0 | number | Max screen width (px) to be expanded on start (use with start expanded above) | 52 | | storage-id | string | **optional** | * | Save last expander state in local browser storage | 53 | | expander-card-id | string | **optional** | * | An id to use with [Set state via action](#set-state-via-action) | 54 | | arrow-color | string | primary-text-color,#fff | css-color | Color of ico expand button | 55 | | icon-rotate-degree | string | _180deg_ | css-rotate | Changing the degrees of the button icon when clicked | 56 | | header-color | string | primary-text-color,#fff | css-color | Color of expand button | 57 | | button-background | string | _transparent_ | css-color | Background color of expand button | 58 | | expander-card-background | string | ha-card-background, card-background-color,#fff | css-color | Expander Card Background | 59 | | expander-card-background-expanded | string | Empty | css-color | Expander Card Background when card is opened/expanded| 60 | | expander-card-display | string | block | css-display | Layout/Display of the card | 61 | | clear | boolean | _false_ | true\|false | Remove Background, border | 62 | | gap | string | _0.0em_ | css-size | gap between cards when expander closed. This option depends on your CSS layout: You might need to use `expander-card-display: grid` for this. | 63 | | padding | string | _1em_ | css-size | padding of all card content | 64 | | expanded-gap | string | _0.6em_ | css-size | gap between child cards when expander open | 65 | | child-padding | string | _0.0em_ | css-size | padding of child cards | 66 | | child-margin-top | string | _0.0em_ | css-size | Margin top of child cards | 67 | | clear-children | boolean | _false_ | true\|false | Remove Background, border from children | 68 | | title-card | object | **optional** | LovelaceCardConfig | Replace Title with card | 69 | | title-card-clickable | boolean | _false_ | true\|false | Should the complete div be clickable? | 70 | | title-card-button-overlay | boolean | _false_ | true\|false | Overlay expand button over title-card. If you set `title-card-clickable: true` the overlay will extend across the expander, both horizontally and vertically, and capture the click before the title-card. If you wish to adjust the overlay height you can style `height` on `.header-overlay`. See [Style](#style) | 71 | | overlay-margin | string | _0.0em_ | css-size | Margin from top right of expander button (if overlay) | 72 | | title-card-padding | string | _0px_ | css-size | padding of title-card | 73 | | show-button-users | object[] | **optional** | * | Choose the persons/users that button is visible to them. | 74 | | start-expanded-users | object[] | **optional** | * | Choose the persons/users that card will be start expanded for them. | 75 | | cards | object[] | **optional** | LovelaceCardConfig[] | Child cards to show when expanded | 76 | | style | string | **optional**. | css style rules | Advanced css styling rules. if you wish to style/hide the hover/press ripple of the expander-card button you can use advanced styling. See [Hover/press ripple](#hoverpress-ripple). | 77 | | variables | dictionary | **optional**| List | See Advanced javascript templates | 78 | | templates | dictionary | **optional**| List | See Advanced javascript templates | 79 | 80 | ## Examples 81 | 82 | Here are a few examples of usage. 83 | 84 | ### Title card 85 | 86 | Example title card that is clickable and has 2 nested cards, which is directly expanded 87 | 88 | ```yaml 89 | - type: custom:expander-card 90 | child-margin-top: 0.6em 91 | padding: 0 92 | clear: true 93 | title-card-button-overlay: true 94 | title-card-clickable: true 95 | expanded: true 96 | title-card: 97 | type: "custom:digital-clock" 98 | dateFormat: 99 | weekday: "long" 100 | day: "2-digit" 101 | month: "short" 102 | timeFormat: 103 | hour: "2-digit" 104 | minute: "2-digit" 105 | cards: 106 | - type: custom:simple-weather-card 107 | entity: weather.openweathermap 108 | primary_info: 109 | - wind_speed 110 | - wind_bearing 111 | secondary_info: 112 | - precipitation 113 | - precipitation_probability 114 | - type: custom:hourly-weather 115 | entity: weather.openweathermap 116 | icons: true 117 | show_precipitation_probability: true 118 | show_precipitation_amounts: true 119 | forecast_type: "hourly" 120 | num_segments: 10" 121 | label_spacing: "1" 122 | name: null 123 | show_wind: speed 124 | ``` 125 | 126 | ### Heading Title card 127 | 128 | Example with [heading](https://www.home-assistant.io/dashboards/heading/) title card to the possibility to style your title. 129 | 130 | ```yaml 131 | - type: custom:expander-card 132 | title-card: 133 | type: heading 134 | heading: Title 135 | heading_style: title 136 | badges: 137 | - type: entity 138 | show_name: false 139 | show_state: true 140 | show_icon: true 141 | entity: light.bed_light 142 | icon: mdi:account 143 | ``` 144 | 145 | ### Template Title card with Mushroom 146 | 147 | If you need templates in your title, you can make good use of the Mushroom cards. Here's an example using the [Mushroom title card](//github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/title.md). 148 | https: 149 | 150 | ```yaml 151 | - type: custom:expander-card 152 | title-card: 153 | type: custom:mushroom-title-card 154 | title: |- 155 | {{ now().hour }} 156 | ``` 157 | 158 | ### Simple Title 159 | 160 | Example with title that is clickable and has 2 nested cards. 161 | 162 | ```yaml 163 | - type: custom:expander-card 164 | child-margin-top: 0.6em 165 | padding: 0 166 | title: "Test" 167 | title-card-button-overlay: true 168 | title-card-clickable: true 169 | cards: 170 | - type: custom:simple-weather-card 171 | entity: weather.openweathermap 172 | primary_info: 173 | - wind_speed 174 | - wind_bearing 175 | secondary_info: 176 | - precipitation 177 | - precipitation_probability 178 | - type: custom:hourly-weather 179 | entity: weather.openweathermap 180 | icons: true 181 | show_precipitation_probability: true 182 | show_precipitation_amounts: true 183 | forecast_type: "hourly" 184 | num_segments: 10" 185 | label_spacing: "1" 186 | name: null 187 | show_wind: speed 188 | ``` 189 | 190 | ### Title with min-width-expanded 191 | 192 | Example with title that is clickable and has 2 nested cards with are automatically expanded when the screen is more than 300px. 193 | 194 | ```yaml 195 | - type: custom:expander-card 196 | child-margin-top: 0.6em 197 | padding: 0 198 | title: "Test" 199 | title-card-button-overlay: true 200 | title-card-clickable: true 201 | min-width-expanded: 300 202 | cards: 203 | - type: custom:simple-weather-card 204 | entity: weather.openweathermap 205 | primary_info: 206 | - wind_speed 207 | - wind_bearing 208 | secondary_info: 209 | - precipitation 210 | - precipitation_probability 211 | name: in Gärtringen 212 | - type: custom:hourly-weather 213 | entity: weather.openweathermap 214 | icons: true 215 | show_precipitation_probability: true 216 | show_precipitation_amounts: true 217 | forecast_type: "hourly" 218 | num_segments: 10" 219 | label_spacing: "1" 220 | show_wind: speed 221 | ``` 222 | 223 | ### Title card with action 224 | 225 | The configuration below will open or close the expander when you tap the Mushroom Light Card. This means you cannot switch the light on or off by tapping it, but you can still adjust the brightness. 226 | 227 | ```yaml 228 | type: custom:expander-card 229 | title: Expander Card 230 | expander-card-id: my-light-card 231 | cards: 232 | - type: entities 233 | entities: 234 | - entity: sun.sun 235 | title-card: 236 | type: tile 237 | entity: light.bed_light 238 | vertical: false 239 | features_position: inline 240 | features: 241 | - type: light-brightness 242 | tap_action: 243 | action: fire-dom-event 244 | expander-card: 245 | data: 246 | expander-card-id: my-light-card 247 | action: toggle 248 | ``` 249 | 250 | ## Advanced javascript templates 251 | 252 | Expander card supports javascript templates for the config items listed below. This list may be added to over time based on user feature requests. If you wish for a config item to be supported by javascript template please submit a feature request. 253 | 254 | | Config item | Accepts value | Overrides config items | 255 | | ----------- | ------------- | ---------------------- | 256 | | `expanded` | boolean (`true\|false`) | `expanded`, `min-width-expanded`, `max-width-expanded`, `start-expanded-users` | 257 | | `style` | string | `style` | 258 | 259 | Javascript templates are implemented using the [home-assistant-javascript-templates](https://github.com/elchininet/home-assistant-javascript-templates) library by @elchininet. For objects and methods supported see [Objects and methods available in the templates](https://github.com/elchininet/home-assistant-javascript-templates#objects-and-methods-available-in-the-templates). The `config` object is also available which is the config object for the expander card where all config items can be read. e.g. `config['expander-card-id']`. 260 | 261 | Templates may also use `variables`, which are also javascript templates or just values. Templates are reactive to `variables` such that if a variable template value changes, the template using the variable itself will be evaluated and return its value. Expander card uses the [home-assistant-javascript-templates](https://github.com/elchininet/home-assistant-javascript-templates) `refs` feature using `variables` as the `refsVariableName`. You are best to ignore anything about `refs` in the library unless you know what you are doing. 262 | 263 | Templates are not continually evaluated but rely on reactive properties for updates. Follow the guidelines in [Objects and methods available in the templates](https://github.com/elchininet/home-assistant-javascript-templates#objects-and-methods-available-in-the-templates). 264 | 265 | Javascript for variables and templates are set using `value_template` string, enclosing javascript with `[[[]]]`, that is three open square bracket `[[[` followed by three close square bracket `]]]`. This follows the convention followed by other custom cards for javascript templates. Variables and templates can also return a straight value, which will follow YAML syntax converted to the type required for the config item being templated e.g. for a config item that accepts `boolean`, `true`, `"True"`, `1`, `"1"` are all considered `true`. 266 | 267 | ### Variables 268 | 269 | Variables are defined in the `variables` list of expander card config. 270 | 271 | **IMPORTANT**: As variables are evaluated asynchronously, their initial value will be `undefined`. Your templates need to be written to handle this initial case. 272 | 273 | | List item | Type | Config | 274 | | --------- | ---- | ------ | 275 | | `variable` | string | The `` of the variable which will be available in templates as `variable.`. | 276 | | `value_template` | string \| value \| object | Either javascript that returns a value or a straight value. Javascript must be enclosed by `[[[]]]` with only whitespace preceding of following. | 277 | 278 | Variable `weather_warnings` tracking the state of `input_boolean.weather_warnings`. 279 | 280 | ```yaml 281 | variables: 282 | - variable: weather_warnings 283 | value_template: | 284 | [[[ 285 | return is_state('input_boolean.weather_warnings', 'on'); 286 | ]]] 287 | ``` 288 | 289 | ### Templates 290 | 291 | Templates are defined in the `templates` list of expander card config. 292 | 293 | | List item | Type | Config | 294 | | --------- | ---- | ------ | 295 | | `template` | string | The config item being templated. Only supported config items will be read by expander card. See list in main [Advanced javascript templates](#advanced-javascript-templates) section. | 296 | | `value_template` | string \| value \| object | Either javascript that returns a value or a straight value. Javascript must be enclosed by `[[[]]]` with only whitespace preceding of following. The type of the value \| object returned/set must be applicable to the config item being templated. | 297 | 298 | Template for `expanded` config item tracking state of `input_boolean.weather_warnings`. 299 | 300 | ```yaml 301 | templates: 302 | - template: expanded 303 | value_template: | 304 | [[[ 305 | return is_state('input_boolean.weather_warnings', 'on'); 306 | ]]] 307 | ``` 308 | 309 | Same template using variable `weather_warnings` and adding a `style` template. Note the use of the nullish coalescing (??) operator to handle variables being undefined until their value is set. 310 | 311 | ```yaml 312 | variables: 313 | - variable: weather_warnings 314 | value_template: | 315 | [[[ 316 | return is_state('input_boolean.weather_warnings', 'on'); 317 | ]]] 318 | templates: 319 | - template: expanded 320 | value_template: | 321 | [[[ 322 | return variables.weather_warnings ?? false; 323 | ]]] 324 | - template: style 325 | value_template: | 326 | [[[ 327 | return ` 328 | .title 329 | { 330 | transition: color 0.35s ease, font-weight 0.35s ease; 331 | color: ${variables.weather_warnings ? 'red' : 'var(--primary-text-color)'}; 332 | font-weight: ${variables.weather_warnings ? '700' : 'var(--ha-font-weight-body)'}; 333 | }`; 334 | ]]] 335 | ``` 336 | 337 | More user examples can be found in [Show and tell](https://github.com/MelleD/lovelace-expander-card/discussions/categories/show-and-tell) discussion topic. If you have an example please submit to this discussion topic. 338 | 339 | ## Set state via action 340 | 341 | You can set the state of expander card(s) using the `fire-dom-event` action on any card that supports actions. 342 | 343 | 1. Set expander card(s) to have `expander-card-id`. Multiple expander cards can shared the same id if you wish to set their state together. 344 | 2. Set action on another card using the `fire-dom-event` action. 345 | 346 | ```yaml 347 | tap_action: 348 | action: fire-dom-event 349 | expander-card: 350 | data: 351 | expander-card-id: 352 | action: < open | close | toggle > 353 | ``` 354 | 355 | Example 356 | 357 | ### Expander card config 358 | 359 | ```yaml 360 | - type: custom:expander-card 361 | expander-card-id: my-expander-card 362 | ``` 363 | 364 | ### Action on another card 365 | 366 | ```yaml 367 | show_name: true 368 | show_icon: true 369 | type: button 370 | name: Expand my-expander-card 371 | icon: mdi:chevron-down 372 | tap_action: 373 | action: fire-dom-event 374 | expander-card: 375 | data: 376 | expander-card-id: my-expander-card 377 | action: open 378 | ``` 379 | 380 | ## Style 381 | 382 | You can do advanced styling using the `style` configuration parameter. Classes available are per the images below. 383 | 384 | ![Expander Card Styling - Title](examples/styling2.png) 385 | 386 | ![Expander Card Styling - Title Card](examples/styling1.png) 387 | 388 | ![Expander Card Styling - Title Card & Overlay](examples/styling3.png) 389 | 390 | ### State 391 | 392 | For all elements shown, the class `open` will be added when the Expander card is open, and `closed` added when the Expander is closed. 393 | 394 | ### Animation 395 | 396 | When Expander card animation is enabled, for all elements except those listed below, the class `opening` will be added when the expander is in the process of opening and the class `closing` will be added when the expander is in the process of closing. When not `opening` or `closing`, the class `idle` will be added. The class `animation` will also be added. You may wish to use these classes for transition affects. Expander card uses `0.35s ease` for transitions. See the final example below for transitioning title font size and color. 397 | 398 | > NOTE: `.outer-container` for Title card will not have `animation` or `opening`/`closing` applied. 399 | 400 | ### Special considerations 401 | 402 | 1. `.children-wrapper` is used for opening/closing animation and hiding children cards. You should not style this element. It is shown for completeness. 403 | 2. `margin-bottom` on each children card's `.outer-container` is used to transition cards sliding down and up while animating. Do not style `margin-bottom` and if altering any transitions, extend the included `transition` style for `opening` and `closing`. 404 | 3. As much as possible, use class selector combinations to get your styles to a higher specificity. e.g. `.expander-card.animation.open` is more specific than any built in classes so if you use that selector, you as less likely to need to use `!important`. 405 | 4. For animation, during opening, the classes will be `open` and `opening`. During closing, classes will be `open` and `closing` until the close sequence has ended after which the classes will be `close` and `idle`. 406 | 5. If you are considering any transition affects, check those already applied and extend those with any styling you add. 407 | 408 | ### Styling Examples 409 | 410 | Using an example to set the background based on the status. As background color is a transition element, you need to style both `open` & `opening` and `close` and `closing` to get the background to transition during opening/closing. Otherwise the transition will take place after the expander has opened/closed. 411 | 412 | ```yaml 413 | style: |- 414 | .expander-card.animation.open, 415 | .expander-card.animation.opening { 416 | background-color: red; 417 | } 418 | .expander-card.animation.close, 419 | .expander-card.animation.closing { 420 | background-color: #C8A2C8; 421 | } 422 | ``` 423 | 424 | Only the background of the button. Here `!important` is needed if you wish to override the hover ripple. 425 | 426 | ```yaml 427 | style: | 428 | .header.animation.open, 429 | .header.animation.opening { 430 | background-color: red !important; 431 | } 432 | .header.animation.close, 433 | .header.animation.closing { 434 | background-color: #C8A2C8 !important; 435 | } 436 | ``` 437 | 438 | Switching the arrow from right to left, with reduced horizontal padding of the button. 439 | 440 | ```yaml 441 | style: | 442 | .header { 443 | flex-direction: row-reverse !important; 444 | padding: 0.8em 0 !important; 445 | } 446 | ``` 447 | 448 | If you have a title-card - without overlay 449 | 450 | ```yaml 451 | style: | 452 | .title-card-header { 453 | flex-direction: row-reverse !important; 454 | padding: 0.8em 0 !important; 455 | } 456 | ``` 457 | 458 | If you have a title-card - with overlay 459 | 460 | ```yaml 461 | style: | 462 | .title-card-header-overlay > .header-overlay { 463 | flex-direction: row-reverse !important; 464 | padding: 0.8em 0 !important; 465 | } 466 | ``` 467 | 468 | Transitioning the title font size and color. Here the `!important` on `close`/`closing` is to make sure that the font size and color both change on `closing` as `open` will still be added until fully closed at which stage `close` will be added and `open` removed. 469 | 470 | ```yaml 471 | style: | 472 | .header > .title { 473 | transition: color 0.35s ease, font-size 0.35s ease; 474 | } 475 | .header.animation.close > .title, 476 | .header.animation.closing > .title 477 | { 478 | color: green !important; 479 | font-size: var(--ha-font-size-l) !important; 480 | } 481 | .header.animation.open > .title, 482 | .header.animation.opening > .title 483 | { 484 | color: red; 485 | font-size: var(--ha-font-size-m); 486 | } 487 | ``` 488 | 489 | Change the title size 490 | 491 | ```yaml 492 | style: | 493 | .title { 494 | font-size: var(--ha-font-size-l); 495 | } 496 | ``` 497 | 498 | Change the height of the title-card overlay - match arrow height. 499 | 500 | ```yaml 501 | style: | 502 | .header-overlay { 503 | height: unset !important; 504 | } 505 | ``` 506 | 507 | Change the height of title-card overlay - relative to title-card. The CSS var `--expander-card-overlay-height` is set automatically based on title-card height and overlay margin. 508 | 509 | ```yaml 510 | style: | 511 | .header-overlay { 512 | height: calc(var(--expander-card-overlay-height) * 0.66 ) !important; 513 | } 514 | ``` 515 | 516 | ## Hover/press ripple 517 | 518 | Expander-card uses the inbuilt Home Assistant ripple element `ha-ripple` for hover/press ripple animation on for expander-card button. If you wish to style/hide the ripple you can use the following CSS variables with advanced styling. If these are set in your theme they will be applied and you don't need to do anything at all. 519 | 520 | **NOTE**: If you only wish to style the expander-card ripple itself you will need to apply to the appropriate class listed below. Otherwise if you apply to `.expander-card` it will change the ripple for all cards within the expander. 521 | 522 | | Config | Class | 523 | |--------|-------| 524 | | No title card | `.header` | 525 | | Title card without overlay | `.title-card-header` | 526 | | Title card with overlay | `.header` | 527 | 528 | Change the hover/press ripple color. No title card. 529 | 530 | ```yaml 531 | style: | 532 | .header { 533 | -ha-ripple-color: red; 534 | } 535 | ``` 536 | 537 | | Ripple CSS Variable | Usage | Accepts | Default | 538 | |---------------------|---------|---------|---------| 539 | | `--ha-ripple-color` | Hover/press ripple color. Set to `none` if you wish to disable all ripples. | CSS color | `var(--secondary-text-color)` | 540 | | `--ha-ripple-hover-color` | Hover ripple color. Set if you wish it to be different from pressed color. | CSS color | `var(--ha-ripple-color, var(--secondary-text-color))` | 541 | | `--ha-ripple-pressed-color` | Pressed ripple color. Set if you wish to be different from hover color. | CSS color | `var(--ha-ripple-color, var(--secondary-text-color))` | 542 | | `--ha-ripple-hover-opacity` | Opacity of the hover ripple. | CSS opacity | 0.08 | 543 | | `--ha-ripple-pressed-opacity` | Opacity of the pressed ripple. | CSS opacity | 0.12 | 544 | 545 | ## Card Mod 546 | 547 | Before the `style` attribute, [card mod](https://github.com/thomasloven/lovelace-card-mod) was used to style the card. Although card-mod still works, it is better to switch everything to use the `style` attribute. Please do not open issue(s) when card mod is not working. 548 | 549 | ## Installation 550 | 551 | ### HACS 552 | 553 | Expander-Card is available in [HACS][hacs] (Home Assistant Community Store) by default. 554 | 555 | 1. Install HACS if you don't have it already 556 | 2. Open HACS in Home Assistant 557 | 3. Searching for expander card 558 | 559 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=MelleD&repository=lovelace-expander-card&category=plugin) 560 | 561 | ### Manual 562 | 563 | 1. Download `expander-card.js` file from the [latest release][release-url]. 564 | 2. Put `expander-card.js` file into your `config/www` folder. 565 | 3. Add reference to `expander-card.js` in Dashboard. There's two way to do that: 566 | - **Using UI:** _Settings_ → _Dashboards_ → _More Options icon_ → _Resources_ → _Add Resource_ → Set _Url_ as `/local/expander-card.js` → Set _Resource type_ as `JavaScript Module`. 567 | **Note:** If you do not see the Resources menu, you will need to enable _Advanced Mode_ in your _User Profile_ 568 | - **Using YAML:** Add following code to `lovelace` section. 569 | 570 | ```yaml 571 | resources: 572 | - url: /local/expander-card.js 573 | type: module 574 | ``` 575 | 576 | ## FAQ 577 | 578 | ### Issue after upgrade to HA 2025.6 579 | 580 | There is/was an issue after upgrading to HA 2025.6 (maybe with newer version is not valid anymore) 581 | See [forum](https://community.home-assistant.io/t/expander-accordion-collapsible-card/738817/56?u=melled) and [issue](https://github.com/MelleD/lovelace-expander-card/issues/506). 582 | For the view type [sections](https://www.home-assistant.io/blog/2024/03/04/dashboard-chapter-1/) `cards` is not working anymore. You have to rename it to `sections`. 583 | 584 | Before 585 | 586 | ```yaml 587 | views: 588 | - title: MyView 589 | path: my-view 590 | cards: ... 591 | ``` 592 | 593 | Now 594 | 595 | ```yaml 596 | views: 597 | - title: MyView 598 | path: my-view 599 | sections: ... 600 | ``` 601 | 602 | ### Option Gap is not working 603 | 604 | If this option doesn't work, check your browser's console output. Your current CSS layout might not support this option. 605 | You can use the `expander-card-display: grid` option to set a layout that supports this option. 606 | 607 | ## Support 608 | 609 | Clone and create a PR to help make the card even better. 610 | 611 | Please ⭐️ or sponsor this repo when you like it. 612 | 613 | ## Sponsor ❤️ 614 | 615 | PayPal.Me MelleDennis 616 | 617 | Buy Me A Coffee 618 | 619 | 620 | 621 | [hacs-badge]: https://img.shields.io/badge/hacs-default-orange.svg?style=flat-square 622 | [release-badge]: https://img.shields.io/github/v/release/MelleD/lovelace-expander-card?style=flat-square 623 | [downloads-badge]: https://img.shields.io/github/downloads/MelleD/lovelace-expander-card/total?style=flat-square 624 | [build-badge]: https://img.shields.io/github/actions/workflow/status/MelleD/lovelace-expander-card/build.yml?branch=main&style=flat-square 625 | [paypal-me-badge]: https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal 626 | [buy-me-a-coffee-shield]: https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=6f4e37&logo=buy%20me%20a%20coffee&logoColor=white 627 | 628 | 629 | 630 | [hacs-url]: https://github.com/hacs/integration 631 | [home-assistant]: https://www.home-assistant.io/ 632 | [hacs]: https://hacs.xyz 633 | [release-url]: https://github.com/MelleD/lovelace-expander-card/releases 634 | [paypal-me-url]: https://www.paypal.me/MelleDennis 635 | [buy-me-a-coffee-url]: https://www.buymeacoffee.com/melled 636 | --------------------------------------------------------------------------------