Draw a tile on the editor's mockup the way the screen does: the CYD shows the whole tile again (0.2.76)

Both boards draw a normal tile with the icon on the left and the name and value beside it;
the Vue editor stacked icon, name and value in one column. On a CYD mockup a tile is 47 px
high and that stack needed 58 px (63 for a watch card), so the value line was cut in half and
a big value hid the name. A single tile is now a row (.head: icon + .tx), a watch card puts
the name row on top and the number under it, and the small slider runs underneath; on a CYD
a tile with a slider uses the screen's 4 px padding and 8 px slider. The Guition mockup
follows the screen the same way.

App 0.2.76, firmware stays 0.2.63. Test results in docs/TEST_RESULTS_0276.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 09:56:01 +02:00
parent 261fa82c7d
commit bdb1f12c55
10 changed files with 94 additions and 19 deletions
+43
View File
@@ -0,0 +1,43 @@
# Test results app 0.2.76 / firmware 0.2.63 (2026-09-18)
Max sent a screenshot of a CYD page in the editor ("het gaat over CYD in de EDITOR die niet 100% goed eruit
ziet"): on every single tile the second line (*7,2 W*, *26,0 °C*, *Cleaning*, *Off*) was cut in half at the bottom
edge. See CHANGELOG 0.2.76. Built on 0.2.75 (`261fa82`).
## What happened before
- Both boards draw a normal tile the same way (`runtime_tiles.h`, `TILE_TITLE_X`/`TILE_VALUE_X` in both profiles):
the icon in its circle on the left, the name and the value beside it, vertically centred. A small slider runs
under that row; a watch card has the icon and the name on top and the big value below.
- The Vue editor (`TileCard.vue`, since 0.2.73) stacked the icon, the name and the value in one column. On a CYD
mockup (292 × 219 px, the screen at 0.91) a tile is 47 px high; the stack needed 58 px, a watch card 63 px, and
`.tile { overflow: hidden }` cut off the rest. The Guition's 73 px tiles had room, so it only showed on the CYD.
- The wide tiles already had the screen's row layout and were fine.
## What changed
- `TileCard.vue`: a single tile draws `.head` (icon + `.tx` with name and value) as a row, like the screen. A watch
card has `.head.top` with the big value under it; the small slider sits under the row.
- `app.css`: `.tile .head` is the row, `.tile .tx` (was only for wide tiles) is the name/value column. On a CYD a
tile with a small slider uses the screen's own numbers: 4 px padding above and below, an 8 px slider.
- `web/tests/components.spec.ts`: a test for the three shapes (plain, watch, small slider).
## Checks
- Demo home (`.esphome/readme-render/demo_home.py`) with the editor on Vite, kitchen screen (CYD): every tile
measured with `scrollHeight` against `clientHeight` in the browser.
| Tile | 0.2.75 needs / has | 0.2.76 needs / has |
| --- | --- | --- |
| Pasta · Active | 58 / 47 px | 47 / 47 px |
| Coffee machine · On | 58 / 47 px | 47 / 47 px |
| Kitchen lamp · On | 58 / 47 px | 47 / 47 px |
| Power · 1249 W (watch) | 63 / 47 px | 47 / 47 px |
| Sam · Home | 58 / 47 px | 47 / 47 px |
| Kitchen lamp with a small slider (added in the DOM) | not measured | 47 / 47 px |
- Living room screen (Guition): all 8 tiles fit; the single tiles now show the icon left as on the screen's render
(`docs/images/guition-home.png`).
- `npm test` 47/47 (one new), `npm run check` clean, `npm run build`; Python `unittest discover -s tests` 409 OK.
- No firmware change, so no ESPHome build and no C++ tests; `FIRMWARE_VERSION` stays 0.2.63.
- Still for Max: open a CYD in the editor after updating the app and look at the tiles.
+7
View File
@@ -1,3 +1,10 @@
## 0.2.76 (firmware 0.2.63)
The CYD in the editor shows the whole tile again.
- **A tile on the mockup looks like the tile on the screen.** Both boards draw a normal tile with the icon on the left and the name and value beside it; the editor stacked the icon, the name and the value on top of each other. On a CYD's short tiles that did not fit, so the value under the name was cut in half (*26.0 °C*, *Cleaning*, *Off*) and a large value hid the name. Now the mockup uses the screen's layout on both boards: icon left, name and value next to it, the small slider underneath, and for a large value the name at the top with the number below it.
- Only the editor changed. There is no new firmware: 0.2.63 stays current. Includes everything from 0.2.75.
## 0.2.75 (firmware 0.2.63)
Smooth icons on the CYD: the power ring is a ring again.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>ESP Screens</title>
<script type="module" crossorigin src="./assets/index-FSwnYVTm.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C6IelTvW.css">
<script type="module" crossorigin src="./assets/index-Cj3m_z3Z.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BL5pkID8.css">
</head>
<body>
<div id="app"></div>
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.75"
version: "0.2.76"
slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen
+11 -7
View File
@@ -154,14 +154,18 @@ async function onKey(e: KeyboardEvent) {
</span>
</template>
<template v-else>
<span class="ic mdi" :class="{ lit: isOn }">{{ glyph(tileIconCp(tile)) }}</span>
<span class="lead">
<span v-if="display === 'watch'" class="big">{{ bigValue }}<small v-if="unit && !gone">{{ unit }}</small></span>
<span class="nm">{{ name }}</span>
<span v-if="goesTo" class="goto">Page {{ goesTo }} ›</span>
<span v-else-if="display !== 'watch' && status" class="st" :class="{ off: gone }">{{ status }}</span>
<span v-if="tile.options?.inline === 'slider'" class="mini-slider" :style="sliderStyle"></span>
<!-- As the screen draws it: the icon on the left, the name and the value beside it. A watch
card puts the name on top and the big value under it; the small slider runs underneath. -->
<span class="head" :class="{ top: display === 'watch' }">
<span class="ic mdi" :class="{ lit: isOn }">{{ glyph(tileIconCp(tile)) }}</span>
<span class="tx">
<span class="nm">{{ name }}</span>
<span v-if="goesTo" class="goto">Page {{ goesTo }} ›</span>
<span v-else-if="display !== 'watch' && status" class="st" :class="{ off: gone }">{{ status }}</span>
</span>
</span>
<span v-if="display === 'watch'" class="big">{{ bigValue }}<small v-if="unit && !gone">{{ unit }}</small></span>
<span v-if="tile.options?.inline === 'slider'" class="mini-slider" :style="sliderStyle"></span>
</template>
<button v-if="live" type="button" class="remove" title="Remove tile" :aria-label="`Remove ${name}`" @click.stop="removeTile(tile)">✕</button>
</div>
+7 -1
View File
@@ -149,12 +149,18 @@ a:hover { text-decoration: underline; }
.tile .ic { font-size: 18px; line-height: 1; color: #1b1b1b; flex: none; }
.tile .lead { display: grid; gap: 2px; min-width: 0; flex: 1; align-content: end; }
.tile.wide .lead { display: flex; align-items: center; gap: 8px; align-content: initial; }
.tile.wide .lead .tx { display: grid; min-width: 0; gap: 1px; }
.tile .tx { display: grid; min-width: 0; gap: 1px; }
.tile .head { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; }
.tile .head.top { flex: none; }
.tile .nm { font-size: 11.5px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile .st { font-size: 10.5px; color: #5a5f66; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.device.cyd .tile { padding: 6px 7px; }
.device.cyd .tile .nm { font-size: 10.5px; }
.device.cyd .tile .ic { font-size: 15px; }
.device.cyd .tile .head { gap: 6px; }
/* The screen's own numbers on its short tiles: 4 px above and below, an 8 px slider. */
.device.cyd .tile:has(> .mini-slider) { padding-block: 4px; gap: 3px; }
.device.cyd .tile .mini-slider { height: 8px; }
.tile .mini-slider { height: 10px; border-radius: 5px; background: linear-gradient(to right, #ffbf38 60%, #fff1d3 60%); flex: none; width: 100%; }
.tile.wide .mini-slider { width: 70px; }
.tile .ctl { flex: none; display: flex; align-items: center; gap: 4px; color: #1b1b1b; }
+15
View File
@@ -65,6 +65,21 @@ describe("TileCard", () => {
expect(gone.find(".st").text()).toBe("Unavailable");
expect(gone.find(".st").classes()).toContain("off");
});
it("draws a single tile as the screen does: the icon left, the name and value beside it", () => {
state.liveStates["sensor.t"] = { state: "1249", word: null, a: { unit_of_measurement: "W" } };
const plain = placed({ entity: "sensor.t", name: "Power", slot: 0 });
expect(plain.find(".head > .ic").exists()).toBe(true);
expect(plain.find(".head > .tx > .nm").text()).toBe("Power");
expect(plain.find(".head > .tx > .st").text()).toBe("1249 W");
// A watch card keeps the name next to the icon and puts the big value underneath.
const watch = placed({ entity: "sensor.t", name: "Power", slot: 1, options: { display: "watch" } });
expect(watch.find(".head").classes()).toContain("top");
expect(watch.find(".head .big").exists()).toBe(false);
expect(watch.find(".head + .big").text()).toBe("1249W");
state.liveStates["light.a"] = { state: "on", word: "On", a: { brightness: 255 } };
const lamp = placed({ entity: "light.a", name: "", slot: 2, options: { inline: "slider" } });
expect(lamp.find(".head + .mini-slider").exists()).toBe(true);
});
it("shows the display name when Home Assistant has no value, and nothing for a scene", () => {
const graph = placed({ entity: "sensor.x", name: "Unknown sensor", slot: 0, options: { display: "graph" } });
expect(graph.find(".st").text()).toBe("graph");