[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [MelleD]\nbuy_me_a_coffee: melled\ncustom: [\"https://www.paypal.me/MelleDennis\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n---\n\nREAD THIS FIRST:\nThanks for raising a expander card issue. Please take the time to review the following\ncategories as some of them do not apply here.\n\n🙅 \"Please DO NOT Raise an Issue\" Cases\n- Question\nSTOP!! Please ask questions about how to use something, or to understand why something isn't\nworking as you expect it to Github discussion or on forum https://community.home-assistant.io/t/expander-accordion-collapsible-card/738817/4.\n- Managed Dependency Upgrade\nYou 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.\n- With an Immediate Pull Request\nAn 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.\n\n🐞 Bug report (please don't include this emoji/text, just add your details)\nPlease provide details of the problem, including the version of expander card and your Brwoser that you\nare using. If possible, please provide a test case or sample application that reproduces\nthe problem. This makes it much easier for us to diagnose the problem and to verify that\nwe have fixed it\nFor quick troubleshooting, prepare a [minimally reproducible example](https://en.wikipedia.org/wiki/Minimal_reproducible_example).\n\n!!!Please check your Browser console for Javascript errors!!!\n\n\n\nTIP: You can always edit your issue if it isn't formatted correctly.\n     See https://guides.github.com/features/mastering-markdown \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature request\nassignees: ''\n---\n\n**Is your feature request related to a problem? Please describe.**\nPlease start by describing the problem that you are trying to solve. There may already\nbe a solution, or there may be a way to solve it that you hadn't considered.\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n\n\nTIP: You can always edit your issue if it isn't formatted correctly.\n     See https://guides.github.com/features/mastering-markdown \n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "---\napplyTo: '**/*.svelte,**/*.ts,**/*.js'\n---\n\n# Svelte & Lovelace Expander Card Instructions for Copilot Reviewer\n\n## Project Context\nThis is a Home Assistant Lovelace Custom Card (lovelace-expander-card). It adds expandable UI elements to HA dashboards. Use Lit-like HA conventions combined with Svelte 5 Runes for reactive components.\n\n## Code Style & Best Practices\n- Use Svelte 5 Runes ($state, $derived, $effect) for all reactive state—no legacy Svelte 4 stores.\n- Keep components small and composable; prefer <200 lines per .svelte file when practical.\n- Use TypeScript everywhere: Define interfaces for props like `cardConfig: LovelaceCardConfig`.\n- Format with Prettier + prettier-plugin-svelte: No indentation inside <script> and <style> tags.\n- Avoid manual DOM manipulation in Svelte components; prefer Svelte transitions and actions. When DOM access is required for Home Assistant web component interop (e.g., `hui-card`), use it sparingly, document/justify it in comments, and keep the logic minimal.\n\n## Home Assistant Integration\n- Type props using real HA types such as `HomeAssistant` and `LovelaceCardConfig`.\n- Use `ha-card`, `ha-icon`, `ha-switch` as Home Assistant web components provided at runtime (no `home-assistant-frontend` import in this project).\n- Update entity state reactively using the existing Home Assistant typing/helpers and $derived.\n- Support YAML config: Validate `config.expanded` as boolean.\n- No global state; keep everything local per card.\n\n## Performance & Accessibility\n- Memoize expensive computations with $derived.\n- Avoid unnecessary re-renders: Keep runes in `<script>` (`$state`, `$derived`, `$effect`) and reference the resulting values directly in markup.\n- Add ARIA attributes: `aria-expanded`, `role=\"button/region\"`.\n- Lazy-load icons and styles only when needed.\n\n## Security & HA Standards\n- Validate all config user inputs (sanitize HTML if needed).\n- Avoid `eval()` and avoid `innerHTML`/Svelte `{@html}` wherever possible; if rendering HTML is unavoidable, add DOMPurify as a dependency and sanitize all HTML before use.\n- Handle `hass.user.is_admin` for advanced features.\n\n## Review Style\n- Be concrete and actionable: Suggest code fixes with examples.\n- Prioritize: 1. Bugs, 2. Performance, 3. Style.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  # Enable version updates for npm\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n\n  # Enable version updates for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "# Add/remove labels to PR\nbreaking-change:\n    - '(breaking-change|major-update)'\nbugfix:\n    - '(fix)'\ndocumentation:\n    - '(doc|docs)'\nmiscellaneous:\n    - '(misc)'\nfeature:\n    - '(feat)'"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  categories:\n    - title: ⚠️ Breaking Changes\n      labels:\n        - major-update\n        - breaking-change\n    - title: 🐞 Bug Fixes\n      labels:\n        - bugfix\n    - title: 📦 Dependency Upgrades\n      labels:\n        - dependencies\n    - title: 📔 Documentation\n      labels:\n        - documentation\n    - title: ⚙️ Miscellaneous\n      labels:\n        - miscellaneous\n    - title: ⭐ New Features\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/workflows/autoClose.yml",
    "content": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n  workflow_dispatch:\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #10.2.0\n        with:\n          days-before-issue-stale: 30\n          days-before-issue-close: 14\n          stale-issue-label: \"stale\"\n          stale-issue-message: \"This issue is stale because it has been open for 30 days with no activity.\"\n          close-issue-message: \"This issue was closed because it has been inactive for 14 days since being marked as stale.\"\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          any-of-labels: \"wait-for-user-input,wait-for-mre\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: 'Build'\n\non:\n  pull_request:\n    branches: \n      - main\n  push: \n    branches:\n      - main\n\njobs:\n  build:\n    permissions:\n      contents: read\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #6.0.2\n      - name: Setup Git\n        run: |\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n\n      - uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788\n        name: Install pnpm\n        with:\n            version: 9\n            run_install: false\n      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #6.4.0\n        name: Install Node.js\n        with:\n            node-version: 24\n            cache: 'pnpm'\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n      - name: Build\n        run: pnpm run build\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #7.0.1\n        with:\n          name: expander-card\n          path: |\n              dist\n"
  },
  {
    "path": ".github/workflows/label.yml",
    "content": "name: 'Label PR'\n\non:\n  pull_request_target:\n    types: [opened, edited, synchronize]\n    branches:\n      - main\n\njobs:\n  label: \n    permissions:\n      contents:  read\n      pull-requests: write\n      issues: write\n    name: Label Pull Request\n    runs-on: ubuntu-latest\n    steps:\n      - name:  Checkout configuration\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #6.0.2\n        # Hier wird bewusst main ausgecheckt für die Config-Datei\n        \n      - name: Label issues\n        uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 #3.4.0\n        with:\n          configuration-path: .github/labeler.yml\n          include-title: 1\n          include-body: 0\n          enable-versioned-regex: 0\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release-workflow.yml",
    "content": "name: \"Create Tagged Release\"\n\non:\n  workflow_dispatch:\n    inputs:\n      release_version:\n        description: 'Version number of the release'\n        required: true\n      prerelease:\n        description: 'Mark as pre-release (RC versions)'\n        required: false\n        type: boolean\n        default: false\n\npermissions:\n  contents: write\n  pages: write\n  id-token: write\n        \njobs:\n  gh_tagged_release:\n    name: Create tagged release\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #6.0.2\n\n      - name: Setup Git\n        run: |\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n\n      - uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788\n        name: Install pnpm\n        with:\n          version: 9\n          run_install: false\n      - name: Install Node.js\n        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e\n        with:\n            node-version: 24\n            cache: 'pnpm'\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Set app version (Unix)\n        run: npm version ${{ github.event.inputs.release_version }} --no-git-tag-version\n\n      - name: Build Project\n        run: pnpm run build\n\n      - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #6.2.0\n        with:\n          python-version: 3.x\n      - run: pip install zensical\n        working-directory: docs\n      - run: zensical build --clean\n        working-directory: docs\n      - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 #5.0.0\n        with:\n          path: docs/site\n      - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 #5.0.0\n\n      - name: \"Create Github release (full)\"\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda #v2\n        id: expander_card_release\n        with:\n          body: \"Release version ${{ github.event.inputs.release_version }}.\"\n          tag_name: ${{ github.event.inputs.release_version }}\n          target_commitish: \"main\"\n          draft: false\n          prerelease: ${{ github.event.inputs.prerelease }}\n          files: dist/*\n          generate_release_notes: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN  }}\n\n      - name: \"Notify issues of release their fix is contained in\"\n        if: ${{ !github.event.inputs.prerelease }}\n        uses: apexskier/github-release-commenter@e7813a9625eabd79a875b4bc4046cfcae377ab34 # v1.4.1\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          comment-template: |\n            Release {release_link} addresses this.\n"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: Validate\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  validate-hacs:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #6.0.2\n      - name: HACS validation\n        uses: hacs/action@d556e736723344f83838d08488c983a15381059a\n        with:\n          category: \"plugin\"\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n/dist\n.venv\n.npm\ndocs/site\n\n# Visual test artifacts\ntests/visual/snapshots/*.actual.png\ntests/ha-config/www/expander-card.js\n.ha_env\n__pycache__/\n*.pyc\n.pytest_cache/\nsrc/*.egg-info/\n"
  },
  {
    "path": ".npmrc",
    "content": "engine-strict=true"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"davidanson.vscode-markdownlint\"\n    ]\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    // -------------------------------------------------------------------------\n    // Python — virtual environment setup\n    // -------------------------------------------------------------------------\n\n    // Creates .venv, installs all test dependencies, and installs the\n    // Playwright Chromium browser.  Run this once after cloning the repo.\n    {\n      \"label\": \"Python: Set up virtual environment\",\n      \"type\": \"shell\",\n      \"command\": \"python3 -m venv .venv && .venv/bin/pip install -e '.[test]' && .venv/bin/playwright install chromium\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"dedicated\",\n        \"reveal\": \"always\",\n        \"group\": \"setup\"\n      }\n    },\n\n    // -------------------------------------------------------------------------\n    // Home Assistant — persistent dev server\n    // -------------------------------------------------------------------------\n\n    // Starts a long-lived HA container once and leaves it running so pytest\n    // can be re-run without waiting for HA to boot each time.\n    // Writes HA_URL + HA_TOKEN to .ha_env; the pytest tasks below source it\n    // automatically.  Press Ctrl-C in this panel to stop HA.\n    {\n      \"label\": \"HA: Start persistent server\",\n      \"type\": \"shell\",\n      \"command\": \"HA_CONFIG_PATH=tests/ha-config HA_PLUGINS_YAML=tests/plugins.yaml .venv/bin/python -m ha_testcontainer.ha_server\",\n      \"isBackground\": true,\n      \"problemMatcher\": {\n        \"owner\": \"ha-server\",\n        \"pattern\": {\n          \"regexp\": \"^$\"\n        },\n        \"background\": {\n          \"activeOnStart\": true,\n          \"beginsPattern\": \"^Starting Home Assistant\",\n          \"endsPattern\": \"Home Assistant is ready at\"\n        }\n      },\n      \"presentation\": {\n        \"panel\": \"dedicated\",\n        \"reveal\": \"always\",\n        \"group\": \"ha\"\n      }\n    },\n\n    // -------------------------------------------------------------------------\n    // pytest — visual / integration tests\n    //\n    // NOTE: These tasks use POSIX shell features (`. .ha_env`, env-var prefixes,\n    // `2>/dev/null`).  They require Bash (or another POSIX-compatible shell) as\n    // the VS Code terminal shell.  On Windows, set your terminal profile to Git\n    // Bash or WSL before running these tasks; PowerShell/cmd will not work.\n    //\n    // Each task uses the .venv binaries directly and loads .ha_env (if present)\n    // before running so that a pre-running HA instance started by\n    // \"HA: Start persistent server\" is used automatically — no Docker boot\n    // wait.  If .ha_env doesn't exist the dot-source silently fails and pytest\n    // spins up a fresh container.\n    // -------------------------------------------------------------------------\n\n    {\n      \"label\": \"pytest: All tests\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; .venv/bin/pytest tests/\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: All tests (Update)\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; SNAPSHOT_UPDATE=1 DOC_IMAGE_UPDATE=1 .venv/bin/pytest tests/\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: All scenarios\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; .venv/bin/pytest tests/visual/test_scenarios.py\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: All scenarios (Update)\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; SNAPSHOT_UPDATE=1 .venv/bin/pytest tests/visual/test_scenarios.py\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    // Prompts for a scenario id (or -k expression) and runs just that test.\n    // Start \"HA: Start persistent server\" first for instant re-runs — no\n    // Docker boot wait.  Re-running is safe: the dashboard is created once\n    // and then overwritten per-scenario; there are no stale-state issues.\n    {\n      \"label\": \"pytest: Run single scenario\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; .venv/bin/pytest tests/visual/test_scenarios.py -k '${input:scenarioId}'\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: Run single scenario — update (overwrite)\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; SNAPSHOT_UPDATE=1 .venv/bin/pytest tests/visual/test_scenarios.py -k '${input:scenarioId}'\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: Run single doc image — generate / verify\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; .venv/bin/pytest tests/visual/test_doc_images.py -k '${input:scenarioId}'\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: Run single doc image — update (overwrite)\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; DOC_IMAGE_UPDATE=1 .venv/bin/pytest tests/visual/test_doc_images.py -k '${input:scenarioId}'\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n\n    // -------------------------------------------------------------------------\n    // pytest — documentation images\n    // -------------------------------------------------------------------------\n\n    {\n      \"label\": \"pytest: Doc images — generate / verify\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; .venv/bin/pytest tests/visual/test_doc_images.py\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n    {\n      \"label\": \"pytest: Doc images — update all (overwrite)\",\n      \"type\": \"shell\",\n      \"command\": \". .ha_env 2>/dev/null; DOC_IMAGE_UPDATE=1 .venv/bin/pytest tests/visual/test_doc_images.py\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    },\n\n    // Doc audit needs no HA instance — runs instantly.\n    {\n      \"label\": \"pytest: Doc image audit\",\n      \"type\": \"shell\",\n      \"command\": \".venv/bin/pytest tests/test_doc_audit.py\",\n      \"group\": \"test\",\n      \"problemMatcher\": [],\n      \"presentation\": {\n        \"panel\": \"shared\",\n        \"group\": \"pytest\"\n      }\n    }\n  ],\n  \"inputs\": [\n    {\n      \"id\": \"scenarioId\",\n      \"type\": \"promptString\",\n      \"description\": \"Scenario id or pytest -k expression (e.g. expander_collapsed)\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Makefile",
    "content": "# Expander Card development helpers\n#\n# All targets assume you are running from the repository root with an activated\n# Python virtual environment (source .venv/bin/activate) and the JS already\n# built (pnpm run build).\n#\n# Quick reference\n# ---------------\n#   make ha_up               Start a persistent HA container for fast iterative work\n#   make doc_images_gen      Generate any missing documentation images (first-run bootstrap)\n#   make doc_images_update   Regenerate ALL documentation images (use after HA/card visual changes)\n#   make doc_audit           Check that all doc images are scenario-generated or explicitly excluded\n\n.PHONY: ha_up ha-tests-up doc_images_gen doc_images_update doc_audit\n\n# Start a persistent Home Assistant container and leave it running.\n# The script prints HA_URL and HA_TOKEN and writes them to .ha_env.\n# In a second terminal:  source .ha_env && pytest tests/visual/ -k <id>\n# Press Ctrl-C here to stop HA.\nha_up ha-tests-up:\n\tHA_CONFIG_PATH=tests/ha-config HA_PLUGINS_YAML=tests/plugins.yaml python -m ha_testcontainer.ha_server\n\n# Run the doc-image test suite.  Missing images are created automatically;\n# existing images are verified against the current rendered output.\ndoc_images_gen:\n\tpytest tests/visual/test_doc_images.py\n\n# Regenerate all documentation images, overwriting any existing files.\n# Use this after an intentional visual change to the card or Home Assistant,\n# then review the diff and commit the updated PNGs/GIFs.\ndoc_images_update:\n\tDOC_IMAGE_UPDATE=1 pytest tests/visual/test_doc_images.py\n\n# Audit doc images: report any PNG/GIF referenced in docs/source/ that is\n# neither generated by a scenario nor listed in tests/doc-image-audit-exclusions.txt.\n# Exits non-zero (failing the test) if untracked images are found.\ndoc_audit:\n\tpytest tests/test_doc_audit.py\n"
  },
  {
    "path": "README.md",
    "content": "# Expander Card for HomeAssistant\n\n[![release][release-badge]][release-url]\n![downloads][downloads-badge]\n![build][build-badge]\n[![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)\n[![PayPal.Me][paypal-me-badge]][paypal-me-url]\n[![BuyMeCoffee][buy-me-a-coffee-shield]][buy-me-a-coffee-url]\n\nExpander/Collapsible card for HomeAssistant  \n\n<img src=\"https://melled.github.io/lovelace-expander-card/chapter/assets/logo.png\" width=\"400\">\n\n## Introduction\n\nFirst 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\n\n\n## 🚀 Quick Start\n\nGo to [Quick Start](https://melled.github.io/lovelace-expander-card/quick-start/) for installation instruction.\n\n- [Full Documentation](https://melled.github.io/lovelace-expander-card)\n\n## Support\n\nClone and create a PR to help make the card even better.\n\nPlease ⭐️ or sponsor this repo when you like it.\n\n## Sponsor ❤️\n\n<a href=\"\" target=\"_blank\"><img src=\"https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal\" alt=\"PayPal.Me MelleDennis\" style=\"height: auto !important;width: auto !important;\" ></a>\n\n<a href=\"https://www.buymeacoffee.com/melled\" target=\"_blank\"><img src=\"https://www.buymeacoffee.com/assets/img/custom_images/white_img.png\" alt=\"Buy Me A Coffee\" style=\"height: auto !important;width: auto !important;\" ></a>\n\n<!-- Badges -->\n\n[hacs-badge]: https://img.shields.io/badge/hacs-default-orange.svg?style=flat-square\n[release-badge]: https://img.shields.io/github/v/release/MelleD/lovelace-expander-card?style=flat-square\n[downloads-badge]: https://img.shields.io/github/downloads/MelleD/lovelace-expander-card/total?style=flat-square\n[build-badge]: https://img.shields.io/github/actions/workflow/status/MelleD/lovelace-expander-card/build.yml?branch=main&style=flat-square\n[paypal-me-badge]: https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal\n[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\n\n<!-- References -->\n\n[hacs-url]: https://github.com/hacs/integration\n[home-assistant]: https://www.home-assistant.io/\n[hacs]: https://hacs.xyz\n[release-url]: https://github.com/MelleD/lovelace-expander-card/releases\n[paypal-me-url]: https://www.paypal.me/MelleDennis\n[buy-me-a-coffee-url]: https://www.buymeacoffee.com/melled\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "site_name: Expander Card documentation\ndocs_dir: source\ntheme:\n  name: material\n  variant: classic\n  features:\n    - navigation.tabs\n    - navigation.sections\n    - navigation.indexes\n    - toc.integrate\n    - navigation.top\n    - navigation.footer\n    - search.suggest\n    - search.highlight\n    - content.tabs.link\n    - content.code.annotation\n    - content.code.copy\n    - content.action.edit\n  palette:\n    - schema: default\n      toggle:\n        icon: material/toggle-switch-off-outline\n        name: Switch to dark mode\n      primary: indigo\n      accent: blue\n    - scheme: slate\n      toggle:\n        icon: material/toggle-switch\n        name: Switch to light mode\n  logo: chapter/assets/logo.png\n  favicon: chapter/assets/logo.png\n  primary: indigo\n  accent: light blue\nextra:\n  generator: false\nplugins:\n  - search\n  - git-revision-date-localized:\n      enable_creation_date: true\nmarkdown_extensions:\n  - admonition\n  - pymdownx.details\n  - pymdownx.superfences\n  - pymdownx.tabbed:\n      alternate_style: true\n  - attr_list\n  - pymdownx.emoji:\n      emoji_index: !!python/name:material.extensions.emoji.twemoji\n      emoji_generator: !!python/name:material.extensions.emoji.to_svg\nnav:\n  - 'index.md'\n  - 'quick-start.md'\n  - 'Configuration':\n    - 'chapter/configuration/index.md'\n    - 'chapter/configuration/configuration-overview.md'\n    - 'chapter/configuration/gui-configuration.md'\n    - 'chapter/configuration/examples.md'\n  - 'Template':\n    - 'chapter/templating/index.md'\n    - 'chapter/templating/template.md'\n    - 'chapter/templating/action.md'\n  - 'Styling':\n    - 'chapter/style/index.md'\n    - 'chapter/style/style.md'\n    - 'chapter/style/styling-examples.md'\n    - 'chapter/style/hover.md'\n    - 'chapter/style/card-mod.md'\n  - 'Contribution':\n    - 'chapter/contribution/contribution.md'\n  - 'FAQ':\n      - 'chapter/faq/faq.md'\nrepo_url: https://github.com/MelleD/lovelace-expander-card\nedit_uri: blob/master/docs/source/"
  },
  {
    "path": "docs/scenarios/.gitkeep",
    "content": "# Documentation-asset-only scenarios.\n# Place YAML files here for scenarios that exist solely to generate doc images\n# or animations (no functional assertions).  These scenarios are picked up by\n# tests/visual/test_doc_images.py but are NOT run by test_scenarios.py.\n# See tests/visual/scenarios/ for scenarios that also include assertions.\n"
  },
  {
    "path": "docs/source/chapter/configuration/configuration-overview.md",
    "content": "# Configuration Overview\n\nAll configurations are available for editing in Graphical config editor. Config items supporting [templating](../templating/index.md) are shown with :octicons-project-template-24:.\n\n|Name|Type|Default|Supported options|Description|\n|---|---|---|---|---|\n|type|string|**Required**|`custom:expander-card`|Type of the card.|\n|title :octicons-project-template-24:|string|Empty|*|Title (Not displayed if using Title-Card)|\n|icon :octicons-project-template-24:|string|mdi:chevron-down|mdi icon shortcut|Icon in button|\n|expanded :octicons-project-template-24:|boolean|_false_|true\\|false|Start expanded|\n|animation|boolean|_true_|true\\|false|Should the opening/closing of expander be animated?|\n|haptic|string|_light_|light\\|medium\\|heavy\\|success\\|warning\\|failure\\|selection\\|none|Haptic feedback type for Home Assistant Companion app on iOS and Android when expanding/collapsing the card. Set to 'none' to disable haptic feedback.|\n|min-width-expanded|number|0|number|Min screen width (px) to be expanded on start (use with start expanded above)|\n|max-width-expanded|number|0|number|Max screen width (px) to be expanded on start (use with start expanded above)|\n|storage-id|string|**optional**|*|Save last expander state in local browser storage|\n|expander-card-id|string|**optional**|*|An id to use with [Set state via action](../templating/action.md)|\n|arrow-color :octicons-project-template-24:|string|primary-text-color,#fff|css-color|Color of icon expand button|\n|icon-rotate-degree|string|_180deg_|css-rotate|Changing the degrees of the button icon when clicked|\n|header-color|string|primary-text-color,#fff|css-color|Color of expand button|\n|button-background|string|_transparent_|css-color|Background color of expand button|\n|expander-card-background|string|ha-card-background, card-background-color,#fff|css-color|Expander Card Background|\n|expander-card-background-expanded|string|Empty|css-color|Expander Card Background when card is opened/expanded|\n|expander-card-display|string|block|css-display|Layout/Display of the card|\n|clear|boolean|_false_|true\\|false|Remove Background, border|\n|gap|string|_0.0em_|css-size|gap between cards when expander is closed. This option depends on your CSS layout: You might need to use `expander-card-display: grid` for this.|\n|padding|string|_1em_|css-size|padding of all card content|\n|expanded-gap|string|_0.6em_|css-size|gap between child cards when expander open|\n|child-padding|string|_0.0em_|css-size|padding of child cards|\n|child-margin-top|string|_0.0em_|css-size|Margin top of child cards|\n|clear-children|boolean|_false_|true\\|false|Remove Background, border from children|\n|title-card|object|**optional**|LovelaceCardConfig|Replace Title with card|\n|title-card-clickable|boolean|_false_|true\\|false|Should the complete div be clickable?|\n|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/style.md)|\n|overlay-margin|string|_0.0em_|css-size|Margin from top right of expander button (if overlay)|\n|title-card-padding|string|_0px_|css-size|padding of title-card|\n|show-button-users|object[]|**optional**|*|Choose the persons/users that button is visible to them.|\n|start-expanded-users|object[]|**optional**|*|Choose the persons/users that card will be start expanded for them.|\n|cards|object[]|**optional**|LovelaceCardConfig[]|Child cards to show when expanded|\n|style :octicons-project-template-24:|string or object|**optional**|css style rules or structured style object|Advanced css styling rules. Supports string format (CSS text) or object format (structured with selectors as keys and property string arrays as values). See [Style](../style/style.md) and [Styling Examples](../style/styling-examples.md).|\n|variables|dictionary|**optional**|List|See Advanced javascript templates|\n|templates|dictionary|**optional**|List|See Advanced javascript templates|\n\n!!! tip \"Editing `style` in Graphical config editor\"\n    When editing `style` in Graphical config editor the default mode is CSS text. To enabled Structured object editing, switch to code editor mode, set style to be a YAML object, and switch back to visual editor mode. Your config will now always view as an Structured object in the visual editor.\n"
  },
  {
    "path": "docs/source/chapter/configuration/examples.md",
    "content": "# Examples\n\n## Title card\n\nThis is the title card example from the video on the main page, a room card. \n\n<img width=\"1032\" height=\"280\" alt=\"image\" src=\"https://github.com/user-attachments/assets/afc16d00-1df8-490c-a57d-bdfb870bd797\" />\n\nIt requires [Mushroom Cards](https://github.com/piitaya/lovelace-mushroom), [Vertical Stack-in Card](https://github.com/ofekashery/vertical-stack-in-card), and (Lovelace card-mod)(https://github.com/thomasloven/lovelace-card-mod)\n\n```yaml\n  - type: custom:expander-card\n    child-margin-top: 0.6em\n    padding: 0\n    clear: true\n    title-card-button-overlay: true\n    title-card-clickable: true\n    expanded: true\n    title-card:\n      type: custom:vertical-stack-in-card\n      cards:\n        # --- TOP ROW ---\n        - type: horizontal-stack\n          cards:\n          \n            - type: custom:mushroom-template-card\n              primary: Living Room\n              icon: mdi:sofa\n              layout: vertical\n              card_mod:\n                style:\n                  mushroom-shape-icon$: |\n                    .shape {\n                      background: none !important;\n                      --shape-color: transparent !important;\n                    }\n                  .: |\n                    ha-card {\n                      background: none !important;\n                      border: none !important;\n                      box-shadow: none !important;\n                      /* This moves the entire card (icon + text) up by 6px */\n                      margin-top: -15px !important; \n                    }\n                    mushroom-state-info {\n                      margin-top: -25px !important; /* Keeps the tight text-to-icon gap */\n                    }\n                    mushroom-state-item {\n                      gap: 0px !important;\n                    }\n                    :host {\n                      --mush-icon-size: 70px;\n                      --primary-text-color: white;\n                      --primary-font-weight: 660;\n                    }\n\n            - type: custom:mushroom-light-card\n              entity: light.fibaro_dimmer_2_living_room_spotlight\n              name: Lights\n              card_mod:\n                style: |\n                  ha-card {\n                    background: none !important;\n                    border: none !important;\n                    box-shadow: none !important;\n                    /* Reduced from 35px to 18px */\n                    margin-top: 8px !important; \n                    /* Ensures the card doesn't cut off the text */\n                    width: 100% !important;\n                  }\n              \n        # --- BOTTOM ROW (Chips) ---\n        - type: custom:mushroom-chips-card\n          alignment: center\n          chips:\n            - type: template\n              icon: mdi:thermometer\n              icon_color: orange\n              content: \"21.71 °C\"\n              card_mod:\n                style: |\n                  ha-card {\n                    box-shadow: none !important;\n                    border: none !important;\n                  }\n            - type: template\n              icon: mdi:water-percent\n              icon_color: blue\n              content: \"44.89 %\"\n              card_mod:\n                style: |\n                  ha-card {\n                    box-shadow: none !important;\n                    border: none !important;\n                  }\n            - type: template\n              icon: mdi:gauge\n              icon_color: red\n              content: \"974.3 hPa\"\n              card_mod:\n                style: |\n                  ha-card {\n                    box-shadow: none !important;\n                    border: none !important;\n                  }\n            - type: template\n              icon: mdi:flash\n              icon_color: yellow\n              content: \"357 W\"\n              card_mod:\n                style: |\n                  ha-card {\n                    box-shadow: none !important;\n                    border: none !important;\n                  }\n```\n\nExample title card that is clickable and has 2 nested cards, which is directly expanded\n\n```yaml\n    - type: custom:expander-card\n      child-margin-top: 0.6em\n      padding: 0\n      clear: true\n      title-card-button-overlay: true\n      title-card-clickable: true\n      expanded: true\n      title-card:\n        type: \"custom:digital-clock\"\n        dateFormat:\n          weekday: \"long\"\n          day: \"2-digit\"\n          month: \"short\"\n        timeFormat:\n          hour: \"2-digit\"\n          minute: \"2-digit\"\n      cards:\n        - type: custom:simple-weather-card\n          entity: weather.openweathermap\n          primary_info:\n            - wind_speed\n            - wind_bearing\n          secondary_info:\n            - precipitation\n            - precipitation_probability\n        - type: custom:hourly-weather\n          entity: weather.openweathermap\n          icons: true\n          show_precipitation_probability: true\n          show_precipitation_amounts: true\n          forecast_type: \"hourly\"\n          num_segments: 10\"\n          label_spacing: \"1\"\n          name: null\n          show_wind: speed\n```\n\n## Heading Title card\n\nExample with [heading](https://www.home-assistant.io/dashboards/heading/) title card to provide the possibility of styling your title.\n\n```yaml\n      - type: custom:expander-card\n        title-card:\n          type: heading\n          heading: Title\n          heading_style: title\n          badges:\n            - type: entity\n              show_name: false\n              show_state: true\n              show_icon: true\n              entity: light.bed_light\n          icon: mdi:account\n```\n\n## Template Title card with Mushroom\n\nIf you need templates in your title, you can make good use of the Mushroom cards. Here's an example using the [Mushroom title card](https://github.com/piitaya/lovelace-mushroom/blob/main/docs/cards/title.md).\n\n```yaml\n      - type: custom:expander-card\n        title-card:\n          type: custom:mushroom-title-card\n          title: |-\n            {{ now().hour }}\n```\n\n## Simple Title\n\nExample with title that is clickable and has 2 nested cards.\n\n```yaml\n      - type: custom:expander-card\n        child-margin-top: 0.6em\n        padding: 0\n        title: \"Test\"\n        title-card-button-overlay: true\n        title-card-clickable: true\n        cards:\n          - type: custom:simple-weather-card\n            entity: weather.openweathermap\n            primary_info:\n              - wind_speed\n              - wind_bearing\n            secondary_info:\n              - precipitation\n              - precipitation_probability\n          - type: custom:hourly-weather\n            entity: weather.openweathermap\n            icons: true\n            show_precipitation_probability: true\n            show_precipitation_amounts: true\n            forecast_type: \"hourly\"\n            num_segments: 10\"\n            label_spacing: \"1\"\n            name: null\n            show_wind: speed\n```\n\n## Title with min-width-expanded\n\nExample with title that is clickable and has 2 nested cards which are automatically expanded when the screen is more than 300px.\n\n```yaml\n      - type: custom:expander-card\n        child-margin-top: 0.6em\n        padding: 0\n        title: \"Test\"\n        title-card-button-overlay: true\n        title-card-clickable: true\n        min-width-expanded: 300\n        cards:\n          - type: custom:simple-weather-card\n            entity: weather.openweathermap\n            primary_info:\n              - wind_speed\n              - wind_bearing\n            secondary_info:\n              - precipitation\n              - precipitation_probability\n            name: in Gärtringen\n          - type: custom:hourly-weather\n            entity: weather.openweathermap\n            icons: true\n            show_precipitation_probability: true\n            show_precipitation_amounts: true\n            forecast_type: \"hourly\"\n            num_segments: 10\"\n            label_spacing: \"1\"\n            show_wind: speed\n```\n\n## Title card with action\n\nThe 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.\n\n```yaml\n    type: custom:expander-card\n    title: Expander Card\n    expander-card-id: my-light-card\n    cards:\n      - type: entities\n        entities:\n          - entity: sun.sun\n    title-card:\n      type: tile\n      entity: light.bed_light\n      vertical: false\n      features_position: inline\n      features:\n        - type: light-brightness\n      tap_action:\n        action: fire-dom-event\n        expander-card:\n          data:\n            expander-card-id: my-light-card\n            action: toggle\n```\n"
  },
  {
    "path": "docs/source/chapter/configuration/gui-configuration.md",
    "content": "# Graphical config editor\n\nExpander card aligns its concepts as much as possible to Home Assistant Frontend. As such, all expander cards are expanded by default when using UI edit/preview mode. This matches the concept of a Frontend conditional card showing when in edit/preview mode.\n\nOn a busy dashboard with many expander cards, and perhaps nested expander cards, showing all as expanded may make it difficult to find the expander card you wish to edit.\n\nIf you do not wish to have expander cards expanded by default when in edit/preview mode, you can set `preview-expanded: false` in `expander-card:` yaml key at the top of your dashboard \"Raw configuration\". It is recommended you place this at the top of your dashboard \"Raw configuration\" before any Home Assistant view/panel yaml configuration.\n\n> NOTE: Any expander card using `expanded` templates will always honour the template when in preview/edit mode, including sending open state events to other expanders, regardless of the `preview-expanded:` setting. This way the preview/edit mode will match the current display of expander cards on the dashboard.\n\nTo access your dashboard \"Raw configuration\" from the UI use the following steps:\n\n- Go to the dashboard you wish to edit.\n- In the top-right corner, select the pencil icon. If you are on a small screen you may need to select the three dots menu ⋮ first and then select **Edit dashboard**.\n- Select the three dots menu ⋮ and select **Raw configuration editor**.\n\nExample yaml code to include\n\n```yaml\nexpander-card:\n  preview-expanded: false\nviews:\n  - title: Home\n    # ... other dashboard \"Raw configuration\"\n```\n"
  },
  {
    "path": "docs/source/chapter/configuration/index.md",
    "content": "# Configuration\n\nThis section provides you with a comprehensive overview of the diverse configuration and customization options available to help you optimally adapt the Expander Card to your needs.\n\n## Configuration Overview\n\nThe Expander Card offers a wide range of configuration options that allow you to customize every aspect of the card. From basic settings like the title and initial state to advanced options such as custom animations, styling adjustments, and conditional displays.\n\nConfiguration is primarily done through YAML and can be applied individually per card. This allows you to use different Expander Cards with different settings on the same dashboard.\n\nKey configuration areas include:\n\n- **Basic Properties**: Title, icons, expansion states\n- **Behavior**: Default state (expanded/collapsed), animations, gap settings\n- **Styling**: Colors, fonts, spacing, backgrounds\n- **Advanced Options**: Templating, conditional displays, button functions\n- **Card Management**: Managing child cards within the expander\n\nFor a detailed overview of all available configuration options and their usage, please visit the [Configuration Overview](configuration-overview.md).\n\n## Graphical Config Editor\n\nTo simplify configuration, the Expander Card provides a graphical configuration editor directly within the Home Assistant user interface. This editor allows you to make settings visually without having to manually edit YAML.\n\nWith the graphical editor, you can:\n\n- **Configure Visually**: Change settings through user-friendly forms and dropdown menus\n- **Live Preview**: See your changes in real-time as you configure the card\n- **Validation**: Get immediate feedback on invalid settings\n- **Easy Navigation**: Switch between different configuration sections using tabs\n- **Code View**: Switch to YAML view when needed for advanced customizations\n\nThe graphical editor is particularly useful for beginners or when you want to make quick adjustments without needing to know the YAML syntax in detail.\n\nFor detailed information on using the graphical editor, see the [Graphical Configuration Editor Guide](gui-configuration.md).\n\n## Examples\n\nLearn best through practical examples! In our examples section, you'll find a collection of pre-configured Expander Cards demonstrating various use cases and styling possibilities.\nEach example includes the complete YAML code. You can copy the examples directly and adapt them to your needs.\nVisit the [YAML Examples Page](examples.md) to see what's possible and get inspired for your own dashboard designs!\n"
  },
  {
    "path": "docs/source/chapter/contribution/contribution.md",
    "content": "# 🤝 Contribution\n\nContributions of any kind are warmly welcomed! Whether you're reporting bugs, requesting features, improving documentation, or submitting code, your input helps make the Expander Card better for everyone.\n\n## Feature Requests\n\nHave an idea for a new feature or enhancement? We'd love to hear about it!\n\nTo submit a feature request:\n\n1. Check if a similar request already exists in [existing issues](https://github.com/MelleD/lovelace-expander-card/issues)\n2. If not, create a new feature request using our template\n3. Describe the feature, its use case, and why it would be valuable\n\n[Submit a Feature Request](https://github.com/MelleD/lovelace-expander-card/issues/new?template=feature_request.md)\n\n**Template includes:**\n\n- Feature description\n- Use case scenarios\n- Proposed implementation (optional)\n- Alternative solutions considered\n\n## Bug Reports\n\nFound something that's not working as expected? Please let us know!\n\nWhen reporting a bug:\n\n1. Search [existing issues](https://github.com/MelleD/lovelace-expander-card/issues) to avoid duplicates\n2. Use our bug report template for consistency\n3. Include reproduction steps, expected vs actual behavior\n4. Add relevant configuration YAML and screenshots if possible\n5. Mention your Home Assistant version and browser\n\n[Report a Bug](https://github.com/MelleD/lovelace-expander-card/issues/new?template=bug_report.md)\n\n**What to include:**\n\n- Clear description of the issue\n- Steps to reproduce\n- Expected behavior vs actual behavior\n- YAML configuration (sanitized)\n- Screenshots or videos (if applicable)\n- Environment details (HA version, browser, etc.)\n\n## Discussions\n\nWant to share ideas, ask questions, or connect with the community? Join our discussions!\n\nDiscussion categories:\n\n- **General**: Questions and general discussions\n- **Ideas**: Brainstorm new features and improvements\n- **Show and Tell**: Share your creative expander card configurations\n- **Q&A**: Get help from the community\n\n[Join the Discussion](https://github.com/MelleD/lovelace-expander-card/discussions)\n\n**Perfect for:**\n\n- Asking \"how do I...\" questions\n- Sharing your custom configurations\n- Discussing potential features before opening an issue\n- Getting feedback on your setup\n- Helping other users\n\n## Pull Requests\n\nThe ultimate contribution! We appreciate code contributions, documentation improvements, and bug fixes.\n\n### How to Submit a Pull Request\n\n1. **Fork the repository**\n\n   ```bash\n   # Fork via GitHub UI, then clone your fork\n   git clone https://github.com/YOUR_USERNAME/lovelace-expander-card.git\n   cd lovelace-expander-card\n   ```\n\n2. **Create a feature branch**\n\n   ```bash\n   git checkout -b feature/AmazingFeature\n   # or\n   git checkout -b fix/BugFix\n   ```\n\n3. **Make your changes**\n   - Write clear, commented code\n   - Follow the existing code style\n   - Test your changes thoroughly\n   - Update documentation if needed\n\n4. **Commit your changes**\n\n   ```bash\n   git add .\n   git commit -m 'Add some AmazingFeature'\n   ```\n\n   Use clear, descriptive commit messages\n\n5. **Push to your fork**\n\n   ```bash\n   git push origin feature/AmazingFeature\n   ```\n\n6. **Open a Pull Request**\n   - Go to the [original repository](https://github.com/MelleD/lovelace-expander-card)\n   - Click \"New Pull Request\"\n   - Select your fork and branch\n   - Provide a clear description of your changes\n   - Link any related issues\n\n### Pull Request Guidelines\n\n- **One feature per PR**: Keep pull requests focused on a single feature or fix\n- **Test thoroughly**: Ensure your changes work in different scenarios\n- **Update docs**: If you add features, update the documentation\n- **Code style**: Match the existing code formatting and style\n- **Commit messages**: Use clear, descriptive messages\n- **Be patient**: Maintainers will review your PR as soon as possible\n\n### Local Development Setup\n\n#### Build & Install\n\n```bash\n# Install dependencies\nnpm install\n# or\npnpm install\n\n# Start development server\nnpm run srv\n\n# Build for production\nnpm run build\n\n# Run es linting\nnpm run lint\n\n# Fix es linting\nnpm run lint-fix\n```\n\n#### Visual Tests\n\nOne-time setup — **VS Code**: open the *Terminal › Run Task* palette and choose *Python: Set up virtual environment*.\n\nOne-time setup — **Command line**:\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -e '.[test]'\nplaywright install chromium\n```\n\n**VS Code** — open the *Terminal › Run Task* palette and choose any `pytest:` task (run all, run single scenario, update snapshots, etc.).\n\n**Command line** — activate the virtual environment first, then:\n\n| Goal | Command |\n|---|---|\n| All tests | `pytest tests/` |\n| All tests — update snapshots & doc images | `SNAPSHOT_UPDATE=1 DOC_IMAGE_UPDATE=1 pytest tests/` |\n| Scenario tests only | `pytest tests/visual/test_scenarios.py` |\n| Update scenario snapshots | `SNAPSHOT_UPDATE=1 pytest tests/visual/test_scenarios.py` |\n| Single scenario | `pytest tests/visual/test_scenarios.py -k expander_collapsed` |\n| Single scenario — update snapshot | `SNAPSHOT_UPDATE=1 pytest tests/visual/test_scenarios.py -k expander_collapsed` |\n| Doc images — generate / verify | `pytest tests/visual/test_doc_images.py` |\n| Doc images — update all | `DOC_IMAGE_UPDATE=1 pytest tests/visual/test_doc_images.py` |\n| Doc image audit (no HA needed) | `pytest tests/test_doc_audit.py` |\n\n> **Tip:** Start the persistent HA server (`python -m ha_testcontainer.ha_server` or the *HA: Start persistent server* VS Code task) before running tests to skip the Docker boot wait on every run.\n\n> **Tip (debugging):** When the persistent server is running, its URL is logged in the terminal. You can open that URL in a browser and log in with the ha-testcontainer default credentials — username `testadmin`, password `testpassword123` — to inspect the HA instance manually. **Do not browse to the instance before running tests**; doing so may interfere with the test session. If you have already browsed to it, stop the server and start it again before running tests.\n\n#### Docs\n\nGo to docs folder and install [zensical](https://zensical.org/docs/get-started/)\n\n```bash\n# cd docs\ncd docs\n\n# Install zensical https://zensical.org/docs/get-started/\n...\n\n# build docs\nzensical build --clean\n\n# start server to see updates in realtime\nzensical serve\n```\n\nOpen browser on [http://localhost:8000](http://localhost:8000)\n\n## Other Ways to Contribute\n\n- **Documentation**: Help improve or translate documentation\n- **Examples**: Share your expander card configurations in discussions\n- **Support**: Help answer questions from other users\n- **Testing**: Test beta releases and provide feedback\n- **Spread the word**: Star the repo and tell others about it\n\n## Code of Conduct\n\nBe respectful, constructive, and welcoming. We're all here to make the Expander Card better together.\n\n## Questions?\n\nNot sure where to start? Feel free to:\n\n- Open a [discussion](https://github.com/MelleD/lovelace-expander-card/discussions)\n- Comment on existing issues\n- Check the [documentation](https://github.com/MelleD/lovelace-expander-card/blob/main/README.md)\n\nThank you for contributing! 🎉\n"
  },
  {
    "path": "docs/source/chapter/faq/faq.md",
    "content": "# FAQ\n\n## Issue after upgrade to HA 2025.6\n\nThere was an issue after upgrading to HA 2025.6 (this may not be valid anymore in newer versions)\nSee [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).\nFor 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`.\n\nBefore\n\n ```yaml\nviews:\n  - title: MyView\n    path: my-view\n    cards: ...\n```\n\nNow\n\n ```yaml\nviews:\n  - title: MyView\n    path: my-view\n    sections: ...\n```\n\n## Option Gap is not working\n\nIf this option doesn't work, check your browser's console output. Your current CSS layout might not support this option.\nYou can use the `expander-card-display: grid` option to set a layout that supports this option.\n"
  },
  {
    "path": "docs/source/chapter/style/card-mod.md",
    "content": "## Card Mod\n\nBefore 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."
  },
  {
    "path": "docs/source/chapter/style/hover.md",
    "content": "# Hover/press ripple\n\nThe 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.\n\n**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.\n\nConfig | Class\n--- | ---\nNo title card | `.header`\nTitle card without overlay | `.title-card-header`\nTitle card with overlay | `.header`\n\nChange the hover/press ripple color. No title card.\n\n```yaml\nstyle: |\n  .header {\n    --ha-ripple-color: red;\n  }\n```\n\nRipple CSS Variable|Usage|Accepts|Default\n-|-|-|-\n`--ha-ripple-color`|Hover/press ripple color. Set to `none` if you wish to disable all ripples.|CSS color|`var(--secondary-text-color)`\n`--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))`\n`--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))`\n`--ha-ripple-hover-opacity`|Opacity of the hover ripple.|CSS opacity|0.08\n`--ha-ripple-pressed-opacity`|Opacity of the pressed ripple.|CSS opacity|0.12\n"
  },
  {
    "path": "docs/source/chapter/style/index.md",
    "content": "# Styling\n\n From basic color changes to advanced CSS animations and transitions, the Expander Card provides comprehensive styling options to match your dashboard design perfectly.\n\nThe Expander Card offers multiple approaches to styling, giving you full control over every visual aspect of the card. Whether you're making simple adjustments or creating complex animated effects, you'll find the tools you need here.\n\n## Style Overview\n\nThe Expander Card provides a powerful `style` configuration parameter that allows you to apply custom CSS to various elements of the card. This includes the card container, header, title, arrow, children wrapper, and more.\n\nKey styling features:\n\n- **Element-Specific Styling**: Target individual components like headers, titles, and arrows\n- **State-Based Styling**: Apply different styles based on expanded/collapsed states\n- **Animation Support**: Create smooth transitions and animated effects during expansion\n- **Title Card Integration**: Style title cards with or without overlays\n- **CSS Variables**: Use and override built-in CSS variables for consistent theming\n- **Flexible Selectors**: Use CSS classes for precise control over styling\n\nThe styling system is designed to work seamlessly with Home Assistant's theming system while providing the flexibility to create unique, custom designs.\n\nFor comprehensive information on available CSS classes, state classes, animation classes, and styling examples, visit the [Advanced Styling Guide](style.md).\n\n## Practical Examples\n\nLearning by example is often the best approach to styling. Our examples section provides a curated collection of ready-to-use styling snippets that demonstrate various techniques and effects.\nYou can copy these examples directly into your configuration and adapt them to your needs.\n\nExplore the [YAML Styling Examples](examples/yaml-styling-examples.md) to see what's possible and get inspired for your own custom designs.\n\n## Hover Effects\n\nThe Expander Card uses Home Assistant's built-in ripple element (`ha-ripple`) for interactive feedback on button presses and hover states.\n\nFor detailed information on CSS variables, configuration classes, and hover customization examples, see the [Hover Styling Guide](hover.md).\n\n## Card-Mod\n\nBefore the `style` attribute was introduced, [card-mod](https://github.com/thomasloven/lovelace-card-mod) was the primary method for styling expander cards. While card-mod still works with the Expander Card, it is strongly recommended to migrate to the native `style` attribute for better performance and maintainability.\n\nIf you're currently using card-mod, migrating to the `style` attribute is usually as simple as moving your CSS from the card-mod configuration to the `style` parameter.\n\nFor more information and migration notes, see the [Card-Mod Information](card-mod.md).\n"
  },
  {
    "path": "docs/source/chapter/style/style.md",
    "content": "# Style\n\nYou can do advanced styling using the `style` configuration parameter. The `style` parameter supports two formats:\n\n1. **String format**: CSS rules as a string\n2. **Object format**: Structured object with CSS selectors as keys and property string arrays as values\n\n!!! tip \"Editing `style` in Graphical config editor\"\n    When editing `style` in Graphical config editor the default mode is CSS text. To enabled Structured object editing, switch to code editor mode, set style to be a YAML object, and switch back to visual editor mode. Your config will now always view as an Structured object in the visual editor.\n\n## Style Format Examples\n\n### String Format\n\n```yaml\n  style: |\n    .expander-card {\n      background-color: red;\n    }\n```\n\n### Object Format\n\n```yaml\nstyle:\n  .expander-card:\n    - background-color: red\n```\n\nBoth formats produce the same result. See [Styling Examples](styling-examples.md) for more details and comprehensive examples.\n\n### Object returned from template\n\nTemplates can return either string arrays or object arrays where the first object only is used:\n\n```yaml\ntemplates:\n  - template: style\n    value_template: |\n      [[[ \n        return { \n          '.expander-card': [\n            'background-color: red !important',\n            'padding: 1em'\n          ],\n          '.header > .title': [\n            { 'color': 'white' },\n            { 'font-size': 'var(--ha-font-size-l)' }\n          ]\n        }\n      ]]]\n```\n\n## CSS Classes\n\nClasses available are per the images below.\n\n![Expander Card Styling - Title](../assets/styling2.png)\n\n![Expander Card Styling - Title Card](../assets/styling1.png)\n\n![Expander Card Styling - Title Card & Overlay](../assets/styling3.png)\n\n## State\n\nFor all elements shown, the class `open` will be added when the Expander card is open, and `closed` added when the Expander is closed.\n\n## Animation\n\nWhen 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.\n\n> NOTE: `.outer-container` for Title card will not have `animation` or `opening`/`closing` applied.\n\n## Special considerations\n\n1. `.children-wrapper` is used for opening/closing animation and hiding children cards. You should not style this element. It is shown for completeness.\n2. `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`.\n3. 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`.\n4. 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`.\n5. If you are considering any transition effects, check those already applied and extend those with any styling you add.\n"
  },
  {
    "path": "docs/source/chapter/style/styling-examples.md",
    "content": "# Styling examples\n\nThis page provides practical examples of styling the Expander Card using the `style` configuration parameter. Each example includes complete YAML code that you can copy and adapt to your needs.\n\nThese examples demonstrate various styling techniques, from simple color changes to complex animations and layout modifications. Understanding these patterns will help you create custom designs that perfectly match your dashboard.\n\n## Style Format\n\nThe `style` configuration parameter supports two formats:\n\n### String Format (CSS Text)\n\nThe traditional way to define styles using a string containing CSS rules:\n\n```yaml\ntype: custom:expander-card\ntitle: String Style Example\nstyle: |\n  .expander-card {\n    background-color: red;\n    padding: 1em;\n  }\ncards:\n  - type: entity\n    entity: light.living_room\n```\n\n### Object Format (Structured)\n\nAlternatively, you can use an object structure where keys are CSS selectors and values are arrays of property strings:\n\n```yaml\ntype: custom:expander-card\ntitle: Object Style Example\nstyle:\n  .expander-card:\n    - background-color: red\n    - padding: 1em\n  .header > .title:\n    - font-size: var(--ha-font-size-l)\n    - color: green\ncards:\n  - type: entity\n    entity: light.living_room\n```\n\nBoth formats produce the same result. The object format can be more readable and easier to maintain for complex styles.\n\n## Background Color Transitions\n\n### Animated Background Based on State\n\nThis example sets the background color based on the expansion state with smooth transitions. As background color is a transition element, you need to style both `open` & `opening` and `close` & `closing` to get the background to transition during opening/closing. Otherwise the transition will take place after the expander has fully opened/closed.\n\n```yaml\ntype: custom:expander-card\ntitle: Animated Background\nstyle: |-\n  .expander-card.animation.open,\n  .expander-card.animation.opening {\n    background-color: red;\n  }\n  .expander-card.animation.close,\n  .expander-card.animation.closing {\n    background-color: #C8A2C8;\n  }\ncards:\n  - type: entity\n    entity: light.living_room\n```\n\n### Button Background Only\n\nStyle only the background of the header button. Here `!important` is needed if you wish to override the hover ripple effect.\n\n```yaml\ntype: custom:expander-card\ntitle: Button Background\nstyle: |\n  .header.animation.open,\n  .header.animation.opening {\n    background-color: red !important;\n  }\n  .header.animation.close,\n  .header.animation.closing {\n    background-color: #C8A2C8 !important;\n  }\ncards:\n  - type: entity\n    entity: light.bedroom\n```\n\nThe same example using the object format:\n\n```yaml\ntype: custom:expander-card\ntitle: Button Background (Object Format)\nstyle:\n  .header.animation.open, .header.animation.opening:\n    background-color: red !important\n  .header.animation.close, .header.animation.closing:\n    background-color: '#C8A2C8 !important'\ncards:\n  - type: entity\n    entity: light.bedroom\n```\n\n## Layout Modifications\n\n### Arrow Position - Left Side\n\nMove the arrow from the right side to the left side, with reduced horizontal padding of the button.\n\n```yaml\ntype: custom:expander-card\ntitle: Left Arrow\nstyle: |\n  .header {\n    flex-direction: row-reverse !important;\n    padding: 0.8em 0 !important;\n  }\ncards:\n  - type: entity\n    entity: sensor.temperature\n```\n\n### Arrow Position with Title Card (No Overlay)\n\nIf you have a title card without overlay, use the `.title-card-header` class.\n\n```yaml\ntype: custom:expander-card\ntitle-card:\n  type: entity\n  entity: sensor.weather\nstyle: |\n  .title-card-header {\n    flex-direction: row-reverse !important;\n    padding: 0.8em 0 !important;\n  }\ncards:\n  - type: entity\n    entity: sensor.humidity\n```\n\n### Arrow Position with Title Card (With Overlay)\n\nIf you have a title card with overlay enabled, target the overlay header.\n\n```yaml\ntype: custom:expander-card\ntitle-card:\n  type: picture\n  image: /local/image.jpg\ntitle-card-overlay: true\nstyle: |\n  .title-card-header-overlay > .header-overlay {\n    flex-direction: row-reverse !important;\n    padding: 0.8em 0 !important;\n  }\ncards:\n  - type: entity\n    entity: media_player.living_room\n```\n\n## Title Styling\n\n### Animated Title Font and Color\n\nTransition the title font size and color during expansion. The `!important` on `close`/`closing` ensures that the font size and color both change during the closing animation, as the `open` class remains until fully closed.\n\n```yaml\ntype: custom:expander-card\ntitle: Dynamic Title\nstyle: |\n  .header > .title {\n    transition: color 0.35s ease, font-size 0.35s ease;\n  }\n  .header.animation.close > .title, \n  .header.animation.closing > .title\n  {\n    color: green !important;\n    font-size: var(--ha-font-size-l) !important;\n  }\n  .header.animation.open > .title,\n  .header.animation.opening > .title\n  {\n    color: red;\n    font-size: var(--ha-font-size-m);\n  }\ncards:\n  - type: entities\n    entities:\n      - light.kitchen\n```\n\n### Static Title Size\n\nSimply change the title font size without animations.\n\n```yaml\ntype: custom:expander-card\ntitle: Large Title\nstyle: |\n  .title {\n    font-size: var(--ha-font-size-l);\n  }\ncards:\n  - type: entity\n    entity: climate.thermostat\n```\n\n## Title Card Overlay Adjustments\n\n### Match Overlay to Arrow Height\n\nChange the height of the title card overlay to match the arrow height.\n\n```yaml\ntype: custom:expander-card\ntitle-card:\n  type: picture\n  image: /local/background.jpg\ntitle-card-overlay: true\nstyle: |\n  .header-overlay {\n    height: unset !important;\n  }\ncards:\n  - type: weather-forecast\n    entity: weather.home\n```\n\n### Relative Overlay Height\n\nChange the height of the title card overlay relative to the title card height. The CSS variable `--expander-card-overlay-height` is automatically set based on title card height and overlay margin.\n\n```yaml\ntype: custom:expander-card\ntitle-card:\n  type: picture\n  image: /local/header.jpg\ntitle-card-overlay: true\nstyle: |\n  .header-overlay {\n    height: calc(var(--expander-card-overlay-height) * 0.66) !important;\n  }\ncards:\n  - type: entities\n    entities:\n      - sensor.temperature\n      - sensor.humidity\n```\n\n## Complex Example Using Object Format\n\nHere's a comprehensive example demonstrating the object format with multiple selectors and properties:\n\n```yaml\ntype: custom:expander-card\ntitle: Complex Styling Example\nstyle:\n  .expander-card.animation.open, .expander-card.animation.opening:\n    - background-color: '#1e3a8a'\n    - transition: background-color 0.35s ease\n  .expander-card.animation.close, .expander-card.animation.closing:\n    - background-color: '#047857'\n    - transition: background-color 0.35s ease\n  .header > .title:\n    - font-size: var(--ha-font-size-l)\n    - color: '#ffffff'\n    - font-weight: bold\n    - transition: color 0.35s ease, font-size 0.35s ease\n  .header.animation.open > .title, .header.animation.opening > .title:\n    - color: '#fbbf24'\n    - font-size: var(--ha-font-size-xl)\n  .header.animation.close > .title, .header.animation.closing > .title:\n    - color: '#ffffff'\n    - font-size: var(--ha-font-size-l)\n  .arrow:\n    - color: '#fbbf24'\ncards:\n  - type: entity\n    entity: light.living_room\n```\n\nThis example demonstrates:\n\n- Background color transitions between states\n- Title color and size changes with smooth transitions\n- Custom arrow color\n- Multiple CSS properties per selector\n- Combining multiple selectors with commas\n\n## Tips and Best Practices\n\n### Style Format Choice\n\nChoose the format that works best for your use case:\n\n- **String format**: Better for copy-pasting existing CSS or when using complex selectors\n- **Object format**: More readable and easier to maintain, especially for configurations with many style rules\n\n### Specificity\n\nUse class selector combinations to increase specificity and avoid needing `!important`:\n\n- `.expander-card.animation.open` is more specific than just `.expander-card`\n- `.header.animation.open > .title` targets specifically the title in an open animated header\n\n### Animation States\n\nWhen styling for animations, remember:\n\n- During opening: `open` and `opening` classes are both present\n- During closing: `open` and `closing` classes are both present until fully closed\n- When idle: `idle` class is present along with either `open` or `close`\n\n### Transitions\n\nFor smooth animations:\n\n- Use matching transition durations (expander card uses `0.35s ease` by default)\n- Apply transitions to the base element, not just the state classes\n- Style both opening and closing states for seamless transitions\n\n### Testing\n\nAlways test your styles in the Home Assistant UI:\n\n- Check both light and dark themes if your instance supports theme switching\n- Test expansion and collapse animations to ensure smooth transitions\n- Verify that hover effects work as expected\n- Check responsive behavior at different screen sizes\n\n### Resources\n\nFor more advanced styling:\n\n- See the [Advanced Styling Guide](../style.md) for CSS class documentation\n- Check [Hover Effects](../hover.md) for ripple customization\n- Review Home Assistant's CSS variables in your theme configuration\n- Explore the [templating section](../../templating/index.md) for dynamic styling based on entity states\n"
  },
  {
    "path": "docs/source/chapter/templating/action.md",
    "content": "# Action Configuration\n\nYou can set the state of expander card(s) using the `fire-dom-event` action on any card that supports actions.\n\n1. Set expander card(s) to have `expander-card-id`. Multiple expander cards can share the same id if you wish to set their state together.\n2. Set action on another card using the `fire-dom-event` action.\n\n ```yaml\n  tap_action:\n    action: fire-dom-event\n    expander-card:\n      data:\n        expander-card-id: <expander-card-id>\n        action: < open | close | toggle >\n ```\n\n## Expander card config\n\n```yaml\n    - type: custom:expander-card\n      expander-card-id: my-expander-card\n```\n\n## Action on another card\n\n```yaml\nshow_name: true\nshow_icon: true\ntype: button\nname: Expand my-expander-card\nicon: mdi:chevron-down\ntap_action:\n  action: fire-dom-event\n  expander-card:\n    data:\n      expander-card-id: my-expander-card\n      action: open\n```\n"
  },
  {
    "path": "docs/source/chapter/templating/index.md",
    "content": "# Templating\n\nWelcome to the templating section of the Expander Card! This powerful feature enables you to create dynamic, state-responsive expander cards that automatically adapt based on entity states, conditions, and user interactions. Templating takes your dashboard from static to intelligent.\n\nTemplating allows you to programmatically control various aspects of the Expander Card using JavaScript expressions. Instead of hardcoding values, you can create rules that evaluate in real-time, making your cards react to changes in your Home Assistant entities and their states.\n\n## JavaScript Templates\n\nThe Expander Card offers comprehensive JavaScript templating support to dynamically customize behavior, styling, and display properties. Templates allow you to create conditional logic, respond to entity state changes, and build sophisticated automation directly into your cards.\n\nKey templating capabilities include:\n\n- **Dynamic Expansion State**: Control whether cards are expanded or collapsed based on entity states or conditions\n- **Conditional Styling**: Change colors, fonts, and visual appearance based on real-time data\n- **Responsive Content**: Update titles, icons, and other properties dynamically\n- **Variable Support**: Define reusable variables for cleaner template code\n- **Cross-Card Communication**: Track and respond to the state of other expander cards\n- **Entity State Tracking**: React automatically when your Home Assistant entities change\n\nTemplates are evaluated reactively, meaning they automatically update when the underlying data changes, providing a seamless and responsive user experience.\n\nFor a comprehensive guide on implementing JavaScript templates, including syntax, available methods, variables, and practical examples, visit the [JavaScript Template Documentation](template.md).\n\n## Actions\n\nThe Expander Card supports a powerful action system that allows you to control expander cards programmatically from other cards and automations. This enables you to create interactive dashboards where buttons, entity cards, or other elements can open, close, or toggle expander cards.\n\nAction support includes:\n\n- **Remote Control**: Open, close, or toggle expander cards from any card that supports actions\n- **Grouped Control**: Control multiple expander cards simultaneously using shared IDs\n- **Fire-DOM-Event Integration**: Seamless integration with Home Assistant's action system\n- **Button Integration**: Create dedicated control buttons for your expander cards\n- **Automation Triggers**: Integrate with your Home Assistant automations and scripts\n\nActions provide a flexible way to create interactive dashboard layouts where different parts of your UI can communicate and respond to user interactions.\n\nFor detailed information on configuring and using actions, including complete examples and setup instructions, see the [Action Configuration Guide](action.md).\n"
  },
  {
    "path": "docs/source/chapter/templating/template.md",
    "content": "# Javascript Template\n\nExpander 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.\n\n| Config item | Accepts value | Overrides config items |\n| --- | --- | --- |\n| `expanded` | boolean (`true\\|false`) | `expanded`, `min-width-expanded`, `max-width-expanded`, `start-expanded-users` |\n| `title` | string | `title` |\n| `icon` | string | `icon` |\n| `arrow-color` | CSS color (string) | `arrow-color` |\n| `style` | string | `style` |\n\nJavascript 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']`.\n\nTemplates 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.\n\nTemplates also have access to special variables which track the expanded state of any expander on the Dashboard which has `expander-card-id` config set. See [Templates - tracking expanded state of other expander cards](#templates---tracking-expanded-state-of-other-expander-cards)\n\nTemplates 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).\n\nJavascript 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`.\n\n## Variables\n\nVariables are defined in the `variables` list of expander card config.\n\n**IMPORTANT**: As variables are evaluated asynchronously, their initial value will be `undefined`. Your templates need to be written to handle this initial case.\n\n| List item | Type | Config |\n| --------- | ---- | ------ |\n| `variable` | string | The `<name>` of the variable which will be available in templates as `variable.<name>`. |\n| `value_template` | string \\| value \\| object | Either javascript that returns a value or a straight value. Javascript must be enclosed by `[[[]]]` with only whitespace preceding or following. |\n\nVariable `weather_warnings` tracking the state of `input_boolean.weather_warnings`.\n\n```yaml\nvariables:\n  - variable: weather_warnings\n    value_template: |\n      [[[\n        return is_state('input_boolean.weather_warnings', 'on');\n      ]]]\n```\n\n## Templates\n\nTemplates are defined in the `templates` list of expander card config.\n\n| List item | Type | Config |\n| --------- | ---- | ------ |\n| `template` | string | The config item being templated. Only supported config items will be read by expander card. See list in main [Advanced javascript templates](../templating/template.md) section. |\n| `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. |\n\nTemplate for `expanded` config item tracking state of `input_boolean.weather_warnings`.\n\n```yaml\ntemplates:\n  - template: expanded\n    value_template: |\n       [[[\n         return is_state('input_boolean.weather_warnings', 'on');\n       ]]]\n```\n\nSame 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.\n\n```yaml\nvariables:\n  - variable: weather_warnings\n    value_template: |\n      [[[\n        return is_state('input_boolean.weather_warnings', 'on');\n      ]]]\ntemplates:\n  - template: expanded\n    value_template: |\n       [[[\n         return variables.weather_warnings ?? false;\n       ]]]\n  - template: style\n    value_template: |\n      [[[\n        return `\n          .title \n          { \n            transition: color 0.35s ease, font-weight 0.35s ease;\n            color: ${variables.weather_warnings ? 'red' : 'var(--primary-text-color)'};\n            font-weight: ${variables.weather_warnings ? '700' : 'var(--ha-font-weight-body)'};\n          }`;\n      ]]]\n```\n\nMore 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.\n\n### Templates - tracking expanded state of other expander cards\n\nIf you set `expander-card-id` in an expander card it will broadcast its state to all expander cards on the open Dashboard. When using templates, the expanded state will be in special variables of the form `variables.['<expander-card-id>_open']`.\n\nIf you have an expander card with `expander-card-id: my-expander-id` its expanded state will be available in templates via `variables['my-expander-id_open']`. Note here that Javascript access to the variable is via `[]` due to the `expander-card-id` having `-` in it. If you use `_` so that the `expander-card-id` is a valid object accessor, you can use `.` accessor. e.g. `expander-card-id: my_expander_id` => `variables.my_expander_id_open`.\n\n### Example - using expander card open state variables\n\nThis example shows using expander card open state variable for expanded template of other expander cards. The variables can also be used in any template config supported by expander card.\n\nMaster expander card\n\n```yaml\ntype: custom:expander-card\ntitle: Expander Card - Master\nexpander-card-id: test_expander_id\ncards:\n  - type: entity\n    entity: light.kitchen_lights\n```\n\nExpander card following master\n\n```yaml\ntype: custom:expander-card\ntitle: Expander Card - Follow\ncards:\n  - type: entity\n    entity: light.bed_light\ntemplates:\n  - template: expanded\n    value_template: |-\n      [[[\n        return variables.test_expander_id_open;\n      ]]]\n```\n\nExpander card following with opposite open state\n\n```yaml\ntype: custom:expander-card\ntitle: Expander Card - Opposite Follow\ncards:\n  - type: entity\n    entity: light.bed_light\ntemplates:\n  - template: expanded\n    value_template: |-\n      [[[\n        return !variables.test_expander_id_open;\n      ]]]\n```\n"
  },
  {
    "path": "docs/source/index.md",
    "content": "# About Expander Card\n\nWelcome to the documentation of the Expander Card!\n\nThe Expander Card is a highly configurable Lovelace card that allows you to organize multiple cards within an expandable container. Through its numerous configuration options, you can precisely control the appearance, behavior, and functionality of the card, tailoring it to your dashboard design.\n\nFollow [Quick start](quick-start.md) for the initial installation / setup\n\n:material-github: [Github project](https://github.com/MelleD/lovelace-expander-card)\n\n[![PayPal](https://img.shields.io/static/v1.svg?label=%20&message=PayPal.Me&logo=paypal)](https://www.paypal.me/MelleDennis)\n\n:material-coffee: [Buy Me A Coffee](https://www.buymeacoffee.com/melled)\n\n## Demo\n\n![Sample gif](chapter/assets/example.gif)\n\n---\n\n### Expand button as overlay\n\n![Sample lights overlay](chapter/assets/lights_overlay_button.png)\n\n---\n\n### You can even nest expanders\n\n![Sample nesting](chapter/assets/nested.png)\n\n---\n\n### Clear Background (default theme)\n\n![Sample clear router](chapter/assets/clear_router.png)\n"
  },
  {
    "path": "docs/source/quick-start.md",
    "content": "# Quick Start\n\n## Installation\n\n### HACS\n\nExpander-Card is available in [HACS](https://www.hacs.xyz/) (Home Assistant Community Store) by default.\n\n1. Install HACS if you don't have it already\n2. Open HACS in Home Assistant\n3. Searching for expander card\n\n[![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)\n\n### Manual\n\n1. Download `expander-card.js` file from the [latest release](https://github.com/MelleD/lovelace-expander-card/releases/latest).\n2. Put `expander-card.js` file into your `config/www` folder.\n3. Add reference to `expander-card.js` in Dashboard. There's two way to do that:\n    - **Using UI:** _Settings_ → _Dashboards_ → _More Options icon_ → _Resources_ → _Add Resource_ → Set _Url_ as `/local/expander-card.js` → Set _Resource type_ as `JavaScript Module`.\n      **Note:** If you do not see the Resources menu, you will need to enable _Advanced Mode_ in your _User Profile_\n    - **Using YAML:** Add following code to `lovelace` section.\n\n      ```yaml\n        resources:\n            - url: /local/expander-card.js\n              type: module\n        ```\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { FlatCompat } from '@eslint/eslintrc';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport stylistic from '@stylistic/eslint-plugin'\n\n\n// mimic CommonJS variables -- not needed if using CommonJS\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst compat = new FlatCompat({\n    baseDirectory: __dirname\n});\n\nimport js from '@eslint/js';\nimport tseslint from 'typescript-eslint';\nimport eslintPluginSvelte from 'eslint-plugin-svelte';\nimport svelteEslintParser from 'svelte-eslint-parser';\nimport eslintPluginPrettier from 'eslint-plugin-prettier/recommended';\nimport globals from 'globals';\n\n\nexport default tseslint.config(\n    eslintPluginPrettier,\n    js.configs.recommended,\n    ...tseslint.configs.recommended,\n    ...eslintPluginSvelte.configs['flat/recommended'],\n    {\n        ignores: ['eslint.config.mjs','rollup.config.mjs','dist/**/*','vite.config.js', 'svelte.config.js', 'eslint.config.js', '.svelte-kit/**/*', 'build/**/*', 'docs/**/*', '.venv/**/*'],\n    },\n    {\n        plugins: {\n            '@typescript-eslint': tseslint.plugin,\n            '@stylistic': stylistic\n        },\n        languageOptions: {\n            parser: tseslint.parser,\n            globals: {\n                ...globals.browser,\n                ...globals.node,\n                ...globals.es2021,\n            },\n            parserOptions: {\n                sourceType: 'module',\n                ecmaVersion: 2020,\n                project: './tsconfig.json',\n                extraFileExtensions: ['.svelte'],\n            },\n        },\n        rules: {\n            'prettier/prettier': 'off',\n            '@typescript-eslint/no-unused-expressions': 'off',\n            '@typescript-eslint/no-namespace': 'off',\n            '@typescript-eslint/adjacent-overload-signatures': 'error',\n            '@typescript-eslint/array-type': 'error',\n            '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],\n            '@typescript-eslint/no-inferrable-types': 'error',\n            '@typescript-eslint/no-misused-new': 'error',\n            '@typescript-eslint/no-this-alias': 'error',\n            '@typescript-eslint/prefer-for-of': 'error',\n            '@typescript-eslint/prefer-function-type': 'error',\n            '@typescript-eslint/prefer-namespace-keyword': 'error',\n            'no-inner-declarations': 'off',\n            '@typescript-eslint/triple-slash-reference': 'error',\n            '@stylistic/type-annotation-spacing': 'error',\n            '@typescript-eslint/unified-signatures': 'error',\n            '@typescript-eslint/no-explicit-any': 'error',\n            '@typescript-eslint/no-unused-vars': 'error',\n            '@typescript-eslint/unbound-method': 'warn',\n            '@typescript-eslint/no-floating-promises': 'error',\n            '@typescript-eslint/no-unnecessary-type-assertion': 'error',\n            'object-curly-spacing': ['error', 'always'],\n            '@stylistic/semi': [\n                'error',\n                'always'\n            ],\n            '@stylistic/quotes': [\n                'warn',\n                'single'\n            ],\n            '@stylistic/member-delimiter-style': [\n                'error',\n                {\n                    multiline: {\n                        delimiter: 'semi',\n                        requireLast: true\n                    },\n                    singleline: {\n                        delimiter: 'semi',\n                        requireLast: false\n                    }\n                }\n            ],\n            '@stylistic/indent': [\n                'warn',\n                4,\n                {\n                    FunctionDeclaration: {\n                        parameters: 'first'\n                    },\n                    FunctionExpression: {\n                        parameters: 'first'\n                    },\n                    SwitchCase: 1\n                }\n            ],\n\n            '@typescript-eslint/explicit-member-accessibility': [\n                'error',\n                {\n                    accessibility: 'explicit'\n                }\n            ],\n            '@typescript-eslint/no-use-before-define': ['error', { functions: false }],\n            'no-console': 'warn',\n            'no-return-await': 'error',\n            'arrow-body-style': 'error',\n            'arrow-parens': [\n                'error',\n                'always'\n            ],\n            'comma-dangle': [\n                'error',\n                {\n                    objects: 'never',\n                    arrays: 'never',\n                    functions: 'never'\n                }\n            ],\n            'prefer-arrow-callback': 'error',\n            'prefer-const': 'error',\n            'quote-props': [\n                'error',\n                'consistent-as-needed'\n            ],\n            'no-var': 'error',\n            'new-parens': 'error',\n            'no-caller': 'error',\n            'no-cond-assign': 'error',\n            'no-debugger': 'error',\n            'no-empty': 'error',\n            'no-eval': 'error',\n            'no-multiple-empty-lines': 'warn',\n            'no-new-wrappers': 'error',\n            'no-redeclare': 'error',\n            'no-shadow': [\n                'error',\n                {\n                    hoist: 'all'\n                }\n            ],\n            'no-throw-literal': 'error',\n            'no-trailing-spaces': 'error',\n            'no-undef-init': 'error',\n            'no-underscore-dangle': 'error',\n            'no-unsafe-finally': 'error',\n            'no-unused-labels': 'error',\n            'spaced-comment': 'error',\n            'use-isnan': 'error',\n            'max-len': [\n                'warn',\n                {\n                    code: 180\n                }\n            ],\n            'dot-notation': 'error',\n            'eqeqeq': 'error',\n            'eol-last': 'error',\n            'linebreak-style': ['error', 'unix'],\n            'block-spacing': ['error', 'always'],\n            'tsdoc/syntax': 'off'\n        },\n    },\n    {\n        files: ['*.svelte', '**/*.svelte'],\n        languageOptions: {\n            parser: svelteEslintParser,\n            parserOptions: {\n                parser: tseslint.parser,\n            }\n        },\n        rules: {\n            'prettier/prettier': ['warn', {\n                'svelteStrictMode': true,\n                'svelteBracketNewLine': false,\n                'svelteAllowShorthand': false,\n                'svelteIndentScriptAndStyle': false,\n                'tabWidth': 4,\n                'bracketSpacing': true,\n                'trailingComma': 'none',\n                'arrowParens': 'always',\n                'semi': true,\n                'singleQuote': true,\n                'printWidth': 110,\n                'proseWrap': 'preserve',\n                'plugins': ['prettier-plugin-svelte'],\n            }],\n        }\n    }\n);\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n    \"name\": \"expander-card\",\n    \"content_in_root\": false,\n    \"render_readme\": true,\n    \"filename\": \"expander-card.js\",\n    \"homeassistant\": \"2026.3.0\"\n}"
  },
  {
    "path": "license.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"lovelace-expander-card\",\n    \"version\": \"0.0.1\",\n    \"type\": \"module\",\n    \"watch\": {\n        \"buildAndStart\": {\n            \"patterns\": [\n                \"src\"\n            ],\n            \"extensions\": \"svelte\"\n        }\n    },\n    \"scripts\": {\n        \"build\": \"eslint . && vite build\",\n        \"srv\": \"pnpx serve -l 5000 --cors dist\",\n        \"lint\": \"eslint .\",\n        \"lint-fix\": \"eslint . --fix\"\n    },\n    \"devDependencies\": {\n        \"@eslint/eslintrc\": \"^3.3.5\",\n        \"@eslint/js\": \"^10.0.1\",\n        \"@stylistic/eslint-plugin\": \"^5.10.0\",\n        \"@sveltejs/adapter-node\": \"^5.5.4\",\n        \"@sveltejs/enhanced-img\": \"^0.6.1\",\n        \"@sveltejs/vite-plugin-svelte\": \"5.1.1\",\n        \"@types/eslint\": \"9.6.1\",\n        \"eslint\": \"^10.3.0\",\n        \"eslint-config-prettier\": \"^10.1.8\",\n        \"eslint-plugin-prefer-arrow\": \"^1.2.3\",\n        \"eslint-plugin-prettier\": \"^5.5.5\",\n        \"eslint-plugin-svelte\": \"^3.17.1\",\n        \"globals\": \"^17.6.0\",\n        \"postcss-preset-env\": \"^11.2.1\",\n        \"prettier\": \"^3.8.3\",\n        \"prettier-plugin-svelte\": \"^3.5.1\",\n        \"svelte\": \"5.55.5\",\n        \"svelte-check\": \"^4.4.8\",\n        \"svelte-eslint-parser\": \"^1.6.1\",\n        \"tslib\": \"^2.8.1\",\n        \"typescript\": \"^6.0.3\",\n        \"typescript-eslint\": \"8.59.2\",\n        \"vite\": \"^6.4.2\"\n    },\n    \"dependencies\": {\n        \"home-assistant-javascript-templates\": \"^7.0.0\",\n        \"home-assistant-query-selector\": \"^6.0.1\",\n        \"lit\": \"^3.3.2\",\n        \"svelte\": \"5.55.5\"\n    },\n    \"pnpm\": {\n        \"ignoredBuiltDependencies\": [\n            \"@sveltejs/kit\"\n        ]\n    }\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"lovelace-expander-card\"\nversion = \"0.0.1\"\nrequires-python = \">=3.11\"\n\n[project.optional-dependencies]\ntest = [\n    \"ha-testcontainer[test]>=2.1\",\n    \"pytest>=7.0\",\n    \"PyYAML>=6.0\",\n    \"playwright>=1.0\",\n]\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\naddopts = \"-v --tb=short\"\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "import resolve from '@rollup/plugin-node-resolve';\nimport replace from '@rollup/plugin-replace';\nimport svelte from 'rollup-plugin-svelte';\nimport commonjs from '@rollup/plugin-commonjs';\nimport typescript from '@rollup/plugin-typescript';\nimport terser from '@rollup/plugin-terser';\nimport sveltePreprocess from 'svelte-preprocess';\nimport json from '@rollup/plugin-json';\n\n\nconst MAIN_COMPONENT_NAME = 'ExpanderCard';\nconst TAG_NAME = 'expander-card';\nconst CONTAINER_TAG_NAME ='expander-child-card';\nconst FILE_NAME = `${TAG_NAME}.js`;\n\n\nexport default (commandlineargs) => {\n    console.log('commandlineargs: ', commandlineargs);\n    return ({\n        input: 'src/index.ts',\n        output: {\n            sourcemap: true,\n            format: 'umd',\n            name: MAIN_COMPONENT_NAME,\n            file: `dist/${FILE_NAME}`\n        },\n        plugins: [\n            replace({\n                'tag-name': TAG_NAME,\n                'container-tag-name': CONTAINER_TAG_NAME,\n                'preventAssignment': true\n            }),\n            svelte({\n                preprocess:\n                sveltePreprocess({\n                    sourceMap: true\n                }),\n                compilerOptions: {\n                    customElement: true,\n                    hydratable: true,\n                    dev: true\n                },\n                emitCss: true\n            }),\n            resolve({\n                browser: true,\n                dedupe: ['svelte']\n            }),\n            commonjs(),\n            json(),\n            typescript({\n                sourceMap: true,\n                inlineSources: !production\n            }),\n            production && terser()\n        ],\n        watch: {\n            clearScreen: false\n        }\n    }); };\n"
  },
  {
    "path": "src/Card.svelte",
    "content": "<!--\n/*\nCopyright 2021-2022 Peter Repukat - FlatspotSoftware\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n-->\n<!-- eslint-disable-next-line svelte/valid-compile -->\n<svelte:options customElement='expander-sub-card' />\n\n<script lang=\"ts\">\n    import { onMount, untrack } from 'svelte';\n    import type { AnimationState, HomeAssistant, HuiCard, LovelaceCardConfig } from './types';\n    import { computeCardSize } from './helpers/compute-card-size';\n\n    const {\n        config,\n        hass,\n        preview,\n        marginTop ='0px',\n        open,\n        animation = true,\n        animationState,\n        clearCardCss = false\n    }: {\n        config: LovelaceCardConfig;\n        hass: HomeAssistant | undefined;\n        preview: boolean;\n        marginTop?: string;\n        open: boolean;\n        animation: boolean;\n        animationState: AnimationState;\n        clearCardCss: boolean;\n    } = $props();\n\n    let outerContainer: HTMLElement | null = null;\n    let container = $state<HuiCard | null>(null);\n    let loading = $state(true);\n    let cardHeight = $state(0);\n    const cardConfig: LovelaceCardConfig = untrack(() => JSON.parse(JSON.stringify(config)));\n    $effect(() => {\n        if (container) {\n            container.hass = hass;\n        }\n    });\n    $effect(() => {\n        if (container && preview !== undefined) {\n            container.preview = preview;\n        }\n    });\n    $effect(() => {\n        if (container) {\n            // do not set hui-card hidden as this prevents it from updating its display\n            // card disabled config sets hui-card to correctly update its display\n            cardConfig.disabled = !open;\n            // eslint-disable-next-line no-underscore-dangle\n            container._element?.dispatchEvent(new CustomEvent('card-visibility-changed', { detail: { value: open }, bubbles: true, composed: false }));\n        }\n    });\n\n    onMount(async () => {\n        const el: HuiCard = document.createElement('hui-card') as HuiCard;\n        el.hass = hass;\n        el.preview = preview;\n        // do not set hui-card hidden as this prevents it from updating its display\n        // card disabled config sets hui-card to correctly update its display\n        cardConfig.disabled = !open;\n        el.config = cardConfig;\n        el.load();\n\n        // eslint-disable-next-line svelte/no-dom-manipulating\n        outerContainer?.appendChild(el);\n        container = el;\n        loading = false;\n\n        // hui-card will fire card-updated on ll-upgrade which causes some view to reload\n        // so we capture ll-upgrade, stop propagation and set hass on the upgraded element ourselves\n        container.addEventListener('ll-upgrade', (ev) => {\n            ev.stopPropagation();\n            // eslint-disable-next-line no-underscore-dangle\n            if (container?._element && hass) {\n                // eslint-disable-next-line no-underscore-dangle\n                container._element.hass = hass;\n            }\n        }, { capture: true });\n\n        if (clearCardCss) {\n            el.style.setProperty('--ha-card-background', 'transparent');\n            el.style.setProperty('--ha-card-box-shadow', 'none');\n            el.style.setProperty('--ha-card-border-color', 'transparent');\n            el.style.setProperty('--ha-card-border-width', '0px');\n            el.style.setProperty('--ha-card-backdrop-filter', 'none');\n        }\n\n        if (animation) {\n            // Start with an estimated height.\n            // Update with resize observer once we have the real height.\n            // 56px is the height of one card size unit\n            cardHeight = await computeCardSize(el) * 56;\n            if (outerContainer) {\n                cardHeight += window.getComputedStyle(outerContainer).marginTop\n                    ? parseFloat(window.getComputedStyle(outerContainer).marginTop)\n                    : 0;\n            }\n            const resizeObserver = new ResizeObserver((entries) => {\n                for (const entry of entries) {\n                    if (entry.contentBoxSize) {\n                        const contentBoxSize = Array.isArray(entry.contentBoxSize)\n                            ? entry.contentBoxSize[0]\n                            : entry.contentBoxSize;\n                        if (contentBoxSize.blockSize) {\n                            cardHeight = contentBoxSize.blockSize;\n                            if (container) {\n                                cardHeight += window.getComputedStyle(container).marginTop\n                                    ? parseFloat(window.getComputedStyle(container).marginTop)\n                                    : 0;\n                            }\n                        }\n                    } else if (entry.contentRect) {\n                        cardHeight = entry.contentRect.height;\n                        if (container) {\n                            cardHeight += window.getComputedStyle(container).marginTop\n                                ? parseFloat(window.getComputedStyle(container).marginTop)\n                                : 0;\n                        }\n                    }\n                }\n            });\n            resizeObserver.observe(el);\n        }\n    });\n\n</script>\n\n<div class=\"outer-container{open ? ' open' : ' close'}{animation ? ' animation ' + animationState : ''}\"\n  style=\"--child-card-margin-top: {open ? marginTop : '0px'};{cardHeight ? ` --expander-animation-height: -${cardHeight}px;` : ''}\"\n  bind:this={outerContainer}>\n    {#if loading}\n        <span class=\"loading\"> Loading... </span>\n    {/if}\n</div>\n\n\n<style>\n  .loading {\n    padding: 1em;\n    display: block;\n  }\n .animation :global {\n    hui-card {\n        display: flex;\n        flex-direction: column;\n    }\n  }\n  .outer-container.animation {\n    transition: margin-bottom 0.35s ease;\n  }\n  .outer-container.animation.open,\n  .outer-container.animation.opening {\n    margin-bottom: inherit;\n  }\n  .outer-container.animation.close,\n  .outer-container.animation.closing {\n    margin-bottom: var(--expander-animation-height, -100%);\n  }\n  .outer-container.animation.opening {\n    animation: fadeInOpacity 0.5s forwards ease;\n    -webkit-animation: fadeInOpacity 0.5s forwards ease;\n  }\n  .outer-container.animation.closing {\n      animation: fadeOutOpacity 0.5s forwards ease;\n      -webkit-animation: fadeOutOpacity 0.5s forwards ease;\n  }\n  .outer-container > :global(hui-card) {\n    margin-top: var(--child-card-margin-top, 0px);\n  }\n  @keyframes fadeInOpacity {\n      0% {\n          opacity: 0;\n      }\n      100% {\n          opacity: 1;\n      }\n  }\n  @-webkit-keyframes fadeInOpacity {\n      0% {\n          opacity: 0;\n      }\n      100% {\n          opacity: 1;\n      }\n  }\n    @keyframes fadeOutOpacity {\n      0% {\n          opacity: 1;\n      }\n      100% {\n          opacity: 0;\n      }\n  }\n  @-webkit-keyframes fadeOutOpacity {\n      0% {\n          opacity: 1;\n      }\n      100% {\n          opacity: 0;\n      }\n  }\n</style>\n"
  },
  {
    "path": "src/ExpanderCard.svelte",
    "content": "<script lang=\"ts\" module>\n        export const defaults = {\n            'gap': '0.0em',\n            'expanded-gap': '0.6em',\n            'padding': '1em',\n            'clear': false,\n            'clear-children': false,\n            'title': ' ',\n            'overlay-margin': '0.0em',\n            'child-padding': '0.0em',\n            'child-margin-top': '0.0em',\n            'button-background': 'transparent',\n            'expander-card-background': 'var(--ha-card-background,var(--card-background-color,#fff))',\n            'header-color': 'var(--primary-text-color,#fff)',\n            'arrow-color': 'var(--arrow-color,var(--primary-text-color,#fff))',\n            'expander-card-display': 'block',\n            'title-card-clickable': false,\n            'min-width-expanded': 0,\n            'max-width-expanded': 0,\n            'icon': 'mdi:chevron-down',\n            'icon-rotate-degree': '180deg',\n            'animation': true,\n            'haptic': 'light' as const\n        };\n        import { loadExpanderCardEditor } from './ExpanderCardEditor';\n</script>\n\n<!-- eslint-disable-next-line svelte/valid-compile -->\n<svelte:options customElement={{\n    tag: 'expander-card',\n    extend: (customElementConstructor) => class extends customElementConstructor {\n        // re-declare props used in customClass.\n        public config!: ExpanderConfig;\n\n        public static async getConfigElement() {\n            await loadExpanderCardEditor();\n            return document.createElement('expander-card-editor');\n        }\n\n        public static getStubConfig() {\n            return {\n                type: 'custom:expander-card',\n                title: 'Expander Card',\n                cards: []\n            };\n        }\n\n        public setConfig(conf = {}) {\n            this.config = { ...defaults, ...conf };\n        };\n    }\n}}/>\n\n<script lang=\"ts\">\n    import type { ExpanderCardEventDetail, ExpanderCardLlCustomEventDetail, HaRipple, HomeAssistant } from './types';\n    import Card from './Card.svelte';\n    import { onMount, untrack } from 'svelte';\n    import type { ExpanderCardTemplates, ExpanderConfig, ExpanderCardRawConfig } from './configtype';\n    import type { AnimationState } from './types';\n    import { forwardHaptic } from './helpers/forward-haptic';\n    import { isJSTemplate, getJSTemplateRenderer, trackJSTemplate, setJSTemplateRef, trackJSTemplateEvent } from './helpers/templates';\n    import type { HomeAssistantJavaScriptTemplatesRenderer } from 'home-assistant-javascript-templates';\n    import { getDashboardRawConfig } from './helpers/raw-config';\n    import { styleToString } from './helpers/style-converter';\n\n    const {\n        hass,\n        preview,\n        config = defaults\n    }: {hass: HomeAssistant; preview: boolean; config: ExpanderConfig} = $props();\n\n    let touchPreventClick = $state(false);\n    let touchPreventClickTimeout: ReturnType<typeof setTimeout> | null = $state(null);\n\n    let open = $state(untrack(() => preview) ? true : false);\n    let previewState = $state(untrack(() => preview) ? true : false);\n    let showButtonUsers = $state(untrack(() => preview || (userInList(config['show-button-users']) ?? true)));\n    let animationState: AnimationState = $state<AnimationState>('idle');\n    let animationTimeout: ReturnType<typeof setTimeout> | null = $state(null);\n    let backgroundAnimationDuration = $state(0);\n    let overlayHeight = $state(0);\n    let expanderCard: HTMLElement | null = $state(null);\n    let titleCardDiv: HTMLElement | null = $state(null);\n    let buttonElement: HTMLElement | null = $state(null);\n    let ripple: HaRipple | null = $state(null);\n    const templateEvents: Record<string, () => void> = {};\n    const variableRenders: Record<string, Promise<(() => void)>> = {};\n    const templateRenderers: Record<string, Promise<(() => void)>> = {};\n    const templateValues: Record<string, unknown> = $state({});\n    let dashboardRawConfig: ExpanderCardRawConfig = $state( getDashboardRawConfig() );\n\n    const userStyleTemplateOrConfig: string | null = $derived.by(() => {\n        const templateStyle = templateValues.style;\n        const configStyle = config.style;\n\n        let styleString: string | null = null;\n\n        if (templateStyle !== undefined) {\n            // Handle templateValues.style - could be string or object\n            styleString = typeof templateStyle === 'string'\n                ? templateStyle\n                : (typeof templateStyle === 'object' && templateStyle !== null)\n                    ? styleToString(templateStyle as Record<string, Record<string, string>>)\n                    : String(templateStyle);\n        } else if (configStyle) {\n            // Handle config.style - could be string or object\n            styleString = styleToString(configStyle);\n        }\n\n        return styleString ? `<style>${styleString}</style>` : null;\n    });\n    const iconConfigOrTemplate: string | undefined = $derived(\n        templateValues.icon !== undefined ?\n            String(templateValues.icon) :\n            config.icon);\n    const titleConfigOrTemplate: string | undefined = $derived(\n        templateValues.title !== undefined ?\n            String(templateValues.title) :\n            config.title);\n    const arrowColorConfigOrTemplate: string | undefined = $derived(\n        templateValues['arrow-color'] !== undefined ?\n            String(templateValues['arrow-color']) :\n            config['arrow-color']);\n    const configId = untrack(() => config['storage-id']);\n    const lastStorageOpenStateId = 'expander-open-' + configId;\n\n    $effect(() => {\n        // effect for template 'expanded'. We untrack preview and open to avoid infinite loop effect loops.\n        if (templateValues.expanded === undefined) {\n            return;\n        }\n        if (untrack(() => preview && dashboardRawConfig['preview-expanded'] !== false)) {\n            return;\n        }\n        const resultBoolean = Boolean(templateValues.expanded);\n\n        // Use queueMicrotask to avoid effect loop as open needs to be updated after this effect completes.\n        queueMicrotask(() => {\n            if (resultBoolean !== open) {\n                toggleOpen(resultBoolean);\n            }\n        });\n    });\n\n    $effect(() => {\n        // effect for preview changes. We untrack templateValues.expanded to avoid unnecessary effect triggering.\n        if (preview === previewState || preview === undefined) {\n            return;\n        }\n        previewState = preview;\n        if (previewState && dashboardRawConfig['preview-expanded'] !== false) {\n            setOpenState(true);\n            showButtonUsers = true;\n            return;\n        }\n        showButtonUsers = userInList(config['show-button-users']) ?? true;\n\n        if (configTemplate('expanded')) {\n            const templateExpanded = untrack(() => templateValues.expanded);\n            if (templateExpanded !== undefined) {\n                toggleOpen(Boolean(templateExpanded));\n            }\n            return;\n        }\n        setDefaultOpenState();\n    });\n\n    function configTemplate(templateKey: string): ExpanderCardTemplates | undefined {\n        const template = config.templates && Array.isArray(config.templates) ?\n            config.templates.find((t) => t.template === templateKey) : undefined;\n        if (template && isJSTemplate(template.value_template)) {\n            return template;\n        }\n        return undefined;\n    }\n\n    function dispatchOpenStateEvent(openState: boolean) {\n        if (!config['expander-card-id']) {\n            return;\n        }\n        const detail: ExpanderCardEventDetail = {};\n        detail[config['expander-card-id']] = {\n            property: 'open',\n            value: openState\n        };\n        document.dispatchEvent(new CustomEvent('expander-card', {\n            detail: detail,\n            bubbles: true,\n            composed: true\n        }));\n    }\n\n    function userInList(userList: string[] | undefined): boolean | undefined {\n        if (userList === undefined) {\n            return undefined;\n        }\n        return hass?.user?.name !== undefined && userList.includes(hass?.user?.name);\n    }\n\n    function setDefaultOpenState() {\n        // Do not run setDefaultOpenState if config.expanded is a JS template\n        if (configTemplate('expanded')) {\n            return;\n        }\n        if (userInList(config['start-expanded-users'])) {\n            setOpenStateAndDispatchEvent(true);\n            return;\n        }\n        if (configId === undefined) {\n            setOpenStateFromConfig();\n            return;\n        }\n        try {\n            const storageValue = localStorage.getItem(lastStorageOpenStateId);\n            if(storageValue === null){\n                setOpenStateFromConfig();\n                return;\n            }\n            // last state is stored in local storage\n            const openStateByStorage = storageValue ? storageValue === 'true' : open;\n            setOpenStateAndDispatchEvent(openStateByStorage);\n        } catch (e) {\n            console.error(e);\n            setOpenStateAndDispatchEvent(false);\n        }\n\n    }\n\n    function setOpenStateFromConfig() {\n        // first time, set the state from config\n        if (config.expanded !== undefined) {\n            setOpenStateAndDispatchEvent(config.expanded);\n            return;\n        }\n        setOpenStateAndDispatchEvent(false);\n    }\n\n    function toggleOpen(openState?: boolean) {\n        if (animationTimeout) {\n            clearTimeout(animationTimeout);\n            animationTimeout  = null;\n        }\n        const newOpenState = openState !== undefined ? openState : !open;\n        if (!config.animation) {\n            setOpenStateAndDispatchEvent(newOpenState);\n            return;\n        }\n\n        dispatchOpenStateEvent(newOpenState);\n        animationState = newOpenState ? 'opening' : 'closing';\n        if (newOpenState) {\n            setOpenState(true);\n            animationTimeout = setTimeout(() => {\n                animationState = 'idle';\n                animationTimeout = null;\n            }, 350);\n            return;\n        }\n        animationTimeout = setTimeout(() => {\n            setOpenState(false);\n            animationState = 'idle';\n            animationTimeout = null;\n        }, 350);\n    }\n\n    function setOpenStateAndDispatchEvent(openState: boolean) {\n        setOpenState(openState);\n        dispatchOpenStateEvent(openState);\n    }\n\n    function setOpenState(openState: boolean) {\n        open = openState;\n        if (!preview && configId !== undefined) {\n            try {\n                localStorage.setItem(lastStorageOpenStateId, open ? 'true' : 'false');\n            } catch (e) {\n                /* eslint no-console: 0 */\n                console.error(e);\n            }\n        }\n        if (open && backgroundAnimationDuration === 0) {\n            backgroundAnimationDuration = 0.35;\n        }\n    }\n\n    function handleRawConfigUpdate(event: Event) {\n        const rawConfig: ExpanderCardRawConfig = (event as CustomEvent).detail?.rawConfig;\n        if (!rawConfig) {\n            return;\n        }\n        if (JSON.stringify(rawConfig) !== JSON.stringify(dashboardRawConfig)) {\n            dashboardRawConfig = rawConfig;\n        }\n    };\n\n    function handleLlCustomEvent(event: Event) {\n        const data: ExpanderCardLlCustomEventDetail = (event as CustomEvent).detail?.['expander-card']?.data;\n        if (data?.['expander-card-id'] && data['expander-card-id'] === config['expander-card-id']) {\n            if (data.action === 'open' && !open) {\n                toggleOpen(true);\n                return;\n            }\n\n            if (data.action === 'close' && open) {\n                toggleOpen(false);\n                return;\n            }\n\n            if (data.action === 'toggle') {\n                toggleOpen();\n            }\n        }\n    };\n\n    function cleanup() {\n        document.body.removeEventListener('ll-custom', handleLlCustomEvent);\n        document.body.removeEventListener('expander-card-raw-config-updated', handleRawConfigUpdate);\n        Object.entries(templateRenderers).forEach(([key, renderer]) => {\n            renderer.then((untrackFunc) => {\n                untrackFunc();\n                delete templateRenderers[key];\n            }).catch(() => {});\n        });\n        Object.entries(variableRenders).forEach(([key, renderer]) => {\n            renderer.then((untrackFunc) => {\n                untrackFunc();\n                delete variableRenders[key];\n            }).catch(() => {});\n        });\n        Object.entries(templateEvents).forEach(([key, untrackFunc]) => {\n            untrackFunc();\n            delete templateEvents[key];\n        });\n    };\n\n    const triggerHapticFeedback = (element: HTMLElement) => {\n        if (config.haptic && config.haptic !== 'none') {\n            forwardHaptic(element, config.haptic);\n        }\n    };\n\n    let touchElement: HTMLElement | undefined;\n    let isScrolling = false;\n    let startX = 0;\n    let startY = 0;\n    const touchStart = (event: TouchEvent) => {\n        ripple && (ripple.disabled = true);\n        touchElement = event.target as HTMLElement;\n        startX = event.touches[0].clientX;\n        startY = event.touches[0].clientY;\n        isScrolling = false;\n    };\n\n    const touchMove = (event: TouchEvent) => {\n        const currentX = event.touches[0].clientX;\n        const currentY = event.touches[0].clientY;\n        isScrolling = Math.abs(currentX - startX) > 10 || Math.abs(currentY - startY) > 10;\n    };\n\n    const touchCancel = () => {\n        ripple && (ripple.disabled = false);\n        touchElement = undefined;\n        isScrolling = false;\n    };\n\n    const touchEnd = () => {\n        ripple && (ripple.disabled = false);\n    };\n\n    const touchEndAction = (event: TouchEvent) => {\n        if (!isScrolling && touchElement === event.target && config['title-card-clickable']) {\n            triggerHapticFeedback(touchElement);\n            toggleOpen();\n            touchPreventClick = true;\n            // A touch event may not always be followed by a click event so we set a timeout to reset\n            touchPreventClickTimeout = window.setTimeout(() => {\n                touchPreventClick = false;\n                touchPreventClickTimeout = null;\n            }, 100);\n            //  A touch event may not always be followed by a click event so we manually control the ripple\n            if (ripple) {\n                ripple.startPressAnimation();\n                ripple.endPressAnimation();\n            }\n        }\n        touchElement = undefined;\n        isScrolling = false;\n    };\n\n    const bindTemplateVariables = (haJS: Promise<HomeAssistantJavaScriptTemplatesRenderer>) => {\n        for (const v of Object.values(config.variables ?? {})) {\n            if (isJSTemplate(v.value_template)) {\n                variableRenders[v.variable] = trackJSTemplate(\n                    haJS,\n                    (res) => {\n                        setJSTemplateRef(haJS, v.variable, res);\n                    },\n                    v.value_template as string,\n                    { config: config }\n                );\n            } else {\n                setJSTemplateRef(haJS, v.variable, v.value_template);\n            }\n        }\n    };\n\n    const bindExpanderCardEvents = (haJS: Promise<HomeAssistantJavaScriptTemplatesRenderer>) => {\n        templateEvents['expander-card'] = trackJSTemplateEvent(haJS, 'expander-card');\n    };\n\n    const bindTemplates = () => {\n        if (!config.templates) return;\n        const refs = Object.values(config.variables || {}).reduce(\n            (obj, value) => {\n                obj[value.variable] = undefined;\n                return obj;\n            },\n            {} as Record<string, unknown>\n        );\n        const haJS: Promise<HomeAssistantJavaScriptTemplatesRenderer> = getJSTemplateRenderer( { config: config, expanderCard: {} }, refs );\n        bindTemplateVariables(haJS);\n        bindExpanderCardEvents(haJS);\n        Object.values(config.templates || {}).forEach((t) => {\n            if (isJSTemplate(t.value_template)) {\n                templateRenderers[t.template] = trackJSTemplate(\n                    haJS,\n                    (res) => {\n                        templateValues[t.template] = res;\n                    },\n                    t.value_template as string,\n                    { config: config }\n                );\n            } else {\n                templateValues[t.template] = t.value_template;\n            }\n        });\n    };\n\n    function setExpandedFromConfig(){\n        if (configTemplate('expanded')) {\n            return;\n        }\n\n        const minWidthExpanded = config['min-width-expanded'];\n        const maxWidthExpanded = config['max-width-expanded'];\n        const offsetWidth = document.body.offsetWidth;\n\n        if (minWidthExpanded && maxWidthExpanded) {\n            config.expanded = offsetWidth >= minWidthExpanded && offsetWidth <= maxWidthExpanded;\n            return;\n        }\n        if (minWidthExpanded) {\n            config.expanded = offsetWidth >= minWidthExpanded;\n            return;\n        }\n\n        if (maxWidthExpanded) {\n            config.expanded = offsetWidth <= maxWidthExpanded;\n        }\n    }\n\n    function setOpenStateByPreview() {\n        if (preview && dashboardRawConfig['preview-expanded'] !== false) {\n            // all expanders will be open so we don't dispatch event\n            setOpenState(true);\n            return;\n        }\n\n        if (configTemplate('expanded')) {\n            const templateExpanded = untrack(() => templateValues.expanded);\n            if (templateExpanded !== undefined) {\n                setOpenStateAndDispatchEvent(Boolean(templateExpanded));\n            } else {\n                setOpenStateAndDispatchEvent(false);\n            }\n        } else{\n            setDefaultOpenState();\n        }\n    }\n\n    function getTouchEventElement(): HTMLElement | undefined {\n        if (config['title-card-clickable'] && !config['title-card-button-overlay'] && titleCardDiv) {\n            return titleCardDiv;\n        }\n        if (buttonElement) {\n            return buttonElement;\n        }\n        return undefined;\n    }\n\n    onMount(() => {\n        bindTemplates();\n        // dispatch initial state to listeners once templates are bound\n        dispatchOpenStateEvent(false);\n        setExpandedFromConfig();\n        setOpenStateByPreview();\n\n        document.body.addEventListener('ll-custom', handleLlCustomEvent);\n        document.body.addEventListener('expander-card-raw-config-updated', handleRawConfigUpdate);\n\n        const touchEventElement = getTouchEventElement();\n\n        if (touchEventElement) {\n            touchEventElement.addEventListener('touchstart', touchStart, { passive: true, capture: true });\n            touchEventElement.addEventListener('touchmove', touchMove, { passive: true,capture: true });\n            touchEventElement.addEventListener('touchcancel', touchCancel, { passive: true, capture: true });\n            touchEventElement.addEventListener('touchend', touchEnd, { passive: true, capture: true });\n            touchEventElement.addEventListener('touchend', touchEndAction, { passive: false, capture: false });\n        }\n\n        if (config['title-card-clickable'] && config['title-card-button-overlay'] && titleCardDiv) {\n            const resizeObserver = new ResizeObserver(() => {\n                if (buttonElement && titleCardDiv && expanderCard) {\n                    const titleRect = titleCardDiv.getBoundingClientRect();\n                    // While margin/padding set by expander-card is equal, users may have styled different margin/padding\n                    overlayHeight = titleRect.height -\n                        parseFloat(getComputedStyle(buttonElement).marginTop) -\n                        parseFloat(getComputedStyle(buttonElement).marginBottom) +\n                        parseFloat(getComputedStyle(expanderCard).paddingTop) +\n                        parseFloat(getComputedStyle(expanderCard).paddingBottom);\n                }\n            });\n            resizeObserver.observe(titleCardDiv);\n        }\n\n        return cleanup;\n    });\n\n    const buttonClick = (event: MouseEvent) => {\n        if (!touchPreventClick) {\n            triggerHapticFeedback(event.currentTarget as HTMLElement);\n            toggleOpen();\n            return undefined;\n        }\n\n        event.preventDefault();\n        event.stopImmediatePropagation();\n        touchPreventClick = false;\n        if (touchPreventClickTimeout) {\n            clearTimeout(touchPreventClickTimeout);\n            touchPreventClickTimeout = null;\n        }\n        return false;\n    };\n</script>\n\n<ha-card\n    class={`expander-card${config.clear ? ' clear' : ''}${open ? ' open' : ' close'} ${animationState}${config.animation ? ' animation ' + animationState : ''}`}\n    style=\"--expander-card-display:{config['expander-card-display']};\n     --gap:{open && animationState !=='closing' ? config['expanded-gap'] : config.gap}; --padding:{config.padding};\n     --expander-state:{open};\n     --icon-rotate-degree:{config['icon-rotate-degree']};\n     --card-background:{open && animationState !== 'closing' &&\n         config['expander-card-background-expanded'] ?\n         config['expander-card-background-expanded'] : config['expander-card-background']};\n     --background-animation-duration:{backgroundAnimationDuration}s;\n     --expander-card-overlay-height:{overlayHeight ? `${overlayHeight}px` : 'auto'};\n    \"\n    bind:this={expanderCard}>\n    {#if config['title-card']}\n        <div id='id1' class={`title-card-header${config['title-card-button-overlay'] ?\n            '-overlay' : ''}${open ? ' open' : ' close'}${config.animation ?\n            ' animation ' + animationState : ''}${config['title-card-clickable'] ? ' clickable' : ''}`}\n            onclick={config['title-card-clickable'] && !config['title-card-button-overlay'] ? buttonClick : null}\n            role={config['title-card-clickable'] && !config['title-card-button-overlay'] ? 'button' : undefined}\n            bind:this={titleCardDiv}\n            >\n            <div id='id2'\n                class={`title-card-container${open ? ' open' : ' close'}${config.animation ? ' animation ' + animationState : ''}`}\n                style=\"--title-padding:{config['title-card-padding'] ? config['title-card-padding'] : '0px'};\">\n                <Card hass={hass}\n                    preview={preview}\n                    config={config['title-card']}\n                    animation={false}\n                    open={true}\n                    animationState='idle'\n                    clearCardCss={config['clear-children']!}\n                />\n            </div>\n            {#if showButtonUsers}\n                <button\n                    onclick={!config['title-card-clickable'] || config['title-card-button-overlay'] ? buttonClick : null }\n                    style=\"--overlay-margin:{config['overlay-margin']}; --button-background:{config[\n                        'button-background'\n                    ]}; --header-color:{config['header-color']};\"\n                    class={`header ${config['title-card-button-overlay'] ?\n                        ' header-overlay' : ''}${open ? ' open' : ' close'}${config.animation ? ' animation ' + animationState : ''}`}\n                    aria-label=\"Toggle button\"\n                    bind:this={buttonElement}\n                >\n                    <ha-icon style=\"--arrow-color:{arrowColorConfigOrTemplate}\"\n                      icon={iconConfigOrTemplate}\n                      class={`ico${open && animationState !=='closing' ? ' flipped open' : ' close'}${config.animation ? ' animation ' + animationState : ''}`}>\n                    </ha-icon>\n                    {#if !config['title-card-clickable'] || config['title-card-button-overlay'] }\n                    <ha-ripple bind:this={ripple}></ha-ripple>\n                    {/if}\n                </button>\n            {/if}\n            {#if config['title-card-clickable'] && !config['title-card-button-overlay'] }\n            <ha-ripple bind:this={ripple}></ha-ripple>\n            {/if}\n        </div>\n    {:else}\n        {#if showButtonUsers}\n            <button onclick={buttonClick}\n                class={`header${open ? ' open' : ' close'}${config.animation ? ' animation ' + animationState : ''}`}\n                style=\"--header-width:100%; --button-background:{config['button-background']};--header-color:{config['header-color']};\"\n                bind:this={buttonElement}\n                >\n                <div class={`primary title${open ? ' open' : ' close'}`}>{titleConfigOrTemplate}</div>\n                <ha-icon style=\"--arrow-color:{arrowColorConfigOrTemplate}\"\n                  icon={iconConfigOrTemplate}\n                  class={`ico${open && animationState !=='closing' ? ' flipped open' : ' close'}${config.animation ? ' animation ' + animationState : ''}`}>\n                </ha-icon>\n                <ha-ripple bind:this={ripple}></ha-ripple>\n            </button>\n        {/if}\n    {/if}\n    {#if config.cards}\n        <div class=\"children-wrapper {config.animation ? 'animation ' + animationState : ''}{open ? ' open' : ' close'}\">\n            <div\n                style=\"--expander-card-display:{config['expander-card-display']};\n                --gap:{open && animationState !=='closing' ? config['expanded-gap'] : config.gap};\n                --child-padding:{open && animationState !=='closing' ? config['child-padding'] : '0px'};\"\n                class=\"children-container{open ? ' open' : ' close'}{config.animation ? ' animation ' + animationState : ''}\"\n            >\n                {#each config.cards as card (card)}\n                    <Card hass={hass}\n                        preview={open && preview}\n                        config={card}\n                        marginTop={config['child-margin-top']}\n                        open={open}\n                        animation={config.animation!}\n                        animationState={animationState}\n                        clearCardCss={config['clear-children']!}\n                    />\n                {/each}\n            </div>\n        </div>\n    {/if}\n    {#if userStyleTemplateOrConfig}\n        <!-- eslint-disable-next-line svelte/no-at-html-tags -->\n        {@html userStyleTemplateOrConfig}\n    {/if}\n</ha-card>\n\n<style>\n    .expander-card {\n        display: var(--expander-card-display,block);\n        gap: var(--gap);\n        padding: var(--padding);\n        background: var(--card-background,#fff);\n        -webkit-tap-highlight-color: transparent;\n    }\n    .expander-card.animation {\n        transition: gap 0.35s ease, background-color var(--background-animation-duration, 0) ease;\n    }\n    .children-wrapper {\n        display: flex;\n        flex-direction: column;\n    }\n    .children-wrapper.animation.opening,\n    .children-wrapper.animation.closing {\n        overflow: hidden;\n    }\n    .children-container.animation {\n        transition: padding 0.35s ease, gap 0.35s ease;\n    }\n    .children-container {\n        padding: var(--child-padding);\n        display: var(--expander-card-display,block);\n        gap: var(--gap);\n    }\n    .clear {\n        background: none !important;\n        background-color: transparent !important;\n        border-style: none !important;\n        box-shadow: none !important;\n    }\n\n    .title-card-header {\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        flex-direction: row;\n        position: relative;\n    }\n    .title-card-header.clickable {\n        cursor: pointer;\n        border-style: none;\n        border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));\n    }\n    .title-card-header-overlay {\n        display: block;\n    }\n    .title-card-container {\n        width: 100%;\n        padding: var(--title-padding);\n    }\n    .header {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        padding: 0.85em 0.85em;\n        background: var(--button-background);\n        border-style: none;\n        border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));\n        width: var(--header-width,auto);\n        color: var(--header-color,#fff);\n        cursor: pointer;\n        position: relative;\n        font-family: var(--ha-font-family-body);\n        font-size: var(--ha-font-size-m);\n    }\n    .header-overlay {\n        position: absolute;\n        top: 0;\n        right: 0;\n        margin: var(--overlay-margin);\n        height: var(--expander-card-overlay-height, auto);\n        z-index: 1;\n    }\n    .title-card-header-overlay.clickable  > .header-overlay {\n        width: calc(100% - var(--overlay-margin) * 2);\n        justify-content: flex-end;\n    }\n    .title-card-header-overlay.clickable > .title-card-container {\n        width: calc(100% - var(--overlay-margin) * 2);\n    }\n    .title {\n        width: 100%;\n        text-align: left;\n    }\n    .ico.animation {\n        transition-property: transform;\n        transition-duration: 0.35s;\n    }\n    .ico {\n        color: var(--arrow-color,var(--primary-text-color,#fff));\n    }\n\n    .flipped {\n        transform: rotate(var(--icon-rotate-degree,180deg));\n    }\n</style>\n"
  },
  {
    "path": "src/ExpanderCardEditor.ts",
    "content": "/* eslint-disable no-underscore-dangle */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { ExpanderCardEditorNulls, ExpanderCardEditorSchema, expanderCardEditorTemplates, styleSchemaCSS, styleSchemaObject, StyleSchemaTypes } from './editortype';\nimport { showTitleCardEditFormDialog, TitleCardEditFormParams } from './title-card/showTitleCardEditForm';\nimport { HomeAssistantUser } from './types';\n\nconst wdw = window; // NOSONAR es2019\n\nlet helpers = (wdw as any).cardHelpers;\nconst helperPromise = new Promise<void>((resolve) => {\n    if (helpers) resolve();\n    if ((wdw as any).loadCardHelpers) {\n        (wdw as any).loadCardHelpers().then((loadedHelpers: any) => {\n            helpers = loadedHelpers;\n            (wdw as any).cardHelpers = helpers;\n            resolve();\n        });\n    }\n});\n\nasync function fetchUsers(): Promise<void> {\n    const el = document.querySelector('home-assistant');\n    const hass = (el as any)?.hass;\n    if (!hass) return;\n    const users = await hass.callWS({ type: 'config/auth/list' });\n    return users\n        .filter((user: HomeAssistantUser) => !user.system_generated)\n        .map((user: HomeAssistantUser) => user.name);\n}\n\nconst loader = async (): Promise<any> => {\n    // create a temporary vertical-stack card to inherit from\n    const verticalStackCard = await helperPromise.then(() =>\n        helpers.createCardElement({ type: 'vertical-stack', cards: [] }));\n    // get its editor class once hui-vertical-stack-card is defined\n    // we need check hui-vertical-stack-card is defined as it is lazily loaded\n    const verticalStackEditor = await customElements.whenDefined('hui-vertical-stack-card')\n        .then(() => verticalStackCard.constructor.getConfigElement());\n    // fetch users\n    const users = await fetchUsers();\n    // return a new class that extends the vertical-stack editor\n    return class ExpanderCardEditor extends verticalStackEditor.constructor {\n        public constructor() {\n            super();\n            this._users = users;\n        }\n\n        // override setConfig to store config only and not assert stack editor config\n        // we also upgrade any old config here if needed\n        public setConfig(config: any): void {\n            this._config = config;\n        }\n\n        // define _schema getter to return our own schema\n        public get _schema(): any {\n            const schema = ExpanderCardEditorSchema;\n            const schemaJSON = JSON.stringify(schema);\n            const usersEscaped = this._users\n                .map((u: string) => u.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')) // NOSONAR es2019\n                .join('\",\"');\n            let populatedSchemaJSON = schemaJSON.replace(/\\[\\[users\\]\\]/g, usersEscaped); // NOSONAR es2019\n            // populate templates options, but only those not already in config\n            populatedSchemaJSON = populatedSchemaJSON.replace(/\\[\\[templates\\]\\]/g, // NOSONAR es2019\n                expanderCardEditorTemplates\n                    .filter((t: any) => !this._config.templates?.some((ct: any) => ct.template === t))\n                    .join('\",\"'));\n            // populate advanced styling schema\n            const styleSchemaType: StyleSchemaTypes = this._config.style && typeof this._config.style === 'object' ? StyleSchemaTypes.Object : StyleSchemaTypes.CSS;\n            const styleSchema = styleSchemaType === StyleSchemaTypes.CSS ? JSON.stringify(styleSchemaCSS) : JSON.stringify(styleSchemaObject);\n            populatedSchemaJSON = populatedSchemaJSON.replace(/\"\\[\\[style\\]\\]\"/g, styleSchema); // NOSONAR es2019\n            const populatedSchema = JSON.parse(populatedSchemaJSON);\n            return populatedSchema;\n        }\n\n        // _schema setter does nothing as we want to use our own schema\n        public set _schema(_) {\n            // do nothing\n        }\n\n        public connectedCallback(): void {\n            super.connectedCallback();\n\n            this.addEventListener('show-dialog', this.showDialogCallback.bind(this), true);\n        }\n\n        public disconnectedCallback(): void {\n            super.disconnectedCallback();\n            this.removeEventListener('show-dialog', this.showDialogCallback.bind(this), true);\n        }\n\n        private readonly showDialogCallback = (ev: CustomEvent): void => {\n            const isExpanderCardTitleCardSchema =\n                ev.detail?.dialogParams?.schema?.find((s: any) => s.name === 'expander_card_title_card_marker');\n            if (isExpanderCardTitleCardSchema) {\n                ev.stopPropagation();\n                // load the form-dialog element to make sure ha-dialog is defined\n                // then show the title card edit form dialog\n                if (ev.detail?.dialogImport) {\n                    ev.detail.dialogImport().then(async () => {\n                        const params: TitleCardEditFormParams = {\n                            title: 'Title card',\n                            config: this._config['title-card'] || {},\n                            submit: ev.detail?.dialogParams?.submit,\n                            cancel: ev.detail?.dialogParams?.cancel,\n                            submitText: ev.detail?.dialogParams?.submitText,\n                            cancelText: ev.detail?.dialogParams?.cancelText,\n                            lovelace: this.lovelace\n                        };\n                        await showTitleCardEditFormDialog(\n                            this as unknown as HTMLElement,\n                            params\n                        );\n                    });\n                }\n            }\n        };\n\n        // override _computeLabelCallback to show label or name\n        public _computeLabelCallback = (item: any): string => item.label ?? item.name ?? '';\n\n        // override _valueChanged to remove null values from config before storing and firing event\n        public _valueChanged = (ev: CustomEvent): void => {\n            const config = ev.detail.value;\n            const entries = Object.entries(ExpanderCardEditorNulls);\n            for (const [key, value] of entries) {\n                if (typeof value === 'object' && Array.isArray(value) && Array.isArray(config[key])) {\n                    if (JSON.stringify(config[key]) === JSON.stringify(value)) {\n                        delete config[key];\n                    }\n                    continue;\n                }\n                if (config[key] === value) {\n                    delete config[key];\n                }\n            }\n            this._config = config;\n            this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this._config } }));\n        };\n    };\n};\n\nexport const loadExpanderCardEditor = (async () => {\n    // Wait for scoped customElements registry to be set up\n    while (customElements.get('home-assistant') === undefined)\n        await new Promise((resolve) => wdw.setTimeout(resolve, 100));\n\n    if (!customElements.get('expander-card-editor')) {\n        const expanderCardEditor = await loader();\n        customElements.define('expander-card-editor', expanderCardEditor);\n    }\n});\n"
  },
  {
    "path": "src/configtype.ts",
    "content": "/*\nCopyright 2021-2022 Peter Repukat - FlatspotSoftware\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\nimport type { LovelaceCardConfig } from './types';\nexport interface ExpanderCardVariables  {\n    variable: string;\n    value_template: unknown;\n}\n\nexport interface ExpanderCardTemplates {\n    template: string;\n    value_template: unknown;\n}\nexport interface ExpanderConfig {\n    clear?: boolean;\n    'clear-children'?: boolean;\n    cards?: { type: string }[];\n    gap?: string;\n    'expanded-gap'?: string;\n    padding?: string;\n    title?: string;\n    'title-card'?: LovelaceCardConfig;\n    'title-card-padding'?: string;\n    'title-card-button-overlay'?: false;\n    'title-card-clickable'?: boolean;\n    'overlay-margin'?: string;\n    'child-padding'?: string;\n    'child-margin-top'?: string;\n    expanded?: boolean;\n    'expander-card-background'?: string;\n    'expander-card-background-expanded'?: string;\n    'header-color'?: string;\n    'button-background'?: string;\n    'arrow-color'?: string;\n    'expander-card-display'?: string;\n    'min-width-expanded'?: number;\n    'max-width-expanded'?: number;\n    icon?: string;\n    'storage-id'?: string;\n    'icon-rotate-degree'?: string;\n    'show-button-users'?: string[];\n    'start-expanded-users'?: string[];\n    animation?: boolean;\n    haptic?: 'success' | 'warning' | 'failure' | 'light' | 'medium' | 'heavy' | 'selection' | 'none';\n    'expander-card-id'?: string;\n    style?: string | Record<string, (string | Record<string, string>)[]>;\n    variables?: Record<string, ExpanderCardVariables>;\n    templates?: Record<string, ExpanderCardTemplates>;\n}\n\nexport interface ExpanderCardRawConfig {\n    'preview-expanded'?: boolean;\n}\n"
  },
  {
    "path": "src/editortype.ts",
    "content": "/* eslint-disable quote-props */\nimport { ExpanderConfig } from './configtype';\n\nexport const ExpanderCardEditorNulls: ExpanderConfig = {\n    icon: '',\n    'arrow-color': '',\n    'icon-rotate-degree': '',\n    'header-color': '',\n    'button-background': '',\n    'min-width-expanded': 0,\n    'max-width-expanded': 0,\n    'storage-id': '',\n    'expander-card-id': '',\n    'show-button-users': [],\n    'start-expanded-users': [],\n    'expander-card-background': '',\n    'expander-card-background-expanded': '',\n    'expander-card-display': '',\n    gap: '',\n    padding: '',\n    'expanded-gap': '',\n    'child-padding': '',\n    'child-margin-top': '',\n    'overlay-margin': '',\n    'title-card-padding': '',\n    'style': ''\n};\n\nexport const expanderCardEditorTemplates = [\n    'expanded',\n    'icon',\n    'arrow-color',\n    'title',\n    'style'\n];\n\nexport enum StyleSchemaTypes {\n    CSS = 'css',\n    Object = 'object'\n}\n\nexport const styleSchemaCSS = { name: 'style', label: 'CSS text', selector: { text: { multiline: true } } };\nexport const styleSchemaObject = { name: 'style', label: 'CSS structured object', selector: { object: {} } };\n\nconst iconSelector = { icon: {} };\nconst textSelector = { text: {} };\nconst booleanSelector = { boolean: {} };\nconst numberSelector = (unit_of_measurement: string) => ({\n    number: {\n        unit_of_measurement\n    }\n});\n\nconst iconField = (name: string, label: string) => ({\n    name,\n    label,\n    selector: iconSelector\n});\n\nconst textField = (name: string, label: string) => ({\n    name,\n    label,\n    selector: textSelector\n});\n\nconst booleanField = (name: string, label: string) => ({\n    name,\n    label,\n    selector: booleanSelector\n});\n\nconst numberField = (name: string, label: string, unit_of_measurement: string) => ({\n    name,\n    label,\n    selector: numberSelector(unit_of_measurement)\n});\n\n// See https://www.home-assistant.io/docs/blueprint/selectors\nexport const ExpanderCardEditorSchema = [\n    {\n        type: 'expandable',\n        label: 'Expander Card Settings',\n        icon: 'mdi:arrow-down-bold-box-outline',\n        schema: [\n            {\n                ...textField('title', 'Title')\n            },\n            {\n                ...iconField('icon', 'Icon')\n            },\n            {\n                type: 'expandable',\n                label: 'Expander control',\n                icon: 'mdi:cog-outline',\n                schema: [\n                    {\n                        type: 'grid',\n                        schema: [\n                            {\n                                ...booleanField('expanded', 'Start expanded')\n                            },\n                            {\n                                ...booleanField('animation', 'Enable animation')\n                            },\n                            {\n                                name: 'haptic',\n                                label: 'Haptic feedback',\n                                selector: {\n                                    select: {\n                                        mode: 'dropdown',\n                                        options: [\n                                            { value: 'light', label: 'Light' },\n                                            { value: 'medium', label: 'Medium' },\n                                            { value: 'heavy', label: 'Heavy' },\n                                            { value: 'success', label: 'Success' },\n                                            { value: 'warning', label: 'Warning' },\n                                            { value: 'failure', label: 'Failure' },\n                                            { value: 'selection', label: 'Selection' },\n                                            { value: 'none', label: 'None' }\n                                        ]\n                                    }\n                                }\n                            },\n                            {\n                                ...numberField('min-width-expanded', 'Min width expanded', 'px')\n                            },\n                            {\n                                ...numberField('max-width-expanded', 'Max width expanded', 'px')\n                            },\n                            {\n                                ...textField('storage-id', 'Storage ID')\n                            },\n                            {\n                                ...textField('expander-card-id', 'Expander card ID')\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                type: 'expandable',\n                label: 'Expander styling',\n                icon: 'mdi:palette-swatch',\n                schema: [\n                    {\n                        type: 'grid',\n                        schema: [\n                            {\n                                ...textField('arrow-color', 'Icon color')\n                            },\n                            {\n                                ...textField('icon-rotate-degree', 'Icon rotate degree')\n                            },\n                            {\n                                ...textField('header-color', 'Header color')\n                            },\n                            {\n                                ...textField('button-background', 'Button background color')\n                            },\n                            {\n                                ...textField('expander-card-background', 'Background')\n                            },\n                            {\n                                ...textField('expander-card-background-expanded', 'Background when expanded')\n                            },\n                            {\n                                ...textField('expander-card-display', 'Expander card display')\n                            },\n                            {\n                                ...booleanField('clear', 'Clear border and background')\n                            },\n                            {\n                                ...textField('gap', 'Gap')\n                            },\n                            {\n                                ...textField('padding', 'Padding')\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                type: 'expandable',\n                label: 'Card styling',\n                icon: 'mdi:palette-swatch-outline',\n                schema: [\n                    {\n                        type: 'grid',\n                        schema: [\n                            {\n                                ...textField('expanded-gap', 'Card gap')\n                            },\n                            {\n                                ...textField('child-padding', 'Card padding')\n                            },\n                            {\n                                ...textField('child-margin-top', 'Card margin top')\n                            },\n                            {\n                                ...booleanField('clear-children', 'Clear card border and background')\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                type: 'expandable',\n                label: 'Title card',\n                icon: 'mdi:subtitles-outline',\n                schema: [\n                    {\n                        // title-card selector. We will override Add and Edit to show card UI editor\n                        name: 'title-card',\n                        label: 'Title card',\n                        selector: {\n                            object: {\n                                label_field: 'type',\n                                fields: {\n                                    type: {\n                                        label: 'Card type',\n                                        required: true,\n                                        selector: { text: {} }\n                                    },\n                                    // include a marker field so we can identify schema in show-dialog event\n                                    expander_card_title_card_marker: {\n                                        required: false,\n                                        selector: { text: {} }\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    {\n                        type: 'grid',\n                        schema: [\n                            {\n                                ...booleanField('title-card-clickable', 'Make title card clickable to expand/collapse')\n                            },\n                            {\n                                ...booleanField('title-card-button-overlay', 'Overlay expand button on title card')\n                            },\n                            {\n                                ...textField('overlay-margin', 'Overlay margin')\n                            },\n                            {\n                                ...textField('title-card-padding', 'Title card padding')\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                type: 'expandable',\n                label: 'User settings',\n                icon: 'mdi:account-multiple-outline',\n                schema: [\n                    {\n                        type: 'grid',\n                        schema: [\n                            {\n                                name: 'show-button-users',\n                                label: 'Show button users',\n                                selector: {\n                                    select: {\n                                        multiple: true,\n                                        mode: 'dropdown',\n                                        custom: true, // to allow for unknown users\n                                        options: ['[[users]]'] // to be populated dynamically\n                                    }\n                                }\n                            },\n                            {\n                                name: 'start-expanded-users',\n                                label: 'Start expanded users',\n                                selector: {\n                                    select: {\n                                        multiple: true,\n                                        mode: 'dropdown',\n                                        custom: true, // to allow for unknown users\n                                        options: ['[[users]]'] // to be populated dynamically\n                                    }\n                                }\n                            }\n                        ]\n                    }\n                ]\n            },\n            {\n                type: 'expandable',\n                label: 'Advanced styling',\n                icon: 'mdi:brush-outline',\n                schema: ['[[style]]'] // to be populated dynamically\n            },\n            {\n                type: 'expandable',\n                label: 'Advanced templates',\n                icon: 'mdi:code-brackets',\n                schema: [\n                    {\n                        type: 'expandable',\n                        label: 'Variables',\n                        icon: 'mdi:variable',\n                        schema: [\n                            {\n                                name: 'variables',\n                                label: 'Variables',\n                                selector: {\n                                    object: {\n                                        label_field: 'variable',\n                                        multiple: true,\n                                        fields: {\n                                            variable: {\n                                                label: 'Variable name',\n                                                required: true,\n                                                selector: { text: {} }\n                                            },\n                                            value_template: {\n                                                label: 'Value template',\n                                                required: true,\n                                                selector: { text: { multiline: true } }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        ]\n                    },\n                    {\n                        type: 'expandable',\n                        label: 'Templates',\n                        icon: 'mdi:code-brackets',\n                        schema: [\n                            {\n                                name: 'templates',\n                                label: 'Templates',\n                                selector: {\n                                    object: {\n                                        label_field: 'template',\n                                        multiple: true,\n                                        fields: {\n                                            template: {\n                                                label: 'Config item',\n                                                required: true,\n                                                selector: {\n                                                    select: {\n                                                        mode: 'dropdown',\n                                                        custom_value: true, // to allow for current templates not in dropdown\n                                                        sort: true,\n                                                        options: ['[[templates]]'] // to be populated dynamically\n                                                    }\n                                                }\n                                            },\n                                            value_template: {\n                                                label: 'Value template',\n                                                required: true,\n                                                selector: { template: {} }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        ]\n                    }\n                ]\n            }\n        ]\n    }\n];\n"
  },
  {
    "path": "src/helpers/compute-card-size.ts",
    "content": "import { HuiCard } from '../types';\nimport { TimeoutError } from './promise-timeout';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const promiseTimeout = (ms: number, promise: Promise<any> | any) => { // NOSONAR\n    const timeout = new Promise((_resolve, reject) => {\n        setTimeout(() => {\n            reject(new TimeoutError(ms));\n        }, ms);\n    });\n\n    // Returns a race between our timeout and the passed in promise\n    return Promise.race([promise, timeout]);\n};\n\nexport const computeCardSize = (\n    card: HuiCard\n): number | Promise<number> => {\n    if (typeof card.getCardSize === 'function') {\n        try {\n            return promiseTimeout(500, card.getCardSize()).catch(\n                () => 1\n            ) as Promise<number>;\n        } catch {\n            return 1;\n        }\n    }\n    if (customElements.get(card.localName)) {\n        return 1;\n    }\n    return customElements\n        .whenDefined(card.localName)\n        .then(() => computeCardSize(card));\n};\n"
  },
  {
    "path": "src/helpers/forward-haptic.ts",
    "content": "// Allowed types are from iOS HIG.\n// https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics\n// Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible.\nexport type HapticType = 'success' | 'warning' | 'failure' | 'light' | 'medium' | 'heavy' | 'selection' | 'none';\n\ndeclare global {\n    // for fire event\n    interface HASSDomEvents {\n        haptic: HapticType;\n    }\n}\n\nexport const forwardHaptic = (node: HTMLElement, hapticType: HapticType) => {\n    node.dispatchEvent?.(\n        new CustomEvent('haptic',\n            { detail: hapticType, bubbles: true, composed: true }\n        )\n    );\n};\n"
  },
  {
    "path": "src/helpers/ha-dialog-styles.ts",
    "content": "import { css } from 'lit';\n\nexport const haStyleDialog = css`\n  ha-dialog,\n  ha-adaptive-dialog {\n    --mdc-dialog-min-width: 400px;\n    --mdc-dialog-max-width: 600px;\n    --mdc-dialog-max-width: min(600px, 95vw);\n    --justify-action-buttons: space-between;\n    --dialog-container-padding: var(--safe-area-inset-top, 0)\n      var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)\n      var(--safe-area-inset-left, 0);\n    --dialog-surface-padding: 0px;\n  }\n\n  ha-dialog .form,\n  ha-adaptive-dialog .form {\n    color: var(--primary-text-color);\n  }\n\n  a {\n    color: var(--primary-color);\n  }\n\n  /* make dialog fullscreen on small screens */\n  @media all and (max-width: 450px), all and (max-height: 500px) {\n    ha-dialog,\n    ha-adaptive-dialog {\n      --mdc-dialog-min-width: 100vw;\n      --mdc-dialog-max-width: 100vw;\n      --mdc-dialog-min-height: 100vh;\n      --mdc-dialog-min-height: 100svh;\n      --mdc-dialog-max-height: 100vh;\n      --mdc-dialog-max-height: 100svh;\n      --dialog-container-padding: 0px;\n      --dialog-surface-padding: var(--safe-area-inset-top, 0)\n        var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)\n        var(--safe-area-inset-left, 0);\n      --vertical-align-dialog: flex-end;\n    }\n    ha-dialog {\n      --ha-dialog-border-radius: var(--ha-border-radius-square);\n    }\n  }\n  .error {\n    color: var(--error-color);\n  }\n`;\n\nexport const haStyleDialogFixedTop = css`\n  ha-dialog,\n  ha-adaptive-dialog {\n    /* Pin dialog to top so it doesn't jump when content changes size */\n    --vertical-align-dialog: flex-start;\n    --dialog-surface-margin-top: var(--ha-space-10);\n    --mdc-dialog-max-height: calc(\n      100vh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(\n          --safe-area-inset-y,\n          0px\n        )\n    );\n    --mdc-dialog-max-height: calc(\n      100svh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(\n          --safe-area-inset-y,\n          0px\n        )\n    );\n    --ha-dialog-max-height: calc(\n      100vh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(\n          --safe-area-inset-y,\n          0px\n        )\n    );\n    --ha-dialog-max-height: calc(\n      100svh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(\n          --safe-area-inset-y,\n          0px\n        )\n    );\n  }\n\n  @media all and (max-width: 450px), all and (max-height: 500px) {\n    ha-dialog,\n    ha-adaptive-dialog {\n      /* When in fullscreen, dialog should be attached to top */\n      --dialog-surface-margin-top: 0px;\n      --mdc-dialog-min-height: 100vh;\n      --mdc-dialog-min-height: 100svh;\n      --mdc-dialog-max-height: 100vh;\n      --mdc-dialog-max-height: 100svh;\n      --ha-dialog-max-height: 100vh;\n      --ha-dialog-max-height: 100svh;\n    }\n  }\n`;\n"
  },
  {
    "path": "src/helpers/promise-timeout.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nexport class TimeoutError extends Error {\n    public timeout: number;\n\n    // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n    constructor(timeout: number, ...params: undefined[]) {\n        super(...params);\n\n        // Maintains proper stack trace for where our error was thrown (only available on V8)\n        if ((Error as any).captureStackTrace) {\n            (Error as any).captureStackTrace(this, TimeoutError);\n        }\n\n        this.name = 'TimeoutError';\n        // Custom debugging information\n        this.timeout = timeout;\n        this.message = `Timed out in ${timeout} ms.`;\n    }\n}\n\nexport const promiseTimeout = (ms: number, promise: Promise<any> | any) => { // NOSONAR\n    const timeout = new Promise((_resolve, reject) => {\n        setTimeout(() => {\n            reject(new TimeoutError(ms));\n        }, ms);\n    });\n\n    // Returns a race between our timeout and the passed in promise\n    return Promise.race([promise, timeout]);\n};\n"
  },
  {
    "path": "src/helpers/raw-config.ts",
    "content": "import { HAQuerySelector } from 'home-assistant-query-selector';\nimport { ExpanderCardRawConfig } from '../configtype';\nimport { HuiRoot } from '../types';\n\nconst instance = new HAQuerySelector();\nlet rawConfig: ExpanderCardRawConfig = {};\n\ninstance.addEventListener('onLovelacePanelLoad', ({ detail }) => {\n    detail.HUI_ROOT.element.then((root) => {\n        const lovelaceConfig = (root as HuiRoot)?.lovelace;\n        if (lovelaceConfig?.config) {\n            rawConfig = lovelaceConfig.config['expander-card'] || {};\n        }\n    }).catch(() => {\n        rawConfig = {};\n    }).finally(() => {\n        document.body.dispatchEvent(new CustomEvent('expander-card-raw-config-updated', {\n            detail: { rawConfig }, bubbles: true, composed: true\n        }));\n    });\n});\ninstance.listen();\n\nexport const getDashboardRawConfig = (): ExpanderCardRawConfig => rawConfig;\n"
  },
  {
    "path": "src/helpers/style-converter.ts",
    "content": "/**\n * Converts a style value (string or object) to a CSS string.\n * @param style - The style value to convert, can be a string or an object with CSS selectors as keys\n *                and CSS property arrays as values (either strings or objects)\n * @returns A CSS string suitable for injection into a <style> tag\n */\nexport const styleToString = (style: string | Record<string, (string | Record<string, string>)[]> | undefined): string | null => {\n    if (!style) {\n        return null;\n    }\n\n    if (typeof style === 'string') {\n        return style;\n    }\n\n    // Convert nested object to CSS string\n    // Object format: { \".selector\": [\"property1: value\", ...] } or { \".selector\": [{\"property1\": \"value\"}, ...] }\n    return Object.entries(style)\n        .map(([selector, properties]) => {\n            // Skip if properties is not an array\n            if (!Array.isArray(properties)) {\n                return null;\n            }\n            const rules = properties\n                .map((property) => {\n                    // Handle both string format and object format\n                    if (typeof property === 'string') {\n                        return `  ${property};`;\n                    }\n                    // For objects, get the first key-value pair\n                    const [key, value] = Object.entries(property)[0];\n                    return `  ${key}: ${value};`;\n                })\n                .join('\\n');\n            return `${selector} {\\n${rules}\\n}`;\n        })\n        .filter((rule) => rule !== null)\n        .join('\\n');\n};\n"
  },
  {
    "path": "src/helpers/templates.ts",
    "content": "import HomeAssistantJavaScriptTemplates, { HomeAssistant, HomeAssistantJavaScriptTemplatesRenderer } from 'home-assistant-javascript-templates';\nimport { ExpanderCardEventDetail } from '../types';\n\nexport function getJSTemplateRenderer(variables: Record<string, unknown> = {}, refs: Record<string, unknown> = {}): Promise<HomeAssistantJavaScriptTemplatesRenderer> {\n    return new HomeAssistantJavaScriptTemplates(\n        document.querySelector('home-assistant') as HomeAssistant,\n        {\n            autoReturn: false,\n            variables,\n            refs,\n            refsVariableName: 'variables'\n        }\n    ).getRenderer();\n}\n\nexport function isJSTemplate(template: unknown): boolean {\n    if (!template || typeof template !== 'string') return false;\n    return String(template).trim().startsWith('[[[') && String(template).trim().endsWith(']]]');\n}\n\nexport function renderJSTemplate(\n    templatesRenderer: Promise<HomeAssistantJavaScriptTemplatesRenderer>,\n    template: string,\n    variables: Record<string, unknown> = {}) {\n    if (!isJSTemplate(template)) {\n        throw new Error('Not a valid JS template');\n    }\n    template = String(template).trim().slice(3, -3);\n    void templatesRenderer.then((renderer) => renderer.renderTemplate(template, { variables } ));\n}\n\nexport function trackJSTemplate(\n    templatesRenderer: Promise<HomeAssistantJavaScriptTemplatesRenderer>,\n    callback: (result: unknown) => void,\n    template: string,\n    variables: Record<string, unknown> = {}): Promise<(() => void)> {\n    if (!isJSTemplate(template)) {\n        throw new Error('Not a valid JS template');\n    }\n    template = String(template).trim().slice(3, -3);\n    return templatesRenderer.then((renderer) => renderer.trackTemplate(template, callback, { variables }));\n}\n\nexport function setJSTemplateRef(\n    templatesRenderer: Promise<HomeAssistantJavaScriptTemplatesRenderer>,\n    refName: string,\n    refValue: unknown) {\n    void templatesRenderer.then((renderer) => {\n        renderer.refs[refName] = refValue;\n    });\n}\n\nfunction eventHandler(templatesRenderer: Promise<HomeAssistantJavaScriptTemplatesRenderer>, event: Event) {\n    void templatesRenderer.then((renderer) => {\n        const detail = (event as CustomEvent).detail as ExpanderCardEventDetail;\n        Object.keys(detail).forEach((key) => {\n            const property = detail[key].property;\n            const value = detail[key].value;\n            const variableName = `${key}_${property}`;\n            renderer.refs[variableName] = value;\n        });\n    });\n}\n\nexport function trackJSTemplateEvent(\n    templatesRenderer: Promise<HomeAssistantJavaScriptTemplatesRenderer>,\n    eventName: string): () => void {\n    const boundEventHandler: EventListener = eventHandler.bind(null, templatesRenderer);\n    document.addEventListener(eventName, boundEventHandler);\n    return () => {\n        document.removeEventListener(eventName, boundEventHandler);\n    };\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "/*\nCopyright 2021-2022 Peter Repukat - FlatspotSoftware\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\nexport { default } from './ExpanderCard.svelte';\nimport { version } from '../package.json';\nimport { TitleCardEditForm } from './title-card/titleCardEditForm';\ndeclare global {\n    interface Window {\n        customCards?: {\n            type: string;\n            name: string;\n            preview: boolean;\n            description: string;\n        }[];\n    }\n}\n\n/* eslint no-console: 0 */\nconsole.info(\n    `%c  Expander-Card \\n%c Version ${version}`,\n    'color: orange; font-weight: bold; background: black',\n    'color: white; font-weight: bold; background: dimgray'\n);\n\nwindow.customCards = window.customCards || []; // NOSONAR es2019\nwindow.customCards.push(...[ // NOSONAR es2019\n    {\n        type: 'expander-card',\n        name: 'Expander Card',\n        preview: true,\n        description: 'Expander card'\n    }\n]);\n\nif (!customElements.get('expander-card-title-card-edit-form')) {\n    customElements.define('expander-card-title-card-edit-form', TitleCardEditForm);\n}\n"
  },
  {
    "path": "src/title-card/showTitleCardEditForm.ts",
    "content": "export type TitleCardConfig = {\n    type?: string;\n} & Record<string, unknown>;\n\nexport interface TitleCardEditFormParams {\n    title: string;\n    config?: TitleCardConfig;\n    submit?: (config: TitleCardConfig) => void;\n    cancel?: () => void;\n    submitText?: string;\n    cancelText?: string;\n    lovelace?: unknown;\n};\n\nexport const showTitleCardEditFormDialog = (\n    element: HTMLElement,\n    dialogParams: TitleCardEditFormParams\n) =>\n    new Promise<TitleCardConfig | null>((resolve) => {\n        const origCancel = dialogParams.cancel;\n        const origSubmit = dialogParams.submit;\n\n        element.dispatchEvent(\n            new CustomEvent('show-dialog',\n                {\n                    detail:{\n                        dialogTag: 'expander-card-title-card-edit-form',\n                        dialogImport: () => customElements.whenDefined('expander-card-title-card-edit-form'),\n                        dialogParams: {\n                            ...dialogParams,\n                            cancel: () => {\n                                resolve(null);\n                                if (origCancel) {\n                                    origCancel();\n                                }\n                            },\n                            submit: (data: TitleCardConfig) => {\n                                resolve(data);\n                                if (origSubmit) {\n                                    origSubmit(data);\n                                }\n                            }\n                        }\n                    },\n                    bubbles: true,\n                    composed: true\n                }\n            )\n        );\n    });\n"
  },
  {
    "path": "src/title-card/titleCardEditForm.ts",
    "content": "/* eslint-disable no-underscore-dangle */\nimport { css, html, LitElement, nothing } from 'lit';\nimport { customElement, property, query, state } from 'lit/decorators.js';\nimport { HomeAssistant, HuiElementEditor } from '../types';\nimport { TitleCardConfig, TitleCardEditFormParams } from './showTitleCardEditForm';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { haStyleDialog, haStyleDialogFixedTop } from '../helpers/ha-dialog-styles';\n\nexport const CUSTOM_TYPE_PREFIX = 'custom:';\n\nconst isCustomType = (type: string) =>\n    type.startsWith(CUSTOM_TYPE_PREFIX);\n\nconst getCustomCardEntry = (type: string) =>\n    window.customCards?.find((card) => card.type === type); // NOSONAR es2019\n\nconst stripCustomPrefix = (type: string) =>\n    type.replace(CUSTOM_TYPE_PREFIX, '');\n\n@customElement('expander-card-title-card-edit-form')\nexport class TitleCardEditForm extends LitElement {\n    @property({ attribute: false }) public hass?: HomeAssistant;\n    @property({ type: Boolean, reflect: true }) public large = false;\n    @property({ attribute: false }) public lovelace?: unknown;\n\n    @state() private _params?: TitleCardEditFormParams;\n    @state() private _config: TitleCardConfig = {};\n    @state() private _cardGUIMode = true;\n    @state() private _cardGUIModeAvailable = true;\n    @state() private _error = false;\n\n    @query('hui-card-element-editor') private _cardEditorEl?: HuiElementEditor; // NOSONAR Lit @query decorator updates\n\n    public async showDialog(params: TitleCardEditFormParams): Promise<void> {\n        this._params = params;\n        this._config = params.config ?? {};\n        this.lovelace = params.lovelace;\n        this.large = false;\n    }\n\n    public closeDialog() {\n        this._params = undefined;\n        this._config = {};\n        this.dispatchEvent(new CustomEvent('dialog-closed', { detail: { dialog: this.localName } }));\n        return true;\n    }\n\n    private _submit(): void {\n        this._params?.submit?.(this._config);\n        this.closeDialog();\n    }\n\n    private _cancel(): void {\n        this._params?.cancel?.();\n        this.closeDialog();\n    }\n\n    private _enlarge() {\n        this.large = !this.large;\n    }\n\n    private _ignoreKeydown(ev: KeyboardEvent): void {\n        ev.stopPropagation();\n    }\n\n    protected render() {\n        if (!this._params || !this.hass) {\n            return nothing;\n        }\n\n        const disableSave = (!this._config.type || this._error) || undefined;\n        let heading: string = this._params.title ?? '';\n        if (this._config.type) {\n            let cardName: string | undefined;\n            if (isCustomType(this._config.type)) {\n            // prettier-ignore\n                cardName = getCustomCardEntry(\n                    stripCustomPrefix(this._config.type)\n                )?.name;\n                // Trim names that end in \" Card\" so as not to redundantly duplicate it\n                if (cardName?.toLowerCase().endsWith(' card')) {\n                    cardName = cardName.substring(0, cardName.length - 5);\n                }\n            } else {\n                cardName = this.hass.localize(\n                    `ui.panel.lovelace.editor.card.${this._config.type}.name`\n                );\n            }\n            heading = `${heading} - ${this.hass.localize(\n                'ui.panel.lovelace.editor.edit_card.typed_header',\n                { type: cardName }\n            )}`;\n        }\n\n        return html`\n        <ha-dialog\n            open\n            scrimClickAction\n            escapeKeyAction\n            @keydown=${this._ignoreKeydown.bind(this)}\n            @closed=${ this._cancel.bind(this) }\n            .heading=${heading}\n            .width=${this.large ? 'full' : 'large'}\n        >\n            <ha-dialog-header slot=\"header\">\n                <ha-icon-button\n                    slot=\"navigationIcon\"\n                    dialogAction=\"cancel\"\n                    .label=${this.hass.localize('ui.common.close')}\n                >\n                    <ha-icon .icon=${'mdi:close'}></ha-icon>\n                </ha-icon-button>\n                <span slot=\"title\" @click=${this._enlarge.bind(this)}>${heading}</span>\n            </ha-dialog-header>\n            ${this._renderCardEditor()}\n            <ha-dialog-footer slot=\"footer\">\n                <div slot=\"primaryAction\" @click=${this._submit.bind(this)}>\n                    <ha-button\n                        appearance=\"plain\"\n                        size=\"small\"\n                        @click=${this._cancel.bind(this)}\n                        dialogInitialFocus\n                    >\n                        ${this._params.cancelText || this.hass.localize('ui.common.cancel')}\n                    </ha-button>\n                    <ha-button\n                        size=\"small\"\n                        @click=${this._submit.bind(this)} \n                        disabled=${ifDefined(disableSave)}\n                    >\n                        ${this._params.submitText || this.hass.localize('ui.common.save')}\n                    </ha-button>\n                </div>\n                ${this._renderCardEditorActions()}\n            </ha-dialog-footer>\n        </ha-dialog>\n        `;\n    }\n\n    private _toggleCardMode() {\n        this._cardEditorEl?.toggleMode();\n    }\n\n    private _deleteCard() {\n        this._config = {};\n    }\n\n    private _cardConfigChanged(ev: CustomEvent) {\n        ev.stopPropagation();\n        this._config = { ...ev.detail.config };\n        this._error = ev.detail.error;\n        this._cardGUIModeAvailable = ev.detail.guiModeAvailable;\n    }\n\n    private _cardGUIModeChanged(ev: CustomEvent) {\n        ev.stopPropagation();\n        this._cardGUIMode = ev.detail.guiMode;\n        this._cardGUIModeAvailable = ev.detail.guiModeAvailable;\n    }\n\n    private _renderCardEditorActions() {\n        if (!this._config.type) {\n            return nothing;\n        }\n        const cardMode: string = this.hass!.localize(\n            (!this._cardEditorEl || this._cardGUIMode)\n                ? 'ui.panel.lovelace.editor.edit_card.show_code_editor'\n                : 'ui.panel.lovelace.editor.edit_card.show_visual_editor'\n        );\n        return html`\n            <div slot=\"secondaryAction\">\n                <ha-button\n                appearance=\"plain\"\n                size=\"small\"\n                @click=${this._toggleCardMode.bind(this)}\n                .disabled=${!this._cardGUIModeAvailable}\n                >\n                    ${cardMode}\n                </ha-button>\n                <ha-button\n                appearance=\"plain\"\n                size=\"small\"\n                @click=${this._deleteCard.bind(this)}\n                >\n                    Change card\n                </ha-button>\n            </div>\n        `;\n    }\n\n    private _renderCardEditor() {\n        const cardBlurClass = this._error ? 'blur' : '';\n        const cardRenderBlurSpinner = this._error\n            ? html` <ha-spinner aria-label=\"Can't update card\"></ha-spinner> `\n            : '';\n        return html`\n        ${this._config.type\n            ? html`\n            <div class=\"content\">\n                <div class=\"element-editor\">\n                    <hui-card-element-editor\n                        .hass=${this.hass}\n                        .lovelace=${this.lovelace}\n                        .value=${this._config}\n                        @config-changed=${this._cardConfigChanged.bind(this)}\n                        @GUImode-changed=${this._cardGUIModeChanged.bind(this)}\n                    ></hui-card-element-editor>\n                </div>\n                <div class=\"element-preview\">\n                    <hui-card\n                        .hass=${this.hass}\n                        .config=${this._config}\n                        preview\n                        class=${cardBlurClass}\n                    ></hui-card>\n                    ${cardRenderBlurSpinner}\n                </div>\n            </div>\n            `\n            : html`\n            <hui-card-picker\n                .hass=${this.hass}\n                .lovelace=${this.lovelace}\n                @config-changed=${this._cardConfigChanged.bind(this)}\n            ></hui-card-picker>\n            `}\n        `;\n    }\n\n    public static readonly styles = [\n        haStyleDialog,\n        haStyleDialogFixedTop,\n        css`\n            :host {\n                --code-mirror-max-height: calc(100vh - 176px);\n            }\n\n            ha-dialog {\n                --dialog-z-index: 6;\n                --dialog-content-padding: var(--ha-space-2);\n            }\n\n            .content {\n                width: 100%;\n                max-width: 100%;\n            }\n\n            @media all and (max-width: 450px), all and (max-height: 500px) {\n            /* overrule the ha-style-dialog max-height on small screens */\n                .content {\n                    width: 100%;\n                    max-width: 100%;\n                }\n            }\n\n            @media all and (min-width: 451px) and (min-height: 501px) {\n                :host([large]) .content {\n                    max-width: none;\n                }\n            }\n\n            .center {\n                margin-left: auto;\n                margin-right: auto;\n            }\n\n            .content {\n                display: flex;\n                flex-direction: column;\n            }\n\n            .content hui-card {\n                display: block;\n                padding: 4px;\n                margin: 0 auto;\n                max-width: 390px;\n            }\n            .content hui-section {\n                display: block;\n                padding: 4px;\n                margin: 0 auto;\n                max-width: var(--ha-view-sections-column-max-width, 500px);\n            }\n            .content .element-editor {\n                margin: 0 10px;\n            }\n\n            @media (min-width: 1000px) {\n                .content {\n                    flex-direction: row;\n                }\n                .content > * {\n                    flex-basis: 0;\n                    flex-grow: 1;\n                    flex-shrink: 1;\n                    min-width: 0;\n                }\n                .content hui-card {\n                    padding: 8px 10px;\n                    margin: auto 0px;\n                    max-width: 500px;\n                }\n                .content hui-section {\n                    padding: 8px 10px;\n                    margin: auto 0px;\n                    max-width: var(--ha-view-sections-column-max-width, 500px);\n                }\n            }\n            .hidden {\n                display: none;\n            }\n            .element-editor {\n                margin-bottom: 8px;\n            }\n            .blur {\n                filter: blur(2px) grayscale(100%);\n            }\n            .element-preview {\n                position: relative;\n                height: max-content;\n                background: var(--primary-background-color);\n                padding: 4px;\n                border-radius: var(--ha-border-radius-sm);\n                position: sticky;\n                top: 0;\n            }\n            .element-preview ha-spinner {\n                top: calc(50% - 24px);\n                left: calc(50% - 24px);\n                position: absolute;\n                z-index: 10;\n            }\n            hui-card {\n                padding-top: 8px;\n                margin-bottom: 4px;\n                display: block;\n                width: 100%;\n                box-sizing: border-box;\n            }\n\n            [slot=\"primaryAction\"] {\n                gap: var(--ha-space-2);\n                display: flex;\n            }\n            [slot=\"secondaryAction\"] {\n                gap: var(--ha-space-2);\n                display: flex;\n                margin-left: 0px;\n                margin-right: auto;\n                margin-inline-end: auto;\n                margin-inline-start: initial;\n            }\n            [slot=\"navigationIcon\"] {\n                --ha-icon-display: block;\n            }\n        `];\n}\n\ndeclare global {\n    interface HTMLElementTagNameMap {\n        'expander-card-title-card-edit-form': TitleCardEditForm;\n    }\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export type AnimationState = 'opening' | 'closing' | 'idle';\n\nexport interface HomeAssistantUser {\n    id: string;\n    name: string;\n    is_admin: boolean;\n    is_owner: boolean;\n    system_generated: boolean;\n}\nexport interface HomeAssistant {\n    user?: HomeAssistantUser;\n    [key: string]: unknown;\n    localize(key: string, ...args: unknown[]): string;\n}\n\nexport interface LovelaceCardConfig {\n    index?: number;\n    view_index?: number;\n    type: string;\n    disabled?: boolean;\n    [key: string]: unknown;\n}\n\nexport interface LovelaceCard extends HTMLElement {\n    hass?: HomeAssistant;\n    isPanel?: boolean;\n    preview?: boolean;\n    getCardSize(): number | Promise<number>;\n    config?: LovelaceCardConfig;\n}\n\nexport interface HuiCard extends LovelaceCard {\n    load(): void;\n    _element?: LovelaceCard;\n}\n\nexport interface HaRipple extends HTMLElement {\n    disabled?: boolean;\n    startPressAnimation(event?: Event): void;\n    endPressAnimation(): void;\n}\n\nexport interface HuiRoot extends HTMLElement {\n    lovelace?: {\n        config: {\n            [key: string]: unknown;\n        };\n    };\n}\n\nexport interface ExpanderCardLlCustomEventDetail {\n    'expander-card-id'?: string;\n    action?: 'open' | 'close' | 'toggle';\n}\n\nexport interface ExpanderCardEventDetail {\n    [key: string]: {\n        'property': string;\n        'value': unknown;\n    };\n}\n\nexport interface HuiElementEditor extends HTMLElement {\n    lovelace?: unknown;\n    hasError?: boolean;\n    toggleMode(): void;\n}\n"
  },
  {
    "path": "svelte.config.js",
    "content": "import adapter from '@sveltejs/adapter-node';\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\t// Consult https://kit.svelte.dev/docs/integrations#preprocessors\n\t// for more information about preprocessors\n    preprocess: vitePreprocess({\n        sourceMap: false,\n\tpostcss: true,\n    \tscript: true\n    }),\n    compilerOptions: {\n        // runes: true,\n        customElement: true,\n        enableSourcemap: true,\n    },\n\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.\n\t\t// If your environment is not supported or you settled on a specific environment, switch out the adapter.\n\t\t// See https://kit.svelte.dev/docs/adapters for more information about adapters.\n        adapter: adapter(),\n        alias: {\n            '$': 'src',\n            '$components': 'src/lib/components',\n            '$assets': 'src/assets',\n\n        },\n    },\n};\n\nexport default config;\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"Expander-card-specific pytest configuration.\n\nSets environment-variable defaults consumed by the ``ha_testcontainer``\npytest plugin so that tests run against a properly configured Home Assistant\ninstance with the expander card loaded.\n\nAll session-scoped HA container fixtures (``ha``, ``ha_url``, ``ha_token``,\n``ha_lovelace_url_path``, ``ha_browser_context``, ``ha_page``) are provided\nautomatically by ``ha_testcontainer.pytest_plugin``, which is registered via\nthe ``pytest11`` entry-point and requires no explicit import.\n\nEnvironment variables\n---------------------\nHA_VERSION\n    Docker image tag to use.  Defaults to ``stable``.\n    Set to ``beta``, ``dev``, or a pinned version such as ``2024.6.0``.\nHA_URL\n    Base URL of a **pre-running** Home Assistant instance (e.g.\n    ``http://localhost:12345``).  When set together with ``HA_TOKEN``, the\n    test session connects to that instance instead of starting a new Docker\n    container — eliminating the boot-time overhead for fast iterative work.\n    Start a persistent instance with ``make ha_up``.\nHA_TOKEN\n    Long-lived access token for the pre-running HA instance.  Required when\n    ``HA_URL`` is set.\nHA_PLUGINS_YAML\n    Path to an alternative ``plugins.yaml``.  Defaults to\n    ``tests/plugins.yaml``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Expander-card-specific env-var defaults — consumed by ha_testcontainer\n# ---------------------------------------------------------------------------\n# Set at module import time so they are visible to the session-scoped ``ha``\n# fixture before it runs.  os.environ.setdefault() leaves any externally-set\n# value (e.g. from ``source .ha_env``) unchanged.\n\n_REPO_ROOT = Path(__file__).parent.parent\n\nos.environ.setdefault(\"HA_CONFIG_PATH\", str(_REPO_ROOT / \"tests\" / \"ha-config\"))  # NOSONAR\nos.environ.setdefault(\"HA_PLUGINS_YAML\", str(_REPO_ROOT / \"tests\" / \"plugins.yaml\"))  # NOSONAR\n"
  },
  {
    "path": "tests/doc-image-audit-exclusions.txt",
    "content": "# Doc image audit exclusions.\n#\n# Images listed here are intentionally hand-crafted and are NOT auto-generated\n# by a doc_image:/doc_animation: scenario.  One repo-relative path per line.\n# Lines starting with # and blank lines are ignored.\n#\n# To convert a hand-crafted image to an auto-generated one, add a\n# doc_image:/doc_animation: key to a scenario YAML and remove the entry here.\n\n# Existing hand-crafted assets committed before automated doc-image generation\n# was introduced.  These may be replaced with generated versions over time.\ndocs/source/chapter/assets/example.gif\ndocs/source/chapter/assets/lights_overlay_button.png\ndocs/source/chapter/assets/nested.png\ndocs/source/chapter/assets/clear_router.png\ndocs/source/chapter/assets/editor.png\ndocs/source/chapter/assets/logo.png\ndocs/source/chapter/assets/pplogo.png\ndocs/source/chapter/assets/styling1.png\ndocs/source/chapter/assets/styling2.png\ndocs/source/chapter/assets/styling3.png\n"
  },
  {
    "path": "tests/ha-config/configuration.yaml",
    "content": "homeassistant:\n  country: US\n  unit_system: metric\n  time_zone: UTC\n  customize: !include customize.yaml\n\ndefault_config:\n\ndemo:\n\nlovelace:\n  # Dashboard configs are managed by HA storage (writable at runtime via\n  # the WebSocket API).  Resources (JS modules) use YAML so that\n  # ha_testcontainer can register third-party plugins by writing\n  # lovelace_resources.yaml before the container starts.\n  mode: storage\n  resource_mode: yaml\n  resources: !include lovelace_resources.yaml\n"
  },
  {
    "path": "tests/ha-config/customize.yaml",
    "content": "# Entity customization overrides.\n# Add any per-entity friendly names, icons, or hidden flags here.\n"
  },
  {
    "path": "tests/ha-config/www/.gitkeep",
    "content": "# expander-card.js is made available here for tests by the Home Assistant test container/plugin setup\n# (see plugins.yaml), rather than being copied by tests/conftest.py.\n# Do not commit expander-card.js — it is a build artifact (see .gitignore).\n"
  },
  {
    "path": "tests/plugins.yaml",
    "content": "# Lovelace plugins to load on every HA instance startup.\n#\n# Local plugin (this card):\n#   local_path - path to the built JS file, relative to this file\n#   filename   - filename to write inside www/\n#\n# Third-party plugins:\n#   repo     - GitHub repository in \"owner/name\" format\n#   asset    - name of the JS asset to download from the latest release\n#   filename - filename to write inside www/ (usually the same as `asset`)\n#\n# All entries are handled automatically by ha_testcontainer (>=2.1):\n#   1. Copied/downloaded into www/ at container startup.\n#   2. Registered as Lovelace resources.\n\n- local_path: ../dist/expander-card.js\n  filename: expander-card.js\n"
  },
  {
    "path": "tests/test_doc_audit.py",
    "content": "\"\"\"Audit test: every PNG/GIF image referenced in docs/source/ must be either\ngenerated by a ``doc_image:`` / ``doc_animation:`` scenario, or explicitly\nlisted in ``tests/doc-image-audit-exclusions.txt``.\n\nRunning\n-------\n.. code-block:: bash\n\n    # Via the Makefile alias\n    make doc_audit\n\n    # Directly with pytest\n    pytest tests/test_doc_audit.py\n\nIf the test fails it prints the list of untracked images so you can decide\nwhether to add a scenario that generates them, or add them to the exclusion\nfile because they are intentionally hand-crafted.\n\nExclusion list\n--------------\n``tests/doc-image-audit-exclusions.txt`` contains one repo-relative path per\nline.  Lines that start with ``#`` and blank lines are ignored.  Add a path\nhere for any image that is deliberately *not* auto-generated by a scenario\n(diagrams, logos, screenshots that cannot or need not be reproduced in\nautomated tests).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nimport yaml\n\n# ---------------------------------------------------------------------------\n# Paths\n# ---------------------------------------------------------------------------\n\nREPO_ROOT = Path(__file__).parent.parent\nDOCS_SOURCE = REPO_ROOT / \"docs\" / \"source\"\nSCENARIO_DIRS = [\n    REPO_ROOT / \"tests\" / \"visual\" / \"scenarios\",\n    REPO_ROOT / \"docs\" / \"scenarios\",\n]\nEXCLUSIONS_FILE = Path(__file__).parent / \"doc-image-audit-exclusions.txt\"\n\n# Matches Markdown image syntax:  ![alt text](path/to/image.png)\n_IMG_RE = re.compile(r\"!\\[[^\\]]*\\]\\(([^)]+)\\)\")  # NOSONAR\n\n# Extensions considered \"documentation images\" for audit purposes.\n_IMAGE_EXTS = {\".png\", \".gif\"}\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _collect_referenced_images() -> set[str]:\n    \"\"\"Return repo-relative paths of PNG/GIF images embedded in docs/source/ markdown files.\n\n    Only standard Markdown image syntax (``![alt](path)``) is scanned.  URL\n    fragments such as ``#only-light`` are stripped.  Absolute HTTP/HTTPS URLs\n    and local HA paths (starting with ``/``) are skipped.\n    \"\"\"\n    images: set[str] = set()\n    for md_file in sorted(DOCS_SOURCE.rglob(\"*.md\")):\n        text = md_file.read_text(encoding=\"utf-8\")\n        for match in _IMG_RE.finditer(text):\n            # Strip URL fragment and surrounding whitespace.\n            raw = match.group(1).split(\"#\")[0].strip()\n            # Skip absolute HTTP URLs and absolute local paths.\n            if raw.startswith((\"http://\", \"https://\", \"/\")):  # NOSONAR\n                continue\n            # Only care about PNG/GIF files.\n            if Path(raw).suffix.lower() not in _IMAGE_EXTS:\n                continue\n            resolved = (md_file.parent / raw).resolve()\n            try:\n                rel = resolved.relative_to(REPO_ROOT)\n                images.add(rel.as_posix())\n            except ValueError:\n                pass  # outside the repo root — skip\n    return images\n\n\ndef _collect_scenario_outputs() -> set[str]:  # NOSONAR\n    \"\"\"Return all ``output:`` paths declared in ``doc_image`` and ``doc_animation`` keys.\n\n    Scans all scenario YAML files under ``tests/visual/scenarios/`` and\n    ``docs/scenarios/``.  Paths are normalised to forward slashes relative to\n    the repo root.\n    \"\"\"\n    outputs: set[str] = set()\n    for scenarios_dir in SCENARIO_DIRS:\n        if not scenarios_dir.exists():\n            continue\n        for yaml_file in sorted(scenarios_dir.rglob(\"*.yaml\")):\n            with yaml_file.open(encoding=\"utf-8\") as fh:\n                data: dict[str, Any] = yaml.safe_load(fh) or {}\n\n            # doc_image: accepts a single mapping or a list of mappings.\n            raw_di = data.get(\"doc_image\")\n            if raw_di:\n                entries = raw_di if isinstance(raw_di, list) else [raw_di]\n                for entry in entries:\n                    if isinstance(entry, dict) and \"output\" in entry:\n                        outputs.add(entry[\"output\"].lstrip(\"/\"))\n\n            # doc_animation: always a single mapping.\n            raw_da = data.get(\"doc_animation\")\n            if isinstance(raw_da, dict) and \"output\" in raw_da:\n                outputs.add(raw_da[\"output\"].lstrip(\"/\"))\n\n    return outputs\n\n\ndef _load_exclusions() -> set[str]:\n    \"\"\"Return paths from the audit exclusion file (one per line; ``#`` lines ignored).\"\"\"\n    if not EXCLUSIONS_FILE.exists():\n        return set()\n    lines = EXCLUSIONS_FILE.read_text(encoding=\"utf-8\").splitlines()\n    return {\n        line.strip()\n        for line in lines\n        if line.strip() and not line.strip().startswith(\"#\")\n    }\n\n\n# ---------------------------------------------------------------------------\n# Test\n# ---------------------------------------------------------------------------\n\n\ndef test_all_doc_images_are_tracked() -> None:\n    \"\"\"Every PNG/GIF referenced in docs/source/ must be scenario-generated or excluded.\n\n    To fix a failure either:\n\n    1. Add a ``doc_image:`` or ``doc_animation:`` key to a scenario YAML so\n       the image is auto-generated and verified on every CI run, **or**\n    2. Add the path to ``tests/doc-image-audit-exclusions.txt`` because the\n       image is intentionally hand-crafted (diagram, logo, etc.).\n    \"\"\"\n    referenced = _collect_referenced_images()\n    generated = _collect_scenario_outputs()\n    excluded = _load_exclusions()\n\n    untracked = sorted(referenced - generated - excluded)\n\n    if untracked:\n        exclusions_path = EXCLUSIONS_FILE.relative_to(REPO_ROOT)\n        pytest.fail(\n            f\"{len(untracked)} doc image(s) are not generated by any scenario \"\n            f\"and are not listed in {exclusions_path}.\\n\\n\"\n            \"Add a doc_image:/doc_animation: key to a scenario YAML to auto-generate \"\n            f\"each image, or add it to {exclusions_path} if it is intentionally \"\n            \"hand-crafted:\\n\\n\"\n            + \"\\n\".join(f\"  {p}\" for p in untracked)\n        )\n"
  },
  {
    "path": "tests/visual/conftest.py",
    "content": "\"\"\"Expander card visual-test conftest — configure the ha_testcontainer scenario runner.\n\nAll Playwright and HA container fixtures (``ha_browser_context``, ``ha_page``,\n``ha``, ``ha_url``, ``ha_token``, ``ha_lovelace_url_path``) are provided\nautomatically by the ``ha_testcontainer`` pytest plugin.\n\nThis file configures the path globals that ``ha_testcontainer.visual.scenario_runner``\nexposes as module-level variables.  The assignment must happen at conftest\nimport time — before the test modules are collected and their module-level\ncalls to ``load_all_scenarios()`` / ``load_all_doc_image_scenarios()`` run.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport ha_testcontainer.visual.scenario_runner as _sr\n\n_REPO_ROOT = Path(__file__).parent.parent.parent\n\n_sr.SCENARIOS_DIR = Path(__file__).parent / \"scenarios\"\n_sr.SNAPSHOTS_DIR = Path(__file__).parent / \"snapshots\"\n_sr.REPO_ROOT = _REPO_ROOT\n_sr.DOCS_SCENARIOS_DIR = _REPO_ROOT / \"docs\" / \"scenarios\"\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_01_collapsed.yaml",
    "content": "id: expander_collapsed\ndescription: \"Expander card renders in collapsed state with title and chevron button\"\nview_path: expander-collapsed\n\ncard:\n  type: custom:expander-card\n  title: Lights\n  cards:\n    - type: entities\n      entities:\n        - light.bed_light\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".header\"\n\n  - type: snapshot\n    name: \"expander_01_collapsed\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-collapsed.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_02_expanded.yaml",
    "content": "id: expander_expanded\ndescription: \"Expander card renders in expanded state showing child cards\"\nview_path: expander-expanded\n\ncard:\n  type: custom:expander-card\n  title: Lights\n  expanded: true\n  cards:\n    - type: entities\n      entities:\n        - light.bed_light\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".header\"\n\n  - type: element_present\n    root: expander-card\n    selector: \".children-container.open\"\n\n  - type: snapshot\n    name: \"expander_02_expanded\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-expanded.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_03_expand_click.yaml",
    "content": "id: expander_expand_click\ndescription: \"Clicking the expander button opens the card and reveals child cards\"\nview_path: expander-expand-click\n\ncard:\n  type: custom:expander-card\n  title: Lights\n  cards:\n    - type: entities\n      entities:\n        - light.bed_light\n\ninteractions:\n  - type: click\n    root: expander-card\n    selector: \".header\"\n    settle_ms: 800\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".header\"\n\n  - type: element_present\n    root: expander-card\n    selector: \".children-container.open\"\n\n  - type: snapshot\n    name: \"expander_03_expand_click\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-expand-click.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_04_clear_background.yaml",
    "content": "id: expander_clear_background\ndescription: \"Expander card with clear:true removes ha-card background and border\"\nview_path: expander-clear-background\n\ncard:\n  type: custom:expander-card\n  title: Router\n  clear: true\n  expanded: true\n  cards:\n    - type: entities\n      entities:\n        - light.bed_light\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".children-container.open\"\n\n  - type: snapshot\n    name: \"expander_04_clear_background\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-clear.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_05_title_card.yaml",
    "content": "id: expander_title_card\ndescription: \"Expander card with a heading title-card replacing the text title\"\nview_path: expander-title-card\n\ncard:\n  type: custom:expander-card\n  title-card:\n    type: heading\n    heading: Lights\n    heading_style: title\n    icon: mdi:lightbulb\n  expanded: true\n  cards:\n    - type: entities\n      entities:\n        - light.bed_light\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".children-container.open\"\n\n  - type: snapshot\n    name: \"expander_05_title_card\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-title-card.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/scenarios/expander/expander_06_nested.yaml",
    "content": "id: expander_nested\ndescription: \"Expander cards can be nested — an outer expander contains an inner expander\"\nview_path: expander-nested\n\ncard:\n  type: custom:expander-card\n  title: Outer Expander\n  expanded: true\n  cards:\n    - type: custom:expander-card\n      title: Inner Expander\n      expanded: true\n      cards:\n        - type: entities\n          entities:\n            - light.bed_light\n\nassertions:\n  - type: element_present\n    root: expander-card\n    selector: \".children-container.open\"\n\n  - type: snapshot\n    name: \"expander_06_nested\"\n    threshold: 0.02\n\ndoc_image:\n  output: docs/source/chapter/assets/expander-nested.png\n  root: expander-card\n  padding: 8\n  threshold: 0.02\n"
  },
  {
    "path": "tests/visual/snapshots/.gitkeep",
    "content": "# Visual test baseline snapshots.\n# PNG baselines are committed here (generated by ha_testcontainer on first run\n# or when SNAPSHOT_UPDATE=1 is set).\n# Actual screenshots (*.actual.png) are gitignored.\n"
  },
  {
    "path": "tests/visual/test_doc_images.py",
    "content": "\"\"\"Generate and verify documentation images and animations from scenario YAML files.\n\nAny scenario that declares a ``doc_image:`` or ``doc_animation:`` key participates\nin doc asset generation.\n\n``doc_image``\n    Captures a static PNG screenshot.  ``doc_image`` accepts a **single mapping**\n    or a **list of mappings**.  Each list entry may include its own\n    ``interactions`` sub-key to advance the page to a new state before that\n    capture, enabling stepped documentation:\n\n    .. code-block:: yaml\n\n        # Single image\n        doc_image:\n          output: docs/source/chapter/assets/my-feature.png\n          root: expander-card\n          padding: 16\n          threshold: 0.02\n\n        # Stepped capture — each entry runs additional interactions then captures\n        doc_image:\n          - output: docs/source/chapter/assets/my-feature-closed.png\n            root: expander-card\n            padding: 8\n          - interactions:\n              - type: click\n                root: expander-card\n                selector: .header\n                settle_ms: 600\n            output: docs/source/chapter/assets/my-feature-open.png\n            root: expander-card\n            padding: 8\n\n``doc_animation``\n    Captures an animated GIF.  Pillow is required.\n\n    .. code-block:: yaml\n\n        doc_animation:\n          output: docs/source/chapter/assets/my-feature.gif\n          root: expander-card\n          padding: 8\n          frames: 12\n          interval_ms: 100\n          threshold: 0.02\n\nScenarios are loaded from two locations:\n\n* ``tests/visual/scenarios/`` — regular test scenarios that *also* declare\n  ``doc_image:`` and/or ``doc_animation:``.\n* ``docs/scenarios/`` — documentation-asset-only scenarios with no functional\n  assertions.\n\nUsage\n-----\n\n.. code-block:: bash\n\n    # Makefile aliases (recommended)\n    make doc_images_gen      # generate missing images; verify existing ones\n    make doc_images_update   # regenerate ALL doc images (overwrite existing)\n\n    # Or run pytest directly\n    pytest tests/visual/test_doc_images.py\n    DOC_IMAGE_UPDATE=1 pytest tests/visual/test_doc_images.py\n\n    # Single image by scenario id\n    pytest tests/visual/test_doc_images.py -k expander_collapsed\n\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom ha_testcontainer import HATestContainer\nfrom playwright.sync_api import Page\n\nfrom ha_testcontainer.visual.scenario_runner import (\n    capture_doc_animation,\n    capture_doc_image,\n    clear_scenario,\n    goto_scenario,\n    load_all_doc_image_scenarios,\n    push_scenario,\n    reset_theme,\n    run_interactions,\n    set_theme,\n)\n\n# ---------------------------------------------------------------------------\n# Collect scenarios that declare a doc_image: or doc_animation: key.\n# ---------------------------------------------------------------------------\n\n_DOC_SCENARIOS = load_all_doc_image_scenarios()\n_DOC_SCENARIO_IDS = [s[\"id\"] for s in _DOC_SCENARIOS]\n_DOC_SCENARIO_MAP = {s[\"id\"]: s for s in _DOC_SCENARIOS}\n\n\n# ---------------------------------------------------------------------------\n# Parametrised test\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"scenario_id\", _DOC_SCENARIO_IDS)\ndef test_doc_image(\n    scenario_id: str,\n    ha: HATestContainer,\n    ha_page: Page,\n    ha_url: str,\n    ha_lovelace_url_path: str,\n) -> None:\n    \"\"\"Capture and verify documentation assets for an expander card scenario.\n\n    The test pushes the scenario's card configuration to the shared\n    ``ha-tests`` Lovelace dashboard, navigates to it, runs any declared\n    interactions (setup and post-navigation), then calls\n    :func:`capture_doc_image` and :func:`capture_doc_animation` to produce\n    the requested documentation assets and compare them against the on-disk\n    files.\n\n    The test fails when a captured asset differs from the stored file beyond\n    the configured ``threshold``.  Run with ``DOC_IMAGE_UPDATE=1`` to\n    regenerate all doc assets.\n    \"\"\"\n    scenario = _DOC_SCENARIO_MAP[scenario_id]\n    theme = scenario.get(\"theme\")\n\n    try:\n        push_scenario(ha, ha_lovelace_url_path, scenario)\n        if theme:\n            set_theme(ha, theme)\n        run_interactions(ha_page, scenario, ha=ha, key=\"setup\")\n        goto_scenario(ha_page, ha_url, ha_lovelace_url_path, scenario[\"view_path\"])\n        run_interactions(ha_page, scenario, ha=ha)\n        capture_doc_image(ha_page, scenario, ha=ha)\n        capture_doc_animation(ha_page, scenario, ha=ha)\n    finally:\n        run_interactions(ha_page, scenario, ha=ha, key=\"teardown\")\n        if theme:\n            reset_theme(ha)\n        clear_scenario(ha, ha_lovelace_url_path)\n"
  },
  {
    "path": "tests/visual/test_scenarios.py",
    "content": "\"\"\"YAML-driven parametrised visual tests for the expander card.\n\nEach ``.yaml`` file under ``tests/visual/scenarios/`` defines one test\nscenario.  Tests are collected automatically and can be run individually::\n\n    # Run all scenarios\n    pytest tests/visual/test_scenarios.py\n\n    # Run a single scenario by its ``id`` field\n    pytest tests/visual/test_scenarios.py -k expander_collapsed\n\n    # Run all expander scenarios\n    pytest tests/visual/test_scenarios.py -k expander\n\nAdding a new test\n-----------------\nCreate a new ``.yaml`` file in ``tests/visual/scenarios/`` (or any\nsub-directory).  No Python changes are needed.  See the existing files for the\nschema and the ``scenario_runner`` module for the full list of assertion types.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom playwright.sync_api import Page\n\nfrom ha_testcontainer.visual.scenario_runner import (\n    clear_scenario,\n    goto_scenario,\n    load_all_scenarios,\n    push_scenario,\n    reset_theme,\n    run_assertions,\n    run_interactions,\n    set_theme,\n)\n\n# ---------------------------------------------------------------------------\n# Collect scenarios at import time so pytest can parametrize correctly.\n# ---------------------------------------------------------------------------\n\n_ALL_SCENARIOS = load_all_scenarios()\n_SCENARIO_IDS = [s[\"id\"] for s in _ALL_SCENARIOS]\n_SCENARIO_MAP = {s[\"id\"]: s for s in _ALL_SCENARIOS}\n\n\n# ---------------------------------------------------------------------------\n# Parametrised test\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.parametrize(\"scenario_id\", _SCENARIO_IDS)\ndef test_scenario(\n    scenario_id: str,\n    ha,\n    ha_page: Page,\n    ha_url: str,\n    ha_lovelace_url_path: str,\n) -> None:\n    \"\"\"Run a single expander card visual scenario defined in a YAML file.\n\n    The test pushes the scenario's card configuration to the shared\n    ``ha-tests`` Lovelace dashboard, navigates to it, runs all declared\n    assertions, then cleans up.  An optional ``theme:`` key in the YAML\n    activates the named HA theme before navigation and resets it afterwards.\n    \"\"\"\n    scenario = _SCENARIO_MAP[scenario_id]\n    theme = scenario.get(\"theme\")\n\n    try:\n        push_scenario(ha, ha_lovelace_url_path, scenario)\n        if theme:\n            set_theme(ha, theme)\n        run_interactions(ha_page, scenario, ha=ha, key=\"setup\")\n        goto_scenario(ha_page, ha_url, ha_lovelace_url_path, scenario[\"view_path\"])\n        run_interactions(ha_page, scenario, ha=ha)\n        run_assertions(ha_page, scenario)\n    finally:\n        run_interactions(ha_page, scenario, ha=ha, key=\"teardown\")\n        if theme:\n            reset_theme(ha)\n        clear_scenario(ha, ha_lovelace_url_path)\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"types\": [\"svelte\"],\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n        \"module\": \"esnext\",\n        \"moduleResolution\": \"bundler\",\n\t\t\"experimentalDecorators\": true,\n\t\t\"lib\": [\"es2019\", \"dom\"]\n\t},\n    \"include\": [\"src/**/*\"],\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "/* eslint-disable no-console */\nimport { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n    plugins: [\n       svelte({\n          preprocess: vitePreprocess({\n            postcss: true,\n            script: true\n            })\n        })\n    ],\n    build: {\n        sourcemap: true,\n        lib: {\n            entry: 'src/index.ts',\n            name: 'ExpanderCard',   // Name der globalen Variable für UMD/IIFE\n            fileName: 'expander-card',  // Basisname der generierten Datei\n        }\n    },\n    rollupOptions: {\n        output: {\n            entryFileNames: 'expander-card.js' // Dateiname der generierten Haupt-JS-Datei\n\n        }\n    }\n});\n\nprocess\n    .on('unhandledRejection', (reason, p) => {\n        console.error(reason, 'Unhandled Rejection at Promise', p);\n        process.exit(1);\n    })\n    .on('uncaughtException', (err) => {\n        console.error(err, 'Uncaught Exception thrown');\n        process.exit(1);\n    });\n"
  }
]