Merge branch 'dev' into allthebackupchanges

This commit is contained in:
Paul Bottein 2024-11-18 11:38:32 +01:00
commit 0c0b657c79
No known key found for this signature in database
21 changed files with 680 additions and 212 deletions

View File

@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.0.9
uses: softprops/action-gh-release@v2.1.0
with:
files: |
dist/*.whl

View File

@ -127,6 +127,7 @@ gulp.task("fetch-lokalise", async function () {
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
})
.then((download) => fetch(download.bundle_url))
.then((response) => {

View File

@ -27,7 +27,7 @@
"dependencies": {
"@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.2",
"@codemirror/autocomplete": "6.18.3",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.2",
@ -121,7 +121,7 @@
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0",
"luxon": "3.5.0",
"marked": "14.1.4",
"marked": "15.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -153,7 +153,7 @@
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/helper-define-polyfill-provider": "0.6.3",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.0",
@ -166,7 +166,7 @@
"@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-receiver": "6.0.18",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",

View File

@ -1,5 +1,6 @@
import type { HaDurationData } from "../../components/ha-duration-input";
import type { FrontendLocaleData } from "../../data/translation";
import { formatListWithAnds } from "../string/format-list";
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
@ -42,3 +43,62 @@ export const formatDuration = (
}
return null;
};
export const formatDurationLong = (
locale: FrontendLocaleData,
duration: HaDurationData
) => {
const d = duration.days || 0;
const h = duration.hours || 0;
const m = duration.minutes || 0;
const s = duration.seconds || 0;
const ms = duration.milliseconds || 0;
const parts: string[] = [];
if (d > 0) {
parts.push(
Intl.NumberFormat(locale.language, {
style: "unit",
unit: "day",
unitDisplay: "long",
}).format(d)
);
}
if (h > 0) {
parts.push(
Intl.NumberFormat(locale.language, {
style: "unit",
unit: "hour",
unitDisplay: "long",
}).format(h)
);
}
if (m > 0) {
parts.push(
Intl.NumberFormat(locale.language, {
style: "unit",
unit: "minute",
unitDisplay: "long",
}).format(m)
);
}
if (s > 0) {
parts.push(
Intl.NumberFormat(locale.language, {
style: "unit",
unit: "second",
unitDisplay: "long",
}).format(s)
);
}
if (ms > 0) {
parts.push(
Intl.NumberFormat(locale.language, {
style: "unit",
unit: "millisecond",
unitDisplay: "long",
}).format(ms)
);
}
return formatListWithAnds(locale, parts);
};

View File

@ -113,7 +113,6 @@ class HaDataTableLabels extends LitElement {
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
outline: 1px solid var(--outline-color);
}
ha-button-menu {
border-radius: 10px;

View File

@ -2,9 +2,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
@ -20,7 +18,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public rows = 8;
@property({ attribute: false }) public columns = 4;
@property({ attribute: false }) public columns = 12;
@property({ attribute: false }) public rowMin?: number;
@ -32,6 +30,8 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean;
@property({ attribute: false }) public step: number = 1;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) {
@ -51,8 +51,9 @@ export class HaGridSizeEditor extends LitElement {
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
const columnMin = this.columnMin ?? 1;
const columnMax = this.columnMax ?? this.columns;
const columnMin = Math.ceil((this.columnMin ?? 1) / this.step) * this.step;
const columnMax =
Math.ceil((this.columnMax ?? this.columns) / this.step) * this.step;
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
const columnValue = this._localValue?.columns;
@ -67,9 +68,11 @@ export class HaGridSizeEditor extends LitElement {
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : this.value?.columns}
.step=${this.step}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
tooltip-mode="always"
></ha-grid-layout-slider>
<ha-grid-layout-slider
@ -85,6 +88,7 @@ export class HaGridSizeEditor extends LitElement {
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
tooltip-mode="always"
></ha-grid-layout-slider>
${!this.isDefault
? html`
@ -102,34 +106,44 @@ export class HaGridSizeEditor extends LitElement {
</ha-icon-button>
`
: nothing}
<div
class="preview ${classMap({ "full-width": fullWidth })}"
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": rowValue,
"--columns": fullWidth ? this.columns : columnValue,
})}
>
<div>
${Array(this.rows * this.columns)
<div class="preview">
<table>
${Array(this.rows)
.fill(0)
.map((_, index) => {
const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1;
const row = index + 1;
return html`
<div
class="cell"
data-row=${row}
data-column=${column}
@click=${this._cellClick}
></div>
<tr>
${Array(this.columns)
.fill(0)
.map((__, columnIndex) => {
const column = columnIndex + 1;
if (
column % this.step !== 0 ||
(this.columns > 24 && column % 3 !== 0)
) {
return nothing;
}
return html`
<td
data-row=${row}
data-column=${column}
@click=${this._cellClick}
></td>
`;
})}
</tr>
`;
})}
</div>
<div class="selected">
<div class="cell"></div>
</div>
</table>
<div
class="preview-card"
style=${styleMap({
"--rows": rowValue,
"--columns": fullWidth ? this.columns : columnValue,
"--total-columns": this.columns,
})}
></div>
</div>
</div>
`;
@ -223,42 +237,40 @@ export class HaGridSizeEditor extends LitElement {
}
.reset {
grid-area: reset;
--mdc-icon-button-size: 36px;
}
.preview {
position: relative;
grid-area: preview;
}
.preview > div {
position: relative;
display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 25px);
gap: 4px;
.preview table,
.preview tr,
.preview td {
border: 2px dotted var(--divider-color);
border-collapse: collapse;
}
.preview .cell {
background-color: var(--disabled-color);
grid-column: span 1;
grid-row: span 1;
border-radius: 4px;
opacity: 0.2;
cursor: pointer;
}
.preview .selected {
position: absolute;
pointer-events: none;
top: 0;
left: 0;
height: 100%;
.preview table {
width: 100%;
}
.selected .cell {
background-color: var(--primary-color);
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
opacity: 0.5;
.preview tr {
height: 30px;
}
.preview.full-width .selected .cell {
grid-column: 1 / -1;
.preview td {
cursor: pointer;
}
.preview-card {
position: absolute;
top: 0;
left: 0;
background-color: var(--primary-color);
opacity: 0.3;
border-radius: 8px;
height: calc(var(--rows, 1) * 30px);
width: calc(var(--columns, 1) * 100% / var(--total-columns, 12));
pointer-events: none;
transition:
width ease-in-out 180ms,
height ease-in-out 180ms;
}
`,
];

View File

@ -26,6 +26,7 @@ class HaLabel extends LitElement {
0.15
);
--ha-label-background-opacity: 1;
border: 1px solid var(--outline-color);
position: relative;
box-sizing: border-box;
display: inline-flex;

View File

@ -1,6 +1,9 @@
import type { HassConfig } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { formatDuration } from "../common/datetime/format_duration";
import {
formatDuration,
formatDurationLong,
} from "../common/datetime/format_duration";
import {
formatTime,
formatTimeWithSeconds,
@ -720,6 +723,38 @@ const tryDescribeTrigger = (
}`;
}
// Calendar Trigger
if (trigger.trigger === "calendar") {
const calendarEntity = hass.states[trigger.entity_id]
? computeStateName(hass.states[trigger.entity_id])
: trigger.entity_id;
let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
let offset: string | string[] = trigger.offset.startsWith("-")
? trigger.offset.substring(1).split(":")
: trigger.offset.split(":");
const duration = {
hours: offset.length > 0 ? +offset[0] : 0,
minutes: offset.length > 1 ? +offset[1] : 0,
seconds: offset.length > 2 ? +offset[2] : 0,
};
offset = formatDurationLong(hass.locale, duration);
if (offset === "") {
offsetChoice = "other";
}
return hass.localize(
`${triggerTranslationBaseKey}.calendar.description.full`,
{
eventChoice: trigger.event,
offsetChoice: offsetChoice,
offset: offset,
hasCalendar: trigger.entity_id ? "true" : "false",
calendar: calendarEntity,
}
);
}
return (
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`

View File

@ -1,5 +1,6 @@
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import memoizeOne from "memoize-one";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog";
@ -13,19 +14,6 @@ import type {
} from "./show-dialog-schedule-block-info";
import type { SchemaUnion } from "../../../../components/ha-form/types";
const SCHEMA = [
{
name: "from",
required: true,
selector: { time: { no_second: true } },
},
{
name: "to",
required: true,
selector: { time: { no_second: true } },
},
];
class DialogScheduleBlockInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -35,10 +23,39 @@ class DialogScheduleBlockInfo extends LitElement {
@state() private _params?: ScheduleBlockInfoDialogParams;
private _expand = false;
private _schema = memoizeOne((expand: boolean) => [
{
name: "from",
required: true,
selector: { time: { no_second: true } },
},
{
name: "to",
required: true,
selector: { time: { no_second: true } },
},
{
name: "advanced_settings",
type: "expandable" as const,
flatten: true,
expanded: expand,
schema: [
{
name: "data",
required: false,
selector: { object: {} },
},
],
},
]);
public showDialog(params: ScheduleBlockInfoDialogParams): void {
this._params = params;
this._error = undefined;
this._data = params.block;
this._expand = !!params.block?.data;
}
public closeDialog(): void {
@ -66,7 +83,7 @@ class DialogScheduleBlockInfo extends LitElement {
<div>
<ha-form
.hass=${this.hass}
.schema=${SCHEMA}
.schema=${this._schema(this._expand)}
.data=${this._data}
.error=${this._error}
.computeLabel=${this._computeLabelCallback}
@ -110,12 +127,20 @@ class DialogScheduleBlockInfo extends LitElement {
}
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "from":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.start");
case "to":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
case "data":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.data");
case "advanced_settings":
return this.hass!.localize(
"ui.dialogs.helper_settings.schedule.advanced_settings"
);
}
return "";
};

View File

@ -3,6 +3,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
export interface ScheduleBlockInfo {
from: string;
to: string;
data?: Record<string, any>;
}
export interface ScheduleBlockInfoDialogParams {

View File

@ -8,6 +8,18 @@ import "../../../../../../components/ha-circular-progress";
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
import "./zwave_js-capability-control-multilevel-switch";
enum ColorComponent {
"Warm White" = 0,
"Cold White",
Red,
Green,
Blue,
Amber,
Cyan,
Purple,
Index,
}
@customElement("zwave_js-capability-control-color_switch")
class ZWaveJSCapabilityColorSwitch extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -20,7 +32,7 @@ class ZWaveJSCapabilityColorSwitch extends LitElement {
@property({ type: Number }) public version!: number;
@state() private _color_components?: number[];
@state() private _color_components?: ColorComponent[];
@state() private _error?: string;
@ -37,7 +49,9 @@ class ZWaveJSCapabilityColorSwitch extends LitElement {
${this.hass.localize(
"ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.color_component"
)}:
${color}
${this.hass.localize(
`ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.colors.${color}`
)}
</h5>
<zwave_js-capability-control-multilevel_switch
.hass=${this.hass}

View File

@ -22,12 +22,14 @@ import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import { computeDeviceName } from "../../../../../data/device_registry";
import type {
ZWaveJSNodeCapabilities,
ZWaveJSNodeConfigParam,
ZWaveJSNodeConfigParams,
ZWaveJSSetConfigParamResult,
ZwaveJSNodeMetadata,
} from "../../../../../data/zwave_js";
import {
fetchZwaveNodeCapabilities,
fetchZwaveNodeConfigParameters,
fetchZwaveNodeMetadata,
invokeZWaveCCApi,
@ -68,6 +70,8 @@ class ZWaveJSNodeConfig extends LitElement {
@state() private _config?: ZWaveJSNodeConfigParams;
@state() private _canResetAll = false;
@state() private _results: Record<string, ZWaveJSSetConfigParamResult> = {};
@state() private _error?: string;
@ -183,17 +187,19 @@ class ZWaveJSNodeConfig extends LitElement {
</ha-card>
</div>`
)}
<div class="reset">
<ha-progress-button
.disabled=${this._resetDialogProgress}
.progress=${this._resetDialogProgress}
@click=${this._openResetDialog}
>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.reset_to_default.button_label"
)}
</ha-progress-button>
</div>
${this._canResetAll
? html`<div class="reset">
<ha-progress-button
.disabled=${this._resetDialogProgress}
.progress=${this._resetDialogProgress}
@click=${this._openResetDialog}
>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.reset_to_default.button_label"
)}
</ha-progress-button>
</div>`
: nothing}
<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.custom_config"
@ -468,10 +474,19 @@ class ZWaveJSNodeConfig extends LitElement {
return;
}
[this._nodeMetadata, this._config] = await Promise.all([
let capabilities: ZWaveJSNodeCapabilities | undefined;
[this._nodeMetadata, this._config, capabilities] = await Promise.all([
fetchZwaveNodeMetadata(this.hass, device.id),
fetchZwaveNodeConfigParameters(this.hass, device.id),
fetchZwaveNodeCapabilities(this.hass, device.id),
]);
this._canResetAll =
capabilities &&
Object.values(capabilities).some((endpoint) =>
endpoint.some(
(capability) => capability.id === 0x70 && capability.version >= 4
)
);
}
private async _openResetDialog(event: Event) {

View File

@ -106,7 +106,8 @@ export class HaConfigLabels extends LitElement {
style="
background-color: ${computeCssColor(label.color)};
border-radius: 10px;
outline: 1px solid var(--outline-color);
border: 1px solid var(--outline-color);
box-sizing: border-box;
width: 20px;
height: 20px;"
></div>`

View File

@ -79,6 +79,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@ -602,12 +604,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
}
private async confirmUnsavedChanged(): Promise<boolean> {
@ -723,7 +727,21 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
history.back();
}
private _switchUiMode() {
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
@ -763,6 +781,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
private async _saveScript(): Promise<void> {
if (this._yamlErrors) {
showToast(this, {
message: this._yamlErrors,
});
return;
}
if (!this.scriptId) {
const saved = await this._promptScriptAlias();
if (!saved) {

View File

@ -3,16 +3,11 @@ import type { LovelaceGridOptions, LovelaceLayoutOptions } from "../types";
export const GRID_COLUMN_MULTIPLIER = 3;
export const multiplyBy = <T extends number | string | undefined>(
const multiplyBy = <T extends number | string | undefined>(
value: T,
multiplier: number
): T => (typeof value === "number" ? ((value * multiplier) as T) : value);
export const divideBy = <T extends number | string | undefined>(
value: T,
divider: number
): T => (typeof value === "number" ? (Math.ceil(value / divider) as T) : value);
export const migrateLayoutToGridOptions = (
options: LovelaceLayoutOptions
): LovelaceGridOptions => {
@ -42,6 +37,9 @@ export type CardGridSize = {
columns: number | "full";
};
export const isPreciseMode = (options: LovelaceGridOptions) =>
typeof options.columns === "number" && options.columns % 3 !== 0;
export const computeCardGridSize = (
options: LovelaceGridOptions
): CardGridSize => {

View File

@ -149,7 +149,10 @@ class ActionHandler extends HTMLElement implements ActionHandlerType {
element.actionHandler.end = (ev: Event) => {
// Don't respond when moved or scrolled while touch
if (["touchend", "touchcancel"].includes(ev.type) && this.cancelled) {
if (
ev.type === "touchcancel" ||
(ev.type === "touchend" && this.cancelled)
) {
return;
}
const target = ev.target as HTMLElement;

View File

@ -23,6 +23,8 @@ const A11Y_KEY_CODES = new Set([
"End",
]);
type TooltipMode = "never" | "always" | "interaction";
@customElement("ha-grid-layout-slider")
export class HaGridLayoutSlider extends LitElement {
@property({ type: Boolean, reflect: true })
@ -34,6 +36,9 @@ export class HaGridLayoutSlider extends LitElement {
@property({ attribute: "touch-action" })
public touchAction?: string;
@property({ attribute: "tooltip-mode" })
public tooltipMode: TooltipMode = "interaction";
@property({ type: Number })
public value?: number;
@ -52,6 +57,9 @@ export class HaGridLayoutSlider extends LitElement {
@state()
public pressed = false;
@state()
public tooltipVisible = false;
private _mc?: HammerManager;
private get _range() {
@ -135,11 +143,13 @@ export class HaGridLayoutSlider extends LitElement {
this._mc.on("panstart", () => {
if (this.disabled) return;
this.pressed = true;
this._showTooltip();
savedValue = this.value;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this.pressed = false;
this._hideTooltip();
this.value = savedValue;
});
this._mc.on("panmove", (e) => {
@ -152,6 +162,7 @@ export class HaGridLayoutSlider extends LitElement {
this._mc.on("panend", (e) => {
if (this.disabled) return;
this.pressed = false;
this._hideTooltip();
const percentage = this._getPercentageFromEvent(e);
const value = this._percentageToValue(percentage);
this.value = this._steppedValue(this._boundedValue(value));
@ -223,6 +234,23 @@ export class HaGridLayoutSlider extends LitElement {
fireEvent(this, "value-changed", { value: this.value });
}
private _tooltipTimeout?: number;
_showTooltip() {
if (this._tooltipTimeout != null) window.clearTimeout(this._tooltipTimeout);
this.tooltipVisible = true;
}
_hideTooltip(delay?: number) {
if (!delay) {
this.tooltipVisible = false;
return;
}
this._tooltipTimeout = window.setTimeout(() => {
this.tooltipVisible = false;
}, delay);
}
private _getPercentageFromEvent = (e: HammerInput) => {
if (this.vertical) {
const y = e.center.y;
@ -236,6 +264,30 @@ export class HaGridLayoutSlider extends LitElement {
return Math.max(Math.min(1, (x - offset) / total), 0);
};
private _renderTooltip() {
if (this.tooltipMode === "never") return nothing;
const position = this.vertical ? "left" : "top";
const visible =
this.tooltipMode === "always" ||
(this.tooltipVisible && this.tooltipMode === "interaction");
const value = this._boundedValue(this._steppedValue(this.value ?? 0));
return html`
<span
aria-hidden="true"
class="tooltip ${classMap({
visible,
[position]: true,
})}"
>
${value}
</span>
`;
}
protected render(): TemplateResult {
return html`
<div
@ -257,9 +309,29 @@ export class HaGridLayoutSlider extends LitElement {
})}
></div>
</div>
${Array(this._range / this.step)
.fill(0)
.map((_, i) => {
const percentage = i / (this._range / this.step);
const disabled =
this.min >= i * this.step || i * this.step > this.max;
if (disabled) {
return nothing;
}
return html`
<div
class="dot"
style=${styleMap({
"--value": `${percentage}`,
})}
></div>
`;
})}
${this.value !== undefined
? html`<div class="handle"></div>`
: nothing}
${this._renderTooltip()}
</div>
</div>
`;
@ -269,7 +341,7 @@ export class HaGridLayoutSlider extends LitElement {
return css`
:host {
display: block;
--grid-layout-slider: 48px;
--grid-layout-slider: 36px;
height: var(--grid-layout-slider);
width: 100%;
outline: none;
@ -297,6 +369,7 @@ export class HaGridLayoutSlider extends LitElement {
}
.slider * {
pointer-events: none;
user-select: none;
}
.track {
position: absolute;
@ -315,12 +388,11 @@ export class HaGridLayoutSlider extends LitElement {
position: absolute;
inset: 0;
background: var(--disabled-color);
opacity: 0.2;
opacity: 0.4;
}
.active {
position: absolute;
background: grey;
opacity: 0.7;
background: var(--primary-color);
top: 0;
right: calc(var(--max) * 100%);
bottom: 0;
@ -351,6 +423,27 @@ export class HaGridLayoutSlider extends LitElement {
height: 16px;
width: 100%;
}
.dot {
position: absolute;
top: 0;
bottom: 0;
opacity: 0.6;
margin: auto;
width: 4px;
height: 4px;
flex-shrink: 0;
transform: translate(-50%, 0);
background: var(--card-background-color);
left: calc(var(--value, 0%) * 100%);
border-radius: 2px;
}
:host([vertical]) .dot {
transform: translate(0, -50%);
left: 0;
right: 0;
bottom: inherit;
top: calc(var(--value, 0%) * 100%);
}
.handle::after {
position: absolute;
inset: 0;
@ -358,7 +451,7 @@ export class HaGridLayoutSlider extends LitElement {
border-radius: 2px;
height: 100%;
margin: auto;
background: grey;
background: var(--primary-color);
content: "";
}
:host([vertical]) .handle::after {
@ -374,9 +467,88 @@ export class HaGridLayoutSlider extends LitElement {
:host(:disabled) .active {
background: var(--disabled-color);
}
.tooltip {
position: absolute;
background-color: var(--clear-background-color);
color: var(--primary-text-color);
font-size: var(--control-slider-tooltip-font-size);
border-radius: 0.8em;
padding: 0.2em 0.4em;
opacity: 0;
white-space: nowrap;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition:
opacity 180ms ease-in-out,
left 180ms ease-in-out,
bottom 180ms ease-in-out;
--handle-spacing: calc(2 * var(--handle-margin) + var(--handle-size));
--slider-tooltip-margin: 0px;
--slider-tooltip-range: 100%;
--slider-tooltip-offset: 0px;
--slider-tooltip-position: calc(
min(
max(
var(--value) * var(--slider-tooltip-range) +
var(--slider-tooltip-offset),
0%
),
100%
)
);
}
.tooltip.start {
--slider-tooltip-offset: calc(-0.5 * (var(--handle-spacing)));
}
.tooltip.end {
--slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
}
.tooltip.cursor {
--slider-tooltip-range: calc(100% - var(--handle-spacing));
--slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
}
.tooltip.show-handle {
--slider-tooltip-range: calc(100% - var(--handle-spacing));
--slider-tooltip-offset: calc(0.5 * (var(--handle-spacing)));
}
.tooltip.visible {
opacity: 1;
}
.tooltip.top {
transform: translate3d(-50%, -100%, 0);
top: var(--slider-tooltip-margin);
left: 50%;
}
.tooltip.bottom {
transform: translate3d(-50%, 100%, 0);
bottom: var(--slider-tooltip-margin);
left: 50%;
}
.tooltip.left {
transform: translate3d(-100%, -50%, 0);
top: 50%;
left: var(--slider-tooltip-margin);
}
.tooltip.right {
transform: translate3d(100%, -50%, 0);
top: 50%;
right: var(--slider-tooltip-margin);
}
:host(:not([vertical])) .tooltip.top,
:host(:not([vertical])) .tooltip.bottom {
left: var(--slider-tooltip-position);
}
:host([vertical]) .tooltip.right,
:host([vertical]) .tooltip.left {
top: var(--slider-tooltip-position);
}
.pressed .handle {
transition: none;
}
.pressed .tooltip {
transition: opacity 180ms ease-in-out;
}
`;
}
}

View File

@ -26,16 +26,12 @@ import type { HuiCard } from "../../cards/hui-card";
import type { CardGridSize } from "../../common/compute-card-grid-size";
import {
computeCardGridSize,
divideBy,
GRID_COLUMN_MULTIPLIER,
isPreciseMode,
migrateLayoutToGridOptions,
multiplyBy,
} from "../../common/compute-card-grid-size";
import type { LovelaceGridOptions } from "../../types";
const computePreciseMode = (columns?: number | string) =>
typeof columns === "number" && columns % 3 !== 0;
@customElement("hui-card-layout-editor")
export class HuiCardLayoutEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -63,22 +59,6 @@ export class HuiCardLayoutEditor extends LitElement {
private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _simplifyOptions = (
options: LovelaceGridOptions
): LovelaceGridOptions => ({
...options,
columns: divideBy(options.columns, GRID_COLUMN_MULTIPLIER),
max_columns: divideBy(options.max_columns, GRID_COLUMN_MULTIPLIER),
min_columns: divideBy(options.min_columns, GRID_COLUMN_MULTIPLIER),
});
private _standardizeOptions = (options: LovelaceGridOptions) => ({
...options,
columns: multiplyBy(options.columns, GRID_COLUMN_MULTIPLIER),
max_columns: multiplyBy(options.max_columns, GRID_COLUMN_MULTIPLIER),
min_columns: multiplyBy(options.min_columns, GRID_COLUMN_MULTIPLIER),
});
private _isDefault = memoizeOne(
(options?: LovelaceGridOptions) =>
options?.columns === undefined && options?.rows === undefined
@ -101,14 +81,11 @@ export class HuiCardLayoutEditor extends LitElement {
this._defaultGridOptions
);
const gridOptions = this._preciseMode
? options
: this._simplifyOptions(options);
const gridOptions = options;
const gridValue = this._computeCardGridSize(gridOptions);
const columnSpan = this.sectionConfig.column_span ?? 1;
const gridTotalColumns =
(12 * columnSpan) / (this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER);
const gridTotalColumns = 12 * columnSpan;
return html`
<div class="header">
@ -173,7 +150,7 @@ export class HuiCardLayoutEditor extends LitElement {
: html`
<ha-grid-size-picker
style=${styleMap({
"max-width": `${(this.sectionConfig.column_span ?? 1) * 200 + 50}px`,
"max-width": `${(this.sectionConfig.column_span ?? 1) * 250 + 40}px`,
})}
.columns=${gridTotalColumns}
.hass=${this.hass}
@ -184,6 +161,7 @@ export class HuiCardLayoutEditor extends LitElement {
.rowMax=${gridOptions.max_rows}
.columnMin=${gridOptions.min_columns}
.columnMax=${gridOptions.max_columns}
.step=${this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER}
></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
@ -267,17 +245,21 @@ export class HuiCardLayoutEditor extends LitElement {
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
if (changedProps.has("config")) {
const columns = this.config.grid_options?.columns;
const preciseMode = computePreciseMode(columns);
const options = this.config.grid_options;
// Reset precise mode when grid options config is reset
if (!options) {
this._preciseMode = this._defaultGridOptions
? isPreciseMode(this._defaultGridOptions)
: false;
return;
}
// Force precise mode if columns count is not a multiple of 3
const preciseMode = isPreciseMode(options);
if (!this._preciseMode && preciseMode) {
this._preciseMode = preciseMode;
}
// Reset precise mode when grid options config is reset
if (columns === undefined) {
const defaultColumns = this._defaultGridOptions?.columns;
this._preciseMode = computePreciseMode(defaultColumns);
}
}
}
@ -296,18 +278,10 @@ export class HuiCardLayoutEditor extends LitElement {
ev.stopPropagation();
const value = ev.detail.value as CardGridSize;
const gridOptions = {
columns: value.columns,
rows: value.rows,
};
const newOptions = this._preciseMode
? gridOptions
: this._standardizeOptions(gridOptions);
this._updateGridOptions({
...this.config.grid_options,
...newOptions,
columns: value.columns,
rows: value.rows,
});
}
@ -322,7 +296,7 @@ export class HuiCardLayoutEditor extends LitElement {
const value = ev.target.checked;
this._updateGridOptions({
...this.config.grid_options,
columns: value ? "full" : (this._defaultGridOptions?.min_columns ?? 1),
columns: value ? "full" : undefined,
});
}
@ -331,13 +305,14 @@ export class HuiCardLayoutEditor extends LitElement {
this._preciseMode = ev.target.checked;
if (this._preciseMode) return;
const newOptions = this._standardizeOptions(
this._simplifyOptions(this.config.grid_options ?? {})
);
if (newOptions.columns !== this.config.grid_options?.columns) {
const columns = this.config.grid_options?.columns;
if (typeof columns === "number" && columns % GRID_COLUMN_MULTIPLIER !== 0) {
const newColumns =
Math.ceil(columns / GRID_COLUMN_MULTIPLIER) * GRID_COLUMN_MULTIPLIER;
this._updateGridOptions({
...this.config.grid_options,
columns: newOptions.columns,
columns: newColumns,
});
}
}

View File

@ -7,7 +7,7 @@ import {
mdiViewGridPlus,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
@ -20,6 +20,7 @@ import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
@ -27,8 +28,19 @@ import type { HuiBadge } from "../badges/hui-badge";
import "../badges/hui-view-badges";
import type { HuiCard } from "../cards/hui-card";
import "../components/hui-badge-edit-mode";
import { addSection, deleteSection, moveSection } from "../editor/config-util";
import { findLovelaceContainer } from "../editor/lovelace-path";
import {
addSection,
deleteSection,
moveCard,
moveSection,
} from "../editor/config-util";
import type { LovelaceCardPath } from "../editor/lovelace-path";
import {
findLovelaceContainer,
findLovelaceItems,
getLovelaceContainerPath,
parseLovelaceCardPath,
} from "../editor/lovelace-path";
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
import type { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
@ -231,19 +243,35 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
)}
${editMode
? html`
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
filter="button"
.rollback=${false}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
)}
</p>
</div>
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
</div>
</ha-sortable>
`
: nothing}
${editMode && this._config?.cards?.length
@ -280,6 +308,52 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
`;
}
private _defaultSection(includeHeading: boolean): LovelaceSectionConfig {
return {
type: "grid",
cards: includeHeading
? [
{
type: "heading",
heading: this.hass!.localize(
"ui.panel.lovelace.editor.section.default_section_title"
),
},
]
: [],
};
}
private _handleCardAdded(ev) {
const { data } = ev.detail;
const oldPath = data as LovelaceCardPath;
const { cardIndex } = parseLovelaceCardPath(oldPath);
const containerPath = getLovelaceContainerPath(oldPath);
const cards = findLovelaceItems(
"cards",
this.lovelace!.config,
containerPath
);
const cardConfig = cards![cardIndex];
const configWithNewSection = addSection(
this.lovelace!.config,
this.index!,
this._defaultSection(cardConfig.type !== "heading") // If we move a heading card, we don't want to include a heading in the new section
);
const viewConfig = configWithNewSection.views[
this.index!
] as LovelaceViewConfig;
const newPath = [
this.index!,
viewConfig.sections!.length - 1,
1,
] as LovelaceCardPath;
const newConfig = moveCard(configWithNewSection, oldPath, newPath);
this.lovelace!.saveConfig(newConfig);
}
private _importedCardSectionConfig = memoizeOne(
(cards: LovelaceCardConfig[]) => ({
type: "grid",
@ -288,17 +362,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
);
private _createSection(): void {
const newConfig = addSection(this.lovelace!.config, this.index!, {
type: "grid",
cards: [
{
type: "heading",
heading: this.hass!.localize(
"ui.panel.lovelace.editor.section.default_section_title"
),
},
],
});
const newConfig = addSection(
this.lovelace!.config,
this.index!,
this._defaultSection(true)
);
this.lovelace!.saveConfig(newConfig);
}
@ -415,8 +483,55 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
padding: 8px;
}
.create-section {
.create-section-container {
position: relative;
display: flex;
flex-direction: column;
margin-top: 36px;
}
.create-section-container .card {
display: none;
}
.create-section-container:has(.card) .drop-helper {
display: flex;
}
.create-section-container:has(.card) .create-section {
display: none;
}
.drop-helper {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
outline: none;
background: none;
cursor: pointer;
border-radius: var(--ha-card-border-radius, 12px);
border: 2px dashed var(--primary-color);
height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px));
padding: 8px;
box-sizing: border-box;
width: 100%;
--ha-ripple-color: var(--primary-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
}
.drop-helper p {
color: var(--primary-text-color);
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: center;
}
.create-section {
display: block;
position: relative;
outline: none;
background: none;
cursor: pointer;
@ -426,6 +541,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px));
padding: 8px;
box-sizing: border-box;
width: 100%;
--ha-ripple-color: var(--primary-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;

View File

@ -1612,7 +1612,9 @@
"confirm_delete": "Do you want to delete this item?",
"edit_schedule_block": "Edit schedule block",
"start": "Start",
"end": "End"
"end": "End",
"data": "Additional data",
"advanced_settings": "Advanced settings"
},
"template": {
"time": "[%key:ui::panel::developer-tools::tabs::templates::time%]",
@ -2995,7 +2997,8 @@
"before": "Before",
"after": "After",
"description": {
"picker": "When a calendar event starts or ends."
"picker": "When a calendar event starts or ends.",
"full": "When{offsetChoice, select, \n before { it's {offset} before}\n after { it's {offset} after}\n other {}\n} a calendar event{eventChoice, select, \n start { starts}\n end { ends}\n other { starts or ends}\n}{hasCalendar, select, \n true { in {calendar}}\n other {}\n}"
}
},
"device": {
@ -3242,7 +3245,7 @@
"description": {
"picker": "Test if multiple conditions are true.",
"no_conditions": "If multiple conditions match",
"full": "If {count} {count, plural,\n one {condition match}\n other {conditions matches}\n}"
"full": "If {count} {count, plural,\n one {condition matches}\n other {conditions match}\n}"
}
},
"device": {
@ -3290,7 +3293,7 @@
"state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"description": {
"picker": "If an entity (or attribute) is in a specific state.",
"no_entity": "Confirm state",
"no_entity": "If state confirmed",
"full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n =0 {an entity is}\n one {{entities} is}\n other {{entities} are}\n} {numberOfStates, plural,\n =0 {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }"
}
},
@ -3353,7 +3356,7 @@
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]",
"description": {
"picker": "If someone (or something) is in a zone.",
"full": "Confirm {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n} "
"full": "If {entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} in {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
}
}
}
@ -5265,7 +5268,18 @@
"control_failed": "Failed to control transition. {error}"
},
"color_switch": {
"color_component": "Color component"
"color_component": "Color component",
"colors": {
"0": "Warm White",
"1": "Cold White",
"2": "Red",
"3": "Green",
"4": "Blue",
"5": "Amber",
"6": "Cyan",
"7": "Purple",
"8": "Index"
}
}
}
}
@ -5881,6 +5895,7 @@
"section": {
"add_badge": "Add badge",
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
"drop_card_create_section": "Drop card here to create a new section",
"create_section": "Create section",
"default_section_title": "New section",
"imported_cards_title": "Imported cards",

View File

@ -144,9 +144,9 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-define-polyfill-provider@npm:0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.2":
version: 0.6.2
resolution: "@babel/helper-define-polyfill-provider@npm:0.6.2"
"@babel/helper-define-polyfill-provider@npm:0.6.3, @babel/helper-define-polyfill-provider@npm:^0.6.2":
version: 0.6.3
resolution: "@babel/helper-define-polyfill-provider@npm:0.6.3"
dependencies:
"@babel/helper-compilation-targets": "npm:^7.22.6"
"@babel/helper-plugin-utils": "npm:^7.22.5"
@ -155,7 +155,7 @@ __metadata:
resolve: "npm:^1.14.2"
peerDependencies:
"@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
checksum: 10/bb32ec12024d3f16e70641bc125d2534a97edbfdabbc9f69001ec9c4ce46f877c7a224c566aa6c8c510c3b0def2e43dc4433bf6a40896ba5ce0cef4ea5ccbcff
checksum: 10/b79a77ac8fbf1aaf6c7f99191871760508e87d75a374ff3c39c6599a17d9bb82284797cd451769305764e504546caf22ae63367b22d6e45e32d0a8f4a34aab53
languageName: node
linkType: hard
@ -1265,9 +1265,9 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/autocomplete@npm:6.18.2":
version: 6.18.2
resolution: "@codemirror/autocomplete@npm:6.18.2"
"@codemirror/autocomplete@npm:6.18.3":
version: 6.18.3
resolution: "@codemirror/autocomplete@npm:6.18.3"
dependencies:
"@codemirror/language": "npm:^6.0.0"
"@codemirror/state": "npm:^6.0.0"
@ -1278,7 +1278,7 @@ __metadata:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
"@lezer/common": ^1.0.0
checksum: 10/35bd17afb53e8c99b1342964616f0bcc13f5f06a5d4e2d9936afdaea61742b1c20b3856d513c5d5676e3a9b6fd95e997c842467d21dfa106845e65ab1720b2f4
checksum: 10/a9a684cfc4a5d5d7293993a2480c9f3a5c270b2b4a2f192d9179b4458bb30f9f299912915a1d919320c1281c8e15507cfbc76e792f42f304e5f333e4019f5723
languageName: node
linkType: hard
@ -3792,10 +3792,10 @@ __metadata:
languageName: node
linkType: hard
"@types/chromecast-caf-receiver@npm:6.0.17":
version: 6.0.17
resolution: "@types/chromecast-caf-receiver@npm:6.0.17"
checksum: 10/a8cd00861da900dd56795fc4a7d483c25dfccda9732bea0aaf314e28be2a254cd8c892ebddc44c9ce62087570a9d225767fa3904767993af3e5c79adb80ea966
"@types/chromecast-caf-receiver@npm:6.0.18":
version: 6.0.18
resolution: "@types/chromecast-caf-receiver@npm:6.0.18"
checksum: 10/abf21a7d1959a3b78be55ed927fe0764ce70a5c574f3fcb364008eb248f0c19f6c5df714c47640075d51dfe2467f0e281bedf562721e4838a20c50ca638fd306
languageName: node
linkType: hard
@ -6506,13 +6506,13 @@ __metadata:
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
version: 7.0.5
resolution: "cross-spawn@npm:7.0.5"
dependencies:
path-key: "npm:^3.1.0"
shebang-command: "npm:^2.0.0"
which: "npm:^2.0.1"
checksum: 10/e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce
checksum: 10/c95062469d4bdbc1f099454d01c0e77177a3733012d41bf907a71eb8d22d2add43b5adf6a0a14ef4e7feaf804082714d6c262ef4557a1c480b86786c120d18e2
languageName: node
linkType: hard
@ -8636,7 +8636,7 @@ __metadata:
resolution: "home-assistant-frontend@workspace:."
dependencies:
"@babel/core": "npm:7.26.0"
"@babel/helper-define-polyfill-provider": "npm:0.6.2"
"@babel/helper-define-polyfill-provider": "npm:0.6.3"
"@babel/plugin-proposal-decorators": "npm:7.25.9"
"@babel/plugin-transform-runtime": "npm:7.25.9"
"@babel/preset-env": "npm:7.26.0"
@ -8644,7 +8644,7 @@ __metadata:
"@babel/runtime": "npm:7.26.0"
"@braintree/sanitize-url": "npm:7.1.0"
"@bundle-stats/plugin-webpack-filter": "npm:4.16.0"
"@codemirror/autocomplete": "npm:6.18.2"
"@codemirror/autocomplete": "npm:6.18.3"
"@codemirror/commands": "npm:6.7.1"
"@codemirror/language": "npm:6.10.3"
"@codemirror/legacy-modes": "npm:6.4.2"
@ -8713,7 +8713,7 @@ __metadata:
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@thomasloven/round-slider": "npm:0.6.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.17"
"@types/chromecast-caf-receiver": "npm:6.0.18"
"@types/chromecast-caf-sender": "npm:1.0.10"
"@types/color-name": "npm:2.0.0"
"@types/glob": "npm:8.1.0"
@ -8798,7 +8798,7 @@ __metadata:
luxon: "npm:3.5.0"
magic-string: "npm:0.30.12"
map-stream: "npm:0.0.7"
marked: "npm:14.1.4"
marked: "npm:15.0.0"
memoize-one: "npm:6.0.0"
mocha: "npm:10.8.2"
node-vibrant: "npm:3.2.1-alpha.1"
@ -10522,12 +10522,12 @@ __metadata:
languageName: node
linkType: hard
"marked@npm:14.1.4":
version: 14.1.4
resolution: "marked@npm:14.1.4"
"marked@npm:15.0.0":
version: 15.0.0
resolution: "marked@npm:15.0.0"
bin:
marked: bin/marked.js
checksum: 10/e3526e7907aa1c13481d205b667a178bd372c01318439e4cd8a3d4b55e3983bccef8c17489129c6a0e31dbecb0b417deff6c27f9f16083faa4eea16a22784a86
checksum: 10/f7c21c3a3364a52069aac072c7492ebac6eda03f495905d15f1999f246dadbd26d621ff4a4e834a44fa5d26830e576a031b2c00ec104002d38e4e59150a6d6ff
languageName: node
linkType: hard