mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Analog style for clock card (#26557)
Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
300
src/panels/lovelace/cards/clock/hui-clock-card-analog.ts
Normal file
300
src/panels/lovelace/cards/clock/hui-clock-card-analog.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
@customElement("hui-clock-card-analog")
|
||||
export class HuiClockCardAnalog extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: ClockCardConfig;
|
||||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _hourDeg?: number;
|
||||
|
||||
@state() private _minuteDeg?: number;
|
||||
|
||||
@state() private _secondDeg?: number;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = this.hass.locale;
|
||||
if (this.config.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h12",
|
||||
timeZone:
|
||||
this.config.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const hourStr = parts.find((p) => p.type === "hour")?.value;
|
||||
const minuteStr = parts.find((p) => p.type === "minute")?.value;
|
||||
const secondStr = parts.find((p) => p.type === "second")?.value;
|
||||
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
|
||||
this._hourDeg = hour * 30 + minute * 0.5; // 30deg per hour + 0.5deg per minute
|
||||
this._minuteDeg = minute * 6 + second * 0.1; // 6deg per minute + 0.1deg per second
|
||||
this._secondDeg = this.config?.show_seconds ? second * 6 : undefined; // 6deg per second
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="analog-clock ${sizeClass}"
|
||||
role="img"
|
||||
aria-label="Analog clock"
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
dial: true,
|
||||
"dial-border": this.config.border ?? false,
|
||||
})}
|
||||
>
|
||||
${this.config.ticks === "quarter"
|
||||
? Array.from({ length: 4 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 4 ticks
|
||||
html`
|
||||
<div
|
||||
aria-hidden
|
||||
class="tick hour"
|
||||
style=${`--tick-rotation: ${i * 90}deg;`}
|
||||
>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: !this.config.ticks || // Default to hour ticks
|
||||
this.config.ticks === "hour"
|
||||
? Array.from({ length: 12 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 12 ticks
|
||||
html`
|
||||
<div
|
||||
aria-hidden
|
||||
class="tick hour"
|
||||
style=${`--tick-rotation: ${i * 30}deg;`}
|
||||
>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: this.config.ticks === "minute"
|
||||
? Array.from({ length: 60 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 60 ticks
|
||||
html`
|
||||
<div
|
||||
aria-hidden
|
||||
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
|
||||
style=${`--tick-rotation: ${i * 6}deg;`}
|
||||
>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${`--hand-rotation: ${this._hourDeg ?? 0}deg;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${`--hand-rotation: ${this._minuteDeg ?? 0}deg;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
class="hand second"
|
||||
style=${`--hand-rotation: ${this._secondDeg ?? 0}deg;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.analog-clock {
|
||||
--clock-size: 100px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--clock-size);
|
||||
height: var(--clock-size);
|
||||
}
|
||||
|
||||
.analog-clock.size-medium {
|
||||
--clock-size: 160px;
|
||||
}
|
||||
|
||||
.analog-clock.size-large {
|
||||
--clock-size: 220px;
|
||||
}
|
||||
|
||||
.dial {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dial-border {
|
||||
border: 2px solid var(--divider-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(var(--tick-rotation));
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tick .line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: calc(var(--clock-size) * 0.04);
|
||||
background: var(--primary-text-color);
|
||||
opacity: 0.5;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.tick.hour .line {
|
||||
width: 2px;
|
||||
height: calc(var(--clock-size) * 0.07);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.center-dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-text-color);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hand {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform-origin: 50% 100%;
|
||||
transform: translate(-50%, 0) rotate(var(--hand-rotation, 0deg));
|
||||
background: var(--primary-text-color);
|
||||
border-radius: 2px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.hand.hour {
|
||||
width: 4px;
|
||||
height: calc(var(--clock-size) * 0.25); /* 25% of the clock size */
|
||||
background: var(--primary-text-color);
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hand.minute {
|
||||
width: 3px;
|
||||
height: calc(var(--clock-size) * 0.35); /* 35% of the clock size */
|
||||
background: var(--primary-text-color);
|
||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.9;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hand.second {
|
||||
width: 2px;
|
||||
height: calc(var(--clock-size) * 0.42); /* 42% of the clock size */
|
||||
background: var(--ha-color-border-danger-normal);
|
||||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.8;
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-clock-card-analog": HuiClockCardAnalog;
|
||||
}
|
||||
}
|
||||
196
src/panels/lovelace/cards/clock/hui-clock-card-digital.ts
Normal file
196
src/panels/lovelace/cards/clock/hui-clock-card-digital.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { useAmPm } from "../../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
@customElement("hui-clock-card-digital")
|
||||
export class HuiClockCardDigital extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: ClockCardConfig;
|
||||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _timeHour?: string;
|
||||
|
||||
@state() private _timeMinute?: string;
|
||||
|
||||
@state() private _timeSecond?: string;
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = this.hass?.locale;
|
||||
|
||||
if (this.config?.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone:
|
||||
this.config?.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
|
||||
this._timeHour = parts.find((part) => part.type === "hour")?.value;
|
||||
this._timeMinute = parts.find((part) => part.type === "minute")?.value;
|
||||
this._timeSecond = this.config?.show_seconds
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="time-parts ${sizeClass}">
|
||||
<div class="time-part hour">${this._timeHour}</div>
|
||||
<div class="time-part minute">${this._timeMinute}</div>
|
||||
${this._timeSecond !== undefined
|
||||
? html`<div class="time-part second">${this._timeSecond}</div>`
|
||||
: nothing}
|
||||
${this._timeAmPm !== undefined
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.time-parts {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"hour minute second"
|
||||
"hour minute am-pm";
|
||||
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: 0.8;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.time-title + .time-parts {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.time-parts.size-medium {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.time-parts.size-large {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.time-parts.size-medium .time-part.second,
|
||||
.time-parts.size-medium .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-l);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.time-parts.size-large .time-part.second,
|
||||
.time-parts.size-large .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour {
|
||||
grid-area: hour;
|
||||
}
|
||||
|
||||
.time-parts .time-part.minute {
|
||||
grid-area: minute;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second {
|
||||
grid-area: second;
|
||||
line-height: 0.9;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.time-parts .time-part.am-pm {
|
||||
grid-area: am-pm;
|
||||
line-height: 0.9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second,
|
||||
.time-parts .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour:after {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-clock-card-digital": HuiClockCardDigital;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -11,10 +10,8 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { ClockCardConfig } from "./types";
|
||||
import { useAmPm } from "../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../common/datetime/resolve-time-zone";
|
||||
|
||||
const INTERVAL = 1000;
|
||||
export const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card")
|
||||
export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
@@ -33,45 +30,14 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _config?: ClockCardConfig;
|
||||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _timeHour?: string;
|
||||
|
||||
@state() private _timeMinute?: string;
|
||||
|
||||
@state() private _timeSecond?: string;
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
public setConfig(config: ClockCardConfig): void {
|
||||
this._config = config;
|
||||
this._initDate();
|
||||
}
|
||||
|
||||
private _initDate() {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
// Dynamically import the clock type based on the configuration
|
||||
if (config.clock_style === "analog") {
|
||||
import("./clock/hui-clock-card-analog");
|
||||
} else {
|
||||
import("./clock/hui-clock-card-digital");
|
||||
}
|
||||
|
||||
let locale = this.hass?.locale;
|
||||
|
||||
if (this._config?.time_format) {
|
||||
locale = { ...locale, time_format: this._config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone:
|
||||
this._config?.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -80,77 +46,59 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
if (this._config?.clock_size === "medium") {
|
||||
return {
|
||||
min_rows: this._config?.title ? 2 : 1,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 4,
|
||||
columns: 6,
|
||||
};
|
||||
switch (this._config?.clock_style) {
|
||||
case "analog":
|
||||
switch (this._config?.clock_size) {
|
||||
case "medium":
|
||||
return {
|
||||
min_rows: this._config?.title ? 4 : 3,
|
||||
rows: 3,
|
||||
min_columns: 5,
|
||||
columns: 6,
|
||||
};
|
||||
case "large":
|
||||
return {
|
||||
min_rows: this._config?.title ? 5 : 4,
|
||||
rows: 4,
|
||||
min_columns: 6,
|
||||
columns: 6,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
min_rows: this._config?.title ? 3 : 2,
|
||||
rows: 2,
|
||||
min_columns: 2,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
default:
|
||||
switch (this._config?.clock_size) {
|
||||
case "medium":
|
||||
return {
|
||||
min_rows: this._config?.title ? 2 : 1,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 4,
|
||||
columns: 6,
|
||||
};
|
||||
case "large":
|
||||
return {
|
||||
min_rows: 2,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 6,
|
||||
columns: 6,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
min_rows: 1,
|
||||
rows: 1,
|
||||
max_rows: 4,
|
||||
min_columns: 3,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this._config?.clock_size === "large") {
|
||||
return {
|
||||
min_rows: 2,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 6,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
min_rows: 1,
|
||||
rows: 1,
|
||||
max_rows: 4,
|
||||
min_columns: 3,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
|
||||
this._timeHour = parts.find((part) => part.type === "hour")?.value;
|
||||
this._timeMinute = parts.find((part) => part.type === "minute")?.value;
|
||||
this._timeSecond = this._config?.show_seconds
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -170,16 +118,19 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
${this._config.title !== undefined
|
||||
? html`<div class="time-title">${this._config.title}</div>`
|
||||
: nothing}
|
||||
<div class="time-parts">
|
||||
<div class="time-part hour">${this._timeHour}</div>
|
||||
<div class="time-part minute">${this._timeMinute}</div>
|
||||
${this._timeSecond !== undefined
|
||||
? html`<div class="time-part second">${this._timeSecond}</div>`
|
||||
: nothing}
|
||||
${this._timeAmPm !== undefined
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config.clock_style === "analog"
|
||||
? html`
|
||||
<hui-clock-card-analog
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
></hui-clock-card-analog>
|
||||
`
|
||||
: html`
|
||||
<hui-clock-card-digital
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
></hui-clock-card-digital>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -234,74 +185,6 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.time-parts {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"hour minute second"
|
||||
"hour minute am-pm";
|
||||
|
||||
font-size: 2rem;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: 0.8;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.time-title + .time-parts {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.time-wrapper.size-medium .time-parts {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.time-wrapper.size-large .time-parts {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.time-wrapper.size-medium .time-parts .time-part.second,
|
||||
.time-wrapper.size-medium .time-parts .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-l);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.time-wrapper.size-large .time-parts .time-part.second,
|
||||
.time-wrapper.size-large .time-parts .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour {
|
||||
grid-area: hour;
|
||||
}
|
||||
|
||||
.time-parts .time-part.minute {
|
||||
grid-area: minute;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second {
|
||||
grid-area: second;
|
||||
line-height: 0.9;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.time-parts .time-part.am-pm {
|
||||
grid-area: am-pm;
|
||||
line-height: 0.9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second,
|
||||
.time-parts .time-part.am-pm {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour:after {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -371,11 +371,15 @@ export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
type: "clock";
|
||||
title?: string;
|
||||
clock_style?: "digital" | "analog";
|
||||
clock_size?: "small" | "medium" | "large";
|
||||
show_seconds?: boolean | undefined;
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
}
|
||||
|
||||
export interface MediaControlCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
defaulted,
|
||||
enums,
|
||||
literal,
|
||||
object,
|
||||
@@ -30,6 +31,7 @@ const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
title: optional(string()),
|
||||
clock_style: optional(union([literal("digital"), literal("analog")])),
|
||||
clock_size: optional(
|
||||
union([literal("small"), literal("medium"), literal("large")])
|
||||
),
|
||||
@@ -37,6 +39,19 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("quarter"),
|
||||
literal("hour"),
|
||||
literal("minute"),
|
||||
]),
|
||||
literal("hour")
|
||||
)
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -50,9 +65,23 @@ export class HuiClockCardEditor
|
||||
@state() private _config?: ClockCardConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, clockStyle: "digital" | "analog") =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.clock_styles.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clock_size",
|
||||
selector: {
|
||||
@@ -69,20 +98,66 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["auto", ...Object.values(TimeFormat)].map(
|
||||
(value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
|
||||
),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: clockStyle === "analog"
|
||||
? ([
|
||||
{
|
||||
name: "border",
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
),
|
||||
},
|
||||
default: false,
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ticks",
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.description`
|
||||
),
|
||||
},
|
||||
default: "hour",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["none", "quarter", "hour", "minute"].map(
|
||||
(value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
|
||||
),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "time_zone",
|
||||
selector: {
|
||||
@@ -107,10 +182,15 @@ export class HuiClockCardEditor
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_zone: "auto",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
...config,
|
||||
}));
|
||||
|
||||
@@ -128,8 +208,13 @@ export class HuiClockCardEditor
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
(this._data(this._config).clock_style as "digital" | "analog") ??
|
||||
"digital"
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
@@ -143,6 +228,14 @@ export class HuiClockCardEditor
|
||||
delete ev.detail.value.time_format;
|
||||
}
|
||||
|
||||
if (ev.detail.value.clock_style === "analog") {
|
||||
ev.detail.value.border = ev.detail.value.border ?? false;
|
||||
ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
|
||||
} else {
|
||||
delete ev.detail.value.border;
|
||||
delete ev.detail.value.ticks;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
@@ -154,6 +247,10 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.title"
|
||||
);
|
||||
case "clock_style":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.clock_style`
|
||||
);
|
||||
case "clock_size":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.clock_size`
|
||||
@@ -174,6 +271,31 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.label`
|
||||
);
|
||||
case "ticks":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.label`
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
);
|
||||
case "ticks":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.description`
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -7704,6 +7704,11 @@
|
||||
"clock": {
|
||||
"name": "Clock",
|
||||
"description": "The Clock card displays the current time using your desired size and format.",
|
||||
"clock_style": "Clock style",
|
||||
"clock_styles": {
|
||||
"digital": "Digital",
|
||||
"analog": "Analog"
|
||||
},
|
||||
"clock_size": "Clock size",
|
||||
"clock_sizes": {
|
||||
"small": "Small",
|
||||
@@ -7723,7 +7728,31 @@
|
||||
"time_zones": {
|
||||
"auto": "Use user settings"
|
||||
},
|
||||
"no_background": "No background"
|
||||
"no_background": "No background",
|
||||
"border": {
|
||||
"label": "Border",
|
||||
"description": "Whether to show a border around the clock"
|
||||
},
|
||||
"ticks": {
|
||||
"label": "Ticks",
|
||||
"description": "Whether to show ticks (indices) on the outside of the clock",
|
||||
"none": {
|
||||
"label": "None",
|
||||
"description": "No ticks (Hands only)"
|
||||
},
|
||||
"quarter": {
|
||||
"label": "Quarter",
|
||||
"description": "4 ticks (Every 15 minutes)"
|
||||
},
|
||||
"hour": {
|
||||
"label": "Hour",
|
||||
"description": "12 ticks (Every hour)"
|
||||
},
|
||||
"minute": {
|
||||
"label": "Minute",
|
||||
"description": "60 ticks (Every minute)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"media-control": {
|
||||
"name": "Media control",
|
||||
|
||||
Reference in New Issue
Block a user