diff --git a/package.json b/package.json
index 328b4eb9c7..939b7a9dbe 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
+ "@shoelace-style/shoelace": "2.20.0",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.4",
"@vaadin/vaadin-themable-mixin": "24.6.4",
diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts
index 0495ea4d9d..415bed6d7f 100644
--- a/src/components/ha-sidebar.ts
+++ b/src/components/ha-sidebar.ts
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
+import "@shoelace-style/shoelace/dist/components/skeleton/skeleton";
import {
mdiBell,
mdiCalendar,
@@ -212,18 +213,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
- @storage({
- key: "sidebarPanelOrder",
- state: true,
- subscribe: true,
- })
+ @storage({ key: "sidebarPanelOrder", state: true, subscribe: true })
private _devicePanelOrder?: string[];
- @storage({
- key: "sidebarHiddenPanels",
- state: true,
- subscribe: true,
- })
+ @storage({ key: "sidebarHiddenPanels", state: true, subscribe: true })
private _deviceHiddenPanels?: string[];
@state()
@@ -232,11 +225,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@state()
private _userHiddenPanels: string[] = [];
+ @state()
+ private _loadingUserPreferences = true;
+
public hassSubscribe(): UnsubscribeFunc[] {
const subscribeFunctions = [
subscribeSidebarPreferences(this.hass, (sidebar) => {
this._userPanelOrder = sidebar?.panelOrder || [];
this._userHiddenPanels = sidebar?.hiddenPanels || [];
+ this._loadingUserPreferences = false;
}),
];
if (this.hass.user?.is_admin) {
@@ -402,35 +399,43 @@ class HaSidebar extends SubscribeMixin(LitElement) {
`;
}
- private _getPanelPreferencesMemoized = memoizeOne((userPreferences: SidebarPreferences, devicePreferences: SidebarPreferences): { panelOrder: string[], hiddenPanels: string[] } => {
- let panelOrder = userPreferences.panelOrder ?? [];
- let hiddenPanels = userPreferences.hiddenPanels ?? [];
+ private _getPanelPreferencesMemoized = memoizeOne(
+ (userPreferences: SidebarPreferences, devicePreferences: SidebarPreferences, userPreferencesLoading: boolean): { panelOrder: string[]; hiddenPanels: string[]; loading: boolean } => {
+ let panelOrder = userPreferences.panelOrder ?? [];
+ let hiddenPanels = userPreferences.hiddenPanels ?? [];
- if (devicePreferences.panelOrder || devicePreferences.hiddenPanels) {
- panelOrder = devicePreferences.panelOrder ?? [];
- hiddenPanels = devicePreferences.hiddenPanels ?? [];
+ let loading = userPreferencesLoading;
+
+ if (devicePreferences.panelOrder || devicePreferences.hiddenPanels) {
+ panelOrder = devicePreferences.panelOrder ?? [];
+ hiddenPanels = devicePreferences.hiddenPanels ?? [];
+ loading = false;
+ }
+
+ return {
+ panelOrder,
+ hiddenPanels,
+ loading
+ };
}
-
- return { panelOrder, hiddenPanels }
- })
+ );
private _getPanelPreferences() {
return this._getPanelPreferencesMemoized(
{
panelOrder: this._userPanelOrder,
- hiddenPanels: this._userHiddenPanels
+ hiddenPanels: this._userHiddenPanels,
},
{
panelOrder: this._devicePanelOrder,
- hiddenPanels: this._deviceHiddenPanels
- }
- )
+ hiddenPanels: this._deviceHiddenPanels,
+ },
+ this._loadingUserPreferences
+ );
}
private _renderAllPanels() {
- // TODO render skeleton loading if panels are not loaded yet
-
- const { panelOrder, hiddenPanels } = this._getPanelPreferences();
+ const { panelOrder, hiddenPanels, loading } = this._getPanelPreferences();
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
@@ -457,12 +462,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
- ${this.editMode
- ? this._renderPanelsEdit(beforeSpacer)
- : this._renderPanels(beforeSpacer)}
- ${this._renderSpacer()}
- ${this._renderPanels(afterSpacer)}
- ${this._renderExternalConfiguration()}
+ ${loading ? html`
+
+
+
+
+
+
+ ` : html`
+ ${this.editMode
+ ? this._renderPanelsEdit(beforeSpacer)
+ : this._renderPanels(beforeSpacer)}
+ ${this._renderSpacer()}
+ ${this._renderPanels(afterSpacer)}
+ ${this._renderExternalConfiguration()}
+ `}
`;
}
@@ -768,9 +782,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
- this.hass.auth.external!.fireMessage({
- type: "config_screen/show",
- });
+ this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
}
private get _tooltip() {
@@ -813,13 +825,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
-
+
// Make a copy for Memoize
this._setHiddenPanels([...hiddenPanels, panel]);
// Remove it from the panel order
- this._setPanelOrder(panelOrder.filter(
- (order) => order !== panel
- ));
+ this._setPanelOrder(panelOrder.filter((order) => order !== panel));
}
private async _unhidePanel(ev: Event) {
@@ -868,9 +878,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
- @eventOptions({
- passive: true,
- })
+ @eventOptions({ passive: true })
private _listboxScroll() {
// On keypresses on the listbox, we're going to ignore scroll events
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
@@ -1201,6 +1209,32 @@ class HaSidebar extends SubscribeMixin(LitElement) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
+
+ .loading {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ sl-skeleton {
+ --border-radius: 8px;
+ height: 24px;
+ --color: var(--outline-color);
+ --sheen-color: var(--outline-hover-color);
+ }
+
+ sl-skeleton:nth-child(2) {
+ width: 70%;
+ }
+
+ sl-skeleton:nth-child(3) {
+ width: 30%;
+ }
+
+ sl-skeleton:nth-child(4) {
+ width: 90%;
+ }
`,
];
}
diff --git a/yarn.lock b/yarn.lock
index 6aa6894725..34445df444 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1388,6 +1388,13 @@ __metadata:
languageName: node
linkType: hard
+"@ctrl/tinycolor@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "@ctrl/tinycolor@npm:4.1.0"
+ checksum: 10/e64569399139ef0abd2eb0ec9fb7267dfd7820f7ad7d4567a63e5fc35e5cfdcb8ecdb3bad65cb9244b47ba6c77bc51085826c00e981acf263a3221dc89343adc
+ languageName: node
+ linkType: hard
+
"@discoveryjs/json-ext@npm:0.5.7, @discoveryjs/json-ext@npm:^0.5.7":
version: 0.5.7
resolution: "@discoveryjs/json-ext@npm:0.5.7"
@@ -1667,6 +1674,32 @@ __metadata:
languageName: node
linkType: hard
+"@floating-ui/core@npm:^1.6.0":
+ version: 1.6.9
+ resolution: "@floating-ui/core@npm:1.6.9"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.9"
+ checksum: 10/656fcd383da17fffca2efa0635cbe3c0b835c3312949e30bd19d05bf42479f2ac22aaf336a6a31cb160621fc6f35cfc9e115e76c5cf48ba96e33474d123ced22
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.6.12":
+ version: 1.6.13
+ resolution: "@floating-ui/dom@npm:1.6.13"
+ dependencies:
+ "@floating-ui/core": "npm:^1.6.0"
+ "@floating-ui/utils": "npm:^0.2.9"
+ checksum: 10/4bb732baf3270007741bcdc91be1de767b2bb5d8b891eb838e5f1e7c4cccad998643dbdd4e8b8cec4c5d12c9898f80febc68e9793dd6e26a445283c4fb1b6a78
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.9":
+ version: 0.2.9
+ resolution: "@floating-ui/utils@npm:0.2.9"
+ checksum: 10/0ca786347db3dd8d9034b86d1449fabb96642788e5900cc5f2aee433cd7b243efbcd7a165bead50b004ee3f20a90ddebb6a35296fc41d43cfd361b6f01b69ffb
+ languageName: node
+ linkType: hard
+
"@formatjs/ecma402-abstract@npm:2.3.3":
version: 2.3.3
resolution: "@formatjs/ecma402-abstract@npm:2.3.3"
@@ -2262,6 +2295,15 @@ __metadata:
languageName: node
linkType: hard
+"@lit/react@npm:^1.0.6":
+ version: 1.0.7
+ resolution: "@lit/react@npm:1.0.7"
+ peerDependencies:
+ "@types/react": 17 || 18 || 19
+ checksum: 10/9bdf90e233c91822065d0f09aa0d085544b5d70902b05bb6204075404f7e0e5a62a9a447eac55178761690ea1f707b1c13fecd8f8b109a25f978afe7a2c74fea
+ languageName: node
+ linkType: hard
+
"@lit/reactive-element@npm:1.6.3":
version: 1.6.3
resolution: "@lit/reactive-element@npm:1.6.3"
@@ -4373,6 +4415,36 @@ __metadata:
languageName: node
linkType: hard
+"@shoelace-style/animations@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@shoelace-style/animations@npm:1.2.0"
+ checksum: 10/73773147cebc5833f362f01f96245cc156e9619cc04a8ee342bd9d320661d0fce30ba2fee3a515603eb1141da005c163a608b6356fd5b478f50a483bc9806e16
+ languageName: node
+ linkType: hard
+
+"@shoelace-style/localize@npm:^3.2.1":
+ version: 3.2.1
+ resolution: "@shoelace-style/localize@npm:3.2.1"
+ checksum: 10/e22e108a27ce7da6b86a7b2f16f8db69e9b3c7d2aaf4e34fc39023c2f060aa7a5004d02ffd1cce2fbef3de7e5cd2a60e79c77fbba5cbd5e9456881fa3a452db1
+ languageName: node
+ linkType: hard
+
+"@shoelace-style/shoelace@npm:2.20.0":
+ version: 2.20.0
+ resolution: "@shoelace-style/shoelace@npm:2.20.0"
+ dependencies:
+ "@ctrl/tinycolor": "npm:^4.1.0"
+ "@floating-ui/dom": "npm:^1.6.12"
+ "@lit/react": "npm:^1.0.6"
+ "@shoelace-style/animations": "npm:^1.2.0"
+ "@shoelace-style/localize": "npm:^3.2.1"
+ composed-offset-position: "npm:^0.0.6"
+ lit: "npm:^3.2.1"
+ qr-creator: "npm:^1.0.0"
+ checksum: 10/202b226c9fa92950c6900c80899b6f95b13c0d79210c956a1c9be75ae6c87b4d7a210bfe355a6580ef423a78c32e141869dba302713dba055ec2339c9dbdc383
+ languageName: node
+ linkType: hard
+
"@sindresorhus/merge-streams@npm:^2.1.0":
version: 2.3.0
resolution: "@sindresorhus/merge-streams@npm:2.3.0"
@@ -6840,6 +6912,15 @@ __metadata:
languageName: node
linkType: hard
+"composed-offset-position@npm:^0.0.6":
+ version: 0.0.6
+ resolution: "composed-offset-position@npm:0.0.6"
+ peerDependencies:
+ "@floating-ui/utils": ^0.2.5
+ checksum: 10/f0e403f11a6a677631d39b5e7a742c242067c44c2278c6616362d46ee2b9a376dd9cb2d676640bf1f395cc69da52e5ebac9cd5b4f4dc51d9f4d6f2cb60d4a49b
+ languageName: node
+ linkType: hard
+
"compressible@npm:~2.0.16, compressible@npm:~2.0.18":
version: 2.0.18
resolution: "compressible@npm:2.0.18"
@@ -9378,6 +9459,7 @@ __metadata:
"@rsdoctor/rspack-plugin": "npm:0.4.13"
"@rspack/cli": "npm:1.2.3"
"@rspack/core": "npm:1.2.3"
+ "@shoelace-style/shoelace": "npm:2.20.0"
"@thomasloven/round-slider": "npm:0.6.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.21"
@@ -12475,6 +12557,13 @@ __metadata:
languageName: node
linkType: hard
+"qr-creator@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "qr-creator@npm:1.0.0"
+ checksum: 10/77325a895fabfc899a54f0fc4598696dc234dc5056714181bbadb62bb15944366be7cd56ec19e36eb6e92a99f086143435f654855e512726bd8923149d94f709
+ languageName: node
+ linkType: hard
+
"qr-scanner@npm:1.4.2":
version: 1.4.2
resolution: "qr-scanner@npm:1.4.2"