From 056a038e81d8a67028419abf637c076cd993d1d0 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 5 May 2026 16:39:38 -0700 Subject: [PATCH] fix: Improve folder icon rendering (#2467) --- .../thumbnail/feature/controller.ts | 10 +++++++- src/scss/thumbnail-feature-thumbnail.scss | 6 +++++ src/scss/thumbnail-feature.scss | 23 ++++++++++++++++--- .../thumbnail/feature/controller.test.ts | 13 +++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/components-lib/thumbnail/feature/controller.ts b/src/components-lib/thumbnail/feature/controller.ts index 7e594d2c..15acc1ca 100644 --- a/src/components-lib/thumbnail/feature/controller.ts +++ b/src/components-lib/thumbnail/feature/controller.ts @@ -76,7 +76,15 @@ export class ThumbnailFeatureController { if (thumbnail) { this._thumbnail = thumbnail; this._icon = null; - this._thumbnailClass = isBrandUrl(thumbnail) ? 'placeholder' : null; + // Treat as a placeholder (centered, contain-fit) when either the URL + // looks brand-like, or the item is a folder. The folder check is + // necessary because HA's media browser often returns folder thumbnails + // as local/proxy URLs that don't match isBrandUrl, even though they + // visually represent integration logos. + this._thumbnailClass = + isBrandUrl(thumbnail) || ViewItemClassifier.isFolder(item) + ? 'placeholder' + : null; } else { this._thumbnail = null; this._thumbnailClass = null; diff --git a/src/scss/thumbnail-feature-thumbnail.scss b/src/scss/thumbnail-feature-thumbnail.scss index f2c5e125..6ea283a3 100644 --- a/src/scss/thumbnail-feature-thumbnail.scss +++ b/src/scss/thumbnail-feature-thumbnail.scss @@ -31,6 +31,12 @@ img { object-fit: cover; } +// Placeholder images (brand logos, folder icons) are not previews of content, +// so they should be shown in full rather than cropped to fill a square. +:host(.placeholder) img { + object-fit: contain; +} + advanced-camera-card-icon { display: flex; width: 50%; diff --git a/src/scss/thumbnail-feature.scss b/src/scss/thumbnail-feature.scss index b1bdbea8..950fae61 100644 --- a/src/scss/thumbnail-feature.scss +++ b/src/scss/thumbnail-feature.scss @@ -45,13 +45,30 @@ advanced-camera-card-thumbnail-feature-thumbnail:not(.placeholder) { left: 0; } -// Placeholder icons/thumbnails: centered, 90% of container height +// Placeholder icons/thumbnails: nearly fill the .media container so wide logos +// can use the full width. Per-image fit (contain vs cover) is handled inside +// the inner thumbnail/icon. +// +// The two size rules look inverted (95% with text, 60% without) because the +// percentages are relative to .media, which itself differs by case: +// .has-text → .media is 100% × 50% of tile, so 95% ≈ 47% of tile height +// :not(.has-text) → .media is 100% × 100% of tile, so 60% = 60% of tile height +// Both cases land at roughly half-tile visible; without the no-text shrink the +// placeholder would fill the entire tile and stop reading as an icon. advanced-camera-card-thumbnail-feature-thumbnail.placeholder, advanced-camera-card-icon.placeholder { - height: 90%; - aspect-ratio: 1 / 1; --mdc-icon-size: 100%; } +.media.has-text > advanced-camera-card-thumbnail-feature-thumbnail.placeholder, +.media.has-text > advanced-camera-card-icon.placeholder { + width: 95%; + height: 95%; +} +.media:not(.has-text) > advanced-camera-card-thumbnail-feature-thumbnail.placeholder, +.media:not(.has-text) > advanced-camera-card-icon.placeholder { + width: 60%; + height: 60%; +} advanced-camera-card-icon.background { display: block; diff --git a/tests/components-lib/thumbnail/feature/controller.test.ts b/tests/components-lib/thumbnail/feature/controller.test.ts index d39204e7..69ac769c 100644 --- a/tests/components-lib/thumbnail/feature/controller.test.ts +++ b/tests/components-lib/thumbnail/feature/controller.test.ts @@ -177,5 +177,18 @@ describe('ThumbnailFeatureController', () => { expect(controller.getThumbnail()).toBe('https://card.camera/thumbnail.jpg'); expect(controller.getThumbnailClass()).toBeNull(); }); + + it('should set placeholder thumbnail for non-brand folder thumbnail', () => { + const controller = new ThumbnailFeatureController(); + const folder = new ViewFolder(createFolder(), [], { + title: 'Test Folder', + thumbnail: 'https://card.camera/thumbnail.jpg', + }); + + controller.calculate(null, folder, false); + + expect(controller.getThumbnail()).toBe('https://card.camera/thumbnail.jpg'); + expect(controller.getThumbnailClass()).toBe('placeholder'); + }); }); });