Clean up: Remove build files, update README with current features
- Removed TypeScript source and build configuration files - Updated README with all current features (icon picker, icon size, show/hide options) - Updated example configuration - Card is now standalone JavaScript file ready to use
This commit is contained in:
90
README.md
90
README.md
@@ -6,22 +6,24 @@ A custom Lovelace card for Home Assistant that combines a button entity (for tog
|
|||||||
|
|
||||||
- **Button Toggle**: Click the card to send a toggle command to your button entity
|
- **Button Toggle**: Click the card to send a toggle command to your button entity
|
||||||
- **Status Display**: Shows the current state from your binary sensor entity
|
- **Status Display**: Shows the current state from your binary sensor entity
|
||||||
- **Custom Icons**: Support for custom icons with separate icons for on/off states
|
- **Custom Icons**: Icon picker with Material Design Icons support
|
||||||
- **Visual Feedback**: Status indicator dot and icon color changes based on state
|
- **Icon Size Control**: Adjustable icon size (16px to 500px)
|
||||||
- **Card Editor**: Full UI editor support in Lovelace
|
- **Visual Feedback**: Icon color changes based on state (yellow when on, gray when off)
|
||||||
|
- **Show/Hide Options**: Toggle visibility of name and status text
|
||||||
|
- **Card Editor**: Full visual editor support in Lovelace with filtered entity pickers
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
1. Copy `dist/light-button-card.js` to your `www/community/light-button-card/` directory in Home Assistant
|
1. Copy `light-button-card.js` to your `www/light-button-card/` directory in Home Assistant
|
||||||
2. Add the resource to your Lovelace configuration:
|
2. Add the resource to your Lovelace configuration:
|
||||||
|
- Go to **Settings** → **Dashboards** → **Resources**
|
||||||
```yaml
|
- Click **"+ ADD RESOURCE"**
|
||||||
resources:
|
- Enter the URL: `/local/light-button-card/light-button-card.js`
|
||||||
- url: /local/light-button-card/light-button-card.js
|
- Set Type to: **JavaScript Module**
|
||||||
type: module
|
- Click **CREATE**
|
||||||
```
|
3. Refresh your browser (hard refresh: Ctrl+Shift+R)
|
||||||
|
|
||||||
### HACS Installation (if published)
|
### HACS Installation (if published)
|
||||||
|
|
||||||
@@ -48,12 +50,23 @@ type: custom:light-button-card
|
|||||||
button_entity: button.plc_light_1 # Required: Button entity to toggle
|
button_entity: button.plc_light_1 # Required: Button entity to toggle
|
||||||
binary_sensor_entity: binary_sensor.plc_light_1_status # Required: Binary sensor for status
|
binary_sensor_entity: binary_sensor.plc_light_1_status # Required: Binary sensor for status
|
||||||
name: Living Room Light # Optional: Display name
|
name: Living Room Light # Optional: Display name
|
||||||
icon: mdi:lightbulb # Optional: Default icon (MDI format)
|
icon: mdi:lightbulb # Optional: Icon (MDI format)
|
||||||
icon_on: mdi:lightbulb # Optional: Icon when state is "on"
|
icon_size: 64 # Optional: Icon size in pixels (16-500, default: 64)
|
||||||
icon_off: mdi:lightbulb-outline # Optional: Icon when state is "off"
|
|
||||||
show_name: true # Optional: Show/hide name (default: true)
|
show_name: true # Optional: Show/hide name (default: true)
|
||||||
|
show_status: true # Optional: Show/hide status text (default: true)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Visual Editor
|
||||||
|
|
||||||
|
The card includes a full visual editor with:
|
||||||
|
- **Button Entity Picker**: Filtered to show only button entities
|
||||||
|
- **Binary Sensor Entity Picker**: Filtered to show only binary sensor entities
|
||||||
|
- **Icon Picker**: Dropdown with Material Design Icons
|
||||||
|
- **Icon Size Field**: Number input for icon size (16-500px)
|
||||||
|
- **Name Field**: Text input for card name
|
||||||
|
- **Show Name Switch**: Toggle name visibility
|
||||||
|
- **Show Status Switch**: Toggle status text visibility
|
||||||
|
|
||||||
## Icon Configuration
|
## Icon Configuration
|
||||||
|
|
||||||
Icons use Material Design Icons (MDI) format. You can use any icon from [Material Design Icons](https://materialdesignicons.com/).
|
Icons use Material Design Icons (MDI) format. You can use any icon from [Material Design Icons](https://materialdesignicons.com/).
|
||||||
@@ -64,6 +77,7 @@ Examples:
|
|||||||
- `mdi:lamp` - Lamp
|
- `mdi:lamp` - Lamp
|
||||||
- `mdi:ceiling-light` - Ceiling light
|
- `mdi:ceiling-light` - Ceiling light
|
||||||
- `mdi:wall-sconce` - Wall sconce
|
- `mdi:wall-sconce` - Wall sconce
|
||||||
|
- `mdi:light-recessed` - Recessed light
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
@@ -72,9 +86,10 @@ type: custom:light-button-card
|
|||||||
button_entity: button.plc_kitchen_light
|
button_entity: button.plc_kitchen_light
|
||||||
binary_sensor_entity: binary_sensor.plc_kitchen_light_status
|
binary_sensor_entity: binary_sensor.plc_kitchen_light_status
|
||||||
name: Kitchen Light
|
name: Kitchen Light
|
||||||
icon_on: mdi:lightbulb
|
icon: mdi:lightbulb
|
||||||
icon_off: mdi:lightbulb-outline
|
icon_size: 90
|
||||||
show_name: true
|
show_name: true
|
||||||
|
show_status: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
@@ -84,25 +99,19 @@ show_name: true
|
|||||||
2. **Binary Sensor Entity**: The card continuously monitors the binary sensor entity to display the current state (on/off) of the PLC output.
|
2. **Binary Sensor Entity**: The card continuously monitors the binary sensor entity to display the current state (on/off) of the PLC output.
|
||||||
|
|
||||||
3. **Visual Feedback**:
|
3. **Visual Feedback**:
|
||||||
- The icon changes color and style based on the state
|
- The icon changes color based on the state (yellow when on, gray when off)
|
||||||
- A status indicator dot shows green when on, gray when off
|
- Icon size is customizable from 16px to 500px
|
||||||
- The state text displays the current state
|
- Name and status text can be shown or hidden
|
||||||
|
- Hover effects provide visual feedback
|
||||||
|
|
||||||
## Development
|
## File Structure
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
HomeAssistant-Light-Buttons/
|
||||||
The compiled file will be in `dist/light-button-card.js`.
|
├── light-button-card.js # Main card file (ready to use)
|
||||||
|
├── example-configuration.yaml # Example YAML configuration
|
||||||
### Development Mode
|
├── hacs.json # HACS metadata
|
||||||
|
└── README.md # This file
|
||||||
```bash
|
|
||||||
npm run watch
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -111,7 +120,24 @@ npm run watch
|
|||||||
- Button entity (for toggling)
|
- Button entity (for toggling)
|
||||||
- Binary sensor entity (for status)
|
- Binary sensor entity (for status)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Card not appearing
|
||||||
|
1. Clear browser cache (Ctrl+Shift+R)
|
||||||
|
2. Reload Lovelace resources (Settings → Dashboards → Resources → Reload resources)
|
||||||
|
3. Verify the resource URL is `/local/light-button-card/light-button-card.js`
|
||||||
|
4. Check browser console (F12) for errors
|
||||||
|
|
||||||
|
### Icon size not changing
|
||||||
|
1. Hard refresh browser (Ctrl+Shift+R)
|
||||||
|
2. Enter the size value directly in the field
|
||||||
|
3. Check that the value is between 16 and 500
|
||||||
|
|
||||||
|
### Can only type one character in editor
|
||||||
|
1. Clear browser cache completely
|
||||||
|
2. Hard refresh (Ctrl+Shift+R)
|
||||||
|
3. The editor should maintain focus while typing
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ type: custom:light-button-card
|
|||||||
button_entity: button.plc_light_1
|
button_entity: button.plc_light_1
|
||||||
binary_sensor_entity: binary_sensor.plc_light_1_status
|
binary_sensor_entity: binary_sensor.plc_light_1_status
|
||||||
name: Living Room Light
|
name: Living Room Light
|
||||||
icon_on: mdi:lightbulb
|
icon: mdi:lightbulb
|
||||||
icon_off: mdi:lightbulb-outline
|
icon_size: 64
|
||||||
show_name: true
|
show_name: true
|
||||||
|
show_status: true
|
||||||
|
|
||||||
# Another example with different icon
|
# Another example with different settings
|
||||||
# type: custom:light-button-card
|
# type: custom:light-button-card
|
||||||
# button_entity: button.plc_light_2
|
# button_entity: button.plc_light_2
|
||||||
# binary_sensor_entity: binary_sensor.plc_light_2_status
|
# binary_sensor_entity: binary_sensor.plc_light_2_status
|
||||||
# name: Kitchen Light
|
# name: Kitchen Light
|
||||||
# icon_on: mdi:ceiling-light
|
# icon: mdi:ceiling-light
|
||||||
# icon_off: mdi:ceiling-light-outline
|
# icon_size: 90
|
||||||
# show_name: true
|
# show_name: true
|
||||||
|
# show_status: false
|
||||||
|
|||||||
458
light-button-card.js
Normal file
458
light-button-card.js
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
// Light Button Card for Home Assistant
|
||||||
|
// Based on: https://developers.home-assistant.io/docs/frontend/custom-ui/custom-card/
|
||||||
|
|
||||||
|
class LightButtonCard extends HTMLElement {
|
||||||
|
set hass(hass) {
|
||||||
|
this._hass = hass;
|
||||||
|
this._updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
if (!config.button_entity || !config.binary_sensor_entity) {
|
||||||
|
throw new Error('button_entity and binary_sensor_entity are required');
|
||||||
|
}
|
||||||
|
this._config = {
|
||||||
|
icon: 'mdi:lightbulb',
|
||||||
|
name: '',
|
||||||
|
show_name: true,
|
||||||
|
show_status: true,
|
||||||
|
icon_size: 64,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
this._updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateContent() {
|
||||||
|
if (!this._config || !this._hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.content) {
|
||||||
|
this.innerHTML = `
|
||||||
|
<ha-card>
|
||||||
|
<div class="button-container">
|
||||||
|
<div class="icon-container">
|
||||||
|
<ha-icon class="icon"></ha-icon>
|
||||||
|
</div>
|
||||||
|
<div class="name"></div>
|
||||||
|
<div class="state-text"></div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
this.content = this.querySelector('.button-container');
|
||||||
|
this.icon = this.querySelector('ha-icon');
|
||||||
|
this.nameDiv = this.querySelector('.name');
|
||||||
|
this.stateText = this.querySelector('.state-text');
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
this.querySelector('ha-card').addEventListener('click', () => this._handleClick());
|
||||||
|
|
||||||
|
// Add styles
|
||||||
|
if (!this.styleSheet) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
ha-card {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
ha-card:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
ha-card:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
ha-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
transition: color 0.3s, transform 0.2s;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
--mdc-icon-size: var(--icon-size, 64px) !important;
|
||||||
|
}
|
||||||
|
ha-icon svg,
|
||||||
|
ha-icon ha-svg-icon,
|
||||||
|
ha-icon ha-svg-icon svg {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.icon-container.on ha-icon {
|
||||||
|
color: var(--paper-item-icon-active-color, #fdd835);
|
||||||
|
filter: drop-shadow(0 0 8px rgba(253, 216, 53, 0.6));
|
||||||
|
}
|
||||||
|
.icon-container.off ha-icon {
|
||||||
|
color: var(--disabled-text-color, #9e9e9e);
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.state-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.appendChild(style);
|
||||||
|
this.styleSheet = style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = this._getState();
|
||||||
|
const isOn = this._isOn();
|
||||||
|
const icon = this._getIcon();
|
||||||
|
const iconSize = this._config.icon_size || 64;
|
||||||
|
|
||||||
|
// Update icon
|
||||||
|
this.icon.setAttribute('icon', icon);
|
||||||
|
this.icon.style.setProperty('--icon-size', `${iconSize}px`);
|
||||||
|
this.icon.style.width = `${iconSize}px`;
|
||||||
|
this.icon.style.height = `${iconSize}px`;
|
||||||
|
this.icon.style.minWidth = `${iconSize}px`;
|
||||||
|
this.icon.style.minHeight = `${iconSize}px`;
|
||||||
|
this.icon.style.maxWidth = `${iconSize}px`;
|
||||||
|
this.icon.style.maxHeight = `${iconSize}px`;
|
||||||
|
this.icon.style.setProperty('--mdc-icon-size', `${iconSize}px`);
|
||||||
|
|
||||||
|
// Ensure internal SVG scales - check multiple possible structures
|
||||||
|
const svgIcon = this.icon.querySelector('ha-svg-icon');
|
||||||
|
if (svgIcon) {
|
||||||
|
svgIcon.style.width = `${iconSize}px`;
|
||||||
|
svgIcon.style.height = `${iconSize}px`;
|
||||||
|
svgIcon.style.setProperty('--mdc-icon-size', `${iconSize}px`);
|
||||||
|
const svg = svgIcon.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
svg.style.width = `${iconSize}px`;
|
||||||
|
svg.style.height = `${iconSize}px`;
|
||||||
|
svg.setAttribute('width', `${iconSize}`);
|
||||||
|
svg.setAttribute('height', `${iconSize}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const svg = this.icon.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
svg.style.width = `${iconSize}px`;
|
||||||
|
svg.style.height = `${iconSize}px`;
|
||||||
|
svg.setAttribute('width', `${iconSize}`);
|
||||||
|
svg.setAttribute('height', `${iconSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update icon container class
|
||||||
|
const iconContainer = this.querySelector('.icon-container');
|
||||||
|
if (isOn) {
|
||||||
|
iconContainer.classList.add('on');
|
||||||
|
iconContainer.classList.remove('off');
|
||||||
|
} else {
|
||||||
|
iconContainer.classList.add('off');
|
||||||
|
iconContainer.classList.remove('on');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update name
|
||||||
|
if (this._config.show_name && this._config.name) {
|
||||||
|
this.nameDiv.textContent = this._config.name;
|
||||||
|
this.nameDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.nameDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state text
|
||||||
|
if (this._config.show_status) {
|
||||||
|
this.stateText.textContent = state;
|
||||||
|
this.stateText.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.stateText.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getState() {
|
||||||
|
if (!this._hass || !this._config.binary_sensor_entity) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
const stateObj = this._hass.states[this._config.binary_sensor_entity];
|
||||||
|
if (!stateObj) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
return stateObj.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isOn() {
|
||||||
|
const state = this._getState();
|
||||||
|
return state === 'on' || state === 'true' || state === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
_getIcon() {
|
||||||
|
return this._config.icon || 'mdi:lightbulb';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleClick() {
|
||||||
|
if (!this._hass || !this._config.button_entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonEntity = this._hass.states[this._config.button_entity];
|
||||||
|
if (!buttonEntity) {
|
||||||
|
console.error(`Button entity ${this._config.button_entity} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the button press service
|
||||||
|
await this._hass.callService('button', 'press', {
|
||||||
|
entity_id: this._config.button_entity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardSize() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStubConfig() {
|
||||||
|
return {
|
||||||
|
button_entity: 'button.example',
|
||||||
|
binary_sensor_entity: 'binary_sensor.example',
|
||||||
|
icon: 'mdi:lightbulb',
|
||||||
|
name: 'Light',
|
||||||
|
show_name: true,
|
||||||
|
show_status: true,
|
||||||
|
icon_size: 64,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('light-button-card', LightButtonCard);
|
||||||
|
|
||||||
|
// Card editor
|
||||||
|
class LightButtonCardEditor extends HTMLElement {
|
||||||
|
set hass(hass) {
|
||||||
|
this._hass = hass;
|
||||||
|
if (this.content && this._elements) {
|
||||||
|
// Update hass on existing elements
|
||||||
|
this._elements.buttonPicker.hass = hass;
|
||||||
|
this._elements.sensorPicker.hass = hass;
|
||||||
|
} else if (this.content) {
|
||||||
|
this._updateEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
this._config = { ...config };
|
||||||
|
if (!this.content) {
|
||||||
|
this._updateEditor();
|
||||||
|
} else {
|
||||||
|
// Update values without rebuilding
|
||||||
|
this._updateEditorValues();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateEditorValues() {
|
||||||
|
if (!this._elements) return;
|
||||||
|
|
||||||
|
if (this._elements.buttonPicker) {
|
||||||
|
this._elements.buttonPicker.value = this._config.button_entity || '';
|
||||||
|
}
|
||||||
|
if (this._elements.sensorPicker) {
|
||||||
|
this._elements.sensorPicker.value = this._config.binary_sensor_entity || '';
|
||||||
|
}
|
||||||
|
if (this._elements.iconPicker) {
|
||||||
|
this._elements.iconPicker.value = this._config.icon || 'mdi:lightbulb';
|
||||||
|
}
|
||||||
|
if (this._elements.iconSizeField) {
|
||||||
|
this._elements.iconSizeField.value = this._config.icon_size || 64;
|
||||||
|
}
|
||||||
|
if (this._elements.nameField) {
|
||||||
|
this._elements.nameField.value = this._config.name || '';
|
||||||
|
}
|
||||||
|
if (this._elements.showNameSwitch) {
|
||||||
|
this._elements.showNameSwitch.checked = this._config.show_name !== false;
|
||||||
|
}
|
||||||
|
if (this._elements.showStatusSwitch) {
|
||||||
|
this._elements.showStatusSwitch.checked = this._config.show_status !== false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateEditor() {
|
||||||
|
if (!this._hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.content) {
|
||||||
|
this.innerHTML = `
|
||||||
|
<div class="card-config"></div>
|
||||||
|
`;
|
||||||
|
this.content = this.querySelector('.card-config');
|
||||||
|
|
||||||
|
// Add styles
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.card-config {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear and rebuild editor only if elements don't exist
|
||||||
|
if (!this._elements) {
|
||||||
|
this.content.innerHTML = '';
|
||||||
|
this._elements = {};
|
||||||
|
|
||||||
|
// Button Entity Picker
|
||||||
|
const buttonPicker = document.createElement('ha-entity-picker');
|
||||||
|
buttonPicker.label = 'Button Entity';
|
||||||
|
buttonPicker.hass = this._hass;
|
||||||
|
buttonPicker.value = this._config.button_entity || '';
|
||||||
|
buttonPicker.configValue = 'button_entity';
|
||||||
|
buttonPicker.required = true;
|
||||||
|
buttonPicker.includeDomains = ['button'];
|
||||||
|
buttonPicker.addEventListener('value-changed', (ev) => {
|
||||||
|
this._config.button_entity = ev.detail.value;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
this.content.appendChild(buttonPicker);
|
||||||
|
this._elements.buttonPicker = buttonPicker;
|
||||||
|
|
||||||
|
// Binary Sensor Entity Picker
|
||||||
|
const sensorPicker = document.createElement('ha-entity-picker');
|
||||||
|
sensorPicker.label = 'Binary Sensor Entity';
|
||||||
|
sensorPicker.hass = this._hass;
|
||||||
|
sensorPicker.value = this._config.binary_sensor_entity || '';
|
||||||
|
sensorPicker.configValue = 'binary_sensor_entity';
|
||||||
|
sensorPicker.required = true;
|
||||||
|
sensorPicker.includeDomains = ['binary_sensor'];
|
||||||
|
sensorPicker.addEventListener('value-changed', (ev) => {
|
||||||
|
this._config.binary_sensor_entity = ev.detail.value;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
this.content.appendChild(sensorPicker);
|
||||||
|
this._elements.sensorPicker = sensorPicker;
|
||||||
|
|
||||||
|
// Icon Picker
|
||||||
|
const iconPicker = document.createElement('ha-icon-picker');
|
||||||
|
iconPicker.label = 'Icon';
|
||||||
|
iconPicker.value = this._config.icon || 'mdi:lightbulb';
|
||||||
|
iconPicker.configValue = 'icon';
|
||||||
|
iconPicker.addEventListener('value-changed', (ev) => {
|
||||||
|
this._config.icon = ev.detail.value;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
this.content.appendChild(iconPicker);
|
||||||
|
this._elements.iconPicker = iconPicker;
|
||||||
|
|
||||||
|
// Icon Size field
|
||||||
|
const iconSizeField = document.createElement('ha-textfield');
|
||||||
|
iconSizeField.label = 'Icon Size (pixels)';
|
||||||
|
iconSizeField.type = 'number';
|
||||||
|
iconSizeField.value = this._config.icon_size || 64;
|
||||||
|
iconSizeField.configValue = 'icon_size';
|
||||||
|
iconSizeField.min = 16;
|
||||||
|
iconSizeField.max = 500;
|
||||||
|
iconSizeField.step = 1;
|
||||||
|
iconSizeField.addEventListener('input', (ev) => {
|
||||||
|
const value = parseInt(ev.target.value);
|
||||||
|
if (!isNaN(value) && value >= 16) {
|
||||||
|
this._config.icon_size = value;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.content.appendChild(iconSizeField);
|
||||||
|
this._elements.iconSizeField = iconSizeField;
|
||||||
|
|
||||||
|
// Name field
|
||||||
|
const nameField = document.createElement('ha-textfield');
|
||||||
|
nameField.label = 'Name (optional)';
|
||||||
|
nameField.value = this._config.name || '';
|
||||||
|
nameField.configValue = 'name';
|
||||||
|
nameField.addEventListener('input', (ev) => {
|
||||||
|
this._config.name = ev.target.value;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
this.content.appendChild(nameField);
|
||||||
|
this._elements.nameField = nameField;
|
||||||
|
|
||||||
|
// Show Name switch
|
||||||
|
const showNameContainer = document.createElement('div');
|
||||||
|
showNameContainer.style.display = 'flex';
|
||||||
|
showNameContainer.style.alignItems = 'center';
|
||||||
|
showNameContainer.style.justifyContent = 'space-between';
|
||||||
|
const showNameLabel = document.createElement('div');
|
||||||
|
showNameLabel.textContent = 'Show Name';
|
||||||
|
showNameLabel.style.flex = '1';
|
||||||
|
const showNameSwitch = document.createElement('ha-switch');
|
||||||
|
showNameSwitch.checked = this._config.show_name !== false;
|
||||||
|
showNameSwitch.configValue = 'show_name';
|
||||||
|
showNameSwitch.addEventListener('change', (ev) => {
|
||||||
|
this._config.show_name = ev.target.checked;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
showNameContainer.appendChild(showNameLabel);
|
||||||
|
showNameContainer.appendChild(showNameSwitch);
|
||||||
|
this.content.appendChild(showNameContainer);
|
||||||
|
this._elements.showNameSwitch = showNameSwitch;
|
||||||
|
|
||||||
|
// Show Status switch
|
||||||
|
const showStatusContainer = document.createElement('div');
|
||||||
|
showStatusContainer.style.display = 'flex';
|
||||||
|
showStatusContainer.style.alignItems = 'center';
|
||||||
|
showStatusContainer.style.justifyContent = 'space-between';
|
||||||
|
const showStatusLabel = document.createElement('div');
|
||||||
|
showStatusLabel.textContent = 'Show Status';
|
||||||
|
showStatusLabel.style.flex = '1';
|
||||||
|
const showStatusSwitch = document.createElement('ha-switch');
|
||||||
|
showStatusSwitch.checked = this._config.show_status !== false;
|
||||||
|
showStatusSwitch.configValue = 'show_status';
|
||||||
|
showStatusSwitch.addEventListener('change', (ev) => {
|
||||||
|
this._config.show_status = ev.target.checked;
|
||||||
|
this._fireConfigChanged();
|
||||||
|
});
|
||||||
|
showStatusContainer.appendChild(showStatusLabel);
|
||||||
|
showStatusContainer.appendChild(showStatusSwitch);
|
||||||
|
this.content.appendChild(showStatusContainer);
|
||||||
|
this._elements.showStatusSwitch = showStatusSwitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_fireConfigChanged() {
|
||||||
|
const event = new CustomEvent('config-changed', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { config: { ...this._config } }
|
||||||
|
});
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('light-button-card-editor', LightButtonCardEditor);
|
||||||
|
|
||||||
|
// Register card for card picker
|
||||||
|
LightButtonCard.getConfigElement = () => {
|
||||||
|
return document.createElement('light-button-card-editor');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.customCards = window.customCards || [];
|
||||||
|
window.customCards.push({
|
||||||
|
type: 'light-button-card',
|
||||||
|
name: 'Light Button Card',
|
||||||
|
description: 'A card with button toggle and binary sensor status',
|
||||||
|
});
|
||||||
31
package.json
31
package.json
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "homeassistant-light-buttons",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Custom Lovelace card for Home Assistant with button toggle and binary sensor status",
|
|
||||||
"main": "dist/light-button-card.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --mode production",
|
|
||||||
"watch": "webpack --mode development --watch"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"home-assistant",
|
|
||||||
"lovelace",
|
|
||||||
"custom-card"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.0.0",
|
|
||||||
"ts-loader": "^9.5.0",
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"webpack": "^5.88.0",
|
|
||||||
"webpack-cli": "^5.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lit": "^2.8.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"home-assistant-js-websocket": "^8.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
import { LitElement, html, css, TemplateResult } from 'lit';
|
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
|
||||||
|
|
||||||
// Type definitions for Home Assistant
|
|
||||||
interface HomeAssistant {
|
|
||||||
states: { [key: string]: HassEntity };
|
|
||||||
callService(domain: string, service: string, data?: any): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HassEntity {
|
|
||||||
entity_id: string;
|
|
||||||
state: string;
|
|
||||||
attributes: { [key: string]: any };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LovelaceCardEditor {
|
|
||||||
setConfig(config: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LightButtonCardConfig {
|
|
||||||
type: string;
|
|
||||||
button_entity: string;
|
|
||||||
binary_sensor_entity: string;
|
|
||||||
icon?: string;
|
|
||||||
icon_on?: string;
|
|
||||||
icon_off?: string;
|
|
||||||
name?: string;
|
|
||||||
show_name?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to fire events
|
|
||||||
function fireEvent(node: HTMLElement, type: string, detail?: any): void {
|
|
||||||
const event = new CustomEvent(type, {
|
|
||||||
detail,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
node.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('light-button-card')
|
|
||||||
export class LightButtonCard extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
@state() private _config!: LightButtonCardConfig;
|
|
||||||
|
|
||||||
static getStubConfig(): Partial<LightButtonCardConfig> {
|
|
||||||
return {
|
|
||||||
button_entity: 'button.example',
|
|
||||||
binary_sensor_entity: 'binary_sensor.example',
|
|
||||||
icon: 'mdi:lightbulb',
|
|
||||||
name: 'Light',
|
|
||||||
show_name: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfig(config: LightButtonCardConfig): void {
|
|
||||||
if (!config.button_entity || !config.binary_sensor_entity) {
|
|
||||||
throw new Error('button_entity and binary_sensor_entity are required');
|
|
||||||
}
|
|
||||||
this._config = {
|
|
||||||
icon: 'mdi:lightbulb',
|
|
||||||
icon_on: 'mdi:lightbulb',
|
|
||||||
icon_off: 'mdi:lightbulb-outline',
|
|
||||||
name: '',
|
|
||||||
show_name: true,
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
ha-card {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card:hover {
|
|
||||||
transform: scale(1.02);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
transition: color 0.3s, transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-container.on ha-icon {
|
|
||||||
color: var(--paper-item-icon-active-color, #fdd835);
|
|
||||||
filter: drop-shadow(0 0 8px rgba(253, 216, 53, 0.6));
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-container.off ha-icon {
|
|
||||||
color: var(--disabled-text-color, #9e9e9e);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: var(--disabled-text-color, #9e9e9e);
|
|
||||||
border: 2px solid var(--card-background-color, #ffffff);
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.on {
|
|
||||||
background-color: #4caf50;
|
|
||||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.state-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getState(): string {
|
|
||||||
if (!this.hass || !this._config.binary_sensor_entity) {
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
const stateObj = this.hass.states[this._config.binary_sensor_entity];
|
|
||||||
if (!stateObj) {
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
return stateObj.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isOn(): boolean {
|
|
||||||
const state = this._getState();
|
|
||||||
return state === 'on' || state === 'true' || state === '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getIcon(): string {
|
|
||||||
if (this._isOn()) {
|
|
||||||
return this._config.icon_on || this._config.icon || 'mdi:lightbulb';
|
|
||||||
}
|
|
||||||
return this._config.icon_off || this._config.icon || 'mdi:lightbulb-outline';
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleClick(): Promise<void> {
|
|
||||||
if (!this.hass || !this._config.button_entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonEntity = this.hass.states[this._config.button_entity];
|
|
||||||
if (!buttonEntity) {
|
|
||||||
console.error(`Button entity ${this._config.button_entity} not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the button press service
|
|
||||||
await this.hass.callService('button', 'press', {
|
|
||||||
entity_id: this._config.button_entity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fire event for card updates
|
|
||||||
fireEvent(this, 'hass-more-info', { entityId: this._config.button_entity });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this._config || !this.hass) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOn = this._isOn();
|
|
||||||
const state = this._getState();
|
|
||||||
const icon = this._getIcon();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-card @click=${this._handleClick}>
|
|
||||||
<div class="button-container">
|
|
||||||
<div class="icon-container ${isOn ? 'on' : 'off'}">
|
|
||||||
<ha-icon .icon=${icon}></ha-icon>
|
|
||||||
<div class="status-indicator ${isOn ? 'on' : ''}"></div>
|
|
||||||
</div>
|
|
||||||
${this._config.show_name && this._config.name
|
|
||||||
? html`<div class="name">${this._config.name}</div>`
|
|
||||||
: ''}
|
|
||||||
<div class="state-text">${state}</div>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'light-button-card': LightButtonCard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card editor for Lovelace UI
|
|
||||||
@customElement('light-button-card-editor')
|
|
||||||
export class LightButtonCardEditor extends LitElement implements LovelaceCardEditor {
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
@property({ attribute: false }) private _config?: LightButtonCardConfig;
|
|
||||||
|
|
||||||
public setConfig(config: LightButtonCardConfig): void {
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
.card-config {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (!this.hass || !this._config) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="card-config">
|
|
||||||
<ha-entity-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._config.button_entity}
|
|
||||||
.label=${'Button Entity'}
|
|
||||||
.configValue=${'button_entity'}
|
|
||||||
.required=${true}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
></ha-entity-picker>
|
|
||||||
|
|
||||||
<ha-entity-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._config.binary_sensor_entity}
|
|
||||||
.label=${'Binary Sensor Entity'}
|
|
||||||
.configValue=${'binary_sensor_entity'}
|
|
||||||
.required=${true}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
></ha-entity-picker>
|
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
.label=${'Name (optional)'}
|
|
||||||
.value=${this._config.name || ''}
|
|
||||||
.configValue=${'name'}
|
|
||||||
@input=${this._valueChanged}
|
|
||||||
></ha-textfield>
|
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
.label=${'Icon (MDI icon name, optional)'}
|
|
||||||
.value=${this._config.icon || ''}
|
|
||||||
.configValue=${'icon'}
|
|
||||||
@input=${this._valueChanged}
|
|
||||||
></ha-textfield>
|
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
.label=${'Icon On (MDI icon name, optional)'}
|
|
||||||
.value=${this._config.icon_on || ''}
|
|
||||||
.configValue=${'icon_on'}
|
|
||||||
@input=${this._valueChanged}
|
|
||||||
></ha-textfield>
|
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
.label=${'Icon Off (MDI icon name, optional)'}
|
|
||||||
.value=${this._config.icon_off || ''}
|
|
||||||
.configValue=${'icon_off'}
|
|
||||||
@input=${this._valueChanged}
|
|
||||||
></ha-textfield>
|
|
||||||
|
|
||||||
<ha-switch
|
|
||||||
.checked=${this._config.show_name !== false}
|
|
||||||
.configValue=${'show_name'}
|
|
||||||
@change=${this._valueChanged}
|
|
||||||
>
|
|
||||||
<span>Show Name</span>
|
|
||||||
</ha-switch>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: Event): void {
|
|
||||||
if (!this._config || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = ev.target as any;
|
|
||||||
const configValue = target.configValue;
|
|
||||||
|
|
||||||
if (configValue === 'show_name') {
|
|
||||||
this._config = {
|
|
||||||
...this._config,
|
|
||||||
[configValue]: target.checked,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this._config = {
|
|
||||||
...this._config,
|
|
||||||
[configValue]: target.value || target.checked,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, 'config-changed', { config: this._config });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'light-button-card-editor': LightButtonCardEditor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the card editor
|
|
||||||
if (customElements.get('light-button-card-editor') === undefined) {
|
|
||||||
customElements.define('light-button-card-editor', LightButtonCardEditor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card configuration helper for Lovelace
|
|
||||||
(window as any).customCards = (window as any).customCards || [];
|
|
||||||
(window as any).customCards.push({
|
|
||||||
type: 'light-button-card',
|
|
||||||
name: 'Light Button Card',
|
|
||||||
description: 'A card with button toggle and binary sensor status',
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ES2020",
|
|
||||||
"lib": ["ES2020", "DOM"],
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './src/light-button-card.ts',
|
|
||||||
output: {
|
|
||||||
filename: 'light-button-card.js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
library: {
|
|
||||||
type: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
outputModule: true,
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.ts$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.js'],
|
|
||||||
},
|
|
||||||
mode: 'production',
|
|
||||||
optimization: {
|
|
||||||
minimize: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user