Compare commits

...

72 Commits

Author SHA1 Message Date
Aidan Timson
4e6fb3c103 Scale small based on date length 2026-01-13 10:52:35 +00:00
Aidan Timson
4b8cf9a056 Sizing (CSS Impl) 2026-01-13 10:40:36 +00:00
Aidan Timson
bcac688538 Sizing (JS Impl) 2026-01-13 10:40:09 +00:00
Aidan Timson
616209c0f0 Format 2026-01-13 08:57:33 +00:00
Aidan Timson
21ba3952d9 Add 2026-01-13 08:57:33 +00:00
Aidan Timson
7392e05230 Improve 2026-01-13 08:57:33 +00:00
Aidan Timson
74de8365eb Type 2026-01-13 08:57:33 +00:00
Aidan Timson
999d54147d Setup 2026-01-13 08:57:33 +00:00
Aidan Timson
721d0237ac Match 2026-01-13 08:57:33 +00:00
Aidan Timson
a3ad223230 Setup analog clock 2026-01-13 08:57:33 +00:00
Aidan Timson
69290a2ffb Add date to digital clock 2026-01-13 08:57:33 +00:00
Aidan Timson
c840af63f5 Setup 2026-01-13 08:57:33 +00:00
ildar170975
1beca4bfa6 ha-data-table: issues with "numeric" column (#28916)
Co-authored-by: uptimeZERO_ <pavilionsahota@gmail.com>
2026-01-13 08:38:15 +00:00
Kristel
82ab29cfc5 Add "Voice assistant" filter to helpers, automations, scenes and scripts pages (#28914) 2026-01-13 08:29:28 +00:00
Simon Lamon
3579c66f71 Update dropdown adjustments (#28294) 2026-01-13 08:54:17 +01:00
ildar170975
c042a8e310 ha-sidebar: remove scrollIntoViewIfNeeded() (#28938)
remove scrollIntoViewIfNeeded()
2026-01-13 07:23:20 +01:00
renovate[bot]
8d2794a4ee Update dependency vite-tsconfig-paths to v6.0.4 (#28952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 07:15:15 +01:00
Paul Bottein
50be1d9345 Use action button text name for empty state card (#28948) 2026-01-12 17:42:01 +01:00
Petar Petrov
c551bf03b6 Sanitize names in history card and map card (#28947) 2026-01-12 15:28:32 +00:00
Paul Bottein
cd062293fc Add config to empty state card and use it in area empty page (#28946)
* Add config to empty state card and use it in area empty page

* Remove old translations
2026-01-12 16:58:59 +02:00
TheJulianJES
e89ea47d3a Add Matter status to config dashboard (#28825)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-01-12 15:45:18 +01:00
SmartCoder
2cd209a6a4 Fixed modal visibility issue in settings -> areas -> edit room (#28907)
* Fixed modal visibility issue in settings -> areas -> edit room

* converting both components to use ha-wa-dialog

* removed z-index from ha-wa-dialog

* fixed hardcoded .open in media browser dialog and remove unnecessary z-index CSS variables
2026-01-12 15:07:56 +02:00
Marcin Bauer
9bbc761736 Fix: Allow dismissing add integration and helper dialogs with escape/click (#28944)
* refactor: polish automation dialog UI and component styles

* Revert "Merge pull request #1 from marcinbauer85/fix/ui-polish-automation-dialog"

This reverts commit c2c47197e2, reversing
changes made to 49bed5e6a6.

* Fix: Allow dismissing add integration and helper dialogs

* Apply suggestions from code review

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-12 13:39:16 +01:00
Daniel O'Connor
9097faa04b Config > Helpers > Add loading filter state from URL (#28924) 2026-01-12 13:38:04 +01:00
SmartCoder
fcf844cf1a Fix issue #28896: "Last 12 months" in the Datetime Picker selects last year (#28902)
Summary of the fix:
The Problem:
now-12m was selecting the calendar year (Jan 1st to Dec 31st) instead of the last 12 months from now
It used startOfMonth and endOfMonth, which snap to month boundaries
The Solution:
Changed to match the now-7d and now-30d pattern
Now uses subMonths(today, 12) for start and subMonths(today, 0) (which equals today) for end
This gives exactly the last 12 months (365/366 days) ending at the current time
The Fix:
// Before (WRONG):calcDate(subMonths(today, 12), startOfMonth, ...)  // Jan 1st of 12 months agocalcDate(subMonths(today, 1), endOfMonth, ...)     // Dec 31st of last month// After (CORRECT):calcDate(today, subMonths, hass.locale, hass.config, 12)  // 12 months ago from nowcalcDate(today, subMonths, hass.locale, hass.config, 0)   // now
2026-01-12 11:53:08 +00:00
dcapslock
8808c31e98 Fix ha-card styling of .card-content when not first element but not following .card-header (#28935) 2026-01-12 12:41:14 +01:00
Michael
e0a9f5a08a Show also not installable updates on update overview page (#28717)
* add "show not installable option" to update page

* split updates by install feature and show always

* fix

* fix "no update" panel

* use `nothing` instead of empty string

* re-add `outlined` to ha-card

* keep title, use different for not-installable updates
2026-01-12 13:18:53 +02:00
Petar Petrov
56d71c8e54 Use temp & humidity data from attributes in Area card (#28530)
* Use temp & humidity data from attributes in Area card

* Avoid duplicate sensor readings by tracking devices contributing values
2026-01-12 12:01:12 +01:00
karwosts
125ab4c671 Update energy summary visibility condition (#28913)
* Update energy summary visibility condition

* add grid power as special case

* Always show summary when you have powersource
2026-01-12 12:42:16 +02:00
Eduardo Tsen
8014216c45 Fix ha-entity-toggle not restoring old state on exception (#28915) 2026-01-12 10:28:23 +00:00
ildar170975
55ba331489 developer-tools-statistics: alignment for "fix" column (#28942) 2026-01-12 11:25:44 +01:00
karwosts
ad2ff672b0 Add configurable confirmation title & button text (#28931) 2026-01-12 10:19:09 +00:00
JLo
00907ecd17 Add area and device context to media player join dialog (#28926)
* Add area and device context to media player join dialog

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Add memoization to avoid recomputing display data

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 11:08:44 +01:00
Petar Petrov
07d8219136 Add ES5-compatible keyed directive implementation (#28941) 2026-01-12 10:50:38 +01:00
Eduardo Tsen
f37241c84c Fix hui-select-entity-row restoring old state (#28918) 2026-01-12 09:43:31 +00:00
SmartCoder
65d046132d Updated entity name to friendly name (#28928) 2026-01-12 10:14:23 +01:00
Simon Lamon
122cf40092 Don't close dialog upon tooltip close (#28927) 2026-01-11 20:23:42 -05:00
SmartCoder
28ed5c86c7 Fix automation row menu icon being pushed off-screen on mobile (#28893)
When entity names are too long, the header text would push the three-dot menu icon off the right edge of the screen, making it inaccessible. This fix ensures the menu icon remains visible by:

- Adding min-width: 0 to the header slot to allow proper flexbox shrinking and text wrapping

- Adding flex-shrink: 0 to the icons container to prevent it from being compressed

The fix uses standard flexbox properties that work universally across all screen sizes, ensuring the menu icon stays visible on both mobile and desktop views.
2026-01-10 15:58:28 +01:00
Kristel
1f99c3d895 Add Voice assistants filter to Entities page (#28854)
* create Assistants filter

* render logo and name

* make the Voice assistants filter work

* integrate cloudStatus

* code clean-up

* remove cloudStatus

* bugfix

* remove console log

* remove cloudstatus

* set ha-list clientHeight to 49px
2026-01-10 12:57:33 +01:00
LG-ThinQ-Integration
f2293713de Add target_humidity_step to humidifier (#28005)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2026-01-10 10:12:55 +01:00
Brendan Annable
b3f202400c Fix timer restore bug (#28898) 2026-01-10 09:51:19 +01:00
ildar170975
010d87bd0d ha-dialog-automation-save: small improvements & fixes (#28561)
* explictly set line-height for "helper" element

* move "description" to bottom, css tweaks

* revert

* revert, make a helper persistent
2026-01-10 09:40:10 +01:00
karwosts
b403b8f09e Implement allow_negative for duration selector (#28909) 2026-01-10 08:58:14 +01:00
karwosts
b9a3dc795b Duration selector: migrate legacy duration formats (#28880) 2026-01-09 20:30:09 +01:00
Bram Kragten
35dbfdebcf Add support for choose selector to initial form data (#28876)
* Add support for choose selector to initial form data

* Update compute-initial-ha-form-data.ts
2026-01-09 19:57:32 +01:00
renovate[bot]
c5e5fb3ace Update formatjs monorepo (#28905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 17:41:54 +00:00
Yosi Levy
e649472b20 Arrow fixes in media browser (#28890) 2026-01-09 18:31:25 +01:00
Yosi Levy
3cbb24a4c5 Fix for volume scroll in media player (#28891) 2026-01-09 18:30:45 +01:00
renovate[bot]
f92608a9d3 Update dependency @codemirror/view to v6.39.9 (#28903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:23:11 +01:00
renovate[bot]
6591cdc5c1 Update dependency @rspack/core to v1.7.1 (#28892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:22:51 +01:00
renovate[bot]
0ae1ac367d Update dependency lit-html to v3.3.2 (#28762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 08:32:27 +02:00
renovate[bot]
6d3a1b93e1 Update dependency lit to v3.3.2 (#28761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 21:03:20 +01:00
renovate[bot]
6d7b22a21c Update dependency typescript-eslint to v8.52.0 (#28879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 20:54:45 +01:00
Petar Petrov
784ee22623 Removes duplicate closing tag in ha-auth-form-string (#28883) 2026-01-08 20:53:55 +01:00
Aidan Timson
c03654ef8e Fix wa dialog esc behaviour when preventing scrim closure (#28875)
* Fix wa dialog esc behaviour when preventing scrim closure

* Use wa-hide event to prevend closure
2026-01-08 17:10:53 +00:00
Pegasus
826cb3117d Fix: update the id, pan id to capitalize (#28873)
fix: update the id, pan id to capitalize
2026-01-08 12:26:49 +00:00
Aidan Timson
f77fa26ffe Fix type error for calendar card (#28869) 2026-01-08 12:57:46 +02:00
Bram Kragten
35e30f9184 Fix color palette creation (#28867) 2026-01-08 10:14:03 +00:00
DAccord
7dd3ade678 Handling empty history (#28852)
Co-authored-by: DAccord <11232265+DAccord@users.noreply.github.com>
2026-01-08 09:58:38 +00:00
karwosts
6d1e15d11a Add a devtools event listener filter (#28849) 2026-01-08 10:58:24 +01:00
Timothy
f5b33922ff Move companion app settings to a dedicated section in the settings (#28830) 2026-01-08 10:36:50 +01:00
dcapslock
ceb7baf851 Fix choose selector active_choice when card editor config changes (#28858) 2026-01-08 10:20:42 +01:00
ildar170975
d195fd3244 Views: allow showing both icon & text title (#28690) 2026-01-07 19:03:48 +00:00
renovate[bot]
231cd632d6 Update dependency @bundle-stats/plugin-webpack-filter to v4.21.8 (#28846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 17:23:37 +01:00
Wendelin
82d72ea39c Fix logs provider picker mobile width (#28847) 2026-01-07 16:30:49 +01:00
Wendelin
022bebb14f Throttle unknown value checks in ha-generic-picker (#28842) 2026-01-07 16:14:33 +01:00
Paul Bottein
0981ae1b4a Prefill the field with current value when editing a custom text item (#28840) 2026-01-07 15:42:47 +01:00
Paul Bottein
9608824a28 Remove ha-combo-box-textfield (#28841) 2026-01-07 15:39:58 +01:00
Marcin Bauer
33d215533e Add Shift+/ shortcut to shortcuts dialog and use Unicode command character (#28838)
* refactor: polish automation dialog UI and component styles

* Revert "Merge pull request #1 from marcinbauer85/fix/ui-polish-automation-dialog"

This reverts commit c2c47197e2, reversing
changes made to 49bed5e6a6.

* Add shortcuts dialog shortcut and use Unicode command character

* Update shortcut description text
2026-01-07 14:17:18 +00:00
Paul Bottein
5c503ecac0 Reduce shadow effect for scrollable fade mixin (#28832) 2026-01-07 14:16:41 +00:00
Wendelin
d114693fed Improve device picker performance (#28835) 2026-01-07 14:28:18 +01:00
Kristel
7a8cb80413 Add Voice assistant column to data tables (#28785)
* added Voice assistant column to data tables

* remove commented code

* fix column settings

* code review changes

* reuse voice-assistants-expose-assistant-icon

* refactor getEntityVoiceAssistantsKeys

* fix column width

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-07 13:15:01 +00:00
83 changed files with 2875 additions and 933 deletions

View File

@@ -213,7 +213,9 @@ const createRspackConfig = ({
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": "lit/directives/keyed.js",
"lit/directives/keyed$": latestBuild
? "lit/directives/keyed.js"
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",

View File

@@ -34,18 +34,18 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.3",
"@codemirror/view": "6.39.8",
"@codemirror/view": "6.39.9",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.1.1",
"@formatjs/intl-displaynames": "7.1.1",
"@formatjs/intl-durationformat": "0.9.1",
"@formatjs/intl-getcanonicallocales": "3.1.1",
"@formatjs/intl-listformat": "8.1.1",
"@formatjs/intl-locale": "5.1.1",
"@formatjs/intl-numberformat": "9.1.1",
"@formatjs/intl-pluralrules": "6.1.1",
"@formatjs/intl-relativetimeformat": "12.1.1",
"@formatjs/intl-datetimeformat": "7.1.2",
"@formatjs/intl-displaynames": "7.1.2",
"@formatjs/intl-durationformat": "0.9.2",
"@formatjs/intl-getcanonicallocales": "3.1.2",
"@formatjs/intl-listformat": "8.1.2",
"@formatjs/intl-locale": "5.1.2",
"@formatjs/intl-numberformat": "9.1.2",
"@formatjs/intl-pluralrules": "6.1.2",
"@formatjs/intl-relativetimeformat": "12.1.2",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -112,13 +112,13 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.0.8",
"intl-messageformat": "11.0.9",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "3.3.1",
"lit-html": "3.3.1",
"lit": "3.3.2",
"lit-html": "3.3.2",
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
@@ -150,13 +150,13 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@bundle-stats/plugin-webpack-filter": "4.21.8",
"@lokalise/node-api": "15.6.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.4.0",
"@rspack/core": "1.7.0",
"@rspack/core": "1.7.1",
"@rspack/dev-server": "1.1.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -215,8 +215,8 @@
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.51.0",
"vite-tsconfig-paths": "6.0.3",
"typescript-eslint": "8.52.0",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.16",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
@@ -224,8 +224,8 @@
},
"resolutions": {
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "3.3.1",
"lit-html": "3.3.1",
"lit": "3.3.2",
"lit-html": "3.3.2",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",

View File

@@ -38,13 +38,11 @@ export class HaAuthFormString extends HaFormString {
}
</style>
<ha-auth-textfield
.type=${
!this.isPassword
.type=${!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"
}
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
@@ -55,18 +53,17 @@ export class HaAuthFormString extends HaFormString {
.name=${this.schema.name}
.autocomplete=${this.schema.autocomplete}
?autofocus=${this.schema.autofocus}
.suffix=${
this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix
}
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
.suffix=${this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required
? this.localize?.("ui.panel.page-authorize.form.error_required")
: undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
${this.renderIcon()}
</ha-auth-textfield>
></ha-auth-textfield>
${this.renderIcon()}
`;
}
}

View File

@@ -79,7 +79,7 @@ export const generateColorPalette = (
}
return steps.map((step) => {
const name = `color-${label}-${step}`;
const name = `ha-color-${label}-${step}`;
// Base color at 50%
if (step === 50) {

View File

@@ -93,8 +93,8 @@ export const calcDateRange = (
];
case "now-12m":
return [
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
calcDate(today, subMonths, hass.locale, hass.config, 12),
calcDate(today, subMonths, hass.locale, hass.config, 0),
];
case "now-1h":
return [

View File

@@ -0,0 +1,53 @@
/**
* ES5-compatible implementation of the keyed directive.
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
*
* This implementation avoids parameter destructuring in the update() method,
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
*
* Used only for ES5 builds (legacy browsers). Modern builds use the original
* lit-html keyed directive.
*
* @see https://github.com/home-assistant/frontend/issues/28732
*/
// eslint-disable-next-line import/extensions
import { directive, Directive } from "lit-html/directive.js";
// eslint-disable-next-line import/extensions
import { setCommittedValue } from "lit-html/directive-helpers.js";
// eslint-disable-next-line lit/no-legacy-imports
import { nothing } from "lit-html";
// eslint-disable-next-line import/extensions
import type { Part } from "lit-html/directive.js";
class KeyedES5 extends Directive {
private _key: unknown = nothing;
render(k: unknown, v: unknown) {
this._key = k;
return v;
}
update(part: unknown, args: [unknown, unknown]) {
const k = args[0];
const v = args[1];
if (k !== this._key) {
// Clear the part before returning a value. The one-arg form of
// setCommittedValue sets the value to a sentinel which forces a
// commit the next render.
setCommittedValue(part as Part);
this._key = k;
}
return v;
}
}
/**
* Associates a renderable value with a unique key. When the key changes, the
* previous DOM is removed and disposed before rendering the next value, even
* if the value - such as a template - is the same.
*
* This is useful for forcing re-renders of stateful components, or working
* with code that expects new data to generate new HTML elements, such as some
* animation techniques.
*/
export const keyed = directive(KeyedES5);

View File

@@ -21,6 +21,7 @@ import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { filterXSS } from "../../common/util/xss";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -184,7 +185,7 @@ export class StateHistoryChartLine extends LitElement {
}
if (param.seriesName) {
return `${param.marker} ${param.seriesName}: ${value}`;
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
}
return `${param.marker} ${value}`;
})

View File

@@ -1364,6 +1364,9 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell > * {
transition: var(--float-start) 0.2s ease;
}
.mdc-data-table__header-cell--numeric > span {
transition: none;
}
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
position: absolute;

View File

@@ -18,6 +18,7 @@ import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -94,7 +95,30 @@ export class HaDevicePicker extends LitElement {
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
private _getDevicesMemoized = memoizeOne(getDevices);
private _getDevicesMemoized = memoizeOne(
(
_devices: HomeAssistant["devices"],
configEntryLookup: Record<string, ConfigEntry>,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string
) =>
getDevices(
this.hass,
configEntryLookup,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeDevices,
value
)
);
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
@@ -110,7 +134,7 @@ export class HaDevicePicker extends LitElement {
private _getItems = () =>
this._getDevicesMemoized(
this.hass,
this.hass.devices,
this._configEntryLookup,
this.includeDomains,
this.excludeDomains,

View File

@@ -275,6 +275,11 @@ export class HaEntityNamePicker extends LitElement {
this._editIndex = idx;
await this.updateComplete;
await this._picker?.open();
const value = this._items[idx];
// Pre-fill the field value when editing a text item
if (value.type === "text" && value.text) {
this._picker?.setFieldValue(value.text);
}
}
private get _items(): EntityNameItem[] {

View File

@@ -143,17 +143,19 @@ export class HaEntityToggle extends LitElement {
// Optimistic update.
this._isOn = turnOn;
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
try {
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
} finally {
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
}
}
static styles = css`

View File

@@ -174,12 +174,14 @@ export class HaAutomationRow extends LitElement {
}
::slotted([slot="header"]) {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
margin: 0 var(--ha-space-3);
}
.icons {
display: flex;
align-items: center;
flex-shrink: 0;
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -51,7 +51,10 @@ export class HaCard extends LitElement {
font-weight: var(--ha-font-weight-normal);
}
:host ::slotted(.card-content:not(:first-child)),
:host
::slotted(
.card-content:not(:nth-child(1 of .card-content, .card-header))
),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0;
margin-top: calc(var(--ha-space-2) * -1);

View File

@@ -1,24 +0,0 @@
import type { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { HaTextField } from "./ha-textfield";
@customElement("ha-combo-box-textfield")
export class HaComboBoxTextField extends HaTextField {
@property({ type: Boolean, attribute: "force-blank-value" })
public forceBlankValue = false;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
if (this.forceBlankValue && this.value) {
this.value = "";
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-textfield": HaComboBoxTextField;
}
}

View File

@@ -1,9 +1,11 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -13,6 +15,8 @@ export interface HaDurationData {
milliseconds?: number;
}
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@@ -29,41 +33,80 @@ class HaDurationInput extends LitElement {
@property({ attribute: "enable-day", type: Boolean })
public enableDay = false;
@property({ attribute: "allow-negative", type: Boolean })
public allowNegative = false;
@property({ type: Boolean }) public disabled = false;
private _toggleNegative = false;
protected render(): TemplateResult {
return html`
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
<div class="row">
${this.allowNegative
? html`
<ha-button-toggle-group
size="small"
.buttons=${[
{ label: "+", iconPath: mdiPlusThick, value: "+" },
{ label: "-", iconPath: mdiMinusThick, value: "-" },
]}
.active=${this._negative ? "-" : "+"}
@value-changed=${this._negativeChanged}
></ha-button-toggle-group>
`
: nothing}
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
</div>
`;
}
private get _negative() {
return (
this._toggleNegative ||
(this.data?.days
? this.data.days < 0
: this.data?.hours
? this.data.hours < 0
: this.data?.minutes
? this.data.minutes < 0
: this.data?.seconds
? this.data.seconds < 0
: this.data?.milliseconds
? this.data.milliseconds < 0
: false)
);
}
private get _days() {
return this.data?.days
? Number(this.data.days)
? this.allowNegative
? Math.abs(Number(this.data.days))
: Number(this.data.days)
: this.required || this.data
? 0
: NaN;
@@ -71,7 +114,9 @@ class HaDurationInput extends LitElement {
private get _hours() {
return this.data?.hours
? Number(this.data.hours)
? this.allowNegative
? Math.abs(Number(this.data.hours))
: Number(this.data.hours)
: this.required || this.data
? 0
: NaN;
@@ -79,7 +124,9 @@ class HaDurationInput extends LitElement {
private get _minutes() {
return this.data?.minutes
? Number(this.data.minutes)
? this.allowNegative
? Math.abs(Number(this.data.minutes))
: Number(this.data.minutes)
: this.required || this.data
? 0
: NaN;
@@ -87,7 +134,9 @@ class HaDurationInput extends LitElement {
private get _seconds() {
return this.data?.seconds
? Number(this.data.seconds)
? this.allowNegative
? Math.abs(Number(this.data.seconds))
: Number(this.data.seconds)
: this.required || this.data
? 0
: NaN;
@@ -95,7 +144,9 @@ class HaDurationInput extends LitElement {
private get _milliseconds() {
return this.data?.milliseconds
? Number(this.data.milliseconds)
? this.allowNegative
? Math.abs(Number(this.data.milliseconds))
: Number(this.data.milliseconds)
: this.required || this.data
? 0
: NaN;
@@ -113,6 +164,14 @@ class HaDurationInput extends LitElement {
if ("days" in value) value.days ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (this.allowNegative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = Math.abs(value[t]);
}
});
}
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
@@ -135,12 +194,47 @@ class HaDurationInput extends LitElement {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
if (this._negative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = -Math.abs(value[t]);
}
});
}
}
fireEvent(this, "value-changed", {
value,
});
}
private _negativeChanged(ev) {
ev.stopPropagation();
const negative = (ev.detail?.value || ev.target.value) === "-";
this._toggleNegative = negative;
const value = this.data;
if (value) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
}
});
fireEvent(this, "value-changed", {
value,
});
}
}
static styles = css`
.row {
display: flex;
align-items: center;
}
ha-button-toggle-group {
margin: var(--ha-space-2);
}
`;
}
declare global {

View File

@@ -0,0 +1,198 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-label";
import "./ha-list";
import "./ha-list-item";
import "./voice-assistant-brand-icon";
import { voiceAssistants } from "../data/expose";
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
@customElement("ha-filter-voice-assistants")
export class HaFilterVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// the list of selected voiceAssistantIds
@property({ attribute: false }) public value: string[] = [];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@state() private _voiceAssistantOptions: string[] = [];
@state() private _shouldRender = false;
protected render() {
return html`
<ha-expansion-panel
left-chevron
.expanded=${this.expanded}
@expanded-will-change=${this._expandedWillChange}
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this.hass.localize(
"ui.panel.config.dashboard.voice_assistants.main"
)}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing}
</div>
${this._shouldRender
? html`<ha-list
@selected=${this._assistantsSelected}
class="ha-scrollbar"
multi
>
${repeat(
this._voiceAssistantOptions,
(voiceAssistantId) => voiceAssistantId,
(voiceAssistantId) =>
html`<ha-check-list-item
.value=${voiceAssistantId}
.selected=${(this.value || []).includes(voiceAssistantId)}
hasMeta
graphic="icon"
>
<voice-assistant-brand-icon
slot="graphic"
.voiceAssistantId=${voiceAssistantId}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
${voiceAssistants[voiceAssistantId].name}
</ha-check-list-item>`
)}
</ha-list> `
: nothing}
</ha-expansion-panel>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._voiceAssistantOptions = Object.keys(voiceAssistants);
}
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
}, 300);
}
}
private _expandedWillChange(ev) {
this._shouldRender = ev.detail.expanded;
}
private _expandedChanged(ev) {
this.expanded = ev.detail.expanded;
}
private async _assistantsSelected(
ev: CustomEvent<SelectedDetail<Set<number>>>
) {
if (!ev.detail.index) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const newvalue: string[] = [];
for (const index of ev.detail.index) {
newvalue.push(this._voiceAssistantOptions![index]);
}
this.value = newvalue;
fireEvent(this, "data-table-filter-changed", {
value: this.value,
items: undefined,
});
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = [];
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
position: relative;
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
}
ha-expansion-panel {
--ha-card-border-radius: var(--ha-border-radius-square);
--expansion-panel-content-padding: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
.add {
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-filter-voice-assistants": HaFilterVoiceAssistants;
}
}

View File

@@ -1,5 +1,21 @@
import type { Selector } from "../../data/selector";
import type { HaFormSchema } from "./types";
import type { HaFormData, HaFormSchema } from "./types";
const setDefaultValue = (
field: HaFormSchema,
value: HaFormData | undefined
) => {
if ("selector" in field && "choose" in field.selector) {
const firstChoice = Object.keys(field.selector.choose.choices)[0];
if (firstChoice) {
return {
active_choice: firstChoice,
[firstChoice]: value,
};
}
}
return value;
};
export const computeInitialHaFormData = (
schema: HaFormSchema[] | readonly HaFormSchema[]
@@ -10,9 +26,12 @@ export const computeInitialHaFormData = (
field.description?.suggested_value !== undefined &&
field.description?.suggested_value !== null
) {
data[field.name] = field.description.suggested_value;
data[field.name] = setDefaultValue(
field,
field.description.suggested_value
);
} else if ("default" in field) {
data[field.name] = field.default;
data[field.name] = setDefaultValue(field, field.default);
} else if (field.type === "expandable") {
const expandableData = computeInitialHaFormData(field.schema);
if (field.required || Object.keys(expandableData).length) {
@@ -108,6 +127,21 @@ export const computeInitialHaFormData = (
data[field.name] = {};
} else if ("state" in selector) {
data[field.name] = selector.state?.multiple ? [] : "";
} else if ("choose" in selector) {
const firstChoice = Object.keys(selector.choose.choices)[0];
if (!firstChoice) {
data[field.name] = {};
} else {
data[field.name] = {
active_choice: firstChoice,
[firstChoice]: computeInitialHaFormData([
{
name: firstChoice,
selector: selector.choose.choices[firstChoice].selector,
},
])[firstChoice],
};
}
} else {
throw new Error(
`Selector ${Object.keys(selector)[0]} not supported in initial form data`

View File

@@ -1,12 +1,19 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiPlaylistPlus } from "@mdi/js";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import {
css,
html,
LitElement,
nothing,
type CSSResultGroup,
type PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { throttle } from "../common/util/throttle";
import { PickerMixin } from "../mixins/picker-mixin";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
@@ -114,6 +121,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@state() private _openedNarrow = false;
@state() private _unknownValue = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
@@ -130,6 +139,25 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
private _unsubscribeTinyKeys?: () => void;
protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("value")) {
this._setUnknownValue();
return;
}
if (changedProperties.has("hass")) {
this._throttleUnknownValue();
}
}
public setFieldValue(value: string) {
if (this._comboBox) {
this._comboBox.setFieldValue(value);
return;
}
// Store initial value to set when opened
this._initialFieldValue = value;
}
protected render() {
// Only show label if it's not a top label and there is a value.
const label = this.useTopLabel && this.value ? undefined : this.label;
@@ -157,11 +185,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
type="button"
class=${this._opened ? "opened" : ""}
compact
.unknown=${this._unknownValue(
this.allowCustomValue,
this.value,
this.getItems()
)}
.unknown=${this._unknownValue}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)}
@click=${this.open}
@@ -182,40 +206,42 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
</ha-picker-field>`}
</slot>
</div>
${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
? html`
<wa-popover
.open=${this._pickerWrapperOpen}
style="--body-width: ${this._popoverWidth}px;"
without-arrow
distance="-4"
.placement=${this.popoverPlacement}
for="picker"
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._dialogOpened}
@wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.label || "Select option"}
>
${this._renderComboBox()}
</wa-popover>
`
: this._pickerWrapperOpen || this._opened
? html`<ha-bottom-sheet
flexcontent
.open=${this._pickerWrapperOpen}
@wa-after-show=${this._dialogOpened}
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.label || "Select option"}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>`
: nothing}
${this._pickerWrapperOpen || this._opened
? this._openedNarrow
? html`
<ha-bottom-sheet
flexcontent
.open=${this._pickerWrapperOpen}
@wa-after-show=${this._dialogOpened}
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.label || "Select option"}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>
`
: html`
<wa-popover
.open=${this._pickerWrapperOpen}
style="--body-width: ${this._popoverWidth}px;"
without-arrow
distance="-4"
.placement=${this.popoverPlacement}
for="picker"
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._dialogOpened}
@wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.label || "Select option"}
>
${this._renderComboBox()}
</wa-popover>
`
: nothing}
</div>
${this._renderHelper()}`;
}
@@ -248,26 +274,29 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
`;
}
private _unknownValue = memoizeOne(
(
allowCustomValue: boolean,
value?: string,
items?: (PickerComboBoxItem | string)[]
) => {
if (
allowCustomValue ||
value === undefined ||
value === null ||
value === "" ||
!items
) {
return false;
}
return !items.some(
(item) => typeof item !== "string" && item.id === value
);
private _setUnknownValue = () => {
const items = this.getItems();
if (
this.allowCustomValue ||
this.value === undefined ||
this.value === null ||
this.value === "" ||
!items
) {
this._unknownValue = false;
return;
}
this._unknownValue = !items.some(
(item) => typeof item !== "string" && item.id === this.value
);
};
private _throttleUnknownValue = throttle(
this._setUnknownValue,
1000,
true,
false
);
private _renderHelper() {
@@ -283,9 +312,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
</ha-input-helper-text>`;
}
private _initialFieldValue?: string;
private _dialogOpened = () => {
this._opened = true;
requestAnimationFrame(() => {
// Set initial field value if needed
if (this._initialFieldValue) {
this._comboBox?.setFieldValue(this._initialFieldValue);
this._initialFieldValue = undefined;
}
if (this.hass && isIosApp(this.hass)) {
this.hass.auth.external!.fireMessage({
type: "focus_element",
@@ -295,6 +331,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
});
return;
}
this._comboBox?.focus();
});
};
@@ -376,6 +413,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.container {
position: relative;
display: block;
max-width: 100%;
}
label[disabled] {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));

View File

@@ -153,6 +153,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@state() private _items: PickerComboBoxItem[] = [];
public setFieldValue(value: string) {
if (this._searchFieldElement) {
this._searchFieldElement.value = value;
}
}
protected get scrollableElement(): HTMLElement | null {
return this._virtualizerElement as HTMLElement | null;
}

View File

@@ -38,6 +38,13 @@ export class HaChooseSelector extends LitElement {
) {
this._setActiveChoice();
}
if (
changedProperties.has("value") &&
changedProperties.get("value")?.active_choice &&
changedProperties.get("value")?.active_choice !== this._activeChoice
) {
this._setActiveChoice();
}
}
protected render() {

View File

@@ -1,3 +1,4 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { DurationSelector } from "../../data/selector";
@@ -11,7 +12,10 @@ export class HaTimeDuration extends LitElement {
@property({ attribute: false }) public selector!: DurationSelector;
@property({ attribute: false }) public value?: HaDurationData;
@property({ attribute: false }) public value?:
| HaDurationData
| string
| number;
@property() public label?: string;
@@ -21,16 +25,47 @@ export class HaTimeDuration extends LitElement {
@property({ type: Boolean }) public required = true;
private _data = memoizeOne(
(value?: HaDurationData | string | number): HaDurationData | undefined => {
if (typeof value === "number") {
return { seconds: value };
}
if (typeof value === "string") {
const negative = value.trim()[0] === "-";
const parts = value
.split(":")
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
if (parts.length === 1) {
return { seconds: parts[0] };
}
if (parts.length === 2) {
return { hours: parts[0], minutes: parts[1] };
}
if (parts.length === 3) {
return {
hours: parts[0],
minutes: parts[1],
seconds: parts[2],
};
}
return undefined;
}
return value;
}
);
protected render() {
return html`
<ha-duration-input
.label=${this.label}
.helper=${this.helper}
.data=${this.value}
.data=${this._data(this.value)}
.disabled=${this.disabled}
.required=${this.required}
.enableDay=${this.selector.duration?.enable_day}
.enableMillisecond=${this.selector.duration?.enable_millisecond}
.allowNegative=${this.selector.duration?.allow_negative}
></ha-duration-input>
`;
}

View File

@@ -52,8 +52,6 @@ import "./ha-spinner";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
const SORT_VALUE_URL_PATHS = {
energy: 1,
map: 2,
@@ -344,17 +342,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
if (oldHass?.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".selected");
if (selectedEl) {
// @ts-ignore
selectedEl.scrollIntoViewIfNeeded();
}
}
}
private _calculateCounts = throttle(() => {

View File

@@ -57,6 +57,7 @@ export class HaSlider extends Slider {
#thumb {
border: none;
background-color: var(--ha-slider-thumb-color, var(--primary-color));
overflow: hidden;
}
#thumb:after {

View File

@@ -1,4 +1,5 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import {
@@ -49,7 +50,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
@@ -114,6 +114,8 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@state()
private _bodyScrolled = false;
private _escapePressed = false;
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
@@ -139,6 +141,8 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
)}
aria-describedby=${ifDefined(this.ariaDescribedBy)}
@keydown=${this._handleKeyDown}
@wa-hide=${this._handleHide}
@wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide}
@@ -208,9 +212,11 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "after-show");
};
private _handleAfterHide = () => {
this._open = false;
fireEvent(this, "closed");
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
if (ev.eventPhase === Event.AT_TARGET) {
this._open = false;
fireEvent(this, "closed");
}
};
public disconnectedCallback(): void {
@@ -223,6 +229,23 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
}
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
this._escapePressed = true;
}
}
private _handleHide(ev: CustomEvent<{ source: Element }>) {
if (
this.preventScrimClose &&
this._escapePressed &&
ev.detail.source === (ev.target as WaDialog).dialog
) {
ev.preventDefault();
}
this._escapePressed = false;
}
static get styles() {
return [
...super.styles,

View File

@@ -24,6 +24,7 @@ import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
@@ -381,7 +382,7 @@ export class HaMap extends ReactiveElement {
this.hass.config
);
}
return `${path.name}<br>${formattedTime}`;
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
}
private _drawPaths(): void {
@@ -549,7 +550,7 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
el.textContent = title;
iconHTML = el.outerHTML;
}

View File

@@ -1,7 +1,6 @@
import type { ActionDetail } from "@material/mwc-list";
import {
mdiAlphaABoxOutline,
mdiArrowLeft,
mdiClose,
mdiDotsVertical,
mdiGrid,
@@ -21,9 +20,10 @@ import type {
} from "../../data/media-player";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import "../ha-wa-dialog";
import "../ha-dialog-header";
import "../ha-list-item";
import "../ha-icon-button-arrow-prev";
import "./ha-media-manage-button";
import "./ha-media-player-browse";
import type {
@@ -44,6 +44,8 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
@state() private _open = false;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
@@ -54,9 +56,11 @@ class DialogMediaPlayerBrowse extends LitElement {
media_content_type: undefined,
},
];
this._open = true;
}
public closeDialog() {
this._open = false;
this._params = undefined;
this._navigateIds = undefined;
this._currentItem = undefined;
@@ -71,28 +75,20 @@ class DialogMediaPlayerBrowse extends LitElement {
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
flexContent
.heading=${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
@closed=${this.closeDialog}
@opened=${this._dialogOpened}
>
<ha-dialog-header show-border slot="heading">
<ha-dialog-header show-border slot="header">
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
? html`
<ha-icon-button
<ha-icon-button-arrow-prev
slot="navigationIcon"
.path=${mdiArrowLeft}
@click=${this._goBack}
></ha-icon-button>
></ha-icon-button-arrow-prev>
`
: nothing}
<span slot="title">
@@ -153,7 +149,7 @@ class DialogMediaPlayerBrowse extends LitElement {
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
data-dialog="close"
slot="actionItems"
></ha-icon-button>
</ha-dialog-header>
@@ -173,7 +169,7 @@ class DialogMediaPlayerBrowse extends LitElement {
@media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed}
></ha-media-player-browse>
</ha-dialog>
</ha-wa-dialog>
`;
}
@@ -225,8 +221,7 @@ class DialogMediaPlayerBrowse extends LitElement {
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-z-index: 9;
ha-wa-dialog {
--dialog-content-padding: 0;
}
@@ -241,9 +236,9 @@ class DialogMediaPlayerBrowse extends LitElement {
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-max-height: calc(
ha-wa-dialog {
--ha-dialog-max-width: 800px;
--ha-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
}

View File

@@ -1,14 +1,15 @@
import { type CSSResultGroup, LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiSpeaker } from "@mdi/js";
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../../types";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeRTL } from "../../common/util/compute_rtl";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-switch";
import "../ha-svg-icon";
import type { MediaPlayerEntity } from "../../data/media-player";
@customElement("ha-media-player-toggle")
class HaMediaPlayerToggle extends LitElement {
@@ -20,15 +21,61 @@ class HaMediaPlayerToggle extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _computeDisplayData = memoizeOne(
(
entityId: string,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
isRTL: boolean,
stateObj: HomeAssistant["states"][string]
) => {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
entities,
devices,
areas,
floors
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return { primary, secondary };
}
);
protected render() {
const stateObj = this.hass.states[this.entityId];
let icon = mdiSpeaker;
if (stateObj.state === "playing") {
icon = mdiSpeakerPlay;
} else if (stateObj.state === "paused") {
icon = mdiSpeakerPause;
}
const isRTL = computeRTL(this.hass);
const { primary, secondary } = this._computeDisplayData(
this.entityId,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors,
isRTL,
stateObj
);
return html`<div class="list-item">
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
<ha-svg-icon .path=${icon}></ha-svg-icon>
<div class="info">
<div class="main-text">${computeStateName(stateObj)}</div>
<div class="secondary-text">
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
</div>
<div class="main-text">${primary}</div>
<div class="secondary-text">${secondary}</div>
</div>
<ha-switch
.disabled=${this.disabled}
@@ -38,16 +85,6 @@ class HaMediaPlayerToggle extends LitElement {
</div>`;
}
private _formatSecondaryText(stateObj: MediaPlayerEntity): string {
if (stateObj.state !== "playing") {
return this.hass.localize("ui.card.media_player.idle");
}
return [stateObj.attributes.media_title, stateObj.attributes.media_artist]
.filter((segment) => segment)
.join(" · ");
}
static get styles(): CSSResultGroup {
return [
css`

View File

@@ -0,0 +1,51 @@
import { customElement, property } from "lit/decorators";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { voiceAssistants } from "../data/expose";
import { brandsUrl } from "../util/brands-url";
@customElement("voice-assistant-brand-icon")
export class VoiceAssistantBrandicon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public voiceAssistantId!: string;
protected render() {
return html`
<img
class="logo"
alt=${voiceAssistants[this.voiceAssistantId].name}
src=${brandsUrl({
domain: voiceAssistants[this.voiceAssistantId].domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.logo {
position: relative;
height: 24px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"voice-assistant-brand-icon": VoiceAssistantBrandicon;
}
}

View File

@@ -69,6 +69,7 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
current_humidity: "%",
min_humidity: "%",
max_humidity: "%",
target_humidity_step: "%",
},
light: {
color_temp: "mired",

View File

@@ -1,4 +1,6 @@
import type { HomeAssistant } from "../types";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import { entityRegistryByEntityId } from "./entity/entity_registry";
export const voiceAssistants = {
conversation: { domain: "assist_pipeline", name: "Assist" },
@@ -52,3 +54,13 @@ export const listExposedEntities = (hass: HomeAssistant) =>
hass.callWS<{ exposed_entities: Record<string, ExposeEntitySettings> }>({
type: "homeassistant/expose_entity/list",
});
export const getEntityVoiceAssistantsIds = (
entityRegistry: EntityRegistryEntry[],
entityId: string
) => {
const entity = entityRegistryByEntityId(entityRegistry)[entityId];
return Object.keys(voiceAssistants).filter(
(vaKey) => entity?.options?.[vaKey]?.should_expose
);
};

View File

@@ -16,6 +16,7 @@ export type HumidifierEntity = HassEntityBase & {
mode?: string;
action?: HumidifierAction;
available_modes?: string[];
target_humidity_step?: number;
};
};

View File

@@ -52,6 +52,9 @@ export interface BaseActionConfig {
export interface ConfirmationRestrictionConfig {
text?: string;
title?: string;
confirm_text?: string;
dismiss_text?: string;
exemptions?: RestrictionConfig[];
}

View File

@@ -49,6 +49,7 @@ export interface LovelaceBaseViewConfig {
title?: string;
path?: string;
icon?: string;
show_icon_and_title?: boolean;
theme?: string;
panel?: boolean;
background?: string | LovelaceViewBackgroundConfig;

View File

@@ -221,6 +221,7 @@ export interface DurationSelector {
duration: {
enable_day?: boolean;
enable_millisecond?: boolean;
allow_negative?: boolean;
} | null;
}

View File

@@ -44,14 +44,27 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
entity.attributes.update_percentage !== null;
export const updateAvailable = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version));
export const updateCanInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
updateAvailable(entity, showSkipped) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateCanNotInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
updateAvailable(entity, showSkipped) &&
!supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
!!(
entity.attributes.latest_version &&
@@ -108,13 +121,17 @@ export const filterUpdateEntities = (
);
});
export const filterUpdateEntitiesWithInstall = (
export const filterUpdateEntitiesParameterized = (
entities: HassEntities,
showSkipped = false
showSkipped = false,
showNotInstallable = false
) =>
filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity, showSkipped)
);
filterUpdateEntities(entities).filter((entity) => {
if (showNotInstallable) {
return updateCanNotInstall(entity, showSkipped);
}
return updateCanInstall(entity, showSkipped);
});
export const checkForEntityUpdates = async (
element: HTMLElement,

View File

@@ -766,7 +766,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
}
.content-wrapper.settings-view .fade-bottom {
bottom: var(--ha-space-18);
bottom: calc(
var(--ha-space-14) +
max(var(--safe-area-inset-bottom), var(--ha-space-4))
);
}
.child-view {

View File

@@ -1,4 +1,3 @@
import { mdiAppleKeyboardCommand } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
@@ -154,6 +153,10 @@ const _SHORTCUTS: Section[] = [
shortcut: ["M"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
},
{
shortcut: ["Shift", "/"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.show_shortcuts",
},
],
},
];
@@ -184,9 +187,7 @@ class DialogShortcuts extends LitElement {
html`<span
>${shortcutKey === CTRL_CMD
? isMac
? html`<ha-svg-icon
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
? "⌘"
: this.hass.localize("ui.panel.config.automation.editor.ctrl")
: typeof shortcutKey === "string"
? shortcutKey

View File

@@ -152,7 +152,7 @@ export const provideHass = (
for (const ent of ensureArray(newEntities)) {
hass().entities[ent.entityId] = {
entity_id: ent.entityId,
name: ent.name,
name: ent.attributes.friendly_name || null,
icon: ent.icon,
platform: "demo",
labels: [],

View File

@@ -50,7 +50,7 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
/**
* Safe area padding in pixels for the scrollable element.
*/
protected scrollFadeSafeAreaPadding = 16;
protected scrollFadeSafeAreaPadding = 4;
/**
* Scroll threshold in pixels for showing the fades.
@@ -73,6 +73,9 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated?.(changedProperties);
if (this.scrollableElement) {
this._updateScrollableState(this.scrollableElement);
}
this._attachScrollableElement();
}
@@ -83,6 +86,8 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
disconnectedCallback() {
this._detachScrollableElement();
this._contentScrolled = false;
this._contentScrollable = false;
super.disconnectedCallback();
}
@@ -125,16 +130,16 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
position: absolute;
left: 0;
right: 0;
height: var(--ha-space-4);
height: var(--ha-space-2);
pointer-events: none;
transition: opacity 180ms ease-in-out;
background: linear-gradient(
to bottom,
var(--shadow-color),
transparent
);
border-radius: var(--ha-border-radius-square);
opacity: 0;
background: linear-gradient(
to bottom,
var(--ha-color-shadow-scrollable-fade),
transparent
);
}
.fade-top {
top: 0;

View File

@@ -157,7 +157,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
`
: nothing}
${this._visibleOptionals.includes("description")
? html` <ha-textarea
? html`<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
@@ -168,6 +168,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
autogrow
.value=${this._newDescription}
.helper=${supportsMarkdownHelper(this.hass.localize)}
helperPersistent
@input=${this._valueChanged}
></ha-textarea>`
: nothing}
@@ -570,7 +571,7 @@ ${dump(this._params.config)}
ha-category-picker,
ha-labels-picker,
ha-area-picker,
ha-chip-set {
ha-chip-set:has(> ha-assist-chip) {
margin-top: 16px;
}
ha-alert {

View File

@@ -57,6 +57,7 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
@@ -115,6 +116,8 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type AutomationItem = AutomationEntity & {
name: string;
@@ -376,6 +379,31 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-icon-button>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.automation.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (automation) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
automation.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) =>
html` <voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>`
)
: "—"}`;
},
},
};
return columns;
}
@@ -633,6 +661,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
<ha-filter-blueprints
.hass=${this.hass}
.type=${"automation"}
@@ -1003,8 +1040,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
}
if (
} else if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -1026,6 +1062,29 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.automations
.filter((automation) =>
getEntityVoiceAssistantsIds(
this._entityReg,
automation.entity_id
).some((va) => (filter.value as string[]).includes(va))
)
.forEach((automation) => assistItems.add(automation.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredAutomations = items ? [...items] : undefined;

View File

@@ -206,8 +206,8 @@ class HaBlueprintOverview extends LitElement {
sortable: true,
valueColumn: "usageCount",
type: "numeric",
minWidth: "100px",
maxWidth: "120px",
minWidth: "90px",
maxWidth: "90px",
template: (blueprint) => {
const count = blueprint.usageCount ?? 0;
return html`

View File

@@ -1,19 +1,16 @@
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import {
mdiDotsVertical,
mdiLocationEnter,
mdiLocationExit,
mdiRefresh,
} from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import type {
HassioSupervisorInfo,
@@ -26,13 +23,16 @@ import {
} from "../../../data/hassio/supervisor";
import {
checkForEntityUpdates,
filterUpdateEntitiesWithInstall,
filterUpdateEntitiesParameterized,
} from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@@ -53,7 +53,11 @@ class HaConfigSectionUpdates extends LitElement {
}
protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
const canInstallUpdates = this._filterInstallableUpdateEntities(
this.hass.states,
this._showSkipped
);
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
this.hass.states,
this._showSkipped
);
@@ -73,57 +77,86 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-button-menu multi>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-check-list-item
left
@request-selected=${this._toggleSkipped}
.selected=${this._showSkipped}
<ha-dropdown-item
type="checkbox"
.checked=${this._showSkipped}
value="show_skipped"
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-check-list-item>
</ha-dropdown-item>
${this._supervisorInfo
? html`
<li divider role="separator"></li>
<ha-list-item
@request-selected=${this._toggleBeta}
<wa-divider></wa-divider>
<ha-dropdown-item
value="toggle_beta"
.disabled=${this._supervisorInfo.channel === "dev"}
>
<ha-svg-icon
.path=${this._supervisorInfo.channel === "stable"
? mdiLocationEnter
: mdiLocationExit}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.updates.${this._supervisorInfo.channel === "stable" ? "join" : "leave"}_beta`
)}
${this._supervisorInfo.channel === "stable"
? this.hass.localize("ui.panel.config.updates.join_beta")
: this.hass.localize(
"ui.panel.config.updates.leave_beta"
)}
</ha-list-item>
</ha-dropdown-item>
`
: ""}
</ha-button-menu>
: nothing}
</ha-dropdown>
</div>
<div class="content">
<ha-card outlined>
<div class="card-content">
${canInstallUpdates.length
? html`
${canInstallUpdates.length
? html`
<ha-card outlined>
<div class="card-content">
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
showAll
></ha-config-updates>
`
: html`
<div class="no-updates">
${this.hass.localize(
"ui.panel.config.updates.no_updates"
)}
</div>
`}
</div>
</ha-card>
</div>
</ha-card>
`
: nothing}
${notInstallableUpdates.length
? html`
<ha-card outlined>
<div class="card-content">
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${notInstallableUpdates}
.isInstallable=${false}
showAll
></ha-config-updates>
</div>
</ha-card>
`
: nothing}
${canInstallUpdates.length + notInstallableUpdates.length
? nothing
: html`
<ha-card outlined>
<div class="no-updates">
${this.hass.localize("ui.panel.config.updates.no_updates")}
</div>
</ha-card>
`}
</div>
</hass-subpage>
`;
@@ -133,27 +166,19 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
}
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
private _handleOverflowAction(
ev: CustomEvent<{ item: { value: string } }>
): void {
if (ev.detail.item.value === "toggle_beta") {
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
} else if (ev.detail.item.value === "show_skipped") {
this._showSkipped = !this._showSkipped;
}
}
@@ -177,9 +202,14 @@ class HaConfigSectionUpdates extends LitElement {
checkForEntityUpdates(this, this.hass);
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
private _filterInstallableUpdateEntities = memoizeOne(
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesWithInstall(entities, showSkipped)
filterUpdateEntitiesParameterized(entities, showSkipped, false)
);
private _filterNotInstallableUpdateEntities = memoizeOne(
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesParameterized(entities, showSkipped, true)
);
static styles = css`

View File

@@ -31,7 +31,7 @@ import {
import type { UpdateEntity } from "../../../data/update";
import {
checkForEntityUpdates,
filterUpdateEntitiesWithInstall,
filterUpdateEntitiesParameterized,
} from "../../../data/update";
import {
QuickBarMode,
@@ -161,24 +161,27 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
configSections.dashboard_2,
configSections.dashboard_3,
]);
private _pages = memoizeOne(
(cloudStatus, isCloudLoaded, hasExternalSettings) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
hasExternalSettings ? configSections.dashboard_external_settings : [],
configSections.dashboard_2,
configSections.dashboard_3,
]
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -203,7 +206,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesWithInstall(
this._filterUpdateEntitiesParameterized(
this.hass.states,
this.hass.entities
);
@@ -288,6 +291,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
@@ -310,7 +314,8 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
).map((categoryPages) =>
categoryPages.length === 0
? nothing
@@ -344,14 +349,16 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
showShortcutsDialog(this);
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
private _filterUpdateEntitiesParameterized = memoizeOne(
(
entities: HomeAssistant["states"],
entityRegistry: HomeAssistant["entities"]
): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesWithInstall(entities).filter(
(entity) => !entityRegistry[entity.entity_id]?.hidden
);
const updates = filterUpdateEntitiesParameterized(
entities,
false,
false
).filter((entity) => !entityRegistry[entity.entity_id]?.hidden);
return {
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),

View File

@@ -32,6 +32,8 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
@property({ type: Number }) public total?: number;
@property({ attribute: false }) public isInstallable = true;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _entities?: EntityRegistryEntry[];
@@ -89,9 +91,16 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
return html`
<div class="title" role="heading" aria-level="2">
${this.hass.localize("ui.panel.config.updates.title", {
count: this.total || this.updateEntities.length,
})}
${this.isInstallable
? this.hass.localize("ui.panel.config.updates.title", {
count: this.total || this.updateEntities.length,
})
: this.hass.localize(
"ui.panel.config.updates.title_not_installable",
{
count: this.total || this.updateEntities.length,
}
)}
</div>
<ha-md-list>
${updates.map((entity) => {

View File

@@ -64,6 +64,7 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
@@ -115,6 +116,8 @@ import { isHelperDomain } from "../helpers/const";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
export interface StateEntity extends Omit<
EntityRegistryEntry,
@@ -493,6 +496,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
template: (entry) =>
entry.label_entries.map((lbl) => lbl.name).join(" "),
},
voice_assistants: {
title: localize(
"ui.panel.config.entities.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (entry) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entities,
entry.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) =>
html` <voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>`
)
: "—"}`;
},
},
})
);
@@ -637,6 +665,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) => (filter as string[]).includes(lbl))
);
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter) &&
filter.length
) {
filteredEntities = filteredEntities.filter((entity) =>
getEntityVoiceAssistantsIds(this._entities, entity.entity_id).some(
(va) => (filter as string[]).includes(va)
)
);
}
});
@@ -1076,6 +1114,15 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${
includeAddDeviceFab
? html`<ha-fab
@@ -1128,6 +1175,7 @@ ${
const subEntry = this._searchParms.get("sub_entry");
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!domain && !configEntry && !label && !device) {
return;
@@ -1140,6 +1188,7 @@ ${
"ha-filter-integrations": domain ? [domain] : [],
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
config_entry: configEntry ? [configEntry] : [],
sub_entry: subEntry ? [subEntry] : [],
};

View File

@@ -105,7 +105,24 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#3263C3",
},
],
dashboard_external_settings: [
{
path: "#external-app-configuration",
translationKey: "companion",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
],
dashboard_2: [
{
path: "/config/matter",
name: "Matter",
iconPath:
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
},
{
path: "/config/zha",
name: "Zigbee",
@@ -173,12 +190,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#5A87FA",
component: ["person", "users"],
},
{
path: "#external-app-configuration",
translationKey: "companion",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
{
path: "/config/system",
translationKey: "system",

View File

@@ -260,8 +260,6 @@ export class DialogHelperDetail extends LitElement {
open
@closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })}
scrimClickAction
escapeKeyAction
.hideActions=${!this._domain}
.heading=${createCloseHeading(
this.hass,

View File

@@ -105,7 +105,7 @@ class HaTimerForm extends LitElement {
<ha-checkbox
.configValue=${"restore"}
.checked=${this._restore}
@click=${this._toggleRestore}
@change=${this._toggleRestore}
.disabled=${this.disabled}
>
</ha-checkbox>
@@ -135,11 +135,8 @@ class HaTimerForm extends LitElement {
});
}
private _toggleRestore() {
if (this.disabled) {
return;
}
this._restore = !this._restore;
private _toggleRestore(ev) {
this._restore = ev.target.checked;
fireEvent(this, "value-changed", {
value: { ...this._item, restore: this._restore },
});

View File

@@ -51,6 +51,7 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -122,6 +123,8 @@ import "../integrations/ha-integration-overflow-menu";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { isHelperDomain, type HelperDomain } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
interface HelperItem {
id: string;
@@ -205,7 +208,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@state() private _stateItems: HassEntity[] = [];
@state() private _helperEntities: HassEntity[] = [];
@state() private _disabledEntityEntries?: EntityRegistryEntry[];
@@ -223,6 +226,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _diagnosticHandlers?: Record<string, boolean>;
@state() private _searchParms = new URLSearchParams(window.location.search);
@storage({
storage: "sessionStorage",
key: "helpers-table-filters",
@@ -245,7 +250,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _filteredStateItems?: string[] | null;
@state() private _filteredHelperEntityIds?: string[] | null;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -480,6 +485,32 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.helpers.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (helper) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
helper.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) => html`
<voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>
`
)
: "—"}`;
},
},
})
);
@@ -610,7 +641,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
if (
!this.hass ||
this._stateItems === undefined ||
this._helperEntities === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
@@ -685,14 +716,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const helpers = this._getItems(
this.hass.localize,
this._stateItems,
this._helperEntities,
this._disabledEntityEntries || [],
this._entityEntries,
this._configEntries,
this._entityReg,
this._categories,
this._labels,
this._filteredStateItems
this._filteredHelperEntityIds
);
return html`
<hass-tabs-subpage-data-table
@@ -779,6 +810,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -941,7 +981,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
filter.length
) {
const labelItems = new Set<string>();
this._stateItems
this._helperEntities
.filter((stateItem) =>
entityRegistryByEntityId(this._entityReg)[
stateItem.entity_id
@@ -960,14 +1000,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
}
if (
} else if (
key === "ha-filter-categories" &&
Array.isArray(filter) &&
filter.length
) {
const categoryItems = new Set<string>();
this._stateItems
this._helperEntities
.filter(
(stateItem) =>
filter[0] ===
@@ -987,10 +1026,85 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter) &&
filter.length
) {
const assistItems = new Set<string>();
this._helperEntities
.filter((stateItem) =>
getEntityVoiceAssistantsIds(
this._entityReg,
stateItem.entity_id
).some((va) => (filter as string[]).includes(va))
)
.forEach((stateItem) => assistItems.add(stateItem.entity_id));
(this._disabledEntityEntries || [])
.filter((entry) =>
getEntityVoiceAssistantsIds(this._entityReg, entry.entity_id).some(
(va) => (filter as string[]).includes(va)
)
)
.forEach((entry) => assistItems.add(entry.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredHelperEntityIds = items ? [...items] : undefined;
}
this._filteredStateItems = items ? [...items] : undefined;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
window.addEventListener("popstate", this._popState);
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("location-changed", this._locationChanged);
window.removeEventListener("popstate", this._popState);
}
private _locationChanged = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
}
};
private _popState = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
}
};
private _setFiltersFromUrl() {
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const category = this._searchParms.get("category");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!category && !label && !device) {
return;
}
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-categories": category ? [category] : [],
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
};
}
private _clearFilter() {
@@ -1093,7 +1207,7 @@ ${rejected
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._setFiltersFromUrl();
this._fetchEntitySources();
if (isComponentLoaded(this.hass, "diagnostics")) {
@@ -1206,6 +1320,10 @@ ${rejected
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._setFiltersFromUrl();
}
if (!this._entityEntries || !this._configEntries) {
return;
}
@@ -1225,7 +1343,7 @@ ${rejected
}
let changed =
!this._stateItems ||
!this._helperEntities ||
changedProps.has("_entityEntries") ||
changedProps.has("_configEntries") ||
changedProps.has("_entitySource");
@@ -1240,17 +1358,17 @@ ${rejected
const entityIds = Object.keys(this._entitySource);
const newStates = Object.values(this.hass!.states).filter(
const newHelpers = Object.values(this.hass!.states).filter(
(entity) =>
entityIds.includes(entity.entity_id) ||
isHelperDomain(computeStateDomain(entity))
);
if (
this._stateItems.length !== newStates.length ||
!this._stateItems.every((val, idx) => newStates[idx] === val)
this._helperEntities.length !== newHelpers.length ||
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
) {
this._stateItems = newStates;
this._helperEntities = newHelpers;
}
}

View File

@@ -327,7 +327,6 @@ class AddIntegrationDialog extends LitElement {
return html`<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
hideActions
.heading=${createCloseHeading(
this.hass,

View File

@@ -23,7 +23,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { isDevVersion } from "../../../common/config/version";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
@@ -213,10 +212,7 @@ class HaConfigEntryRow extends LitElement {
? html`<ha-button slot="end" @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</ha-button>`
: configPanel &&
(item.domain !== "matter" ||
isDevVersion(this.hass.config.version)) &&
!stateText
: configPanel && !stateText
? html`<a
slot="end"
href=${`/${configPanel}?config_entry=${item.entry_id}`}

View File

@@ -1,11 +1,19 @@
import { mdiAlertCircle, mdiCheckCircle, mdiPlus } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type { HomeAssistant } from "../../../../../types";
import {
acceptSharedMatterDevice,
canCommissionMatterExternal,
@@ -18,7 +26,6 @@ import {
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@customElement("matter-config-dashboard")
export class MatterConfigDashboard extends LitElement {
@@ -26,6 +33,8 @@ export class MatterConfigDashboard extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _configEntry?: ConfigEntry;
@state() private _error?: string;
private _unsub?: UnsubscribeFunc;
@@ -35,10 +44,33 @@ export class MatterConfigDashboard extends LitElement {
this._stopRedirect();
}
protected render(): TemplateResult {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchConfigEntry();
}
}
private _matterDeviceCount = memoizeOne(
(devices: HomeAssistant["devices"]): number =>
Object.values(devices).filter((device) =>
device.identifiers.some((identifier) => identifier[0] === "matter")
).length
);
protected render(): TemplateResult | typeof nothing {
if (!this._configEntry) {
return nothing;
}
const isOnline = this._configEntry.state === "loaded";
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
${isComponentLoaded(this.hass, "otbr")
<hass-subpage
.narrow=${this.narrow}
.hass=${this.hass}
header="Matter"
has-fab
>
${isComponentLoaded(this.hass, "thread")
? html`
<ha-button
appearance="plain"
@@ -51,53 +83,114 @@ export class MatterConfigDashboard extends LitElement {
)}</ha-button
>
`
: ""}
<div class="content">
<ha-card header="Matter">
<ha-alert alert-type="warning"
>${this.hass.localize(
"ui.panel.config.matter.panel.experimental_note"
)}</ha-alert
>
: nothing}
<div class="container">
<ha-card class="network-status">
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
<div class="heading">
<div class="icon">
<ha-svg-icon
.path=${isOnline ? mdiCheckCircle : mdiAlertCircle}
class=${isOnline ? "online" : "offline"}
></ha-svg-icon>
</div>
<div class="details">
Matter
${this.hass.localize(
"ui.panel.config.matter.panel.status_title"
)}:
${this.hass.localize(
`ui.panel.config.matter.panel.status_${isOnline ? "online" : "offline"}`
)}<br />
<small>
${this.hass.localize(
"ui.panel.config.matter.panel.devices",
{ count: this._matterDeviceCount(this.hass.devices) }
)}
</small>
</div>
</div>
</div>
<div class="card-actions">
${canCommissionMatterExternal(this.hass)
? html`<ha-button
appearance="plain"
@click=${this._startMobileCommissioning}
>${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}</ha-button
>`
: ""}
<ha-button appearance="plain" @click=${this._commission}
>${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}</ha-button
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
appearance="plain"
size="small"
>
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
>${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setWifi}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setThread}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}</ha-button
${this.hass.localize("ui.panel.config.devices.caption")}
</ha-button>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
${this.hass.localize("ui.panel.config.entities.caption")}
</ha-button>
</div>
</ha-card>
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_title"
)}
.secondary=${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_description"
)}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="dev-tools-content">
<p>
${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_info"
)}
</p>
<div class="dev-tools-actions">
${canCommissionMatterExternal(this.hass)
? html`<ha-button
appearance="plain"
@click=${this._startMobileCommissioning}
>${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}</ha-button
>`
: nothing}
<ha-button appearance="plain" @click=${this._commission}
>${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
>${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setWifi}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setThread}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}</ha-button
>
</div>
</div>
</ha-expansion-panel>
</div>
<a href="/config/matter/add" slot="fab">
<ha-fab
.label=${this.hass.localize(
"ui.panel.config.matter.panel.add_device"
)}
extended
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>
</hass-subpage>
`;
}
@@ -236,27 +329,101 @@ export class MatterConfigDashboard extends LitElement {
}
}
static styles = [
haStyle,
css`
ha-alert[alert-type="warning"] {
position: relative;
top: -16px;
}
.content {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card:first-child {
margin-bottom: 16px;
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
`,
];
private async _fetchConfigEntry(): Promise<void> {
const configEntries = await getConfigEntries(this.hass, {
domain: "matter",
});
if (configEntries.length) {
this._configEntry = configEntries[0];
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin: auto;
margin-top: var(--ha-space-4);
max-width: 500px;
}
ha-card .card-actions {
display: flex;
justify-content: flex-end;
}
.network-status div.heading {
display: flex;
align-items: center;
}
.network-status div.heading .icon {
margin-inline-end: var(--ha-space-4);
}
.network-status div.heading ha-svg-icon {
--mdc-icon-size: 48px;
}
.network-status div.heading .details {
font-size: var(--ha-font-size-xl);
}
.network-status small {
font-size: var(--ha-font-size-m);
}
.network-status .online {
color: var(--state-on-color, var(--success-color));
}
.network-status .offline {
color: var(--error-color, var(--error-color));
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
ha-expansion-panel {
margin: auto;
margin-top: var(--ha-space-4);
max-width: 500px;
background: var(--card-background-color);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
--expansion-panel-summary-padding: var(--ha-space-2) var(--ha-space-4);
--expansion-panel-content-padding: 0 var(--ha-space-4);
}
.dev-tools-content {
padding: 0 0 var(--ha-space-4);
}
.dev-tools-content p {
margin: 0 0 var(--ha-space-4);
color: var(--primary-text-color);
}
.dev-tools-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--ha-space-2);
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
a[slot="fab"] {
text-decoration: none;
}
`,
];
}
}
declare global {

View File

@@ -45,9 +45,9 @@ class DialogThreadDataset extends LitElement implements HassDialog {
<div>
Network name: ${dataset.network_name}<br />
Channel: ${dataset.channel}<br />
Dataset id: ${dataset.dataset_id}<br />
Pan id: ${dataset.pan_id}<br />
Extended Pan id: ${dataset.extended_pan_id}<br />
Dataset ID: ${dataset.dataset_id}<br />
PAN ID: ${dataset.pan_id}<br />
Extended PAN ID: ${dataset.extended_pan_id}<br />
${hasOTBR
? html`OTBR URL: ${otbrInfo.url}<br />

View File

@@ -372,7 +372,7 @@ export class HaConfigLogs extends LitElement {
@media all and (max-width: 870px) {
ha-generic-picker {
max-width: 50%;
max-width: max(30%, 160px);
}
ha-button {
max-width: 100%;

View File

@@ -51,6 +51,7 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -107,6 +108,8 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type SceneItem = SceneEntity & {
name: string;
@@ -410,6 +413,31 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.scene.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (scene) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
scene.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) =>
html` <voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>`
)
: "—"}`;
},
},
};
return columns;
@@ -652,6 +680,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -887,8 +924,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
}
if (
} else if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -910,6 +946,28 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.scenes
.filter((scene) =>
getEntityVoiceAssistantsIds(this._entityReg, scene.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((scene) => assistItems.add(scene.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredScenes = items ? [...items] : undefined;

View File

@@ -53,6 +53,7 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -111,6 +112,8 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type ScriptItem = ScriptEntity & {
name: string;
@@ -398,8 +401,32 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.script.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (script) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
script.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) =>
html` <voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>`
)
: "—"}`;
},
},
};
return columns;
}
);
@@ -635,6 +662,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
<ha-filter-blueprints
.hass=${this.hass}
.type=${"script"}
@@ -893,8 +929,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
}
if (
} else if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -916,6 +951,28 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.scripts
.filter((script) =>
getEntityVoiceAssistantsIds(this._entityReg, script.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((script) => assistItems.add(script.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredScripts = items ? [...items] : undefined;

View File

@@ -23,7 +23,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
render() {
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
return html`
<div class="container" id="container">
<img

View File

@@ -21,6 +21,8 @@ class EventSubscribeCard extends LitElement {
@state() private _subscribed?: () => void;
@state() private _eventFilter = "";
@state() private _events: {
id: number;
event: HassEvent;
@@ -30,6 +32,8 @@ class EventSubscribeCard extends LitElement {
private _eventCount = 0;
@state() _ignoredEventsCount = 0;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._subscribed) {
@@ -70,6 +74,16 @@ class EventSubscribeCard extends LitElement {
.value=${this._eventType}
@input=${this._valueChanged}
></ha-textfield>
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.developer-tools.tabs.events.filter_events"
)}
.value=${this._eventFilter}
.disabled=${this._subscribed !== undefined}
helperPersistent
.helper=${`${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
@input=${this._filterChanged}
></ha-textfield>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
@@ -135,6 +149,46 @@ class EventSubscribeCard extends LitElement {
this._error = undefined;
}
private _filterChanged(ev): void {
this._eventFilter = ev.target.value;
}
private _testEventFilter(event: HassEvent): boolean {
if (!this._eventFilter) {
return true;
}
const searchStr = this._eventFilter;
function visit(node) {
// Handle primitives directly
if (node === null || typeof node !== "object") {
return String(node).includes(searchStr);
}
// Handle arrays and plain objects
for (const key in node) {
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
// Check key
if (key.includes(searchStr)) return true;
const value = node[key];
// Check primitive value
if (value === null || typeof value !== "object") {
if (String(value).includes(searchStr)) return true;
} else if (visit(value)) {
// Recurse into nested object/array
return true;
}
}
return false;
}
return visit(event);
}
private async _startOrStopListening(): Promise<void> {
if (this._subscribed) {
this._subscribed();
@@ -144,6 +198,10 @@ class EventSubscribeCard extends LitElement {
try {
this._subscribed =
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
if (!this._testEventFilter(event)) {
this._ignoredEventsCount++;
return;
}
const tail =
this._events.length > 30
? this._events.slice(0, 29)
@@ -168,6 +226,7 @@ class EventSubscribeCard extends LitElement {
private _clearEvents(): void {
this._events = [];
this._eventCount = 0;
this._ignoredEventsCount = 0;
this._error = undefined;
}

View File

@@ -201,6 +201,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
label: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
),
type: "icon",
template: (statistic) =>
html`${statistic.issues
? html`<ha-button

View File

@@ -283,13 +283,18 @@ class PanelEnergy extends LitElement {
["grid", "solar", "battery"].includes(source.type)
);
const hasPower =
this._prefs.energy_sources.some(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
) || this._prefs.device_consumption.some((device) => device.stat_rate);
const hasPowerSource = this._prefs.energy_sources.some(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
);
const hasDevicePower = this._prefs.device_consumption.some(
(device) => device.stat_rate
);
const hasPower = hasPowerSource || hasDevicePower;
const hasWater =
this._prefs.energy_sources.some((source) => source.type === "water") ||
@@ -314,7 +319,10 @@ class PanelEnergy extends LitElement {
if (hasPower) {
views.push(POWER_VIEW);
}
if (views.length > 1) {
if (
hasPowerSource ||
[hasEnergy, hasGas, hasWater].filter(Boolean).length > 1
) {
views.unshift(OVERVIEW_VIEW);
}
return {

View File

@@ -2,6 +2,8 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
import type { HomeAssistant } from "../../../../types";
import type { ClockCardConfig } from "../types";
@@ -26,6 +28,11 @@ function romanize12HourClock(num: number) {
return numerals[num];
}
const INTERVAL = 1000;
const QUARTER_TICKS = Array.from({ length: 4 }, (_, i) => i);
const HOUR_TICKS = Array.from({ length: 12 }, (_, i) => i);
const MINUTE_TICKS = Array.from({ length: 60 }, (_, i) => i);
@customElement("hui-clock-card-analog")
export class HuiClockCardAnalog extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -40,27 +47,32 @@ export class HuiClockCardAnalog extends LitElement {
@state() private _secondOffsetSec?: number;
private _initDate() {
if (!this.config || !this.hass) {
return;
@state() private _year = "";
@state() private _month = "";
@state() private _day = "";
private _tickInterval?: undefined | number;
private _currentDate = new Date();
public connectedCallback() {
super.connectedCallback();
document.addEventListener("visibilitychange", this._handleVisibilityChange);
this._computeDateTime();
if (this.config?.date && this.config.date !== "none") {
this._startTick();
}
}
let locale = this.hass.locale;
if (this.config.time_format) {
locale = { ...locale, time_format: this.config.time_format };
}
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h12",
timeZone:
this.config.time_zone ||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
this._computeOffsets();
public disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener(
"visibilitychange",
this._handleVisibilityChange
);
this._stopTick();
}
protected updated(changedProps: PropertyValues) {
@@ -72,30 +84,78 @@ export class HuiClockCardAnalog extends LitElement {
}
}
public connectedCallback() {
super.connectedCallback();
document.addEventListener("visibilitychange", this._handleVisibilityChange);
this._computeOffsets();
}
public disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener(
"visibilitychange",
this._handleVisibilityChange
);
}
private _handleVisibilityChange = () => {
if (!document.hidden) {
this._computeOffsets();
this._computeDateTime();
}
};
private _computeOffsets() {
private _initDate() {
if (!this.config || !this.hass) {
return;
}
let locale = this.hass.locale;
if (this.config.time_format) {
locale = { ...locale, time_format: this.config.time_format };
}
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
...(this.config.date && this.config.date !== "none"
? this.config.date === "day"
? {
day: "numeric",
}
: this.config.date === "day-month"
? {
month: "short",
day: "numeric",
}
: this.config.date === "day-month-long"
? {
month: "long",
day: "numeric",
}
: {
year: "numeric",
month: this.config.date === "long" ? "long" : "short",
day: "numeric",
}
: {}),
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h12",
timeZone:
this.config.time_zone ||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
this._computeDateTime();
}
private _startTick() {
this._tick();
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
}
private _stopTick() {
if (this._tickInterval) {
clearInterval(this._tickInterval);
this._tickInterval = undefined;
}
}
private _updateDate() {
this._currentDate = new Date();
}
private _computeDateTime() {
if (!this._dateTimeFormat) return;
const parts = this._dateTimeFormat.formatToParts();
this._updateDate();
const parts = this._dateTimeFormat.formatToParts(this._currentDate);
const hourStr = parts.find((p) => p.type === "hour")?.value;
const minuteStr = parts.find((p) => p.type === "minute")?.value;
const secondStr = parts.find((p) => p.type === "second")?.value;
@@ -103,7 +163,7 @@ export class HuiClockCardAnalog extends LitElement {
const hour = hourStr ? parseInt(hourStr, 10) : 0;
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
const second = secondStr ? parseInt(secondStr, 10) : 0;
const ms = new Date().getMilliseconds();
const ms = this._currentDate.getMilliseconds();
const secondsWithMs = second + ms / 1000;
const hour12 = hour % 12;
@@ -111,18 +171,38 @@ export class HuiClockCardAnalog extends LitElement {
this._secondOffsetSec = secondsWithMs;
this._minuteOffsetSec = minute * 60 + secondsWithMs;
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
// Also update date parts if date is shown
if (this.config?.date && this.config.date !== "none") {
this._year = parts.find((p) => p.type === "year")?.value ?? "";
this._month = parts.find((p) => p.type === "month")?.value ?? "";
this._day = parts.find((p) => p.type === "day")?.value ?? "";
}
}
private _tick() {
this._computeDateTime();
}
private _computeClock = memoizeOne((config: ClockCardConfig) => {
const faceParts = config.face_style?.split("_");
const isLongDate =
config.date === "day-month-long" || config.date === "long";
return {
sizeClass: config.clock_size ? `size-${config.clock_size}` : "",
isNumbers: faceParts?.includes("numbers") ?? false,
isRoman: faceParts?.includes("roman") ?? false,
isUpright: faceParts?.includes("upright") ?? false,
isLongDate,
};
});
render() {
if (!this.config) return nothing;
const sizeClass = this.config.clock_size
? `size-${this.config.clock_size}`
: "";
const isNumbers = this.config?.face_style?.startsWith("numbers");
const isRoman = this.config?.face_style?.startsWith("roman");
const isUpright = this.config?.face_style?.endsWith("upright");
const { sizeClass, isNumbers, isRoman, isUpright, isLongDate } =
this._computeClock(this.config);
const indicator = (number?: number) => html`
<div
@@ -163,14 +243,14 @@ export class HuiClockCardAnalog extends LitElement {
})}
>
${this.config.ticks === "quarter"
? Array.from({ length: 4 }, (_, i) => i).map(
? QUARTER_TICKS.map(
(i) =>
// 4 ticks (12, 3, 6, 9) at 0°, 90°, 180°, 270°
html`
<div
aria-hidden="true"
class="tick hour"
style=${`--tick-rotation: ${i * 90}deg;`}
style=${styleMap({ "--tick-rotation": `${i * 90}deg` })}
>
${indicator([12, 3, 6, 9][i])}
</div>
@@ -178,28 +258,30 @@ export class HuiClockCardAnalog extends LitElement {
)
: !this.config.ticks || // Default to hour ticks
this.config.ticks === "hour"
? Array.from({ length: 12 }, (_, i) => i).map(
? HOUR_TICKS.map(
(i) =>
// 12 ticks (1-12)
html`
<div
aria-hidden="true"
class="tick hour"
style=${`--tick-rotation: ${i * 30}deg;`}
style=${styleMap({ "--tick-rotation": `${i * 30}deg` })}
>
${indicator(((i + 11) % 12) + 1)}
</div>
`
)
: this.config.ticks === "minute"
? Array.from({ length: 60 }, (_, i) => i).map(
? MINUTE_TICKS.map(
(i) =>
// 60 ticks (1-60)
html`
<div
aria-hidden="true"
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
style=${`--tick-rotation: ${i * 6}deg;`}
style=${styleMap({
"--tick-rotation": `${i * 6}deg`,
})}
>
${i % 5 === 0
? indicator(((i / 5 + 11) % 12) + 1)
@@ -208,14 +290,33 @@ export class HuiClockCardAnalog extends LitElement {
`
)
: nothing}
${this.config?.date && this.config.date !== "none"
? html`<div
class=${classMap({
"date-parts": true,
[sizeClass]: true,
"long-date": isLongDate,
})}
>
<span class="date-part day-month"
>${this._day} ${this._month}</span
>
<span class="date-part year">${this._year}</span>
</div>`
: nothing}
<div class="center-dot"></div>
<div
class="hand hour"
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
style=${styleMap({
"animation-delay": `-${this._hourOffsetSec ?? 0}s`,
})}
></div>
<div
class="hand minute"
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
style=${styleMap({
"animation-delay": `-${this._minuteOffsetSec ?? 0}s`,
})}
></div>
${this.config.show_seconds
? html`<div
@@ -224,11 +325,13 @@ export class HuiClockCardAnalog extends LitElement {
second: true,
step: this.config.seconds_motion === "tick",
})}
style=${`animation-delay: -${
(this.config.seconds_motion === "tick"
? Math.floor(this._secondOffsetSec ?? 0)
: (this._secondOffsetSec ?? 0)) as number
}s;`}
style=${styleMap({
"animation-delay": `-${
(this.config.seconds_motion === "tick"
? Math.floor(this._secondOffsetSec ?? 0)
: (this._secondOffsetSec ?? 0)) as number
}s`,
})}
></div>`
: nothing}
</div>
@@ -270,6 +373,14 @@ export class HuiClockCardAnalog extends LitElement {
box-sizing: border-box;
}
/* Modern browsers: Use container queries for responsive font sizing */
@supports (container-type: inline-size) {
.dial {
container-type: inline-size;
container-name: clock;
}
}
.dial-border {
border: 2px solid var(--divider-color);
border-radius: var(--ha-border-radius-circle);
@@ -407,6 +518,78 @@ export class HuiClockCardAnalog extends LitElement {
transform: translate(-50%, 0) rotate(360deg);
}
}
.date-parts {
position: absolute;
top: 68%;
left: 50%;
transform: translate(-50%, -50%);
display: grid;
align-items: center;
grid-template-areas:
"day-month"
"year";
direction: ltr;
color: var(--primary-text-color);
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
text-align: center;
opacity: 0.8;
max-width: 87%;
overflow: hidden;
white-space: nowrap;
}
/* Modern browsers: Use container queries for responsive font sizing */
@supports (container-type: inline-size) {
/* Small clock with long date: reduce to xs */
@container clock (max-width: 139px) {
.date-parts.long-date {
font-size: var(--ha-font-size-xs);
}
}
/* Medium clock: scale up */
@container clock (min-width: 140px) {
.date-parts {
font-size: var(--ha-font-size-l);
}
}
/* Large clock: scale up more */
@container clock (min-width: 200px) {
.date-parts {
font-size: var(--ha-font-size-xl);
}
}
}
/* Legacy browsers: Use existing size classes */
@supports not (container-type: inline-size) {
/* Small clock (no size class) with long date */
.date-parts.long-date:not(.size-medium):not(.size-large) {
font-size: var(--ha-font-size-xs);
}
.date-parts.size-medium {
font-size: var(--ha-font-size-l);
}
.date-parts.size-large {
font-size: var(--ha-font-size-xl);
}
}
.date-part.day-month {
grid-area: day-month;
overflow: hidden;
}
.date-part.year {
grid-area: year;
overflow: hidden;
}
`;
}

View File

@@ -24,6 +24,8 @@ export class HuiClockCardDigital extends LitElement {
@state() private _timeAmPm?: string;
@state() private _date?: string;
private _tickInterval?: undefined | number;
private _initDate() {
@@ -39,6 +41,27 @@ export class HuiClockCardDigital extends LitElement {
const h12 = useAmPm(locale);
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
...(this.config.date && this.config.date !== "none"
? this.config.date === "day"
? {
day: "numeric",
}
: this.config.date === "day-month"
? {
month: "short",
day: "numeric",
}
: this.config.date === "day-month-long"
? {
month: "long",
day: "numeric",
}
: {
year: "numeric",
month: this.config.date === "long" ? "long" : "short",
day: "numeric",
}
: {}),
hour: h12 ? "numeric" : "2-digit",
minute: "2-digit",
second: "2-digit",
@@ -93,6 +116,16 @@ export class HuiClockCardDigital extends LitElement {
? parts.find((part) => part.type === "second")?.value
: undefined;
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
this._date = this.config?.date
? [
parts.find((part) => part.type === "day")?.value,
parts.find((part) => part.type === "month")?.value,
parts.find((part) => part.type === "year")?.value,
]
.filter(Boolean)
.join(" ")
: undefined;
}
render() {
@@ -113,6 +146,9 @@ export class HuiClockCardDigital extends LitElement {
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
: nothing}
</div>
${this.config.date && this.config.date !== "none"
? html`<div class="date ${sizeClass}">${this._date}</div>`
: nothing}
`;
}
@@ -188,6 +224,20 @@ export class HuiClockCardDigital extends LitElement {
content: ":";
margin: 0 2px;
}
.date {
text-align: center;
opacity: 0.8;
font-size: var(--ha-font-size-s);
}
.date.size-medium {
font-size: var(--ha-font-size-l);
}
.date.size-large {
font-size: var(--ha-font-size-2xl);
}
`;
}

View File

@@ -67,6 +67,19 @@ export const SUM_DEVICE_CLASSES = [
"water",
];
// Additional sources for sensor device classes from entity attributes
// Maps device_class -> array of { domain, attribute } to include in aggregation
export const SENSOR_ATTRIBUTE_SOURCES: Record<
string,
{ domain: string; attribute: string }[]
> = {
temperature: [{ domain: "climate", attribute: "current_temperature" }],
humidity: [
{ domain: "climate", attribute: "current_humidity" },
{ domain: "humidifier", attribute: "current_humidity" },
],
};
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
exclude_entities?: string[];
}
@@ -251,6 +264,24 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
}
);
private _domainEntityIds = memoizeOne(
(
entities: HomeAssistant["entities"],
areaId: string,
domains: string[],
excludeEntities?: string[]
): string[] => {
const filter = generateEntityFilter(this.hass, {
area: areaId,
entity_category: "none",
domain: domains,
});
return Object.keys(entities).filter(
(id) => filter(id) && !excludeEntities?.includes(id)
);
}
);
private _computeActiveAlertStates(): HassEntity[] {
const areaId = this._config?.area;
const area = areaId ? this.hass.areas[areaId] : undefined;
@@ -359,58 +390,91 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
: this.hass.formatEntityState(stateObj);
}
const entityIds = groupedEntities.get(sensorClass);
const sensorEntityIds = groupedEntities.get(sensorClass) || [];
const values: number[] = [];
let uom: string | undefined;
if (!entityIds) {
return undefined;
// Track devices that have sensor entities contributing values
// to avoid duplicate readings from climate/humidifier attributes
const devicesWithSensorValues = new Set<string>();
for (const entityId of sensorEntityIds) {
const stateObj = this.hass.states[entityId];
if (
stateObj &&
!isUnavailableState(stateObj.state) &&
isNumericState(stateObj) &&
!isNaN(Number(stateObj.state))
) {
if (!uom) {
uom = stateObj.attributes.unit_of_measurement;
}
if (stateObj.attributes.unit_of_measurement === uom) {
values.push(Number(stateObj.state));
// Track the device this sensor belongs to
const entityEntry = this.hass.entities[entityId];
if (entityEntry?.device_id) {
devicesWithSensorValues.add(entityEntry.device_id);
}
}
}
}
// Ensure all entities have state
const entities = entityIds
.map((entityId) => this.hass.states[entityId])
.filter(Boolean);
// Collect values from additional attribute sources
const attrSources = SENSOR_ATTRIBUTE_SOURCES[sensorClass];
if (attrSources) {
const domains = [...new Set(attrSources.map((s) => s.domain))];
const attrEntityIds = this._domainEntityIds(
this.hass.entities,
area.area_id,
domains,
excludeEntities
);
if (entities.length === 0) {
return undefined;
for (const entityId of attrEntityIds) {
const stateObj = this.hass.states[entityId];
if (!stateObj) continue;
// Skip if this entity's device already has a sensor contributing values
const entityEntry = this.hass.entities[entityId];
if (
entityEntry?.device_id &&
devicesWithSensorValues.has(entityEntry.device_id)
) {
continue;
}
const domain = entityId.split(".")[0];
const source = attrSources.find((s) => s.domain === domain);
if (!source) continue;
const attrValue = stateObj.attributes[source.attribute];
if (attrValue == null || isNaN(Number(attrValue))) continue;
if (!uom) {
// Determine unit from attribute
uom = this._getAttributeUnit(sensorClass, domain);
}
values.push(Number(attrValue));
}
}
// If only one entity, return its formatted state
if (entities.length === 1) {
const stateObj = entities[0];
return isUnavailableState(stateObj.state)
? ""
: this.hass.formatEntityState(stateObj);
}
// Use the first entity's unit_of_measurement for formatting
const uom = entities.find(
(entity) => entity.attributes.unit_of_measurement
)?.attributes.unit_of_measurement;
// Ensure all entities have the same unit_of_measurement
const validEntities = entities.filter(
(entity) =>
entity.attributes.unit_of_measurement === uom &&
isNumericState(entity) &&
!isNaN(Number(entity.state))
);
if (validEntities.length === 0) {
if (values.length === 0) {
return undefined;
}
const value = SUM_DEVICE_CLASSES.includes(sensorClass)
? this._computeSumState(validEntities)
: this._computeMedianState(validEntities);
? values.reduce((acc, v) => acc + v, 0)
: this._computeMedianValue(values);
const formattedAverage = formatNumber(value, this.hass!.locale, {
const formattedValue = formatNumber(value, this.hass.locale, {
maximumFractionDigits: 1,
});
const formattedUnit = uom
? `${blankBeforeUnit(uom, this.hass!.locale)}${uom}`
? `${blankBeforeUnit(uom, this.hass.locale)}${uom}`
: "";
return `${formattedAverage}${formattedUnit}`;
return `${formattedValue}${formattedUnit}`;
})
.filter(Boolean)
.join(" · ");
@@ -418,20 +482,25 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return sensorStates;
}
private _computeSumState(entities: HassEntity[]): number {
return entities.reduce((acc, entity) => acc + Number(entity.state), 0);
private _getAttributeUnit(sensorClass: string, domain: string): string {
// Return the expected unit for attributes from specific domains
if (sensorClass === "temperature" && domain === "climate") {
return this.hass.config.unit_system.temperature;
}
if (sensorClass === "humidity") {
return "%";
}
return "";
}
private _computeMedianState(entities: HassEntity[]): number {
const sortedStates = entities
.map((entity) => Number(entity.state))
.sort((a, b) => a - b);
if (sortedStates.length % 2 === 0) {
const medianIndex = sortedStates.length / 2;
return (sortedStates[medianIndex] + sortedStates[medianIndex - 1]) / 2;
private _computeMedianValue(values: number[]): number {
const sortedValues = [...values].sort((a, b) => a - b);
if (sortedValues.length % 2 === 0) {
const medianIndex = sortedValues.length / 2;
return (sortedValues[medianIndex] + sortedValues[medianIndex - 1]) / 2;
}
const medianIndex = Math.floor(sortedStates.length / 2);
return sortedStates[medianIndex];
const medianIndex = Math.floor(sortedValues.length / 2);
return sortedValues[medianIndex];
}
private _featurePosition = memoizeOne((config: AreaCardConfig) => {

View File

@@ -94,10 +94,12 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
(changedProps.has("_config") && this._config?.entities)
) {
const computedStyles = getComputedStyle(this);
this._calendars = this._config!.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
}));
if (this._config?.entities) {
this._calendars = this._config.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
}));
}
}
}

View File

@@ -1,63 +1,116 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCard } from "../types";
import { handleAction } from "../common/handle-action";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { EmptyStateCardConfig } from "./types";
@customElement("hui-empty-state-card")
export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-empty-state-card-editor");
return document.createElement("hui-empty-state-card-editor");
}
public static getStubConfig(): EmptyStateCardConfig {
return {
type: "empty-state",
title: "Welcome Home",
content: "This is an empty state card.",
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public getCardSize(): number {
return 2;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public setConfig(_config: EmptyStateCardConfig): void {}
public setConfig(config: EmptyStateCardConfig): void {
this._config = config;
}
protected render() {
if (!this.hass) {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.title"
)}
class=${classMap({
"content-only": this._config.content_only ?? false,
})}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.no_devices"
)}
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/config/integrations/dashboard">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.go_to_integrations_page"
)}
</ha-button>
<div class="container">
${this._config.icon
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
: nothing}
${this._config.title ? html`<h1>${this._config.title}</h1>` : nothing}
${this._config.content
? html`<p>${this._config.content}</p>`
: nothing}
${this._config.tap_action && this._config.action_button_text
? html`
<ha-button @click=${this._handleAction}>
${this._config.action_button_text}
</ha-button>
`
: nothing}
</div>
</ha-card>
`;
}
private _handleAction(): void {
if (this._config?.tap_action && this.hass) {
handleAction(this, this.hass, this._config, "tap");
}
}
static styles = css`
.content {
margin-top: -1em;
padding: 16px;
:host {
display: block;
height: 100%;
}
.card-actions a {
text-decoration: none;
ha-card {
height: 100%;
}
ha-button {
margin-left: -8px;
margin-inline-start: -8px;
margin-inline-end: initial;
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
padding: var(--ha-space-8) var(--ha-space-4);
box-sizing: border-box;
gap: var(--ha-space-4);
max-width: 640px;
margin: 0 auto;
}
ha-icon {
--mdc-icon-size: var(--ha-space-12);
color: var(--secondary-text-color);
}
h1 {
margin: 0;
font-size: var(--ha-font-size-xl);
font-weight: 500;
}
p {
margin: 0;
color: var(--secondary-text-color);
}
.content-only {
background: none;
box-shadow: none;
border: none;
}
`;
}

View File

@@ -58,8 +58,12 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
}
export interface EmptyStateCardConfig extends LovelaceCardConfig {
content: string;
content_only?: boolean;
icon?: string;
title?: string;
content?: string;
action_button_text?: string;
tap_action?: ActionConfig;
}
export interface EntityCardConfig extends LovelaceCardConfig {
@@ -417,6 +421,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
time_format?: TimeFormat;
time_zone?: string;
no_background?: boolean;
date?: "none" | "short" | "long" | "day" | "day-month" | "day-month-long";
// Analog clock options
border?: boolean;
ticks?: "none" | "quarter" | "hour" | "minute";

View File

@@ -21,5 +21,8 @@ export const confirmAction = async (
hass.localize("ui.panel.lovelace.cards.actions.action_confirmation", {
action,
}),
title: config.title,
dismissText: config.dismiss_text,
confirmText: config.confirm_text,
});
};

View File

@@ -368,6 +368,7 @@ export const generateViewConfig = (
path: string,
title: string | undefined,
icon: string | undefined,
show_icon_and_title: boolean | undefined,
entities: HassEntities
): LovelaceViewConfig => {
const ungroupedEntitites: Record<string, string[]> = {};
@@ -497,6 +498,9 @@ export const generateViewConfig = (
if (icon) {
view.icon = icon;
}
if (show_icon_and_title) {
view.show_icon_and_title = show_icon_and_title;
}
return view;
};
@@ -517,6 +521,7 @@ export const generateDefaultViewConfig = (
const path = "default_view";
const title = "Home";
const icon = undefined;
const show_icon_and_title = undefined;
// In the case of a default view, we want to use the group order attribute
const groupOrders = {};
@@ -566,6 +571,7 @@ export const generateDefaultViewConfig = (
path,
title,
icon,
show_icon_and_title,
splittedByGroups.ungrouped
);

View File

@@ -7,6 +7,11 @@ const calcPoints = (
height: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
// handling empty history (for example unavailable for long time)
if (history.length === 0) {
return { points: [], yAxisOrigin: height };
}
let yAxisOrigin = height;
let minY = limits?.minY ?? history[0][1];
let maxY = limits?.maxY ?? history[0][1];

View File

@@ -89,6 +89,9 @@ export const handleAction = async (
) ||
actionConfig.action,
}),
title: actionConfig.confirmation.title,
dismissText: actionConfig.confirmation.dismiss_text,
confirmText: actionConfig.confirmation.confirm_text,
}))
) {
return;

View File

@@ -39,6 +39,19 @@ const cardConfigStruct = assign(
time_zone: optional(enums(Object.keys(timezones))),
show_seconds: optional(boolean()),
no_background: optional(boolean()),
date: optional(
defaulted(
union([
literal("none"),
literal("short"),
literal("long"),
literal("day"),
literal("day-month"),
literal("day-month-long"),
]),
literal("none")
)
),
// Analog clock options
border: optional(defaulted(boolean(), false)),
ticks: optional(
@@ -93,7 +106,7 @@ export class HuiClockCardEditor
name: "clock_style",
selector: {
select: {
mode: "dropdown",
mode: "box",
options: ["digital", "analog"].map((value) => ({
value,
label: localize(
@@ -119,6 +132,27 @@ export class HuiClockCardEditor
},
{ name: "show_seconds", selector: { boolean: {} } },
{ name: "no_background", selector: { boolean: {} } },
{
name: "date",
selector: {
select: {
mode: "dropdown",
options: [
"none",
"short",
"long",
"day",
"day-month",
"day-month-long",
].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.dates.${value}`
),
})),
},
},
},
...(clockStyle === "digital"
? ([
{
@@ -260,13 +294,14 @@ export class HuiClockCardEditor
] as const satisfies readonly HaFormSchema[]
);
private _data = memoizeOne((config) => ({
private _data = memoizeOne((config: ClockCardConfig) => ({
clock_style: "digital",
clock_size: "small",
time_zone: "auto",
time_format: "auto",
show_seconds: false,
no_background: false,
date: "none",
// Analog clock options
border: false,
ticks: "hour",
@@ -290,8 +325,9 @@ export class HuiClockCardEditor
.data=${this._data(this._config)}
.schema=${this._schema(
this.hass.localize,
this._data(this._config).clock_style,
this._data(this._config).ticks,
this._data(this._config)
.clock_style as ClockCardConfig["clock_style"],
this._data(this._config).ticks as ClockCardConfig["ticks"],
this._data(this._config).show_seconds
)}
.computeLabel=${this._computeLabelCallback}
@@ -367,6 +403,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.no_background`
);
case "date":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date.label`
);
case "border":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.border.label`
@@ -392,6 +432,10 @@ export class HuiClockCardEditor
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "date":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date.description`
);
case "border":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.border.description`

View File

@@ -0,0 +1,153 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
content_only: optional(boolean()),
icon: optional(string()),
title: optional(string()),
content: optional(string()),
action_button_text: optional(string()),
tap_action: optional(actionConfigStruct),
})
);
@customElement("hui-empty-state-card-editor")
export class HuiEmptyStateCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public setConfig(config: EmptyStateCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "style",
selector: {
select: {
mode: "box",
options: (
[
{ value: "card", image: "card" },
{ value: "content-only", image: "text_only" },
] as const
).map((style) => ({
label: localize(
`ui.panel.lovelace.editor.card.empty_state.style_options.${style.value}`
),
image: {
src: `/static/images/form/markdown_${style.image}.svg`,
src_dark: `/static/images/form/markdown_${style.image}_dark.svg`,
flip_rtl: true,
},
value: style.value,
})),
},
},
},
{ name: "icon", selector: { icon: {} } },
{ name: "title", selector: { text: {} } },
{ name: "content", selector: { text: { multiline: true } } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{ name: "action_button_text", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data = {
...this._config,
style: this._config.content_only ? "content-only" : "card",
};
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
const config = { ...ev.detail.value };
if (config.style === "content-only") {
config.content_only = true;
} else {
delete config.content_only;
}
delete config.style;
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "style":
case "content":
case "action_button_text":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-empty-state-card-editor": HuiEmptyStateCardEditor;
}
}

View File

@@ -73,6 +73,12 @@ export class HuiViewEditor extends LitElement {
icon: {},
},
},
{
name: "show_icon_and_title",
selector: {
boolean: {},
},
},
{ name: "path", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
@@ -207,6 +213,7 @@ export class HuiViewEditor extends LitElement {
case "path":
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url");
case "type":
case "show_icon_and_title":
case "subview":
case "max_columns":
case "dense_section_placement":
@@ -227,6 +234,7 @@ export class HuiViewEditor extends LitElement {
) => {
switch (schema.name) {
case "path":
case "show_icon_and_title":
case "subview":
case "dense_section_placement":
case "top_margin":

View File

@@ -112,7 +112,20 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
forwardHaptic(this, "light");
setSelectOption(this.hass!, stateObj.entity_id, option);
setSelectOption(this.hass!, stateObj.entity_id, option)
.catch((_err) => {
// silently swallow exception
})
.finally(() =>
setTimeout(() => {
const newStateObj = this.hass!.states[this._config!.entity];
if (newStateObj === stateObj) {
const select = this.shadowRoot?.querySelector("ha-select");
const index = select?.options.indexOf(stateObj.state) ?? -1;
select?.select(index);
}
}, 2000)
);
}
}

View File

@@ -496,6 +496,10 @@ class HUIRoot extends LitElement {
const tabs = html`<ha-tab-group @wa-tab-show=${this._handleViewSelected}>
${views.map((view, index) => {
const icon_and_title =
view.show_icon_and_title && view.icon && view.title;
const icon_only = view.icon && !icon_and_title;
const title_only = !icon_only && !icon_and_title;
const hidden =
!this._editMode && (view.subview || _isTabHiddenForUser(view));
return html`
@@ -506,7 +510,8 @@ class HUIRoot extends LitElement {
.disabled=${hidden}
aria-label=${ifDefined(view.title)}
class=${classMap({
icon: Boolean(view.icon),
"icon-only": Boolean(icon_only),
"icon-and-title": Boolean(icon_and_title),
"hide-tab": Boolean(hidden),
})}
>
@@ -523,18 +528,20 @@ class HUIRoot extends LitElement {
></ha-icon-button-arrow-prev>
`
: nothing}
${view.icon
? html`
<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>
`
: view.title ||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")}
${icon_only || icon_and_title
? html`<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>`
: nothing}
${icon_and_title ? view.title : nothing}
${title_only
? view.title ||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")
: nothing}
${this._editMode
? html`
<ha-icon-button
@@ -1489,24 +1496,27 @@ class HUIRoot extends LitElement {
ha-tab-group-tab {
--ha-tab-group-tab-height: var(--header-height, 56px);
}
.tab-bar ha-tab-group-tab {
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
}
ha-tab-group-tab[aria-selected="true"] .edit-icon {
display: inline-flex;
}
ha-tab-group-tab::part(base) {
padding-inline-start: var(--ha-tab-padding-start, var(--wa-space-l));
padding-inline-end: var(--ha-tab-padding-end, var(--wa-space-l));
}
ha-tab-group-tab::part(base) {
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2);
}
ha-tab-group-tab.icon::part(base) {
ha-tab-group-tab.icon-only::part(base),
ha-tab-group-tab.icon-and-title::part(base) {
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2 - 2px);
padding-bottom: calc(
(var(--ha-tab-group-tab-height) - 20px) / 2 - 4px
);
}
.tab-bar ha-tab-group-tab {
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
ha-tab-group-tab.icon-and-title ha-icon {
margin-inline-end: var(--ha-space-2);
}
.edit-mode ha-tab-group-tab[aria-selected="true"]::part(base) {
padding: 0;

View File

@@ -11,7 +11,10 @@ import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig } from "../../cards/types";
import type {
EmptyStateCardConfig,
HeadingCardConfig,
} from "../../cards/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import {
getSummaryLabel,
@@ -354,6 +357,26 @@ export class HomeAreaViewStrategy extends ReactiveElement {
});
}
// No sections, show empty state
if (sections.length === 0) {
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:sofa-outline",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_content"
),
} as EmptyStateCardConfig,
],
};
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);

View File

@@ -6,6 +6,7 @@ import type { AreasDisplayValue } from "../../../../components/ha-areas-display-
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
export interface OriginalStatesViewStrategyConfig {
@@ -64,9 +65,33 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
// User has no entities
if (view.cards!.length === 0) {
view.cards!.push({
type: "empty-state",
});
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:home-assistant",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_content"
),
...(hass.user?.is_admin
? {
action_button_text: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_action"
),
tap_action: {
action: "navigate",
navigation_path: "/config/integrations/dashboard",
},
}
: {}),
} as EmptyStateCardConfig,
],
};
}
return view;

View File

@@ -1,7 +1,6 @@
import type { ActionDetail } from "@material/mwc-list";
import {
mdiAlphaABoxOutline,
mdiArrowLeft,
mdiDotsVertical,
mdiGrid,
mdiListBoxOutline,
@@ -97,7 +96,6 @@ class PanelMediaBrowser extends LitElement {
? html`
<ha-icon-button-arrow-prev
slot="navigationIcon"
.path=${mdiArrowLeft}
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`

View File

@@ -156,6 +156,9 @@ export const semanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
/* Scrollable fade */
--ha-color-shadow-scrollable-fade: rgba(0, 0, 0, 0.08);
}
`;

View File

@@ -50,7 +50,9 @@ export class HaStateControlHumidifierHumidity extends LitElement {
}
}
private _step = 1;
private get _step() {
return this.stateObj.attributes.target_humidity_step ?? 1;
}
private get _min() {
return this.stateObj.attributes.min_humidity ?? 0;

View File

@@ -2172,7 +2172,8 @@
},
"other": {
"title": "Other",
"my_link": "get My Home Assistant link"
"my_link": "get My Home Assistant link",
"show_shortcuts": "show keyboard shortcuts"
}
}
},
@@ -2318,7 +2319,7 @@
},
"companion": {
"main": "Companion app",
"secondary": "Location and notifications"
"secondary": "Sensors, widgets, notifications and more"
},
"system": {
"main": "System",
@@ -2382,6 +2383,7 @@
"updates_refreshed": "State of {count} {count, plural,\n one {update}\n other {updates}\n} refreshed",
"checking_updates": "Checking for updates...",
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"title_not_installable": "{count} not installable {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to load updates",
"more_updates": "Show all updates",
"show": "show",
@@ -3339,7 +3341,8 @@
"type": "Type",
"editable": "Editable",
"category": "Category",
"area": "Area"
"area": "Area",
"voice_assistants": "Voice assistants"
},
"create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!",
@@ -3980,7 +3983,8 @@
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon"
"icon": "Icon",
"voice_assistants": "Voice assistants"
},
"bulk_action": "Action",
"bulk_actions": {
@@ -4997,7 +5001,8 @@
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon"
"icon": "Icon",
"voice_assistants": "Voice assistants"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -5125,7 +5130,8 @@
"category": "Category",
"editable": "[%key:ui::panel::config::helpers::picker::headers::editable%]",
"area": "Area",
"icon": "Icon"
"icon": "Icon",
"voice_assistants": "Voice assistants"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -5578,7 +5584,8 @@
"domain": "Domain",
"availability": "Availability",
"visibility": "Visibility",
"enabled": "Enabled"
"enabled": "Enabled",
"voice_assistants": "Voice assistants"
},
"selected": "{number} selected",
"enable_selected": {
@@ -6838,9 +6845,15 @@
},
"matter": {
"panel": {
"thread_panel": "Visit Thread Panel",
"experimental_note": "Matter is still in the early phase of development, it is not meant to be used in production. This panel is for development only.",
"add_devices": "You can add Matter devices by commissioning them if they are not set up yet, or share them from another controller and enter the sharing code.",
"thread_panel": "Visit Thread panel",
"add_device": "Add device",
"status_title": "status",
"status_online": "online",
"status_offline": "offline",
"devices": "{count, plural,\n one {# device}\n other {# devices}\n}",
"developer_tools_title": "Developer tools",
"developer_tools_description": "Advanced commissioning options for development",
"developer_tools_info": "These options are intended for developers and advanced users. In most cases, use the \"Add device\" button in the bottom right corner instead.",
"mobile_app_commisioning": "Commission device with mobile app",
"commission_device": "Commission device",
"add_shared_device": "Add shared device",
@@ -7220,10 +7233,14 @@
"lovelace": {
"strategy": {
"original-states": {
"helpers": "[%key:ui::panel::config::helpers::caption%]"
"helpers": "[%key:ui::panel::config::helpers::caption%]",
"empty_state_title": "Welcome Home",
"empty_state_content": "This page allows you to control your devices, however it looks like you have no devices set up yet.",
"empty_state_action": "Go to the integrations page"
},
"areas": {
"no_entities": "No entities in this area.",
"empty_state_title": "No devices",
"empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.",
"sensors": "Sensors",
"sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
"edit_the_area": "edit the area",
@@ -7295,11 +7312,6 @@
"no_url": "No URL to open specified",
"no_action": "No action to run specified"
},
"empty_state": {
"title": "Welcome Home",
"no_devices": "This page allows you to control your devices, however it looks like you have no devices set up yet. Head to the integrations page to get started.",
"go_to_integrations_page": "Go to the integrations page."
},
"entities": {
"never_triggered": "Never triggered"
},
@@ -7597,6 +7609,7 @@
"sidebar": "Sidebar",
"panel": "Panel (single card)"
},
"show_icon_and_title": "Show icon and title",
"subview": "Subview",
"max_columns": "Max number of sections wide",
"section_specifics": "Sections view specific settings",
@@ -7604,6 +7617,7 @@
"dense_section_placement_helper": "Will try to fill gaps with sections that fit the gap. This may make section placement less predictable.",
"top_margin": "Add additional space above",
"top_margin_helper": "Helps reveal more of the background",
"show_icon_and_title_helper": "Show both icon and text title.",
"subview_helper": "Subviews don't appear in tabs and have a back button.",
"path_helper": "This value will become part of the URL path to open this view.",
"edit_ui": "Edit in visual editor",
@@ -7969,6 +7983,17 @@
"name": "Entity",
"description": "The Entity card gives you a quick overview of your entity's state."
},
"empty_state": {
"name": "Empty state",
"description": "The Empty state card displays a centered message with an optional icon and action button.",
"style": "Style",
"style_options": {
"card": "Card",
"content-only": "Content only"
},
"content": "Content",
"action_text": "Action button text"
},
"button": {
"name": "Button",
"description": "The Button card allows you to add buttons to perform tasks.",
@@ -8216,6 +8241,18 @@
"large": "Large"
},
"show_seconds": "Display seconds",
"date": {
"label": "Date",
"description": "Whether to show the date on the clock. Can also be a custom date format."
},
"dates": {
"none": "No date",
"short": "Short",
"long": "Long",
"day": "Day only",
"day-month": "Day and month",
"day-month-long": "Day and month (long)"
},
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
"time_formats": {
"auto": "Use user settings",
@@ -9235,6 +9272,9 @@
"active_listeners": "Active listeners",
"count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})",
"listen_to_events": "Listen to events",
"filter_events": "Filter events",
"filter_helper": "Only capture events containing the filter string.",
"filter_ignored": "Ignored {count} events.",
"listening_to": "Listening to",
"subscribe_to": "Event to subscribe to",
"start_listening": "Start listening",

557
yarn.lock
View File

@@ -1204,14 +1204,14 @@ __metadata:
languageName: node
linkType: hard
"@bundle-stats/plugin-webpack-filter@npm:4.21.7":
version: 4.21.7
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.7"
"@bundle-stats/plugin-webpack-filter@npm:4.21.8":
version: 4.21.8
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.8"
dependencies:
tslib: "npm:2.8.1"
peerDependencies:
core-js: ^3.0.0
checksum: 10/c63f2d0742bc0729450b08523cf0b38711c336cb284c9e2beeae4509cbdf891626000e60e4f771ca53f0de6fcb8824998d01bb99016b5f3be7b70cfff8dc845f
checksum: 10/909d1ca31880b8f40fdad2fdc60484f086c3167d1654688be1fdd3c280a94ca73ee31dcde5ea118dcc0ee5dfe0dbfedcdec521cff831f13c69baa5af79beabcf
languageName: node
linkType: hard
@@ -1282,15 +1282,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.39.8, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.39.8
resolution: "@codemirror/view@npm:6.39.8"
"@codemirror/view@npm:6.39.9, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.39.9
resolution: "@codemirror/view@npm:6.39.9"
dependencies:
"@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/a15941940fabc9b595da00a7760947cf7ce83f3f819be31250a73d2a1de5d1b5528a5803aa19c74656d2d7cbc39f47daec4962190ffc0849f4f359e45b4f1c3a
checksum: 10/9e86b35f31fd4f8b4c2fe608fa6116ddc71261acd842c405de41de1f752268c47ea8e0c400818b4d0481a629e1f773dda9e6f0d24d38ed6a9f6b3d58b2dff669
languageName: node
linkType: hard
@@ -1582,18 +1582,18 @@ __metadata:
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0":
version: 4.9.0
resolution: "@eslint-community/eslint-utils@npm:4.9.0"
"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1":
version: 4.9.1
resolution: "@eslint-community/eslint-utils@npm:4.9.1"
dependencies:
eslint-visitor-keys: "npm:^3.4.3"
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
checksum: 10/89b1eb3137e14c379865e60573f524fcc0ee5c4b0c7cd21090673e75e5a720f14b92f05ab2d02704c2314b67e67b6f96f3bb209ded6b890ced7b667aa4bf1fa2
checksum: 10/863b5467868551c9ae34d03eefe634633d08f623fc7b19d860f8f26eb6f303c1a5934253124163bee96181e45ed22bf27473dccc295937c3078493a4a8c9eddd
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1":
"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2":
version: 4.12.2
resolution: "@eslint-community/regexpp@npm:4.12.2"
checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c
@@ -1708,166 +1708,166 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:3.0.7":
version: 3.0.7
resolution: "@formatjs/ecma402-abstract@npm:3.0.7"
"@formatjs/ecma402-abstract@npm:3.0.8":
version: 3.0.8
resolution: "@formatjs/ecma402-abstract@npm:3.0.8"
dependencies:
"@formatjs/fast-memoize": "npm:3.0.2"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/fast-memoize": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.5"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/a79f43d3c3e5301722d3288806e6910d8598e2f0c849775398f6e20bac78b568db9c03b6605c9db8eb3aa80dff4fc260cb153e67b167924a547e0d29a1b2d8c3
checksum: 10/5b8e00790f79c011a6928644baee1409b86353576a91f7a8e9b4562a4a5d486997036ff04cd8451891ab8b7691e683c34cd40d76d4a1751e7df6052d46c54f06
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:3.0.2":
version: 3.0.2
resolution: "@formatjs/fast-memoize@npm:3.0.2"
"@formatjs/fast-memoize@npm:3.0.3":
version: 3.0.3
resolution: "@formatjs/fast-memoize@npm:3.0.3"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/381afd816ca67d7e3e333247f1115ede11420226d72f109c54e8300741212b9bb08a6bacec17d0f0f2172789e39d0cef61fcae0a484e1dff2e9c33eee930694e
checksum: 10/466aa1250eee77897455b6e0282bd178357e326790e18f59e33c77371ebb6099cbf8c520300193ef10ab4909174286ab1d922fa930f02b81dddf08d28eaef1a1
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.2.1":
version: 3.2.1
resolution: "@formatjs/icu-messageformat-parser@npm:3.2.1"
"@formatjs/icu-messageformat-parser@npm:3.3.0":
version: 3.3.0
resolution: "@formatjs/icu-messageformat-parser@npm:3.3.0"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/icu-skeleton-parser": "npm:2.0.7"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/icu-skeleton-parser": "npm:2.0.8"
tslib: "npm:^2.8.0"
checksum: 10/4e853881ad10472547e893648474841f7943b16c0262c9db7e92fd29d49214da0b5723e48f53872ad3673b3f4a9d11c6683e102f6a864efc78d3a4203a451dff
checksum: 10/a8f03e9799c9b4f682f6051ea09d3327abd93dbc1ce17b55e07ec45da7d6e3609c1c87a2ad1bfccbd7bdf54c903d98e8bbe06fa282c4dc44263c55b1927f154e
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:2.0.7":
version: 2.0.7
resolution: "@formatjs/icu-skeleton-parser@npm:2.0.7"
"@formatjs/icu-skeleton-parser@npm:2.0.8":
version: 2.0.8
resolution: "@formatjs/icu-skeleton-parser@npm:2.0.8"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/ecma402-abstract": "npm:3.0.8"
tslib: "npm:^2.8.0"
checksum: 10/4a496854f877af04f87ac9bcf36593bfe4ec949a58a695c40c87dcbfff4a15473941448d6812644ab6fc832fc96408d6374015de74d3842cb03654f136b783d1
checksum: 10/9917502129e663da5ec8008a9a27c9032e01477adcf4c71788eb475225f57f4602711f9b8f3a3b669c801b0232e9eb53e58c0fdb11aa81e2339e1416f636be3f
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:7.1.1":
version: 7.1.1
resolution: "@formatjs/intl-datetimeformat@npm:7.1.1"
"@formatjs/intl-datetimeformat@npm:7.1.2":
version: 7.1.2
resolution: "@formatjs/intl-datetimeformat@npm:7.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/cda6fd9d6244ad11f9ed2d780971388643eba514397417d63de48a8aaac2ccc43ecc2a4331e4458f0ccc16d29516ae1d1addd5b790edbea49432d58ccdf4e10e
checksum: 10/e31d35aa8c38b5fd08254726d2d90629629534b5bbc134f2264b60eb1bce36224526112d6ceb2c4eeaed73b29e77c21d3fc8a906b767e28613798dd4ea0819cc
languageName: node
linkType: hard
"@formatjs/intl-displaynames@npm:7.1.1":
version: 7.1.1
resolution: "@formatjs/intl-displaynames@npm:7.1.1"
"@formatjs/intl-displaynames@npm:7.1.2":
version: 7.1.2
resolution: "@formatjs/intl-displaynames@npm:7.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
tslib: "npm:^2.8.0"
checksum: 10/f7b44c546b26cdd50b60983f5a8cfa672ac5b0a03f9f3c85caac98bc20b6768721e67fde62b977cfa98c3720f9cb1dadfa5448be88a3ee5616f48b1f84b1474f
checksum: 10/8c2a98d8f5fb6361d380beaae107cf58ce9ab963ea291e37081dc896407686160683837259d634e9a46a4a41d6c0bee93403de33c929d91ec320ee7bcfaed20b
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:0.9.1":
version: 0.9.1
resolution: "@formatjs/intl-durationformat@npm:0.9.1"
"@formatjs/intl-durationformat@npm:0.9.2":
version: 0.9.2
resolution: "@formatjs/intl-durationformat@npm:0.9.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
tslib: "npm:^2.8.0"
checksum: 10/b9fae713b39238c4658102b0edbb684e786007e2c95b8135435009b5c7c64c4a5d8cd50b92326f9d2c4e2b1fbbb61f49ee0709daad82dbf90636cd79713596d5
checksum: 10/7c4492640683aa30fbcbd844878e50eb44ef7aa0a415fd6cf10a1b8804cc80324437b4f8121bac2d56d3f4ec4d3c61e14e870852ec6b233a6c5747292fbc3585
languageName: node
linkType: hard
"@formatjs/intl-enumerator@npm:2.1.1":
version: 2.1.1
resolution: "@formatjs/intl-enumerator@npm:2.1.1"
"@formatjs/intl-enumerator@npm:2.1.2":
version: 2.1.2
resolution: "@formatjs/intl-enumerator@npm:2.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/ecma402-abstract": "npm:3.0.8"
tslib: "npm:^2.8.0"
checksum: 10/cabc2387bead8498c32e3068b98557a3e217777bbccf46ee2eb4918decab15ae750cf20f4b6186deecb3e0242873c0aba40e2294665683dabe1f1f67333b5195
checksum: 10/1a551a3a3d4f113d071b503946ea8aaf6b18b69fa746cc6d7e2ca520023794f4f1b82777cd7e9632ea8980473b951c252d52b8375f264e5c151c6cac6a19d6e5
languageName: node
linkType: hard
"@formatjs/intl-getcanonicallocales@npm:3.1.1":
version: 3.1.1
resolution: "@formatjs/intl-getcanonicallocales@npm:3.1.1"
"@formatjs/intl-getcanonicallocales@npm:3.1.2":
version: 3.1.2
resolution: "@formatjs/intl-getcanonicallocales@npm:3.1.2"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/23e6a3c1cf42257f130fd6b1cc080ecaf7b267743e3990ea7eb9f6cc981c649f2c18c935d1f581593ec241095a982c248441ac10d0e871575463dbfebf942f5c
checksum: 10/490f92e9b4c8d887df9afc602fbc857dc47f9e4cca29e73fcb92e35083cdef6091bcacce59f9d8f75334a3010ce41d9ae16143c1b1827550e29d0d01558516b4
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:8.1.1":
version: 8.1.1
resolution: "@formatjs/intl-listformat@npm:8.1.1"
"@formatjs/intl-listformat@npm:8.1.2":
version: 8.1.2
resolution: "@formatjs/intl-listformat@npm:8.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
tslib: "npm:^2.8.0"
checksum: 10/29daf29492f396a6266324196af764123b9bbd19fcd25ed02dfccbc4f31dcc592be47e02c39d8b5c1f8b8411f22ca961f82b4c66a98e7e44dd8cfa3459ec42f3
checksum: 10/ce9f5d821bdd9ec0b973f8869b17fcb84808442a1d43209099a6e4a0c44d5bd2a978c33d61a0929ba6e02aef5bbea2d36129e16c311f433b3cfa6ab7c07f51d2
languageName: node
linkType: hard
"@formatjs/intl-locale@npm:5.1.1":
version: 5.1.1
resolution: "@formatjs/intl-locale@npm:5.1.1"
"@formatjs/intl-locale@npm:5.1.2":
version: 5.1.2
resolution: "@formatjs/intl-locale@npm:5.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-enumerator": "npm:2.1.1"
"@formatjs/intl-getcanonicallocales": "npm:3.1.1"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-enumerator": "npm:2.1.2"
"@formatjs/intl-getcanonicallocales": "npm:3.1.2"
tslib: "npm:^2.8.0"
checksum: 10/02be5269e4ef1d058d5be8a0fe580f775e6a760ed4417ebf43a651bc6f0234833528ab76d64df276857d53d90ca7fcd022d0f5d667df3be485c85ae7a9bee617
checksum: 10/85b1257231461e9ccf7ba0b4fc65d97a7a4e4cbb7fdf50da209fe0b91fb56e00f19e3ca8bacf67eb581feda6ceb861112d9cc4624805e19c80e8d0b63535b821
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.7.4":
version: 0.7.4
resolution: "@formatjs/intl-localematcher@npm:0.7.4"
"@formatjs/intl-localematcher@npm:0.7.5":
version: 0.7.5
resolution: "@formatjs/intl-localematcher@npm:0.7.5"
dependencies:
"@formatjs/fast-memoize": "npm:3.0.2"
"@formatjs/fast-memoize": "npm:3.0.3"
tslib: "npm:^2.8.0"
checksum: 10/e2eb759628b35b45d95bddfc33bc8e985afd5d17c3d22bd243ea537fcb3b22eba2bbd8ce9e0f5cd6e29c3b514fe077ba99f5e7e5a1730983c9bfad649dc76985
checksum: 10/9899304f23206c6fceb66e25d073278f64b2f02b66b81d5f982f3d5b644e83d81ba74520352289b3db3f8afd932c959a4631784386bbcb74105b8126101ea9ac
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:9.1.1":
version: 9.1.1
resolution: "@formatjs/intl-numberformat@npm:9.1.1"
"@formatjs/intl-numberformat@npm:9.1.2":
version: 9.1.2
resolution: "@formatjs/intl-numberformat@npm:9.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/dd2e0e331162f7d4253dcaebbfcead2e5a13669348890b042fe80d109088e930d08f09a6c74969ccd5e3fd3a3190a5d0c6013645e2cde289d8dc425ae1b3ea3a
checksum: 10/9eef02a74154a9f7c8c9856073bbb588290e0382672da2d6075c003256549dd491657056e86cd5c33916cefb78af8c9a0fd6695e6051e711d4179418e4f427a7
languageName: node
linkType: hard
"@formatjs/intl-pluralrules@npm:6.1.1":
version: 6.1.1
resolution: "@formatjs/intl-pluralrules@npm:6.1.1"
"@formatjs/intl-pluralrules@npm:6.1.2":
version: 6.1.2
resolution: "@formatjs/intl-pluralrules@npm:6.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/94b900a5396bb066e61fc76af01e4634b60935a66ae27e57d148210dacc5ba7d95ee1dd2158ece1b240da50a9f88d44929af9bae95db6839f025f6ed03625dc9
checksum: 10/e1f611094eb1d1dafccdf815933062e014ac73c1bfba2ded7654d429d1366be15d77b06f8a00e165d530e6b3488995ef38fb73114e0065edc71eda7cb1eb767c
languageName: node
linkType: hard
"@formatjs/intl-relativetimeformat@npm:12.1.1":
version: 12.1.1
resolution: "@formatjs/intl-relativetimeformat@npm:12.1.1"
"@formatjs/intl-relativetimeformat@npm:12.1.2":
version: 12.1.2
resolution: "@formatjs/intl-relativetimeformat@npm:12.1.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/intl-localematcher": "npm:0.7.4"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/intl-localematcher": "npm:0.7.5"
tslib: "npm:^2.8.0"
checksum: 10/102a694f7b9764aa3e43702af4a2e47e6c9df7116815810a3210d80919ffe4b60e8a2098ebd424d3074df052b5fc3b5846ff6691a09f53103186253a006e5354
checksum: 10/bb324d20baf9e20588425196bef49f6096578bb7b90317b5c356b775e4b36ea8eb39e11565d692bd02b9f20c2d3021f07c559bf58f03817d685ea0c322f851cd
languageName: node
linkType: hard
@@ -3993,92 +3993,92 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-darwin-arm64@npm:1.7.0"
"@rspack/binding-darwin-arm64@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-darwin-arm64@npm:1.7.1"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-darwin-x64@npm:1.7.0"
"@rspack/binding-darwin-x64@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-darwin-x64@npm:1.7.1"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.0"
"@rspack/binding-linux-arm64-gnu@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.1"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.0"
"@rspack/binding-linux-arm64-musl@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.1"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.0"
"@rspack/binding-linux-x64-gnu@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.1"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-linux-x64-musl@npm:1.7.0"
"@rspack/binding-linux-x64-musl@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-linux-x64-musl@npm:1.7.1"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-wasm32-wasi@npm:1.7.0"
"@rspack/binding-wasm32-wasi@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-wasm32-wasi@npm:1.7.1"
dependencies:
"@napi-rs/wasm-runtime": "npm:1.0.7"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.0"
"@rspack/binding-win32-arm64-msvc@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.1"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.0"
"@rspack/binding-win32-ia32-msvc@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.1"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.0"
"@rspack/binding-win32-x64-msvc@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.1"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/binding@npm:1.7.0"
"@rspack/binding@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/binding@npm:1.7.1"
dependencies:
"@rspack/binding-darwin-arm64": "npm:1.7.0"
"@rspack/binding-darwin-x64": "npm:1.7.0"
"@rspack/binding-linux-arm64-gnu": "npm:1.7.0"
"@rspack/binding-linux-arm64-musl": "npm:1.7.0"
"@rspack/binding-linux-x64-gnu": "npm:1.7.0"
"@rspack/binding-linux-x64-musl": "npm:1.7.0"
"@rspack/binding-wasm32-wasi": "npm:1.7.0"
"@rspack/binding-win32-arm64-msvc": "npm:1.7.0"
"@rspack/binding-win32-ia32-msvc": "npm:1.7.0"
"@rspack/binding-win32-x64-msvc": "npm:1.7.0"
"@rspack/binding-darwin-arm64": "npm:1.7.1"
"@rspack/binding-darwin-x64": "npm:1.7.1"
"@rspack/binding-linux-arm64-gnu": "npm:1.7.1"
"@rspack/binding-linux-arm64-musl": "npm:1.7.1"
"@rspack/binding-linux-x64-gnu": "npm:1.7.1"
"@rspack/binding-linux-x64-musl": "npm:1.7.1"
"@rspack/binding-wasm32-wasi": "npm:1.7.1"
"@rspack/binding-win32-arm64-msvc": "npm:1.7.1"
"@rspack/binding-win32-ia32-msvc": "npm:1.7.1"
"@rspack/binding-win32-x64-msvc": "npm:1.7.1"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4100,23 +4100,23 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/152155585fe2b31c68efc1ddaa3272bcff04f483220d6b5c1ac4e35d69622261233657d66e3e0d73f28c08ccfe31742e4c284a9e35da82ee2cf32f896a6bd87b
checksum: 10/2ebdcfb4787aa6b41f143e386915c6afddea306ea7edddf094071030e8ec49ee4ba84d1bf1ddf4813b34dba8df884bd782eebb90df5244f3c33831f85369877f
languageName: node
linkType: hard
"@rspack/core@npm:1.7.0":
version: 1.7.0
resolution: "@rspack/core@npm:1.7.0"
"@rspack/core@npm:1.7.1":
version: 1.7.1
resolution: "@rspack/core@npm:1.7.1"
dependencies:
"@module-federation/runtime-tools": "npm:0.22.0"
"@rspack/binding": "npm:1.7.0"
"@rspack/binding": "npm:1.7.1"
"@rspack/lite-tapable": "npm:1.1.0"
peerDependencies:
"@swc/helpers": ">=0.5.1"
peerDependenciesMeta:
"@swc/helpers":
optional: true
checksum: 10/79786219475e74ef2e8bb418b33e6fa39eb152639e64a83ee3887427c536d3eac4a09fda1b589f5806d62fda96b4b0350ae1bc8b26e93049be97eaae21dd6a02
checksum: 10/0fe6c0d1fa9ffe9c6b4c0032c6c69b50bdd4ff5d3dbe508ca68630053e1413ef249b3bc2a423e5e16c0bfa1de5da3c39337e87976a22eb86039fc0226ed1880d
languageName: node
linkType: hard
@@ -4949,138 +4949,138 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.51.0"
"@typescript-eslint/eslint-plugin@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.52.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.51.0"
"@typescript-eslint/type-utils": "npm:8.51.0"
"@typescript-eslint/utils": "npm:8.51.0"
"@typescript-eslint/visitor-keys": "npm:8.51.0"
ignore: "npm:^7.0.0"
"@eslint-community/regexpp": "npm:^4.12.2"
"@typescript-eslint/scope-manager": "npm:8.52.0"
"@typescript-eslint/type-utils": "npm:8.52.0"
"@typescript-eslint/utils": "npm:8.52.0"
"@typescript-eslint/visitor-keys": "npm:8.52.0"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.2.0"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
"@typescript-eslint/parser": ^8.51.0
"@typescript-eslint/parser": ^8.52.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/cb6c129a9f7e723218458ec86acb86176f7cddc1b41924416478a31fd633c2991849be56e21dca8df0ce535ea2014b361073b043085e39795816de528747c80b
checksum: 10/4f2a2ada2597cfa22c913d436a4ce3f0d20fa17445dda0c6b3eb6088c4b0d1d8ba0ebc0a72c6a1577a3e58c96f7a2f260c201646cb1fb0308a0e248cc9d81cca
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/parser@npm:8.51.0"
"@typescript-eslint/parser@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/parser@npm:8.52.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.51.0"
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/typescript-estree": "npm:8.51.0"
"@typescript-eslint/visitor-keys": "npm:8.51.0"
debug: "npm:^4.3.4"
"@typescript-eslint/scope-manager": "npm:8.52.0"
"@typescript-eslint/types": "npm:8.52.0"
"@typescript-eslint/typescript-estree": "npm:8.52.0"
"@typescript-eslint/visitor-keys": "npm:8.52.0"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/ad2cb5d9cea5749fca712699ef971d68506f6170766d465ecc5864b5b5a15401c85d4042231df9edcc200f14b2579e6d4b2fbe66db824d925193004cde02ba79
checksum: 10/f221411fb3cc6c5a9e9fa6bec45cd16d5e5d7c1eeba331c97dae97756103bd4b5f67956e2288d478ad96cce7bc4c3c91b510b06d54283c7c0c86acaf4cdb4abf
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/project-service@npm:8.51.0"
"@typescript-eslint/project-service@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/project-service@npm:8.52.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.51.0"
"@typescript-eslint/types": "npm:^8.51.0"
debug: "npm:^4.3.4"
"@typescript-eslint/tsconfig-utils": "npm:^8.52.0"
"@typescript-eslint/types": "npm:^8.52.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/f8b38bf1c92c3a5d33ae0304fc920b8ac20b24c308c056ea70fb5b257f9447be62fe54e9595260fd8611b36c8a66229e4ca7ef859edad42e1321434986132aac
checksum: 10/bfa786007ed4a603fb8f31c6e354f0ba0cca576b03e402584ae3cf0d674f07adfbde9e976a5bf165fa44c484d4b4f310bd18b34d1b0e75b4210253edbdaabb87
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/scope-manager@npm:8.51.0"
"@typescript-eslint/scope-manager@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/scope-manager@npm:8.52.0"
dependencies:
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/visitor-keys": "npm:8.51.0"
checksum: 10/3d873589cd5b39f2bdc1e5b4f0052095f2cc5b2d59115ceea99533346839539ad7123ab0ba43ff4dd28c6afd4e8ae77798beb3618c93f24010104d38dedad6ad
"@typescript-eslint/types": "npm:8.52.0"
"@typescript-eslint/visitor-keys": "npm:8.52.0"
checksum: 10/89d9c04cd2567e6aa9adcbe85e2eab24fbc64bde5a33c688764e7c896e9a02c06aad2ec88e8bdc4d5bfabadbc510906a0cb4f3e0b73a5b80d10218f7a6a4ea27
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.51.0, @typescript-eslint/tsconfig-utils@npm:^8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.51.0"
"@typescript-eslint/tsconfig-utils@npm:8.52.0, @typescript-eslint/tsconfig-utils@npm:^8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.52.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/1a423e2b8bb6900e86f54ef78e967f17e6f51a528c64d78460338b916fda4e44bb8c73a7aba6b129295c59c3f364edbf27b34580f525df700674afbf0fc034df
checksum: 10/5b26227ab549e20a6b15725a4f8373acb70ae1c83570c8d670e242bfcd22ac0c9111d4d28ea16ee3939572caacce50e113388ce943f238fc2ca17f6c5a040cd2
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/type-utils@npm:8.51.0"
"@typescript-eslint/type-utils@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/type-utils@npm:8.52.0"
dependencies:
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/typescript-estree": "npm:8.51.0"
"@typescript-eslint/utils": "npm:8.51.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.2.0"
"@typescript-eslint/types": "npm:8.52.0"
"@typescript-eslint/typescript-estree": "npm:8.52.0"
"@typescript-eslint/utils": "npm:8.52.0"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/1dd5b72e4c7d6ffeafff1c24f745c335dc4073ec2a5c7e40f1a05be3be91097246c96be8d927389d8e4ccfa15db946a45ac85dbfd0c788d6cf003dced1830c7d
checksum: 10/e46d77192e2678561e2cdefe2c2b1ba8965458a88e6dd0d1967656ff5dcb00b75217ec6b084323710028215f64a65ba6ec288e5b021a0c9a325450b4bcfc4f43
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/types@npm:8.51.0"
checksum: 10/34c4b602d2f07edb72879e8af5359b0d54d3787030ae0a8325691972b07f2c6429bef9586938d18ff7df03a2be858064c7890a42136514ff0f2b7847c1432193
"@typescript-eslint/types@npm:8.52.0, @typescript-eslint/types@npm:^8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/types@npm:8.52.0"
checksum: 10/05a630c5d25cce74d1bfa51027f1232f2e8a97a8f483ce0274e928373b4633cdf713be53eca39926f0372d52a3335f13786c7910d2edfd546a0cf1d66b3bcf51
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/typescript-estree@npm:8.51.0"
"@typescript-eslint/typescript-estree@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/typescript-estree@npm:8.52.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.51.0"
"@typescript-eslint/tsconfig-utils": "npm:8.51.0"
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/visitor-keys": "npm:8.51.0"
debug: "npm:^4.3.4"
minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0"
"@typescript-eslint/project-service": "npm:8.52.0"
"@typescript-eslint/tsconfig-utils": "npm:8.52.0"
"@typescript-eslint/types": "npm:8.52.0"
"@typescript-eslint/visitor-keys": "npm:8.52.0"
debug: "npm:^4.4.3"
minimatch: "npm:^9.0.5"
semver: "npm:^7.7.3"
tinyglobby: "npm:^0.2.15"
ts-api-utils: "npm:^2.2.0"
ts-api-utils: "npm:^2.4.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/4f9f82b18b9a62e3ffe1253e7914a396ec1c550fec36f862eff9d3d49be4bf01349eb2540be68ecc35b245aab7f72843d2b5cd1e73c8e820db5cf6f3c6542916
checksum: 10/4e699f44a05e9c487531557a1eaf6412a97f370ec946a03596c8d445f584c3d17e9aa34cde5ce8998ae9d6908c1daffb2c9b523cb07e5988cf249eae6dea50fd
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/utils@npm:8.51.0"
"@typescript-eslint/utils@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/utils@npm:8.52.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.51.0"
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/typescript-estree": "npm:8.51.0"
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.52.0"
"@typescript-eslint/types": "npm:8.52.0"
"@typescript-eslint/typescript-estree": "npm:8.52.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e5ced67d08129e62fd6866a72b94fa685cd33aa7f5d1fc3bb9a34187d0fc0b469062ec8dfd44cd8c06494af070a050a314a6db672dda392fbc907b64b1ecc57f
checksum: 10/11a02ab0fd26bb1284dfa8c02d40c54cabd3aa795e82ab26b060ea3839998c28a41822b075f9d23fb51e291e465147213166c8ddaf3c8d5807e70b0a4345d967
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/visitor-keys@npm:8.51.0"
"@typescript-eslint/visitor-keys@npm:8.52.0":
version: 8.52.0
resolution: "@typescript-eslint/visitor-keys@npm:8.52.0"
dependencies:
"@typescript-eslint/types": "npm:8.51.0"
"@typescript-eslint/types": "npm:8.52.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/922fb3d2e59f6e8a9583a1bc7b13e66d4ec7bd7df3256602810465a7017549b890cd1a7897efc71ed64cc287c718be5dddbc8071b74e8ee41fe441f6d5251d3d
checksum: 10/4d841402cc65e876382ede464b68cf167c7d24905b15225c472516bb759140abbef02f250c6335ca35327f7328975ff3b28c3249a5183319cfd01f1d5541e3c1
languageName: node
linkType: hard
@@ -6887,7 +6887,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.1":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.1, debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
@@ -9003,25 +9003,25 @@ __metadata:
"@babel/preset-env": "npm:7.28.5"
"@babel/runtime": "npm:7.28.4"
"@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.7"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.8"
"@codemirror/autocomplete": "npm:6.20.0"
"@codemirror/commands": "npm:6.10.1"
"@codemirror/language": "npm:6.12.1"
"@codemirror/legacy-modes": "npm:6.5.2"
"@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.3"
"@codemirror/view": "npm:6.39.8"
"@codemirror/view": "npm:6.39.9"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:7.1.1"
"@formatjs/intl-displaynames": "npm:7.1.1"
"@formatjs/intl-durationformat": "npm:0.9.1"
"@formatjs/intl-getcanonicallocales": "npm:3.1.1"
"@formatjs/intl-listformat": "npm:8.1.1"
"@formatjs/intl-locale": "npm:5.1.1"
"@formatjs/intl-numberformat": "npm:9.1.1"
"@formatjs/intl-pluralrules": "npm:6.1.1"
"@formatjs/intl-relativetimeformat": "npm:12.1.1"
"@formatjs/intl-datetimeformat": "npm:7.1.2"
"@formatjs/intl-displaynames": "npm:7.1.2"
"@formatjs/intl-durationformat": "npm:0.9.2"
"@formatjs/intl-getcanonicallocales": "npm:3.1.2"
"@formatjs/intl-listformat": "npm:8.1.2"
"@formatjs/intl-locale": "npm:5.1.2"
"@formatjs/intl-numberformat": "npm:9.1.2"
"@formatjs/intl-pluralrules": "npm:6.1.2"
"@formatjs/intl-relativetimeformat": "npm:12.1.2"
"@fullcalendar/core": "npm:6.1.20"
"@fullcalendar/daygrid": "npm:6.1.20"
"@fullcalendar/interaction": "npm:6.1.20"
@@ -9066,7 +9066,7 @@ __metadata:
"@octokit/rest": "npm:22.0.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.4.0"
"@rspack/core": "npm:1.7.0"
"@rspack/core": "npm:1.7.1"
"@rspack/dev-server": "npm:1.1.5"
"@swc/helpers": "npm:0.5.18"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -9136,7 +9136,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:11.0.8"
intl-messageformat: "npm:11.0.9"
js-yaml: "npm:4.1.1"
jsdom: "npm:27.4.0"
jszip: "npm:3.10.1"
@@ -9144,9 +9144,9 @@ __metadata:
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
lint-staged: "npm:16.2.7"
lit: "npm:3.3.1"
lit: "npm:3.3.2"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.1"
lit-html: "npm:3.3.2"
lodash.merge: "npm:4.6.2"
lodash.template: "npm:4.5.0"
luxon: "npm:3.7.2"
@@ -9173,9 +9173,9 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.51.0"
typescript-eslint: "npm:8.52.0"
ua-parser-js: "npm:2.0.7"
vite-tsconfig-paths: "npm:6.0.3"
vite-tsconfig-paths: "npm:6.0.4"
vitest: "npm:4.0.16"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
@@ -9447,7 +9447,7 @@ __metadata:
languageName: node
linkType: hard
"ignore@npm:^7.0.0, ignore@npm:^7.0.3":
"ignore@npm:^7.0.3, ignore@npm:^7.0.5":
version: 7.0.5
resolution: "ignore@npm:7.0.5"
checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6
@@ -9543,15 +9543,15 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:11.0.8":
version: 11.0.8
resolution: "intl-messageformat@npm:11.0.8"
"intl-messageformat@npm:11.0.9":
version: 11.0.9
resolution: "intl-messageformat@npm:11.0.9"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.7"
"@formatjs/fast-memoize": "npm:3.0.2"
"@formatjs/icu-messageformat-parser": "npm:3.2.1"
"@formatjs/ecma402-abstract": "npm:3.0.8"
"@formatjs/fast-memoize": "npm:3.0.3"
"@formatjs/icu-messageformat-parser": "npm:3.3.0"
tslib: "npm:^2.8.0"
checksum: 10/e6b1317c683732c5c9677d897e049669b89569422f96edd8160d11ae252a5795ba62f27812b3ec2be57ca88e49ac336be6608642edbd2c2b36515f55b13385e9
checksum: 10/36481eabd854d623ae9941476cad1dce720efa9c40dbb8cb036b11b1ffda6804689b0cf9da5d2421268cb1138a49f7a0b553fcf8fed03b5a37af283baf326bb4
languageName: node
linkType: hard
@@ -10521,12 +10521,12 @@ __metadata:
languageName: node
linkType: hard
"lit-html@npm:3.3.1":
version: 3.3.1
resolution: "lit-html@npm:3.3.1"
"lit-html@npm:3.3.2":
version: 3.3.2
resolution: "lit-html@npm:3.3.2"
dependencies:
"@types/trusted-types": "npm:^2.0.2"
checksum: 10/7b438ca58670313beac29e4bb3b56f4ac8150def10b9fa222ad04f1caf6189a194ced8bfed3296cf1c94003a1bd1960ef4c22b00bbeda15fda6b7be398b27e9f
checksum: 10/5e938739641e312673e722eb41d5ca48dfcf69277e665c72dadd6514f53fc3dc4d5172cf8ca6af67736df1bce267f6189b3ef6fcb4125386b2490cd10c266b8f
languageName: node
linkType: hard
@@ -10541,14 +10541,14 @@ __metadata:
languageName: node
linkType: hard
"lit@npm:3.3.1":
version: 3.3.1
resolution: "lit@npm:3.3.1"
"lit@npm:3.3.2":
version: 3.3.2
resolution: "lit@npm:3.3.2"
dependencies:
"@lit/reactive-element": "npm:^2.1.0"
lit-element: "npm:^4.2.0"
lit-html: "npm:^3.3.0"
checksum: 10/ec70ff33db610537fd7cfc608cc7728126ecfae2d5593aa94844c614d2f6840448f1b995a58aeba593b0bf0e8169af5988036c11d3c5b55313fe8e722417c17d
checksum: 10/0af8e06a0ef5bb6a84c3dd58b64b6b554baf4bc195d725951ae60bbd9999618364815dc4438bf949051e7f15497b71b2bb9f7514772b33c4526882bdfb805858
languageName: node
linkType: hard
@@ -10948,7 +10948,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^9.0.4":
"minimatch@npm:^9.0.5":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
dependencies:
@@ -12686,7 +12686,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.7.3":
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.7.3":
version: 7.7.3
resolution: "semver@npm:7.7.3"
bin:
@@ -13873,12 +13873,12 @@ __metadata:
languageName: node
linkType: hard
"ts-api-utils@npm:^2.2.0":
version: 2.3.0
resolution: "ts-api-utils@npm:2.3.0"
"ts-api-utils@npm:^2.4.0":
version: 2.4.0
resolution: "ts-api-utils@npm:2.4.0"
peerDependencies:
typescript: ">=4.8.4"
checksum: 10/33472e0aa22222947512bcbdad37f078a6a4fb8f79e5ce271267f3520e7303ab204067cc7eaa508b2e4f91539b0bb9ecb9254f5f62397d27cf52dc5e73540a8f
checksum: 10/d6b2b3b6caad8d2f4ddc0c3785d22bb1a6041773335a1c71d73a5d67d11d993763fe8e4faefc4a4d03bb42b26c6126bbcf2e34826baed1def5369d0ebad358fa
languageName: node
linkType: hard
@@ -14041,18 +14041,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.51.0":
version: 8.51.0
resolution: "typescript-eslint@npm:8.51.0"
"typescript-eslint@npm:8.52.0":
version: 8.52.0
resolution: "typescript-eslint@npm:8.52.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.51.0"
"@typescript-eslint/parser": "npm:8.51.0"
"@typescript-eslint/typescript-estree": "npm:8.51.0"
"@typescript-eslint/utils": "npm:8.51.0"
"@typescript-eslint/eslint-plugin": "npm:8.52.0"
"@typescript-eslint/parser": "npm:8.52.0"
"@typescript-eslint/typescript-estree": "npm:8.52.0"
"@typescript-eslint/utils": "npm:8.52.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/491dc8b6fb56f69c94515c31f6ff1db961d79c06e647f5db55647cca34dacedff4bab879a211559fc46e925a7a85ff60a1a68b7eac1e8a5db37828c57a65c718
checksum: 10/c12d55948c903549b779bc5a46aab85825be63679118c6fefeb8dcab21173e17665b054ab23cf6e57ac0549fb2f6edcaace03cde165d61a9f0dccadb03e2ab59
languageName: node
linkType: hard
@@ -14426,25 +14426,26 @@ __metadata:
languageName: node
linkType: hard
"vite-tsconfig-paths@npm:6.0.3":
version: 6.0.3
resolution: "vite-tsconfig-paths@npm:6.0.3"
"vite-tsconfig-paths@npm:6.0.4":
version: 6.0.4
resolution: "vite-tsconfig-paths@npm:6.0.4"
dependencies:
debug: "npm:^4.1.1"
globrex: "npm:^0.1.2"
tsconfck: "npm:^3.0.3"
vite: "npm:*"
peerDependencies:
vite: "*"
peerDependenciesMeta:
vite:
optional: true
checksum: 10/271cdc040a03181d13e4253e99ec6deec1cbe4ec4cb2a1377d46dba403483f8e38a5c2ed29ad0493ed51c2cfd658847cb1cdc73e438d6a8bcdd2b52df19d72e2
checksum: 10/85f871cd5e321f2865972559b01c518664e6e34f9039b630dd77c2f379f8fdc386e15f7237aa5c108d813030c6e9bc8edfbf61687df7684803111a2495edadac
languageName: node
linkType: hard
"vite@npm:^6.0.0 || ^7.0.0":
version: 7.3.0
resolution: "vite@npm:7.3.0"
"vite@npm:*, vite@npm:^6.0.0 || ^7.0.0":
version: 7.3.1
resolution: "vite@npm:7.3.1"
dependencies:
esbuild: "npm:^0.27.0"
fdir: "npm:^6.5.0"
@@ -14493,7 +14494,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10/044490133aaf4cc024700995edaac10ff182262c721a44a8ac7839207b3e5af435c8b9dd8ee4658dc0f47147fa4211632cca3120177aa4bab99a7cb095e5149e
checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208
languageName: node
linkType: hard