Files
luma-home/cards/luma-range-card.js
T
Kaloyan DanchevandClaude Opus 4.6 d681f20884
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Add HACS integration, 8 Lovelace cards, and pairing page
New HA integration (integration/custom_components/luma_home/) with:
- WS API: luma/get_config (Lovelace→v2 bridge), luma/pair, luma/pair_status
- Config flow with dashboard URL path setting

8 HACS frontend cards (cards/) matching TV app visual spec:
- toggle, range, climate, media, curtain, action, vacuum, weather
- Glass card design: rgba(36,40,48,0.75) bg, 22px radius, accent glows

Pairing page (pairing/luma-pair.html) for TV token provisioning.
Web dashboard (web-dashboard/) — legacy config editor (deprecated).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-25 13:19:26 +02:00

123 lines
5.7 KiB
JavaScript

const LUMA_RANGE_VERSION = '0.1.0';
class LumaRangeCard extends HTMLElement {
set hass(hass) { this._hass = hass; this._render(); }
setConfig(config) {
if (!config.entity) throw new Error('Entity is required');
this._config = { entity: config.entity, name: config.name || '', size: config.size || '1x1',
accent_color: config.accent_color || '', glow_color: config.glow_color || '',
icon: config.icon || '', tap_action: config.tap_action || { action: 'more-info' },
hold_action: config.hold_action || { action: 'more-info' }, confirmation: config.confirmation || false, ...config };
this._render();
}
getCardSize() { return (this._config?.size === '1x2' || this._config?.size === '2x2') ? 4 : 2; }
static getStubConfig() { return { entity: '', name: '', size: '1x1' }; }
connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); }
_getState() { return this._hass?.states?.[this._config?.entity]; }
_isOn() { const s = this._getState(); return s && s.state === 'on'; }
_getBrightness() {
const s = this._getState();
if (!s || s.state !== 'on') return 0;
const b = parseFloat(s.attributes?.brightness);
return isNaN(b) ? 0 : Math.round(b / 255 * 100);
}
_getAccent() {
if (this._config?.accent_color) return this._config.accent_color;
const s = this._getState();
if (s?.attributes?.rgb_color) {
const [r, g, b] = s.attributes.rgb_color;
return `rgb(${r},${g},${b})`;
}
return this._isOn() ? '#fab95c' : '#bbbbbb';
}
_getGlow() {
if (this._config?.glow_color) return this._config.glow_color;
const s = this._getState();
if (s?.attributes?.rgb_color) {
const [r, g, b] = s.attributes.rgb_color;
return `rgb(${r},${g},${b})`;
}
return '#fab95c';
}
_handleClick() {
if (!this._hass || !this._config) return;
const action = this._config.tap_action?.action || 'more-info';
if (action === 'toggle') {
this._hass.callService('homeassistant', 'toggle', { entity_id: this._config.entity });
} else {
const ev = new Event('hass-more-info', { composed: true });
ev.detail = { entityId: this._config.entity };
this.dispatchEvent(ev);
}
}
_render() {
if (!this.shadowRoot || !this._config) return;
const isOn = this._isOn();
const pct = this._getBrightness();
const accent = this._getAccent();
const glow = this._getGlow();
const name = this._config.name || this._getState()?.attributes?.friendly_name || '';
const icon = this._config.icon || 'mdi:lightbulb';
const label = isOn ? `${pct}%` : 'Off';
const glowAlpha = isOn ? (pct / 100 * 0.35).toFixed(2) : '0';
this.shadowRoot.innerHTML = `
<style>
:host { display: block; --accent: ${accent}; --glow: ${glow}; }
.card {
position: relative; overflow: hidden; background: rgba(36,40,48,0.75);
border: 1px solid rgba(255,255,255,0.10); border-radius: 22px; padding: 16px;
cursor: pointer; transition: border-color .15s, box-shadow .15s;
height: 100%; min-height: 120px; display: flex; flex-direction: column;
justify-content: space-between; box-shadow: 0 2px 6px rgba(0,0,0,.06);
}
.card:hover { border-color: rgba(255,255,255,0.18); box-shadow: 0 8px 28px rgba(0,0,0,.28); }
.card.on { border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
.glow {
position: absolute; top: -20px; left: -20px; width: 200px; height: 200px;
border-radius: 50%; pointer-events: none; opacity: 0; transition: opacity .2s;
background: radial-gradient(circle, color-mix(in srgb, var(--glow) ${Math.round(glowAlpha * 100)}%, transparent) 0%, transparent 70%);
}
.card.on .glow { opacity: 1; }
.top { display: flex; align-items: center; gap: 12px; position: relative; z-index: 1; }
.icon-circle {
width: 48px; height: 48px; border-radius: 24px; display: flex;
align-items: center; justify-content: center; flex-shrink: 0;
background: ${isOn ? `color-mix(in srgb, var(--accent) 20%, transparent)` : 'rgba(255,255,255,0.08)'};
transition: background .15s;
}
.icon-circle ha-icon { --mdc-icon-size: 28px; color: ${isOn ? 'var(--accent)' : 'rgba(187,187,187,0.5)'}; }
.info { flex: 1; min-width: 0; }
.type-label { font-size: 12px; font-weight: 400; letter-spacing: 0.04em; color: ${isOn ? 'var(--accent)' : 'rgba(232,232,232,0.45)'}; }
.name { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.status { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; color: ${isOn ? '#fff' : 'rgba(187,187,187,0.5)'}; position: relative; z-index: 1; }
</style>
<div class="card ${isOn ? 'on' : ''}">
<div class="glow"></div>
<div class="top">
<div class="icon-circle"><ha-icon icon="${icon}"></ha-icon></div>
<div class="info">
<div class="type-label">Light</div>
<div class="name">${name}</div>
</div>
</div>
<div class="status">${label}</div>
</div>`;
this.shadowRoot.querySelector('.card').addEventListener('click', () => this._handleClick());
}
}
customElements.define('luma-range-card', LumaRangeCard);
window.customCards = window.customCards || [];
window.customCards.push({ type: 'luma-range-card', name: 'Luma Range', description: 'Luma-style dimmable light card with brightness control.', preview: true });
console.info(`%c LUMA-RANGE-CARD %c v${LUMA_RANGE_VERSION} `, 'background:#fab95c;color:#111;font-weight:bold', 'background:#161a22;color:#fab95c');