feat(lotus): layout, settings and reactions over the widget API (cinny #43)

The Lotus host drove these by clicking EC's hidden controls in our DOM and
read screenshare/layout state from it, which only works while the frame is
same-origin and breaks whenever a data-testid changes. New actions:

- toWidget io.lotus.set_layout { layout: "grid" | "spotlight" } → the layout
  switch view model's setLayout.
- toWidget io.lotus.open_settings { open?: boolean } → setSettingsOpen
  (toggle when `open` is omitted).
- toWidget io.lotus.toggle_reactions → a window event the reactions button
  listens for.
- fromWidget io.lotus.controls_state { screensharing, layout } on change; its
  arrival tells the host the fork supports the actions above.

Screensharing stays host-driven for now: getDisplayMedia needs the user's
click to reach this frame (Capability Delegation), which a widget message
doesn't carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-24 22:16:24 -04:00
co-authored by Claude Opus 5.5
parent d8880daca5
commit 6beebd3ea7
6 changed files with 180 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
/*
Copyright 2026 Lotus Guild
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, expect, it } from "vitest";
import { parseLayoutPayload, parseSettingsPayload } from "./lotusControls";
describe("parseLayoutPayload", () => {
it("accepts grid and spotlight", () => {
expect(parseLayoutPayload({ layout: "grid" })).toBe("grid");
expect(parseLayoutPayload({ layout: "spotlight" })).toBe("spotlight");
});
it("rejects anything else", () => {
expect(parseLayoutPayload({ layout: "pip" })).toBeUndefined();
expect(parseLayoutPayload({})).toBeUndefined();
expect(parseLayoutPayload(null)).toBeUndefined();
expect(parseLayoutPayload("grid")).toBeUndefined();
});
});
describe("parseSettingsPayload", () => {
it("uses an explicit open flag", () => {
expect(parseSettingsPayload({ open: true }, true)).toBe(true);
expect(parseSettingsPayload({ open: false }, false)).toBe(false);
});
it("toggles when open is missing or not a boolean", () => {
expect(parseSettingsPayload({}, false)).toBe(true);
expect(parseSettingsPayload(undefined, true)).toBe(false);
expect(parseSettingsPayload({ open: "yes" }, true)).toBe(false);
});
});