187 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-03 17:54:35 +00:00
1 parent 16c9b4d7f4
commit d6413e19b5
187 files changed
+15542 -2765

No files matched your search

@@ -9,8 +9,22 @@ export const panelStyles = css`
background: var(--primary-background-color);
}
/* The panel lays itself out: a bounded host, the header on top and
.content as THE scroll container (virtualized table, sticky bulk bar
and the docked split-view detail all hang off its scroll position).
HA 2026.8 started wrapping custom panels in a height-less, safe-area
padded block — which turned our 100% into "auto" and moved scrolling to
the document. panel.py opts out of that wrapper (handle_safe_area), so
the insets are ours to apply: same padding HA would have added, inside
the bounded box. The vars are 0 on desktop / older cores. */
.panel {
height: 100%;
box-sizing: border-box;
padding:
var(--safe-area-inset-top, 0px)
var(--safe-area-content-inset-right, var(--safe-area-inset-right, 0px))
var(--safe-area-inset-bottom, 0px)
var(--safe-area-content-inset-left, var(--safe-area-inset-left, 0px));
display: flex;
flex-direction: column;
}
@@ -141,6 +155,19 @@ export const panelStyles = css`
padding-left: 2px;
}
/* Saved-views "save" icon-button sits in the select row: size it to the
select (36px) so its icon centres on the select, not 6px above it.
HA ≥2025.11 sizes ha-icon-button via --ha-icon-button-size (the old
--mdc-icon-button-size is a no-op there); older cores the other way
round, so declare both. */
.views-save-btn {
--ha-icon-button-size: 36px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
margin: 0 -2px 0 -4px;
}
.filter-bar select {
padding: 8px;
border: 1px solid var(--divider-color);
@@ -230,6 +257,42 @@ export const panelStyles = css`
}
.bulk-check { display: flex; align-items: center; justify-content: center; cursor: pointer; }
/* Group-by sections: the OUTER .task-table owns the column tracks (every
breakpoint's template above applies to it unchanged); each section card
and its row block are column subgrids, so the badge/object/name columns
line up across sections — a section whose rows are all "OK" no longer
starts its names 60px left of one that also carries "Overdue". */
.task-table.grouped > .group-section {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
.task-table.grouped .group-section-header { grid-column: 1 / -1; }
.task-table.grouped .group-rows {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
padding: 0 12px;
}
/* Object page task list: it has no object-name cell (the page IS the
object), so under the shared 7-track template every cell sat one track
to the left — the ~190px action pair landed in the 150px due track and
poked past the row's right edge (a horizontal scrollbar as soon as a
classic vertical one took its 17px; wider still with German labels).
Six tracks for six cells. Below 769px the narrow/tight layouts place
the cells explicitly and keep their own four tracks. */
@media (min-width: 769px) {
.task-table.object-tasks {
grid-template-columns: auto minmax(120px, 1fr) minmax(0, 220px) 100px 150px auto;
}
}
@media (min-width: 1200px) {
.task-table.object-tasks {
grid-template-columns: auto fit-content(400px) minmax(0, 1fr) 100px 150px auto;
}
}
/* Master-detail split (>=1500px panel, 2026-09-01): list left, docked task
detail right - the width finally carries content instead of slack. Only
the dashboard tab uses it; below the threshold everything is unchanged. */
@@ -248,12 +311,18 @@ export const panelStyles = css`
.split-list .task-table.bulk { grid-template-columns: auto auto minmax(100px, 180px) minmax(120px, 1fr) 150px auto; }
.split-list .cell.type { display: none; }
.split-list .task-sub { display: none; }
/* Grouped list: the section cards carry a 12px vertical margin that does
not collapse inside the grid, so the first card sat 12px below the
pane's top edge and the two cards read as misaligned. Level them. */
.split-list .task-table.grouped > .group-section:first-child { margin-top: 0; }
/* The pane never scrolls on its own: it is as tall as the detail and
docks scroll-aware (helpers/sticky-pane sets top/margin-top inline) —
top edge while scrolling up, bottom edge while scrolling down, so a
long detail is read by scrolling the list, not a nested scrollbar. */
.split-pane {
min-width: 0;
position: sticky;
top: 8px;
max-height: calc(100vh - 140px);
overflow-y: auto;
border: 1px solid var(--divider-color);
border-radius: 12px;
padding: 4px 16px 16px;
@@ -412,6 +481,11 @@ export const panelStyles = css`
}
.cell { font-size: 14px; }
/* Task name + object name travel as one group: in the wide grids the
wrapper dissolves (display: contents) so both stay direct grid items in
their own tracks; the narrow/tight layouts turn it into the first row
(name left, object right — #150). */
.row-head { display: contents; }
.cell.object-name { color: var(--primary-color); cursor: pointer; }
.cell.task-name { font-weight: 500; }
.cell.type { color: var(--secondary-text-color); }
@@ -474,7 +548,7 @@ export const panelStyles = css`
/* DESIGN PROTOTYPE (#145 wish 1): labelled, colour-coded action buttons
instead of bare icons. Complete = filled success pill, Skip = outlined
neutral pill; both keep the icon as a leading glyph. */
warning (orange) pill; both keep the icon as a leading glyph. */
.row-actions.as-buttons { gap: 4px; }
.row-actions.as-buttons ha-button {
--ha-button-font-size: 13px;
@@ -893,15 +967,24 @@ export const panelStyles = css`
/* Stale-bundle handshake banner (roadmap guard 2) */
.update-banner {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
gap: 6px 10px;
padding: 8px 16px;
background: color-mix(in srgb, var(--primary-color) 14%, var(--card-background-color, #fff));
border-bottom: 1px solid var(--divider-color);
font-size: 14px;
}
/* The text claims the row; on a phone the buttons wrap under it instead of
squeezing it into a one-word-per-line column (#150 round, 2026-09-02). */
.update-banner span {
flex: 1;
flex: 1 1 220px;
}
.update-banner ha-button {
margin-left: auto;
}
.update-banner ha-button + ha-button {
margin-left: 0;
}
.popup-menu {
@@ -1518,24 +1601,54 @@ export const panelStyles = css`
X-positions across rows regardless of content (sparkline, bar, %).
Earlier flex-wrap-based layouts let the row wrap unpredictably so
"X days" sometimes sat near the middle, sometimes at the right edge.
Grid template:
[badges auto | task-name 1fr | due-cell 100px | actions auto]
Task-name spans the full top row (own row above), chips span the
full bottom row. */
Two rows since #150 (was three: name / badge+object+due+actions /
chips — a phone list scrolled forever):
row 1 task name ................................ object name
row 2 [status icon | chips | due 100px | actions]
The status pill drops its label (icon + colour stay, title/aria keep
the word), the object name moves up beside the task name, and the
chips take the freed middle track. */
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
grid-template-rows: auto auto auto;
row-gap: 4px;
grid-template-rows: auto auto;
row-gap: 6px;
padding: 12px;
}
:host([narrow]) .cell.type { display: none; }
:host([narrow]) .row-head {
display: flex;
grid-column: 1 / -1;
grid-row: 1;
align-items: baseline;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
/* The object page's rows have no object name — there the task name is a
direct grid item and the grid placement applies; inside .row-head the
flex rule wins and the grid lines are inert. */
:host([narrow]) .cell.task-name {
grid-column: 1 / -1;
grid-row: 1;
flex: 1 1 auto;
min-width: 0;
}
:host([narrow]) .cell.object-name {
order: 1;
flex: 0 1 auto;
max-width: 50%;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: right;
}
/* Bulk selection on a phone: the checkbox takes the badge column of the
first row, the name group starts one track further right. */
:host([narrow]) .task-table.bulk .bulk-check { grid-column: 1; grid-row: 1; align-self: start; }
:host([narrow]) .task-table.bulk .row-head { grid-column: 2 / -1; }
:host([narrow]) .cell-badges {
grid-column: 1;
grid-row: 2;
@@ -1547,19 +1660,11 @@ export const panelStyles = css`
align-items: flex-start;
gap: 4px;
}
:host([narrow]) .cell.object-name {
grid-column: 2;
grid-row: 2;
min-width: 0;
/* Cap long object names at 2 lines with ellipsis instead of growing
unbounded vertically. The full name is still readable via the panel
object-detail view (one tap on the object). */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.2;
}
/* Icon-only status pill (#150): a round 22 px disc; the colour + shape
still carry the state, the label lives in title/aria-label. */
:host([narrow]) .task-row .status-label { display: none; }
:host([narrow]) .task-row .status-badge { min-width: 0; padding: 3px; border-radius: 50%; }
:host([narrow]) .task-row .status-badge ha-icon { --mdc-icon-size: 16px; margin-left: 0; }
:host([narrow]) .due-cell {
grid-column: 3;
grid-row: 2;
@@ -1583,15 +1688,15 @@ export const panelStyles = css`
}
:host([narrow]) .row-actions.as-buttons ha-button,
:host([tight]) .row-actions.as-buttons ha-button { --ha-button-font-size: 12px; }
/* buttons_compact: the labelled buttons collapse to an icon-only ha-button
plus ha-icon-button side by side — the colour carries the meaning, the
row keeps the icon-era height. */
/* buttons_compact: the labelled buttons collapse to two icon-only
ha-buttons side by side (solid green Complete, outlined orange Skip) —
the colour carries the meaning, the row keeps the icon-era height.
--wa-form-control-padding-inline is the wa-button inset HA's ha-button
forwards (16 px default -> a 58 px pill around a 20 px glyph). */
:host([narrow]) .row-actions.as-buttons.compact,
:host([tight]) .row-actions.as-buttons.compact { flex-direction: row; align-items: center; gap: 2px; }
:host([tight]) .row-actions.as-buttons.compact { flex-direction: row; align-items: center; gap: 4px; }
:host([narrow]) .row-actions.compact ha-button,
:host([tight]) .row-actions.compact ha-button { --ha-button-height: 36px; min-width: 0; }
:host([narrow]) .row-actions.compact ha-icon-button,
:host([tight]) .row-actions.compact ha-icon-button { --mdc-icon-button-size: 40px; --mdc-icon-size: 22px; color: var(--secondary-text-color); }
:host([tight]) .row-actions.compact ha-button { --ha-button-height: 36px; --wa-form-control-padding-inline: 10px; min-width: 0; }
:host([narrow]) .row-actions.compact ha-icon,
:host([tight]) .row-actions.compact ha-icon { --mdc-icon-size: 20px; }
@@ -1609,26 +1714,35 @@ export const panelStyles = css`
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
grid-template-rows: auto auto auto;
row-gap: 4px;
grid-template-rows: auto auto;
row-gap: 6px;
padding: 12px;
min-width: 0;
}
:host([tight]:not([narrow])) .cell.type { display: none; }
:host([tight]:not([narrow])) .cell.task-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; }
:host([tight]:not([narrow])) .row-head { display: flex; grid-column: 1 / -1; grid-row: 1; align-items: baseline; justify-content: space-between; gap: 10px; min-width: 0; }
:host([tight]:not([narrow])) .cell.task-name { grid-column: 1 / -1; grid-row: 1; flex: 1 1 auto; min-width: 0; }
:host([tight]:not([narrow])) .cell.object-name { order: 1; flex: 0 1 auto; max-width: 50%; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
:host([tight]:not([narrow])) .task-table.bulk .bulk-check { grid-column: 1; grid-row: 1; align-self: start; }
:host([tight]:not([narrow])) .task-table.bulk .row-head { grid-column: 2 / -1; }
:host([tight]:not([narrow])) .cell-badges { grid-column: 1; grid-row: 2; flex-direction: column; align-items: flex-start; gap: 4px; }
:host([tight]:not([narrow])) .cell.object-name { grid-column: 2; grid-row: 2; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.2; }
:host([tight]:not([narrow])) .task-row .status-label { display: none; }
:host([tight]:not([narrow])) .task-row .status-badge { min-width: 0; padding: 3px; border-radius: 50%; }
:host([tight]:not([narrow])) .task-row .status-badge ha-icon { --mdc-icon-size: 16px; margin-left: 0; }
:host([tight]:not([narrow])) .due-cell { grid-column: 3; grid-row: 2; align-items: flex-end; min-width: 0; }
:host([tight]:not([narrow])) .row-actions { grid-column: 4; grid-row: 2; }
:host([tight]:not([narrow])) .task-sub { grid-column: 1 / -1; grid-row: 3; font-size: 11px; gap: 6px; justify-content: flex-start; flex-wrap: wrap; }
:host([tight]:not([narrow])) .task-sub { grid-column: 2; grid-row: 2; align-self: center; font-size: 11px; gap: 4px; justify-content: flex-start; flex-wrap: wrap; }
:host([tight]:not([narrow])) .task-sub-empty { display: none; }
:host([tight]:not([narrow])) .mini-sparkline { display: none; }
:host([tight]:not([narrow])) .trend-arrow { display: inline; }
/* Chips sit in the middle track of row 2 — the slot the object name
vacated — vertically centred on the 36 px action buttons. */
:host([narrow]) .task-sub {
grid-column: 1 / -1;
grid-row: 3;
grid-column: 2;
grid-row: 2;
align-self: center;
font-size: 11px;
gap: 6px;
gap: 4px;
justify-content: flex-start;
flex-wrap: wrap;
}
@@ -1647,7 +1761,6 @@ export const panelStyles = css`
column-gap: 6px;
}
:host([narrow]) .task-row { padding: 12px 8px; }
:host([narrow]) .row-actions.compact ha-icon-button { --mdc-icon-button-size: 32px; --mdc-icon-size: 20px; }
}
:host([narrow]) .detail-header {
@@ -1781,17 +1894,23 @@ export const panelStyles = css`
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
grid-template-rows: auto auto auto;
row-gap: 4px;
grid-template-rows: auto auto;
row-gap: 6px;
padding: 12px;
}
.cell.type { display: none; }
.cell.task-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; }
.row-head { display: flex; grid-column: 1 / -1; grid-row: 1; align-items: baseline; justify-content: space-between; gap: 10px; min-width: 0; }
.cell.task-name { grid-column: 1 / -1; grid-row: 1; flex: 1 1 auto; min-width: 0; }
.cell.object-name { order: 1; flex: 0 1 auto; max-width: 50%; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
.task-table.bulk .bulk-check { grid-column: 1; grid-row: 1; align-self: start; }
.task-table.bulk .row-head { grid-column: 2 / -1; }
.cell-badges { grid-column: 1; grid-row: 2; flex-direction: column; align-items: flex-start; gap: 4px; }
.cell.object-name { grid-column: 2; grid-row: 2; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.2; }
.task-row .status-label { display: none; }
.task-row .status-badge { min-width: 0; padding: 3px; border-radius: 50%; }
.task-row .status-badge ha-icon { --mdc-icon-size: 16px; margin-left: 0; }
.due-cell { grid-column: 3; grid-row: 2; align-items: flex-end; min-width: 0; }
.row-actions { grid-column: 4; grid-row: 2; }
.task-sub { grid-column: 1 / -1; grid-row: 3; font-size: 11px; gap: 6px; justify-content: flex-start; flex-wrap: wrap; }
.task-sub { grid-column: 2; grid-row: 2; align-self: center; font-size: 11px; gap: 4px; justify-content: flex-start; flex-wrap: wrap; }
.task-sub-empty { display: none; }
.mini-sparkline { display: none; }
.trend-arrow { display: inline; }