main 09d134f9bfda cached
38 files
93.6 KB
22.6k tokens
68 symbols
1 requests
Download .txt
Repository: jcwillox/lovelace-paper-buttons-row
Branch: main
Commit: 09d134f9bfda
Files: 38
Total size: 93.6 KB

Directory structure:
gitextract_uym2nibo/

├── .editorconfig
├── .git-blame-ignore-revs
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json
│   └── workflows/
│       ├── ci.yaml
│       ├── release.yaml
│       └── validate.yaml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .vscode/
│   ├── extensions.json
│   ├── settings.json
│   └── tasks.json
├── LICENSE
├── README.md
├── biome.json
├── hacs.json
├── info.md
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── action-handler.ts
│   ├── action.ts
│   ├── const.ts
│   ├── entity-row.ts
│   ├── entity.ts
│   ├── get-lovelace.ts
│   ├── main.ts
│   ├── presets.ts
│   ├── styles.css
│   ├── template.ts
│   ├── types.ts
│   ├── utils.ts
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2


================================================
FILE: .git-blame-ignore-revs
================================================
# git-blame supports ignoring specific commits, this allows us to hide formatting
# commits from git-blame.
#
# You can use the ignore list file by running:
#
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs

# refactor: reformat and sort imports
8b31e632dbf8a17dd4f52c05f6fc620bfc6ddbf1

# refactor: use default prettier config
1b5fa9a994cba8295141a56b2973397291d55255

# refactor: replace eslint with biome
c119669d580673b29d3aed0c8f19a0491f4680c0

# refactor: migrate to biome v2
1f72b1f60595db38bfe10b76fc8cbf668725dc2b


================================================
FILE: .gitattributes
================================================
# normalise line endings to lf on all platforms
* text=auto eol=lf


================================================
FILE: .github/FUNDING.yml
================================================
ko_fi: jcwillox


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Report an issue
description: Report an issue with the Paper Buttons Row plugin.
body:
  - type: markdown
    attributes:
      value: |
        This issue form is for reporting bugs, you should search through
        the existing issues to see if others have had the same problem.

        Try fill as many fields as you can, to make it easier to address the issue.
  - type: textarea
    attributes:
      label: The problem
      description: >-
        Describe the issue you are experiencing here, to communicate to the
        maintainers. Tell us what you were trying to do and what happened.

        Provide a clear and concise description of what the problem is.
  - type: markdown
    attributes:
      value: |
        ## Environment
  - type: input
    id: version
    attributes:
      label: What version of Paper Buttons Row has the issue?
      description: >
        Can be found in: [HACS -> Frontend -> Paper Buttons Row](https://my.home-assistant.io/redirect/hacs_repository/?owner=jcwillox&repository=lovelace-paper-buttons-row&category=plugin). The version will be displayed in first chip at the top.

        [![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=jcwillox&repository=lovelace-paper-buttons-row&category=plugin)
  - type: input
    id: ha_version
    attributes:
      label: What version of Home Assistant are you running?
      placeholder: Home Assistant YYYY.MM.XX
      description: >
        Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).

        [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/)
  - type: input
    id: frontend_version
    attributes:
      label: What version of the Frontend are you running?
      placeholder: Frontend YYYYMMDD.X
      description: >
        Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).

        [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/)
  - type: markdown
    attributes:
      value: |
        # Details
  - type: textarea
    attributes:
      label: Example YAML snippet
      description: |
        If applicable, please provide an example piece of YAML that can help reproduce this problem.
        This can be from an automation, script, service or configuration.
      render: yml
  - type: textarea
    attributes:
      label: Anything in the logs that might be useful for us?
      description: |
        For example, error message, or stack traces.

        To view the browsers logs, press F12 and go to the "Console" tab, or use the "Inspect" option in the browsers right-click menu.
      render: python3
  - type: textarea
    attributes:
      label: Additional information
      description: >
        If you have any additional information for us, use the field below.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature request
description: Suggest an idea for this project.
title: "[FR]: "
body:
  - type: markdown
    attributes:
      value: |
        This form is for submitting feature requests.

        You may want to open a [discussion](https://github.com/jcwillox/lovelace-paper-buttons-row/discussions) instead for less concrete ideas that need to be discussed further.

        Please fill out all the fields that are relevant to your feature request.
  - type: textarea
    attributes:
      label: Is your feature request related to a problem? Please describe.
      description: >-
        A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
  - type: textarea
    attributes:
      label: Describe the solution you'd like
      description: >-
        A clear and concise description of what you want to happen.
  - type: textarea
    attributes:
      label: Describe alternatives you've considered
      description: >-
        A clear and concise description of any alternative solutions or features you've considered.
  - type: textarea
    attributes:
      label: Additional context
      description: >-
        Add any other context or screenshots about the feature request here.


================================================
FILE: .github/renovate.json
================================================
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["github>jcwillox/renovate-config", ":automergeMinor"],
  "packageRules": [
    {
      "matchPackageNames": ["lit"],
      "allowedVersions": "<3.0.0"
    }
  ]
}


================================================
FILE: .github/workflows/ci.yaml
================================================
name: "CI"

on:
  push:
    branches:
      - "main"
      - "feat**"
      - "fix**"
    tags-ignore:
      - "**"
  pull_request:

jobs:
  lint:
    name: "Lint"
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout the repository"
        uses: actions/checkout@v4.2.2

      - name: "Setup Biome"
        uses: biomejs/setup-biome@v2.6.0

      - name: "Run Biome"
        run: biome ci .

  build:
    name: "Build & Test"
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout the repository"
        uses: actions/checkout@v4.2.2

      - name: "Setup pnpm"
        uses: pnpm/action-setup@v4.1.0

      - name: "Setup node"
        uses: actions/setup-node@v4.4.0
        with:
          node-version-file: package.json
          cache: "pnpm"

      - name: "Install dependencies"
        run: pnpm install

      - name: "Typecheck"
        run: pnpm run typecheck

      - name: "Run Build"
        run: pnpm run build


================================================
FILE: .github/workflows/release.yaml
================================================
name: "Release"

on:
  push:
    branches:
      - "beta"
      - "alpha"
  workflow_dispatch:
    inputs:
      draft:
        type: boolean
        description: "Draft release"
        default: false
      release_type:
        type: choice
        description: "Release type"
        default: "auto"
        options:
          - "auto"
          - "patch"
          - "minor"
          - "major"

jobs:
  publish:
    name: "Publish"
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout the repository"
        uses: actions/checkout@v4.2.2

      - name: "Setup pnpm"
        uses: pnpm/action-setup@v4.1.0

      - name: "Setup node"
        uses: actions/setup-node@v4.4.0
        with:
          node-version-file: package.json
          cache: "pnpm"

      - name: "Install dependencies"
        run: pnpm install

      - name: "Release Package 📦"
        run: pnpm dlx @jcwillox/semantic-release-config
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SEMANTIC_RELEASE_GITHUB_DRAFT: ${{ inputs.draft }}
          SEMANTIC_RELEASE_FORCE_RELEASE: ${{ inputs.release_type }}
          SEMANTIC_RELEASE_GITHUB_ASSETS: "dist/*"
          SEMANTIC_RELEASE_CMD_PREPARE: "VERSION=${nextRelease.version} pnpm build"


================================================
FILE: .github/workflows/validate.yaml
================================================
name: "Validate"

on:
  push:
    branches:
      - "main"
      - "feat**"
      - "fix**"
    tags-ignore:
      - "**"
  pull_request:
  schedule:
    - cron: "0 0 * * *"

jobs:
  validate-hacs:
    runs-on: ubuntu-latest
    name: "HACS"
    steps:
      - name: "Checkout the repository"
        uses: actions/checkout@v4.2.2

      - name: "Validate HACS"
        uses: hacs/action@main
        with:
          category: plugin


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# IDE files
.idea/


================================================
FILE: .husky/pre-commit
================================================
lint-staged


================================================
FILE: .npmrc
================================================
save-exact=true


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "formulahendry.auto-rename-tag",
    "editorconfig.editorconfig",
    "dbaeumer.vscode-eslint",
    "christian-kohler.path-intellisense",
    "esbenp.prettier-vscode",
    "redhat.vscode-yaml"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.linkedEditing": true,
  "editor.renderWhitespace": "trailing",
  "html.autoClosingTags": true,
  "editor.quickSuggestions": {
    "strings": true
  },
  "editor.codeActionsOnSave": {
    "source.fixAll": "explicit"
  },
  "[javascript][javascriptreact][typescript][typescriptreact][json][jsonc][yaml][html][markdown][postcss][css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "vite.autoStart": false
}


================================================
FILE: .vscode/tasks.json
================================================
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "install",
      "label": "npm: install"
    },
    {
      "type": "npm",
      "script": "lint",
      "problemMatcher": ["$eslint-stylish"],
      "label": "npm: lint"
    },
    {
      "type": "npm",
      "script": "build",
      "group": "build",
      "label": "npm: build"
    }
  ]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Joshua Cowie-Willox

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Paper Buttons Row

[![HACS Badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration)
[![License](https://img.shields.io/github/license/jcwillox/lovelace-paper-buttons-row?style=for-the-badge)](https://github.com/jcwillox/lovelace-paper-buttons-row/blob/main/LICENSE)
[![Latest Release](https://img.shields.io/github/v/release/jcwillox/lovelace-paper-buttons-row?style=for-the-badge)](https://github.com/jcwillox/lovelace-paper-buttons-row/releases)
[![GZIP Size](https://img.badgesize.io/https:/github.com/jcwillox/lovelace-paper-buttons-row/releases/latest/download/paper-buttons-row.js?style=for-the-badge&compression=gzip)](https://github.com/jcwillox/lovelace-paper-buttons-row/releases)

This is a complete rewrite of the original [`button-entity-row`](https://github.com/custom-cards/button-entity-row) plugin, that is more consistent with Home Assistant's [button card](https://www.home-assistant.io/lovelace/button/), it uses **actions** including `tap_action`, `double_tap_action` and `hold_action` allowing for greater customisation of the buttons behaviour. It also retains the ability to style the button based on state, but adds the ability to style the icon, text, and ripple effect separately. There is a new option for **icon alignment** and the buttons have haptic feedback.

![example-main](examples/example-5.gif)

## Options

| Name         | Type                                                  | Requirement  | Description                                                                                                                                           |
| ------------ | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| type         | `string`                                              | **Required** | `custom:paper-buttons-row`                                                                                                                            |
| preset       | `string`                                              | **Optional** | The preset configuration to use e.g. `mushroom`. [See presets](#presets)                                                                              |
| buttons      | List [`string` or [`button object`](#button-options)] | **Required** | List of buttons to display. [See button options](#button-options)                                                                                     |
| base_config  | [`button object`](#button-options)                    | **Optional** | Specify a base config that will be deep-merged with each buttons config. Buttons can override the base config                                         |
| styles       | `object`                                              | **Optional** | CSS styles to apply to the entire button group. e.g. to change the flex-box alignment.                                                                |
| extra_styles | `string`                                              | **Optional** | Inject CSS directly into the paper-buttons-row container, useful for animations. [See extra styles](#extra-styles)                                    |
|              |                                                       |              |                                                                                                                                                       |
| position     | `"center"` \| `"right"`                               | **Optional** | Position embedded buttons in the middle or end of the entity-row (default: `center`). [See embedding in entity rows](#embedding-in-other-entity-rows) |
| hide_badge   | `boolean`                                             | **Optional** | Hide state badge when embedding in an entity-row                                                                                                      |
| hide_state   | `boolean`                                             | **Optional** | Hide state text or toggle when embedding in an entity-row                                                                                             |

### Button Options

When only an `entity` is provided the button will attempt to toggle it by default.

| Name              | Type                                           | Requirement  | Description                                                                                                                                      |
| ----------------- | ---------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| entity            | `string`                                       | **Optional** | The entity_id of the entity you want to show.                                                                                                    |
| name              | `string` \| [`template`](#templating)          | **Optional** | Name to use for entity. Use `false` to hide name.                                                                                                |
| state             | `string` \| [`template`](#templating)          | **Optional** | State to display for entity. Use `true` to show the entity state.                                                                                |
| icon              | `string` \| [`template`](#templating)          | **Optional** | The icon to display. Use `false` to hide icon.                                                                                                   |
| image             | `string` \| [`template`](#templating)          | **Optional** | Display an image instead of an icon. e.g. `/local/custom.png`.                                                                                   |
| preset            | `string`                                       | **Optional** | The preset configuration to use e.g. `mushroom`. [See presets](#presets)                                                                         |
| active            | `string` \| `list[string]`                     | **Optional** | Configure the states which the button considers itself to be active, defaults to [`on`, `open`, `unlocked`]. [See CSS variables](#css-variables) |
| ripple            | `"fill"` \| `"none"` \| `"circle"`             | **Optional** | Override the default shape of the ripple.                                                                                                        |
| layout            | `string` \| `object`                           | **Optional** | Change the layout of the icon, name and state fields. [See layout options](#layout)                                                              |
| tooltip           | `string`                                       | **Optional** | Override the default tooltip. Use `false` to hide tooltip.                                                                                       |
|                   |                                                |              |                                                                                                                                                  |
| tap_action        | `map`                                          | **Optional** | Tap action map [See action options](#action-options)                                                                                             |
| hold_action       | `map`                                          | **Optional** | Hold action map [See action options](#action-options)                                                                                            |
| double_tap_action | `map`                                          | **Optional** | Double Tap action map [See action options](#action-options)                                                                                      |
|                   |                                                |              |                                                                                                                                                  |
| styles            | [`style object`](#style-options) (templatable) | **Optional** | Map of CSS styles to apply to the button, icon, text or ripple. [See style options](#style-options)                                              |
| state_styles      | `map[state: style object]`                     | **Optional** | Map of states to a [`style object`](#style-options), [See example](#using-style-and-state_styles).                                               |
| state_icons       | `map[state: icon]`                             | **Optional** | Material icon for each state of the entity. Map state to icon, [See example](#using-state-icons-state-text-and-actions).                         |
| state_text        | `map[state: text]`                             | **Optional** | Button text for each state of the entity, Map state to text, [See example](#using-state-icons-state-text-and-actions).                           |

### Action Options

Each button supports the same actions as seen in Home Assistant's [button card](https://www.home-assistant.io/lovelace/button).

| Name              | Type           | Default  | Supported options                                                                                   | Description                                                                                                         |
| ----------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `action`          | `string`       | `toggle` | `more-info`, `toggle`, `call-service`, `fire-event`, `none`, `navigate`, `url`                      | Action to perform                                                                                                   |
| `entity`          | `string`       | none     | Any entity id                                                                                       | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info`                 |
| `navigation_path` | `string`       | none     | Eg: `/lovelace/0/`                                                                                  | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate                                           |
| `url_path`        | `string`       | none     | Eg: `https://www.google.com`                                                                        | URL to open on click when action is `url`.                                                                          |
|                   |                |          |                                                                                                     |                                                                                                                     |
| `service`         | `string`       | none     | Any service                                                                                         | Service to call (e.g. `remote.send_command`) when `action` defined as `call-service`                                |
| `service_data`    | `map`          | none     | Any service data                                                                                    | Service data to include (e.g. `command: play_pause`) when `action` defined as `call-service`.                       |
| `target`          | `map`          | none     | Any service target                                                                                  | Service target to include (e.g. `entity_id: remote.bedroom`) when `action` defined as `call-service`.               |
|                   |                |          |                                                                                                     |                                                                                                                     |
| `event_type`      | `string`       | none     | Any event                                                                                           | Event to call (e.g. `custom_event`) when `action` defined as `fire-event`                                           |
| `event_data`      | `map`          | none     | Any event data                                                                                      | Event data to include when `action` defined as `fire-event`.                                                        |
|                   |                |          |                                                                                                     |                                                                                                                     |
| `repeat`          | `number`       | none     | Eg: `500`                                                                                           | **Only valid for `hold_action`** optionally set the action to repeat every N milliseconds while the button is held. |
|                   |                |          |                                                                                                     |                                                                                                                     |
| `confirmation`    | `boolean\|map` | false    | [See confirmation object](https://www.home-assistant.io/lovelace/actions/#options-for-confirmation) | Present a confirmation dialog to confirm the action.                                                                |

### Presets

A preset is just a predefined [button config](#button-options) object that will be deep-merged with the config, just like the `base_config` option.

**Built-in Presets**

Presets are now supported by default only the `mushroom` preset is included.

![example-mushroom-light](examples/mushroom-light.png)

![example-mushroom-dark](examples/mushroom-dark.png)

```yaml
type: entities
entities:
  - type: custom:paper-buttons-row
    # apply to all buttons
    preset: mushroom
    base_config:
      # the same as above applies to all buttons
      preset: mushroom
    buttons:
      - entity: light.bedroom_light
        # or override on a button level
        preset: mushroom

      - entity: lock.front_door
        # set what state is considered active
        active: unlocked
        styles:
          # override the inactive color
          --pbs-button-rgb-color: red
          # override the active color
          --pbs-button-rgb-active-color: green

      - icon: mdi:power
```

**User-defined Presets**

Presets can be defined in the top level of your dashboard, using the "Raw configuration editor" mode.

```yaml
paper_buttons_row:
  presets:
    my_custom_preset:
      ripple: fill
      styles:
        button:
          color: red

views: ...
```

### Style Options

| Name   | Type     | Requirement  | Description                                            |
| ------ | -------- | ------------ | ------------------------------------------------------ |
| button | `object` | **Optional** | CSS styles to apply to the button.                     |
| icon   | `object` | **Optional** | CSS styles to apply to specifically the icon.          |
| name   | `object` | **Optional** | CSS styles to apply to specifically the name field.    |
| state  | `object` | **Optional** | CSS styles to apply to specifically the state field.   |
| ripple | `object` | **Optional** | CSS styles to apply to specifically the ripple effect. |

Each key can be templated e.g.

```yaml
styles:
  button:
    color: >-
      {% if is_state('light.bedroom', 'on') %}
        red
      {% else%}
        cyan
      {% endif %}
```

### CSS Variables

**Base State**

- `--pbs-button-color` – used to override the color of the button.
- `--pbs-button-rgb-color` – same as above but expects a list of rgb values, e.g. `123, 123, 0`.
- `--pbs-button-rgb-state-color` – this is set automatically to reference an `--rgb-state-*-color` variable.
- `--pbs-button-rgb-default-color` - this is used to set the default color of the paper-buttons, it is not set by default.
- `--rgb-state-default-color` – this is the default color provided by Home Assistant.

**Base State (Background)**

- `--pbs-button-bg-color` – used to override the background of the button default is not set.
- `--pbs-button-rgb-bg-color` – same as above but expects a list of rgb values, e.g. `123, 123, 0`.
- `--pbs-button-rgb-bg-opacity` – defaults to 1.

**Active State**

- `--paper-item-icon-active-color` – (deprecated) unset in 2022.12 was originally set to `#fdd835`.
- `--pbs-button-active-color`
- `--pbs-button-rgb-active-color`
- `--pbs-button-rgb-state-color`
- `--pbs-button-rgb-default-color`
- `--rgb-state-default-color`

**Active State (Background)**

- `--pbs-button-bg-active-color`
- `--pbs-button-rgb-bg-active-color`
- `--pbs-button-rgb-bg-active-opacity`
- `--pbs-button-rgb-bg-color`
- `--pbs-button-rgb-bg-opacity`

**Unavailable State**

- `--pbs-button-unavailable-color`
- `--pbs-button-rgb-unavailable-color`
- `--rgb-disabled-color`

### Extra Styles

The `extra_styles` option allow you to embed extra CSS into paper-buttons-row this allows you to specify custom animations and style the hover and active states among other things.

**Animations & Hover/Active Effects**

![example-embedded-hide](examples/example-animation-hover-active.gif)

There are two built-in animations `blink` and `rotating`.

```yaml
- type: custom:paper-buttons-row
  extra_styles: |
    /* define custom animation */
    @keyframes bgswap1 {
      0% {
        background-image: url("/local/christmas-lights-ro.png");
      }
      25% {
        background-image: url("/local/christmas-lights-ro.png");
      }
      50% {
        background-image: url("/local/christmas-lights-gb.png");
      }
      75% {
        background-image: url("/local/christmas-lights-gb.png");
      }
      100% {
        background-image: url("/local/christmas-lights-ro.png");
      }
    }
    /* set hover and active effects for buttons */
    paper-button:hover {
      background-color: red;
    }
    paper-button:active {
      background-color: yellow;
    }
    /* styles for the third button only */
    paper-button:nth-child(3):hover {
      background-color: green;
    }
    paper-button:nth-child(3):active {
      background-color: purple;
    }
  buttons:
    - icon: mdi:power
      styles:
        button:
          animation: blink 2s ease infinite
    - styles:
        button:
          width: 64px
          height: 64px
          background-size: cover
          # use custom animation defined earlier
          animation: bgswap1 2s ease infinite
    - icon: mdi:power
      styles:
        button:
          - animation: rotating 2s ease infinite
```

#### Data Attributes

If you use the [`extra_styles`](#extra-styles) option you can use data attributes to style the button based on the domain or state of the configured entity.

- `data-domain` – The domain of the entity
- `data-state` – The current templated state, which defaults to the entity state but could refer to an attribute if you configure the `state` option
- `data-entity-state` – The state of the current entity.

```css
paper-button[data-state="on"] {
  color: red;
}
```

### Global styles & base config

You can specify `styles` that apply to the actual flex-box used to contain each row of buttons. You can also specify a default `base_config` that is deep-merged with the config for each button, this helps reduce repetition in your configs.

```yaml
type: custom:paper-buttons-row
# styles applied to the row container
styles:
  # override off/on colors
  --pbs-button-bg-color: red
  --pbs-button-bg-active-color: green
  # align all buttons to the left
  justify-content: flex-start
buttons:
  - entity: light.bedroom_light
```

```yaml
type: custom:paper-buttons-row
base_config:
  # will be applied to all configured buttons
  state_styles:
    "on":
      # override color for the entire button
      button:
        color: yellow
      # or override for the name only
      name:
        color: var(--primary-text-color)
    "off":
      button:
        color: red
buttons:
  - entity: light.bedroom_light
  - entity: light.kitchen_light
```

### Layout

The pipe or bar `|` symbol is used to put elements next to each other, and an underscore `_` is used to place items below each other.
You can also define layouts using a list (row) and nested lists (columns).

These are some examples of simple layouts:

![example-layout](examples/example-layout.png)

```yaml
type: entities
entities:
  - type: custom:paper-buttons-row
    buttons:
      - entity: light.bedroom_light
        layout: icon|name
        # layout: [icon, name]

      - entity: light.bedroom_light
        layout: icon_name
        # layout: [[icon, name]]

      - entity: light.bedroom_light
        layout: name|icon
        # layout: [name, icon]

      - entity: light.bedroom_light
        layout: name_icon
        # layout: [[name, icon]]
```

Advanced example

![example-layout-advanced](examples/example-layout-advanced.png)

```yaml
type: entities
entities:
  - type: custom:paper-buttons-row
    buttons:
      - entity: light.bedroom_light
        layout: icon_name|state
        # layout: [[icon, name], [state]]
```

### Templating

#### Template Object

| Name        | Type     | Description                                                                       |
| ----------- | -------- | --------------------------------------------------------------------------------- |
| `entity`    | `string` | Optional: entity to extract data from, defaults to the rows configured entity.    |
| `attribute` | `object` | Optional: extract an attribute from the entity, otherwise the state will be used. |
| `prefix`    | `string` | Optional: string to append **before** the attribute/state.                        |
| `postfix`   | `string` | Optional: string to append **after** the attribute/state.                         |
| `case`      | `string` | Optional: change case of result must be one of `upper`, `lower`, `first`          |

**Examples**

![example-templating](examples/example-templating.png)

```yaml
type: entities
entities:
  - type: custom:paper-buttons-row
    buttons:
      - entity: light.bedroom_light
        layout: icon|name|state
        name:
          attribute: friendly_name
          postfix: ": "
        state:
          case: first
```

The `state_text` and `state_styles` options will use the lowercase result of the template for the state field.

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      - entity: fan.bedroom
        layout: icon|state
        state:
          attribute: speed
        state_styles:
          high:
            color: red
          medium:
            color: yellow
          low:
            color: green
        state_text:
          high: III
          medium: II
          low: I
        # ...
```

#### Jinja Templates

_Note: that Jinja2 templates are slightly slower to load initially due to latency, as they are rendered in the backend, whereas the other are rendered in the frontend._

Jinja templates have access to a few special variables. Those are:

- `config` - an object containing the entity row configuration.
- `entity` - the entity_id from the current entity row configuration. This **must** be used instead of `config.entity` for the template to automatically update.
- `user` - the username of the currently logged in user.
- `browser` - the deviceID of the current browser (see [browser_mod](https://github.com/thomasloven/hass-browser_mod)).
- `hash` - the hash part of the current URL.

**Example**

```yaml
type: entities
entities:
  - type: custom:paper-buttons-row
    buttons:
      - entity: light.bedroom_light
        layout: icon|name|state
        name: "{{ state_attr(config.entity, 'friendly_name') }}: "
        state: "{{ states(config.entity) | title }}"
```

## Embedding in other entity rows

![example-minimal-setup](examples/example-embedded.png)

Paper Buttons Row can be embedded within most entity rows. As shown in the image above it inserts a `paper-buttons-row` row inline, this can be either before or after the final element.

```yaml
type: entities
entities:
  - entity: light.bedroom_light
    # add the following to a normal entity row to embed paper buttons.
    extend_paper_buttons_row:
      position: # can be either `center` or `right`, defaults to `center`.
      # ... normal paper-buttons-row config goes here.
```

When extending entity rows there are options to control the position of the inserted buttons, as well as to hide the badge or state elements.

![example-embedded-hide](examples/example-embedded-hide.png)

```yaml
type: entities
entities:
  - entity: input_boolean.test
  - entity: input_boolean.test
    name: Hide State
    extend_paper_buttons_row:
      hide_state: true
      buttons:
        - icon: mdi:power
  - entity: input_select.test
  - entity: input_select.test
    name: Hide Badge
    extend_paper_buttons_row:
      hide_badge: true
      position: right
      buttons:
        - icon: mdi:close
```

<details>
<summary>Full example for the image above</summary>

```yaml
type: entities
show_header_toggle: false
entities:
  - entity: light.bedroom_light
    extend_paper_buttons_row:
      # position defaults to center.
      buttons:
        - entity: scene.daylight
          icon: "mdi:brightness-5"
          name: false
        - entity: script.light_colour_flow
          icon: "mdi:all-inclusive"
          name: false
        - entity: scene.evening
          icon: "mdi:brightness-3"
          name: false
          styles:
            button:
              margin-right: 8px

  - type: divider

  - entity: media_player.family_room_tv
    name: TV
    extend_paper_buttons_row:
      # position after power button.
      position: right
      # use base config to set the default margin for all buttons.
      base_config:
        styles:
          button:
            margin-left: 2px
            margin-right: 2px
      buttons:
        - icon: "mdi:volume-mute"
          # override left margin for first button.
          styles:
            button:
              margin-left: 0px
        - icon: "mdi:volume-minus"
        - icon: "mdi:volume-plus"
```

</details>

## Examples

### Minimal Setup.

![example-minimal-setup](examples/example-3.png)

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      - scene.daylight # simplest way to create a button.

      - entity: scene.rave
        icon: "mdi:track-light" # override or add a mdi icon.

      - entity: script.light_colour_flow
        icon: "mdi:all-inclusive"
        name: false # makes the button icon only.

      - entity: scene.evening
        icon: "mdi:brightness-3"
        name: false
```

---

### Using style and state_styles.

![example-styled-toggle-button](examples/example-1.gif)
![example-styled-toggle-button-top-aligned](examples/example-1-1.gif)

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      - entity: light.desk_leds
        icon: "mdi:lightbulb"
        styles: # These are the default styles that can be overridden by state styles.
          button:
            border-radius: 10px
            font-size: 16px
        state_styles:
          "off": # define a state then provide a style object.
            button:
              background-color: var(--table-row-alternative-background-color)
            name:
              color: orange
          "on":
            button:
              background-color: var(--primary-color)
            icon:
              color: "#fdd835" # this will change the icon colour when the entities state is on.
            ripple:
              color: orange # colour the ripple effect.

      - entity: light.monitor_leds
        icon: "mdi:lightbulb"
        layout: icon_name
        # layout: [[icon, name]]
        styles:
          button:
            background-color: var(--table-row-alternative-background-color)
            border-radius: 10px
            font-size: 1.2rem
            padding: 8px
          icon:
            --mdc-icon-size: 40px # make the icon bigger.
        state_styles:
          "on":
            button:
              background-color: var(--primary-color)
            icon:
              color: "#fdd835"
            ripple:
              color: orange
```

---

### Using state icons, state text and actions.

![example-lock-toggle](examples/example-4.gif)

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      - entity: lock.front_door
        layout: icon|state # show the state field
        state_icons:
          "unlocked": "mdi:lock-open"
          "locked": "mdi:lock"
        state_text:
          "unlocked": "Unlocked"
          "locked": "Locked"

        state_styles:
          "unlocked":
            button:
              color: green
          "locked":
            button:
              color: red
        styles:
          button:
            background-color: var(--table-row-alternative-background-color)
            border-radius: 40px
            padding: 10px
            font-size: 1.2rem

        tap_action:
          action: call-service
          service: lock.lock
          service_data:
            entity_id: lock.front_door

        hold_action:
          action: call-service
          service: lock.unlock
          service_data:
            entity_id: lock.front_door

          # it's also possible to add a confirmation dialog to the action.
          confirmation:
            exemptions:
              - user: 22a1119b08c54960822a0c6b896bed2
            text: Are you sure you want to unlock?
```

---

### Multiple rows of buttons.

![example-multiple-rows-of-buttons](examples/example-2.png)

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      - icon: "mdi:chevron-up"
        tap_action:
          action: call-service
          service: esphome.family_room_node_transmit_panasonic
          service_data:
            command: 218145196

  # for multiple rows define multiple `paper-buttons-row`s.
  - type: "custom:paper-buttons-row"
    buttons:
      - icon: "mdi:chevron-left"
        tap_action:
          action: call-service
          service: esphome.family_room_node_transmit_panasonic
          service_data:
            command: 218161644
      - icon: "mdi:checkbox-blank-circle-outline"
      - icon: "mdi:chevron-right"
        tap_action:
          action: call-service
          service: esphome.family_room_node_transmit_panasonic
          service_data:
            command: 218108188

  - type: "custom:paper-buttons-row"
    buttons:
      - icon: "mdi:chevron-down"
        tap_action:
          action: call-service
          service: esphome.family_room_node_transmit_panasonic
          service_data:
            command: 218128748
```

```yaml
type: entities
entities:
  - type: "custom:paper-buttons-row"
    buttons:
      # multiple rows of buttons can also be defined in one paper-buttons-row
      # by using a list of lists of buttons.
      - - light.monitor_leds
        - light.desk_leds

      - - light.bedroom_light
        - entity: light.bedroom_underglow
          icon: "mdi:lightbulb"
```

## Installation

```yaml
resources:
  - url: /hacsfiles/lovelace-paper-buttons-row/paper-buttons-row.js
    type: module
```


================================================
FILE: biome.json
================================================
{
  "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
  "files": {
    "includes": ["**", "!**/pnpm-lock.yaml", "!**/dist/**/*"]
  },
  "formatter": {
    "enabled": true,
    "useEditorconfig": true
  },
  "assist": { "actions": { "source": { "organizeImports": "on" } } },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedImports": "warn",
        "noUnusedVariables": "warn"
      },
      "style": {
        "noParameterAssign": "off",
        "useNamingConvention": {
          "level": "warn",
          "options": {
            "strictCase": false,
            "conventions": [
              {
                "selector": {
                  "kind": "catchParameter"
                },
                "match": "_|err"
              },
              {
                "match": ".*"
              }
            ]
          }
        }
      }
    }
  }
}


================================================
FILE: hacs.json
================================================
{
  "name": "Paper Buttons Row"
}


================================================
FILE: info.md
================================================
This is a complete rewrite of the original [`button-entity-row`](https://github.com/custom-cards/button-entity-row) plugin, that is more consistent with Home Assistant's [button card](https://www.home-assistant.io/lovelace/button/), it uses **actions** including `tap_action`, `double_tap_action` and `hold_action` allowing for greater customisation of the buttons behaviour. It also retains the ability to style the button based on state, but adds the ability to style the icon, text, and ripple effect separately. There is a new option for **icon alignment** and the buttons have haptic feedback.

Check out the [documentation](https://github.com/jcwillox/lovelace-paper-buttons-row) for the configuration options and examples.

## Images

<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-5.gif?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-3.png?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-1.gif?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-1-1.gif?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-4.gif?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-2.png?raw=true" width="400px">
<img src="https://github.com/jcwillox/lovelace-paper-buttons-row/blob/master/examples/example-embedded.png?raw=true" width="400px">

More example images than you could poke a stick at, I know...

## Features

- Create icon, text, or icon-text buttons.
- Add css styling to each button per state!
- Style the icon, name, state, and ripple effect separately.
- Change the icon alignment and layout of the icon, name and state.
- Add actions for `tap_action`, `double_tap_action` and `hold_action`.
- Create multiple rows of buttons.
- Embed buttons in other entity rows.
- Tooltip support, configure custom tooltips.
- Templating support.


================================================
FILE: package.json
================================================
{
  "name": "paper-buttons-row",
  "version": "0.0.0-dev",
  "type": "module",
  "private": true,
  "scripts": {
    "prepare": "husky",
    "dev": "vite build --watch",
    "build": "vite build",
    "build:tsc": "tsc && vite build",
    "typecheck": "tsc",
    "lint": "eslint . --cache --max-warnings=0 --ext js,cjs,mjs,jsx,ts,tsx",
    "lint:fix": "pnpm run lint --fix",
    "format": "prettier --cache --write .",
    "format:check": "prettier --cache --check ."
  },
  "lint-staged": {
    "*": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
  },
  "dependencies": {
    "card-tools": "github:thomasloven/lovelace-card-tools#477f3d4",
    "custom-card-helpers": "1.9.0",
    "deepmerge": "4.3.1",
    "fast-deep-equal": "3.1.3",
    "home-assistant-js-websocket": "9.5.0",
    "lit": "2.8.0",
    "vite": "6.1.6"
  },
  "devDependencies": {
    "@biomejs/biome": "2.1.3",
    "@types/node": "22.17.0",
    "husky": "9.1.7",
    "lint-staged": "15.5.2",
    "typescript": "5.8.3",
    "vite-plugin-compression": "0.5.1"
  },
  "packageManager": "pnpm@10.13.1",
  "engines": {
    "node": ">=20.x"
  }
}


================================================
FILE: pnpm-workspace.yaml
================================================
onlyBuiltDependencies:
  - '@biomejs/biome'
  - esbuild


================================================
FILE: src/action-handler.ts
================================================
import {
  type ActionHandlerDetail,
  type ActionHandlerOptions,
  fireEvent,
} from "custom-card-helpers";
import deepEqual from "fast-deep-equal";
import { noChange } from "lit";
import {
  type AttributePart,
  Directive,
  type DirectiveParameters,
  directive,
} from "lit/directive.js";

declare global {
  interface Navigator {
    msMaxTouchPoints: number;
  }
}

const isTouch =
  "ontouchstart" in window ||
  navigator.maxTouchPoints > 0 ||
  navigator.msMaxTouchPoints > 0;

export interface CustomActionHandlerOptions extends ActionHandlerOptions {
  disabled?: boolean;
  repeat?: number;
  stopPropagation?: boolean;
}

interface Ripple extends HTMLElement {
  primary: boolean;
  disabled: boolean;
  unbounded: boolean;
  startPress: () => void;
  endPress: () => void;
}

interface IActionHandler extends HTMLElement {
  holdTime: number;
  bind: (
    element: ActionHandlerElement,
    options?: CustomActionHandlerOptions,
  ) => void;
}

interface ActionHandlerElement extends HTMLElement {
  actionHandler?: {
    options: ActionHandlerOptions;
    start?: (ev: Event) => void;
    end?: (ev: Event) => void;
    handleKeyDown?: (ev: KeyboardEvent) => void;
  };
}

declare global {
  interface HTMLElementTagNameMap {
    "action-handler": ActionHandler;
  }

  interface HASSDomEvents {
    action: ActionHandlerDetail;
  }
}

class ActionHandler extends HTMLElement implements IActionHandler {
  public holdTime = 500;

  public ripple: Ripple;

  protected timer?: number;

  protected held = false;

  private cancelled = false;

  private dblClickTimeout?: number;

  private repeatTimeout: NodeJS.Timeout | undefined;

  private isRepeating = false;

  constructor() {
    super();
    this.ripple = document.createElement("mwc-ripple") as Ripple;
  }

  public connectedCallback(): void {
    Object.assign(this.style, {
      position: "fixed",
      width: isTouch ? "100px" : "50px",
      height: isTouch ? "100px" : "50px",
      transform: "translate(-50%, -50%) scale(0)",
      pointerEvents: "none",
      zIndex: "999",
      background: "var(--primary-color)",
      display: null,
      opacity: "0.2",
      borderRadius: "50%",
      transition: "transform 180ms ease-in-out",
    });

    this.appendChild(this.ripple);
    this.ripple.primary = true;

    for (const ev of [
      "touchcancel",
      "mouseout",
      "mouseup",
      "touchmove",
      "mousewheel",
      "wheel",
      "scroll",
    ]) {
      document.addEventListener(
        ev,
        () => {
          this.cancelled = true;
          if (this.timer) {
            this._stopAnimation();
            clearTimeout(this.timer);
            this.timer = undefined;
            if (this.isRepeating && this.repeatTimeout) {
              clearInterval(this.repeatTimeout);
              this.isRepeating = false;
            }
          }
        },
        { passive: true },
      );
    }
  }

  public bind(
    element: ActionHandlerElement,
    options: CustomActionHandlerOptions = {},
  ): void {
    if (
      element.actionHandler &&
      deepEqual(options, element.actionHandler.options)
    ) {
      return;
    }

    if (element.actionHandler) {
      if (element.actionHandler.start) {
        element.removeEventListener("touchstart", element.actionHandler.start);
        element.removeEventListener("mousedown", element.actionHandler.start);
      }
      if (element.actionHandler.end) {
        element.removeEventListener("touchend", element.actionHandler.end);
        element.removeEventListener("touchcancel", element.actionHandler.end);
        element.removeEventListener("click", element.actionHandler.end);
      }
      if (element.actionHandler.handleKeyDown) {
        element.removeEventListener(
          "keydown",
          element.actionHandler.handleKeyDown,
        );
      }
    } else {
      element.addEventListener("contextmenu", (ev: Event) => {
        const e = ev || window.event;
        if (e.preventDefault) {
          e.preventDefault();
        }
        if (e.stopPropagation) {
          e.stopPropagation();
        }
        e.cancelBubble = true;
        e.returnValue = false;
        return false;
      });
    }

    element.actionHandler = { options };

    if (options.disabled) {
      return;
    }

    element.actionHandler.start = (ev: Event) => {
      if (options.stopPropagation) {
        ev.stopPropagation();
      }
      this.cancelled = false;
      let x: number;
      let y: number;
      if ((ev as TouchEvent).touches) {
        x = (ev as TouchEvent).touches[0].clientX;
        y = (ev as TouchEvent).touches[0].clientY;
      } else {
        x = (ev as MouseEvent).clientX;
        y = (ev as MouseEvent).clientY;
      }

      if (options.hasHold) {
        this.held = false;
        this.timer = window.setTimeout(() => {
          this._startAnimation(x, y);
          this.held = true;
          if (options.repeat && !this.isRepeating) {
            this.isRepeating = true;
            this.repeatTimeout = setInterval(() => {
              fireEvent(element, "action", { action: "hold" });
            }, options.repeat);
          }
        }, this.holdTime);
      }
    };

    element.actionHandler.end = (ev: Event) => {
      if (options.stopPropagation) {
        ev.stopPropagation();
      }
      // Don't respond when moved or scrolled while touch
      if (
        ev.type === "touchcancel" ||
        (ev.type === "touchend" && this.cancelled)
      ) {
        if (this.isRepeating && this.repeatTimeout) {
          clearInterval(this.repeatTimeout);
          this.isRepeating = false;
        }
        return;
      }
      const target = ev.target as HTMLElement;
      // Prevent mouse event if touch event
      if (ev.cancelable) {
        ev.preventDefault();
      }
      if (options.hasHold) {
        clearTimeout(this.timer);
        if (this.isRepeating && this.repeatTimeout) {
          clearInterval(this.repeatTimeout);
        }
        this.isRepeating = false;
        this._stopAnimation();
        this.timer = undefined;
      }
      if (options.hasHold && this.held) {
        if (!options.repeat) {
          fireEvent(target, "action", { action: "hold" });
        }
      } else if (options.hasDoubleClick) {
        if (
          (ev.type === "click" && (ev as MouseEvent).detail < 2) ||
          !this.dblClickTimeout
        ) {
          this.dblClickTimeout = window.setTimeout(() => {
            this.dblClickTimeout = undefined;
            fireEvent(target, "action", { action: "tap" });
          }, 250);
        } else {
          clearTimeout(this.dblClickTimeout);
          this.dblClickTimeout = undefined;
          fireEvent(target, "action", { action: "double_tap" });
        }
      } else {
        fireEvent(target, "action", { action: "tap" });
      }
    };

    element.actionHandler.handleKeyDown = (ev: KeyboardEvent) => {
      if (!["Enter", " "].includes(ev.key)) {
        return;
      }
      (ev.currentTarget as ActionHandlerElement).actionHandler?.end?.(ev);
    };

    element.addEventListener("touchstart", element.actionHandler.start, {
      passive: true,
    });
    element.addEventListener("touchend", element.actionHandler.end);
    element.addEventListener("touchcancel", element.actionHandler.end);

    element.addEventListener("mousedown", element.actionHandler.start, {
      passive: true,
    });
    element.addEventListener("click", element.actionHandler.end);

    element.addEventListener("keydown", element.actionHandler.handleKeyDown);
  }

  private _startAnimation(x: number, y: number) {
    Object.assign(this.style, {
      left: `${x}px`,
      top: `${y}px`,
      transform: "translate(-50%, -50%) scale(1)",
    });
    this.ripple.disabled = false;
    this.ripple.startPress();
    this.ripple.unbounded = true;
  }

  private _stopAnimation() {
    this.ripple.endPress();
    this.ripple.disabled = true;
    Object.assign(this.style, {
      left: null,
      top: null,
      transform: "translate(-50%, -50%) scale(0)",
    });
  }
}

customElements.define("paper-buttons-row-action-handler", ActionHandler);

const getActionHandler = (): ActionHandler => {
  const body = document.body;
  if (body.querySelector("paper-buttons-row-action-handler")) {
    return body.querySelector(
      "paper-buttons-row-action-handler",
    ) as ActionHandler;
  }

  const actionHandler = document.createElement(
    "paper-buttons-row-action-handler",
  );
  body.appendChild(actionHandler);

  return actionHandler as ActionHandler;
};

export const actionHandlerBind = (
  element: ActionHandlerElement,
  options?: CustomActionHandlerOptions,
): void => {
  const actionHandler: ActionHandler = getActionHandler();
  if (!actionHandler) {
    return;
  }
  actionHandler.bind(element, options);
};

export const actionHandler = directive(
  class extends Directive {
    update(part: AttributePart, [options]: DirectiveParameters<this>) {
      actionHandlerBind(part.element as ActionHandlerElement, options);
      return noChange;
    }

    render(_options?: CustomActionHandlerOptions) {}
  },
);


================================================
FILE: src/action.ts
================================================
import {
  fireEvent,
  forwardHaptic,
  type HomeAssistant,
  navigate,
  toggleEntity,
} from "custom-card-helpers";
import type { ButtonActionConfig, ButtonConfig } from "./types";
import { showToast } from "./utils";

export const handleAction = (
  node: HTMLElement,
  hass: HomeAssistant,
  config: ButtonConfig,
  action: string,
): void => {
  let actionConfig: ButtonActionConfig | undefined;

  if (action === "double_tap" && config.double_tap_action) {
    actionConfig = config.double_tap_action;
  } else if (action === "hold" && config.hold_action) {
    actionConfig = config.hold_action;
  } else if (action === "tap" && config.tap_action) {
    actionConfig = config.tap_action;
  }

  handleActionConfig(node, hass, config, actionConfig);
};

export function handleActionConfig(
  node: HTMLElement,
  hass: HomeAssistant,
  config: ButtonConfig,
  actionConfig: ButtonActionConfig | undefined,
) {
  if (!actionConfig) {
    actionConfig = {
      action: "more-info",
    };
  }

  if (
    actionConfig.confirmation &&
    (!actionConfig.confirmation.exemptions ||
      !actionConfig.confirmation.exemptions.some(
        (e) => e.user === hass?.user?.id,
      ))
  ) {
    forwardHaptic("warning");

    if (
      !confirm(
        actionConfig.confirmation.text ||
          `Are you sure you want to ${actionConfig.action}?`,
      )
    ) {
      return;
    }
  }

  switch (actionConfig.action) {
    case "more-info": {
      const entityId = actionConfig.entity || config.entity;
      if (entityId) {
        fireEvent(node, "hass-more-info", { entityId });
      } else {
        showToast(node, {
          message: hass.localize(
            "ui.panel.lovelace.cards.actions.no_entity_more_info",
          ),
        });
        forwardHaptic("failure");
      }
      break;
    }
    case "navigate":
      if (!actionConfig.navigation_path) {
        showToast(node, {
          message: hass.localize(
            "ui.panel.lovelace.cards.actions.no_navigation_path",
          ),
        });
        forwardHaptic("failure");
        return;
      }
      navigate(node, actionConfig.navigation_path);
      forwardHaptic("light");
      break;
    case "url":
      if (!actionConfig.url_path) {
        showToast(node, {
          message: hass.localize("ui.panel.lovelace.cards.actions.no_url"),
        });
        forwardHaptic("failure");
        return;
      }
      window.open(actionConfig.url_path);
      forwardHaptic("light");
      break;
    case "toggle":
      if (!config.entity) {
        showToast(node, {
          message: hass.localize(
            "ui.panel.lovelace.cards.actions.no_entity_toggle",
          ),
        });
        forwardHaptic("failure");
        return;
      }
      toggleEntity(hass, config.entity);
      forwardHaptic("light");
      break;
    case "call-service": {
      if (!actionConfig.service) {
        showToast(node, {
          message: hass.localize("ui.panel.lovelace.cards.actions.no_service"),
        });
        forwardHaptic("failure");
        return;
      }
      const [domain, service] = actionConfig.service.split(".", 2);
      hass.callService(
        domain,
        service,
        actionConfig.service_data,
        actionConfig.target,
      );
      forwardHaptic("light");
      break;
    }
    case "fire-event": {
      if (!actionConfig.event_type) {
        showToast(node, {
          message: "No event to call specified",
        });
        forwardHaptic("failure");
        return;
      }
      hass.callApi(
        "POST",
        `events/${actionConfig.event_type}`,
        actionConfig.event_data || {},
      );
      forwardHaptic("light");
      break;
    }
    case "fire-dom-event": {
      fireEvent(node, "ll-custom", actionConfig);
      forwardHaptic("light");
    }
  }
}

export function hasAction(config?: ButtonActionConfig): boolean {
  return config !== undefined && config.action !== "none";
}


================================================
FILE: src/const.ts
================================================
export const DOMAINS_TOGGLE = new Set([
  "fan",
  "input_boolean",
  "light",
  "switch",
  "group",
  "automation",
  "cover",
  "script",
  "vacuum",
  "lock",
]);

export const STATES_ON = new Set(["open", "unlocked", "on"]);
export const STATE_ON = "on";
export const STATE_OFF = "off";

export const STATE_UNAVAILABLE = "unavailable";

export const TEMPLATE_OPTIONS = ["name", "icon", "image", "state"];


================================================
FILE: src/entity-row.ts
================================================
import { provideHass } from "card-tools/src/hass";
import {
  createThing,
  fireEvent,
  type HomeAssistant,
} from "custom-card-helpers";
import type { LitElement, PropertyValues } from "lit";
import type { ExternalPaperButtonRowConfig } from "./types";

interface LovelaceElement extends LitElement {
  hass?: HomeAssistant;
  config?: Record<string, unknown>;
  _config?: Record<string, unknown>;
}

type FirstUpdatedFn = (
  this: LovelaceElement,
  changedProperties: PropertyValues,
) => void;

export function createModule(element: string, firstUpdated: FirstUpdatedFn) {
  customElements.whenDefined(element).then(() => {
    const el = customElements.get(element) as CustomElementConstructor;
    const oFirstUpdated = el.prototype.firstUpdated;

    el.prototype.firstUpdated = function (changedProperties) {
      oFirstUpdated.call(this, changedProperties);
      firstUpdated.call(this, changedProperties);
    };

    fireEvent(window, "ll-rebuild", {});
  });
}

createModule("hui-generic-entity-row", function () {
  if (this.config?.extend_paper_buttons_row && this.shadowRoot) {
    const pbConfig = this.config
      .extend_paper_buttons_row as ExternalPaperButtonRowConfig;

    const paperButtons = createThing(
      {
        type: "custom:paper-buttons-row",
        ...pbConfig,
        is_extended_row: true,
      },
      true,
    );

    provideHass(paperButtons);

    let el = this.shadowRoot.querySelector<HTMLElement>("slot");
    if (!el) return;

    if (el.parentElement) {
      if (el.parentElement.parentElement) {
        if (
          el.parentElement.classList.contains("state") &&
          el.parentElement.parentElement.classList.contains("text-content")
        ) {
          el = el.parentElement.parentElement;
        } else {
          console.error("unexpected parent node found");
        }
      } else if (el.parentElement.classList.contains("text-content")) {
        el = el.parentElement;
      } else {
        console.error("unexpected parent node found");
      }
    }

    if (pbConfig.hide_state) {
      el.style.display = "none";
    }

    if (pbConfig.hide_badge) {
      const el = this.shadowRoot.querySelector<HTMLElement>("state-badge");
      if (el) {
        el.style.visibility = "hidden";
        el.style.marginLeft = "-48px";
      }
    }

    if (pbConfig.position === "right") {
      insertAfter(paperButtons, el);
    } else {
      insertBefore(paperButtons, el);
    }
  }
});

function insertBefore(node: HTMLElement, element: Element) {
  element.parentNode?.insertBefore(node, element);
}

function insertAfter(node: HTMLElement, element: Element) {
  if (element.nextElementSibling) {
    insertBefore(node, element.nextElementSibling);
  } else {
    element.parentNode?.appendChild(node);
  }
}


================================================
FILE: src/entity.ts
================================================
import { computeEntity, type HomeAssistant } from "custom-card-helpers";
import type { ButtonConfig } from "./types";

export const computeStateName = (stateObj) => {
  if (stateObj?.attributes?.friendly_name) {
    return stateObj.attributes.friendly_name;
  }
  return stateObj?.entity_id
    ? computeEntity(stateObj.entity_id).replace(/_/g, " ")
    : "Unknown";
};

function computeActionTooltip(hass, state, config, isHold) {
  if (!config || !config.action || config.action === "none") {
    return "";
  }
  let tooltip = `${
    isHold
      ? hass.localize("ui.panel.lovelace.cards.picture-elements.hold")
      : hass.localize("ui.panel.lovelace.cards.picture-elements.tap")
  } `;
  switch (config.action) {
    case "navigate":
      tooltip += `${hass.localize(
        "ui.panel.lovelace.cards.picture-elements.navigate_to",
        "location",
        config.navigation_path,
      )}`;
      break;
    case "url":
      tooltip += `${hass.localize(
        "ui.panel.lovelace.cards.picture-elements.url",
        "url_path",
        config.url_path,
      )}`;
      break;
    case "toggle":
      tooltip += `${hass.localize(
        "ui.panel.lovelace.cards.picture-elements.toggle",
        "name",
        state,
      )}`;
      break;
    case "call-service":
      tooltip += `${hass.localize(
        "ui.panel.lovelace.cards.picture-elements.call_service",
        "name",
        config.service,
      )}`;
      break;
    case "more-info":
      tooltip += `${hass.localize(
        "ui.panel.lovelace.cards.picture-elements.more_info",
        "name",
        state,
      )}`;
      break;
  }
  return tooltip;
}

export const computeTooltip = (config: ButtonConfig, hass?: HomeAssistant) => {
  if (!hass || config.tooltip === false) {
    return "";
  }
  if (config.tooltip) {
    return config.tooltip;
  }
  let stateName = "";
  let tooltip = "";
  if (config.entity) {
    stateName =
      config.entity in hass.states
        ? computeStateName(hass.states[config.entity])
        : config.entity;
  }
  if (!config.tap_action && !config.hold_action) {
    return stateName;
  }
  const tapTooltip = config.tap_action
    ? computeActionTooltip(hass, stateName, config.tap_action, false)
    : "";
  const holdTooltip = config.hold_action
    ? computeActionTooltip(hass, stateName, config.hold_action, true)
    : "";
  const newline = tapTooltip && holdTooltip ? "\n" : "";
  tooltip = tapTooltip + newline + holdTooltip;
  return tooltip;
};


================================================
FILE: src/get-lovelace.ts
================================================
import type { LovelaceConfig } from "custom-card-helpers";

type HuiRootElement = HTMLElement & {
  lovelace: {
    config: LovelaceConfig;
    current_view: number;
    [key: string]: unknown;
  };
  ___curView: number;
};

declare global {
  interface HTMLElementTagNameMap {
    "hui-root": HuiRootElement;
  }
}

export const getLovelace = (): HuiRootElement["lovelace"] | null => {
  const root = document
    .querySelector("home-assistant")
    ?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot;

  const resolver =
    root?.querySelector("ha-drawer partial-panel-resolver") ||
    root?.querySelector("app-drawer-layout partial-panel-resolver");

  const huiRoot = (resolver?.shadowRoot || resolver)
    ?.querySelector("ha-panel-lovelace")
    ?.shadowRoot?.querySelector("hui-root");

  if (huiRoot) {
    const ll = huiRoot.lovelace;
    ll.current_view = huiRoot.___curView;
    return ll;
  }
  return null;
};


================================================
FILE: src/main.ts
================================================
import { hass } from "card-tools/src/hass";
import {
  type ActionHandlerEvent,
  computeDomain,
  type HomeAssistant,
} from "custom-card-helpers";
import deepmerge from "deepmerge";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, type PropertyValues, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { type StyleInfo, styleMap } from "lit/directives/style-map.js";
import { handleAction, hasAction } from "./action";
import { actionHandler } from "./action-handler";
import {
  DOMAINS_TOGGLE,
  STATE_OFF,
  STATE_ON,
  STATE_UNAVAILABLE,
  STATES_ON,
  TEMPLATE_OPTIONS,
} from "./const";
import { computeStateName, computeTooltip } from "./entity";
import { handleButtonPreset } from "./presets";
import styles from "./styles.css?inline";
import { renderTemplateObjects, subscribeTemplate } from "./template";
import type {
  ButtonConfig,
  ExternalButtonConfig,
  ExternalButtonType,
  ExternalPaperButtonRowConfig,
  PaperButtonRowConfig,
  StyleConfig,
} from "./types";
import { arrayToObject } from "./utils";
import "./entity-row";

console.groupCollapsed(
  `%c ${__NAME__} %c ${__VERSION__} `,
  "color: white; background: #039be5; font-weight: 700;",
  "color: #039be5; background: white; font-weight: 700;",
);
console.info(`branch   : ${__BRANCH__}`);
console.info(`commit   : ${__COMMIT__}`);
console.info(`built at : ${__BUILD_TIME__}`);
console.info("https://github.com/jcwillox/lovelace-paper-buttons-row");
console.groupEnd();

const computeStateIcon = (config: ButtonConfig) => {
  if (config.state_icons && typeof config.state === "string")
    return config.state_icons[config.state.toLowerCase()];
  return undefined;
};

const computeStateText = (config: ButtonConfig) => {
  if (config.state_text && typeof config.state === "string")
    return config.state_text[config.state.toLowerCase()] || config.state;
  return config.state;
};

const migrateIconAlignment = (alignment: string) => {
  console.warn(
    __NAME__,
    "'align_icon' and 'align_icons' is deprecated and will be removed in a future version",
  );
  switch (alignment) {
    case "top":
      return [["icon", "name"]];
    case "bottom":
      return [["name", "icon"]];
    case "right":
      return ["name", "icon"];
    default:
      return ["icon", "name"];
  }
};

@customElement("paper-buttons-row")
export class PaperButtonsRow extends LitElement {
  static readonly styles = unsafeCSS(styles);

  @property() private hass?: HomeAssistant;
  @property() private _config?: PaperButtonRowConfig;

  _templates?: unknown[];
  _entities?: string[];

  // convert an externally set config to the correct internal structure
  private _transformConfig(
    config: ExternalPaperButtonRowConfig,
  ): PaperButtonRowConfig {
    // check valid config
    if (!config) throw new Error("Invalid configuration");
    if (!config.buttons) throw new Error("Missing buttons.");
    if (!Array.isArray(config.buttons))
      throw new Error("Buttons must be an array.");
    if (config.buttons.length <= 0)
      throw new Error("At least one button required.");

    // deep copy config
    config = JSON.parse(JSON.stringify(config));

    // ensure we always have 1 row
    if (config.buttons.every((item) => !Array.isArray(item))) {
      config.buttons = [config.buttons as Array<ExternalButtonType>];
    } else if (!config.buttons.every((item) => Array.isArray(item))) {
      throw new Error("Cannot mix rows and buttons");
    }

    if (config.styles === undefined) {
      // ensure styles is not undefined
      config.styles = {} as StyleConfig;
    } else {
      // ensure styles are an object
      for (const key in config.styles) {
        config.styles[key] = arrayToObject(config.styles[key]);
      }
    }

    config.buttons = (config.buttons as Array<Array<ExternalButtonType>>).map(
      (row) => {
        return row.map((bConfig) => {
          // handle when config is not defined as a dictionary.
          if (typeof bConfig === "string") {
            bConfig = { entity: bConfig };
          }

          bConfig = deepmerge(config.base_config || {}, bConfig);

          // transform layout config
          if (typeof bConfig.layout === "string") {
            bConfig.layout = bConfig.layout
              .split("|")
              .map((column) =>
                column.includes("_") ? column.split("_") : column,
              );
          }

          // ensure active is a list
          if (typeof bConfig.active === "string") {
            bConfig.active = [bConfig.active];
          }

          // migrate `style` to `styles`
          if (bConfig.styles === undefined) {
            bConfig.styles = bConfig.style;
          }
          if (bConfig.styles === undefined) {
            // ensure styles is not undefined
            bConfig.styles = {} as StyleConfig;
          } else {
            // ensure styles are an object
            for (const key in bConfig.styles) {
              bConfig.styles[key] = arrayToObject(bConfig.styles[key]);
            }
          }
          if (bConfig.state_styles) {
            // ensure styles are an object
            for (const stateKey in bConfig.state_styles) {
              for (const key in bConfig.state_styles[stateKey]) {
                bConfig.state_styles[stateKey][key] = arrayToObject(
                  bConfig.state_styles[stateKey][key],
                );
              }
            }
          }

          // apply default services.
          bConfig = this._defaultConfig(config, bConfig);

          return bConfig;
        });
      },
    );

    return config as PaperButtonRowConfig;
  }

  setConfig(config: ExternalPaperButtonRowConfig) {
    this._config = this._transformConfig(config);
    if (!this.hass) {
      this.hass = hass() as HomeAssistant;
    }
    this._entities = [];
    this._templates = [];

    // fix config.
    this._config.buttons = this._config.buttons.map((row) => {
      return row.map((config) => {
        config = handleButtonPreset(config, this._config);

        // create list of entities to monitor for changes.
        if (config.entity) {
          this._entities?.push(config.entity);
        }

        // subscribe template options
        for (const key of TEMPLATE_OPTIONS) {
          subscribeTemplate.call(this, config, config, key);
        }

        // subscribe template styles
        for (const styles of Object.values(config.styles)) {
          if (typeof styles === "object")
            for (const key of Object.keys(styles)) {
              subscribeTemplate.call(this, config, styles, key);
            }
        }

        return config;
      });
    });
  }

  render() {
    if (!this._config || !this.hass) {
      return html``;
    }

    renderTemplateObjects(this._templates, this.hass);

    return html`
      ${
        this._config.extra_styles
          ? html`
            <style>
              ${this._config.extra_styles}
            </style>
          `
          : ""
      }
      ${this._config.buttons.map((row) => {
        return html`
          <div
            class="flex-box"
            style="${styleMap(this._config?.styles as StyleInfo)}"
          >
            ${row.map((config) => {
              const stateObj =
                (config.entity !== undefined &&
                  this.hass?.states[config.entity]) ||
                undefined;
              const domain = config.entity && computeDomain(config.entity);
              const styles = this._getStyles(config);
              const buttonStyles = {
                ...this._getBaseStyles(),
                ...this._getStateStyles(domain, stateObj),
                ...(styles.button || {}),
              } as StyleInfo;

              const activeStates = config.active
                ? new Set(config.active)
                : STATES_ON;

              return html`
                <paper-button
                  @action="${(ev: ActionHandlerEvent) =>
                    this._handleAction(ev, config)}"
                  .actionHandler="${actionHandler({
                    hasHold: hasAction(config.hold_action),
                    hasDoubleClick: hasAction(config.double_tap_action),
                    repeat: config.hold_action?.repeat,
                    stopPropagation: !!this._config?.is_extended_row,
                  })}"
                  style="${styleMap(buttonStyles)}"
                  class="${this._getClass(
                    activeStates,
                    config.state,
                    stateObj?.state,
                  )}"
                  title="${computeTooltip(config, this.hass)}"
                  data-domain="${ifDefined(domain)}"
                  data-entity-state="${ifDefined(stateObj?.state)}"
                  data-state="${ifDefined(
                    typeof config.state === "string" &&
                      config.state.toLowerCase(),
                  )}"
                >
                  ${config.layout?.map((column) => {
                    if (Array.isArray(column))
                      return html`
                        <div class="flex-column">
                          ${column.map((row) =>
                            this.renderElement(row, config, styles, stateObj),
                          )}
                        </div>
                      `;
                    return this.renderElement(column, config, styles, stateObj);
                  })}

                  <paper-ripple
                    center
                    style="${styleMap((styles.ripple || {}) as StyleInfo)}"
                    class="${this._getRippleClass(config)}"
                  ></paper-ripple>
                </paper-button>
              `;
            })}
          </div>
        `;
      })}
    `;
  }

  renderElement(
    item: string,
    config: ButtonConfig,
    styles: StyleConfig,
    entity?: HassEntity,
  ) {
    const style: StyleInfo = styles?.[item] || {};
    switch (item) {
      case "icon":
        return this.renderIcon(config, style, entity);
      case "name":
        return this.renderName(config, style, entity);
      case "state":
        return this.renderState(config, style);
    }
  }

  renderIcon(config: ButtonConfig, style: StyleInfo, entity?: HassEntity) {
    const icon =
      config.icon !== false && (config.icon || config.entity)
        ? computeStateIcon(config) || config.icon
        : false;

    return config.image
      ? html`<img
          src="${config.image}"
          class="image"
          style="${styleMap(style)}"
          alt="icon"
        />`
      : icon || entity
        ? html`
          <ha-state-icon
          style="${styleMap(style)}"
          .hass=${this.hass}
          .stateObj=${entity}
          .state=${entity}
          .icon="${icon}"
        />`
        : "";
  }

  renderName(config: ButtonConfig, style: StyleInfo, stateObj?: HassEntity) {
    return config.name !== false && (config.name || config.entity)
      ? html`
        <span style="${styleMap(style)}">
            ${config.name || computeStateName(stateObj)}
          </span>
      `
      : "";
  }

  renderState(config: ButtonConfig, style: StyleInfo) {
    return config.state !== false
      ? html`
        <span style="${styleMap(style)}"> ${computeStateText(config)} </span>
      `
      : "";
  }

  private _handleAction(ev: ActionHandlerEvent, config: ButtonConfig): void {
    if (this.hass && config && ev.detail.action) {
      if (this._config?.is_extended_row) {
        ev.stopPropagation();
      }
      handleAction(this, this.hass, config, ev.detail.action);
    }
  }

  _getClass(
    activeStates: Set<string>,
    state?: ButtonConfig["state"],
    entityState?: string,
  ) {
    if (typeof state === "string" && activeStates.has(state.toLowerCase())) {
      return "button-active";
    }
    if (STATE_UNAVAILABLE === entityState) {
      return "button-unavailable";
    }
    return "";
  }

  _getBaseStyles(): StyleInfo {
    const hex = getComputedStyle(this).getPropertyValue("--state-icon-color");
    return {
      "--rgb-state-default-color": this._hexToRgb(hex)?.join(", "),
    };
  }

  _getStateStyles(domain?: string, stateObj?: HassEntity): StyleInfo {
    if (!domain || !stateObj) return {};

    if (stateObj.attributes.rgb_color) {
      return {
        "--pbs-button-rgb-state-color": stateObj.attributes.rgb_color,
      };
    }

    const rgb = this._getStateColor(stateObj, domain);
    if (rgb) {
      return {
        "--pbs-button-rgb-state-color": rgb.join(", "),
      };
    }

    return {};
  }

  _getStateColor = (stateObj: HassEntity, domain?: string) => {
    const styles = getComputedStyle(this);

    // from `device_class`
    if (stateObj.attributes.device_class) {
      const hex = styles.getPropertyValue(
        `--state-${domain}-${stateObj.attributes.device_class}-${stateObj.state}-color`,
      );
      if (hex) {
        return this._hexToRgb(hex);
      }
    }

    // from `state`
    let hex = styles.getPropertyValue(
      `--state-${domain}-${stateObj.state}-color`,
    );
    if (hex) return this._hexToRgb(hex);

    // from `domain`
    if (stateObj.state === STATE_ON || stateObj.state === STATE_OFF) {
      const active = stateObj.state === STATE_ON ? "active" : "inactive";

      hex = styles.getPropertyValue(`--state-${domain}-${active}-color`);
      if (hex) return this._hexToRgb(hex);

      if (stateObj.state === STATE_ON) {
        hex = styles.getPropertyValue("--state-active-color");
        if (hex) return this._hexToRgb(hex);
      }
    }
  };

  _hexToRgb(hex: string) {
    return hex.match(/[A-Za-z0-9]{2}/g)?.map((v) => Number.parseInt(v, 16));
  }

  _getRippleClass(config: ButtonConfig) {
    switch (config.ripple) {
      case "none":
        return "hidden";
      case "circle":
        return "circle";
      case "fill":
        return "";
    }
    if (config.layout?.length === 1 && config.layout[0] === "icon") {
      return "circle";
    }
    if (
      config.name ||
      (config.name !== false && config.entity) ||
      config.layout?.includes("state")
    ) {
      return "";
    }
    return "circle";
  }

  _getStyles(config: ButtonConfig): StyleConfig {
    if (typeof config.state !== "string" || !config.state_styles) {
      return config.styles;
    }
    const stateStyle = config.state_styles[config.state.toLowerCase()];
    if (!stateStyle) {
      return config.styles;
    }
    return deepmerge(config.styles, stateStyle);
  }

  _defaultConfig(
    config: ExternalPaperButtonRowConfig,
    bConfig: ExternalButtonConfig,
  ) {
    if (!bConfig.layout) {
      // migrate align_icon to layout
      const alignment = bConfig.align_icon || config.align_icons;
      if (alignment) bConfig.layout = migrateIconAlignment(alignment);
      else bConfig.layout = ["icon", "name"];
    }

    // default state template
    if (!bConfig.state && bConfig.entity) {
      bConfig.state = { case: "upper" };
    }

    if (bConfig.entity) {
      const domain = computeDomain(bConfig.entity);
      // default hold action
      if (!bConfig.hold_action) {
        bConfig.hold_action = { action: "more-info" };
      }
      // default tap action
      if (!bConfig.tap_action) {
        if (DOMAINS_TOGGLE.has(domain)) {
          bConfig.tap_action = { action: "toggle" };
        } else if (domain === "scene") {
          bConfig.tap_action = {
            action: "call-service",
            service: "scene.turn_on",
            service_data: {
              entity_id: bConfig.entity,
            },
          };
        } else {
          bConfig.tap_action = { action: "more-info" };
        }
      }
    }
    return bConfig;
  }

  shouldUpdate(changedProps: PropertyValues) {
    if (changedProps.has("_config")) {
      return true;
    }
    if (this._entities) {
      const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
      if (!oldHass) {
        return true;
      }
      // only update if monitored entity changed state.
      return this._entities.some(
        (entity) => oldHass.states[entity] !== this.hass?.states[entity],
      );
    }
    return false;
  }
}


================================================
FILE: src/presets.ts
================================================
import deepmerge from "deepmerge";
import { getLovelace } from "./get-lovelace";
import type { ButtonConfig, PaperButtonRowConfig } from "./types";

declare module "custom-card-helpers" {
  interface LovelaceConfig {
    paper_buttons_row?: {
      presets?: {
        [key: string]: ButtonConfig;
      };
    };
  }
}

let lovelace = getLovelace();

export function handleButtonPreset(
  bConfig: ButtonConfig,
  config?: PaperButtonRowConfig,
): ButtonConfig {
  if (!lovelace) lovelace = getLovelace();
  const userPresets = lovelace?.config?.paper_buttons_row?.presets || {};
  const preset = bConfig.preset || config?.preset;
  return preset
    ? deepmerge(
        {
          mushroom: presetMushroom,
        }[preset] ||
          userPresets[preset] ||
          {},
        bConfig,
      )
    : bConfig;
}

const presetMushroom: ButtonConfig = {
  ripple: "none",
  styles: {
    button: {
      "min-width": "42px",
      "min-height": "42px",
      "border-radius": "12px",
      "box-sizing": "border-box",
      transition: "background-color 280ms ease-in-out 0s",
      "--pbs-button-rgb-color": "var(--rgb-primary-text-color)",
      "--pbs-button-rgb-default-color": "var(--rgb-primary-text-color)",
      "--pbs-button-rgb-active-color": "var(--pbs-button-rgb-state-color)",
      "--pbs-button-rgb-bg-color": "var(--pbs-button-rgb-color)",
      "--pbs-button-rgb-bg-active-color": "var(--pbs-button-rgb-active-color)",
      "--pbs-button-rgb-bg-opacity": "0.05",
      "--pbs-button-rgb-bg-active-opacity": "0.2",
    },
  },
};


================================================
FILE: src/styles.css
================================================
.flex-box {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
}

.flex-column {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
}

.hidden {
  display: none;
}

paper-button {
  --pbs-button-rgb-fallback: 68, 115, 158;

  color: var(
    --pbs-button-color,
    rgb(
      var(
        --pbs-button-rgb-color,
        var(
          --pbs-button-rgb-state-color,
          var(
            --pbs-button-rgb-default-color,
            var(--rgb-state-default-color, var(--pbs-button-rgb-fallback))
          )
        )
      )
    )
  );
  background-color: var(
    --pbs-button-bg-color,
    rgba(var(--pbs-button-rgb-bg-color), var(--pbs-button-rgb-bg-opacity, 1))
  );

  padding: 6px;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  user-select: none;
}

span {
  padding: 2px;
  text-align: center;
}

ha-icon {
  padding: 2px;
}

.button-active {
  color: var(
    --paper-item-icon-active-color,
    var(
      --pbs-button-active-color,
      rgb(
        var(
          --pbs-button-rgb-active-color,
          var(
            --pbs-button-rgb-state-color,
            var(
              --pbs-button-rgb-default-color,
              var(--rgb-state-default-color, var(--pbs-button-rgb-fallback))
            )
          )
        )
      )
    )
  );
  background-color: var(
    --pbs-button-bg-active-color,
    rgba(
      var(--pbs-button-rgb-bg-active-color, var(--pbs-button-rgb-bg-color)),
      var(
        --pbs-button-rgb-bg-active-opacity,
        var(--pbs-button-rgb-bg-opacity, 1)
      )
    )
  );
}

.button-unavailable {
  color: var(
    --pbs-button-unavailable-color,
    rgb(var(--pbs-button-rgb-unavailable-color, var(--rgb-disabled-color)))
  );
}

.image {
  position: relative;
  display: inline-block;
  width: 28px;
  border-radius: 50%;
  height: 28px;
  text-align: center;
  background-size: cover;
  line-height: 28px;
  vertical-align: middle;
  box-sizing: border-box;
}

@keyframes blink {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* Safari and Chrome */
@-webkit-keyframes rotating {
  from {
    -webkit-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  to {
    -webkit-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@keyframes rotating {
  from {
    -ms-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  to {
    -ms-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -webkit-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

[rotating] {
  -webkit-animation: rotating 2s linear infinite;
  -moz-animation: rotating 2s linear infinite;
  -ms-animation: rotating 2s linear infinite;
  -o-animation: rotating 2s linear infinite;
  animation: rotating 2s linear infinite;
}


================================================
FILE: src/template.ts
================================================
import { hasTemplate, subscribeRenderTemplate } from "card-tools/src/templates";
import type { PaperButtonsRow } from "./main";

export function renderTemplateObjects(templates, hass) {
  for (const item of templates) {
    item.callback(renderTemplateObject(item.template, hass));
  }
}

export function renderTemplateObject(template, hass) {
  let state = hass.states[template.entity];

  if (!state) return;

  if (template.attribute) {
    state = state.attributes[template.attribute];
  } else {
    state = state.state;
  }

  let result = (template.prefix || "") + state + (template.postfix || "");

  if (template.case) {
    result = handleCase(result, template.case);
  }

  return result;
}

function handleCase(text, text_case) {
  switch (text_case) {
    case "upper":
      return text.toUpperCase();
    case "lower":
      return text.toLowerCase();
    case "first":
      return text[0].toUpperCase() + text.slice(1);
  }
}

export function subscribeTemplate(this: PaperButtonsRow, config, object, key) {
  const option = object[key];

  if (typeof option === "object") {
    if (!option.entity) option.entity = config.entity;

    if (option.entity !== config.entity) this._entities?.push(option.entity);

    this._templates?.push({
      template: option,
      callback: (res) => {
        if (res) {
          object[key] = res;
        }
      },
    });
  } else if (hasTemplate(option)) {
    subscribeRenderTemplate(
      null,
      (res) => {
        object[key] = res;
        this.requestUpdate();
      },
      {
        template: option,
        variables: { config: config },
      },
    );
    object[key] = "";
  }
}


================================================
FILE: src/types.ts
================================================
import type { ActionConfig, LovelaceCard } from "custom-card-helpers";
import type { HapticType } from "custom-card-helpers/src/haptic";
import type { BaseActionConfig } from "custom-card-helpers/src/types";

declare global {
  interface HTMLElementTagNameMap {
    "hui-error-card": LovelaceCard;
  }

  const __NAME__: string;
  const __BRANCH__: string;
  const __COMMIT__: string;
  const __VERSION__: string;
  const __BUILD_TIME__: string;
}

export interface TemplateConfig {
  entity?: string;
  attribute?: string;
  prefix?: string;
  postfix?: string;
  case?: "upper" | "lower" | "first";
}

export type Template = string | TemplateConfig;

export type StyleConfig = Partial<
  Record<
    "button" | "icon" | "name" | "state" | "ripple",
    Record<string, string | Template>
  >
>;

export interface ButtonConfig {
  entity?: string;
  name?: string | false | Template;
  state?: false | string | Template;
  icon?: false | string | Template;
  image?: string | Template;
  preset?: string;
  active?: string[];
  ripple?: "fill" | "none" | "circle";
  layout?: Array<string | Array<string>>;
  align_icon?: "top" | "left" | "right" | "bottom"; // deprecated
  tooltip?: string | false;
  tap_action?: ButtonActionConfig;
  hold_action?: ButtonActionConfig;
  double_tap_action?: ButtonActionConfig;
  styles: StyleConfig;
  state_styles?: Record<string, StyleConfig>;
  state_icons?: Record<string, string>;
  state_text?: Record<string, string>;
}

export interface PaperButtonRowConfig {
  type?: string;
  preset?: string;
  buttons: ButtonConfig[][];
  align_icons?: "top" | "left" | "right" | "bottom";
  base_config?: ButtonConfig;
  styles: Record<string, string | Template>;
  extra_styles?: string;
  position?: "center" | "right";
  hide_badge?: boolean;
  hide_state?: boolean;
  is_extended_row?: boolean;
}

export interface ExternalButtonConfig
  extends Omit<ButtonConfig, "layout" | "active" | "style" | "styles"> {
  layout?: string | Array<string | Array<string>>;
  active?: string | string[];
  style?: StyleConfig;
  styles?: StyleConfig;
}

export type ExternalButtonType = string | ExternalButtonConfig;

export interface ExternalPaperButtonRowConfig
  extends Omit<PaperButtonRowConfig, "buttons" | "styles"> {
  buttons: Array<ExternalButtonType | Array<ExternalButtonType>>;
  styles?: Record<string, string | Template>;
  is_extended_row?: boolean;
}

export interface FireEventActionConfig extends BaseActionConfig {
  action: "fire-event";
  event_type: string;
  event_data?: Record<string, unknown>;
  repeat?: number;
  haptic?: HapticType;
}

export type ButtonActionConfig = ActionConfig | FireEventActionConfig;


================================================
FILE: src/utils.ts
================================================
import { fireEvent } from "custom-card-helpers";

declare global {
  interface HASSDomEvents {
    "hass-notification": ShowToastParams;
  }
}

export interface ShowToastParams {
  message: string;
  action?: ToastActionParams;
  duration?: number;
  dismissable?: boolean;
}

export interface ToastActionParams {
  action: () => void;
  text: string;
}

export const showToast = (el: HTMLElement, params: ShowToastParams) => {
  return fireEvent(el, "hass-notification", params);
};

export const arrayToObject = (data) =>
  Array.isArray(data)
    ? // biome-ignore lint/performance/noAccumulatingSpread: rework this
      data.reduce((obj, item) => Object.assign(obj, item), {})
    : data;


================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "Node",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "noImplicitAny": false,
    "resolveJsonModule": true
  },
  "include": ["src", "vite.config.ts"]
}


================================================
FILE: vite.config.ts
================================================
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { defineConfig, type UserConfig } from "vite";
import viteCompression from "vite-plugin-compression";
import pkg from "./package.json";

const $ = async (command: string, env = "") =>
  process.env[env] ?? (await promisify(exec)(command)).stdout.trim();

const all = async (obj: Record<string, string | Promise<string>>) =>
  Object.fromEntries(
    await Promise.all(
      Object.entries(obj).map(async ([k, v]) => [k, JSON.stringify(await v)]),
    ),
  );

export default defineConfig(
  async (): Promise<UserConfig> => ({
    build: {
      target: "es6",
      lib: {
        entry: "src/main.ts",
        formats: ["es"],
      },
    },
    esbuild: {
      legalComments: "none",
    },
    plugins: [viteCompression({ verbose: false })],
    define: await all({
      __NAME__: pkg.name.toUpperCase(),
      __BRANCH__: $("git rev-parse --abbrev-ref HEAD", "GITHUB_REF_NAME"),
      __VERSION__: $("git describe --tags --dirty --always", "VERSION"),
      __COMMIT__: $("git rev-parse HEAD", "GITHUB_SHA"),
      __BUILD_TIME__: new Date().toISOString(),
    }),
  }),
);
Download .txt
gitextract_uym2nibo/

├── .editorconfig
├── .git-blame-ignore-revs
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json
│   └── workflows/
│       ├── ci.yaml
│       ├── release.yaml
│       └── validate.yaml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .vscode/
│   ├── extensions.json
│   ├── settings.json
│   └── tasks.json
├── LICENSE
├── README.md
├── biome.json
├── hacs.json
├── info.md
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── action-handler.ts
│   ├── action.ts
│   ├── const.ts
│   ├── entity-row.ts
│   ├── entity.ts
│   ├── get-lovelace.ts
│   ├── main.ts
│   ├── presets.ts
│   ├── styles.css
│   ├── template.ts
│   ├── types.ts
│   ├── utils.ts
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (68 symbols across 11 files)

FILE: src/action-handler.ts
  type Navigator (line 16) | interface Navigator {
  type CustomActionHandlerOptions (line 26) | interface CustomActionHandlerOptions extends ActionHandlerOptions {
  type Ripple (line 32) | interface Ripple extends HTMLElement {
  type IActionHandler (line 40) | interface IActionHandler extends HTMLElement {
  type ActionHandlerElement (line 48) | interface ActionHandlerElement extends HTMLElement {
  type HTMLElementTagNameMap (line 58) | interface HTMLElementTagNameMap {
  type HASSDomEvents (line 62) | interface HASSDomEvents {
  class ActionHandler (line 67) | class ActionHandler extends HTMLElement implements IActionHandler {
    method constructor (line 84) | constructor() {
    method connectedCallback (line 89) | public connectedCallback(): void {
    method bind (line 135) | public bind(
    method _startAnimation (line 286) | private _startAnimation(x: number, y: number) {
    method _stopAnimation (line 297) | private _stopAnimation() {
  method update (line 339) | update(part: AttributePart, [options]: DirectiveParameters<this>) {
  method render (line 344) | render(_options?: CustomActionHandlerOptions) {}

FILE: src/action.ts
  function handleActionConfig (line 30) | function handleActionConfig(
  function hasAction (line 154) | function hasAction(config?: ButtonActionConfig): boolean {

FILE: src/const.ts
  constant DOMAINS_TOGGLE (line 1) | const DOMAINS_TOGGLE = new Set([
  constant STATES_ON (line 14) | const STATES_ON = new Set(["open", "unlocked", "on"]);
  constant STATE_ON (line 15) | const STATE_ON = "on";
  constant STATE_OFF (line 16) | const STATE_OFF = "off";
  constant STATE_UNAVAILABLE (line 18) | const STATE_UNAVAILABLE = "unavailable";
  constant TEMPLATE_OPTIONS (line 20) | const TEMPLATE_OPTIONS = ["name", "icon", "image", "state"];

FILE: src/entity-row.ts
  type LovelaceElement (line 10) | interface LovelaceElement extends LitElement {
  type FirstUpdatedFn (line 16) | type FirstUpdatedFn = (
  function createModule (line 21) | function createModule(element: string, firstUpdated: FirstUpdatedFn) {
  function insertBefore (line 91) | function insertBefore(node: HTMLElement, element: Element) {
  function insertAfter (line 95) | function insertAfter(node: HTMLElement, element: Element) {

FILE: src/entity.ts
  function computeActionTooltip (line 13) | function computeActionTooltip(hass, state, config, isHold) {

FILE: src/get-lovelace.ts
  type HuiRootElement (line 3) | type HuiRootElement = HTMLElement & {
  type HTMLElementTagNameMap (line 13) | interface HTMLElementTagNameMap {

FILE: src/main.ts
  class PaperButtonsRow (line 79) | class PaperButtonsRow extends LitElement {
    method _transformConfig (line 89) | private _transformConfig(
    method setConfig (line 179) | setConfig(config: ExternalPaperButtonRowConfig) {
    method render (line 215) | render() {
    method renderElement (line 305) | renderElement(
    method renderIcon (line 322) | renderIcon(config: ButtonConfig, style: StyleInfo, entity?: HassEntity) {
    method renderName (line 347) | renderName(config: ButtonConfig, style: StyleInfo, stateObj?: HassEnti...
    method renderState (line 357) | renderState(config: ButtonConfig, style: StyleInfo) {
    method _handleAction (line 365) | private _handleAction(ev: ActionHandlerEvent, config: ButtonConfig): v...
    method _getClass (line 374) | _getClass(
    method _getBaseStyles (line 388) | _getBaseStyles(): StyleInfo {
    method _getStateStyles (line 395) | _getStateStyles(domain?: string, stateObj?: HassEntity): StyleInfo {
    method _hexToRgb (line 447) | _hexToRgb(hex: string) {
    method _getRippleClass (line 451) | _getRippleClass(config: ButtonConfig) {
    method _getStyles (line 473) | _getStyles(config: ButtonConfig): StyleConfig {
    method _defaultConfig (line 484) | _defaultConfig(
    method shouldUpdate (line 526) | shouldUpdate(changedProps: PropertyValues) {

FILE: src/presets.ts
  type LovelaceConfig (line 6) | interface LovelaceConfig {
  function handleButtonPreset (line 17) | function handleButtonPreset(

FILE: src/template.ts
  function renderTemplateObjects (line 4) | function renderTemplateObjects(templates, hass) {
  function renderTemplateObject (line 10) | function renderTemplateObject(template, hass) {
  function handleCase (line 30) | function handleCase(text, text_case) {
  function subscribeTemplate (line 41) | function subscribeTemplate(this: PaperButtonsRow, config, object, key) {

FILE: src/types.ts
  type HTMLElementTagNameMap (line 6) | interface HTMLElementTagNameMap {
  type TemplateConfig (line 17) | interface TemplateConfig {
  type Template (line 25) | type Template = string | TemplateConfig;
  type StyleConfig (line 27) | type StyleConfig = Partial<
  type ButtonConfig (line 34) | interface ButtonConfig {
  type PaperButtonRowConfig (line 55) | interface PaperButtonRowConfig {
  type ExternalButtonConfig (line 69) | interface ExternalButtonConfig
  type ExternalButtonType (line 77) | type ExternalButtonType = string | ExternalButtonConfig;
  type ExternalPaperButtonRowConfig (line 79) | interface ExternalPaperButtonRowConfig
  type FireEventActionConfig (line 86) | interface FireEventActionConfig extends BaseActionConfig {
  type ButtonActionConfig (line 94) | type ButtonActionConfig = ActionConfig | FireEventActionConfig;

FILE: src/utils.ts
  type HASSDomEvents (line 4) | interface HASSDomEvents {
  type ShowToastParams (line 9) | interface ShowToastParams {
  type ToastActionParams (line 16) | interface ToastActionParams {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (102K chars).
[
  {
    "path": ".editorconfig",
    "chars": 131,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\ninden"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 534,
    "preview": "# git-blame supports ignoring specific commits, this allows us to hide formatting\n# commits from git-blame.\n#\n# You can "
  },
  {
    "path": ".gitattributes",
    "chars": 67,
    "preview": "# normalise line endings to lf on all platforms\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 16,
    "preview": "ko_fi: jcwillox\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 3188,
    "preview": "name: Report an issue\ndescription: Report an issue with the Paper Buttons Row plugin.\nbody:\n  - type: markdown\n    attri"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1239,
    "preview": "name: Feature request\ndescription: Suggest an idea for this project.\ntitle: \"[FR]: \"\nbody:\n  - type: markdown\n    attrib"
  },
  {
    "path": ".github/renovate.json",
    "chars": 245,
    "preview": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"github>jcwillox/renovate-config\", \":a"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 945,
    "preview": "name: \"CI\"\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"feat**\"\n      - \"fix**\"\n    tags-ignore:\n      - \"**\"\n  pu"
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 1253,
    "preview": "name: \"Release\"\n\non:\n  push:\n    branches:\n      - \"beta\"\n      - \"alpha\"\n  workflow_dispatch:\n    inputs:\n      draft:\n"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "chars": 434,
    "preview": "name: \"Validate\"\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"feat**\"\n      - \"fix**\"\n    tags-ignore:\n      - \"**"
  },
  {
    "path": ".gitignore",
    "chars": 1837,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": ".husky/pre-commit",
    "chars": 12,
    "preview": "lint-staged\n"
  },
  {
    "path": ".npmrc",
    "chars": 16,
    "preview": "save-exact=true\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 228,
    "preview": "{\n  \"recommendations\": [\n    \"formulahendry.auto-rename-tag\",\n    \"editorconfig.editorconfig\",\n    \"dbaeumer.vscode-esli"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 467,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.linkedEditing\": true,\n  \"editor.renderWhitespace\": \"trailing\",\n  \"html.autoCl"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 374,
    "preview": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"npm\",\n      \"script\": \"install\",\n      \"label\": \"npm: install\""
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2025 Joshua Cowie-Willox\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "README.md",
    "chars": 32001,
    "preview": "# Paper Buttons Row\n\n[![HACS Badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge)](https://g"
  },
  {
    "path": "biome.json",
    "chars": 939,
    "preview": "{\n  \"$schema\": \"https://biomejs.dev/schemas/2.1.3/schema.json\",\n  \"files\": {\n    \"includes\": [\"**\", \"!**/pnpm-lock.yaml\""
  },
  {
    "path": "hacs.json",
    "chars": 34,
    "preview": "{\n  \"name\": \"Paper Buttons Row\"\n}\n"
  },
  {
    "path": "info.md",
    "chars": 2131,
    "preview": "This is a complete rewrite of the original [`button-entity-row`](https://github.com/custom-cards/button-entity-row) plug"
  },
  {
    "path": "package.json",
    "chars": 1136,
    "preview": "{\n  \"name\": \"paper-buttons-row\",\n  \"version\": \"0.0.0-dev\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"pr"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 56,
    "preview": "onlyBuiltDependencies:\n  - '@biomejs/biome'\n  - esbuild\n"
  },
  {
    "path": "src/action-handler.ts",
    "chars": 9143,
    "preview": "import {\n  type ActionHandlerDetail,\n  type ActionHandlerOptions,\n  fireEvent,\n} from \"custom-card-helpers\";\nimport deep"
  },
  {
    "path": "src/action.ts",
    "chars": 3955,
    "preview": "import {\n  fireEvent,\n  forwardHaptic,\n  type HomeAssistant,\n  navigate,\n  toggleEntity,\n} from \"custom-card-helpers\";\ni"
  },
  {
    "path": "src/const.ts",
    "chars": 410,
    "preview": "export const DOMAINS_TOGGLE = new Set([\n  \"fan\",\n  \"input_boolean\",\n  \"light\",\n  \"switch\",\n  \"group\",\n  \"automation\",\n  "
  },
  {
    "path": "src/entity-row.ts",
    "chars": 2790,
    "preview": "import { provideHass } from \"card-tools/src/hass\";\nimport {\n  createThing,\n  fireEvent,\n  type HomeAssistant,\n} from \"cu"
  },
  {
    "path": "src/entity.ts",
    "chars": 2488,
    "preview": "import { computeEntity, type HomeAssistant } from \"custom-card-helpers\";\nimport type { ButtonConfig } from \"./types\";\n\ne"
  },
  {
    "path": "src/get-lovelace.ts",
    "chars": 938,
    "preview": "import type { LovelaceConfig } from \"custom-card-helpers\";\n\ntype HuiRootElement = HTMLElement & {\n  lovelace: {\n    conf"
  },
  {
    "path": "src/main.ts",
    "chars": 16308,
    "preview": "import { hass } from \"card-tools/src/hass\";\nimport {\n  type ActionHandlerEvent,\n  computeDomain,\n  type HomeAssistant,\n}"
  },
  {
    "path": "src/presets.ts",
    "chars": 1555,
    "preview": "import deepmerge from \"deepmerge\";\nimport { getLovelace } from \"./get-lovelace\";\nimport type { ButtonConfig, PaperButton"
  },
  {
    "path": "src/styles.css",
    "chars": 3059,
    "preview": ".flex-box {\n  display: flex;\n  justify-content: space-evenly;\n  align-items: center;\n}\n\n.flex-column {\n  display: inline"
  },
  {
    "path": "src/template.ts",
    "chars": 1657,
    "preview": "import { hasTemplate, subscribeRenderTemplate } from \"card-tools/src/templates\";\nimport type { PaperButtonsRow } from \"."
  },
  {
    "path": "src/types.ts",
    "chars": 2662,
    "preview": "import type { ActionConfig, LovelaceCard } from \"custom-card-helpers\";\nimport type { HapticType } from \"custom-card-help"
  },
  {
    "path": "src/utils.ts",
    "chars": 694,
    "preview": "import { fireEvent } from \"custom-card-helpers\";\n\ndeclare global {\n  interface HASSDomEvents {\n    \"hass-notification\": "
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 570,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"Node\",\n    \"lib\": [\"ES"
  },
  {
    "path": "vite.config.ts",
    "chars": 1172,
    "preview": "import { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { defineConfig, type UserConfig"
  }
]

About this extraction

This page contains the full source code of the jcwillox/lovelace-paper-buttons-row GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (93.6 KB), approximately 22.6k tokens, and a symbol index with 68 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!