diff --git a/docs/configuration/elements/custom/README.md b/docs/configuration/elements/custom/README.md index 0e521389..f9c73814 100644 --- a/docs/configuration/elements/custom/README.md +++ b/docs/configuration/elements/custom/README.md @@ -79,6 +79,7 @@ elements: | `selected` | `false` | Whether or not to show this item as selected. | | `state_color` | `true` | Whether or not the title and icon should be stylized based on state. | | `style` | | Position and style the element using CSS. | +| `subtitle` | | Optional additional text to display. | | `tap_action`, `double_tap_action`, `hold_action`, `start_tap`, `end_tap` | | The [actions](../../actions/README.md) to take when this item is interacted with. | | `title` | | An optional title to display. | diff --git a/src/camera-manager/frigate/event-watcher.ts b/src/camera-manager/frigate/event-watcher.ts index 6a66fc74..b085353c 100644 --- a/src/camera-manager/frigate/event-watcher.ts +++ b/src/camera-manager/frigate/event-watcher.ts @@ -39,8 +39,12 @@ export class FrigateEventWatcher implements FrigateEventWatcherSubscriptionInter ); if (!this._hasSubscribers(request.instanceID)) { - await this._unsubscribeCallback[request.instanceID](); + const callback = this._unsubscribeCallback[request.instanceID]; delete this._unsubscribeCallback[request.instanceID]; + + // Callback may be undefined if unsubscribe is called while subscribe is + // still awaiting the Home Assistant connection. + await callback?.(); } } diff --git a/src/components/key-assigner.ts b/src/components/key-assigner.ts index 29275157..a39dd02c 100644 --- a/src/components/key-assigner.ts +++ b/src/components/key-assigner.ts @@ -7,7 +7,6 @@ import { unsafeCSS, } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; import { KeyAssignerController } from '../components-lib/key-assigner-controller'; import { KeyboardShortcut } from '../config/schema/view'; import { localize } from '../localize/localize'; @@ -44,21 +43,22 @@ export class AdvancedCameraCardKeyAssigner extends LitElement { return html`
${this.label}
{ this._controller.toggleAssigning(); }} > - - - ${this._controller.isAssigning() ? '' : localize('key_assigner.assign')} - + + ${ this._controller.hasValue() ? html` { this._controller.setValue(null); }} @@ -66,7 +66,6 @@ export class AdvancedCameraCardKeyAssigner extends LitElement { - ${localize('key_assigner.unassign')} ` : '' } diff --git a/src/components/submenu/index.ts b/src/components/submenu/index.ts index f007305e..d567b3c3 100644 --- a/src/components/submenu/index.ts +++ b/src/components/submenu/index.ts @@ -1,6 +1,5 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; import { actionHandler } from '../../action-handler-directive.js'; import { getEntityTitle } from '../../ha/get-entity-title.js'; @@ -28,31 +27,31 @@ export class AdvancedCameraCardSubmenu extends LitElement { const title = item.title ?? getEntityTitle(this.hass, item.entity); const style = styleMap(item.style || {}); + const disabled = item.enabled === false; return html` - ) => { // Attach the item so ascendants have access to it. ev.detail.item = item; }} - .actionHandler=${actionHandler({ - allowPropagation: true, - hasHold: hasAction(item.hold_action), - hasDoubleClick: hasAction(item.double_tap_action), - })} + .actionHandler=${disabled + ? undefined + : actionHandler({ + allowPropagation: true, + hasHold: hasAction(item.hold_action), + hasDoubleClick: hasAction(item.double_tap_action), + })} > ${title ?? ''} ${item.subtitle - ? html`${item.subtitle}` + ? html`${item.subtitle}` : ''} - + `; } protected render(): TemplateResult { return html` - ev.stopPropagation() - } - @click=${(ev: Event) => stopEventFromActivatingCardWideActions(ev)} - > + stopEventFromActivatingCardWideActions(ev)}> ${this.items?.map(this._renderItem.bind(this))} - + `; } diff --git a/src/components/submenu/submenu-button.ts b/src/components/submenu/submenu-button.ts index a98a5df1..9ec60337 100644 --- a/src/components/submenu/submenu-button.ts +++ b/src/components/submenu/submenu-button.ts @@ -35,10 +35,6 @@ export class AdvancedCameraCardSubmenuButton extends LitElement { .hass=${this.hass} .icon=${{ icon: this.submenu.icon }} .actionHandler=${actionHandler({ - // Need to allow event to propagate upwards, as it's caught by the - // trigger slot to open/close the menu. Further - // propagation is forbidden by the @click handler on - // . allowPropagation: true, hasHold: hasAction(this.submenu.hold_action), hasDoubleClick: hasAction(this.submenu.double_tap_action), diff --git a/src/ha/side-load-ha-elements.ts b/src/ha/side-load-ha-elements.ts index 5671a4c3..36f92625 100644 --- a/src/ha/side-load-ha-elements.ts +++ b/src/ha/side-load-ha-elements.ts @@ -8,11 +8,12 @@ import { CardHelpers, LovelaceCardWithEditor } from '../types'; */ export const sideLoadHomeAssistantElements = async (): Promise => { const neededElements = [ - 'ha-button-menu', 'ha-button', 'ha-camera-stream', 'ha-card', 'ha-combo-box', + 'ha-dropdown-item', + 'ha-dropdown', 'ha-hls-player', 'ha-icon-button', 'ha-icon', diff --git a/src/scss/editor.scss b/src/scss/editor.scss index 355bd769..17bbeda4 100644 --- a/src/scss/editor.scss +++ b/src/scss/editor.scss @@ -28,6 +28,8 @@ .values { background: var(--secondary-background-color); display: grid; + + grid-template-columns: minmax(0, 1fr); } // Space submenus from the next top-level options. .values + .option, diff --git a/src/scss/key-assigner.scss b/src/scss/key-assigner.scss index 80ff942c..48283936 100644 --- a/src/scss/key-assigner.scss +++ b/src/scss/key-assigner.scss @@ -1,5 +1,3 @@ -@use 'dotdotdot.scss'; - :host { display: flex; flex-direction: row; @@ -13,7 +11,18 @@ :host([assigning]) ha-button.assign span, :host([assigning]) ha-button.assign advanced-camera-card-icon { - color: var(--warning-color); + animation: pulse 0.5s infinite; + + @keyframes pulse { + 50% { + color: var(--warning-color); + } + } +} + +ha-button { + padding-left: 4px; + padding-right: 4px; } advanced-camera-card-icon { diff --git a/src/scss/submenu.scss b/src/scss/submenu.scss index 8176ffb9..dca2a862 100644 --- a/src/scss/submenu.scss +++ b/src/scss/submenu.scss @@ -1,8 +1,24 @@ @use './menu-button.scss'; -@import './z-index.scss'; :host { + // Without this the submenu (in the card menu) cannot be interacted with (e.g. + // hover states don't work). pointer-events: auto; +} - --mdc-menu-z-index: #{$z-index-submenu}; +ha-dropdown-item { + color: var(--advanced-camera-card-submenu-color); + background-color: var(--advanced-camera-card-submenu-background-color); +} + +ha-dropdown-item:hover { + color: var(--advanced-camera-card-submenu-hover-color); + background-color: var(--advanced-camera-card-submenu-hover-background-color); +} + +ha-dropdown-item.selected:not(:hover) { + font-weight: var(--ha-font-weight-medium); + + color: var(--advanced-camera-card-submenu-selected-color); + background-color: var(--advanced-camera-card-submenu-selected-background-color); } diff --git a/src/scss/themes/base.scss b/src/scss/themes/base.scss index 3da18fea..1ba300e5 100644 --- a/src/scss/themes/base.scss +++ b/src/scss/themes/base.scss @@ -98,6 +98,24 @@ // --advanced-camera-card-menu-position-left-background: purple; // --advanced-camera-card-menu-style-overlay-background: pink; + /********* + * Submenu + *********/ + --advanced-camera-card-submenu-color: var(--advanced-camera-card-text-color); + --advanced-camera-card-submenu-background-color: var( + --advanced-camera-card-background + ); + --advanced-camera-card-submenu-hover-color: var(--advanced-camera-card-text-color); + --advanced-camera-card-submenu-hover-background-color: var( + --ha-color-fill-neutral-normal-resting + ); + --advanced-camera-card-submenu-selected-color: var( + --advanced-camera-card-foreground-primary + ); + --advanced-camera-card-submenu-selected-background-color: var( + --ha-color-fill-primary-quiet-resting + ); + /************ * Status bar ************/ diff --git a/src/scss/themes/dark.scss b/src/scss/themes/dark.scss index 9ee5408e..1cf7f40d 100644 --- a/src/scss/themes/dark.scss +++ b/src/scss/themes/dark.scss @@ -1,12 +1,204 @@ :host([themes~='dark']) { - // This file should include only, and exactly match, values from - // https://github.com/home-assistant/frontend/blob/dev/src/resources/styles-data.ts . + --sidebar-text-color: var(--primary-text-color); + --sidebar-background-color: var(--card-background-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-background-color: var(--sidebar-background-color); + --app-header-border-bottom: 1px solid var(--divider-color); - --card-background-color: #1c1c1c; + // ===================== + // Core global color styles + // https://github.com/home-assistant/frontend/blob/dev/src/resources/theme/color/color.globals.ts + // ===================== --primary-background-color: #111111; + --card-background-color: #1c1c1c; --secondary-background-color: #282828; + --clear-background-color: #111111; --primary-text-color: #e1e1e1; + --secondary-text-color: #9b9b9b; + --disabled-text-color: #6f6f6f; + --switch-unchecked-button-color: #999999; + --switch-unchecked-track-color: #9b9b9b; --divider-color: rgba(225, 225, 225, 0.12); - --app-header-background-color: #101e24; - --app-header-text-color: #e1e1e1; + --outline-color: rgba(225, 225, 225, 0.12); + --outline-hover-color: rgba(225, 225, 225, 0.24); + --shadow-color: rgba(0, 0, 0, 0.48); + + --mdc-ripple-color: #aaaaaa; + --mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1); + + --input-idle-line-color: rgba(255, 255, 255, 0.42); + --input-hover-line-color: rgba(255, 255, 255, 0.87); + --input-disabled-line-color: rgba(255, 255, 255, 0.06); + --input-outlined-idle-border-color: rgba(255, 255, 255, 0.38); + --input-outlined-hover-border-color: rgba(255, 255, 255, 0.87); + --input-outlined-disabled-border-color: rgba(255, 255, 255, 0.06); + --input-fill-color: rgba(255, 255, 255, 0.05); + --input-disabled-fill-color: rgba(255, 255, 255, 0.02); + --input-ink-color: rgba(255, 255, 255, 0.87); + --input-label-ink-color: rgba(255, 255, 255, 0.6); + --input-disabled-ink-color: rgba(255, 255, 255, 0.37); + --input-dropdown-icon-color: rgba(255, 255, 255, 0.54); + + --codemirror-keyword: #c792ea; + --codemirror-operator: #89ddff; + --codemirror-variable: #f07178; + --codemirror-variable-2: #eeffff; + --codemirror-variable-3: #decb6b; + --codemirror-builtin: #ffcb6b; + --codemirror-atom: #f78c6c; + --codemirror-number: #ff5370; + --codemirror-def: #82aaff; + --codemirror-string: #c3e88d; + --codemirror-string-2: #f07178; + --codemirror-comment: #545454; + --codemirror-tag: #ff5370; + --codemirror-meta: #ffcb6b; + --codemirror-attribute: #c792ea; + --codemirror-property: #c792ea; + --codemirror-qualifier: #decb6b; + --codemirror-type: #decb6b; + --energy-grid-return-color: #a280db; + --map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) + saturate(0.3); + --disabled-color: #464646; + + --ha-button-primary-light-color: #4082a040; + --ha-button-warning-light-color: #917b54c1; + --ha-button-neutral-color: #d9dae0; + --ha-button-neutral-light-color: #6a7081; + + --bar-box-shadow: 0 2px 12px var(--shadow-color); + + // ===================== + // Semantic color styles + // https://github.com/home-assistant/frontend/blob/dev/src/resources/theme/color/semantic.globals.ts + // ===================== + + /* text */ + --ha-color-text-primary: var(--white-color); + --ha-color-text-secondary: var(--ha-color-neutral-80); + --ha-color-text-link: var(--ha-color-primary-60); + + /* border primary */ + --ha-color-border-normal: var(--ha-color-primary-50); + + /* border neutral */ + --ha-color-border-neutral-quiet: var(--ha-color-neutral-40); + --ha-color-border-neutral-normal: var(--ha-color-neutral-50); + --ha-color-border-neutral-loud: var(--ha-color-neutral-70); + + /* border danger */ + --ha-color-border-danger-normal: var(--ha-color-red-50); + --ha-color-border-danger-loud: var(--ha-color-red-50); + + /* border warning */ + --ha-color-border-warning-normal: var(--ha-color-orange-50); + --ha-color-border-warning-loud: var(--ha-color-orange-50); + + /* fill primary quiet */ + --ha-color-fill-primary-quiet-resting: var(--ha-color-primary-05); + --ha-color-fill-primary-quiet-hover: var(--ha-color-primary-10); + --ha-color-fill-primary-quiet-active: var(--ha-color-primary-05); + + /* fill primary normal */ + --ha-color-fill-primary-normal-resting: var(--ha-color-primary-10); + --ha-color-fill-primary-normal-hover: var(--ha-color-primary-20); + --ha-color-fill-primary-normal-active: var(--ha-color-primary-10); + + /* fill neutral quiet */ + --ha-color-fill-neutral-quiet-resting: var(--ha-color-neutral-05); + --ha-color-fill-neutral-quiet-hover: var(--ha-color-neutral-10); + --ha-color-fill-neutral-quiet-active: var(--ha-color-neutral-00); + + /* fill neutral normal */ + --ha-color-fill-neutral-normal-resting: var(--ha-color-neutral-10); + --ha-color-fill-neutral-normal-hover: var(--ha-color-neutral-20); + --ha-color-fill-neutral-normal-active: var(--ha-color-neutral-10); + + /* fill disabled quiet */ + --ha-color-fill-disabled-quiet-resting: var(--ha-color-neutral-10); + + /* fill disabled normal */ + --ha-color-fill-disabled-normal-resting: var(--ha-color-neutral-20); + + /* fill disabled loud */ + --ha-color-fill-disabled-loud-resting: var(--ha-color-neutral-30); + + /* fill danger quiet */ + --ha-color-fill-danger-quiet-resting: var(--ha-color-red-05); + --ha-color-fill-danger-quiet-hover: var(--ha-color-red-10); + --ha-color-fill-danger-quiet-active: var(--ha-color-red-05); + + /* fill danger normal */ + --ha-color-fill-danger-normal-resting: var(--ha-color-red-10); + --ha-color-fill-danger-normal-hover: var(--ha-color-red-20); + --ha-color-fill-danger-normal-active: var(--ha-color-red-10); + + /* fill danger loud */ + --ha-color-fill-danger-loud-resting: var(--ha-color-red-40); + --ha-color-fill-danger-loud-hover: var(--ha-color-red-30); + --ha-color-fill-danger-loud-active: var(--ha-color-red-40); + + /* fill warning quiet */ + --ha-color-fill-warning-quiet-resting: var(--ha-color-orange-05); + --ha-color-fill-warning-quiet-hover: var(--ha-color-orange-10); + --ha-color-fill-warning-quiet-active: var(--ha-color-orange-05); + + /* fill warning normal */ + --ha-color-fill-warning-normal-resting: var(--ha-color-orange-10); + --ha-color-fill-warning-normal-hover: var(--ha-color-orange-20); + --ha-color-fill-warning-normal-active: var(--ha-color-orange-10); + + /* fill warning loud */ + --ha-color-fill-warning-loud-resting: var(--ha-color-orange-40); + --ha-color-fill-warning-loud-hover: var(--ha-color-orange-30); + --ha-color-fill-warning-loud-active: var(--ha-color-orange-40); + + /* fill success quiet */ + --ha-color-fill-success-quiet-resting: var(--ha-color-green-05); + --ha-color-fill-success-quiet-hover: var(--ha-color-green-10); + --ha-color-fill-success-quiet-active: var(--ha-color-green-05); + + /* fill success normal */ + --ha-color-fill-success-normal-resting: var(--ha-color-green-10); + --ha-color-fill-success-normal-hover: var(--ha-color-green-20); + --ha-color-fill-success-normal-active: var(--ha-color-green-10); + + /* fill success loud */ + --ha-color-fill-success-loud-resting: var(--ha-color-green-40); + --ha-color-fill-success-loud-hover: var(--ha-color-green-30); + --ha-color-fill-success-loud-active: var(--ha-color-green-40); + + /* on primary */ + --ha-color-on-primary-quiet: var(--ha-color-primary-70); + --ha-color-on-primary-normal: var(--ha-color-primary-60); + + /* on neutral */ + --ha-color-on-neutral-quiet: var(--ha-color-neutral-70); + --ha-color-on-neutral-normal: var(--ha-color-neutral-60); + --ha-color-on-neutral-loud: var(--white-color); + + /* on disabled */ + --ha-color-on-disabled-quiet: var(--ha-color-neutral-40); + --ha-color-on-disabled-normal: var(--ha-color-neutral-50); + --ha-color-on-disabled-loud: var(--ha-color-neutral-50); + + /* on danger */ + --ha-color-on-danger-quiet: var(--ha-color-red-70); + --ha-color-on-danger-normal: var(--ha-color-red-60); + --ha-color-on-danger-loud: var(--white-color); + + /* on warning */ + --ha-color-on-warning-quiet: var(--ha-color-orange-70); + --ha-color-on-warning-normal: var(--ha-color-orange-60); + --ha-color-on-warning-loud: var(--white-color); + + /* on success */ + --ha-color-on-success-quiet: var(--ha-color-green-70); + --ha-color-on-success-normal: var(--ha-color-green-60); + --ha-color-on-success-loud: var(--white-color); + + /* Surfaces */ + --ha-color-surface-default: var(--ha-color-neutral-10); + --ha-color-on-surface-default: var(--ha-color-neutral-95); } diff --git a/src/scss/themes/light.scss b/src/scss/themes/light.scss index 3f7043c6..d4ad5fe1 100644 --- a/src/scss/themes/light.scss +++ b/src/scss/themes/light.scss @@ -1,18 +1,525 @@ :host([themes~='light']) { - // This file should include only, and exactly match, values from - // https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-style.ts + // ===================== + // Core global color styles + // https://github.com/home-assistant/frontend/blob/dev/src/resources/theme/color/color.globals.ts + // ===================== + /* text */ + --primary-text-color: var(--ha-color-text-primary); + --secondary-text-color: var(--ha-color-text-secondary); + --text-primary-color: #ffffff; + --text-light-primary-color: #212121; + --disabled-text-color: #bdbdbd; + + /* main interface colors */ + --primary-color: var(--ha-color-primary-40); + --dark-primary-color: #0288d1; + --darker-primary-color: #016194; + --light-primary-color: #b3e5fc; + --accent-color: #ff9800; + --divider-color: rgba(0, 0, 0, 0.12); + --outline-color: rgba(0, 0, 0, 0.12); + --outline-hover-color: rgba(0, 0, 0, 0.24); + --shadow-color: rgba(0, 0, 0, 0.16); + + /* rgb */ + --rgb-primary-color: 0, 154, 199; + --rgb-accent-color: 255, 152, 0; + --rgb-primary-text-color: 33, 33, 33; + --rgb-secondary-text-color: 114, 114, 114; + --rgb-text-primary-color: 255, 255, 255; + --rgb-card-background-color: 255, 255, 255; + + --rgb-warning-color: 255, 166, 0; + --rgb-error-color: 219, 68, 55; + --rgb-success-color: 67, 160, 71; + --rgb-info-color: 3, 155, 229; + + --scrollbar-thumb-color: rgb(194, 194, 194); + + --error-color: #db4437; + --warning-color: #ffa600; + --success-color: #43a047; + --info-color: #039be5; + + /* backgrounds */ --card-background-color: #ffffff; --primary-background-color: #fafafa; - --secondary-background-color: #e5e5e5; - --primary-text-color: #212121; - --divider-color: rgba(0, 0, 0, 0.12); - --app-header-text-color: #ffffff; + --secondary-background-color: #e5e5e5; /* behind the cards on state */ + --clear-background-color: #ffffff; + /* for label-badge */ + --label-badge-grey: #9e9e9e; + + /* states icon */ + --state-icon-color: #44739e; + /* an error state is anything that would be considered an error */ + /* --state-icon-error-color: #db4437; derived from error-color */ + + /* energy */ + --energy-grid-consumption-color: #488fc2; + --energy-grid-return-color: #8353d1; + --energy-solar-color: #ff9800; + --energy-non-fossil-color: #0f9d58; + --energy-battery-out-color: #4db6ac; + --energy-battery-in-color: #f06292; + --energy-gas-color: #8e021b; + --energy-water-color: #00bcd4; + + /* color */ + --disabled-color: #bdbdbd; + --red-color: #f44336; + --pink-color: #e91e63; + --purple-color: #926bc7; + --deep-purple-color: #6e41ab; + --indigo-color: #3f51b5; + --blue-color: #2196f3; + --light-blue-color: #03a9f4; + --cyan-color: #00bcd4; + --teal-color: #009688; + --green-color: #4caf50; + --light-green-color: #8bc34a; + --lime-color: #cddc39; + --yellow-color: #ffeb3b; + --amber-color: #ffc107; + --orange-color: #ff9800; + --deep-orange-color: #ff6f22; + --brown-color: #795548; + --light-grey-color: #bdbdbd; + --grey-color: #9e9e9e; + --dark-grey-color: #606060; + --blue-grey-color: #607d8b; + --black-color: #000000; + --white-color: #ffffff; + + /* colors - used for graphs, calendars, maps, etc */ + --color-1: #4269d0; + --color-2: #f4bd4a; + --color-3: #ff725c; + --color-4: #6cc5b0; + --color-5: #a463f2; + --color-6: #ff8ab7; + --color-7: #9c6b4e; + --color-8: #97bbf5; + --color-9: #01ab63; + --color-10: #094bad; + --color-11: #c99000; + --color-12: #d84f3e; + --color-13: #49a28f; + --color-14: #048732; + --color-15: #d96895; + --color-16: #8043ce; + --color-17: #7599d1; + --color-18: #7a4c31; + --color-19: #6989f4; + --color-20: #ffd444; + --color-21: #ff957c; + --color-22: #8fe9d3; + --color-23: #62cc71; + --color-24: #ffadda; + --color-25: #c884ff; + --color-26: #badeff; + --color-27: #bf8b6d; + --color-28: #927acc; + --color-29: #97ee3f; + --color-30: #bf3947; + --color-31: #9f5b00; + --color-32: #f48758; + --color-33: #8caed6; + --color-34: #f2b94f; + --color-35: #eff26e; + --color-36: #e43872; + --color-37: #d9b100; + --color-38: #9d7a00; + --color-39: #698cff; + --color-40: #00d27e; + --color-41: #d06800; + --color-42: #009f82; + --color-43: #c49200; + --color-44: #cbe8ff; + --color-45: #fecddf; + --color-46: #c27eb6; + --color-47: #8cd2ce; + --color-48: #c4b8d9; + --color-49: #f883b0; + --color-50: #a49100; + --color-51: #f48800; + --color-52: #27d0df; + --color-53: #a04a9b; + --color-54: #4269d0; + + /* history colors */ + --history-unavailable-color: transparent; + + /* input components */ + --input-idle-line-color: rgba(0, 0, 0, 0.42); + --input-hover-line-color: rgba(0, 0, 0, 0.87); + --input-disabled-line-color: rgba(0, 0, 0, 0.06); + --input-outlined-idle-border-color: rgba(0, 0, 0, 0.38); + --input-outlined-hover-border-color: rgba(0, 0, 0, 0.87); + --input-outlined-disabled-border-color: rgba(0, 0, 0, 0.06); + --input-fill-color: rgb(245, 245, 245); + --input-disabled-fill-color: rgb(250, 250, 250); + --input-ink-color: rgba(0, 0, 0, 0.87); + --input-label-ink-color: rgba(0, 0, 0, 0.6); + --input-disabled-ink-color: rgba(0, 0, 0, 0.37); + --input-dropdown-icon-color: rgba(0, 0, 0, 0.54); + + /* for label-badge */ + --label-badge-red: var(--error-color); + --label-badge-blue: var(--info-color); + --label-badge-green: var(--success-color); + --label-badge-yellow: var(--warning-color); + + /* state color */ + --state-active-color: var(--amber-color); + --state-inactive-color: var(--grey-color); + --state-unavailable-color: var( + --state-icon-unavailable-color, + var(--disabled-text-color) + ); + + /* state domain colors */ + --state-alarm_control_panel-armed_away-color: var(--green-color); + --state-alarm_control_panel-armed_custom_bypass-color: var(--green-color); + --state-alarm_control_panel-armed_home-color: var(--green-color); + --state-alarm_control_panel-armed_night-color: var(--green-color); + --state-alarm_control_panel-armed_vacation-color: var(--green-color); + --state-alarm_control_panel-arming-color: var(--orange-color); + --state-alarm_control_panel-disarming-color: var(--orange-color); + --state-alarm_control_panel-pending-color: var(--orange-color); + --state-alarm_control_panel-triggered-color: var(--red-color); + --state-alert-off-color: var(--orange-color); + --state-alert-on-color: var(--red-color); + --state-binary_sensor-active-color: var(--amber-color); + --state-binary_sensor-battery-on-color: var(--red-color); + --state-binary_sensor-carbon_monoxide-on-color: var(--red-color); + --state-binary_sensor-gas-on-color: var(--red-color); + --state-binary_sensor-heat-on-color: var(--red-color); + --state-binary_sensor-lock-on-color: var(--red-color); + --state-binary_sensor-moisture-on-color: var(--red-color); + --state-binary_sensor-problem-on-color: var(--red-color); + --state-binary_sensor-safety-on-color: var(--red-color); + --state-binary_sensor-smoke-on-color: var(--red-color); + --state-binary_sensor-sound-on-color: var(--red-color); + --state-binary_sensor-tamper-on-color: var(--red-color); + --state-climate-auto-color: var(--green-color); + --state-climate-cool-color: var(--blue-color); + --state-climate-dry-color: var(--orange-color); + --state-climate-fan_only-color: var(--cyan-color); + --state-climate-heat-color: var(--deep-orange-color); + --state-climate-heat-cool-color: var(--amber-color); + --state-cover-active-color: var(--purple-color); + --state-device_tracker-active-color: var(--blue-color); + --state-device_tracker-home-color: var(--green-color); + --state-fan-active-color: var(--cyan-color); + --state-humidifier-on-color: var(--blue-color); + --state-lawn_mower-error-color: var(--red-color); + --state-lawn_mower-mowing-color: var(--teal-color); + --state-light-active-color: var(--amber-color); + --state-lock-jammed-color: var(--red-color); + --state-lock-locked-color: var(--green-color); + --state-lock-locking-color: var(--orange-color); + --state-lock-unlocked-color: var(--red-color); + --state-lock-unlocking-color: var(--orange-color); + --state-lock-open-color: var(--red-color); + --state-lock-opening-color: var(--orange-color); + --state-media_player-active-color: var(--light-blue-color); + --state-person-active-color: var(--blue-color); + --state-person-home-color: var(--green-color); + --state-plant-active-color: var(--red-color); + --state-siren-active-color: var(--red-color); + --state-sun-above_horizon-color: var(--amber-color); + --state-sun-below_horizon-color: var(--indigo-color); + --state-switch-active-color: var(--amber-color); + --state-update-active-color: var(--orange-color); + --state-vacuum-active-color: var(--teal-color); + --state-valve-active-color: var(--blue-color); + --state-sensor-battery-high-color: var(--green-color); + --state-sensor-battery-low-color: var(--red-color); + --state-sensor-battery-medium-color: var(--orange-color); + --state-water_heater-eco-color: var(--green-color); + --state-water_heater-electric-color: var(--orange-color); + --state-water_heater-gas-color: var(--orange-color); + --state-water_heater-heat_pump-color: var(--orange-color); + --state-water_heater-high_demand-color: var(--deep-orange-color); + --state-water_heater-performance-color: var(--deep-orange-color); + --state-weather-clear_night-color: var(--deep-purple-color); + --state-weather-cloudy-color: var(--light-grey-color); + --state-weather-exceptional-color: var(--red-color); + --state-weather-fog-color: var(--grey-color); + --state-weather-hail-color: var(--cyan-color); + --state-weather-lightning_rainy-color: var(--lime-color); + --state-weather-lightning-color: var(--yellow-color); + --state-weather-partlycloudy-color: var(--blue-grey-color); + --state-weather-pouring-color: var(--indigo-color); + --state-weather-rainy-color: var(--blue-color); + --state-weather-snowy_rainy-color: var(--light-blue-color); + --state-weather-snowy-color: #c0e0ff; + --state-weather-sunny-color: var(--amber-color); + --state-weather-windy_variant-color: var(--green-color); + --state-weather-windy-color: var(--green-color); + + /* history colors */ + --history-unknown-color: var(--dark-grey-color); + + --state-icon-error-color: var(--error-state-color, var(--error-color)); + --sidebar-text-color: var(--primary-text-color); + --sidebar-background-color: var(--card-background-color); + --sidebar-selected-text-color: var(--primary-color); + --sidebar-selected-icon-color: var(--primary-color); + --sidebar-icon-color: rgba(var(--rgb-primary-text-color), 0.6); + --switch-checked-color: var(--primary-color); + --switch-checked-button-color: var( + --switch-checked-color, + var(--primary-background-color) + ); + --switch-checked-track-color: var(--switch-checked-color, #000000); + --switch-unchecked-button-color: var( + --switch-unchecked-color, + var(--primary-background-color) + ); + --switch-unchecked-track-color: var(--switch-unchecked-color, #000000); + --slider-color: var(--primary-color); + --slider-secondary-color: var(--light-primary-color); + --slider-track-color: var(--scrollbar-thumb-color); + --label-badge-background-color: var(--card-background-color); + --label-badge-text-color: rgba(var(--rgb-primary-text-color), 0.8); + --table-header-background-color: var(--input-fill-color); + --table-row-background-color: var(--primary-background-color); + --table-row-alternative-background-color: var(--secondary-background-color); + --data-table-background-color: var(--card-background-color); + --markdown-code-background-color: var(--primary-background-color); + --bar-box-shadow: 0 2px 12px var(--shadow-color); + + /* https://github.com/material-components/material-web/blob/master/docs/theming.md */ + --mdc-theme-primary: var(--primary-color); + --mdc-theme-secondary: var(--accent-color); + --mdc-theme-background: var(--primary-background-color); + --mdc-theme-surface: var(--card-background-color); + --mdc-theme-on-primary: var(--text-primary-color); + --mdc-theme-on-secondary: var(--text-primary-color); + --mdc-theme-on-surface: var(--primary-text-color); + --mdc-theme-text-disabled-on-light: var(--disabled-text-color); + --mdc-theme-text-primary-on-background: var(--primary-text-color); + --mdc-theme-text-secondary-on-background: var(--secondary-text-color); + --mdc-theme-text-hint-on-background: var(--secondary-text-color); + --mdc-theme-text-icon-on-background: var(--secondary-text-color); + --mdc-theme-error: var(--error-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-background-color: var(--sidebar-background-color); + --app-header-border-bottom: 1px solid var(--divider-color); + --app-theme-color: var(--app-header-background-color); + --mdc-checkbox-unchecked-color: rgba(var(--rgb-primary-text-color), 0.54); + --mdc-checkbox-disabled-color: var(--disabled-text-color); + --mdc-radio-unchecked-color: rgba(var(--rgb-primary-text-color), 0.54); + --mdc-radio-disabled-color: var(--disabled-text-color); + --mdc-tab-text-label-color-default: var(--primary-text-color); + --mdc-button-disabled-ink-color: var(--disabled-text-color); + --mdc-button-outline-color: var(--outline-color); + --mdc-dialog-scroll-divider-color: var(--divider-color); + --mdc-dialog-heading-ink-color: var(--primary-text-color); + --mdc-dialog-content-ink-color: var(--primary-text-color); + --mdc-top-app-bar-fixed-box-shadow: var(--bar-box-shadow); + + --mdc-text-field-idle-line-color: var(--input-idle-line-color); + --mdc-text-field-hover-line-color: var(--input-hover-line-color); + --mdc-text-field-disabled-line-color: var(--input-disabled-line-color); + --mdc-text-field-outlined-idle-border-color: var(--input-outlined-idle-border-color); + --mdc-text-field-outlined-hover-border-color: var(--input-outlined-hover-border-color); + --mdc-text-field-outlined-disabled-border-color: var( + --input-outlined-disabled-border-color + ); + --mdc-text-field-fill-color: var(--input-fill-color); + --mdc-text-field-disabled-fill-color: var(--input-disabled-fill-color); + --mdc-text-field-ink-color: var(--input-ink-color); + --mdc-text-field-label-ink-color: var(--input-label-ink-color); + --mdc-text-field-disabled-ink-color: var(--input-disabled-ink-color); + + --mdc-select-idle-line-color: var(--input-idle-line-color); + --mdc-select-hover-line-color: var(--input-hover-line-color); + --mdc-select-outlined-idle-border-color: var(--input-outlined-idle-border-color); + --mdc-select-outlined-hover-border-color: var(--input-outlined-hover-border-color); + --mdc-select-outlined-disabled-border-color: var( + --input-outlined-disabled-border-color + ); + --mdc-select-fill-color: var(--input-fill-color); + --mdc-select-disabled-fill-color: var(--input-disabled-fill-color); + --mdc-select-ink-color: var(--input-ink-color); + --mdc-select-label-ink-color: var(--input-label-ink-color); + --mdc-select-disabled-ink-color: var(--input-disabled-ink-color); + --mdc-select-dropdown-icon-color: var(--input-dropdown-icon-color); + --mdc-select-disabled-dropdown-icon-color: var(--input-disabled-ink-color); + --ha-assist-chip-filled-container-color: rgba(var(--rgb-primary-text-color), 0.15); + --ha-assist-chip-active-container-color: rgba(var(--rgb-primary-color), 0.15); + --chip-background-color: rgba(var(--rgb-primary-text-color), 0.15); + + // ===================== + // Semantic color styles + // https://github.com/home-assistant/frontend/blob/dev/src/resources/theme/color/semantic.globals.ts + // ===================== + --ha-color-focus: var(--ha-color-orange-60); + + /* text */ + --ha-color-text-primary: var(--ha-color-neutral-05); + --ha-color-text-secondary: var(--ha-color-neutral-40); + --ha-color-text-disabled: var(--ha-color-neutral-60); + --ha-color-text-link: var(--ha-color-primary-40); + /* border primary */ + --ha-color-border-primary-quiet: var(--ha-color-primary-90); + --ha-color-border-primary-normal: var(--ha-color-primary-70); + --ha-color-border-primary-loud: var(--ha-color-primary-40); + + /* border neutral */ + --ha-color-border-neutral-quiet: var(--ha-color-neutral-90); + --ha-color-border-neutral-normal: var(--ha-color-neutral-60); + --ha-color-border-neutral-loud: var(--ha-color-neutral-40); + + /* border danger */ + --ha-color-border-danger-quiet: var(--ha-color-red-90); + --ha-color-border-danger-normal: var(--ha-color-red-70); + --ha-color-border-danger-loud: var(--ha-color-red-40); + + /* border warning */ + --ha-color-border-warning-quiet: var(--ha-color-orange-90); + --ha-color-border-warning-normal: var(--ha-color-orange-70); + --ha-color-border-warning-loud: var(--ha-color-orange-40); + + /* border success */ + --ha-color-border-success-quiet: var(--ha-color-green-90); + --ha-color-border-success-normal: var(--ha-color-green-70); + --ha-color-border-success-loud: var(--ha-color-green-40); + + /* fill primary quiet */ + --ha-color-fill-primary-quiet-resting: var(--ha-color-primary-95); + --ha-color-fill-primary-quiet-hover: var(--ha-color-primary-90); + --ha-color-fill-primary-quiet-active: var(--ha-color-primary-95); + + /* fill primary normal */ + --ha-color-fill-primary-normal-resting: var(--ha-color-primary-90); + --ha-color-fill-primary-normal-hover: var(--ha-color-primary-80); + --ha-color-fill-primary-normal-active: var(--ha-color-primary-90); + + /* fill primary loud */ + --ha-color-fill-primary-loud-resting: var(--ha-color-primary-40); + --ha-color-fill-primary-loud-hover: var(--ha-color-primary-30); + --ha-color-fill-primary-loud-active: var(--ha-color-primary-40); + + /* fill neutral quiet */ + --ha-color-fill-neutral-quiet-resting: var(--ha-color-neutral-95); + --ha-color-fill-neutral-quiet-hover: var(--ha-color-neutral-90); + --ha-color-fill-neutral-quiet-active: var(--ha-color-neutral-95); + + /* fill neutral normal */ + --ha-color-fill-neutral-normal-resting: var(--ha-color-neutral-90); + --ha-color-fill-neutral-normal-hover: var(--ha-color-neutral-80); + --ha-color-fill-neutral-normal-active: var(--ha-color-neutral-90); + + /* fill neutral loud */ + --ha-color-fill-neutral-loud-resting: var(--ha-color-neutral-40); + --ha-color-fill-neutral-loud-hover: var(--ha-color-neutral-30); + --ha-color-fill-neutral-loud-active: var(--ha-color-neutral-40); + + /* fill disabled quiet */ + --ha-color-fill-disabled-quiet-resting: var(--ha-color-neutral-95); + + /* fill disabled normal */ + --ha-color-fill-disabled-normal-resting: var(--ha-color-neutral-95); + + /* fill disabled loud */ + --ha-color-fill-disabled-loud-resting: var(--ha-color-neutral-80); + + /* fill danger quiet */ + --ha-color-fill-danger-quiet-resting: var(--ha-color-red-95); + --ha-color-fill-danger-quiet-hover: var(--ha-color-red-90); + --ha-color-fill-danger-quiet-active: var(--ha-color-red-95); + + /* fill danger normal */ + --ha-color-fill-danger-normal-resting: var(--ha-color-red-90); + --ha-color-fill-danger-normal-hover: var(--ha-color-red-80); + --ha-color-fill-danger-normal-active: var(--ha-color-red-90); + + /* fill danger loud */ + --ha-color-fill-danger-loud-resting: var(--ha-color-red-50); + --ha-color-fill-danger-loud-hover: var(--ha-color-red-40); + --ha-color-fill-danger-loud-active: var(--ha-color-red-50); + + /* fill warning quiet */ + --ha-color-fill-warning-quiet-resting: var(--ha-color-orange-95); + --ha-color-fill-warning-quiet-hover: var(--ha-color-orange-90); + --ha-color-fill-warning-quiet-active: var(--ha-color-orange-95); + + /* fill warning normal */ + --ha-color-fill-warning-normal-resting: var(--ha-color-orange-90); + --ha-color-fill-warning-normal-hover: var(--ha-color-orange-80); + --ha-color-fill-warning-normal-active: var(--ha-color-orange-90); + + /* fill warning loud */ + --ha-color-fill-warning-loud-resting: var(--ha-color-orange-70); + --ha-color-fill-warning-loud-hover: var(--ha-color-orange-50); + --ha-color-fill-warning-loud-active: var(--ha-color-orange-70); + + /* fill success quiet */ + --ha-color-fill-success-quiet-resting: var(--ha-color-green-95); + --ha-color-fill-success-quiet-hover: var(--ha-color-green-90); + --ha-color-fill-success-quiet-active: var(--ha-color-green-95); + + /* fill success normal */ + --ha-color-fill-success-normal-resting: var(--ha-color-green-90); + --ha-color-fill-success-normal-hover: var(--ha-color-green-80); + --ha-color-fill-success-normal-active: var(--ha-color-green-90); + + /* fill success loud */ + --ha-color-fill-success-loud-resting: var(--ha-color-green-50); + --ha-color-fill-success-loud-hover: var(--ha-color-green-40); + --ha-color-fill-success-loud-active: var(--ha-color-green-50); + + /* on primary */ + --ha-color-on-primary-quiet: var(--ha-color-primary-50); + --ha-color-on-primary-normal: var(--ha-color-primary-40); + --ha-color-on-primary-loud: var(--white-color); + + /* on neutral */ + --ha-color-on-neutral-quiet: var(--ha-color-neutral-50); + --ha-color-on-neutral-normal: var(--ha-color-neutral-40); + --ha-color-on-neutral-loud: var(--white-color); + + /* on disabled */ + --ha-color-on-disabled-quiet: var(--ha-color-neutral-80); + --ha-color-on-disabled-normal: var(--ha-color-neutral-70); + --ha-color-on-disabled-loud: var(--ha-color-neutral-95); + + /* on danger */ + --ha-color-on-danger-quiet: var(--ha-color-red-50); + --ha-color-on-danger-normal: var(--ha-color-red-40); + --ha-color-on-danger-loud: var(--white-color); + + /* on warning */ + --ha-color-on-warning-quiet: var(--ha-color-orange-50); + --ha-color-on-warning-normal: var(--ha-color-orange-40); + --ha-color-on-warning-loud: var(--white-color); + + /* on success */ + --ha-color-on-success-quiet: var(--ha-color-green-50); + --ha-color-on-success-normal: var(--ha-color-green-40); + --ha-color-on-success-loud: var(--white-color); + + /* Surfaces */ + --ha-color-surface-default: var(--ha-color-neutral-95); + --ha-color-on-surface-default: var(--ha-color-neutral-05); + + /* Scrollable fade */ + --ha-color-shadow-scrollable-fade: rgba(0, 0, 0, 0.08); + + // ============ // HA dynamically sets these values based on choice of primary/secondary // colors and hex blending techniques. The card does not currently // re-implement this logic, rather just uses the default static value. // See: https://github.com/home-assistant/frontend/blob/dev/src/common/dom/apply_themes_on_element.ts - + // ============ --app-header-background-color: #03a9f4; + --app-header-text-color: white; + + // Menu border colors need to re-adjust based on redefined variable. + --wa-color-surface-border: var(--ha-color-border-neutral-quiet); } diff --git a/src/scss/themes/traditional.scss b/src/scss/themes/traditional.scss index 6d717c47..bd6a2203 100644 --- a/src/scss/themes/traditional.scss +++ b/src/scss/themes/traditional.scss @@ -14,6 +14,7 @@ *********/ --advanced-camera-card-control-background: black; + --advanced-camera-card-control-foreground: white; /****** * Menu diff --git a/src/scss/z-index.scss b/src/scss/z-index.scss index 2d5139b4..d8b4861f 100644 --- a/src/scss/z-index.scss +++ b/src/scss/z-index.scss @@ -12,7 +12,6 @@ $z-index-effect: 7; $z-index-loading: 6; -$z-index-submenu: 5; // Need menu to render above drawer (so the drawer button is below menu-submenus) $z-index-menu: 4; diff --git a/tests/camera-manager/frigate/camera.test.ts b/tests/camera-manager/frigate/camera.test.ts index 6ccd4757..8e566564 100644 --- a/tests/camera-manager/frigate/camera.test.ts +++ b/tests/camera-manager/frigate/camera.test.ts @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { CameraManagerEngine } from '../../../src/camera-manager/engine'; @@ -514,7 +515,9 @@ describe('FrigateCamera', () => { ); expect(camera.getEndpoints({ view: 'media', media })?.ui).toEqual({ - endpoint: 'http://frigate/recording/front_door/2023-01-01/10', + endpoint: + 'http://frigate/recording/front_door/' + + format(media.getStartTime(), 'yyyy-MM-dd/HH'), }); }); diff --git a/tests/camera-manager/frigate/event-watcher.test.ts b/tests/camera-manager/frigate/event-watcher.test.ts index a7e2eb1b..79ace5f0 100644 --- a/tests/camera-manager/frigate/event-watcher.test.ts +++ b/tests/camera-manager/frigate/event-watcher.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, assert, describe, expect, it, vi } from 'vitest'; import { FrigateEventWatcher } from '../../../src/camera-manager/frigate/event-watcher.js'; import { FrigateEventChange } from '../../../src/camera-manager/frigate/types.js'; import { HomeAssistant } from '../../../src/ha/types.js'; @@ -77,6 +77,33 @@ describe('FrigateEventWatcher', () => { expect(unsubscribeCallback).toBeCalledTimes(1); }); + it('should handle unsubscribe during pending subscription', async () => { + const stateWatcher = new FrigateEventWatcher(); + const hass = createHASS(); + + let resolveSubscription: ((callback: () => Promise) => void) | undefined; + const subscriptionPromise = new Promise<() => Promise>((resolve) => { + resolveSubscription = resolve; + }); + vi.mocked(hass.connection.subscribeMessage).mockReturnValue(subscriptionPromise); + + const request = { + instanceID: 'frigate', + callback: vi.fn(), + }; + + // Start subscription (doesn't complete yet as not awaited). + const subscribePromise = stateWatcher.subscribe(hass, request); + + // Unsubscribe while subscription is still pending. + await stateWatcher.unsubscribe(request); + + // Complete the subscription. + assert(resolveSubscription); + resolveSubscription(vi.fn()); + await subscribePromise; + }); + describe('should call handler', () => { afterEach(() => { vi.resetAllMocks(); diff --git a/tsconfig.json b/tsconfig.json index f488f60f..de9c8d39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,11 +23,12 @@ // These tags are provided by the HA frontend and [almost all] cannot be // imported by a custom card directly. "globalTags": [ - "ha-button-menu", "ha-button", "ha-camera-stream", "ha-card", "ha-combo-box", + "ha-dropdown-item", + "ha-dropdown", "ha-hls-player", "ha-icon-button", "ha-icon",