diff --git a/docs/examples.md b/docs/examples.md index 890c9370..9bbfe13b 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1184,6 +1184,26 @@ automations: See [Trigger Templates](./configuration/templates.md?id=triggers). +## Theme + +### Custom border radius + +This example shows how to customize the border radius for both the card and buttons to create a cohesive squared-off look: + +```yaml +type: custom:advanced-camera-card +cameras: + - camera_entity: camera.office +view: + theme: + overrides: + # Set a custom border radius for the card + '--advanced-camera-card-border-radius': '12px' + + # Match the button border radius to complement the card + '--advanced-camera-card-button-border-radius': '12px' +``` + ## Trigger actions You can control the card itself with the `custom:advanced-camera-card-action` action. diff --git a/src/card-controller/style-manager.ts b/src/card-controller/style-manager.ts index 8df563b4..f2446b7a 100644 --- a/src/card-controller/style-manager.ts +++ b/src/card-controller/style-manager.ts @@ -1,7 +1,11 @@ import { StyleInfo } from 'lit/directives/style-map'; import { AdvancedCameraCardConfig, configDefaults } from '../config/schema/types'; import { ThemeConfig, ThemeName } from '../config/schema/view'; -import { aspectRatioToStyle, setOrRemoveAttribute } from '../utils/basic'; +import { + aspectRatioToStyle, + setOrRemoveAttribute, + setOrRemoveStyleProperty, +} from '../utils/basic'; import { View } from '../view/view'; import { CardStyleAPI } from './types'; @@ -94,20 +98,27 @@ export class StyleManager { protected _setPerformance(): void { const STYLE_DISABLE_MAP = { - box_shadow: 'none', - border_radius: '0px', + box_shadow: { + cssKey: '--advanced-camera-card-box-shadow-override', + value: 'none', + }, + border_radius: { + cssKey: '--advanced-camera-card-border-radius-override', + value: '0px', + }, }; const element = this._api.getCardElementManager().getElement(); const performance = this._api.getConfigManager().getCardWideConfig()?.performance; const styles = performance?.style ?? {}; for (const configKey of Object.keys(styles)) { - const CSSKey = `--advanced-camera-card-css-${configKey.replaceAll('_', '-')}`; - if (styles[configKey] === false) { - element.style.setProperty(CSSKey, STYLE_DISABLE_MAP[configKey]); - } else { - element.style.removeProperty(CSSKey); - } + const mapping = STYLE_DISABLE_MAP[configKey]; + setOrRemoveStyleProperty( + element, + !styles[configKey], + mapping.cssKey, + mapping.value, + ); } } diff --git a/src/scss/card.scss b/src/scss/card.scss index cab7a493..cff2395b 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -11,7 +11,7 @@ // this ensures the same experience across all browsers. background-color: var(--advanced-camera-card-background); - border-radius: var(--ha-card-border-radius, 4px); + border-radius: var(--advanced-camera-card-border-radius-final); overflow: auto; height: var(--advanced-camera-card-height); diff --git a/src/scss/drawer-inject.scss b/src/scss/drawer-inject.scss index 0cfe88fd..a80e9cbc 100644 --- a/src/scss/drawer-inject.scss +++ b/src/scss/drawer-inject.scss @@ -42,7 +42,7 @@ :host([location='right'][open]) #d { transform: none; - box-shadow: var(--advanced-camera-card-css-box-shadow, 0px 0px 25px 0px black); + box-shadow: var(--advanced-camera-card-box-shadow-override, 0px 0px 25px 0px black); } #ifs { diff --git a/src/scss/live-carousel.scss b/src/scss/live-carousel.scss index 75d4ab43..99e32152 100644 --- a/src/scss/live-carousel.scss +++ b/src/scss/live-carousel.scss @@ -8,7 +8,7 @@ position: relative; // See: https://github.com/dermotduffy/advanced-camera-card/issues/2107 - border-radius: var(--ha-card-border-radius, 4px); + border-radius: var(--advanced-camera-card-border-radius-final); overflow: hidden; } diff --git a/src/scss/media-gallery.scss b/src/scss/media-gallery.scss index 4c69f8bf..2fa57cb2 100644 --- a/src/scss/media-gallery.scss +++ b/src/scss/media-gallery.scss @@ -25,8 +25,5 @@ advanced-camera-card-thumbnail.selected { // this outer border, we need to add the size of the border to the thumbnail // image border radius. // Related: https://www.30secondsofcode.org/articles/s/css-nested-border-radius - border-radius: calc( - var(--advanced-camera-card-css-border-radius, var(--ha-card-border-radius, 4px)) + - 4px - ); + border-radius: calc(var(--advanced-camera-card-border-radius-final) + 4px); } diff --git a/src/scss/next-previous-control.scss b/src/scss/next-previous-control.scss index caa643ab..15a17fdb 100644 --- a/src/scss/next-previous-control.scss +++ b/src/scss/next-previous-control.scss @@ -31,7 +31,7 @@ border-radius: 50%; height: var(--advanced-camera-card-next-prev-size); top: calc(50% - (var(--advanced-camera-card-next-prev-size) / 2)); - box-shadow: var(--advanced-camera-card-css-box-shadow, 0px 0px 20px 5px black); + box-shadow: var(--advanced-camera-card-box-shadow-override, 0px 0px 20px 5px black); transition: all 0.2s ease-out; opacity: 0.8; aspect-ratio: 1 / 1; diff --git a/src/scss/ptz.scss b/src/scss/ptz.scss index 4ce20e85..0f00c6f7 100644 --- a/src/scss/ptz.scss +++ b/src/scss/ptz.scss @@ -80,7 +80,7 @@ .ptz-zoom, .ptz-presets { - border-radius: var(--ha-card-border-radius, 4px); + border-radius: var(--advanced-camera-card-border-radius-final); } /*********** diff --git a/src/scss/themes/base.scss b/src/scss/themes/base.scss index fb685209..3da18fea 100644 --- a/src/scss/themes/base.scss +++ b/src/scss/themes/base.scss @@ -18,6 +18,16 @@ --advanced-camera-card-text-color: var(--primary-text-color); --advanced-camera-card-divider-color: var(--divider-color); + // Border radius variable cascade: + // 1. --advanced-camera-card-border-radius-override (set by JS for performance config) + // 2. --advanced-camera-card-border-radius (user theme override) + // 3. --ha-card-border-radius (Home Assistant theme) + // 4. 4px (default) + --advanced-camera-card-border-radius-final: var( + --advanced-camera-card-border-radius-override, + var(--advanced-camera-card-border-radius, var(--ha-card-border-radius, 4px)) + ); + --advanced-camera-card-control-background: var(--app-header-background-color); --advanced-camera-card-control-background-opacity: 60%; diff --git a/src/scss/thumbnail-feature-thumbnail.scss b/src/scss/thumbnail-feature-thumbnail.scss index a8eb586f..3e937670 100644 --- a/src/scss/thumbnail-feature-thumbnail.scss +++ b/src/scss/thumbnail-feature-thumbnail.scss @@ -24,10 +24,7 @@ img { vertical-align: top; margin: 0; - border-radius: var( - --advanced-camera-card-css-border-radius, - var(--ha-card-border-radius, 4px) - ); + border-radius: var(--advanced-camera-card-border-radius-final); // Not 'contain' as some thumbnails may vary in aspect-ratio slightly and // should be clipped to fill the thumbnail div whilst maintaining diff --git a/src/scss/thumbnail-feature.scss b/src/scss/thumbnail-feature.scss index efbc957e..81d40e04 100644 --- a/src/scss/thumbnail-feature.scss +++ b/src/scss/thumbnail-feature.scss @@ -11,10 +11,7 @@ border: 1px solid var(--secondary-color); background-color: var(--secondary-background-color); - border-radius: var( - --advanced-camera-card-css-border-radius, - var(--ha-card-border-radius, 4px) - ); + border-radius: var(--advanced-camera-card-border-radius-final); box-sizing: border-box; diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss index 1759cfc8..a513bea1 100644 --- a/src/scss/thumbnail.scss +++ b/src/scss/thumbnail.scss @@ -16,10 +16,7 @@ :host([details]) { border: 1px solid var(--advanced-camera-card-thumbnail-border-color); - border-radius: var( - --advanced-camera-card-css-border-radius, - var(--ha-card-border-radius, 4px) - ); + border-radius: var(--advanced-camera-card-border-radius-final); padding: 2px; // When details are enabled, use a background color so that the details have diff --git a/src/scss/timeline-core.scss b/src/scss/timeline-core.scss index acc9ff27..5911af93 100644 --- a/src/scss/timeline-core.scss +++ b/src/scss/timeline-core.scss @@ -69,7 +69,7 @@ div.timeline { border-color: var(--advanced-camera-card-active-color); background-color: var(--advanced-camera-card-active-color); box-shadow: var( - --advanced-camera-card-css-box-shadow, + --advanced-camera-card-box-shadow-override, 0px 0px 5px 1px var(--advanced-camera-card-active-color) ); } @@ -109,12 +109,12 @@ div.timeline { color: var(--advanced-camera-card-timeline-text-color); background-color: var(--advanced-camera-card-timeline-background); box-shadow: var( - --advanced-camera-card-css-box-shadow, + --advanced-camera-card-box-shadow-override, 0px 0px 5px 1px var(--advanced-camera-card-timeline-item-color) ); } .vis-item.vis-range { - border-radius: var(--advanced-camera-card-css-border-radius, unset); + border-radius: var(--advanced-camera-card-border-radius-final); } .vis-time-axis .vis-grid.vis-minor { diff --git a/src/scss/viewer-carousel.scss b/src/scss/viewer-carousel.scss index a241d392..d99a41b7 100644 --- a/src/scss/viewer-carousel.scss +++ b/src/scss/viewer-carousel.scss @@ -8,7 +8,7 @@ position: relative; // See: https://github.com/dermotduffy/advanced-camera-card/issues/2107 - border-radius: var(--ha-card-border-radius, 4px); + border-radius: var(--advanced-camera-card-border-radius-final); overflow: hidden; } diff --git a/tests/card-controller/style-manager.test.ts b/tests/card-controller/style-manager.test.ts index 76414651..389c495a 100644 --- a/tests/card-controller/style-manager.test.ts +++ b/tests/card-controller/style-manager.test.ts @@ -191,10 +191,10 @@ describe('StyleManager', () => { manager.updateFromConfig(); expect( - element.style.getPropertyValue('--advanced-camera-card-css-box-shadow'), + element.style.getPropertyValue('--advanced-camera-card-box-shadow-override'), ).toBeFalsy(); expect( - element.style.getPropertyValue('--advanced-camera-card-css-border-radius'), + element.style.getPropertyValue('--advanced-camera-card-border-radius-override'), ).toBeFalsy(); }); @@ -217,12 +217,56 @@ describe('StyleManager', () => { manager.updateFromConfig(); expect( - element.style.getPropertyValue('--advanced-camera-card-css-box-shadow'), + element.style.getPropertyValue('--advanced-camera-card-box-shadow-override'), ).toEqual('none'); expect( - element.style.getPropertyValue('--advanced-camera-card-css-border-radius'), + element.style.getPropertyValue('--advanced-camera-card-border-radius-override'), ).toBeFalsy(); }); + + it('box_shadow disabled sets override', () => { + const api = createCardAPI(); + const element = document.createElement('div'); + vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); + vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue( + createConfig({ + performance: { + style: { + box_shadow: false, + }, + }, + }), + ); + const manager = new StyleManager(api); + + manager.updateFromConfig(); + + expect( + element.style.getPropertyValue('--advanced-camera-card-box-shadow-override'), + ).toEqual('none'); + }); + + it('border_radius disabled sets override', () => { + const api = createCardAPI(); + const element = document.createElement('div'); + vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element); + vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue( + createConfig({ + performance: { + style: { + border_radius: false, + }, + }, + }), + ); + const manager = new StyleManager(api); + + manager.updateFromConfig(); + + expect( + element.style.getPropertyValue('--advanced-camera-card-border-radius-override'), + ).toEqual('0px'); + }); }); describe('getAspectRatioStyle', () => {