Compare commits

..

5 Commits

Author SHA1 Message Date
Aidan Timson
a6a83258ab Add 2025-10-08 09:38:17 +01:00
Aidan Timson
01889de462 Add date option 2025-10-08 09:30:56 +01:00
Paul Bottein
a8f8d197f8 Add tooltip instead of title for 'add' button (#27399) 2025-10-07 17:48:49 +02:00
Paul Bottein
4fcac79047 Use right variable for content color in tooltip (#27400) 2025-10-07 15:46:40 +00:00
Paul Bottein
42ddacd41a Add plus and minus button for media player more info (#27398)
* Add plus and minus button for media player even if it support volume slider

* Update src/dialogs/more-info/controls/more-info-media_player.ts

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>

* Remove hardcoded support

---------

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2025-10-07 17:43:17 +02:00
11 changed files with 235 additions and 238 deletions

View File

@@ -1,53 +0,0 @@
/**
* Trigger a view transition if supported by the browser
* @param updateCallback - Callback function that updates the DOM
* @returns Promise that resolves when the transition is complete
*/
export const startViewTransition = async (
updateCallback: () => void | Promise<void>
): Promise<void> => {
// Check if View Transitions API is supported
if (
!document.startViewTransition ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
// Fallback: just run the update without transition
await updateCallback();
return;
}
// Start the view transition
const transition = document.startViewTransition(async () => {
await updateCallback();
});
try {
await transition.finished;
} catch (error) {
// Transitions can be skipped, which is fine
// eslint-disable-next-line no-console
console.debug("View transition skipped or failed:", error);
}
};
/**
* Helper to apply view transition on first render
* @param _element - The element to observe (unused, kept for API consistency)
* @param callback - Callback when element is first rendered
*/
export const applyViewTransitionOnLoad = (
_element: HTMLElement,
callback?: () => void
): void => {
if (!document.startViewTransition) {
callback?.();
return;
}
// Use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
startViewTransition(() => {
callback?.();
});
});
};

View File

@@ -17,7 +17,7 @@ export class HaTooltip extends Tooltip {
css`
:host {
--wa-tooltip-background-color: var(--secondary-background-color);
--wa-tooltip-color: var(--primary-text-color);
--wa-tooltip-content-color: var(--primary-text-color);
--wa-tooltip-font-family: var(
--ha-tooltip-font-family,
var(--ha-font-family-body)

View File

@@ -77,80 +77,84 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
if (!stateActive(this.stateObj)) {
return nothing;
}
const supportsMute = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_MUTE
);
const supportsSliding = supportsFeature(
const supportsSet = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_SET
);
return html`${(supportsFeature(
this.stateObj!,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(this.stateObj!)
? html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
) && !supportsSliding
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
${supportsSliding
? html`
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
</div>
`
: nothing}`;
const supportsStep = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
);
if (!supportsMute && !supportsSet && !supportsStep) {
return nothing;
}
return html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: nothing}
${supportsStep
? html` <ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>`
: nothing}
${supportsSet
? html`
${!supportsMute && !supportsStep
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) * 100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
${supportsStep
? html`
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
</div>
`;
}
protected _renderSourceControl() {

View File

@@ -34,12 +34,16 @@ export class HuiClockCardAnalog extends LitElement {
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
@state() private _dateFormat?: Intl.DateTimeFormat;
@state() private _hourOffsetSec?: number;
@state() private _minuteOffsetSec?: number;
@state() private _secondOffsetSec?: number;
@state() private _date?: string;
private _initDate() {
if (!this.config || !this.hass) {
return;
@@ -60,6 +64,17 @@ export class HuiClockCardAnalog extends LitElement {
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
if (this.config.date_format === "day") {
this._dateFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
day: "2-digit",
timeZone:
this.config.time_zone ||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
} else {
this._dateFormat = undefined;
}
this._computeOffsets();
}
@@ -111,6 +126,13 @@ export class HuiClockCardAnalog extends LitElement {
this._secondOffsetSec = secondsWithMs;
this._minuteOffsetSec = minute * 60 + secondsWithMs;
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
if (this._dateFormat) {
const dateParts = this._dateFormat.formatToParts();
this._date = dateParts.find((part) => part.type === "day")?.value;
} else {
this._date = undefined;
}
}
render() {
@@ -231,6 +253,9 @@ export class HuiClockCardAnalog extends LitElement {
}s;`}
></div>`
: nothing}
${this._date !== undefined
? html`<div class="date ${sizeClass}">${this._date}</div>`
: nothing}
</div>
</div>
`;
@@ -399,6 +424,36 @@ export class HuiClockCardAnalog extends LitElement {
animation-timing-function: steps(60, end);
}
.date {
position: absolute;
top: 50%;
right: 25%;
transform: translate(50%, -50%);
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--primary-text-color);
opacity: 0.6;
z-index: 0;
pointer-events: none;
padding: 1px 3px;
border-radius: 2px;
box-shadow:
inset 0 1px 2px rgba(0, 0, 0, 0.15),
inset 0 -1px 1px rgba(0, 0, 0, 0.1);
}
.date.size-medium {
font-size: var(--ha-font-size-l);
padding: 2px 4px;
border-radius: 2px;
}
.date.size-large {
font-size: var(--ha-font-size-xl);
padding: 2px 5px;
border-radius: 3px;
}
@keyframes ha-clock-rotate {
from {
transform: translate(-50%, 0) rotate(0deg);

View File

@@ -16,6 +16,8 @@ export class HuiClockCardDigital extends LitElement {
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
@state() private _dateFormat?: Intl.DateTimeFormat;
@state() private _timeHour?: string;
@state() private _timeMinute?: string;
@@ -24,6 +26,8 @@ export class HuiClockCardDigital extends LitElement {
@state() private _timeAmPm?: string;
@state() private _date?: string;
private _tickInterval?: undefined | number;
private _initDate() {
@@ -48,6 +52,17 @@ export class HuiClockCardDigital extends LitElement {
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
if (this.config?.date_format === "day") {
this._dateFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
day: "2-digit",
timeZone:
this.config?.time_zone ||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
} else {
this._dateFormat = undefined;
}
this._tick();
}
@@ -93,6 +108,13 @@ export class HuiClockCardDigital extends LitElement {
? parts.find((part) => part.type === "second")?.value
: undefined;
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
if (this._dateFormat) {
const dateParts = this._dateFormat.formatToParts();
this._date = dateParts.find((part) => part.type === "day")?.value;
} else {
this._date = undefined;
}
}
render() {
@@ -113,6 +135,9 @@ export class HuiClockCardDigital extends LitElement {
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
: nothing}
</div>
${this._date !== undefined
? html`<div class="date ${sizeClass}">${this._date}</div>`
: nothing}
`;
}
@@ -121,6 +146,26 @@ export class HuiClockCardDigital extends LitElement {
display: block;
}
.date {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
opacity: 0.6;
text-align: center;
direction: ltr;
margin-top: 4px;
align-self: center;
}
.date.size-medium {
font-size: var(--ha-font-size-l);
margin-top: 6px;
}
.date.size-large {
font-size: var(--ha-font-size-xl);
margin-top: 8px;
}
.time-parts {
align-items: center;
display: grid;

View File

@@ -381,6 +381,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
seconds_motion?: "continuous" | "tick";
time_format?: TimeFormat;
time_zone?: string;
date_format?: "none" | "day";
no_background?: boolean;
// Analog clock options
border?: boolean;

View File

@@ -37,6 +37,9 @@ const cardConfigStruct = assign(
),
time_format: optional(enums(Object.values(TimeFormat))),
time_zone: optional(enums(Object.keys(timezones))),
date_format: optional(
defaulted(union([literal("none"), literal("day")]), literal("none"))
),
show_seconds: optional(boolean()),
no_background: optional(boolean()),
// Analog clock options
@@ -117,6 +120,20 @@ export class HuiClockCardEditor
},
},
},
{
name: "date_format",
selector: {
select: {
mode: "dropdown",
options: ["none", "day"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.date_formats.${value}`
),
})),
},
},
},
{ name: "show_seconds", selector: { boolean: {} } },
{ name: "no_background", selector: { boolean: {} } },
...(clockStyle === "digital"
@@ -265,6 +282,7 @@ export class HuiClockCardEditor
clock_size: "small",
time_zone: "auto",
time_format: "auto",
date_format: "none",
show_seconds: false,
no_background: false,
// Analog clock options
@@ -359,6 +377,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_zone`
);
case "date_format":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date_format`
);
case "show_seconds":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.show_seconds`

View File

@@ -25,10 +25,6 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import {
applyViewTransitionOnLoad,
startViewTransition,
} from "../../common/dom/view_transition";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { goBack, navigate } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
@@ -76,7 +72,7 @@ import {
} from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle, haStyleViewTransitions } from "../../resources/styles";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showToast } from "../../util/toast";
@@ -322,7 +318,7 @@ class HUIRoot extends LitElement {
menu-corner="END"
>
<ha-icon-button
.label=${label}
.id="button-${index}"
.path=${item.icon}
slot="trigger"
></ha-icon-button>
@@ -344,6 +340,9 @@ class HUIRoot extends LitElement {
`
)}
</ha-button-menu>
<ha-tooltip placement="bottom" .for="button-${index}">
${label}
</ha-tooltip>
`
: html`
<ha-icon-button
@@ -623,9 +622,6 @@ class HUIRoot extends LitElement {
window.addEventListener("scroll", this._handleWindowScroll, {
passive: true,
});
// Trigger view transition on initial load
applyViewTransitionOnLoad(this);
}
public connectedCallback(): void {
@@ -1165,45 +1161,43 @@ class HUIRoot extends LitElement {
// Recreate a new element to clear the applied themes.
const root = this._viewRoot;
startViewTransition(() => {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
if (root.lastChild) {
root.removeChild(root.lastChild);
}
if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
root.appendChild(unusedEntities);
return;
}
if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
root.appendChild(unusedEntities);
return;
}
let view;
const viewConfig = this.config.views[viewIndex];
let view;
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
this.lovelace!.setEditMode(true);
return;
}
if (!viewConfig) {
this.lovelace!.setEditMode(true);
return;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
root.appendChild(view);
});
root.appendChild(view);
}
private _openShortcutDialog(ev: Event) {
@@ -1214,7 +1208,6 @@ class HUIRoot extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleViewTransitions,
css`
:host {
-ms-user-select: none;
@@ -1269,7 +1262,6 @@ class HUIRoot extends LitElement {
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
view-transition-name: lovelace-toolbar;
}
.narrow .toolbar {
padding: 0 4px;
@@ -1418,7 +1410,6 @@ class HUIRoot extends LitElement {
hui-view-container > * {
flex: 1 1 100%;
max-width: 100%;
view-transition-name: lovelace-view;
}
/**
* In edit mode we have the tab bar on a new line *

View File

@@ -196,58 +196,3 @@ export const baseEntrypointStyles = css`
width: 100vw;
}
`;
export const haStyleViewTransitions = css`
@media (prefers-reduced-motion: no-preference) {
/* Toolbar fade in */
::view-transition-group(lovelace-toolbar) {
animation-duration: var(--ha-animation-duration);
animation-timing-function: ease-out;
}
::view-transition-new(lovelace-toolbar) {
animation: fade-in var(--ha-animation-duration) ease-out;
animation-delay: var(--ha-animation-delay-base);
}
/* View slide down */
::view-transition-group(lovelace-view) {
animation-duration: var(--ha-animation-duration);
animation-timing-function: ease-out;
}
::view-transition-new(lovelace-view) {
animation: fade-in-slide-down var(--ha-animation-duration) ease-out;
animation-delay: var(--ha-animation-delay-base);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-in-slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in-slide-down {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;

View File

@@ -42,24 +42,6 @@ export const coreStyles = css`
--ha-space-18: 72px;
--ha-space-19: 76px;
--ha-space-20: 80px;
/* Animation timing */
--ha-animation-duration: 350ms;
--ha-animation-delay-base: 50ms;
}
@media (prefers-reduced-motion: reduce) {
html {
--ha-animation-duration: 150ms;
--ha-animation-delay-base: 20ms;
}
}
/* Enable View Transitions API for supported browsers */
@supports (view-transition-name: none) {
:root {
view-transition-name: root;
}
}
`;

View File

@@ -7809,6 +7809,11 @@
"time_zones": {
"auto": "Use user settings"
},
"date_format": "Date format",
"date_formats": {
"none": "None",
"day": "Day"
},
"no_background": "No background",
"border": {
"label": "Border",