mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-14 04:12:16 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4ec72006d | |||
| 393d6a8a0a | |||
| 4a030884f5 | |||
| f65596cad8 | |||
| 1449c17fd1 | |||
| ce0e6a7665 | |||
| 460dace974 | |||
| 7111d8a8a8 | |||
| b96d1f2809 |
+1
-1
@@ -137,7 +137,7 @@
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.6",
|
||||
"@rspack/core": "2.0.8",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
|
||||
@@ -19,6 +19,40 @@ import type { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../data/sensor";
|
||||
|
||||
// Domains whose state is a timezone-agnostic date and/or time string.
|
||||
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
|
||||
|
||||
// Domains whose state is a timestamp.
|
||||
const TIMESTAMP_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
// Maps Intl.NumberFormat part types to ValuePart types for monetary states.
|
||||
const MONETARY_TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
@@ -138,21 +172,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
const type = MONETARY_TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
@@ -191,7 +214,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
return [{ type: "value", value: value }];
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
if (DATE_TIME_DOMAINS.has(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
|
||||
@@ -250,23 +273,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
[
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
TIMESTAMP_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(attributes.device_class))
|
||||
) {
|
||||
|
||||
@@ -40,6 +40,25 @@ export const numberFormatToLocale = (
|
||||
}
|
||||
};
|
||||
|
||||
// Constructing an Intl.NumberFormat is comparatively expensive, and these
|
||||
// formatters are created on every numeric state render. The number of distinct
|
||||
// (locale, options) combinations is small and bounded in practice, so cache the
|
||||
// instances instead of rebuilding them on every call.
|
||||
const numberFormatCache = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
const getNumberFormatter = (
|
||||
locale: string | string[] | undefined,
|
||||
options: Intl.NumberFormatOptions
|
||||
): Intl.NumberFormat => {
|
||||
const key = JSON.stringify([locale, options]);
|
||||
let formatter = numberFormatCache.get(key);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, options);
|
||||
numberFormatCache.set(key, formatter);
|
||||
}
|
||||
return formatter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
@@ -75,7 +94,7 @@ export const formatNumberToParts = (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num))
|
||||
) {
|
||||
return new Intl.NumberFormat(
|
||||
return getNumberFormatter(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).formatToParts(Number(num));
|
||||
@@ -87,7 +106,7 @@ export const formatNumberToParts = (
|
||||
localeOptions?.number_format === NumberFormat.none
|
||||
) {
|
||||
// If NumberFormat is none, use en-US format without grouping.
|
||||
return new Intl.NumberFormat(
|
||||
return getNumberFormatter(
|
||||
"en-US",
|
||||
getDefaultFormatOptions(num, {
|
||||
...options,
|
||||
|
||||
@@ -215,10 +215,16 @@ export class HaDataTable extends LitElement {
|
||||
if (clear) {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
// Map + Set keep a large selection O(rows + ids) instead of O(rows × ids).
|
||||
const rowLookup = new Map(
|
||||
(this._filteredData || []).map((data) => [data[this.id], data])
|
||||
);
|
||||
const checkedRows = new Set(this._checkedRows);
|
||||
ids.forEach((id) => {
|
||||
const row = this._filteredData?.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
const row = rowLookup.get(id);
|
||||
if (row?.selectable !== false && !checkedRows.has(id)) {
|
||||
this._checkedRows.push(id);
|
||||
checkedRows.add(id);
|
||||
}
|
||||
});
|
||||
this._lastSelectedRowId = null;
|
||||
|
||||
@@ -85,15 +85,25 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
await listTTSVoices(this.hass, this.engineId, this.language)
|
||||
).voices;
|
||||
|
||||
if (!this.value) {
|
||||
const valueIsValid =
|
||||
this.value &&
|
||||
this._voices?.some((voice) => voice.voice_id === this.value);
|
||||
|
||||
if (valueIsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._voices ||
|
||||
!this._voices.find((voice) => voice.voice_id === this.value)
|
||||
) {
|
||||
this.value = undefined;
|
||||
// The current value is missing or no longer valid for the loaded voices.
|
||||
// When a voice is required, auto-select the first one (the <ha-select>
|
||||
// already displays it) so the value is propagated to the parent;
|
||||
// otherwise clear it.
|
||||
const newValue =
|
||||
this.required && this._voices?.length
|
||||
? this._voices[0].voice_id
|
||||
: undefined;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,15 +261,22 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -1178,9 +1178,17 @@ export class HaConfigEntities extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the *set* of entity ids matters for the list below. A plain state
|
||||
// value change on an existing entity cannot add an "entity without unique
|
||||
// id", so detecting a newly added entity lets us skip the (potentially
|
||||
// large) rebuild on every state update, which fires constantly.
|
||||
const stateEntityAdded =
|
||||
changedProps.has("hass") &&
|
||||
(!oldHass ||
|
||||
Object.keys(this.hass.states).some((id) => !(id in oldHass.states)));
|
||||
|
||||
if (
|
||||
(changedProps.has("hass") &&
|
||||
(!oldHass || oldHass.states !== this.hass.states)) ||
|
||||
stateEntityAdded ||
|
||||
changedProps.has("_entities") ||
|
||||
changedProps.has("_entitySources") ||
|
||||
changedProps.has("_exposedEntities")
|
||||
|
||||
@@ -1047,7 +1047,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
height: 24px;
|
||||
padding: 16px 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.deleteItemButton {
|
||||
|
||||
@@ -5327,9 +5327,9 @@
|
||||
"type_input": "Numeric value of another entity",
|
||||
"description": {
|
||||
"picker": "Triggers when the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
|
||||
"above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}{duration, select, \n undefined {} \n other { for {duration}}\n }",
|
||||
"below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }",
|
||||
"above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }"
|
||||
"above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} above {above}{duration, select, \n undefined {} \n other { for {duration}}\n }",
|
||||
"below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }",
|
||||
"above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }"
|
||||
}
|
||||
},
|
||||
"persistent_notification": {
|
||||
|
||||
@@ -64,6 +64,26 @@ describe("formatNumber", () => {
|
||||
assert.strictEqual(formatNumber("", defaultLocale), "0");
|
||||
});
|
||||
|
||||
it("Returns consistent results for interleaved calls with different options (formatter cache)", () => {
|
||||
// Exercises the cached Intl.NumberFormat instances: alternating option
|
||||
// shapes must each keep producing their own correct output.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
assert.strictEqual(formatNumber(1234.5, defaultLocale), "1,234.5");
|
||||
assert.strictEqual(
|
||||
formatNumber(1234.5, defaultLocale, { minimumFractionDigits: 2 }),
|
||||
"1,234.50"
|
||||
);
|
||||
assert.strictEqual(formatNumber("1234.50", defaultLocale), "1,234.50");
|
||||
assert.strictEqual(
|
||||
formatNumber(1234.5, {
|
||||
...defaultLocale,
|
||||
number_format: NumberFormat.none,
|
||||
}),
|
||||
"1234.5"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("Formats number with options", () => {
|
||||
assert.strictEqual(
|
||||
formatNumber(1234.5, defaultLocale, {
|
||||
|
||||
@@ -3591,51 +3591,51 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.0.6"
|
||||
"@rspack/binding-darwin-arm64@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.0.8"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.0.6"
|
||||
"@rspack/binding-darwin-x64@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.0.8"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.0.6"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.0.8"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.0.6"
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.0.8"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.0.6"
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.0.8"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.0.6"
|
||||
"@rspack/binding-linux-x64-musl@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.0.8"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.0.6"
|
||||
"@rspack/binding-wasm32-wasi@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.0.8"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:1.10.0"
|
||||
"@emnapi/runtime": "npm:1.10.0"
|
||||
@@ -3644,41 +3644,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.0.6"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.0.8"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.0.6"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.0.8"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.0.6"
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.0.8"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/binding@npm:2.0.6"
|
||||
"@rspack/binding@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/binding@npm:2.0.8"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:2.0.6"
|
||||
"@rspack/binding-darwin-x64": "npm:2.0.6"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.0.6"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.0.6"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.0.6"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.0.6"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.0.6"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.0.6"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.0.6"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.0.6"
|
||||
"@rspack/binding-darwin-arm64": "npm:2.0.8"
|
||||
"@rspack/binding-darwin-x64": "npm:2.0.8"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.0.8"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.0.8"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.0.8"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.0.8"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.0.8"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.0.8"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.0.8"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.0.8"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -3700,15 +3700,15 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/c2e5245abab3257d02f5d98947fad26c8de1b18bb17362734035cfbdd725d9c6c78432372bdff985b32fa4062059d7210e9f5ea7314ae3080805b64f616fe348
|
||||
checksum: 10/aface75866ff0bcd4934fda26e856e8de63e710a1489e654f1c6e5108d6ca46d2183b01aad2a76db1511e99843522272882ece53c2a4cf9fbfe0ac5ab5bcd5c2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@rspack/core@npm:2.0.6"
|
||||
"@rspack/core@npm:2.0.8":
|
||||
version: 2.0.8
|
||||
resolution: "@rspack/core@npm:2.0.8"
|
||||
dependencies:
|
||||
"@rspack/binding": "npm:2.0.6"
|
||||
"@rspack/binding": "npm:2.0.8"
|
||||
peerDependencies:
|
||||
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
|
||||
"@swc/helpers": ^0.5.23
|
||||
@@ -3717,7 +3717,7 @@ __metadata:
|
||||
optional: true
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/d2417690e8135342179bc9e5035e16fe827522b4c0babef029a21ff5903cd56c09b86f08924527bd7d3e66f178f1f678ce099199cac8c1a137b18c5d8892e613
|
||||
checksum: 10/93e34b878dbc69c12f9b06909354246597a5c387c5df77f61e56f3a20e2b45434b9fa8734866f4e662ab0e3456064bf8133ae6f58a3afffee5053a27b8395195
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8483,7 +8483,7 @@ __metadata:
|
||||
"@octokit/rest": "npm:22.0.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.5.12"
|
||||
"@rspack/core": "npm:2.0.6"
|
||||
"@rspack/core": "npm:2.0.8"
|
||||
"@rspack/dev-server": "npm:2.0.3"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
|
||||
Reference in New Issue
Block a user