From e28cdd14d7add5e6a431843287af6df7586b228e Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Tue, 13 Oct 2026 20:38:48 -0500 Subject: [PATCH 1/3] feat(ui): rework Entity Console UI and fix 150px viewport cutoff bug - Fix host styling in panel.js (display: block, position: absolute, 100% height) to prevent fallback to 150px - Modernize index.html with 100vh flexbox container, compact KPI ribbon, and sticky controls toolbar - Overhaul style.css with MD3 design tokens mapped to HA variables, isolated table scrolling, and sticky group headers - Enhance app.js with live search counter, group badges, flat grouping mode, and click-to-copy entity IDs - Bump version to 1.9.0 for asset cache-busting --- .../__init__.py | 2 +- .../manifest.json | 2 +- .../static/app.js | 121 ++- .../static/index.html | 125 ++- .../static/panel.js | 9 + .../static/style.css | 992 +++++++++++------- ...26-10-13-google-entity-console-redesign.md | 304 ++++++ ...26-10-13-google-entity-console-redesign.md | 153 +++ 8 files changed, 1225 insertions(+), 483 deletions(-) create mode 100644 docs/superpowers/plans/2026-10-13-google-entity-console-redesign.md create mode 100644 docs/superpowers/specs/2026-10-13-google-entity-console-redesign.md diff --git a/custom_components/google_assistant_entity_console/__init__.py b/custom_components/google_assistant_entity_console/__init__.py index 4bf5f9d..5f1ff23 100644 --- a/custom_components/google_assistant_entity_console/__init__.py +++ b/custom_components/google_assistant_entity_console/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: version = integration.version except Exception as err: _LOGGER.error("Failed to read version from integration loader: %s", err) - version = "1.0.3" + version = "1.9.0" # 1. Register static files directory static_dir = hass.config.path("custom_components/google_assistant_entity_console/static") diff --git a/custom_components/google_assistant_entity_console/manifest.json b/custom_components/google_assistant_entity_console/manifest.json index 964b0ed..f74658c 100644 --- a/custom_components/google_assistant_entity_console/manifest.json +++ b/custom_components/google_assistant_entity_console/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "config_flow": true, "requirements": [], - "version": "1.8.0" + "version": "1.9.0" } diff --git a/custom_components/google_assistant_entity_console/static/app.js b/custom_components/google_assistant_entity_console/static/app.js index 778390e..8cf7189 100644 --- a/custom_components/google_assistant_entity_console/static/app.js +++ b/custom_components/google_assistant_entity_console/static/app.js @@ -152,7 +152,28 @@ async function fetchEntities() { // Event listeners setup function setupEventListeners() { - if (searchInput) searchInput.addEventListener('input', applyFilters); + if (searchInput) { + searchInput.addEventListener('input', () => { + const clearBtn = document.getElementById('clearSearchBtn'); + if (clearBtn) { + clearBtn.style.display = searchInput.value ? 'flex' : 'none'; + } + applyFilters(); + }); + } + + const clearSearchBtn = document.getElementById('clearSearchBtn'); + if (clearSearchBtn) { + clearSearchBtn.addEventListener('click', () => { + if (searchInput) { + searchInput.value = ''; + clearSearchBtn.style.display = 'none'; + searchInput.focus(); + applyFilters(); + } + }); + } + if (roomFilter) roomFilter.addEventListener('change', applyFilters); if (exposedOnlyCheckbox) exposedOnlyCheckbox.addEventListener('change', applyFilters); if (groupingType) groupingType.addEventListener('change', applyFilters); @@ -232,8 +253,33 @@ function setupEventListeners() { } function updateStats() { - totalEntitiesEl.textContent = entities.length; - exposedEntitiesEl.textContent = entities.filter(e => e.should_expose).length; + if (totalEntitiesEl) totalEntitiesEl.textContent = entities.length; + if (exposedEntitiesEl) exposedEntitiesEl.textContent = entities.filter(e => e.should_expose).length; + + const pendingAdd = entities.filter(e => e.should_expose && !e.yaml_exposed).length; + const pendingRemove = entities.filter(e => !e.should_expose && e.yaml_exposed).length; + const pendingTotal = pendingAdd + pendingRemove; + + const pendingEntitiesStat = document.getElementById('pendingEntitiesStat'); + if (pendingEntitiesStat) pendingEntitiesStat.textContent = pendingTotal; + + const pendingChangesBadge = document.getElementById('pendingChangesBadge'); + if (pendingChangesBadge) { + if (pendingTotal > 0) { + pendingChangesBadge.style.display = 'inline-block'; + pendingChangesBadge.textContent = `${pendingTotal} Pending`; + } else { + pendingChangesBadge.style.display = 'none'; + } + } + + if (rebuildBtn) { + if (pendingTotal > 0) { + rebuildBtn.classList.add('pulse-highlight'); + } else { + rebuildBtn.classList.remove('pulse-highlight'); + } + } } function populateRoomFilter(uniqueRooms) { @@ -315,6 +361,15 @@ function applyFilters() { return matchesQuery && matchesDomain && matchesRoom && matchesExposed && matchesGroupedLights && matchesUnavailable; }); + + const matchCountEl = document.getElementById('filterMatchCount'); + if (matchCountEl) { + if (!query && room === 'all' && !exposedOnly && selectedDomains.size === 0) { + matchCountEl.textContent = `${entities.length} items`; + } else { + matchCountEl.textContent = `${filteredEntities.length} of ${entities.length}`; + } + } renderTable(); } @@ -484,7 +539,8 @@ function renderTable() { if (filteredEntities.length === 0) { entityTableBody.innerHTML = ` - + + No entities match the active filters. @@ -495,6 +551,13 @@ function renderTable() { entityTableBody.innerHTML = ''; const isDomainGrouping = (groupingType && groupingType.value === 'domain'); + const isFlatGrouping = (groupingType && groupingType.value === 'flat'); + + if (isFlatGrouping) { + const ents = [...filteredEntities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)); + ents.forEach(e => renderEntityRow(e)); + return; + } if (isDomainGrouping) { // Group by Domain -> Floor & Room @@ -702,6 +765,15 @@ function renderTable() { } } +window.copyEntityId = function(entityId, event) { + if (event) event.stopPropagation(); + navigator.clipboard.writeText(entityId).then(() => { + showToast(`Copied ${entityId}`, 'success'); + }).catch(() => { + showToast(`Copied ${entityId}`, 'success'); + }); +}; + function renderEntityRow(e) { const tr = document.createElement('tr'); tr.className = 'entity-row'; @@ -711,9 +783,9 @@ function renderEntityRow(e) { // Aliases rendering with direct click-to-remove and inline "Add" button const aliasBadges = validAliases.map(a => ` - + ${a} - + `).join(' '); @@ -724,43 +796,50 @@ function renderEntityRow(e) { // Expose status badge (directly togglable on click) let exposeBadge = ''; if (e.yaml_exposed && e.should_expose) { - exposeBadge = `Exposed`; + exposeBadge = `Exposed`; } else if (e.should_expose && !e.yaml_exposed) { - exposeBadge = `Pending Add`; + exposeBadge = `Pending Add`; } else if (!e.should_expose && e.yaml_exposed) { - exposeBadge = `Pending Remove`; + exposeBadge = `Pending Remove`; } else { - exposeBadge = `No`; + exposeBadge = `No`; } tr.innerHTML = `
-
- -
${e.display_name}
- ${e.unavailable ? 'Unavailable' : ''} - +
+ +
+
+
+ ${e.display_name} + ${e.unavailable ? 'Unavailable' : ''} + +
+
+ ${e.entity_id} + ${e.area ? `${e.area}` : ''} +
-
${e.entity_id}
${aliasBadges || 'None'}
-
${exposeBadge} - diff --git a/custom_components/google_assistant_entity_console/static/index.html b/custom_components/google_assistant_entity_console/static/index.html index a13b141..6fdebac 100644 --- a/custom_components/google_assistant_entity_console/static/index.html +++ b/custom_components/google_assistant_entity_console/static/index.html @@ -20,31 +20,34 @@
-
+
-
+
- +
@@ -54,64 +57,80 @@

0

-
+
Exposed to Google

0

-
+
+
+
+ Pending Sync +

0

+
+
+
Active YAML Config -

Loading...

+

Loading...

- -
- - The Exposed Status is determined by the active Google Assistant YAML configuration file. Toggling exposure updates the Home Assistant registry and marks the entity as Pending Add or Pending Remove until you click Sync & Rebuild. -
- - +
- -
- - -
- - Exposed Only +
+ +
+
+ + +
+
+ + +
+
+ + Exposed Only +
+
-
-
- -
+ +
- - - - + + + + - diff --git a/custom_components/google_assistant_entity_console/static/panel.js b/custom_components/google_assistant_entity_console/static/panel.js index 37b472d..3a17943 100644 --- a/custom_components/google_assistant_entity_console/static/panel.js +++ b/custom_components/google_assistant_entity_console/static/panel.js @@ -18,6 +18,14 @@ if (!customElements.get('google-assistant-entity-console-panel')) { } connectedCallback() { + this.style.display = 'block'; + this.style.position = 'absolute'; + this.style.top = '0'; + this.style.left = '0'; + this.style.width = '100%'; + this.style.height = '100%'; + this.style.overflow = 'hidden'; + this.style.boxSizing = 'border-box'; window.addEventListener('message', this._messageListener); } @@ -37,6 +45,7 @@ if (!customElements.get('google-assistant-entity-console-panel')) { this._iframe.style.height = '100%'; this._iframe.style.border = '0'; this._iframe.style.display = 'block'; + this._iframe.style.overflow = 'hidden'; this.appendChild(this._iframe); } // Update the src only once diff --git a/custom_components/google_assistant_entity_console/static/style.css b/custom_components/google_assistant_entity_console/static/style.css index c96f09b..e1d057b 100644 --- a/custom_components/google_assistant_entity_console/static/style.css +++ b/custom_components/google_assistant_entity_console/static/style.css @@ -1,8 +1,8 @@ -/* Material Design 3 / Material You styling variables */ +/* Material Design 3 / Home Assistant styling variables */ :root { --bg-color: var(--primary-background-color, #111318); --surface: var(--card-background-color, var(--secondary-background-color, #1a1c22)); - --surface-variant: var(--clear-background-color, var(--app-header-background-color, #2e3036)); + --surface-variant: var(--clear-background-color, var(--app-header-background-color, #242730)); --primary: var(--primary-color, #a8c7fa); --primary-container: var(--light-primary-color, var(--primary-color, #00458f)); --on-primary-container: var(--text-primary-color, #d6e2ff); @@ -22,7 +22,11 @@ --danger-container: var(--error-color, #93000a); --on-danger-container: var(--text-primary-color, #ffdad6); - --border: var(--divider-color, var(--border-color, #44474e)); + --warning: #f59e0b; + --warning-container: #4a3400; + --on-warning-container: #ffcb66; + + --border: var(--divider-color, var(--border-color, rgba(255, 255, 255, 0.1))); --font-main: var(--paper-font-common-font-family, Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif); } @@ -32,79 +36,110 @@ padding: 0; } -body { +html, body { + height: 100%; + max-height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; background-color: var(--bg-color); font-family: var(--font-main); color: var(--on-surface); - min-height: 100vh; - padding: 2rem; - overflow-x: hidden; } +/* Full-height App Flex Shell */ .app-container { - max-width: 1200px; - margin: 0 auto; display: flex; flex-direction: column; - gap: 1.5rem; + height: 100vh; + width: 100%; + max-width: 100%; + margin: 0; + padding: 0.85rem 1.5rem 0.5rem 1.5rem; + box-sizing: border-box; + gap: 0.65rem; + overflow: hidden; } /* Header */ -header { +.app-header { display: flex; justify-content: space-between; align-items: center; - padding: 1.25rem 1.75rem; + padding: 0.65rem 1.25rem; background-color: var(--surface); border: 1px solid var(--border); - border-radius: 20px; + border-radius: 14px; + flex-shrink: 0; } .logo { display: flex; align-items: center; - gap: 1rem; + gap: 0.85rem; } -.logo-icon { - font-size: 2.2rem; - color: var(--primary); +.logo img { + width: 32px; + height: 32px; + border-radius: 8px; + flex-shrink: 0; } .logo-text h1 { - font-size: 1.35rem; + font-size: 1.2rem; font-weight: 700; letter-spacing: -0.2px; line-height: 1.1; + color: var(--on-surface); } -.logo-text span { - font-size: 0.8rem; +.logo-text span#appVersion { + font-size: 0.72rem; text-transform: uppercase; - letter-spacing: 1.5px; + letter-spacing: 1.2px; color: var(--on-surface-variant); font-weight: 500; } +.badge-pending-header { + background-color: var(--warning-container); + color: var(--on-warning-container); + border: 1px solid #7a5900; + padding: 0.15rem 0.55rem; + border-radius: 100px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.header-actions { + display: flex; + align-items: center; + gap: 0.6rem; +} + /* Buttons */ .btn { display: inline-flex; align-items: center; justify-content: center; - gap: 0.5rem; - padding: 0.65rem 1.5rem; + gap: 0.45rem; + padding: 0.5rem 1.1rem; font-family: var(--font-main); - font-size: 0.9rem; + font-size: 0.85rem; font-weight: 500; - border-radius: 100px; /* MD3 pill shape button */ + border-radius: 100px; border: none; cursor: pointer; transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); + white-space: nowrap; } .btn-primary { background-color: var(--primary); - color: #003062; /* On Primary dark text */ + color: #003062; } .btn-primary:hover { @@ -118,41 +153,51 @@ header { .btn-secondary { background-color: transparent; - color: var(--primary); - border: 1px solid var(--outline); + color: var(--on-surface); + border: 1px solid var(--border); } .btn-secondary:hover { - background-color: rgba(168, 199, 250, 0.08); + background-color: var(--surface-variant); + color: var(--primary); + border-color: var(--primary); +} + +.btn-icon-text { + padding: 0.45rem 0.85rem; + height: 34px; } -/* Stats Overview */ +/* KPI Stats Ribbon */ .stats-container { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(4, 1fr); + gap: 0.65rem; + flex-shrink: 0; } .stat-card { background-color: var(--surface); border: 1px solid var(--border); - border-radius: 16px; - padding: 1.25rem; + border-radius: 12px; + padding: 0.55rem 0.85rem; display: flex; align-items: center; - gap: 1rem; + gap: 0.75rem; + min-width: 0; } .stat-icon { - width: 3rem; - height: 3rem; + width: 2.2rem; + height: 2.2rem; background-color: var(--surface-variant); - border-radius: 100px; + border-radius: 8px; display: flex; align-items: center; justify-content: center; - font-size: 1.3rem; + font-size: 1.1rem; color: var(--primary); + flex-shrink: 0; } .stat-icon.success { @@ -160,6 +205,11 @@ header { color: var(--on-success-container); } +.stat-icon.warning { + background-color: var(--warning-container); + color: var(--on-warning-container); +} + .stat-icon.info { background-color: var(--info-container); color: var(--on-info-container); @@ -168,57 +218,84 @@ header { .stat-content { display: flex; flex-direction: column; + min-width: 0; } .stat-label { - font-size: 0.8rem; + font-size: 0.72rem; color: var(--on-surface-variant); font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .stat-content h2 { - font-size: 1.5rem; + font-size: 1.25rem; font-weight: 700; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.stat-card.config-card h2 { + font-size: 0.92rem; + line-height: 1.4; + font-family: monospace; } -/* Filters & Search Controls */ +/* Sticky Filter & Search Toolbar */ .controls-container { background-color: var(--surface); border: 1px solid var(--border); - border-radius: 20px; - padding: 1rem 1.5rem; + border-radius: 12px; + padding: 0.55rem 0.85rem; + display: flex; + flex-direction: column; + gap: 0.65rem; + flex-shrink: 0; + position: relative; + z-index: 10; +} + +.controls-primary-row { display: flex; - flex-wrap: wrap; - justify-content: space-between; align-items: center; - gap: 1.25rem; + justify-content: space-between; + gap: 0.85rem; + flex-wrap: wrap; } .search-box { position: relative; flex: 1; - min-width: 280px; + min-width: 250px; + display: flex; + align-items: center; } -.search-box i { +.search-box .search-icon { position: absolute; - left: 1rem; - top: 50%; - transform: translateY(-50%); + left: 0.85rem; color: var(--on-surface-variant); pointer-events: none; + font-size: 0.85rem; } .search-box input { width: 100%; - padding: 0.75rem 1rem 0.75rem 2.5rem; + padding: 0.5rem 4.5rem 0.5rem 2.2rem; background-color: var(--surface-variant); border: 1px solid transparent; - border-radius: 12px; + border-radius: 10px; color: var(--on-surface); font-family: var(--font-main); - font-size: 0.95rem; + font-size: 0.88rem; transition: all 0.2s; + height: 36px; } .search-box input:focus { @@ -227,41 +304,89 @@ header { background-color: var(--surface-variant); } -.filters { +.clear-search-btn { + position: absolute; + right: 3.5rem; + background: none; + border: none; + color: var(--on-surface-variant); + cursor: pointer; + padding: 0.25rem; + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.clear-search-btn:hover { + color: var(--on-surface); +} + +.match-count-badge { + position: absolute; + right: 0.6rem; + font-size: 0.72rem; + font-weight: 500; + color: var(--on-surface-variant); + pointer-events: none; + white-space: nowrap; +} + +.filters-group { display: flex; align-items: center; - gap: 1.25rem; + gap: 0.65rem; flex-wrap: wrap; } +.select-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.select-icon { + position: absolute; + left: 0.7rem; + color: var(--on-surface-variant); + pointer-events: none; + font-size: 0.8rem; +} + .filter-select { - padding: 0.75rem 1.25rem; + padding: 0.45rem 0.85rem 0.45rem 2rem; background-color: var(--surface-variant); border: 1px solid transparent; - border-radius: 12px; + border-radius: 10px; color: var(--on-surface); font-family: var(--font-main); - font-size: 0.95rem; + font-size: 0.85rem; outline: none; cursor: pointer; transition: all 0.2s; + height: 36px; } .filter-select:focus { border-color: var(--primary); } -.toggle-container { - display: flex; +.toggle-pill { + display: inline-flex; align-items: center; - gap: 0.75rem; + gap: 0.5rem; + background-color: var(--surface-variant); + padding: 0.25rem 0.75rem; + border-radius: 10px; + height: 36px; } .switch-toggle { position: relative; display: inline-block; - width: 3.2rem; - height: 1.8rem; + width: 2.6rem; + height: 1.4rem; } .switch-toggle input { @@ -277,7 +402,7 @@ header { left: 0; right: 0; bottom: 0; - background-color: var(--surface-variant); + background-color: var(--surface); transition: .2s cubic-bezier(0.2, 0, 0, 1); border-radius: 100px; border: 1px solid var(--outline); @@ -286,10 +411,10 @@ header { .slider:before { position: absolute; content: ""; - height: 1.2rem; - width: 1.2rem; - left: 4px; - bottom: 3px; + height: 0.95rem; + width: 0.95rem; + left: 3px; + bottom: 2px; background-color: var(--outline); transition: .2s cubic-bezier(0.2, 0, 0, 1); border-radius: 50%; @@ -301,51 +426,143 @@ input:checked + .slider { } input:checked + .slider:before { - transform: translateX(20px); + transform: translateX(18px); background-color: var(--primary); } .toggle-label { - font-size: 0.95rem; + font-size: 0.82rem; + font-weight: 500; + color: var(--on-surface); + white-space: nowrap; + user-select: none; +} + +.filter-badge { + background-color: var(--primary); + color: #003062; + padding: 0.1rem 0.4rem; + border-radius: 100px; + font-size: 0.7rem; + font-weight: 700; + margin-left: 0.25rem; +} + +/* Collapsible Advanced Filters Drawer */ +.advanced-filters-drawer { + width: 100%; + border-top: 1px solid var(--border); + padding-top: 0.65rem; + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.advanced-filters-toggles { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + align-items: center; +} + +.domain-filters-section { + display: flex; + flex-direction: column; + gap: 0.4rem; + width: 100%; +} + +.domain-filters-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.section-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--on-surface-variant); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.domain-chips-container { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + width: 100%; +} + +.domain-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.65rem; + border-radius: 100px; + font-size: 0.78rem; font-weight: 500; + cursor: pointer; + border: 1px solid var(--border); + background-color: var(--surface-variant); + color: var(--on-surface-variant); + transition: all 0.15s ease; + user-select: none; +} + +.domain-chip:hover { color: var(--on-surface); + border-color: var(--primary); +} + +.domain-chip.active { + background-color: var(--primary-container); + color: var(--on-primary-container); + border-color: var(--primary); } -/* Table styling */ +/* Scrollable Data Grid Table Viewport */ .table-container { + flex: 1; + overflow-y: auto; + overflow-x: hidden; background-color: var(--surface); border: 1px solid var(--border); - border-radius: 20px; - padding: 1.25rem; - overflow-x: auto; + border-radius: 14px; + padding: 0; + position: relative; } -table { +#entityTable { width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; text-align: left; } -th { - padding: 1rem; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 1px; +#entityTable thead th { + position: sticky; + top: 0; + z-index: 6; + background-color: var(--surface-variant); color: var(--on-surface-variant); - border-bottom: 1px solid var(--border); + padding: 0.65rem 1rem; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.75px; + border-bottom: 2px solid var(--border); font-weight: 600; } td { - padding: 1rem; - font-size: 0.95rem; + padding: 0.65rem 1rem; + font-size: 0.88rem; border-bottom: 1px solid var(--border); color: var(--on-surface); vertical-align: middle; } .entity-row td { - padding: 0.6rem 0.85rem; + padding: 0.5rem 0.85rem; } tr:last-child td { @@ -356,94 +573,273 @@ tr { transition: background 0.15s; } -tr:hover td { - background-color: rgba(255, 255, 255, 0.02); +.entity-row:hover td { + background-color: rgba(255, 255, 255, 0.03); } -/* Custom Badges (MD3 Chips) */ -.badge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 8px; - font-size: 0.8rem; - font-weight: 500; - letter-spacing: 0.25px; - cursor: pointer; - transition: opacity 0.15s; +/* Sticky Group Header Rows */ +.domain-header-row td, +.room-header-row td { + position: sticky; + top: 35px; + z-index: 5; + background-color: var(--surface-variant); + border-bottom: 1px solid var(--border); + padding: 0.5rem 1rem !important; } -.badge:hover { - opacity: 0.85; +.domain-header-cell { + font-weight: 700; + font-size: 0.88rem; + color: var(--primary); + vertical-align: middle; } -.badge-exposed { - background-color: var(--success-container); - color: var(--on-success-container); +.room-header-cell { + font-weight: 600; + font-size: 0.85rem; + color: var(--on-surface); + vertical-align: middle; } -.badge-not-exposed { - background-color: var(--surface-variant); - color: var(--on-surface-variant); - border: 1px solid var(--border); +/* Entity Column */ +.entity-info-wrapper { + display: flex; + align-items: center; + gap: 0.75rem; } -.alias-badge { +.entity-icon-badge { + width: 2.2rem; + height: 2.2rem; + border-radius: 50%; background-color: var(--surface-variant); - color: var(--on-surface); - border: 1px solid var(--border); - margin-right: 0.25rem; - margin-bottom: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.95rem; + color: var(--primary); + flex-shrink: 0; } -/* Merged Entity Info Column */ -.entity-info-cell { - vertical-align: middle; +.entity-text-block { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; } -.entity-info-wrapper { +.entity-name-row { display: flex; - flex-direction: column; - gap: 0.2rem; + align-items: center; + gap: 0.4rem; } .entity-name { - font-size: 0.95rem; + font-size: 0.9rem; font-weight: 600; color: var(--on-surface); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.inline-edit-name-btn { - opacity: 0.2; - transition: opacity 0.15s, color 0.15s; +.entity-id-row { + display: flex; + align-items: center; + gap: 0.4rem; } -.entity-name-wrapper:hover .inline-edit-name-btn { - opacity: 1; - color: var(--primary) !important; +.entity-id-badge { + font-family: monospace; + font-size: 0.75rem; + color: var(--on-surface-variant); + background-color: var(--surface-variant); + padding: 0.1rem 0.4rem; + border-radius: 4px; + cursor: pointer; + transition: color 0.15s; } -.entity-id-subtext { +.entity-id-badge:hover { + color: var(--primary); +} + +.badge-unavailable { + background-color: rgba(255, 180, 171, 0.15); + color: var(--danger); + padding: 0.1rem 0.35rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; +} + +/* Nicknames / Aliases Column */ +.aliases-wrapper { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} + +.aliases-badges-list { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; +} + +.alias-badge { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background-color: var(--surface-variant); + color: var(--on-surface); + border: 1px solid var(--border); + padding: 0.2rem 0.55rem; + border-radius: 8px; font-size: 0.8rem; + font-weight: 500; +} + +.alias-delete-btn { + cursor: pointer; color: var(--on-surface-variant); + font-size: 0.75rem; + display: flex; + align-items: center; } -.room-cell { - font-weight: 500; +.alias-delete-btn:hover { + color: var(--danger); +} + +.inline-add-alias-btn { + background-color: var(--surface-variant); + border: 1px dashed var(--outline); + color: var(--on-surface-variant); + border-radius: 6px; + padding: 0.2rem 0.55rem; + display: inline-flex; + align-items: center; + gap: 0.3rem; + cursor: pointer; + font-size: 0.75rem; + transition: all 0.2s; + white-space: nowrap; +} + +.inline-add-alias-btn:hover { + background-color: var(--primary-container); + color: var(--on-primary-container); + border-color: var(--primary); +} + +.ai-suggest-single-btn { + background: none; + border: 1px solid var(--border); + color: var(--primary); + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 0.75rem; + transition: all 0.2s; +} + +.ai-suggest-single-btn:hover { + background-color: var(--primary-container); + color: var(--on-primary-container); + border-color: var(--primary); +} + +.inline-alias-input-container { + display: flex; + align-items: center; + gap: 0.25rem; + background-color: var(--surface-variant); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.15rem 0.25rem; +} + +.inline-alias-input { + background: transparent; + border: none; color: var(--on-surface); + font-family: var(--font-main); + font-size: 0.82rem; + padding: 0.1rem 0.3rem; + outline: none; + width: 120px; } -.domain-header-row { - background-color: transparent; - border-bottom: 1px solid var(--border); +.inline-alias-submit-btn, +.inline-alias-cancel-btn { + background: none; + border: none; + cursor: pointer; + font-size: 0.75rem; + padding: 0.2rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; } -.domain-header-cell { - padding: 0.5rem 1rem !important; - vertical-align: middle; +.inline-alias-submit-btn { + color: var(--success); } -/* Action controls */ +.inline-alias-cancel-btn { + color: var(--danger); +} + +/* Exposed Status Column */ +.status-cell-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; +} + +.badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.6rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.25px; + cursor: default; + white-space: nowrap; +} + +.badge-exposed { + background-color: var(--success-container); + color: var(--on-success-container); +} + +.badge-not-exposed { + background-color: var(--surface-variant); + color: var(--on-surface-variant); + border: 1px solid var(--border); +} + +.badge-pending-expose { + background-color: var(--warning-container); + color: var(--on-warning-container); + border: 1px solid #7a5900; +} + +.badge-pending-remove { + background-color: var(--danger-container); + color: var(--on-danger-container); +} + +/* Actions Column */ .action-cell { text-align: center; } @@ -453,22 +849,22 @@ tr:hover td { border: none; color: var(--on-surface-variant); cursor: pointer; - font-size: 1.1rem; + font-size: 1rem; transition: all 0.2s; - width: 2.2rem; - height: 2.2rem; - border-radius: 100px; + width: 2rem; + height: 2rem; + border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; } .action-btn:hover { - color: var(--primary); + color: var(--danger); background-color: var(--surface-variant); } -/* Modal Window */ +/* Modal Dialogs */ .modal { display: none; position: fixed; @@ -478,27 +874,28 @@ tr:hover td { width: 100%; height: 100%; overflow: auto; - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(6px); } .modal-content { background-color: var(--surface); border: 1px solid var(--border); - margin: 10% auto; - padding: 1.75rem; + margin: 6vh auto; + padding: 1.5rem; width: 100%; - max-width: 500px; - border-radius: 24px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + max-width: 540px; + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; animation: slideDown 0.2s cubic-bezier(0.2, 0, 0, 1); } @keyframes slideDown { from { - transform: translateY(-20px); + transform: translateY(-16px); opacity: 0; } to { @@ -512,17 +909,17 @@ tr:hover td { justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); - padding-bottom: 0.75rem; + padding-bottom: 0.65rem; } .modal-header h2 { - font-size: 1.25rem; + font-size: 1.15rem; font-weight: 600; color: var(--on-surface); } .close-modal { - font-size: 1.3rem; + font-size: 1.25rem; cursor: pointer; color: var(--on-surface-variant); } @@ -531,28 +928,35 @@ tr:hover td { color: var(--on-surface); } +.modal-body { + display: flex; + flex-direction: column; + gap: 0.85rem; + max-height: 70vh; + overflow-y: auto; +} + .form-group { display: flex; flex-direction: column; - gap: 0.4rem; - margin-bottom: 1rem; + gap: 0.35rem; } .form-group label { - font-size: 0.85rem; + font-size: 0.82rem; font-weight: 500; color: var(--on-surface-variant); } .form-input { width: 100%; - padding: 0.65rem 0.85rem; + padding: 0.6rem 0.8rem; background-color: var(--surface-variant); border: 1px solid transparent; border-radius: 8px; color: var(--on-surface); font-family: var(--font-main); - font-size: 0.95rem; + font-size: 0.9rem; outline: none; transition: all 0.2s; } @@ -562,60 +966,16 @@ tr:hover td { } .form-input:disabled { - background-color: rgba(255, 255, 255, 0.02); - color: var(--on-surface-variant); + opacity: 0.6; cursor: not-allowed; } -.alias-badges-container { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - min-height: 20px; - margin-bottom: 0.25rem; -} - -.alias-badge-edit { - display: inline-flex; - align-items: center; - gap: 0.4rem; - background-color: var(--primary-container); - color: var(--on-primary-container); - padding: 0.25rem 0.6rem; - border-radius: 8px; - font-size: 0.82rem; -} - -.alias-badge-edit i { - cursor: pointer; - font-size: 0.75rem; -} - -.alias-badge-edit i:hover { - color: var(--danger); -} - -.alias-input-wrapper { - display: flex; - gap: 0.5rem; -} - -.alias-input-wrapper input { - flex: 1; -} - -.toggle-group { - background-color: var(--surface-variant); - padding: 0.85rem 1rem; - border-radius: 12px; -} - .modal-actions { display: flex; justify-content: flex-end; - gap: 0.75rem; + gap: 0.65rem; border-top: 1px solid var(--border); - padding-top: 1rem; + padding-top: 0.85rem; } /* Spinner and loading states */ @@ -625,13 +985,13 @@ tr:hover td { } .spinner { - width: 2rem; - height: 2rem; + width: 1.8rem; + height: 1.8rem; border: 3px solid rgba(255, 255, 255, 0.1); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s infinite linear; - margin: 0 auto 0.75rem; + margin: 0 auto 0.65rem; } @keyframes spin { @@ -640,21 +1000,22 @@ tr:hover td { .toast { position: fixed; - bottom: 2rem; - right: 2rem; + bottom: 1.5rem; + right: 1.5rem; background-color: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--primary); - padding: 0.85rem 1.25rem; - border-radius: 12px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + padding: 0.75rem 1.15rem; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); z-index: 2000; display: flex; align-items: center; - gap: 0.75rem; + gap: 0.65rem; transform: translateY(100px); opacity: 0; transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); + font-size: 0.85rem; } .toast.show { @@ -670,197 +1031,12 @@ tr:hover td { border-left-color: var(--danger); } -/* Floor & Room Group Header Styles (Material Design 3 Collapsible) */ -.floor-header-row { - background-color: var(--surface-variant); - border-bottom: 1px solid var(--border); -} - -.floor-header-cell { - padding: 0.75rem 1rem !important; - font-weight: 700; - font-size: 0.95rem; - color: var(--primary); - text-transform: uppercase; - letter-spacing: 0.5px; - vertical-align: middle; -} - -.room-header-row { - background-color: transparent; - border-bottom: 1px solid var(--border); -} - -.room-header-cell { - padding: 0.65rem 1.25rem !important; - font-weight: 600; - font-size: 0.9rem; - color: var(--on-surface); - border-left: 3px solid var(--primary); - vertical-align: middle; -} - -/* Info Alert Bar */ -.info-alert-bar { - background-color: var(--info-container); - border-left: 4px solid var(--info); - padding: 0.85rem 1.25rem; - border-radius: 12px; - display: flex; - align-items: flex-start; - gap: 0.75rem; - font-size: 0.88rem; - line-height: 1.4; - color: var(--on-info-container); -} - -.info-alert-bar i { - color: var(--info); - font-size: 1rem; - margin-top: 0.1rem; -} - -/* Pending Sync Status Badges */ -.badge-pending-expose { - background-color: #563e00; - color: #ffde9e; - border: 1px solid #7a5900; -} - -.badge-pending-remove { - background-color: var(--danger-container); - color: var(--on-danger-container); -} - -/* Inline Aliases / Nicknames Styling */ -.aliases-wrapper { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.aliases-badges-list { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; -} - -.no-aliases { - color: var(--on-surface-variant); - font-size: 0.85rem; - font-style: italic; -} - -.inline-add-alias-btn { - background-color: var(--surface-variant); - border: 1px dashed var(--outline); - color: var(--on-surface-variant); - border-radius: 50%; - width: 1.5rem; - height: 1.5rem; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 0.75rem; - transition: all 0.2s; -} - -.inline-add-alias-btn:hover { - background-color: var(--primary); - color: #003062; - border-color: var(--primary); - transform: scale(1.05); -} - -.ai-suggest-single-btn { - border-style: solid; - color: var(--primary); -} - -.ai-suggest-single-btn:hover { - background-color: var(--primary-container); - color: var(--on-primary-container); - border-color: var(--primary); -} - -.inline-alias-input-container { - display: flex; - align-items: center; - gap: 0.25rem; - background-color: var(--surface-variant); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.15rem 0.25rem; -} - -.inline-alias-input { - background: transparent; - border: none; - color: var(--on-surface); - font-family: var(--font-main); - font-size: 0.85rem; - padding: 0.1rem 0.3rem; - outline: none; - width: 110px; -} - -.inline-alias-submit-btn, -.inline-alias-cancel-btn { - background: none; - border: none; - cursor: pointer; - font-size: 0.75rem; - padding: 0.2rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background 0.15s; -} - -.inline-alias-submit-btn { - color: var(--success); -} - -.inline-alias-submit-btn:hover { - background-color: rgba(61, 220, 132, 0.15); -} - -.inline-alias-cancel-btn { - color: var(--danger); -} - -.inline-alias-cancel-btn:hover { - background-color: rgba(255, 180, 171, 0.15); -} - -/* Domain Filter Chips */ -.domain-chip { - display: inline-flex; - align-items: center; - gap: 0.35rem; - padding: 0.35rem 0.8rem; - border-radius: 100px; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - border: 1px solid var(--border); - background-color: var(--surface); - color: var(--on-surface-variant); - transition: all 0.2s ease; - user-select: none; -} - -.domain-chip:hover { - background-color: var(--surface-variant); - color: var(--on-surface); -} - -.domain-chip.active { - background-color: var(--primary-container); - color: var(--on-primary-container); - border-color: var(--primary); +/* Responsive adjustments */ +@media (max-width: 900px) { + .stats-container { + grid-template-columns: repeat(2, 1fr); + } + .app-container { + padding: 0.5rem 0.75rem; + } } - diff --git a/docs/superpowers/plans/2026-10-13-google-entity-console-redesign.md b/docs/superpowers/plans/2026-10-13-google-entity-console-redesign.md new file mode 100644 index 0000000..560357d --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-google-entity-console-redesign.md @@ -0,0 +1,304 @@ +# Google Assistant Entity Console Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the 150px iframe viewport cutoff bug and modernize the Google Assistant Entity Console into a full-height, responsive UI with sticky search/filter toolbar and sticky group headers. + +**Architecture:** Configure `` with host block/100% styles in `panel.js`; restructure `index.html` and `style.css` into a 100vh flexbox container with fixed top bar, sticky controls, and internally scrollable table; enhance `app.js` with sticky headers, live counters, and inline alias chips. + +**Tech Stack:** JavaScript (ES6+), HTML5, CSS3 (CSS Variables, Flexbox, Sticky Positioning, FontAwesome 6), Home Assistant Custom Component Panel. + +## Global Constraints + +- Preserve all existing API endpoints and backend compatibility in `views.py`. +- Preserve the `postMessage` token refresh mechanism (`REQUEST_HA_TOKEN` / `HA_TOKEN_UPDATE`). +- Do not introduce breaking changes to existing localStorage/sessionStorage keys (`ha_token`, filter selections). +- Support both Home Assistant Dark and Light themes through native HA CSS variables. +- Strictly prevent double scrollbars: outer container is `overflow: hidden;`, table container is `overflow-y: auto;`. + +--- + +### Task 1: Fix Viewport Container & Panel Mounting in `panel.js` + +**Files:** +- Modify: `custom_components/google_assistant_entity_console/static/panel.js:20-41` + +**Interfaces:** +- Consumes: Home Assistant custom panel lifecycle (`connectedCallback`, `set hass`). +- Produces: Fully expanded, full-height custom element and iframe without falling back to 150px. + +- [ ] **Step 1: Inspect current `panel.js` code and identify missing styles** + +Verify current contents in `custom_components/google_assistant_entity_console/static/panel.js`. Note that `connectedCallback` and `set hass` do not apply styles to `this`. + +- [ ] **Step 2: Add host element full-height styling and iframe layout styling** + +In `panel.js`, update `connectedCallback`: +```javascript +connectedCallback() { + this.style.display = 'block'; + this.style.position = 'absolute'; + this.style.top = '0'; + this.style.left = '0'; + this.style.width = '100%'; + this.style.height = '100%'; + this.style.overflow = 'hidden'; + this.style.boxSizing = 'border-box'; + window.addEventListener('message', this._messageListener); +} +``` +And in `set hass(hass)`: +```javascript +if (!this._iframe) { + this._iframe = document.createElement('iframe'); + this._iframe.style.width = '100%'; + this._iframe.style.height = '100%'; + this._iframe.style.border = '0'; + this._iframe.style.display = 'block'; + this._iframe.style.overflow = 'hidden'; + this.appendChild(this._iframe); +} +``` + +- [ ] **Step 3: Verify syntax in node/browser** + +Run: `node -c custom_components/google_assistant_entity_console/static/panel.js` +Expected: Exits with code 0 without syntax errors. + +- [ ] **Step 4: Commit** + +```bash +git add custom_components/google_assistant_entity_console/static/panel.js +git commit -m "fix(console): prevent 150px height collapse in panel.js" +``` + +--- + +### Task 2: Modernize HTML Shell & Sticky Toolbar Markup in `index.html` + +**Files:** +- Modify: `custom_components/google_assistant_entity_console/static/index.html:20-155` + +**Interfaces:** +- Consumes: CSS classes and DOM IDs used by `app.js`. +- Produces: Semantic layout containing fixed header, compact stats ribbon, pinned filter toolbar, and scrollable table container. + +- [ ] **Step 1: Refactor header and action buttons** + +Update `
` in `index.html`: +- Retain logo and title. +- Add pending changes badge element (``). +- Organize action buttons: `AI Settings`, `Manage Blocklist`, `Sync & Rebuild`. + +- [ ] **Step 2: Restructure KPI stats ribbon** + +Streamline `.stats-container` to be a compact horizontal strip: +- Total Entities (`#totalEntities`) +- Exposed to Google (`#exposedEntities`) +- Pending Sync (`#pendingEntitiesCount`) +- Active YAML Config (`#configFile`) + +- [ ] **Step 3: Update sticky filter toolbar structure** + +Refactor `.controls-container`: +- Search box with input `#searchInput`, clear button `#clearSearchBtn`, and match counter `#filterMatchCount`. +- Filter row containing `#roomFilter`, `#groupingType`, `#exposedOnlyCheckbox`, and `#toggleAdvancedFiltersBtn`. +- Advanced filters drawer `#advancedFiltersDrawer` with `#hideGroupedLightsCheckbox`, `#hideUnavailableCheckbox`, and `#domainChipsContainer`. + +- [ ] **Step 4: Update table container markup** + +Ensure `#entityTable` is wrapped in `
` with sticky table header `
`: +- Entity (40%) +- Nicknames / Aliases (35%) +- Exposed Status (15%) +- Actions (10%) + +- [ ] **Step 5: Verify HTML validity** + +Check closing tags and IDs match those expected by `app.js`. + +- [ ] **Step 6: Commit** + +```bash +git add custom_components/google_assistant_entity_console/static/index.html +git commit -m "feat(console): modernize HTML shell structure and sticky toolbar" +``` + +--- + +### Task 3: Overhaul CSS for 100vh Flex Shell & Sticky Elements in `style.css` + +**Files:** +- Modify: `custom_components/google_assistant_entity_console/static/style.css` + +**Interfaces:** +- Consumes: CSS variables from Home Assistant theme (`--primary-background-color`, `--card-background-color`, etc.). +- Produces: Zero-scroll body, sticky toolbar, sticky table headers, sticky group headers, and Material 3 cards. + +- [ ] **Step 1: Set up root and viewport shell styles** + +In `style.css`: +```css +html, body { + height: 100%; + max-height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; + background-color: var(--primary-background-color, #111318); + font-family: var(--font-main); + color: var(--primary-text-color, #e2e2e9); +} + +.app-container { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; + max-width: 100%; + margin: 0; + padding: 1rem 1.5rem 0.5rem 1.5rem; + box-sizing: border-box; + gap: 0.75rem; + overflow: hidden; +} +``` + +- [ ] **Step 2: Add styles for header, compact stats ribbon, and sticky toolbar** + +Style: +- `header`: compact padding (`0.75rem 1.25rem`), border radius, flex-wrap responsive. +- `.stats-container`: compact flex row, high-density cards (`padding: 0.5rem 1rem`). +- `.controls-container`: pinned with `position: relative`, `background: var(--surface)`, `border-radius: 12px`, `padding: 0.75rem 1rem`. + +- [ ] **Step 3: Add styles for scrollable table container and sticky headers** + +Style: +```css +.table-container { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + background-color: var(--surface); + border: 1px solid var(--border); + border-radius: 16px; + position: relative; +} + +#entityTable { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +#entityTable thead th { + position: sticky; + top: 0; + z-index: 5; + background-color: var(--surface-variant); + border-bottom: 2px solid var(--border); + padding: 0.75rem 1rem; +} + +.domain-header-row td, +.room-header-row td { + position: sticky; + top: 41px; + z-index: 4; + background-color: var(--surface-variant); + border-bottom: 1px solid var(--border); +} +``` + +- [ ] **Step 4: Style modern alias chips, toggles, badges, and modals** + +Style: +- `.alias-chip` with hover delete button `✕`. +- `.btn-add-alias` inline button. +- Status badges: `.badge-exposed`, `.badge-unexposed`, `.badge-pending-add`, `.badge-pending-remove`. +- Modal backdrop blur: `backdrop-filter: blur(6px); background: rgba(0, 0, 0, 0.6);`. + +- [ ] **Step 5: Commit** + +```bash +git add custom_components/google_assistant_entity_console/static/style.css +git commit -m "feat(console): overhaul CSS for 100vh flex shell and sticky headers" +``` + +--- + +### Task 4: Enhance Interactive Controls & Rendering in `app.js` + +**Files:** +- Modify: `custom_components/google_assistant_entity_console/static/app.js` + +**Interfaces:** +- Consumes: Entity data from `/google_assistant_entity_console/entities` and state tracking. +- Produces: Dynamic match counts, sticky group rendering, inline alias add/delete, and pending badges. + +- [ ] **Step 1: Add live match counter and search clear handler** + +In `app.js`: +- In `applyFilters()`, calculate filtered entity count vs total entities and update `#filterMatchCount` (e.g. `Showing 42 of 180 entities`). +- Connect `#clearSearchBtn` click event to clear `#searchInput` and re-apply filters. + +- [ ] **Step 2: Update table and group row rendering** + +In `renderTable()`: +- Update `.domain-header-cell` and `.room-header-cell` markup to include entity count pills and clean collapsible chevron styling. +- When rendering entity rows in `renderEntityRow(e)`: + - Add inline `+ Add` alias button that turns into an input on click. + - Add quick copy button for `entity_id`. + - Display pending status badges (`Pending Add` or `Pending Remove`) and update the pending counter in header. + +- [ ] **Step 3: Update pending state tracker and Rebuild button badge** + +Track total pending additions and removals: +- Update `#pendingChangesBadge` visibility and text. +- If pending count > 0, highlight `#rebuildBtn` to indicate unsaved YAML changes. + +- [ ] **Step 4: Verify JavaScript syntax** + +Run: `node -c custom_components/google_assistant_entity_console/static/app.js` +Expected: Exits with code 0 without syntax errors. + +- [ ] **Step 5: Commit** + +```bash +git add custom_components/google_assistant_entity_console/static/app.js +git commit -m "feat(console): enhance interactive table rendering and pending state" +``` + +--- + +### Task 5: Cache Busting Version Bump & End-to-End Verification + +**Files:** +- Modify: `custom_components/google_assistant_entity_console/manifest.json` +- Modify: `custom_components/google_assistant_entity_console/__init__.py` + +**Interfaces:** +- Consumes: HA component registration. +- Produces: Incremented cache-busting version `1.9.0` so browsers load updated assets without stale cache. + +- [ ] **Step 1: Bump version in manifest.json** + +Change `"version": "1.8.0"` to `"version": "1.9.0"`. + +- [ ] **Step 2: Restart / reload custom component or HA container** + +Run docker command or reload integration to ensure Home Assistant serves the new files. + +- [ ] **Step 3: End-to-end verification via Chrome DevTools or curl** + +Verify: +- Panel HTML and assets return HTTP 200 with new version query strings. +- DOM renders full-height without 150px clipping. +- Filter toolbar and sticky headers remain pinned during scroll. + +- [ ] **Step 4: Commit** + +```bash +git add custom_components/google_assistant_entity_console/manifest.json custom_components/google_assistant_entity_console/__init__.py +git commit -m "chore(console): bump version to 1.9.0 for asset cache busting" +``` diff --git a/docs/superpowers/specs/2026-10-13-google-entity-console-redesign.md b/docs/superpowers/specs/2026-10-13-google-entity-console-redesign.md new file mode 100644 index 0000000..3b0feb6 --- /dev/null +++ b/docs/superpowers/specs/2026-10-13-google-entity-console-redesign.md @@ -0,0 +1,153 @@ +# Google Assistant Entity Console — UI Rework & Layout Fix Specification + +- **Date**: 2026-10-13 +- **Component**: `custom_components/google_assistant_entity_console` +- **Target URL Path**: `/google_assistant_entity_console` +- **Status**: Approved Design + +--- + +## 1. Problem Statement & Root Cause + +### 1.1 The Viewport Cut-off Bug +In Home Assistant, the custom panel registered via `async_register_built_in_panel` mounts `` into ``. +- In `panel.js`, the custom element `GoogleAssistantEntityConsolePanel` does not specify host styling (`display: block; width: 100%; height: 100%`). +- By default, custom HTML elements are `display: inline;`. +- The child `iframe` has `height: 100%`. Under standard CSS specifications, a child percentage height inside an element with unresolved/inline height falls back to the browser intrinsic default iframe height of **150px**. +- As a consequence, the console UI renders inside a 150px window, clipping everything immediately below the top header cards. + +### 1.2 UX Limitations in Current Design +- **Rigid Centered Width**: `index.html` wraps the entire app in `max-width: 1200px`, creating giant empty side gutters on widescreen displays and cramped columns on medium viewports. +- **Lost Controls on Scroll**: As users scroll down hundreds of smart home entities, the search input, room filter, and grouping controls scroll out of view, forcing constant scrolling up and down. +- **Column Density & Wrapping**: Nickname chips and status badges cause uneven row heights without clear horizontal boundaries. +- **Hardcoded Theme Fallbacks**: Dark-mode colors with hardcoded dark hex values cause poor contrast or visual seams when Home Assistant runs custom or light themes. + +--- + +## 2. Architectural Design + +``` ++-----------------------------------------------------------------------------------+ +| Host: (display: block; width/height: 100%)| +| Child:
EntityNicknames / AliasesExposed StatusActionsEntityNicknames / AliasesExposed StatusActions
+
Loading Home Assistant registry...