Port the full-page and navigation tiles into the Vue editor (for 0.2.74)

Sizes single, wide and full in web/src/model/layout.ts (a full tile covers the six cells of a page and starts
it; packing, fitting, arranging and page counting know it), the full card on the mockup, Size with Full page
and its firmware hint in the tile drawer, controls defaulting to none on a full tile, navigation tiles
(screen.page_1..8: Goes to page, size normal or double, icon and colour only, "Page n ›" on the card), the
tile limit of 48 from firmware 0.2.62, the "Screen" domain label. Vitest and the Python source tests cover it.
The add-on side of this round lives on the peer branch fullpage (4be6153).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 08:39:28 +02:00
parent 1ed42dc425
commit 00c9a4e4f3
17 changed files with 300 additions and 101 deletions
+33 -1
View File
@@ -1,7 +1,7 @@
// The store: selecting a screen, editing its layout, what's new, progress, copy and import.
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
addTile, canAlert, copyLayoutFrom, importLayout, layoutJson, liveOf, PHASES, removeTile, select, setTileOption, state, tileLimit,
addTile, canAlert, copyLayoutFrom, importLayout, layoutJson, liveOf, PHASES, removeTile, retargetPageTile, select, setTileOption, state, tileLimit,
topbarItems, topbarView, updateProgress, whatsNew,
} from "../src/store";
import type { Inventory, Screen } from "../src/types";
@@ -156,3 +156,35 @@ describe("updates with content", () => {
expect(canAlert(undefined)).toBe(false);
});
});
describe("full-page and navigation tiles", () => {
it("moves the other tiles of the page behind a tile that grows to the whole page", () => {
select("living");
const [lamp, sensor] = state.layout!.tiles;
setTileOption(lamp, "size", "full");
expect(lamp.slot).toBe(0);
expect(sensor.slot).toBe(6);
expect(state.layout!.pages).toBe(2);
expect(state.toast).toBeNull();
});
it("takes the first empty page when the others cannot move, and gives up with a toast when none is free", () => {
select("living");
state.layout!.tiles = Array.from({ length: 48 }, (_, i) => ({ entity: `light.l${i}`, name: "", slot: i }));
const first = state.layout!.tiles[0];
setTileOption(first, "size", "full");
expect(first.options?.size).toBe("single");
expect(state.toast?.message).toMatch(/No page is free/);
});
it("lets a navigation tile point at another page, once per page", () => {
select("living");
addTile("screen.page_2");
const nav = state.layout!.tiles.find((t) => t.entity === "screen.page_2")!;
expect(nav.options).toBeUndefined();
expect(retargetPageTile(nav, 3)).toBe(true);
expect(nav.entity).toBe("screen.page_3");
addTile("screen.page_4");
expect(retargetPageTile(nav, 4)).toBe(false);
expect(state.toast?.message).toMatch(/already has a tile that goes to page 4/);
expect(retargetPageTile(nav, 9)).toBe(false);
});
});