mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-08 23:51:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b6f4942f |
@@ -91,6 +91,7 @@
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.2.1",
|
||||
"cally": "0.9.2",
|
||||
"chart2music": "1.20.0",
|
||||
"color-name": "2.1.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.49.0",
|
||||
@@ -101,6 +102,7 @@
|
||||
"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.16",
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
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"]);
|
||||
|
||||
// Mirrors the extension's own reading of a point: it drops anything whose y is
|
||||
// not a real number, so a series of nothing but nulls converts to an empty group
|
||||
// and makes Chart2Music throw. Short-circuits on the first usable point.
|
||||
const hasNumericPoint = (data: unknown): boolean =>
|
||||
Array.isArray(data) &&
|
||||
data.some((raw) => {
|
||||
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;
|
||||
}
|
||||
return typeof y === "number" && !Number.isNaN(y);
|
||||
});
|
||||
|
||||
export const canSonifyChart = (data: HaECSeries): boolean => {
|
||||
const series = ensureArray(data);
|
||||
return (
|
||||
// Cards commonly push empty placeholder series, so only require that
|
||||
// something is plottable — but every type has to be convertible.
|
||||
series.some((s) => hasNumericPoint(s.data)) &&
|
||||
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 at least one point it can read.
|
||||
const seriesIndex: number[] = [];
|
||||
ensureArray(chartOptions.series).forEach((s, index) => {
|
||||
if (hasNumericPoint((s as HaECSeriesItem | undefined)?.data)) {
|
||||
seriesIndex.push(index);
|
||||
}
|
||||
});
|
||||
if (!seriesIndex.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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: any) {
|
||||
options.onError(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();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
@@ -47,6 +48,8 @@ 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";
|
||||
|
||||
@@ -146,6 +149,17 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@query(".chart") private _chartContainer?: HTMLDivElement;
|
||||
|
||||
@query(".sonification-output")
|
||||
private _sonificationOutput?: HTMLDivElement;
|
||||
|
||||
private _sonification?: ChartSonification;
|
||||
|
||||
private _sonificationLoading = false;
|
||||
|
||||
@state() private _sonificationUnavailable = false;
|
||||
|
||||
@state() private _sonificationFocusHeld = false;
|
||||
|
||||
private _modifierPressed = false;
|
||||
|
||||
private _isTouchDevice = "ontouchstart" in window;
|
||||
@@ -198,6 +212,7 @@ export class HaChartBase extends LitElement {
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
this._disposeSonification();
|
||||
this.chart?.dispose();
|
||||
this.chart = undefined;
|
||||
this._originalZrFlush = undefined;
|
||||
@@ -312,6 +327,17 @@ 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._disposeSonification();
|
||||
}
|
||||
}
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
@@ -337,6 +363,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const sonifiable =
|
||||
!this._sonificationUnavailable && canSonifyChart(this.data);
|
||||
return html`
|
||||
<div
|
||||
class="container ${classMap({ "has-height": !!this.height })}"
|
||||
@@ -348,8 +376,22 @@ export class HaChartBase extends LitElement {
|
||||
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
<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
|
||||
)}
|
||||
@focus=${this._handleChartFocus}
|
||||
@blur=${this._handleChartBlur}
|
||||
></div>
|
||||
</div>
|
||||
<div class="sonification-output"></div>
|
||||
${this._renderLegend()}
|
||||
<div class="top-controls ${classMap({ small: this.smallControls })}">
|
||||
<slot name="search"></slot>
|
||||
@@ -521,6 +563,61 @@ 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() {
|
||||
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.
|
||||
// Stay programmatically focusable for as long as we hold focus though:
|
||||
// dropping tabindex off the active element resets focus to the document
|
||||
// and costs the user their place in the tab order.
|
||||
this._sonificationFocusHeld =
|
||||
this.shadowRoot?.activeElement === this._chartContainer;
|
||||
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,
|
||||
@@ -533,6 +630,9 @@ 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();
|
||||
}
|
||||
@@ -1450,6 +1550,23 @@ 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);
|
||||
|
||||
@@ -1129,7 +1129,11 @@
|
||||
"zoom_reset": "Reset zoom",
|
||||
"expand_legend": "More",
|
||||
"collapse_legend": "Less",
|
||||
"toggle_visibility": "Toggle visibility"
|
||||
"toggle_visibility": "Toggle visibility",
|
||||
"chart": "Chart",
|
||||
"time": "Time",
|
||||
"value": "Value",
|
||||
"category": "Category"
|
||||
},
|
||||
"map": {
|
||||
"error": "Unable to load map"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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; 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]] }]))).toBe(
|
||||
true
|
||||
);
|
||||
expect(canSonifyChart(series([{ type: "bar", data: [[0, 1]] }]))).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]] },
|
||||
])
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects series types the extension cannot convert", () => {
|
||||
expect(canSonifyChart(series([{ type: "custom", data: [[0, 1]] }]))).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
canSonifyChart(
|
||||
series([
|
||||
{ type: "line", data: [[0, 1]] },
|
||||
{ type: "sankey", data: [[0, 1]] },
|
||||
])
|
||||
)
|
||||
).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 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],
|
||||
],
|
||||
},
|
||||
])
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("reads the y out of object-form points", () => {
|
||||
expect(
|
||||
canSonifyChart(series([{ type: "bar", data: [{ value: [0, 0.28] }] }]))
|
||||
).toBe(true);
|
||||
expect(
|
||||
canSonifyChart(series([{ type: "bar", data: [{ value: [0, null] }] }]))
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2756,6 +2756,27 @@ __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"
|
||||
@@ -2763,6 +2784,17 @@ __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"
|
||||
@@ -2772,6 +2804,16 @@ __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"
|
||||
@@ -2834,6 +2876,15 @@ __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"
|
||||
@@ -2881,6 +2932,24 @@ __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"
|
||||
@@ -7492,6 +7561,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chart2music@npm:1.20.0, 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"
|
||||
@@ -8089,7 +8167,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decimal.js@npm:^10.6.0":
|
||||
"decimal.js@npm:^10.4.3, decimal.js@npm:^10.6.0":
|
||||
version: 10.6.0
|
||||
resolution: "decimal.js@npm:10.6.0"
|
||||
checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69
|
||||
@@ -8355,6 +8433,17 @@ __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"
|
||||
@@ -9958,6 +10047,7 @@ __metadata:
|
||||
barcode-detector: "npm:3.2.1"
|
||||
browserslist-useragent-regexp: "npm:4.1.4"
|
||||
cally: "npm:0.9.2"
|
||||
chart2music: "npm:1.20.0"
|
||||
color-name: "npm:2.1.1"
|
||||
comlink: "npm:4.4.2"
|
||||
core-js: "npm:3.49.0"
|
||||
@@ -9969,6 +10059,7 @@ __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.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
@@ -10340,6 +10431,18 @@ __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"
|
||||
|
||||
Reference in New Issue
Block a user