Compare commits

..

31 Commits

Author SHA1 Message Date
Petar Petrov
882682d0ae fix import 2025-11-12 16:48:25 +02:00
Petar Petrov
ec22937e71 fix import 2025-11-12 11:50:40 +02:00
Petar Petrov
53a87278dd PR comments 2025-11-12 11:41:06 +02:00
Petar Petrov
31383c114b Unify CalendarEventRestData and CalendarEventSubscriptionData
Both interfaces had identical structures, so unified them into a single
CalendarEventSubscriptionData interface that is used for both REST API
responses and WebSocket subscription data.

Changes:
- Removed CalendarEventRestData interface
- Updated fetchCalendarEvents to use CalendarEventSubscriptionData
- Added documentation clarifying the interface is used for both APIs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 16:29:50 +02:00
Petar Petrov
7e01777eaa Replace any types with proper TypeScript types
Added proper types for calendar event data:
- CalendarDateValue: Union type for date values (string | {dateTime} | {date})
- CalendarEventRestData: Interface for REST API event responses
- Updated fetchCalendarEvents to use CalendarEventRestData[]
- Updated CalendarEventSubscriptionData to use CalendarDateValue
- Updated getCalendarDate to use proper type guards with 'in' operator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 15:34:10 +02:00
Petar Petrov
9c64f5ac8b Move date normalization into normalizeSubscriptionEventData
The getCalendarDate helper is part of the normalization process and should be inside the normalization function. This makes normalizeSubscriptionEventData handle both REST API format (with dateTime/date objects) and subscription format (plain strings).

Changes:
- Moved getCalendarDate into normalizeSubscriptionEventData
- Updated CalendarEventSubscriptionData to accept string | any for start/end
- Made normalizeSubscriptionEventData return CalendarEvent | null for invalid dates
- Simplified fetchCalendarEvents to use the shared normalization
- Added null filtering in calendar card and panel event handlers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 15:32:31 +02:00
Petar Petrov
912e636207 Refactor fetchCalendarEvents to use shared normalization utility
Eliminated code duplication by reusing normalizeSubscriptionEventData() in
fetchCalendarEvents(). After extracting date strings from the REST API
response format, we now convert to a subscription-like format and pass
it to the shared utility.

This ensures consistent event normalization across both REST API and
WebSocket subscription code paths, reducing maintenance burden and
potential for divergence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 15:25:31 +02:00
Petar Petrov
43367350b7 Update src/panels/calendar/ha-panel-calendar.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 13:24:02 +02:00
Petar Petrov
64a25cf7f9 Update src/panels/lovelace/cards/hui-calendar-card.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 13:23:51 +02:00
Petar Petrov
6de8f47e24 Update src/panels/lovelace/cards/hui-calendar-card.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 13:23:42 +02:00
Petar Petrov
0427c17a76 Address additional review comments
Fixed remaining issues from code review:

1. **Added @state decorator to _errorCalendars**: Ensures proper reactivity
   in calendar card when errors occur or are cleared, triggering UI updates.

2. **Fixed error accumulation in panel calendar**: Panel now properly
   accumulates errors from multiple calendars similar to the card
   implementation, preventing previously failed calendars from being
   hidden when new errors occur.

3. **Removed duplicate subscription check**: Deleted redundant duplicate
   subscription prevention in _requestSelected() since
   _subscribeCalendarEvents() already handles this at lines 221-227.

Note: The [nitpick] comment about loading states during await is a
performance enhancement suggestion, not a required fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:14:30 +02:00
Petar Petrov
0052f14521 Extract event normalization to shared utility function
Reduced code duplication by extracting the calendar event normalization
logic from both hui-calendar-card.ts and ha-panel-calendar.ts into a
shared utility function in calendar.ts.

The normalizeSubscriptionEventData() function handles the conversion
from subscription format (start/end) to internal format (dtstart/dtend)
in a single, reusable location.

This improves maintainability by ensuring consistent event normalization
across all calendar components and reduces the risk of divergence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:04:04 +02:00
Petar Petrov
792274a82a Address PR review comments
Fixes based on Copilot review feedback:

1. **Fixed race conditions**: Made _unsubscribeAll() async and await it
   before creating new subscriptions to prevent old subscription events
   from updating UI after new subscriptions are created.

2. **Added error handling**: All unsubscribe operations now catch errors
   to handle cases where subscriptions may have already been closed.

3. **Fixed type safety**: Replaced 'any' type with proper
   CalendarEventSubscriptionData type and added interface definition
   for subscription response data structure.

4. **Improved error tracking**: Calendar card now accumulates errors from
   multiple calendars instead of only showing the last error.

5. **Prevented duplicate subscriptions**: Added checks to unsubscribe
   existing subscriptions before creating new ones in both
   _subscribeCalendarEvents and _requestSelected.

6. **Fixed null handling**: Properly convert null values to undefined
   for CalendarEventData fields to match expected types.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 12:58:32 +02:00
Petar Petrov
7b42b16de8 Fix calendar subscription data format mismatch
The websocket subscription returns event data with fields named start and end, but the frontend was expecting dtstart and dtend. This caused events to not display because the data wasn't being properly mapped.

Now properly transform the subscription response format:
- Subscription format: start/end/summary/description
- Internal format: dtstart/dtend/summary/description

This ensures both initial event loading and real-time updates work correctly.
2025-11-11 12:14:42 +02:00
Petar Petrov
fe98c0bdc0 Fix calendar events not loading on initial render
Remove premature subscription attempt in setConfig() that was failing because the date range wasn't available yet. The subscription now properly happens when the view-changed event fires from ha-full-calendar after initial render, which includes the necessary start/end dates.

This ensures calendar events load immediately when the component is first displayed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 12:05:38 +02:00
Petar Petrov
658955a1b9 Use websocket subscription for calendar events
Replace polling-based calendar event fetching with real-time websocket subscriptions. This leverages the new subscription API added in core to provide automatic updates when calendar events change, eliminating the need for periodic polling.

The subscription pattern follows the same approach used for todo items, with proper lifecycle management and cleanup.

Related: home-assistant/core#156340
Related: #27565

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 11:44:00 +02:00
ildar170975
9a627bdea7 Data tables: remove unneeded "direction: asc" lines (#27903)
* remove unneeded "direction: asc"

* remove unneeded "direction: asc"

* remove unneeded "direction: asc"
2025-11-11 08:14:03 +02:00
Norbert Rittel
d9a67f603d Fix grammar in new_automation_setup_failed_text (#27898)
* Fix grammar in `new_automation_setup_failed_text`

* Remove excessive comma
2025-11-10 18:48:26 +01:00
Wendelin
5f37f8c0ab Use generic picker for target-picker (#27850)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-10 17:15:16 +01:00
Petar Petrov
f222702abf Fix entity name in statistics chart (#27896) 2025-11-10 15:08:33 +01:00
Copilot
2107b7c267 Fix doubled tooltips on timeline charts for mobile devices (#27888)
* Initial plan

* Fix doubled date popups in timeline charts on mobile

Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>

* Add comment explaining triggerTooltip fix

* Actual fix

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-10 14:31:30 +01:00
Petar Petrov
1c05afebd7 Smooth sensor card more when "Show more detail" is disabled (#27891)
* Smooth sensor card more when "Show more detail" is disabled

* Set minimum sample points to 10
2025-11-10 14:23:26 +01:00
Paul Bottein
7179bb2d26 Assume default visible true for panels (#27894) 2025-11-10 15:04:14 +02:00
Wendelin
95cf1fdcf7 Fix target picker for entity_id: none (#27893)
Fix notFound condition to exclude 'none' in ha-target-picker-item-row
2025-11-10 12:23:16 +00:00
renovate[bot]
9617956cc6 Update vitest monorepo to v4.0.8 (#27892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 14:15:36 +02:00
karwosts
65df464731 Fix entity editor with non-existant entity (#27875) 2025-11-10 14:09:44 +02:00
Wendelin
bd4e9a3d05 Use ha-ripple in ha-md-list-item (#27889) 2025-11-10 14:04:30 +02:00
ildar170975
963fc13a99 relative_time: increase thresholds (#27870)
* increase thresholds

* restored for days & hours
2025-11-10 12:58:13 +02:00
renovate[bot]
ff614918d4 Update vaadinWebComponents monorepo to v24.9.5 (#27884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 12:55:03 +02:00
ildar170975
48aa5fb970 hui-generic-entity-row: add tooltips for relative-time (#27871)
* add tooltips for relative-rime

* lint

* fix import

* prettier

* move a call of uid() to a private property

* some test change
2025-11-10 12:54:26 +02:00
ildar170975
190af65756 Display tooltips for labels (#27613)
* add a description fo ha-label

* add a description fo ha-label

* add a description fo ha-label

* add a description fo ha-label

* add a description fo ha-label

* add a description fo ha-label

* add a description fo ha-label

* add a description for ha-label

* add ha-tooltip for ha-input-chip

* add ha-tooltip

* replace() -> replaceAll()

* replace() -> replaceAll()

* prettier

* fix styles to enlarge an "active tooltip area"

* additional check for null for "description"

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* simplify a check for description

* call uid() in constructor

* fix a check for null

* attempting to bypass insecure randomness

* move a call of uid() into constructor

* uid generation tweak

* Apply suggestions from code review

* prettier

* simplify

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-10 10:04:31 +00:00
48 changed files with 1697 additions and 1818 deletions

View File

@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1", "@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0", "@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.4", "@vaadin/combo-box": "24.9.5",
"@vaadin/vaadin-themable-mixin": "24.9.4", "@vaadin/vaadin-themable-mixin": "24.9.5",
"@vibrant/color": "4.0.0", "@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0", "@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10", "@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.7", "@vitest/coverage-v8": "4.0.8",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
@@ -219,7 +219,7 @@
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.46.3", "typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.7", "vitest": "4.0.8",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -119,8 +119,8 @@ type Thresholds = Record<
>; >;
export const DEFAULT_THRESHOLDS: Thresholds = { export const DEFAULT_THRESHOLDS: Thresholds = {
second: 45, // seconds to minute second: 59, // seconds to minute
minute: 45, // minutes to hour minute: 59, // minutes to hour
hour: 22, // hour to day hour: 22, // hour to day
day: 5, // day to week day: 5, // day to week
week: 4, // week to months week: 4, // week to months

View File

@@ -6,7 +6,8 @@ export function downSampleLineData<
data: T[] | undefined, data: T[] | undefined,
maxDetails: number, maxDetails: number,
minX?: number, minX?: number,
maxX?: number maxX?: number,
useMean = false
): T[] { ): T[] {
if (!data) { if (!data) {
return []; return [];
@@ -17,15 +18,13 @@ export function downSampleLineData<
const min = minX ?? getPointData(data[0]!)[0]; const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0]; const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.ceil((max - min) / Math.floor(maxDetails)); const step = Math.ceil((max - min) / Math.floor(maxDetails));
const frames = new Map<
number,
{
min: { point: (typeof data)[number]; x: number; y: number };
max: { point: (typeof data)[number]; x: number; y: number };
}
>();
// Group points into frames // Group points into frames
const frames = new Map<
number,
{ point: (typeof data)[number]; x: number; y: number }[]
>();
for (const point of data) { for (const point of data) {
const pointData = getPointData(point); const pointData = getPointData(point);
if (!Array.isArray(pointData)) continue; if (!Array.isArray(pointData)) continue;
@@ -36,28 +35,53 @@ export function downSampleLineData<
const frameIndex = Math.floor((x - min) / step); const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex); const frame = frames.get(frameIndex);
if (!frame) { if (!frame) {
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } }); frames.set(frameIndex, [{ point, x, y }]);
} else { } else {
if (frame.min.y > y) { frame.push({ point, x, y });
frame.min = { point, x, y };
}
if (frame.max.y < y) {
frame.max = { point, x, y };
}
} }
} }
// Convert frames back to points // Convert frames back to points
const result: T[] = []; const result: T[] = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy if (useMean) {
// The order of the data must be preserved so max may be before min // Use mean values for each frame
if (frame.min.x > frame.max.x) { for (const [_i, framePoints] of frames) {
result.push(frame.max.point); const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
const meanY = sumY / framePoints.length;
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
const meanX = sumX / framePoints.length;
const firstPoint = framePoints[0].point;
const pointData = getPointData(firstPoint);
const meanPoint = (
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
) as T;
result.push(meanPoint);
}
} else {
// Use min/max values for each frame
for (const [_i, framePoints] of frames) {
let minPoint = framePoints[0];
let maxPoint = framePoints[0];
for (const p of framePoints) {
if (p.y < minPoint.y) {
minPoint = p;
}
if (p.y > maxPoint.y) {
maxPoint = p;
}
}
// The order of the data must be preserved so max may be before min
if (minPoint.x > maxPoint.x) {
result.push(maxPoint.point);
}
result.push(minPoint.point);
if (minPoint.x < maxPoint.x) {
result.push(maxPoint.point);
} }
result.push(frame.min.point);
if (frame.min.x < frame.max.x) {
result.push(frame.max.point);
} }
} }

View File

@@ -427,6 +427,7 @@ export class HaChartBase extends LitElement {
...axis.axisPointer?.handle, ...axis.axisPointer?.handle,
show: true, show: true,
}, },
label: { show: false },
}, },
} }
: axis : axis

View File

@@ -62,6 +62,7 @@ class HaDataTableLabels extends LitElement {
@click=${clickAction ? this._labelClicked : undefined} @click=${clickAction ? this._labelClicked : undefined}
@keydown=${clickAction ? this._labelClicked : undefined} @keydown=${clickAction ? this._labelClicked : undefined}
style=${color ? `--color: ${color}` : ""} style=${color ? `--color: ${color}` : ""}
.description=${label.description}
> >
${label?.icon ${label?.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`

View File

@@ -197,9 +197,6 @@ export class HaDevicePicker extends LitElement {
const placeholder = const placeholder =
this.placeholder ?? this.placeholder ??
this.hass.localize("ui.components.device-picker.placeholder"); this.hass.localize("ui.components.device-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.device-picker.no_match"
);
const valueRenderer = this._valueRenderer(this._configEntryLookup); const valueRenderer = this._valueRenderer(this._configEntryLookup);
@@ -209,7 +206,10 @@ export class HaDevicePicker extends LitElement {
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.searchLabel=${this.searchLabel} .searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.device-picker.no_devices"
)}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
@@ -233,6 +233,11 @@ export class HaDevicePicker extends LitElement {
this.value = value; this.value = value;
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.device-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -269,9 +269,6 @@ export class HaEntityPicker extends LitElement {
const placeholder = const placeholder =
this.placeholder ?? this.placeholder ??
this.hass.localize("ui.components.entity.entity-picker.placeholder"); this.hass.localize("ui.components.entity.entity-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.entity.entity-picker.no_match"
);
return html` return html`
<ha-generic-picker <ha-generic-picker
@@ -282,7 +279,7 @@ export class HaEntityPicker extends LitElement {
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.searchLabel=${this.searchLabel} .searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value} .value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
@@ -356,6 +353,11 @@ export class HaEntityPicker extends LitElement {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
fireEvent(this, "change"); fireEvent(this, "change");
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.entity.entity-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -271,7 +271,6 @@ export class HaStatisticPicker extends LitElement {
const secondary = [areaName, entityName ? deviceName : undefined] const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean) .filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ "); .join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`; const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
output.push({ output.push({
@@ -279,7 +278,6 @@ export class HaStatisticPicker extends LitElement {
statistic_id: id, statistic_id: id,
primary, primary,
secondary, secondary,
a11y_label: a11yLabel,
stateObj: stateObj, stateObj: stateObj,
type: "entity", type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"), sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
@@ -458,9 +456,6 @@ export class HaStatisticPicker extends LitElement {
const placeholder = const placeholder =
this.placeholder ?? this.placeholder ??
this.hass.localize("ui.components.statistic-picker.placeholder"); this.hass.localize("ui.components.statistic-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.statistic-picker.no_match"
);
return html` return html`
<ha-generic-picker <ha-generic-picker
@@ -468,7 +463,10 @@ export class HaStatisticPicker extends LitElement {
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.label=${this.label} .label=${this.label}
.notFoundLabel=${notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.statistic-picker.no_statistics"
)}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
@@ -521,6 +519,11 @@ export class HaStatisticPicker extends LitElement {
await this.updateComplete; await this.updateComplete;
await this._picker?.open(); await this._picker?.open();
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.statistic-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -369,9 +369,10 @@ export class HaAreaPicker extends LitElement {
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.area-picker.no_match" .emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
)} .disabled=${this.disabled}
.required=${this.required}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
@@ -425,6 +426,11 @@ export class HaAreaPicker extends LitElement {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
fireEvent(this, "change"); fireEvent(this, "change");
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.area-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -109,7 +109,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
.selected=${(this.value || []).includes(label.label_id)} .selected=${(this.value || []).includes(label.label_id)}
hasMeta hasMeta
> >
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon ? html`<ha-icon
slot="icon" slot="icon"

View File

@@ -383,8 +383,9 @@ export class HaFloorPicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.floor-picker.no_match" .emptyLabel=${this.hass.localize(
"ui.components.floor-picker.no_floors"
)} )}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
@@ -444,6 +445,11 @@ export class HaFloorPicker extends LitElement {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
fireEvent(this, "change"); fireEvent(this, "change");
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.floor-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -25,9 +25,6 @@ import "./ha-svg-icon";
export class HaGenericPicker extends LitElement { export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@@ -49,8 +46,11 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean }) @property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false; public hideClearIcon = false;
@property({ attribute: false, type: Array }) @property({ attribute: false })
public getItems?: () => PickerComboBoxItem[]; public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@property({ attribute: false, type: Array }) @property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[]; public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@@ -64,8 +64,11 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>; public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ attribute: "not-found-label", type: String }) @property({ attribute: false })
public notFoundLabel?: string; public notFoundLabel?: string | ((search: string) => string);
@property({ attribute: "empty-label" })
public emptyLabel?: string;
@property({ attribute: "popover-placement" }) @property({ attribute: "popover-placement" })
public popoverPlacement: public popoverPlacement:
@@ -85,6 +88,25 @@ export class HaGenericPicker extends LitElement {
/** If set picker shows an add button instead of textbox when value isn't set */ /** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string; @property({ attribute: "add-button-label" }) public addButtonLabel?: string;
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
@property({ attribute: false }) public sections?: (
| {
id: string;
label: string;
}
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@query(".container") private _containerElement?: HTMLDivElement; @query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -97,6 +119,11 @@ export class HaGenericPicker extends LitElement {
@state() private _openedNarrow = false; @state() private _openedNarrow = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
private _narrow = false; private _narrow = false;
// helper to set new value after closing picker, to avoid flicker // helper to set new value after closing picker, to avoid flicker
@@ -189,16 +216,19 @@ export class HaGenericPicker extends LitElement {
<ha-picker-combo-box <ha-picker-combo-box
.hass=${this.hass} .hass=${this.hass}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ?? .label=${this.searchLabel}
(this.hass?.localize("ui.common.search") || "Search")}
.value=${this.value} .value=${this.value}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer} .rowRenderer=${this.rowRenderer}
.notFoundLabel=${this.notFoundLabel} .notFoundLabel=${this.notFoundLabel}
.emptyLabel=${this.emptyLabel}
.getItems=${this.getItems} .getItems=${this.getItems}
.getAdditionalItems=${this.getAdditionalItems} .getAdditionalItems=${this.getAdditionalItems}
.searchFn=${this.searchFn} .searchFn=${this.searchFn}
.mode=${dialogMode ? "dialog" : "popover"} .mode=${dialogMode ? "dialog" : "popover"}
.sections=${this.sections}
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
></ha-picker-combo-box> ></ha-picker-combo-box>
`; `;
} }

View File

@@ -224,8 +224,9 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.label-picker.no_match" .emptyLabel=${this.hass.localize(
"ui.components.label-picker.no_labels"
)} )}
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")} .addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
.placeholder=${placeholder} .placeholder=${placeholder}
@@ -288,6 +289,11 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.label-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -1,17 +1,32 @@
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { uid } from "../common/util/uid";
import "./ha-tooltip";
@customElement("ha-label") @customElement("ha-label")
class HaLabel extends LitElement { class HaLabel extends LitElement {
@property({ type: Boolean, reflect: true }) dense = false; @property({ type: Boolean, reflect: true }) dense = false;
@property({ attribute: "description" })
public description?: string;
private _elementId = "label-" + uid();
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-tooltip
.for=${this._elementId}
.disabled=${!this.description?.trim()}
>
${this.description}
</ha-tooltip>
<div class="container" .id=${this._elementId}>
<span class="content"> <span class="content">
<slot name="icon"></slot> <slot name="icon"></slot>
<slot></slot> <slot></slot>
</span> </span>
</div>
`; `;
} }
@@ -36,9 +51,7 @@ class HaLabel extends LitElement {
font-weight: var(--ha-font-weight-medium); font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed); line-height: var(--ha-line-height-condensed);
letter-spacing: 0.1px; letter-spacing: 0.1px;
vertical-align: middle;
height: 32px; height: 32px;
padding: 0 16px;
border-radius: var(--ha-border-radius-xl); border-radius: var(--ha-border-radius-xl);
color: var(--ha-label-text-color); color: var(--ha-label-text-color);
--mdc-icon-size: 12px; --mdc-icon-size: 12px;
@@ -66,15 +79,24 @@ class HaLabel extends LitElement {
display: flex; display: flex;
} }
.container {
display: flex;
position: relative;
height: 100%;
padding: 0 16px;
}
span { span {
display: inline-flex; display: inline-flex;
} }
:host([dense]) { :host([dense]) {
height: 20px; height: 20px;
padding: 0 12px;
border-radius: var(--ha-border-radius-md); border-radius: var(--ha-border-radius-md);
} }
:host([dense]) .container {
padding: 0 12px;
}
:host([dense]) ::slotted([slot="icon"]) { :host([dense]) ::slotted([slot="icon"]) {
margin-right: 4px; margin-right: 4px;
margin-left: -4px; margin-left: -4px;

View File

@@ -21,6 +21,7 @@ import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-label-picker"; import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker"; import type { HaLabelPicker } from "./ha-label-picker";
import "./ha-tooltip";
@customElement("ha-labels-picker") @customElement("ha-labels-picker")
export class HaLabelsPicker extends SubscribeMixin(LitElement) { export class HaLabelsPicker extends SubscribeMixin(LitElement) {
@@ -142,9 +143,17 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
const color = label?.color const color = label?.color
? computeCssColor(label.color) ? computeCssColor(label.color)
: undefined; : undefined;
const elementId = "label-" + label.label_id;
return html` return html`
<ha-tooltip
.for=${elementId}
.disabled=${!label?.description?.trim()}
>
${label?.description}
</ha-tooltip>
<ha-input-chip <ha-input-chip
.item=${label} .item=${label}
.id=${elementId}
@remove=${this._removeItem} @remove=${this._removeItem}
@click=${this._openDetail} @click=${this._openDetail}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@@ -125,9 +125,10 @@ export class HaLanguagePicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
popover-placement="bottom-end" popover-placement="bottom-end"
.notFoundLabel=${this.hass?.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.language-picker.no_match" .emptyLabel=${this.hass?.localize(
)} "ui.components.language-picker.no_languages"
) || "No languages available"}
.placeholder=${this.label ?? .placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") || (this.hass?.localize("ui.components.language-picker.language") ||
"Language")} "Language")}
@@ -172,6 +173,15 @@ export class HaLanguagePicker extends LitElement {
this.value = ev.detail.value; this.value = ev.detail.value;
fireEvent(this, "value-changed", { value: this.value }); fireEvent(this, "value-changed", { value: this.value });
} }
private _notFoundLabel = (search: string) => {
const term = html`<b>${search}</b>`;
return this.hass
? this.hass.localize("ui.components.language-picker.no_match", {
term,
})
: html`No languages found for ${term}`;
};
} }
declare global { declare global {

View File

@@ -1,7 +1,8 @@
import { ListItemEl } from "@material/web/list/internal/listitem/list-item"; import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
import { styles } from "@material/web/list/internal/listitem/list-item-styles"; import { styles } from "@material/web/list/internal/listitem/list-item-styles";
import { css } from "lit"; import { css, html, nothing, type TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "./ha-ripple";
export const haMdListStyles = [ export const haMdListStyles = [
styles, styles,
@@ -25,6 +26,18 @@ export const haMdListStyles = [
@customElement("ha-md-list-item") @customElement("ha-md-list-item")
export class HaMdListItem extends ListItemEl { export class HaMdListItem extends ListItemEl {
static override styles = haMdListStyles; static override styles = haMdListStyles;
protected renderRipple(): TemplateResult | typeof nothing {
if (this.type === "text") {
return nothing;
}
return html`<ha-ripple
part="ripple"
for="item"
?disabled=${this.disabled && this.type !== "link"}
></ha-ripple>`;
}
} }
declare global { declare global {

View File

@@ -1,6 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer"; import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify } from "@mdi/js"; import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { import {
@@ -14,11 +14,12 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { HaFuse } from "../resources/fuse"; import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer"; import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-icon"; import "./ha-icon";
import "./ha-textfield"; import "./ha-textfield";
@@ -27,28 +28,18 @@ import type { HaTextField } from "./ha-textfield";
export interface PickerComboBoxItem { export interface PickerComboBoxItem {
id: string; id: string;
primary: string; primary: string;
a11y_label?: string;
secondary?: string; secondary?: string;
search_labels?: string[]; search_labels?: string[];
sorting_label?: string; sorting_label?: string;
icon_path?: string; icon_path?: string;
icon?: string; icon?: string;
} }
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
// Hack to force empty label to always display empty value by default in the search field
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
a11y_label: string;
}
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = ( const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
item item
) => html` ) => html`
<ha-combo-box-item <ha-combo-box-item type="button" compact>
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
compact
>
${item.icon ${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>` ? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path : item.icon_path
@@ -87,8 +78,11 @@ export class HaPickerComboBox extends LitElement {
@state() private _listScrolled = false; @state() private _listScrolled = false;
@property({ attribute: false, type: Array }) @property({ attribute: false })
public getItems?: () => PickerComboBoxItem[]; public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@property({ attribute: false, type: Array }) @property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[]; public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@@ -96,21 +90,45 @@ export class HaPickerComboBox extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>; public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
@property({ attribute: "not-found-label", type: String }) @property({ attribute: false })
public notFoundLabel?: string; public notFoundLabel?: string | ((search: string) => string);
@property({ attribute: "empty-label" })
public emptyLabel?: string;
@property({ attribute: false }) @property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>; public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover"; @property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
@property({ attribute: false }) public sections?: (
| {
id: string;
label: string;
}
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer; @query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
@query("ha-textfield") private _searchFieldElement?: HaTextField; @query("ha-textfield") private _searchFieldElement?: HaTextField;
@state() private _items: PickerComboBoxItemWithLabel[] = []; @state() private _items: (PickerComboBoxItem | string)[] = [];
private _allItems: PickerComboBoxItemWithLabel[] = []; @state() private _sectionTitle?: string;
private _allItems: (PickerComboBoxItem | string)[] = [];
private _selectedItemIndex = -1; private _selectedItemIndex = -1;
@@ -121,6 +139,8 @@ export class HaPickerComboBox extends LitElement {
private _removeKeyboardShortcuts?: () => void; private _removeKeyboardShortcuts?: () => void;
private _search = "";
protected firstUpdated() { protected firstUpdated() {
this._registerKeyboardShortcuts(); this._registerKeyboardShortcuts();
} }
@@ -145,74 +165,142 @@ export class HaPickerComboBox extends LitElement {
"Search"} "Search"}
@input=${this._filterChanged} @input=${this._filterChanged}
></ha-textfield> ></ha-textfield>
${this._renderSectionButtons()}
${this.sections?.length
? html`
<div class="section-title-wrapper">
<div
class="section-title ${!this.selectedSection &&
this._sectionTitle
? "show"
: ""}"
>
${this._sectionTitle}
</div>
</div>
`
: nothing}
<lit-virtualizer <lit-virtualizer
@scroll=${this._onScrollList} .keyFunction=${this._keyFunction}
tabindex="0" tabindex="0"
scroller scroller
.items=${this._items} .items=${this._items}
.renderItem=${this._renderItem} .renderItem=${this._renderItem}
style="min-height: 36px;" style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""} class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList} @focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
> >
</lit-virtualizer> `; </lit-virtualizer> `;
} }
private _defaultNotFoundItem = memoizeOne( private _renderSectionButtons() {
( if (!this.sections || this.sections.length === 0) {
label: this["notFoundLabel"], return nothing;
localize?: LocalizeFunc }
): PickerComboBoxItemWithLabel => ({
id: NO_MATCHING_ITEMS_FOUND_ID,
primary:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
icon_path: mdiMagnify,
a11y_label:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
})
);
private _getAdditionalItems = (searchString?: string) => { return html`
const items = this.getAdditionalItems?.(searchString) || []; <ha-chip-set class="sections">
${this.sections.map((section) =>
section === "separator"
? html`<div class="separator"></div>`
: html`<ha-filter-chip
@click=${this._toggleSection}
.section-id=${section.id}
.selected=${this.selectedSection === section.id}
.label=${section.label}
>
</ha-filter-chip>`
)}
</ha-chip-set>
`;
}
return items.map<PickerComboBoxItemWithLabel>((item) => ({ @eventOptions({ passive: true })
...item, private _visibilityChanged(ev) {
a11y_label: item.a11y_label || item.primary, if (
})); this._virtualizerElement &&
}; this.sectionTitleFunction &&
this.sections?.length
) {
const firstItem = this._virtualizerElement.items[ev.first];
const secondItem = this._virtualizerElement.items[ev.first + 1];
this._sectionTitle = this.sectionTitleFunction({
firstIndex: ev.first,
lastIndex: ev.last,
firstItem: firstItem as PickerComboBoxItem | string,
secondItem: secondItem as PickerComboBoxItem | string,
itemsCount: this._virtualizerElement.items.length,
});
}
}
private _getItems = (): PickerComboBoxItemWithLabel[] => { private _getAdditionalItems = (searchString?: string) =>
const items = this.getItems ? this.getItems() : []; this.getAdditionalItems?.(searchString) || [];
const sortedItems = items private _getItems = () => {
.map<PickerComboBoxItemWithLabel>((item) => ({ let items = [
...item, ...(this.getItems
a11y_label: item.a11y_label || item.primary, ? this.getItems(this._search, this.selectedSection)
})) : []),
.sort((entityA, entityB) => ];
if (!this.sections?.length) {
items = items.sort((entityA, entityB) =>
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
entityA.sorting_label!, (entityA as PickerComboBoxItem).sorting_label!,
entityB.sorting_label!, (entityB as PickerComboBoxItem).sorting_label!,
this.hass?.locale.language ?? navigator.language this.hass?.locale.language ?? navigator.language
) )
); );
}
if (!sortedItems.length) { if (!items.length) {
sortedItems.push( items.push(NO_ITEMS_AVAILABLE_ID);
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
} }
const additionalItems = this._getAdditionalItems(); const additionalItems = this._getAdditionalItems();
sortedItems.push(...additionalItems); items.push(...additionalItems);
return sortedItems;
if (this.mode === "dialog") {
items.push("padding"); // padding for safe area inset
}
return items;
}; };
private _renderItem = (item: PickerComboBoxItem, index: number) => { private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
if (item === "padding") {
return html`<div class="bottom-padding"></div>`;
}
if (item === NO_ITEMS_AVAILABLE_ID) {
return html`
<div class="combo-box-row">
<ha-combo-box-item type="text" compact>
<ha-svg-icon
slot="start"
.path=${this._search ? mdiMagnify : mdiMinusBoxOutline}
></ha-svg-icon>
<span slot="headline"
>${this._search
? typeof this.notFoundLabel === "function"
? this.notFoundLabel(this._search)
: this.notFoundLabel ||
this.hass?.localize("ui.components.combo-box.no_match") ||
"No matching items found"
: this.emptyLabel ||
this.hass?.localize("ui.components.combo-box.no_items") ||
"No items available"}</span
>
</ha-combo-box-item>
</div>
`;
}
if (typeof item === "string") {
return html`<div class="title">${item}</div>`;
}
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER; const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
return html`<div return html`<div
id=${`list-item-${index}`} id=${`list-item-${index}`}
@@ -221,9 +309,7 @@ export class HaPickerComboBox extends LitElement {
.index=${index} .index=${index}
@click=${this._valueSelected} @click=${this._valueSelected}
> >
${item.id === NO_MATCHING_ITEMS_FOUND_ID ${renderer(item, index)}
? DEFAULT_ROW_RENDERER(item, index)
: renderer(item, index)}
</div>`; </div>`;
}; };
@@ -242,10 +328,6 @@ export class HaPickerComboBox extends LitElement {
const value = (ev.currentTarget as any).value as string; const value = (ev.currentTarget as any).value as string;
const newValue = value?.trim(); const newValue = value?.trim();
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
return;
}
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newValue });
}; };
@@ -256,15 +338,19 @@ export class HaPickerComboBox extends LitElement {
private _filterChanged = (ev: Event) => { private _filterChanged = (ev: Event) => {
const textfield = ev.target as HaTextField; const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim(); const searchString = textfield.value.trim();
this._search = searchString;
if (this.sections?.length) {
this._items = this._getItems();
} else {
if (!searchString) { if (!searchString) {
this._items = this._allItems; this._items = this._allItems;
return; return;
} }
const index = this._fuseIndex(this._allItems); const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
const fuse = new HaFuse( const fuse = new HaFuse(
this._allItems, this._allItems as PickerComboBoxItem[],
{ {
shouldSort: false, shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2), minMatchCharLength: Math.min(searchString.length, 2),
@@ -273,34 +359,62 @@ export class HaPickerComboBox extends LitElement {
); );
const results = fuse.multiTermsSearch(searchString); const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[]; let filteredItems = [...this._allItems];
if (results) { if (results) {
const items = results.map((result) => result.item); const items: (PickerComboBoxItem | string)[] = results.map(
if (items.length === 0) { (result) => result.item
items.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
); );
if (!items.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
} }
const additionalItems = this._getAdditionalItems(searchString);
const additionalItems = this._getAdditionalItems();
items.push(...additionalItems); items.push(...additionalItems);
filteredItems = items; filteredItems = items;
} }
if (this.searchFn) { if (this.searchFn) {
filteredItems = this.searchFn( filteredItems = this.searchFn(
searchString, searchString,
filteredItems, filteredItems as PickerComboBoxItem[],
this._allItems this._allItems as PickerComboBoxItem[]
); );
} }
this._items = filteredItems as PickerComboBoxItemWithLabel[]; this._items = filteredItems as PickerComboBoxItem[];
}
this._selectedItemIndex = -1; this._selectedItemIndex = -1;
if (this._virtualizerElement) { if (this._virtualizerElement) {
this._virtualizerElement.scrollTo(0, 0); this._virtualizerElement.scrollTo(0, 0);
} }
}; };
private _toggleSection(ev: Event) {
ev.stopPropagation();
this._resetSelectedItem();
this._sectionTitle = undefined;
const section = (ev.target as HTMLElement)["section-id"] as string;
if (!section) {
return;
}
if (this.selectedSection === section) {
this.selectedSection = undefined;
} else {
this.selectedSection = section;
}
this._items = this._getItems();
// Reset scroll position when filter changes
if (this._virtualizerElement) {
this._virtualizerElement.scrollToIndex(0);
}
}
private _registerKeyboardShortcuts() { private _registerKeyboardShortcuts() {
this._removeKeyboardShortcuts = tinykeys(this, { this._removeKeyboardShortcuts = tinykeys(this, {
ArrowUp: this._selectPreviousItem, ArrowUp: this._selectPreviousItem,
@@ -344,7 +458,7 @@ export class HaPickerComboBox extends LitElement {
return; return;
} }
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) { if (typeof items[nextIndex] === "string") {
// Skip titles, padding and empty search // Skip titles, padding and empty search
if (nextIndex === maxItems) { if (nextIndex === maxItems) {
return; return;
@@ -373,7 +487,7 @@ export class HaPickerComboBox extends LitElement {
return; return;
} }
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) { if (typeof items[nextIndex] === "string") {
// Skip titles, padding and empty search // Skip titles, padding and empty search
if (nextIndex === 0) { if (nextIndex === 0) {
return; return;
@@ -395,13 +509,6 @@ export class HaPickerComboBox extends LitElement {
const nextIndex = 0; const nextIndex = 0;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") { if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex + 1; this._selectedItemIndex = nextIndex + 1;
} else { } else {
@@ -419,13 +526,6 @@ export class HaPickerComboBox extends LitElement {
const nextIndex = this._virtualizerElement.items.length - 1; const nextIndex = this._virtualizerElement.items.length - 1;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") { if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex - 1; this._selectedItemIndex = nextIndex - 1;
} else { } else {
@@ -453,10 +553,7 @@ export class HaPickerComboBox extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem; const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if ( if (this._virtualizerElement?.items.length === 1) {
this._virtualizerElement?.items.length === 1 &&
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: firstItem.id, value: firstItem.id,
}); });
@@ -472,7 +569,7 @@ export class HaPickerComboBox extends LitElement {
const item = this._virtualizerElement?.items[ const item = this._virtualizerElement?.items[
this._selectedItemIndex this._selectedItemIndex
] as PickerComboBoxItem; ] as PickerComboBoxItem;
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) { if (item) {
fireEvent(this, "value-changed", { value: item.id }); fireEvent(this, "value-changed", { value: item.id });
} }
}; };
@@ -484,6 +581,9 @@ export class HaPickerComboBox extends LitElement {
this._selectedItemIndex = -1; this._selectedItemIndex = -1;
} }
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
static styles = [ static styles = [
haStyleScrollbar, haStyleScrollbar,
css` css`
@@ -558,6 +658,80 @@ export class HaPickerComboBox extends LitElement {
background-color: var(--ha-color-fill-neutral-normal-hover); background-color: var(--ha-color-fill-neutral-normal-hover);
} }
} }
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.title {
width: 100%;
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.section-title.show {
opacity: 1;
z-index: 1;
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`, `,
]; ];
} }

View File

@@ -158,7 +158,8 @@ export const computePanels = memoizeOne(
if ( if (
hiddenPanels.includes(panel.url_path) || hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel) || (!panel.title && panel.url_path !== defaultPanel) ||
(!panel.default_visible && !panelsOrder.includes(panel.url_path)) (panel.default_visible === false &&
!panelsOrder.includes(panel.url_path))
) { ) {
return; return;
} }

View File

@@ -1,15 +1,31 @@
import "@home-assistant/webawesome/dist/components/popover/popover"; import "@home-assistant/webawesome/dist/components/popover/popover";
import { consume } from "@lit/context";
// @ts-ignore // @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css"; import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { mdiPlaylistPlus } from "@mdi/js"; import { mdiPlus, mdiTextureBox } from "@mdi/js";
import Fuse from "fuse.js";
import type { HassServiceTarget } from "home-assistant-js-websocket"; import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing, unsafeCSS } from "lit"; import { LitElement, css, html, nothing, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { isValidEntityId } from "../common/entity/valid_entity_id"; import { isValidEntityId } from "../common/entity/valid_entity_id";
import { computeRTL } from "../common/util/compute_rtl";
import {
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../data/area_floor";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { labelsContext } from "../data/context";
import { getDevices, type DevicePickerItem } from "../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity"; import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
import { domainToName } from "../data/integration";
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
import { import {
areaMeetsFilter, areaMeetsFilter,
deviceMeetsFilter, deviceMeetsFilter,
@@ -18,18 +34,23 @@ import {
type TargetTypeFloorless, type TargetTypeFloorless,
} from "../data/target"; } from "../data/target";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { isHelperDomain } from "../panels/config/helpers/const";
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail"; import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
import { HaFuse } from "../resources/fuse";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-bottom-sheet"; import "./ha-generic-picker";
import "./ha-button"; import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-input-helper-text";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group"; import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-selector";
import type { HaTargetPickerSelector } from "./target-picker/ha-target-picker-selector";
import "./target-picker/ha-target-picker-value-chip"; import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@customElement("ha-target-picker") @customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) { export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -68,23 +89,54 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false; @property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
@state() private _open = false; @state() private _selectedSection?: TargetTypeFloorless;
@state() private _addTargetWidth = 0; @state() private _configEntryLookup: Record<string, ConfigEntry> = {};
@state() private _narrow = false; @state()
@consume({ context: labelsContext, subscribe: true })
@state() private _pickerFilter?: TargetTypeFloorless; private _labelRegistry!: LabelRegistryEntry[];
@state() private _pickerWrapperOpen = false;
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLDivElement;
@query("ha-target-picker-selector")
private _targetPickerSelectorElement?: HaTargetPickerSelector;
private _newTarget?: { type: TargetType; id: string }; private _newTarget?: { type: TargetType; id: string };
private _getDevicesMemoized = memoizeOne(getDevices);
private _getLabelsMemoized = memoizeOne(getLabels);
private _getEntitiesMemoized = memoizeOne(getEntities);
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
private get _showEntityId() {
return this.hass.userData?.showEntityIdPicker;
}
private _fuseIndexes = {
area: memoizeOne((states: FloorComboBoxItem[]) =>
this._createFuseIndex(states)
),
entity: memoizeOne((states: EntityComboBoxItem[]) =>
this._createFuseIndex(states)
),
device: memoizeOne((states: DevicePickerItem[]) =>
this._createFuseIndex(states)
),
label: memoizeOne((states: PickerComboBoxItem[]) =>
this._createFuseIndex(states)
),
};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._loadConfigEntries();
}
}
private _createFuseIndex = (states) =>
Fuse.createIndex(["search_labels"], states);
protected render() { protected render() {
if (this.addOnTop) { if (this.addOnTop) {
return html` ${this._renderPicker()} ${this._renderItems()} `; return html` ${this._renderPicker()} ${this._renderItems()} `;
@@ -289,139 +341,65 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
private _renderPicker() { private _renderPicker() {
const sections = [
{
id: "entity",
label: this.hass.localize("ui.components.target-picker.type.entities"),
},
{
id: "device",
label: this.hass.localize("ui.components.target-picker.type.devices"),
},
{
id: "area",
label: this.hass.localize("ui.components.target-picker.type.areas"),
},
"separator" as const,
{
id: "label",
label: this.hass.localize("ui.components.target-picker.type.labels"),
},
];
return html` return html`
<div class="add-target-wrapper"> <div class="add-target-wrapper">
<ha-button <ha-generic-picker
id="add-target-button"
size="small"
appearance="filled"
@click=${this._showPicker}
>
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.components.target-picker.add_target")}
</ha-button>
${!this._narrow && (this._pickerWrapperOpen || this._open)
? html`
<wa-popover
.open=${this._pickerWrapperOpen}
style="--body-width: ${this._addTargetWidth}px;"
without-arrow
distance="-4"
placement="bottom-start"
for="add-target-button"
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._showSelector}
@wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
>
${this._renderTargetSelector()}
</wa-popover>
`
: this._pickerWrapperOpen || this._open
? html`<ha-bottom-sheet
flexcontent
.open=${this._pickerWrapperOpen}
@wa-after-show=${this._showSelector}
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
>
${this._renderTargetSelector(true)}
</ha-bottom-sheet>`
: nothing}
</div>
${this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: nothing}
`;
}
connectedCallback() {
super.connectedCallback();
this._handleResize();
window.addEventListener("resize", this._handleResize);
}
public disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this._handleResize);
}
private _handleResize = () => {
this._narrow =
window.matchMedia("(max-width: 870px)").matches ||
window.matchMedia("(max-height: 500px)").matches;
};
private _showPicker() {
this._addTargetWidth = this._addTargetWrapper?.offsetWidth || 0;
this._pickerWrapperOpen = true;
}
// wait for drawer animation to finish
private _showSelector = () => {
this._open = true;
requestAnimationFrame(() => {
this._targetPickerSelectorElement?.focus();
});
};
private _handleUpdatePickerFilter(
ev: CustomEvent<TargetTypeFloorless | undefined>
) {
this._updatePickerFilter(
typeof ev.detail === "string" ? ev.detail : undefined
);
}
private _updatePickerFilter = (filter?: TargetTypeFloorless) => {
this._pickerFilter = filter;
};
private _hidePicker(ev) {
ev.stopPropagation();
this._open = false;
this._pickerWrapperOpen = false;
if (this._newTarget) {
this._addTarget(this._newTarget.id, this._newTarget.type);
this._newTarget = undefined;
}
}
private _renderTargetSelector(dialogMode = false) {
if (!this._open) {
return nothing;
}
return html`
<ha-target-picker-selector
.hass=${this.hass} .hass=${this.hass}
@filter-type-changed=${this._handleUpdatePickerFilter} .disabled=${this.disabled}
.filterType=${this._pickerFilter} .autofocus=${this.autofocus}
@target-picked=${this._handleTargetPicked} .helper=${this.helper}
@create-domain-picked=${this._handleCreateDomain} .sections=${sections}
.targetValue=${this.value} .notFoundLabel=${this._noTargetFoundLabel}
.deviceFilter=${this.deviceFilter} .emptyLabel=${this.hass.localize(
.entityFilter=${this.entityFilter} "ui.components.target-picker.no_targets"
.includeDomains=${this.includeDomains} )}
.includeDeviceClasses=${this.includeDeviceClasses} .sectionTitleFunction=${this._sectionTitleFunction}
.createDomains=${this.createDomains} .selectedSection=${this._selectedSection}
.mode=${dialogMode ? "dialog" : "popover"} .rowRenderer=${this._renderRow}
></ha-target-picker-selector> .getItems=${this._getItems}
@value-changed=${this._targetPicked}
.addButtonLabel=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
.getAdditionalItems=${this._getAdditionalItems}
>
</ha-generic-picker>
</div>
`; `;
} }
private _targetPicked(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value.startsWith(CREATE_ID)) {
this._createNewDomainElement(value.substring(CREATE_ID.length));
return;
}
const [type, id] = ev.detail.value.split(SEPARATOR);
this._addTarget(id, type as TargetType);
}
private _addTarget(id: string, type: TargetType) { private _addTarget(id: string, type: TargetType) {
const typeId = `${type}_id`; const typeId = `${type}_id`;
@@ -454,26 +432,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
?.removeAttribute("collapsed"); ?.removeAttribute("collapsed");
} }
private _handleTargetPicked = async ( private _createNewDomainElement = (domain: string) => {
ev: CustomEvent<{ type: TargetType; id: string }>
) => {
ev.stopPropagation();
this._pickerWrapperOpen = false;
if (!ev.detail.type || !ev.detail.id) {
return;
}
// save new target temporarily to add it after dialog closes
this._newTarget = ev.detail;
};
private _handleCreateDomain = (ev: CustomEvent<string>) => {
this._pickerWrapperOpen = false;
const domain = ev.detail;
showHelperDetailDialog(this, { showHelperDetailDialog(this, {
domain, domain,
dialogClosedCallback: (item) => { dialogClosedCallback: (item) => {
@@ -675,6 +634,465 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined; return undefined;
} }
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
firstItem,
secondItem,
itemsCount,
}: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => {
if (
firstItem === undefined ||
secondItem === undefined ||
typeof firstItem === "string" ||
(typeof secondItem === "string" && secondItem !== "padding") ||
(firstIndex === 0 && lastIndex === itemsCount - 1)
) {
return undefined;
}
const type = this._getRowType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
| "devices"
| "labels"
| undefined =
type === "area" || type === "floor"
? "areas"
: type === "entity"
? "entities"
: type && type !== "empty"
? `${type}s`
: undefined;
return translationType
? this.hass.localize(
`ui.components.target-picker.type.${translationType}`
)
: undefined;
};
private _getItems = (searchString: string, section: string) => {
this._selectedSection = section as TargetTypeFloorless | undefined;
return this._getItemsMemoized(
this.entityFilter,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.value,
searchString,
this._configEntryLookup,
this._selectedSection
);
};
private _getItemsMemoized = memoizeOne(
(
entityFilter: this["entityFilter"],
deviceFilter: this["deviceFilter"],
includeDomains: this["includeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
targetValue: this["value"],
searchTerm: string,
configEntryLookup: Record<string, ConfigEntry>,
filterType?: TargetTypeFloorless
) => {
const items: (
| string
| FloorComboBoxItem
| EntityComboBoxItem
| PickerComboBoxItem
)[] = [];
if (!filterType || filterType === "entity") {
let entities = this._getEntitiesMemoized(
this.hass,
includeDomains,
undefined,
entityFilter,
includeDeviceClasses,
undefined,
undefined,
targetValue?.entity_id
? ensureArray(targetValue.entity_id)
: undefined,
undefined,
`entity${SEPARATOR}`
);
if (searchTerm) {
entities = this._filterGroup(
"entity",
entities,
searchTerm,
(item: EntityComboBoxItem) =>
item.stateObj?.entity_id === searchTerm
) as EntityComboBoxItem[];
}
if (!filterType && entities.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.entities")
);
}
items.push(...entities);
}
if (!filterType || filterType === "device") {
let devices = this._getDevicesMemoized(
this.hass,
configEntryLookup,
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.device_id
? ensureArray(targetValue.device_id)
: undefined,
undefined,
`device${SEPARATOR}`
);
if (searchTerm) {
devices = this._filterGroup("device", devices, searchTerm);
}
if (!filterType && devices.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.devices")
);
}
items.push(...devices);
}
if (!filterType || filterType === "area") {
let areasAndFloors = this._getAreasAndFloorsMemoized(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
),
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.area_id ? ensureArray(targetValue.area_id) : undefined,
targetValue?.floor_id ? ensureArray(targetValue.floor_id) : undefined
);
if (searchTerm) {
areasAndFloors = this._filterGroup(
"area",
areasAndFloors,
searchTerm
) as FloorComboBoxItem[];
}
if (!filterType && areasAndFloors.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.areas")
);
}
items.push(
...areasAndFloors.map((item, index) => {
const nextItem = areasAndFloors[index + 1];
if (
!nextItem ||
(item.type === "area" && nextItem.type === "floor")
) {
return {
...item,
last: true,
};
}
return item;
})
);
}
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass,
this._labelRegistry,
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
`label${SEPARATOR}`
);
if (searchTerm) {
labels = this._filterGroup("label", labels, searchTerm);
}
if (!filterType && labels.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.labels")
);
}
items.push(...labels);
}
return items;
}
);
private _filterGroup(
type: TargetType,
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
searchTerm: string,
checkExact?: (
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
) => boolean
) {
const fuseIndex = this._fuseIndexes[type](items);
const fuse = new HaFuse(
items,
{
shouldSort: false,
minMatchCharLength: Math.min(searchTerm.length, 2),
},
fuseIndex
);
const results = fuse.multiTermsSearch(searchTerm);
let filteredItems = items;
if (results) {
filteredItems = results.map((result) => result.item);
}
if (!checkExact) {
return filteredItems;
}
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex((item) => checkExact(item));
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
}
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
private _getCreateItems = memoizeOne(
(createDomains: this["createDomains"]) => {
if (!createDomains?.length) {
return [];
}
return createDomains.map((domain) => {
const primary = this.hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
: domainToName(this.hass.localize, domain),
}
);
return {
id: CREATE_ID + domain,
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
});
}
);
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private _renderRow = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem,
index: number
) => {
if (!item) {
return nothing;
}
const type = this._getRowType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;
if (type === "area" || type === "floor") {
item.id = item[type]?.[`${type}_id`];
rtl = computeRTL(this.hass);
hasFloor =
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
}
if (type === "entity") {
showEntityId = !!this._showEntityId;
}
return html`
<ha-combo-box-item
id=${`list-item-${index}`}
tabindex="-1"
.type=${type === "empty" ? "text" : "button"}
class=${type === "empty" ? "empty" : ""}
style=${(item as FloorComboBoxItem).type === "area" && hasFloor
? "--md-list-item-leading-space: var(--ha-space-12);"
: ""}
>
${(item as FloorComboBoxItem).type === "area" && hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "var(--ha-space-12)",
position: "absolute",
top: "var(--ha-space-0)",
left: rtl ? undefined : "var(--ha-space-1)",
right: rtl ? "var(--ha-space-1)" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${(
item as FloorComboBoxItem & { last?: boolean | undefined }
).last}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path}
></ha-svg-icon>`
: type === "entity" && (item as EntityComboBoxItem).stateObj
? html`
<state-badge
slot="start"
.stateObj=${(item as EntityComboBoxItem).stateObj}
.hass=${this.hass}
></state-badge>
`
: type === "device" && (item as DevicePickerItem).domain
? html`
<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: (item as DevicePickerItem).domain!,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
})}
/>
`
: type === "floor"
? html`<ha-floor-icon
slot="start"
.floor=${(item as FloorComboBoxItem).floor!}
></ha-floor-icon>`
: type === "area"
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path || mdiTextureBox}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${(item as EntityComboBoxItem).stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${(item as EntityComboBoxItem).stateObj?.entity_id}
</span>
`
: nothing}
${(item as EntityComboBoxItem).domain_name &&
(type !== "entity" || !showEntityId)
? html`
<div slot="trailing-supporting-text" class="domain">
${(item as EntityComboBoxItem).domain_name}
</div>
`
: nothing}
</ha-combo-box-item>
`;
};
private _noTargetFoundLabel = (search: string) =>
this.hass.localize("ui.components.target-picker.no_target_found", {
term: html`<b>${search}</b>`,
});
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.add-target-wrapper { .add-target-wrapper {
@@ -683,31 +1101,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
margin-top: var(--ha-space-3); margin-top: var(--ha-space-3);
} }
wa-popover { ha-generic-picker {
--wa-space-l: var(--ha-space-0); width: 100%;
}
wa-popover::part(body) {
width: min(max(var(--body-width), 336px), 600px);
max-width: min(max(var(--body-width), 336px), 600px);
max-height: 500px;
height: 70vh;
overflow: hidden;
}
@media (max-height: 1000px) {
wa-popover::part(body) {
max-height: 400px;
}
}
ha-bottom-sheet {
--ha-bottom-sheet-height: 90vh;
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
--ha-bottom-sheet-max-width: 600px;
--ha-bottom-sheet-padding: var(--ha-space-0);
--ha-bottom-sheet-surface-background: var(--card-background-color);
} }
${unsafeCSS(chipStyles)} ${unsafeCSS(chipStyles)}

View File

@@ -545,7 +545,7 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item, name: entityName || deviceName || item,
context, context,
stateObject, stateObject,
notFound: !stateObject && item !== "all", notFound: !stateObject && item !== "all" && item !== "none",
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -128,9 +128,7 @@ class HaUserPicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.user-picker.no_match"
)}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
@@ -149,6 +147,11 @@ class HaUserPicker extends LitElement {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
fireEvent(this, "change"); fireEvent(this, "change");
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.user-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -53,6 +53,9 @@ export const enum CalendarEntityFeature {
UPDATE_EVENT = 4, UPDATE_EVENT = 4,
} }
/** Type for date values that can come from REST API or subscription */
type CalendarDateValue = string | { dateTime: string } | { date: string };
export const fetchCalendarEvents = async ( export const fetchCalendarEvents = async (
hass: HomeAssistant, hass: HomeAssistant,
start: Date, start: Date,
@@ -65,11 +68,11 @@ export const fetchCalendarEvents = async (
const calEvents: CalendarEvent[] = []; const calEvents: CalendarEvent[] = [];
const errors: string[] = []; const errors: string[] = [];
const promises: Promise<CalendarEvent[]>[] = []; const promises: Promise<CalendarEventApiData[]>[] = [];
calendars.forEach((cal) => { calendars.forEach((cal) => {
promises.push( promises.push(
hass.callApi<CalendarEvent[]>( hass.callApi<CalendarEventApiData[]>(
"GET", "GET",
`calendars/${cal.entity_id}${params}` `calendars/${cal.entity_id}${params}`
) )
@@ -77,7 +80,7 @@ export const fetchCalendarEvents = async (
}); });
for (const [idx, promise] of promises.entries()) { for (const [idx, promise] of promises.entries()) {
let result: CalendarEvent[]; let result: CalendarEventApiData[];
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
result = await promise; result = await promise;
@@ -87,53 +90,16 @@ export const fetchCalendarEvents = async (
} }
const cal = calendars[idx]; const cal = calendars[idx];
result.forEach((ev) => { result.forEach((ev) => {
const eventStart = getCalendarDate(ev.start); const normalized = normalizeSubscriptionEventData(ev, cal);
const eventEnd = getCalendarDate(ev.end); if (normalized) {
if (!eventStart || !eventEnd) { calEvents.push(normalized);
return;
} }
const eventData: CalendarEventData = {
uid: ev.uid,
summary: ev.summary,
description: ev.description,
dtstart: eventStart,
dtend: eventEnd,
recurrence_id: ev.recurrence_id,
rrule: ev.rrule,
};
const event: CalendarEvent = {
start: eventStart,
end: eventEnd,
title: ev.summary,
backgroundColor: cal.backgroundColor,
borderColor: cal.backgroundColor,
calendar: cal.entity_id,
eventData: eventData,
};
calEvents.push(event);
}); });
} }
return { events: calEvents, errors }; return { events: calEvents, errors };
}; };
const getCalendarDate = (dateObj: any): string | undefined => {
if (typeof dateObj === "string") {
return dateObj;
}
if (dateObj.dateTime) {
return dateObj.dateTime;
}
if (dateObj.date) {
return dateObj.date;
}
return undefined;
};
export const getCalendars = (hass: HomeAssistant): Calendar[] => export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states) Object.keys(hass.states)
.filter( .filter(
@@ -191,3 +157,89 @@ export const deleteCalendarEvent = (
recurrence_id, recurrence_id,
recurrence_range, recurrence_range,
}); });
/**
* Calendar event data from both REST API and WebSocket subscription.
* Both APIs use the same data format.
*/
export interface CalendarEventApiData {
summary: string;
start: CalendarDateValue;
end: CalendarDateValue;
description?: string | null;
location?: string | null;
uid?: string | null;
recurrence_id?: string | null;
rrule?: string | null;
}
export interface CalendarEventSubscription {
events: CalendarEventApiData[] | null;
}
export const subscribeCalendarEvents = (
hass: HomeAssistant,
entity_id: string,
start: Date,
end: Date,
callback: (update: CalendarEventSubscription) => void
) =>
hass.connection.subscribeMessage<CalendarEventSubscription>(callback, {
type: "calendar/event/subscribe",
entity_id,
start: start.toISOString(),
end: end.toISOString(),
});
const getCalendarDate = (dateObj: CalendarDateValue): string | undefined => {
if (typeof dateObj === "string") {
return dateObj;
}
if ("dateTime" in dateObj) {
return dateObj.dateTime;
}
if ("date" in dateObj) {
return dateObj.date;
}
return undefined;
};
/**
* Normalize calendar event data from API format to internal format.
* Handles both REST API format (with dateTime/date objects) and subscription format (strings).
* Converts to internal format with { dtstart, dtend, ... }
*/
export const normalizeSubscriptionEventData = (
eventData: CalendarEventApiData,
calendar: Calendar
): CalendarEvent | null => {
const eventStart = getCalendarDate(eventData.start);
const eventEnd = getCalendarDate(eventData.end);
if (!eventStart || !eventEnd) {
return null;
}
const normalizedEventData: CalendarEventData = {
summary: eventData.summary,
dtstart: eventStart,
dtend: eventEnd,
description: eventData.description ?? undefined,
uid: eventData.uid ?? undefined,
recurrence_id: eventData.recurrence_id ?? undefined,
rrule: eventData.rrule ?? undefined,
};
return {
start: eventStart,
end: eventEnd,
title: eventData.summary,
backgroundColor: calendar.backgroundColor,
borderColor: calendar.backgroundColor,
calendar: calendar.entity_id,
eventData: normalizedEventData,
};
};

View File

@@ -186,7 +186,8 @@ export const getDevices = (
deviceFilter?: HaDevicePickerDeviceFilterFunc, deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc, entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[], excludeDevices?: string[],
value?: string value?: string,
idPrefix = ""
): DevicePickerItem[] => { ): DevicePickerItem[] => {
const devices = Object.values(hass.devices); const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities); const entities = Object.values(hass.entities);
@@ -298,7 +299,7 @@ export const getDevices = (
const domainName = domain ? domainToName(hass.localize, domain) : undefined; const domainName = domain ? domainToName(hass.localize, domain) : undefined;
return { return {
id: device.id, id: `${idPrefix}${device.id}`,
label: "", label: "",
primary: primary:
deviceName || deviceName ||

View File

@@ -344,7 +344,8 @@ export const getEntities = (
includeUnitOfMeasurement?: string[], includeUnitOfMeasurement?: string[],
includeEntities?: string[], includeEntities?: string[],
excludeEntities?: string[], excludeEntities?: string[],
value?: string value?: string,
idPrefix = ""
): EntityComboBoxItem[] => { ): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = []; let items: EntityComboBoxItem[] = [];
@@ -395,10 +396,9 @@ export const getEntities = (
const secondary = [areaName, entityName ? deviceName : undefined] const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean) .filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ "); .join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return { return {
id: entityId, id: `${idPrefix}${entityId}`,
primary: primary, primary: primary,
secondary: secondary, secondary: secondary,
domain_name: domainName, domain_name: domainName,
@@ -411,7 +411,6 @@ export const getEntities = (
friendlyName, friendlyName,
entityId, entityId,
].filter(Boolean) as string[], ].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj, stateObj: stateObj,
}; };
}); });

View File

@@ -108,7 +108,8 @@ export const getLabels = (
includeDeviceClasses?: string[], includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc, deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc, entityFilter?: HaEntityPickerEntityFilterFunc,
excludeLabels?: string[] excludeLabels?: string[],
idPrefix = ""
): PickerComboBoxItem[] => { ): PickerComboBoxItem[] => {
if (!labels || labels.length === 0) { if (!labels || labels.length === 0) {
return []; return [];
@@ -262,7 +263,7 @@ export const getLabels = (
} }
const items = outputLabels.map<PickerComboBoxItem>((label) => ({ const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: label.label_id, id: `${idPrefix}${label.label_id}`,
primary: label.name, primary: label.name,
secondary: label.description ?? "", secondary: label.description ?? "",
icon: label.icon || undefined, icon: label.icon || undefined,

View File

@@ -105,7 +105,7 @@ class DialogEditSidebar extends LitElement {
// Add default hidden panels that are missing in hidden // Add default hidden panels that are missing in hidden
for (const panel of panels) { for (const panel of panels) {
if ( if (
!panel.default_visible && panel.default_visible === false &&
!this._order.includes(panel.url_path) && !this._order.includes(panel.url_path) &&
!this._hidden.includes(panel.url_path) !this._hidden.includes(panel.url_path)
) { ) {

View File

@@ -1,5 +1,6 @@
import { ResizeController } from "@lit-labs/observers/resize-controller"; import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { mdiChevronDown, mdiPlus, mdiRefresh } from "@mdi/js"; import { mdiChevronDown, mdiPlus, mdiRefresh } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -20,8 +21,17 @@ import "../../components/ha-menu-button";
import "../../components/ha-state-icon"; import "../../components/ha-state-icon";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import "../../components/ha-two-pane-top-app-bar-fixed"; import "../../components/ha-two-pane-top-app-bar-fixed";
import type { Calendar, CalendarEvent } from "../../data/calendar"; import type {
import { fetchCalendarEvents, getCalendars } from "../../data/calendar"; Calendar,
CalendarEvent,
CalendarEventSubscription,
CalendarEventApiData,
} from "../../data/calendar";
import {
getCalendars,
normalizeSubscriptionEventData,
subscribeCalendarEvents,
} from "../../data/calendar";
import { fetchIntegrationManifest } from "../../data/integration"; import { fetchIntegrationManifest } from "../../data/integration";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
@@ -42,6 +52,8 @@ class PanelCalendar extends LitElement {
@state() private _error?: string = undefined; @state() private _error?: string = undefined;
@state() private _errorCalendars: string[] = [];
@state() @state()
@storage({ @storage({
key: "deSelectedCalendars", key: "deSelectedCalendars",
@@ -53,6 +65,8 @@ class PanelCalendar extends LitElement {
private _end?: Date; private _end?: Date;
private _unsubs: Record<string, Promise<UnsubscribeFunc>> = {};
private _showPaneController = new ResizeController(this, { private _showPaneController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width > 750, callback: (entries) => entries[0]?.contentRect.width > 750,
}); });
@@ -78,6 +92,7 @@ class PanelCalendar extends LitElement {
super.disconnectedCallback(); super.disconnectedCallback();
this._mql?.removeListener(this._setIsMobile!); this._mql?.removeListener(this._setIsMobile!);
this._mql = undefined; this._mql = undefined;
this._unsubscribeAll();
} }
private _setIsMobile = (ev: MediaQueryListEvent) => { private _setIsMobile = (ev: MediaQueryListEvent) => {
@@ -194,19 +209,95 @@ class PanelCalendar extends LitElement {
.map((cal) => cal); .map((cal) => cal);
} }
private async _fetchEvents( private _subscribeCalendarEvents(calendars: Calendar[]): void {
start: Date | undefined, if (!this._start || !this._end || calendars.length === 0) {
end: Date | undefined, return;
calendars: Calendar[]
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
if (!calendars.length || !start || !end) {
return { events: [], errors: [] };
} }
return fetchCalendarEvents(this.hass, start, end, calendars); this._error = undefined;
calendars.forEach((calendar) => {
// Unsubscribe existing subscription if any
if (calendar.entity_id in this._unsubs) {
this._unsubs[calendar.entity_id]
.then((unsubFunc) => unsubFunc())
.catch(() => {
// Subscription may have already been closed
});
} }
private async _requestSelected(ev: CustomEvent<RequestSelectedDetail>) { const unsub = subscribeCalendarEvents(
this.hass,
calendar.entity_id,
this._start!,
this._end!,
(update: CalendarEventSubscription) => {
this._handleCalendarUpdate(calendar, update);
}
);
this._unsubs[calendar.entity_id] = unsub;
});
}
private _handleCalendarUpdate(
calendar: Calendar,
update: CalendarEventSubscription
): void {
// Remove events from this calendar
this._events = this._events.filter(
(event) => event.calendar !== calendar.entity_id
);
if (update.events === null) {
// Error fetching events
if (!this._errorCalendars.includes(calendar.entity_id)) {
this._errorCalendars = [...this._errorCalendars, calendar.entity_id];
}
this._handleErrors(this._errorCalendars);
return;
}
// Remove from error list if successfully loaded
this._errorCalendars = this._errorCalendars.filter(
(id) => id !== calendar.entity_id
);
this._handleErrors(this._errorCalendars);
// Add new events from this calendar
const newEvents: CalendarEvent[] = update.events
.map((eventData: CalendarEventApiData) =>
normalizeSubscriptionEventData(eventData, calendar)
)
.filter((event): event is CalendarEvent => event !== null);
this._events = [...this._events, ...newEvents];
}
private async _unsubscribeAll(): Promise<void> {
await Promise.all(
Object.values(this._unsubs).map((unsub) =>
unsub
.then((unsubFunc) => unsubFunc())
.catch(() => {
// Subscription may have already been closed
})
)
);
this._unsubs = {};
}
private _unsubscribeCalendar(entityId: string): void {
if (entityId in this._unsubs) {
this._unsubs[entityId]
.then((unsubFunc) => unsubFunc())
.catch(() => {
// Subscription may have already been closed
});
delete this._unsubs[entityId];
}
}
private _requestSelected(ev: CustomEvent<RequestSelectedDetail>) {
ev.stopPropagation(); ev.stopPropagation();
const entityId = (ev.target as HaListItem).value; const entityId = (ev.target as HaListItem).value;
if (ev.detail.selected) { if (ev.detail.selected) {
@@ -223,13 +314,10 @@ class PanelCalendar extends LitElement {
if (!calendar) { if (!calendar) {
return; return;
} }
const result = await this._fetchEvents(this._start, this._end, [ this._subscribeCalendarEvents([calendar]);
calendar,
]);
this._events = [...this._events, ...result.events];
this._handleErrors(result.errors);
} else { } else {
this._deSelectedCalendars = [...this._deSelectedCalendars, entityId]; this._deSelectedCalendars = [...this._deSelectedCalendars, entityId];
this._unsubscribeCalendar(entityId);
this._events = this._events.filter( this._events = this._events.filter(
(event) => event.calendar !== entityId (event) => event.calendar !== entityId
); );
@@ -254,23 +342,15 @@ class PanelCalendar extends LitElement {
): Promise<void> { ): Promise<void> {
this._start = ev.detail.start; this._start = ev.detail.start;
this._end = ev.detail.end; this._end = ev.detail.end;
const result = await this._fetchEvents( await this._unsubscribeAll();
this._start, this._events = [];
this._end, this._subscribeCalendarEvents(this._selectedCalendars);
this._selectedCalendars
);
this._events = result.events;
this._handleErrors(result.errors);
} }
private async _handleRefresh(): Promise<void> { private async _handleRefresh(): Promise<void> {
const result = await this._fetchEvents( await this._unsubscribeAll();
this._start, this._events = [];
this._end, this._subscribeCalendarEvents(this._selectedCalendars);
this._selectedCalendars
);
this._events = result.events;
this._handleErrors(result.errors);
} }
private _handleErrors(error_entity_ids: string[]) { private _handleErrors(error_entity_ids: string[]) {

View File

@@ -104,7 +104,6 @@ export class HaConfigApplicationCredentials extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc",
}, },
actions: { actions: {
title: "", title: "",

View File

@@ -452,7 +452,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -182,13 +182,11 @@ class HaBlueprintOverview extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
groupable: true, groupable: true,
direction: "asc",
}, },
path: { path: {
title: localize("ui.panel.config.blueprint.overview.headers.file_name"), title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc",
flex: 2, flex: 2,
}, },
fullpath: { fullpath: {

View File

@@ -1,4 +1,4 @@
import { mdiTag, mdiPlus } from "@mdi/js"; import { mdiPlus, mdiTag } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
@@ -194,8 +194,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.notFoundLabel=${this.hass.localize( .notFoundLabel=${this._notFoundLabel}
"ui.components.category-picker.no_match" .emptyLabel=${this.hass.localize(
"ui.components.category-picker.no_categories"
)} )}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
@@ -254,6 +255,11 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.category-picker.no_match", {
term: html`<b>${search}</b>`,
});
} }
declare global { declare global {

View File

@@ -771,7 +771,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -792,7 +792,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -634,7 +634,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -473,7 +473,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -458,7 +458,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.checked=${selected} .checked=${selected}
.indeterminate=${partial} .indeterminate=${partial}
></ha-checkbox> ></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing} : nothing}

View File

@@ -90,7 +90,6 @@ export class HaConfigUsers extends LitElement {
title: localize("ui.panel.config.users.picker.headers.username"), title: localize("ui.panel.config.users.picker.headers.username"),
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc",
template: (user) => html`${user.username || "—"}`, template: (user) => html`${user.username || "—"}`,
}, },
group: { group: {
@@ -98,7 +97,6 @@ export class HaConfigUsers extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
groupable: true, groupable: true,
direction: "asc",
}, },
is_active: { is_active: {
title: this.hass.localize( title: this.hass.localize(

View File

@@ -1,3 +1,4 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@@ -7,8 +8,16 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-card"; import "../../../components/ha-card";
import type { Calendar, CalendarEvent } from "../../../data/calendar"; import type {
import { fetchCalendarEvents } from "../../../data/calendar"; Calendar,
CalendarEvent,
CalendarEventSubscription,
CalendarEventApiData,
} from "../../../data/calendar";
import {
normalizeSubscriptionEventData,
subscribeCalendarEvents,
} from "../../../data/calendar";
import type { import type {
CalendarViewChanged, CalendarViewChanged,
FullCalendarView, FullCalendarView,
@@ -65,12 +74,16 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
@state() private _error?: string = undefined; @state() private _error?: string = undefined;
@state() private _errorCalendars: string[] = [];
private _startDate?: Date; private _startDate?: Date;
private _endDate?: Date; private _endDate?: Date;
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
private _unsubs: Record<string, Promise<UnsubscribeFunc>> = {};
public setConfig(config: CalendarCardConfig): void { public setConfig(config: CalendarCardConfig): void {
if (!config.entities?.length) { if (!config.entities?.length) {
throw new Error("Entities must be specified"); throw new Error("Entities must be specified");
@@ -86,7 +99,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
})); }));
if (this._config?.entities !== config.entities) { if (this._config?.entities !== config.entities) {
this._fetchCalendarEvents(); this._unsubscribeAll();
// Subscription will happen when view-changed event fires
} }
this._config = { initial_view: "dayGridMonth", ...config }; this._config = { initial_view: "dayGridMonth", ...config };
@@ -115,6 +129,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
if (this._resizeObserver) { if (this._resizeObserver) {
this._resizeObserver.disconnect(); this._resizeObserver.disconnect();
} }
this._unsubscribeAll();
} }
protected render() { protected render() {
@@ -170,31 +185,85 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
} }
} }
private _handleViewChanged(ev: HASSDomEvent<CalendarViewChanged>): void { private async _handleViewChanged(
ev: HASSDomEvent<CalendarViewChanged>
): Promise<void> {
this._startDate = ev.detail.start; this._startDate = ev.detail.start;
this._endDate = ev.detail.end; this._endDate = ev.detail.end;
this._fetchCalendarEvents(); await this._unsubscribeAll();
this._subscribeCalendarEvents();
} }
private async _fetchCalendarEvents(): Promise<void> { private _subscribeCalendarEvents(): void {
if (!this._startDate || !this._endDate) { if (!this.hass || !this._startDate || !this._endDate) {
return; return;
} }
this._error = undefined; this._error = undefined;
const result = await fetchCalendarEvents(
this.hass!,
this._startDate,
this._endDate,
this._calendars
);
this._events = result.events;
if (result.errors.length > 0) { this._calendars.forEach((calendar) => {
const unsub = subscribeCalendarEvents(
this.hass!,
calendar.entity_id,
this._startDate!,
this._endDate!,
(update: CalendarEventSubscription) => {
this._handleCalendarUpdate(calendar, update);
}
);
this._unsubs[calendar.entity_id] = unsub;
});
}
private _handleCalendarUpdate(
calendar: Calendar,
update: CalendarEventSubscription
): void {
// Remove events from this calendar
this._events = this._events.filter(
(event) => event.calendar !== calendar.entity_id
);
if (update.events === null) {
// Error fetching events
if (!this._errorCalendars.includes(calendar.entity_id)) {
this._errorCalendars = [...this._errorCalendars, calendar.entity_id];
}
this._error = `${this.hass!.localize( this._error = `${this.hass!.localize(
"ui.components.calendar.event_retrieval_error" "ui.components.calendar.event_retrieval_error"
)}`; )}`;
return;
} }
// Remove from error list if successfully loaded
this._errorCalendars = this._errorCalendars.filter(
(id) => id !== calendar.entity_id
);
if (this._errorCalendars.length === 0) {
this._error = undefined;
}
// Add new events from this calendar
const newEvents: CalendarEvent[] = update.events
.map((eventData: CalendarEventApiData) =>
normalizeSubscriptionEventData(eventData, calendar)
)
.filter((event): event is CalendarEvent => event !== null);
this._events = [...this._events, ...newEvents];
}
private async _unsubscribeAll(): Promise<void> {
await Promise.all(
Object.values(this._unsubs).map((unsub) =>
unsub
.then((unsubFunc) => unsubFunc())
.catch(() => {
// Subscription may have already been closed
})
)
);
this._unsubs = {};
} }
private _measureCard() { private _measureCard() {

View File

@@ -79,11 +79,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
this._errorCard.preview = this.preview; this._errorCard.preview = this.preview;
} }
} }
if (changedProperties.has("layout")) {
this._cards.forEach((card) => {
card.layout = this.layout;
});
}
} }
if (changedProperties.has("layout")) { if (changedProperties.has("layout")) {
@@ -95,7 +90,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.preview = this.preview; element.preview = this.preview;
element.layout = this.layout;
element.config = cardConfig; element.config = cardConfig;
element.load(); element.load();
return element; return element;

View File

@@ -175,9 +175,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
this._names = {}; this._names = {};
this._entities.forEach((config) => { this._entities.forEach((config) => {
const stateObj = this.hass!.states[config.entity]; const stateObj = this.hass!.states[config.entity];
this._names[config.entity] = stateObj this._names[config.entity] =
? computeLovelaceEntityName(this.hass!, stateObj, config.name) computeLovelaceEntityName(this.hass!, stateObj, config.name) ||
: config.entity; config.entity;
}); });
} }

View File

@@ -50,7 +50,8 @@ export const coordinates = (
width: number, width: number,
height: number, height: number,
maxDetails: number, maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
useMean = false
) => { ) => {
history = history.filter((item) => !Number.isNaN(item[1])); history = history.filter((item) => !Number.isNaN(item[1]));
@@ -58,7 +59,8 @@ export const coordinates = (
history, history,
maxDetails, maxDetails,
limits?.minX, limits?.minX,
limits?.maxX limits?.maxX,
useMean
); );
return calcPoints(sampledData, width, height, limits); return calcPoints(sampledData, width, height, limits);
}; };
@@ -68,7 +70,8 @@ export const coordinatesMinimalResponseCompressedState = (
width: number, width: number,
height: number, height: number,
maxDetails: number, maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
useMean = false
) => { ) => {
if (!history?.length) { if (!history?.length) {
return { points: [], yAxisOrigin: 0 }; return { points: [], yAxisOrigin: 0 };
@@ -80,5 +83,5 @@ export const coordinatesMinimalResponseCompressedState = (
item.lu * 1000, item.lu * 1000,
Number(item.s), Number(item.s),
]); ]);
return coordinates(mappedHistory, width, height, maxDetails, limits); return coordinates(mappedHistory, width, height, maxDetails, limits, useMean);
}; };

View File

@@ -39,21 +39,23 @@ export class HuiEntityEditor extends LitElement {
private _renderItem(item: EntityConfig, index: number) { private _renderItem(item: EntityConfig, index: number) {
const stateObj = this.hass.states[item.entity]; const stateObj = this.hass.states[item.entity];
const useDeviceName = entityUseDeviceName( const useDeviceName =
stateObj, stateObj &&
this.hass.entities, entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
this.hass.devices
);
const isRTL = computeRTL(this.hass); const isRTL = computeRTL(this.hass);
const primary = const primary =
(stateObj &&
this.hass.formatEntityName( this.hass.formatEntityName(
stateObj, stateObj,
useDeviceName ? { type: "device" } : { type: "entity" } useDeviceName ? { type: "device" } : { type: "entity" }
) || item.entity; )) ||
item.entity;
const secondary = this.hass.formatEntityName( const secondary =
stateObj &&
this.hass.formatEntityName(
stateObj, stateObj,
useDeviceName useDeviceName
? [{ type: "area" }] ? [{ type: "area" }]

View File

@@ -4,11 +4,14 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_INPUT_ROW } from "../../../common/const"; import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { uid } from "../../../common/util/uid";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { toggleAttribute } from "../../../common/dom/toggle_attribute"; import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import "../../../components/ha-tooltip";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { EntitiesCardEntityConfig } from "../cards/types"; import type { EntitiesCardEntityConfig } from "../cards/types";
@@ -36,6 +39,8 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: "catch-interaction", type: Boolean }) @property({ attribute: "catch-interaction", type: Boolean })
public catchInteraction?; public catchInteraction?;
private _secondaryInfoElementId = "-" + uid();
protected render() { protected render() {
if (!this.hass || !this.config) { if (!this.hass || !this.config) {
return nothing; return nothing;
@@ -100,7 +105,19 @@ export class HuiGenericEntityRow extends LitElement {
? stateObj.entity_id ? stateObj.entity_id
: this.config.secondary_info === "last-changed" : this.config.secondary_info === "last-changed"
? html` ? html`
<ha-tooltip
for="last-changed${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_changed),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time <ha-relative-time
id="last-changed${this._secondaryInfoElementId}"
.hass=${this.hass} .hass=${this.hass}
.datetime=${stateObj.last_changed} .datetime=${stateObj.last_changed}
capitalize capitalize
@@ -108,7 +125,20 @@ export class HuiGenericEntityRow extends LitElement {
` `
: this.config.secondary_info === "last-updated" : this.config.secondary_info === "last-updated"
? html` ? html`
<ha-tooltip
for="last-updated${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_updated),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time <ha-relative-time
id="last-updated${this
._secondaryInfoElementId}"
.hass=${this.hass} .hass=${this.hass}
.datetime=${stateObj.last_updated} .datetime=${stateObj.last_updated}
capitalize capitalize
@@ -117,7 +147,22 @@ export class HuiGenericEntityRow extends LitElement {
: this.config.secondary_info === "last-triggered" : this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered ? stateObj.attributes.last_triggered
? html` ? html`
<ha-tooltip
for="last-triggered${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(
stateObj.attributes.last_triggered
),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time <ha-relative-time
id="last-triggered${this
._secondaryInfoElementId}"
.hass=${this.hass} .hass=${this.hass}
.datetime=${stateObj.attributes .datetime=${stateObj.attributes
.last_triggered} .last_triggered}

View File

@@ -155,16 +155,20 @@ export class HuiGraphHeaderFooter
} }
const width = this.clientWidth || this.offsetWidth; const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels // sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = const maxDetails = Math.max(
10,
this._config.detail! > 1 this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!) ? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!; : this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
const { points } = coordinatesMinimalResponseCompressedState( const { points } = coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity], combinedHistory[this._config.entity],
width, width,
width / 5, width / 5,
maxDetails, maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max } { minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
); );
this._coordinates = points; this._coordinates = points;
}, },

View File

@@ -652,7 +652,7 @@
"edit": "Edit", "edit": "Edit",
"clear": "Clear", "clear": "Clear",
"no_entities": "You don't have any entities", "no_entities": "You don't have any entities",
"no_match": "No matching entities found", "no_match": "No entities found for {term}",
"show_entities": "Show entities", "show_entities": "Show entities",
"new_entity": "Create a new entity", "new_entity": "Create a new entity",
"placeholder": "Select an entity", "placeholder": "Select an entity",
@@ -763,7 +763,7 @@
}, },
"language-picker": { "language-picker": {
"language": "Language", "language": "Language",
"no_match": "No matching languages found", "no_match": "No languages found for {term}",
"no_languages": "No languages available" "no_languages": "No languages available"
}, },
"tts-picker": { "tts-picker": {
@@ -775,7 +775,7 @@
"none": "None" "none": "None"
}, },
"user-picker": { "user-picker": {
"no_match": "No matching users found", "no_match": "No users found for {term}",
"user": "User", "user": "User",
"add_user": "Add user" "add_user": "Add user"
}, },
@@ -786,8 +786,8 @@
"clear": "Clear", "clear": "Clear",
"toggle": "Toggle", "toggle": "Toggle",
"show_devices": "Show devices", "show_devices": "Show devices",
"no_devices": "You don't have any devices", "no_devices": "No devices available",
"no_match": "No matching devices found", "no_match": "No devices found for {term}",
"device": "Device", "device": "Device",
"unnamed_device": "Unnamed device", "unnamed_device": "Unnamed device",
"no_area": "No area", "no_area": "No area",
@@ -801,8 +801,8 @@
"add_category": "Add category", "add_category": "Add category",
"add_new_sugestion": "Add new category ''{name}''", "add_new_sugestion": "Add new category ''{name}''",
"add_new": "Add new category…", "add_new": "Add new category…",
"no_categories": "You don't have any categories", "no_categories": "No categories available",
"no_match": "No matching categories found", "no_match": "No categories found for {term}",
"add_dialog": { "add_dialog": {
"title": "Add new category", "title": "Add new category",
"text": "Enter the name of the new category.", "text": "Enter the name of the new category.",
@@ -817,8 +817,8 @@
"add_new_sugestion": "Add new label ''{name}''", "add_new_sugestion": "Add new label ''{name}''",
"add_new": "Add new label…", "add_new": "Add new label…",
"add": "Add label", "add": "Add label",
"no_labels": "You don't have any labels", "no_labels": "No labels available",
"no_match": "No matching labels found", "no_match": "No labels found for {term}",
"failed_create_label": "Failed to create label." "failed_create_label": "Failed to create label."
}, },
"area-picker": { "area-picker": {
@@ -827,8 +827,8 @@
"area": "Area", "area": "Area",
"add_new_sugestion": "Add new area ''{name}''", "add_new_sugestion": "Add new area ''{name}''",
"add_new": "Add new area…", "add_new": "Add new area…",
"no_areas": "You don't have any areas", "no_areas": "No areas available",
"no_match": "No matching areas found", "no_match": "No areas found for {term}",
"unassigned_areas": "Unassigned areas", "unassigned_areas": "Unassigned areas",
"failed_create_area": "Failed to create area." "failed_create_area": "Failed to create area."
}, },
@@ -838,8 +838,8 @@
"floor": "Floor", "floor": "Floor",
"add_new_sugestion": "Add new floor ''{name}''", "add_new_sugestion": "Add new floor ''{name}''",
"add_new": "Add new floor…", "add_new": "Add new floor…",
"no_floors": "You don't have any floors", "no_floors": "No floors available",
"no_match": "No matching floors found", "no_match": "No floors found for {term}",
"failed_create_floor": "Failed to create floor." "failed_create_floor": "Failed to create floor."
}, },
"area-filter": { "area-filter": {
@@ -853,8 +853,8 @@
"statistic-picker": { "statistic-picker": {
"statistic": "Statistic", "statistic": "Statistic",
"placeholder": "Select a statistic", "placeholder": "Select a statistic",
"no_statistics": "You don't have any statistics", "no_statistics": "No statistics available",
"no_match": "No matching statistics found", "no_match": "No statistics found for {term}",
"no_state": "Entity without state", "no_state": "Entity without state",
"missing_entity": "Why is my entity not listed?", "missing_entity": "Why is my entity not listed?",
"learn_more": "Learn more about statistics" "learn_more": "Learn more about statistics"
@@ -1292,7 +1292,8 @@
"add": "Add interaction" "add": "Add interaction"
}, },
"combo-box": { "combo-box": {
"no_match": "No matching items found" "no_match": "No matching items found",
"no_items": "No items available"
}, },
"suggest_with_ai": { "suggest_with_ai": {
"label": "Suggest", "label": "Suggest",
@@ -3967,7 +3968,7 @@
"type_automation_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::automation%]", "type_automation_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::automation%]",
"type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]", "type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]",
"new_automation_setup_failed_title": "New {type} setup timed out", "new_automation_setup_failed_title": "New {type} setup timed out",
"new_automation_setup_failed_text": "Your new {type} has saved, but waiting for it to setup has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected, and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.", "new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.", "new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.",
"new_automation_setup_timedout_success": "The server has responded and this has now setup successfully. You may now close this dialog.", "new_automation_setup_timedout_success": "The server has responded and this has now setup successfully. You may now close this dialog.",
"item_pasted": "{item} pasted", "item_pasted": "{item} pasted",

336
yarn.lock
View File

@@ -324,7 +324,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": "@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5 version: 7.28.5
resolution: "@babel/parser@npm:7.28.5" resolution: "@babel/parser@npm:7.28.5"
dependencies: dependencies:
@@ -1180,7 +1180,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4": "@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5 version: 7.28.5
resolution: "@babel/types@npm:7.28.5" resolution: "@babel/types@npm:7.28.5"
dependencies: dependencies:
@@ -5082,131 +5082,131 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/a11y-base@npm:~24.9.4": "@vaadin/a11y-base@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/a11y-base@npm:24.9.4" resolution: "@vaadin/a11y-base@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/911df9a219d17b8028e3e491973600b2604b7441a7c45fea9f03cc4bf69ba64c7310a31c9a43f277050e251112e38c87a224e1c91a85301276f7ce40957cb0af checksum: 10/2b77100c1525c9213d66348c789350e7e943c4fb163f478082a58a7cbc8fcdbcb49e6e3eb842ff5a4c7223b80c0210cb906466c5ceb3d0cf6eed3a83e5e30c0c
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/combo-box@npm:24.9.4": "@vaadin/combo-box@npm:24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/combo-box@npm:24.9.4" resolution: "@vaadin/combo-box@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.4" "@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/field-base": "npm:~24.9.4" "@vaadin/field-base": "npm:~24.9.5"
"@vaadin/input-container": "npm:~24.9.4" "@vaadin/input-container": "npm:~24.9.5"
"@vaadin/item": "npm:~24.9.4" "@vaadin/item": "npm:~24.9.5"
"@vaadin/lit-renderer": "npm:~24.9.4" "@vaadin/lit-renderer": "npm:~24.9.5"
"@vaadin/overlay": "npm:~24.9.4" "@vaadin/overlay": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4" "@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.4" "@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/3af6d052c2791774af9091877af808c1a9e15bb80c3c6278fea6e015464cbb350aed5c4913f94745be9d02110119b7354f82412cd54b56d5df90f22c48cdee75 checksum: 10/b3b1d17fc34519458a39b6adf7bff7b109f0e4523472308d7482f53230c48ef94bf17b46155ed83a71c780fc093c8c1660c9f66676ffe95cef340fa1293b01ad
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/component-base@npm:~24.9.4": "@vaadin/component-base@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/component-base@npm:24.9.4" resolution: "@vaadin/component-base@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/vaadin-development-mode-detector": "npm:^2.0.0" "@vaadin/vaadin-development-mode-detector": "npm:^2.0.0"
"@vaadin/vaadin-usage-statistics": "npm:^2.1.0" "@vaadin/vaadin-usage-statistics": "npm:^2.1.0"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/fa8e8819de4564f5333d76402467500fd35fa399333abccd709fbe5b6d53a02e7438bc19ffda4d9119776e6c8d44abcbe5d996a0a4d415f4199fe9f3eb64dc58 checksum: 10/cc68b44dd5001a139c32e3d0d6025b7285d9173c070a91ca5dfda8b4a999ed9b30a13d3638646e791d7567fbe2992eff2ccbecd41af10bd7058c212a64e06680
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/field-base@npm:~24.9.4": "@vaadin/field-base@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/field-base@npm:24.9.4" resolution: "@vaadin/field-base@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.4" "@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/0153fc5fcf912b8e5351270d026da3967300d2b8358e2ef89d354444254022ef4efa0f575d6d4bf41d7a3bc965e9afd62b8488dd2cb782c105a689230d5c7a33 checksum: 10/a42fde9f88a783b3899a64b238eea0022e2b02432116da03d70e92ce6280872477fc5d907f6541700fb56da6b776196ffe2958f02cea1a5bfa3e830a4f279b44
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/icon@npm:~24.9.4": "@vaadin/icon@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/icon@npm:24.9.4" resolution: "@vaadin/icon@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4" "@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/ff9742abcf55b233a00b9ea3a18fcf816bec9cd49f5d2b88e2441716318fa81a9034916e55308a9db1ea789da48b86b3e25322fb8d4c74d192a6574ac42d00f8 checksum: 10/cbe339af9d15b8be7b6796e0e50643a606ad53d0f21ad0571fdb257d62cfd21af9e9666f9a33864d26339749fe0c62ee8937b19d5d7a2fe0c9c48f9ac7d1e1f4
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/input-container@npm:~24.9.4": "@vaadin/input-container@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/input-container@npm:24.9.4" resolution: "@vaadin/input-container@npm:24.9.5"
dependencies: dependencies:
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4" "@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.4" "@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/8b8565b8af0b01fdbcd2e89951d258e7f59690140c25606c32eba481262d47918cc792ce9cb4563b045a83ab9a314f45cc7901fca563f0098cee57b769fdb24e checksum: 10/340abe99169b5a5c268df3b9febcc1b56867b875d22ab9af51db332a00a7187cae2eff222f65f6ff4523f7be890cd74d4fe62ca33f50520ab90b3336308833cb
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/item@npm:~24.9.4": "@vaadin/item@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/item@npm:24.9.4" resolution: "@vaadin/item@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.4" "@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4" "@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.4" "@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/9bb351e013d8066d48edea19af7ebb0718d63582c3753e2d8d49cf7837fc771d7989411b878a4ae3bd4ddd1f586622bdd7cf9a58202bb406bec6d17530c70b70 checksum: 10/4cf5251f5ea8bd24559d91b002e722fb33dd550eb944a2a00272a216e4168e501b71e2fcdcf9889b8fe1aed7c3e1c46355feb37283ceb5b02e5c9f4e026e199b
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/lit-renderer@npm:~24.9.4": "@vaadin/lit-renderer@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/lit-renderer@npm:24.9.4" resolution: "@vaadin/lit-renderer@npm:24.9.5"
dependencies: dependencies:
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/73b1dfe4a028232eadcf6f5ecbf676f38c94651a2147441b44d62a7954a100938c013f2e201e1d290c5b498b2e0130b8b50ad5b501d1fff653f9edce1bc6c58a checksum: 10/abf3cfb4b76b1a696ca9138addc52a26519ff8d4a5dc1dc6e2f4a637cb749678188cf492f4931e623e999708d059babd73b01df973a89fe7486be4b9abc19104
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/overlay@npm:~24.9.4": "@vaadin/overlay@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/overlay@npm:24.9.4" resolution: "@vaadin/overlay@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.4" "@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4" "@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.4" "@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
checksum: 10/dedd7c1aa062e05679d0f7597f3e1e7f07d93f6a645fff83733a5e5245e18431ac94b8de6f9322c5b7239b6efb733d677c3bc09785921e267b682ee07b4f9cff checksum: 10/c20e67923605d94866a6d513b529a9ddf5e0baa7f208e3a3d974a040e645658b92e6501129648c20c6389d188af38ba07ece59cfb1c9a5997d10eb9b4eeebcce
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5217,37 +5217,37 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/vaadin-lumo-styles@npm:~24.9.4": "@vaadin/vaadin-lumo-styles@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/vaadin-lumo-styles@npm:24.9.4" resolution: "@vaadin/vaadin-lumo-styles@npm:24.9.5"
dependencies: dependencies:
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/icon": "npm:~24.9.4" "@vaadin/icon": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
checksum: 10/e892ab2187bf4e7f6889b8d4ae93009794313e07af24921176abcdf08067123b07840d4018d75340bf2b41dd0ac7d36a71a9a999ff1e04c14cde24878dd1d5d3 checksum: 10/fd7aa0c45d71e64e4356fcb58612a1345eb5b2d5a67258826dcbad0215a3606d56340a511a4eeb6d4608c4e4237af6ad72c6cc44bb2989dd40acb1f629e9915a
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/vaadin-material-styles@npm:~24.9.4": "@vaadin/vaadin-material-styles@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/vaadin-material-styles@npm:24.9.4" resolution: "@vaadin/vaadin-material-styles@npm:24.9.5"
dependencies: dependencies:
"@polymer/polymer": "npm:^3.0.0" "@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.4" "@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4" "@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
checksum: 10/955b9fd2a09e3ee5ba403b6745a3a164caeea8fc76b04f57abd0cf6aa7f9d9bd24f984f95987956be5e0457b5343dc275970311937ac8a22e491b3a072cbaa00 checksum: 10/104278b687a3acb621e0296e83aa9c8cc77358d2f80dbfe4b9a38cdd81709891b6c40f4cd5a7884021aeef5dd6553a4b5a288318c01f5d33831c338e6dbd42bd
languageName: node languageName: node
linkType: hard linkType: hard
"@vaadin/vaadin-themable-mixin@npm:24.9.4, @vaadin/vaadin-themable-mixin@npm:~24.9.4": "@vaadin/vaadin-themable-mixin@npm:24.9.5, @vaadin/vaadin-themable-mixin@npm:~24.9.5":
version: 24.9.4 version: 24.9.5
resolution: "@vaadin/vaadin-themable-mixin@npm:24.9.4" resolution: "@vaadin/vaadin-themable-mixin@npm:24.9.5"
dependencies: dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0" "@open-wc/dedupe-mixin": "npm:^1.3.0"
lit: "npm:^3.0.0" lit: "npm:^3.0.0"
style-observer: "npm:^0.0.8" style-observer: "npm:^0.0.8"
checksum: 10/3cb428fd3649512a1c026f56eb9df21d24ff29d2c87813f1e8055f211241c6e93840cba46e5ac49ae459e7f9ba4a319c012255c88b71b0d4d44d6cb0d0ef9066 checksum: 10/e5f5d756abddd5c60eb4fa27d2c5ed1bbe9013850be55b12aa3c16316bd1a308ff333220be3266ad2085626f214316c53c11b7a2a0552ef913e8d500bbd4723f
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5368,52 +5368,52 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/coverage-v8@npm:4.0.7": "@vitest/coverage-v8@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/coverage-v8@npm:4.0.7" resolution: "@vitest/coverage-v8@npm:4.0.8"
dependencies: dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2" "@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.7" "@vitest/utils": "npm:4.0.8"
ast-v8-to-istanbul: "npm:^0.3.5" ast-v8-to-istanbul: "npm:^0.3.8"
debug: "npm:^4.4.3" debug: "npm:^4.4.3"
istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1" istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^5.0.6" istanbul-lib-source-maps: "npm:^5.0.6"
istanbul-reports: "npm:^3.2.0" istanbul-reports: "npm:^3.2.0"
magicast: "npm:^0.3.5" magicast: "npm:^0.5.1"
std-env: "npm:^3.9.0" std-env: "npm:^3.10.0"
tinyrainbow: "npm:^3.0.3" tinyrainbow: "npm:^3.0.3"
peerDependencies: peerDependencies:
"@vitest/browser": 4.0.7 "@vitest/browser": 4.0.8
vitest: 4.0.7 vitest: 4.0.8
peerDependenciesMeta: peerDependenciesMeta:
"@vitest/browser": "@vitest/browser":
optional: true optional: true
checksum: 10/eab89e5da9e8b3ebc0abe08419adfcd2cd54a9cd203252a8cf739017f3a3bbd87a7a6e2dd1ea50105f2edca36618c5f0a394c6f88790d9dc4523440a3588f166 checksum: 10/f97d933c7a776eccbd293fd3f77c0dcdc683e528094ce802b2b309e282c01cca0e8ad51a03b61ec623da7034527879824df2e30f70062d7ca587db40bfa5ffd2
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/expect@npm:4.0.7": "@vitest/expect@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/expect@npm:4.0.7" resolution: "@vitest/expect@npm:4.0.8"
dependencies: dependencies:
"@standard-schema/spec": "npm:^1.0.0" "@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2" "@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.7" "@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.7" "@vitest/utils": "npm:4.0.8"
chai: "npm:^6.0.1" chai: "npm:^6.2.0"
tinyrainbow: "npm:^3.0.3" tinyrainbow: "npm:^3.0.3"
checksum: 10/d64fa5e17b3fd1894200263c36584673e4e9f8ff055158a4fc5339a00e5132038533e8f7aa45f4f4daf0bfbedd9ccb1de2a543e11eac8c4fd507768874dbd11f checksum: 10/342934870fb2b11b7a47db4df2a9df2b711087fe48118568ac013386cd659b9bff8f8252bef643e9519a88d018d54e6758a733c9dedf907e9d5dc53040aa0dc3
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/mocker@npm:4.0.7": "@vitest/mocker@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/mocker@npm:4.0.7" resolution: "@vitest/mocker@npm:4.0.8"
dependencies: dependencies:
"@vitest/spy": "npm:4.0.7" "@vitest/spy": "npm:4.0.8"
estree-walker: "npm:^3.0.3" estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.19" magic-string: "npm:^0.30.21"
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0 vite: ^6.0.0 || ^7.0.0-0
@@ -5422,54 +5422,54 @@ __metadata:
optional: true optional: true
vite: vite:
optional: true optional: true
checksum: 10/cdba9cb3808b6944b9533c9b4152c33b731b89c8204390f2e29ae5851eccb1241a12a02223d4934bf25607e967c17b89ad9fa153d939ea42c9b5171552044df7 checksum: 10/6a624e04a6fa78cc45205961bd0638486674d1cbf4589599772fdd49f58f433c821bcb1eea013ac772171fb9c81154e6f9ffcf1704f27e245860e982c3988cd4
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/pretty-format@npm:4.0.7": "@vitest/pretty-format@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/pretty-format@npm:4.0.7" resolution: "@vitest/pretty-format@npm:4.0.8"
dependencies: dependencies:
tinyrainbow: "npm:^3.0.3" tinyrainbow: "npm:^3.0.3"
checksum: 10/c936c0d503c665bd9565348c52280f10c990da43504fa7da027521b298bab16a6c83866d0eb91c82d7c53ba4aa299042b34a94a6545f1b7b999bf40a1d8b9c13 checksum: 10/7e438ba6875a72b58cfe429dedc1de3025c8f87f523db24c687b2ad298e0c1a3e171e7e22ab938518a52c44383acff58d3e9936620dc45f4e97f1669e3e275da
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/runner@npm:4.0.7": "@vitest/runner@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/runner@npm:4.0.7" resolution: "@vitest/runner@npm:4.0.8"
dependencies: dependencies:
"@vitest/utils": "npm:4.0.7" "@vitest/utils": "npm:4.0.8"
pathe: "npm:^2.0.3" pathe: "npm:^2.0.3"
checksum: 10/9dedaefc0c33736cfe721e1e53ecea05bb6bc9b32611bd55ca486555814aac319f0d7c6df155cebc6ece54f8c7870d810a6285c30006b49b6e511eb68a173873 checksum: 10/cb66c1121c2701bb2400fb0969b7504aee34b400278d03f1ed19d78f8180adb88dbbd0f3f5d4ff1db49a5ae50f0e139964ff7ae32aa1d99f6e9d91d6a57c1ffe
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/snapshot@npm:4.0.7": "@vitest/snapshot@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/snapshot@npm:4.0.7" resolution: "@vitest/snapshot@npm:4.0.8"
dependencies: dependencies:
"@vitest/pretty-format": "npm:4.0.7" "@vitest/pretty-format": "npm:4.0.8"
magic-string: "npm:^0.30.19" magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3" pathe: "npm:^2.0.3"
checksum: 10/df9b0c736d1a7a063eea9b9527e37acb53acaf8158469db49b1deb8b64229db30219bf0596e1981e1d7beec194085c07b06f34c466fc5b5cf114cdfa7b04de47 checksum: 10/d703a4bb4979f94cc9b3c8ecaf5ce9c4741066772f1d4414db7faa1d1ed209ad5da9c85a98118e340f5f5652d680a1895efa39c060b6e3700a95806eefdf40a7
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/spy@npm:4.0.7": "@vitest/spy@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/spy@npm:4.0.7" resolution: "@vitest/spy@npm:4.0.8"
checksum: 10/44f17971c1e8f4aaa4dcc8b26e86bcc9249a4ce8a131baac515980f3befede719494b548e2e48f871060ce2b22b8959fc85bf49db51ba4785fb6c025785b1a7b checksum: 10/944223ffef7d64299d92c94ab895209b27a307ef59d2ef6f5c6c006fc1e85612c9547069b0fde7b2d93adfa484b3770b459a716f6b82f8839226132767fb661c
languageName: node languageName: node
linkType: hard linkType: hard
"@vitest/utils@npm:4.0.7": "@vitest/utils@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "@vitest/utils@npm:4.0.7" resolution: "@vitest/utils@npm:4.0.8"
dependencies: dependencies:
"@vitest/pretty-format": "npm:4.0.7" "@vitest/pretty-format": "npm:4.0.8"
tinyrainbow: "npm:^3.0.3" tinyrainbow: "npm:^3.0.3"
checksum: 10/82110c390309d3bac0ecf314f0428873db8d1df93e0a0bbc5214dca9ec820eb767666ccf2f66593d0b82bfe455ee9037727d2eb310fe24bacb3f71c45a107497 checksum: 10/9f241a8aafbd81caec766143c8c99b8e1e76671460ff6bd1fc0921ea968c2f0d3d8305551b52509c0fb427d436ec9e2f75cafdc273d2f23c1ccaea504269a160
languageName: node languageName: node
linkType: hard linkType: hard
@@ -5951,7 +5951,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ast-v8-to-istanbul@npm:^0.3.5": "ast-v8-to-istanbul@npm:^0.3.8":
version: 0.3.8 version: 0.3.8
resolution: "ast-v8-to-istanbul@npm:0.3.8" resolution: "ast-v8-to-istanbul@npm:0.3.8"
dependencies: dependencies:
@@ -6479,7 +6479,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chai@npm:^6.0.1": "chai@npm:^6.2.0":
version: 6.2.0 version: 6.2.0
resolution: "chai@npm:6.2.0" resolution: "chai@npm:6.2.0"
checksum: 10/199422854e253d8711ea3f220365c6a850c450abf68b31131d2a0f703cbfc5cb48e6c81567e0adbe80e83cdcae6dba82d069a41a77c16bdf6703329c5c3447ef checksum: 10/199422854e253d8711ea3f220365c6a850c450abf68b31131d2a0f703cbfc5cb48e6c81567e0adbe80e83cdcae6dba82d069a41a77c16bdf6703329c5c3447ef
@@ -9288,10 +9288,10 @@ __metadata:
"@types/tar": "npm:6.1.13" "@types/tar": "npm:6.1.13"
"@types/ua-parser-js": "npm:0.7.39" "@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29" "@types/webspeechapi": "npm:0.0.29"
"@vaadin/combo-box": "npm:24.9.4" "@vaadin/combo-box": "npm:24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:24.9.4" "@vaadin/vaadin-themable-mixin": "npm:24.9.5"
"@vibrant/color": "npm:4.0.0" "@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:4.0.7" "@vitest/coverage-v8": "npm:4.0.8"
"@vue/web-component-wrapper": "npm:1.3.0" "@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10" "@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0" "@webcomponents/webcomponentsjs": "npm:2.8.0"
@@ -9376,7 +9376,7 @@ __metadata:
typescript-eslint: "npm:8.46.3" typescript-eslint: "npm:8.46.3"
ua-parser-js: "npm:2.0.6" ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4" vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.7" vitest: "npm:4.0.8"
vue: "npm:2.7.16" vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8" vue2-daterange-picker: "npm:0.6.8"
webpack-stats-plugin: "npm:1.1.3" webpack-stats-plugin: "npm:1.1.3"
@@ -10922,7 +10922,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"magic-string@npm:^0.30.19": "magic-string@npm:^0.30.21":
version: 0.30.21 version: 0.30.21
resolution: "magic-string@npm:0.30.21" resolution: "magic-string@npm:0.30.21"
dependencies: dependencies:
@@ -10931,14 +10931,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"magicast@npm:^0.3.5": "magicast@npm:^0.5.1":
version: 0.3.5 version: 0.5.1
resolution: "magicast@npm:0.3.5" resolution: "magicast@npm:0.5.1"
dependencies: dependencies:
"@babel/parser": "npm:^7.25.4" "@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.25.4" "@babel/types": "npm:^7.28.5"
source-map-js: "npm:^1.2.0" source-map-js: "npm:^1.2.1"
checksum: 10/3a2dba6b0bdde957797361d09c7931ebdc1b30231705360eeb40ed458d28e1c3112841c3ed4e1b87ceb28f741e333c7673cd961193aa9fdb4f4946b202e6205a checksum: 10/ee6149994760f0b539a07f1d36631fed366ae19b9fc82e338c1cdd2a2e0b33a773635327514a6aa73faca9dc0ca37df5e5376b7b0687fb56353f431f299714c4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -13314,7 +13314,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": "source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "source-map-js@npm:1.2.1" resolution: "source-map-js@npm:1.2.1"
checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3
@@ -13469,10 +13469,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"std-env@npm:^3.7.0, std-env@npm:^3.9.0": "std-env@npm:^3.10.0, std-env@npm:^3.7.0":
version: 3.9.0 version: 3.10.0
resolution: "std-env@npm:3.9.0" resolution: "std-env@npm:3.10.0"
checksum: 10/3044b2c54a74be4f460db56725571241ab3ac89a91f39c7709519bc90fa37148784bc4cd7d3a301aa735f43bd174496f263563f76703ce3e81370466ab7c235b checksum: 10/19c9cda4f370b1ffae2b8b08c72167d8c3e5cfa972aaf5c6873f85d0ed2faa729407f5abb194dc33380708c00315002febb6f1e1b484736bfcf9361ad366013a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -14751,24 +14751,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vitest@npm:4.0.7": "vitest@npm:4.0.8":
version: 4.0.7 version: 4.0.8
resolution: "vitest@npm:4.0.7" resolution: "vitest@npm:4.0.8"
dependencies: dependencies:
"@vitest/expect": "npm:4.0.7" "@vitest/expect": "npm:4.0.8"
"@vitest/mocker": "npm:4.0.7" "@vitest/mocker": "npm:4.0.8"
"@vitest/pretty-format": "npm:4.0.7" "@vitest/pretty-format": "npm:4.0.8"
"@vitest/runner": "npm:4.0.7" "@vitest/runner": "npm:4.0.8"
"@vitest/snapshot": "npm:4.0.7" "@vitest/snapshot": "npm:4.0.8"
"@vitest/spy": "npm:4.0.7" "@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.7" "@vitest/utils": "npm:4.0.8"
debug: "npm:^4.4.3" debug: "npm:^4.4.3"
es-module-lexer: "npm:^1.7.0" es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2" expect-type: "npm:^1.2.2"
magic-string: "npm:^0.30.19" magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3" pathe: "npm:^2.0.3"
picomatch: "npm:^4.0.3" picomatch: "npm:^4.0.3"
std-env: "npm:^3.9.0" std-env: "npm:^3.10.0"
tinybench: "npm:^2.9.0" tinybench: "npm:^2.9.0"
tinyexec: "npm:^0.3.2" tinyexec: "npm:^0.3.2"
tinyglobby: "npm:^0.2.15" tinyglobby: "npm:^0.2.15"
@@ -14779,10 +14779,10 @@ __metadata:
"@edge-runtime/vm": "*" "@edge-runtime/vm": "*"
"@types/debug": ^4.1.12 "@types/debug": ^4.1.12
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.7 "@vitest/browser-playwright": 4.0.8
"@vitest/browser-preview": 4.0.7 "@vitest/browser-preview": 4.0.8
"@vitest/browser-webdriverio": 4.0.7 "@vitest/browser-webdriverio": 4.0.8
"@vitest/ui": 4.0.7 "@vitest/ui": 4.0.8
happy-dom: "*" happy-dom: "*"
jsdom: "*" jsdom: "*"
peerDependenciesMeta: peerDependenciesMeta:
@@ -14806,7 +14806,7 @@ __metadata:
optional: true optional: true
bin: bin:
vitest: vitest.mjs vitest: vitest.mjs
checksum: 10/23f872860f2f8ef7aa4a44830ff52fb385ee7879bd6952a116013cada7cc6bad7a2b72d9034d0bbf0134028b662bd00e8827021e5ff4ef6e232e8108e4f4851d checksum: 10/b2fd9e2bb0740860f998f1ecfb948da330cc2e8dc15376669ef742420ad77fedea491731dfaefff17379457d21f06740afaa725952cbcbdcd499a532b95e5717
languageName: node languageName: node
linkType: hard linkType: hard