feat: Add support for overriding card border radius (#2298)

- Closes #2287


[skip ci]
This commit is contained in:
Dermot Duffy
2025-12-20 17:00:04 -08:00
committed by GitHub
parent d17ecd6c12
commit de42a1652f
15 changed files with 111 additions and 38 deletions
+20
View File
@@ -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.
+20 -9
View File
@@ -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,
);
}
}
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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;
}
+1 -4
View File
@@ -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);
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
}
/***********
+10
View File
@@ -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%;
+1 -4
View File
@@ -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
+1 -4
View File
@@ -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;
+1 -4
View File
@@ -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
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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;
}
+48 -4
View File
@@ -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', () => {