feat: Add richer card theme support (#1809)
This commit is contained in:
+40
-13
@@ -1,7 +1,13 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { IconController } from '../components-lib/icon-controller';
|
||||
import iconStyle from '../scss/icon.scss';
|
||||
import { Icon } from '../types';
|
||||
@@ -14,17 +20,35 @@ export class FrigateCardIcon extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public icon?: Icon;
|
||||
|
||||
// Note: This attribute will allow non-active entity state styles (e.g. 'off',
|
||||
// 'unavailable') to be overriden from outside the icon itself. This is useful
|
||||
// in the menu / submenus where we want icons to follow menu theming, unless
|
||||
// they are 'active'. This attribute is not used in code, but matched in
|
||||
// icon.scss .
|
||||
@property({ attribute: 'allow-override-non-active-styles', type: Boolean })
|
||||
public allowOverrideNonActiveStyles = false;
|
||||
|
||||
private _controller = new IconController();
|
||||
private _svg: HTMLElement | null = null;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('icon')) {
|
||||
const customIcon = this._controller.getCustomIcon(this.icon);
|
||||
if (customIcon) {
|
||||
const svgElement = document.createElement('svg');
|
||||
svgElement.innerHTML = customIcon;
|
||||
this._svg = svgElement;
|
||||
} else {
|
||||
this._svg = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const customIconURL = this._controller.getCustomIcon(this.icon);
|
||||
const style = styleMap(this.icon?.style || {});
|
||||
|
||||
if (customIconURL) {
|
||||
return html`<img style="${style}" src="${customIconURL}" />`;
|
||||
}
|
||||
if (this.icon?.icon) {
|
||||
return html`<ha-icon style="${style}" icon="${this.icon.icon}"></ha-icon>`;
|
||||
if (this._svg) {
|
||||
// Use SVG objects (rather than <img>) to ensure styling applies
|
||||
// correctly.
|
||||
return html`${this._svg}`;
|
||||
}
|
||||
if (this.hass && this.icon?.entity) {
|
||||
const stateObj = this._controller.createStateObjectForStateBadge(
|
||||
@@ -35,16 +59,19 @@ export class FrigateCardIcon extends LitElement {
|
||||
// As a special case, need to pass in a color in order for the state
|
||||
// color to be overridden.
|
||||
return html`<state-badge
|
||||
style="${style}"
|
||||
.color="${this.icon.style?.color ?? undefined}"
|
||||
.color="${this.style.color ?? undefined}"
|
||||
.stateColor=${this.icon.stateColor ?? true}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${this.icon.icon}
|
||||
></state-badge>`;
|
||||
}
|
||||
}
|
||||
if (this.icon?.icon) {
|
||||
return html`<ha-icon icon="${this.icon.icon}"></ha-icon>`;
|
||||
}
|
||||
if (this.icon?.fallback) {
|
||||
return html`<ha-icon style="${style}" icon="${this.icon.fallback}"></ha-icon>`;
|
||||
return html`<ha-icon icon="${this.icon.fallback}"></ha-icon>`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import irisLogo from '../images/camera-iris-transparent.svg';
|
||||
import loadingStyle from '../scss/loading.scss';
|
||||
import './icon';
|
||||
|
||||
@customElement('frigate-card-loading')
|
||||
export class FrigateCardLoading extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html` <img src="${irisLogo}" /> `;
|
||||
return html`<frigate-card-icon .icon=${{ icon: 'iris' }}></frigate-card-icon>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
+55
-4
@@ -66,8 +66,6 @@ export class FrigateCardMenu extends LitElement {
|
||||
: button.title;
|
||||
|
||||
return html` <ha-icon-button
|
||||
class="button"
|
||||
style="${styleMap(button.style || {})}"
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: frigateCardHasAction(button.hold_action),
|
||||
hasDoubleClick: frigateCardHasAction(button.double_tap_action),
|
||||
@@ -76,18 +74,70 @@ export class FrigateCardMenu extends LitElement {
|
||||
@action=${(ev) => this._controller.actionHandler(ev, button)}
|
||||
>
|
||||
<frigate-card-icon
|
||||
?allow-override-non-active-styles=${true}
|
||||
style="${styleMap(button.style || {})}"
|
||||
.hass=${this.hass}
|
||||
.icon=${{
|
||||
icon: button.icon,
|
||||
entity: button.entity,
|
||||
stateColor: button.state_color,
|
||||
style: button.style,
|
||||
fallback: 'mdi:gesture-tap-button',
|
||||
}}
|
||||
></frigate-card-icon>
|
||||
</ha-icon-button>`;
|
||||
}
|
||||
|
||||
/** Theme-related styling is dynamically injected into the menu depending on
|
||||
* the configured position, style and alignment to allow precise theming.
|
||||
* The alternative is a massive (post-sass processing) CSS file would need to
|
||||
* be shipped to account for every possible combination.
|
||||
*
|
||||
* Each rule uses 'var' values that have nested fallbacks of decreasing
|
||||
* specificity, so the most specific theme variable will match, followed by
|
||||
* the next most specific, etc.
|
||||
*/
|
||||
protected _renderPerInstanceStyle(): TemplateResult | void {
|
||||
const config = this._controller.getMenuConfig();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = config.position;
|
||||
const style = config.style;
|
||||
const alignment = config.alignment;
|
||||
|
||||
const generateValue = (suffix: string): string => {
|
||||
return `
|
||||
var(--frigate-card-menu-override-${suffix},
|
||||
var(--frigate-card-menu-position-${position}-alignment-${alignment}-style-${style}-${suffix},
|
||||
var(--frigate-card-menu-position-${position}-alignment-${alignment}-${suffix},
|
||||
var(--frigate-card-menu-position-${position}-${suffix},
|
||||
var(--frigate-card-menu-style-${style}-${suffix},
|
||||
var(--frigate-card-menu-alignment-${alignment}-${suffix},
|
||||
var(--frigate-card-menu-${suffix})))))))`;
|
||||
};
|
||||
|
||||
// By definition `rule` will match the current configuration, the choice is
|
||||
// actually which of the var(...) variables will be used after the match.
|
||||
const expandedRule = style === 'hidden' ? '[expanded]' : '';
|
||||
const rule =
|
||||
`[data-position='${position}']` +
|
||||
`[data-style='${style}']` +
|
||||
`[data-alignment='${alignment}']` +
|
||||
expandedRule;
|
||||
|
||||
return html`<style>
|
||||
:host(${rule}) {
|
||||
background: ${generateValue('background')};
|
||||
|
||||
ha-icon-button {
|
||||
color: ${generateValue('button-inactive-color')};
|
||||
background: ${generateValue('button-background')};
|
||||
}
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const config = this._controller.getMenuConfig();
|
||||
const style = config?.style;
|
||||
@@ -97,7 +147,8 @@ export class FrigateCardMenu extends LitElement {
|
||||
const matchingButtons = this._controller.getButtons('matching');
|
||||
const opposingButtons = this._controller.getButtons('opposing');
|
||||
|
||||
return html` <div
|
||||
return html` ${this._renderPerInstanceStyle()}
|
||||
<div
|
||||
class="matching"
|
||||
style="${styleMap({ flex: String(matchingButtons.length) })}"
|
||||
>
|
||||
|
||||
@@ -63,7 +63,6 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
||||
right: this.side === 'right',
|
||||
thumbnails: !renderIcon,
|
||||
icons: renderIcon,
|
||||
button: renderIcon,
|
||||
};
|
||||
|
||||
if (renderIcon) {
|
||||
|
||||
@@ -36,12 +36,48 @@ export class FrigateCardStatusBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Theme-related styling is dynamically injected into the status bar depending on
|
||||
* the configured position and style to allow precise theming.
|
||||
*
|
||||
* Each rule uses 'var' values that have nested fallbacks of decreasing
|
||||
* specificity, so the most specific theme variable will match, followed by
|
||||
* the next most specific, etc.
|
||||
*/
|
||||
protected _renderPerInstanceStyle(): TemplateResult | void {
|
||||
const config = this._controller.getConfig();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = config.position;
|
||||
const style = config.style;
|
||||
|
||||
const generateValue = (suffix: string): string => {
|
||||
return `
|
||||
var(--frigate-card-status-bar-override-${suffix},
|
||||
var(--frigate-card-status-bar-position-${position}-style-${style}-${suffix},
|
||||
var(--frigate-card-status-bar-position-${position}-${suffix},
|
||||
var(--frigate-card-status-bar-style-${style}-${suffix},
|
||||
var(--frigate-card-status-bar-${suffix})))))`;
|
||||
};
|
||||
|
||||
const rule = `[data-position='${position}']` + `[data-style='${style}']`;
|
||||
|
||||
return html`<style>
|
||||
:host(${rule}) {
|
||||
color: ${generateValue('color')};
|
||||
background: ${generateValue('background')};
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._controller.shouldRender()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._renderPerInstanceStyle()}
|
||||
<div class="status">
|
||||
${this._controller.getRenderItems().map((item): TemplateResult | void => {
|
||||
if (item.enabled === false) {
|
||||
|
||||
@@ -43,9 +43,9 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
}
|
||||
|
||||
const title = item.title ?? getEntityTitle(this.hass, item.entity);
|
||||
const style = styleMap(item.style || {});
|
||||
return html`
|
||||
<mwc-list-item
|
||||
style="${styleMap(item.style || {})}"
|
||||
graphic=${ifDefined(item.icon || item.entity ? 'icon' : undefined)}
|
||||
?twoline=${!!item.subtitle}
|
||||
?selected=${item.selected}
|
||||
@@ -61,17 +61,18 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<span>${title ?? ''}</span>
|
||||
${item.subtitle ? html`<span slot="secondary">${item.subtitle}</span>` : ''}
|
||||
<span style="${style}">${title ?? ''}</span>
|
||||
${item.subtitle
|
||||
? html`<span slot="secondary" style="${style}">${item.subtitle}</span>`
|
||||
: ''}
|
||||
<frigate-card-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.icon=${{
|
||||
icon: item.icon,
|
||||
entity: item.entity,
|
||||
style: item.style,
|
||||
}}
|
||||
style="${styleMap(item.style || {})}"
|
||||
style="${style}"
|
||||
></frigate-card-icon>
|
||||
</mwc-list-item>
|
||||
`;
|
||||
@@ -96,7 +97,6 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
style="${style}"
|
||||
class="button"
|
||||
slot="trigger"
|
||||
.label=${this.submenu.title || ''}
|
||||
.actionHandler=${actionHandler({
|
||||
@@ -110,6 +110,7 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
})}
|
||||
>
|
||||
<frigate-card-icon
|
||||
?allow-override-non-active-styles=${true}
|
||||
style="${style}"
|
||||
.hass=${this.hass}
|
||||
.icon=${typeof this.submenu.icon === 'string'
|
||||
@@ -217,7 +218,6 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
||||
icon: this.submenuSelect.icon,
|
||||
entity: entityID,
|
||||
fallback: 'mdi:format-list-bulleted',
|
||||
style: this.submenuSelect.style,
|
||||
},
|
||||
|
||||
type: 'custom:frigate-card-menu-submenu',
|
||||
|
||||
Reference in New Issue
Block a user