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"