mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
feat(ui): L-shell step 7 — live tab state-glyphs (Tier-1, shape+colour)
Conversational tabs now show a live shape+colour state glyph (● ◐ ⚠ ✗ ○) instead of the static ◆/○ placeholders the header removal (5e.2e) left behind — driven by the SAME Tier-1 source + builder the rail uses, so tab and rail always agree. - rail.js: export the glyph() builder (one source of truth for the mapping). - pane.js: ShellPane.stateful + PaneManager.setTabGlyph/statefulTabs — generic (PaneManager owns no glyph vocabulary; the shell passes the built element). A stateful pane builds no static glyph; the shell paints a live .ui-glyph. - shell.js: stateForWs() reads the Tier-1 snapshot; paintConvTabGlyphs() repaints every stateful tab on each Tier-1 render (subscribed to TS_APP.onRender) + per pane on activate. Coordinator + interactive panes are now stateful. - shell.css: .tab .tab-glyph spacing (static + live); live glyphs keep their own .ui-glyph-* state colour (no .tab .glyph override). SINGLE WRITER: the tab glyph is written only by the Tier-1 path (the pane's Tier-2 stream drives its body, not the tab) — no two-tier race, no stale open-time placeholder on reconnect (BRIEFING L144-147). A coordinator-telemetry-parity gap (open Q#2) would stale tab + rail equally, consistently. Verified: 27/27 mechanism harness (8 new glyph asserts) + real-stack wiring harnesses (console coord ui-glyph-running / int ui-glyph-idle; standalone int ui-glyph-running — matching the stubbed Tier-1 state, errs:[]) + 26 shell JS guards + CSS audit at baseline.
This commit is contained in:
@@ -391,3 +391,33 @@ def test_step7_tab_menu_css_promoted_shared() -> None:
|
||||
assert "color-mix(in oklab, var(--err)" in css, (
|
||||
"the destructive item must use the DS --err token (the translation happened)"
|
||||
)
|
||||
|
||||
|
||||
def test_step7_live_tab_state_glyphs() -> None:
|
||||
"""Step 7 #2: conversational tabs show LIVE state glyphs driven by Tier-1 —
|
||||
the SAME source + builder the rail uses (one writer; the pane's Tier-2 stream
|
||||
drives its body, not the tab glyph), so tab and rail agree and the glyph never
|
||||
sits stale at an open-time placeholder. The coord/int static placeholders are
|
||||
retired for a `stateful` flag."""
|
||||
rail = _RAIL_JS.read_text(encoding="utf-8")
|
||||
assert "export function glyph(" in rail, (
|
||||
"rail must export its state-glyph builder so the tab reuses it (single source)"
|
||||
)
|
||||
pane = _PANE_JS.read_text(encoding="utf-8")
|
||||
assert "setTabGlyph(paneId, el)" in pane and "statefulTabs()" in pane, (
|
||||
"PaneManager must expose the generic glyph setter + the stateful-tab list"
|
||||
)
|
||||
assert "this.stateful" in pane, "ShellPane must carry the stateful flag"
|
||||
shell = _SHELL_JS.read_text(encoding="utf-8")
|
||||
assert 'import { mountRail, mountManage, glyph } from "./rail.js"' in shell, (
|
||||
"the shell must import the rail's glyph builder (one source for tab + rail)"
|
||||
)
|
||||
assert "function stateForWs(" in shell and "function paintConvTabGlyphs(" in shell
|
||||
assert "window.TS_APP.onRender" in shell and "paintConvTabGlyphs(pm)" in shell, (
|
||||
"tab glyphs must repaint on every Tier-1 render (live, not stale)"
|
||||
)
|
||||
assert shell.count("stateful: true") == 2, (
|
||||
"the coordinator + interactive panes are stateful (their static placeholders retired)"
|
||||
)
|
||||
css = _SHELL_CSS.read_text(encoding="utf-8")
|
||||
assert ".tab .tab-glyph" in css, "the tab-glyph spacing rule must apply to static + live glyphs"
|
||||
|
||||
@@ -31,7 +31,8 @@ export class ShellPane {
|
||||
this.rawId = opts.id != null ? opts.id : null; // ws_id for keyed panes, null for singletons
|
||||
this.id = this.rawId == null ? this.type : this.type + ":" + this.rawId;
|
||||
this.title = opts.title || this.type || "";
|
||||
this.glyph = opts.glyph || null; // a single char shown in the tab (e.g. "◇"); state glyphs use ui-base .ui-glyph-*
|
||||
this.glyph = opts.glyph || null; // a single static char shown in the tab (e.g. "◇"); stateful panes use a live .ui-glyph-* instead
|
||||
this.stateful = opts.stateful || false; // conversational panes: tab glyph tracks live Tier-1 state (set via setTabGlyph)
|
||||
this.closable = opts.closable !== false; // dashboard is not closable
|
||||
// DOM — created and owned by the PaneManager on mount:
|
||||
this.el = null; // <section class="pane">
|
||||
@@ -109,6 +110,29 @@ export class PaneManager {
|
||||
this._activeSubs.push(cb);
|
||||
}
|
||||
|
||||
/** Replace a tab's leading state glyph with `el` (a built glyph span). The
|
||||
* shell drives this from Tier-1 so a conversational tab shows live shape+colour
|
||||
* state — generic here (PaneManager owns no glyph vocabulary; the shell passes
|
||||
* the element, reusing the rail's builder so tab and rail render identically). */
|
||||
setTabGlyph(paneId, el) {
|
||||
const pane = this._panes.get(paneId);
|
||||
if (!pane || !pane.tabEl || !el) return;
|
||||
el.classList.add("tab-glyph"); // tab spacing (margin); colour stays the el's own
|
||||
if (pane._glyphEl && pane._glyphEl.parentNode === pane.tabEl)
|
||||
pane._glyphEl.replaceWith(el);
|
||||
else pane.tabEl.insertBefore(el, pane.tabEl.firstChild);
|
||||
pane._glyphEl = el;
|
||||
}
|
||||
|
||||
/** Open panes whose tab shows live Tier-1 state — `{id, rawId}[]` (the shell
|
||||
* repaints these on every Tier-1 render). */
|
||||
statefulTabs() {
|
||||
const out = [];
|
||||
for (const p of this._panes.values())
|
||||
if (p.stateful && p.rawId != null) out.push({ id: p.id, rawId: p.rawId });
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Create the pane if absent, then focus it. Auth/cap gating is the caller's.
|
||||
* `extra` is an optional open-time hint passed straight to the factory (e.g.
|
||||
* the interactive pane's `{nodeId}` from a rail click); it is NOT persisted,
|
||||
@@ -253,11 +277,15 @@ export class PaneManager {
|
||||
tab.setAttribute("role", "tab");
|
||||
tab.setAttribute("aria-controls", "pane-" + cssId(pane.id));
|
||||
if (pane.glyph) {
|
||||
// Static decorative char (Dashboard ◇ / Admin ⚙). Stateful panes get NO
|
||||
// static glyph — the shell paints a live .ui-glyph onto the slot via
|
||||
// setTabGlyph (on activate + every Tier-1 render).
|
||||
const g = document.createElement("span");
|
||||
g.className = "glyph";
|
||||
g.className = "glyph tab-glyph";
|
||||
g.setAttribute("aria-hidden", "true"); // decorative glyph, not read as content
|
||||
g.textContent = pane.glyph;
|
||||
tab.append(g);
|
||||
pane._glyphEl = g;
|
||||
}
|
||||
tab.append(document.createTextNode(pane.title));
|
||||
// Tab-action menu (step 7): a pane that exposes `tabMenu()` gets a caret to
|
||||
|
||||
@@ -27,8 +27,10 @@ const STATE_LABEL = {
|
||||
};
|
||||
|
||||
/** A decorative shape+colour state glyph (the row/pill also carries the state
|
||||
* in text/aria, so the glyph itself is aria-hidden). */
|
||||
function glyph(state) {
|
||||
* in text/aria, so the glyph itself is aria-hidden). Exported so the shell can
|
||||
* paint the SAME glyph onto a conversational tab from the SAME Tier-1 source —
|
||||
* rail row and tab stay consistent, one writer (no Tier-2 tab-glyph race). */
|
||||
export function glyph(state) {
|
||||
const s = GLYPH[state] ? state : "idle";
|
||||
const el = document.createElement("span");
|
||||
el.className = "ui-glyph ui-glyph-" + s;
|
||||
|
||||
@@ -411,11 +411,17 @@
|
||||
background: none;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
.tab .glyph {
|
||||
/* Tab glyph spacing — applies to BOTH the static decorative char (Dashboard /
|
||||
Admin, via .glyph) and the live state glyph (.ui-glyph-* on conversational
|
||||
tabs). The static glyph is dim --ink-4; the live glyph keeps its own
|
||||
.ui-glyph-* state colour, so it is NOT given a colour here. */
|
||||
.tab .tab-glyph {
|
||||
margin-right: 6px;
|
||||
color: var(--ink-4);
|
||||
font-size: 11px;
|
||||
}
|
||||
.tab .glyph {
|
||||
color: var(--ink-4);
|
||||
}
|
||||
.tab:hover {
|
||||
background: var(--panel-2);
|
||||
}
|
||||
@@ -424,9 +430,8 @@
|
||||
border-color: var(--hair-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
/* Active tab's glyph brightens so an open session's `○` placeholder doesn't read
|
||||
permanently "idle" beside its live (e.g. running ●) rail row. Step 7 makes
|
||||
tab glyphs live state; this is the interim hint. */
|
||||
/* Active tab's STATIC glyph (Dashboard / Admin) brightens a touch; live state
|
||||
glyphs on conversational tabs keep their .ui-glyph-* state colour instead. */
|
||||
.tab.active .glyph {
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
========================================================================== */
|
||||
|
||||
import { PaneManager, ShellPane } from "./pane.js";
|
||||
import { mountRail, mountManage } from "./rail.js";
|
||||
import { mountRail, mountManage, glyph } from "./rail.js";
|
||||
// The interactive pane is a real ES module beside us in /shared (step 5a) — the
|
||||
// shell imports it directly, and it exists in every deployment. The coordinator
|
||||
// pane lives at an absolute /static path that only the CONSOLE serves, so it is
|
||||
@@ -155,6 +155,36 @@ function nodeForWs(wsId) {
|
||||
// app.js) and a reduced menu in the console — the capability-derived-affordances
|
||||
// thesis applied to the tab menu. `opts`: titleVerbs (Refresh/Edit/Fork title),
|
||||
// deleteVerb (the destructive Delete), closeSession (stop the workstream itself).
|
||||
// The live state of a workstream from the Tier-1 snapshot (the SAME source the
|
||||
// rail reads) — so a conversational tab's state glyph stays consistent with its
|
||||
// rail row and updates live rather than sitting at an open-time placeholder.
|
||||
function stateForWs(wsId) {
|
||||
try {
|
||||
const cs =
|
||||
window.TS_APP &&
|
||||
window.TS_APP.getClusterState &&
|
||||
window.TS_APP.getClusterState();
|
||||
if (cs && cs.nodes) {
|
||||
for (const nid in cs.nodes) {
|
||||
for (const ws of cs.nodes[nid].workstreams || []) {
|
||||
if (ws.id === wsId) return ws.state || "idle";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/* snapshot not ready */
|
||||
}
|
||||
return "idle";
|
||||
}
|
||||
|
||||
// Repaint every stateful tab's glyph from Tier-1. Subscribed to the render
|
||||
// signal (one Tier-1 writer for the tab glyph; the pane's Tier-2 stream drives
|
||||
// its body, not the tab) and called on activate for the initial paint.
|
||||
function paintConvTabGlyphs(pm) {
|
||||
for (const t of pm.statefulTabs())
|
||||
pm.setTabGlyph(t.id, glyph(stateForWs(t.rawId)));
|
||||
}
|
||||
|
||||
function convTabMenu(pane, pm, wsId, opts) {
|
||||
opts = opts || {};
|
||||
const G = window;
|
||||
@@ -294,7 +324,7 @@ async function mountShell() {
|
||||
const pane = new ShellPane({
|
||||
type: "interactive",
|
||||
title: wsTitle(id),
|
||||
glyph: "○",
|
||||
stateful: true, // tab shows live Tier-1 state (no static placeholder)
|
||||
});
|
||||
pane.tabMenu = () =>
|
||||
convTabMenu(pane, pm, id, {
|
||||
@@ -319,6 +349,7 @@ async function mountShell() {
|
||||
});
|
||||
};
|
||||
pane.onActivate = function () {
|
||||
pm.setTabGlyph(pane.id, glyph(stateForWs(id))); // live Tier-1 state glyph
|
||||
if (!this._ctl) return;
|
||||
this._ctl.connect(); // idempotent — opens the stream once, re-marks focus
|
||||
if (!this._loginArmed && window.TS_LOGIN && this._ctl.onLogin) {
|
||||
@@ -352,7 +383,7 @@ async function mountShell() {
|
||||
const pane = new ShellPane({
|
||||
type: "coordinator",
|
||||
title: wsTitle(id),
|
||||
glyph: "◆",
|
||||
stateful: true, // tab shows live Tier-1 state (no static placeholder)
|
||||
});
|
||||
pane.tabMenu = () =>
|
||||
convTabMenu(pane, pm, id, {
|
||||
@@ -366,6 +397,7 @@ async function mountShell() {
|
||||
});
|
||||
};
|
||||
pane.onActivate = function () {
|
||||
pm.setTabGlyph(pane.id, glyph(stateForWs(id))); // live Tier-1 state glyph
|
||||
if (this._ctl && !this._connected) {
|
||||
this._connected = true;
|
||||
this._ctl.connect();
|
||||
@@ -427,6 +459,14 @@ async function mountShell() {
|
||||
// `pm` lets the rail seed its active marker when a restored Admin pane is open.
|
||||
mountManage(shell.manageSec, pm);
|
||||
|
||||
// Live tab state-glyphs (step 7): repaint conversational tabs' state glyphs on
|
||||
// every Tier-1 render — the SAME source + builder the rail uses, so tab and
|
||||
// rail agree and the glyph never sits stale at an open-time placeholder. One
|
||||
// Tier-1 writer for the tab glyph; the pane's Tier-2 stream drives its body.
|
||||
if (window.TS_APP && typeof window.TS_APP.onRender === "function") {
|
||||
window.TS_APP.onRender(() => paintConvTabGlyphs(pm));
|
||||
}
|
||||
|
||||
// Hand off to the legacy boot (login + Tier-1 stream) now that the shell and
|
||||
// its status DOM exist.
|
||||
if (window.TS_APP && typeof window.TS_APP.boot === "function") {
|
||||
|
||||
Reference in New Issue
Block a user