Compare commits

..
Author SHA1 Message Date
Bram Kragten 2cbce1c10b Replace remove device command 2026-08-11 10:46:42 +02:00
10 changed files with 14 additions and 721 deletions
-1
View File
@@ -101,7 +101,6 @@
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "6.1.0",
"echarts-extension-chart2music": "0.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.5.0",
"hls.js": "1.6.17",
-268
View File
@@ -1,268 +0,0 @@
import type { HassConfig } from "home-assistant-js-websocket";
import type { EChartsType } from "echarts/core";
import type { XAXisOption, YAXisOption } from "echarts/types/dist/shared";
import { ensureArray } from "../../common/array/ensure-array";
import { formatDateTime } from "../../common/datetime/format_date_time";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { FrontendLocaleData } from "../../data/translation";
import type {
HaECSeries,
HaECSeriesItem,
} from "../../resources/echarts/echarts";
export interface ChartSonification {
update: () => void;
dispose: () => void;
}
// Series types the Chart2Music ECharts extension can turn into data points. Our
// other series (custom timelines, sankey, network graphs) have no equivalent, and
// the extension refuses the whole chart if a single series is unsupported.
const SONIFIABLE_SERIES_TYPES = new Set(["bar", "line", "pie", "scatter"]);
// Languages shipped by chart2music. Anything else falls back to its English.
const SONIFICATION_LANGUAGES = new Set(["de", "en", "es", "fr", "hmn", "it"]);
// Fewer than this and there is nothing to walk between, so a focus stop would
// lead nowhere.
const MIN_NAVIGABLE_POINTS = 2;
// Mirrors the extension's own reading of a point: it takes `value` as [x, y] and
// drops anything whose y is not a real number. That rejects gap-only series, and
// also value-first pairs like the energy device charts' [amount, "sensor.foo"].
// Counts no further than `limit` so this stays cheap on charts with many points.
const countNumericPoints = (data: unknown, limit: number): number => {
if (!Array.isArray(data)) {
return 0;
}
let found = 0;
for (const raw of data) {
let y: unknown = raw;
if (Array.isArray(raw)) {
y = raw.length > 1 ? raw[1] : raw[0];
} else if (raw && typeof raw === "object") {
const { value } = raw as { value?: unknown };
y = Array.isArray(value)
? value.length > 1
? value[1]
: value[0]
: value;
}
if (typeof y === "number" && !Number.isNaN(y)) {
found += 1;
if (found >= limit) {
break;
}
}
}
return found;
};
const countNavigablePoints = (
series: readonly ({ data?: unknown } | undefined)[]
): number => {
let total = 0;
for (const s of series) {
total += countNumericPoints(s?.data, MIN_NAVIGABLE_POINTS - total);
if (total >= MIN_NAVIGABLE_POINTS) {
break;
}
}
return total;
};
export const canSonifyChart = (
data: HaECSeries,
// Legend-hidden series reach ECharts with their data stripped, so they cannot
// be sonified either.
hiddenDatasets?: ReadonlySet<string>
): boolean => {
const series = ensureArray(data);
const visible = hiddenDatasets?.size
? series.filter((s) => !hiddenDatasets.has(String(s.id ?? s.name)))
: series;
return (
// Cards commonly push empty placeholder series, so judge the chart by the
// points the extension can actually read — but every type has to be
// convertible too.
countNavigablePoints(visible) >= MIN_NAVIGABLE_POINTS &&
series.every((s) => SONIFIABLE_SERIES_TYPES.has(s.type as string))
);
};
interface SonifyChartOptions {
cc: HTMLElement;
localize: LocalizeFunc;
locale: FrontendLocaleData;
config: HassConfig;
onError: (error: string) => void;
}
// Chart2Music appends its help and options dialogs straight to document.body, so
// they can only be themed from a document-level stylesheet.
let stylesAppended = false;
const appendSonificationStyles = () => {
if (stylesAppended) {
return;
}
stylesAppended = true;
const style = document.createElement("style");
style.textContent = `
dialog.chart2music-dialog {
box-sizing: border-box;
max-width: min(600px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
padding: var(--ha-space-6);
border: none;
border-radius: var(--ha-border-radius-lg);
background-color: var(--card-background-color);
color: var(--primary-text-color);
font-family: var(--ha-font-family-body);
font-size: var(--ha-font-size-m);
box-shadow: var(--ha-box-shadow-l);
}
dialog.chart2music-dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
dialog.chart2music-dialog h1 {
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-normal);
margin-block: 0 var(--ha-space-4);
padding-inline-end: var(--ha-space-8);
}
dialog.chart2music-dialog table {
border-collapse: collapse;
width: 100%;
}
dialog.chart2music-dialog th,
dialog.chart2music-dialog td {
text-align: start;
padding: var(--ha-space-1) var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
dialog.chart2music-dialog a {
color: var(--primary-color);
}
dialog.chart2music-dialog > button {
/* The extension inlines "right", which does not mirror in RTL, and inline
styles can only be beaten with !important. */
inset-inline-end: var(--ha-space-4) !important;
inset-inline-start: auto !important;
top: var(--ha-space-4);
min-width: 32px;
min-height: 32px;
cursor: pointer;
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-sm);
background-color: transparent;
color: var(--primary-text-color);
font: inherit;
}
`;
document.head.append(style);
};
export const sonifyChart = async (
chart: EChartsType,
options: SonifyChartOptions
): Promise<ChartSonification | null> => {
const { connect } = await import("echarts-extension-chart2music");
const { localize, locale, config } = options;
appendSonificationStyles();
// ECharts nulls its model on dispose, and the instance can be disposed while
// the chunk is in flight.
const chartOptions = chart.getOption() as ReturnType<
EChartsType["getOption"]
> | null;
if (!chartOptions) {
return null;
}
const xAxis = ensureArray(chartOptions.xAxis)[0] as XAXisOption | undefined;
const yAxis = ensureArray(chartOptions.yAxis)[0] as YAXisOption | undefined;
// Chart2Music throws while validating a group with no points, which is what
// placeholder, legend-hidden and all-null series turn into, so only offer it
// the series carrying points it can read.
const allSeries = ensureArray(chartOptions.series) as (
HaECSeriesItem | undefined
)[];
const readable = allSeries.filter((s) => countNumericPoints(s?.data, 1));
// A single point is not navigable, so it does not earn a focus stop either.
if (countNavigablePoints(readable) < MIN_NAVIGABLE_POINTS) {
return null;
}
const seriesIndex = readable.map((s) => allSeries.indexOf(s));
// Chart2Music always reads out an axis label, and the extension picks the wrong
// axis to name when there is no category axis, so label both explicitly.
const isTimeAxis = xAxis?.type === "time";
const x = {
label:
xAxis?.name ||
localize(
isTimeAxis
? "ui.components.history_charts.time"
: "ui.components.history_charts.category"
),
// Time series carry raw timestamps, which would otherwise be announced as
// epoch milliseconds.
format: isTimeAxis
? (value: number) => formatDateTime(new Date(value), locale, config)
: undefined,
};
const y = {
label: yAxis?.name || localize("ui.components.history_charts.value"),
};
let connection: ReturnType<typeof connect>;
try {
connection = connect(chart, {
cc: options.cc,
seriesIndex,
title: localize("ui.components.history_charts.chart"),
lang: SONIFICATION_LANGUAGES.has(locale.language)
? locale.language
: "en",
errorCallback: options.onError,
axes: { x, y },
});
} catch (err) {
options.onError(err instanceof Error ? err.message : String(err));
return null;
}
if (!connection) {
return null;
}
// Chart2Music bails out silently on mobile user agents, returning an instance
// that never wired anything up. Turning the caption container into a live
// region is the last thing it does, so use that as the "really connected" test
// rather than leaving a focus stop that does nothing.
if (!options.cc.hasAttribute("aria-live")) {
connection.dispose();
return null;
}
const connected = connection;
// The extension re-reads the chart from ECharts' own "finished" event. Run that
// through a guard of our own so a conversion failure cannot escape into
// ECharts' event dispatch and leave the chart half-rendered.
const update = () => {
try {
connected.update();
} catch (_err) {
// Keep whatever Chart2Music last read successfully.
}
};
chart.off("finished", connected.update);
chart.on("finished", update);
return {
update,
dispose: () => {
chart.off("finished", update);
connected.dispose();
},
};
};
+1 -120
View File
@@ -22,7 +22,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
@@ -48,8 +47,6 @@ import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import type { ChartSonification } from "./chart-sonification";
import { canSonifyChart, sonifyChart } from "./chart-sonification";
import { downSampleLineData } from "./down-sample";
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
@@ -149,17 +146,6 @@ export class HaChartBase extends LitElement {
@query(".chart") private _chartContainer?: HTMLDivElement;
@query(".sonification-output")
private _sonificationOutput?: HTMLDivElement;
private _sonification?: ChartSonification;
@state() private _sonificationLoading = false;
@state() private _sonificationUnavailable = false;
@state() private _sonificationFocusHeld = false;
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
@@ -212,7 +198,6 @@ export class HaChartBase extends LitElement {
while (this._listeners.length) {
this._listeners.pop()!();
}
this._disposeSonification();
this.chart?.dispose();
this.chart = undefined;
this._originalZrFlush = undefined;
@@ -327,18 +312,6 @@ export class HaChartBase extends LitElement {
}
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
chartOptions.series = this._getSeries();
// New data, or a series shown again, may well be convertible where the
// last set was not.
this._sonificationUnavailable = false;
// The connection is built from the series that had data at the time, so
// drop it and let the next focus rebuild it against the current set.
if (
this._sonification &&
(changedProps.has("_hiddenDatasets") ||
!canSonifyChart(this.data, this._hiddenDatasets))
) {
this._disposeSonification();
}
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
@@ -364,9 +337,6 @@ export class HaChartBase extends LitElement {
}
protected render() {
const sonifiable =
!this._sonificationUnavailable &&
canSonifyChart(this.data, this._hiddenDatasets);
return html`
<div
class="container ${classMap({ "has-height": !!this.height })}"
@@ -378,23 +348,8 @@ export class HaChartBase extends LitElement {
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
})}
>
<div
class="chart"
role=${ifDefined(sonifiable ? "application" : undefined)}
tabindex=${ifDefined(
sonifiable ? "0" : this._sonificationFocusHeld ? "-1" : undefined
)}
aria-label=${ifDefined(
sonifiable
? this.hass.localize("ui.components.history_charts.chart")
: undefined
)}
aria-busy=${ifDefined(this._sonificationLoading ? "true" : undefined)}
@focus=${this._handleChartFocus}
@blur=${this._handleChartBlur}
></div>
<div class="chart"></div>
</div>
<div class="sonification-output"></div>
${this._renderLegend()}
<div class="top-controls ${classMap({ small: this.smallControls })}">
<slot name="search"></slot>
@@ -566,60 +521,6 @@ export class HaChartBase extends LitElement {
</div>`;
}
// Chart2Music adds ~45 kB gzipped, so it is only fetched once someone actually
// moves keyboard focus into a chart.
private async _handleChartFocus() {
// Dropping tabindex off the active element resets focus to the document and
// costs the user their place in the tab order, so stay programmatically
// focusable for as long as we hold focus, however we stop being sonifiable.
this._sonificationFocusHeld = true;
if (this._sonification || this._sonificationLoading || !this.chart) {
return;
}
this._sonificationLoading = true;
try {
const sonification = await sonifyChart(this.chart, {
cc: this._sonificationOutput!,
localize: this.hass.localize,
locale: this.hass.locale,
config: this.hass.config,
onError: () => {
// Charts the extension cannot describe stay silent rather than
// dropping an error on someone who only pressed Tab.
},
});
if (!this.isConnected || !this.chart) {
sonification?.dispose();
return;
}
if (!sonification) {
// Nothing came back, so stop offering a focus stop that leads nowhere.
this._sonificationUnavailable = true;
return;
}
this._sonification = sonification;
if (this.shadowRoot?.activeElement === this._chartContainer) {
// Chart2Music reads its summary and key hints on focus, which already
// happened while it was still being fetched.
this._chartContainer!.dispatchEvent(new FocusEvent("focus"));
}
} catch (_err) {
// Never let a failure here escape a focus handler. The tab stop stays, so
// focusing the chart again retries.
} finally {
this._sonificationLoading = false;
}
}
private _handleChartBlur() {
this._sonificationFocusHeld = false;
}
private _disposeSonification() {
this._sonification?.dispose();
this._sonification = undefined;
}
private _formatTimeLabel = (value: number | Date) =>
formatTimeLabel(
value,
@@ -632,9 +533,6 @@ export class HaChartBase extends LitElement {
if (this._loading) return;
this._loading = true;
try {
// The connection holds a reference to the chart instance, so it cannot
// outlive it. Focusing the chart again reconnects.
this._disposeSonification();
if (this.chart) {
this.chart.dispose();
}
@@ -1552,23 +1450,6 @@ export class HaChartBase extends LitElement {
height: 100%;
width: 100%;
}
.chart:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
border-radius: var(--ha-border-radius-sm);
}
/* Chart2Music renders its announcements here. It must stay in the layout for
screen readers to pick up the live region, so hide it visually only. */
.sonification-output {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
border: 0;
}
.top-controls {
position: absolute;
top: var(--ha-space-4);
+4 -6
View File
@@ -146,15 +146,13 @@ export const updateDeviceRegistryEntry = (
...updates,
});
export const removeConfigEntryFromDevice = (
export const removeDeviceFromRegistry = (
hass: HomeAssistant,
deviceId: string,
configEntryId: string
deviceId: string
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/remove_config_entry",
hass.callWS<null>({
type: "config/device_registry/remove",
device_id: deviceId,
config_entry_id: configEntryId,
});
export const sortDeviceRegistryByName = (
@@ -65,7 +65,7 @@ import {
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import {
removeConfigEntryFromDevice,
removeDeviceFromRegistry,
updateDeviceRegistryEntry,
} from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
@@ -1218,11 +1218,7 @@ export class HaConfigDevicePage extends LitElement {
}
try {
await removeConfigEntryFromDevice(
this.hass,
this.deviceId,
entry.entry_id
);
await removeDeviceFromRegistry(this.hass, this.deviceId);
} catch (err: unknown) {
showAlertDialog(this, {
title: this.hass.localize(
@@ -65,7 +65,7 @@ import type {
DeviceRegistryEntry,
} from "../../../data/device/device_registry";
import {
removeConfigEntryFromDevice,
removeDeviceFromRegistry,
updateDeviceRegistryEntry,
} from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -1206,19 +1206,9 @@ ${rejected
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
confirm: async () => {
const proms: Promise<DeviceRegistryEntry>[] = [];
const proms: Promise<null>[] = [];
this._selectedCanDelete.forEach((deviceId) => {
const entries = this.hass!.devices[deviceId]?.config_entries;
entries.forEach((entryId) => {
if (
this.entries.find((entry) => entry.entry_id === entryId)
?.supports_remove_device
) {
proms.push(
removeConfigEntryFromDevice(this.hass!, deviceId, entryId)
);
}
});
proms.push(removeDeviceFromRegistry(this.hass!, deviceId));
});
const results = await Promise.allSettled(proms);
if (hasRejectedItems(results)) {
@@ -22,7 +22,7 @@ import {
type DisableConfigEntryResult,
} from "../../../data/config_entries";
import {
removeConfigEntryFromDevice,
removeDeviceFromRegistry,
updateDeviceRegistryEntry,
type DeviceRegistryEntry,
} from "../../../data/device/device_registry";
@@ -315,7 +315,6 @@ class HaConfigEntryDeviceRow extends LitElement {
};
private _handleDeleteDevice = async () => {
const entry = this.entry;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
confirmText: this.hass.localize("ui.common.delete"),
@@ -328,11 +327,7 @@ class HaConfigEntryDeviceRow extends LitElement {
}
try {
await removeConfigEntryFromDevice(
this.hass!,
this.device.id,
entry.entry_id
);
await removeDeviceFromRegistry(this.hass!, this.device.id);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.devices.error_delete"),
+1 -5
View File
@@ -1129,11 +1129,7 @@
"zoom_reset": "Reset zoom",
"expand_legend": "More",
"collapse_legend": "Less",
"toggle_visibility": "Toggle visibility",
"chart": "Chart",
"time": "Time",
"value": "Value",
"category": "Category"
"toggle_visibility": "Toggle visibility"
},
"map": {
"error": "Unable to load map"
@@ -1,192 +0,0 @@
import { describe, expect, it } from "vitest";
import { canSonifyChart } from "../../../src/components/chart/chart-sonification";
import type { HaECSeries } from "../../../src/resources/echarts/echarts";
const series = (
items: { type: string; id?: string; name?: string; data?: unknown[] }[]
) => items as unknown as HaECSeries;
describe("canSonifyChart", () => {
it("accepts line and bar series that carry data", () => {
expect(
canSonifyChart(
series([
{
type: "line",
data: [
[0, 1],
[1, 2],
],
},
])
)
).toBe(true);
expect(
canSonifyChart(
series([
{
type: "bar",
data: [
[0, 1],
[1, 2],
],
},
])
)
).toBe(true);
});
it("accepts a chart whose empty placeholder series sits beside real data", () => {
expect(
canSonifyChart(
series([
{ type: "bar", data: [] },
{
type: "bar",
data: [
[0, 1],
[1, 2],
],
},
])
)
).toBe(true);
});
it("rejects series types the extension cannot convert", () => {
expect(
canSonifyChart(
series([
{
type: "custom",
data: [
[0, 1],
[1, 2],
],
},
])
)
).toBe(false);
expect(
canSonifyChart(
series([
{
type: "line",
data: [
[0, 1],
[1, 2],
],
},
{
type: "sankey",
data: [
[0, 1],
[1, 2],
],
},
])
)
).toBe(false);
});
it("rejects charts with nothing plotted", () => {
expect(canSonifyChart(series([]))).toBe(false);
expect(canSonifyChart(series([{ type: "line", data: [] }]))).toBe(false);
expect(canSonifyChart(series([{ type: "line" }]))).toBe(false);
});
it("rejects value-first pairs, which the extension reads as a non-numeric y", () => {
// The energy device charts encode [amount, categoryName]. Chart2Music takes
// value as [x, y], so every one of those points is dropped.
expect(
canSonifyChart(
series([
{
type: "bar",
data: [
{ value: [12.5, "sensor.a"] },
{ value: [8.25, "sensor.b"] },
],
},
])
)
).toBe(false);
});
it("rejects a chart left with a single readable point", () => {
// The device pie's slices are all unreadable, leaving only its one-number
// total series — nothing the arrow keys could move between.
expect(
canSonifyChart(
series([
{ type: "pie", data: [{ value: [12.5, "sensor.a"] }] },
{ type: "pie", data: [24.5] },
])
)
).toBe(false);
});
it("ignores the points of legend-hidden series", () => {
// Hiding a series strips its data before it reaches ECharts, so it cannot
// be navigated either.
const chart = series([
{ type: "line", id: "a", data: [[0, 1]] },
{ type: "line", name: "b", data: [[1, 2]] },
]);
expect(canSonifyChart(chart, new Set())).toBe(true);
expect(canSonifyChart(chart, new Set(["b"]))).toBe(false);
expect(canSonifyChart(chart, new Set(["a", "b"]))).toBe(false);
});
it("rejects a series whose points are all gaps", () => {
// Chart2Music drops any point without a numeric y, so a series of nothing
// but nulls converts to an empty group and makes it throw.
expect(
canSonifyChart(
series([
{
type: "line",
data: [
[0, null],
[1, null],
],
},
])
)
).toBe(false);
});
it("accepts a series that only becomes numeric partway through", () => {
expect(
canSonifyChart(
series([
{
type: "line",
data: [
[0, null],
[1, 21.5],
[2, 21.9],
],
},
])
)
).toBe(true);
});
it("reads the y out of object-form points", () => {
expect(
canSonifyChart(
series([
{ type: "bar", data: [{ value: [0, 0.28] }, { value: [1, 0.31] }] },
])
)
).toBe(true);
expect(
canSonifyChart(
series([
{ type: "bar", data: [{ value: [0, null] }, { value: [1, null] }] },
])
)
).toBe(false);
});
});
+1 -103
View File
@@ -2765,27 +2765,6 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.4":
version: 2.3.4
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:2.2.7":
version: 2.2.7
resolution: "@formatjs/fast-memoize@npm:2.2.7"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/e7e6efc677d63a13d99a854305db471b69f64cbfebdcb6dbe507dab9aa7eaae482ca5de86f343c856ca0a2c8f251672bd1f37c572ce14af602c0287378097d43
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:3.1.7":
version: 3.1.7
resolution: "@formatjs/fast-memoize@npm:3.1.7"
@@ -2793,17 +2772,6 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.11.2":
version: 2.11.2
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/icu-skeleton-parser": "npm:1.8.14"
tslib: "npm:^2.8.0"
checksum: 10/e919eb2a132ac1d54fb1a7e3a3254007649b55196d3818090df92a4268dcddf20cbdf863c06039fbbe7a35a8a3f17bdc172dade99d1f17c1d8a95dcec444c3e3
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.5.16":
version: 3.5.16
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.16"
@@ -2813,16 +2781,6 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.14":
version: 1.8.14
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.14"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
tslib: "npm:^2.8.0"
checksum: 10/2fbe3155c310358820b118d8c9844f314eff3500a82f1c65402434a3095823e1afeaab8d1762b4a59cc5679d82dc4c8c134683565d7cdae4daace23251f46a47
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:2.1.11":
version: 2.1.11
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.11"
@@ -2885,15 +2843,6 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.6.1":
version: 0.6.1
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.8.13":
version: 0.8.13
resolution: "@formatjs/intl-localematcher@npm:0.8.13"
@@ -2941,24 +2890,6 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl@npm:3.1.6":
version: 3.1.6
resolution: "@formatjs/intl@npm:3.1.6"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/icu-messageformat-parser": "npm:2.11.2"
intl-messageformat: "npm:10.7.16"
tslib: "npm:^2.8.0"
peerDependencies:
typescript: ^5.6.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/10ebdce088898ad7de59c10890f7c02fa9f5aa50518ed3fae7e0ae1c392f3973da00464d2a42229cce97916c2eff1e41630e95c18bab01713d2b6ef5c7fd80c1
languageName: node
linkType: hard
"@fullcalendar/core@npm:6.1.21":
version: 6.1.21
resolution: "@fullcalendar/core@npm:6.1.21"
@@ -7570,15 +7501,6 @@ __metadata:
languageName: node
linkType: hard
"chart2music@npm:^1.20.0":
version: 1.20.0
resolution: "chart2music@npm:1.20.0"
dependencies:
"@formatjs/intl": "npm:3.1.6"
checksum: 10/ed421708740e3644a72356c9f313d3bcb7cac26d94a7fb209fa4bc8f9bc24061ed63f98da89277684e71f9402b5414cd45bb60f01c01e576803ba3dca755f4ba
languageName: node
linkType: hard
"chokidar@npm:^3.5.3":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
@@ -8176,7 +8098,7 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3, decimal.js@npm:^10.6.0":
"decimal.js@npm:^10.6.0":
version: 10.6.0
resolution: "decimal.js@npm:10.6.0"
checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69
@@ -8442,17 +8364,6 @@ __metadata:
languageName: node
linkType: hard
"echarts-extension-chart2music@npm:0.1.0":
version: 0.1.0
resolution: "echarts-extension-chart2music@npm:0.1.0"
dependencies:
chart2music: "npm:^1.20.0"
peerDependencies:
echarts: ">=5.0.0 <7"
checksum: 10/55a82c770355228b2a899007e488e5b60171cbaf5d83363a298c4214d9e4a5df1afd1c7805495f42f2ad6edd7c062c8b366f1f8ba7b1a902f2db7eb47fbfcccf
languageName: node
linkType: hard
"echarts@npm:6.1.0":
version: 6.1.0
resolution: "echarts@npm:6.1.0"
@@ -10067,7 +9978,6 @@ __metadata:
del: "npm:8.0.1"
dialog-polyfill: "npm:0.5.6"
echarts: "npm:6.1.0"
echarts-extension-chart2music: "npm:0.1.0"
element-internals-polyfill: "npm:3.0.2"
eslint: "npm:10.8.1"
eslint-config-prettier: "npm:10.1.8"
@@ -10439,18 +10349,6 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:10.7.16":
version: 10.7.16
resolution: "intl-messageformat@npm:10.7.16"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/icu-messageformat-parser": "npm:2.11.2"
tslib: "npm:^2.8.0"
checksum: 10/c19b77c5e495ce8b0d1aa0d95444bf3a4f73886805f1e08d7159b364abcf2f63686b2ccf202eaafb0e39a0e9fde61848b8dd2db1679efd4f6ec8f6a3d0e77928
languageName: node
linkType: hard
"intl-messageformat@npm:11.2.13":
version: 11.2.13
resolution: "intl-messageformat@npm:11.2.13"