mirror of
https://github.com/mvschwarz/openrig-studio.git
synced 2026-11-07 05:56:21 -10:00
The rail runs the full height and the header starts beside it
Founder-directed, prototyped and measured by studio-impl, routed here rather than landed because app/shell.html is this repo's. The header no longer spans the full width; the rail goes to the top of the page; the collapse control moves onto the rail. #app becomes a row with the rail as its direct child, and a new #maincol holds the header above #shellframes beside it. The launcher is now one continuous edge from the top of the window, and the header belongs to the surface it sits above rather than reading as a second band across everything. The toggle moves for a reason beyond layout: among MARKUP and AGENTS it read as a studio-level control when it is only ever about the launcher. A control belongs to the thing it acts on. ONE CHANGE TO WHAT WAS HANDED OVER, which studio-impl offered: their patch added a second #rail-head block beside the original rather than restructuring, and the same would have applied to body.rail-collapsed #rail-head. Two rules for one selector is two places computing one property — the shape this repo has already paid for three times — so they are merged. The `font-size: 0` that used to hide the head text goes with it: a .headtext wrapper hides explicitly, and an inherited-zero trick sitting beside a display:none that already does the job is one of the two that drifts. MEASURED IN A LIVE SHELL, both states: expanded : rail 212x720 (full viewport height), #topbar.left = 212 collapsed : body.rail-collapsed, rail 52x720, #topbar.left = 52, glyphs intact Every header control hit-tested at its own centre and reachable — box placement and "is this the element a click reaches" are different claims, and an overlap in this same file was found that way before. AND THE THING THE RESTRUCTURE COULD MOST PLAUSIBLY HAVE BROKEN, checked rather than assumed: an agent mark anchored to an element in the surface landed with a gap of 0x0 after #stage moved 212px right. The overlay mounts on #stage, not on #app or #shellframes, so the outer restructure does not reach it — studio-impl said so and it is now measured and pinned. 309 tests. The committed checks are structural and say so.
This commit is contained in:
1 parent
a7970dc4e0
commit
85d8424d0e
2 files changed
+130
-11
No files matched your search
+32
-11
@@ -14,7 +14,13 @@
|
||||
* { box-sizing: border-box; }
|
||||
html { color-scheme: dark; }
|
||||
html, body { height: 100%; margin: 0; background: var(--ink); font-family: var(--sans); }
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
/* THE RAIL IS FULL HEIGHT AND THE HEADER STARTS BESIDE IT. The header used to
|
||||
span the whole width with the rail beneath it, which read as two separate
|
||||
bands and put the launcher's collapse control in the header rather than on
|
||||
the thing it collapses. Now the launcher is one continuous edge from the top
|
||||
of the window, and the header belongs to the surface it sits above. */
|
||||
#app { display: flex; flex-direction: row; height: 100vh; }
|
||||
#maincol { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
|
||||
/* THE TWO PERSISTENT FRAMES. Every surface gets the same launcher on the left
|
||||
and the same header on top, so the studio reads as one application rather
|
||||
than a set of pages that each invented their own chrome. A surface renders
|
||||
@@ -37,7 +43,13 @@
|
||||
font-weight: 600; padding: 7px 12px; cursor: pointer; }
|
||||
#topbar button:hover { color: var(--bone); border-color: var(--bone); }
|
||||
#topbar button.on { background: var(--bone); border-color: var(--bone); color: #111; }
|
||||
#railToggle { width: 34px; padding: 7px 0 !important; text-align: center; flex: none; }
|
||||
/* Lives on the rail now, not in the header: a control belongs to the thing it
|
||||
acts on, and in the header it read as a studio-level control beside MARKUP
|
||||
and AGENTS when it is only ever about the launcher. */
|
||||
#railToggle { flex: none; width: 28px; height: 24px; padding: 0; text-align: center;
|
||||
background: none; border: 1px solid var(--edge2); border-radius: 2px; color: var(--faint);
|
||||
cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
|
||||
#railToggle:hover { color: var(--bone); border-color: var(--bone); }
|
||||
#rail { width: 212px; min-width: 212px; background: var(--panel); border-right: 1px solid var(--edge);
|
||||
display: flex; flex-direction: column; }
|
||||
/* COLLAPSE TO A TOOLSTRIP, not to nothing. Small screens and wide work want the
|
||||
@@ -49,11 +61,16 @@
|
||||
body.rail-collapsed #manifest-health,
|
||||
body.rail-collapsed .tab .name,
|
||||
body.rail-collapsed .tab .dot { display: none; }
|
||||
body.rail-collapsed #rail-head { padding: 20px 0 10px; text-align: center; font-size: 0; }
|
||||
/* Collapsed, the toggle is the only thing left in the head, so it centres
|
||||
rather than sitting against a title that is no longer rendered. */
|
||||
body.rail-collapsed #rail-head { justify-content: center; padding: 16px 0 10px; }
|
||||
body.rail-collapsed #rail-head .headtext { display: none; }
|
||||
body.rail-collapsed #tabs { padding: 6px 4px; }
|
||||
body.rail-collapsed .tab { justify-content: center; padding: 9px 0; }
|
||||
body.rail-collapsed .tab .glyph { width: auto; }
|
||||
#rail-head { padding: 20px 16px 10px; color: var(--bone); font-weight: 700; letter-spacing: 4px; font-size: 13px; }
|
||||
#rail-head { display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 20px 16px 10px; color: var(--bone); font-weight: 700; letter-spacing: 4px; font-size: 13px; }
|
||||
#rail-head .headtext { flex: 1; min-width: 0; display: block; }
|
||||
#rail-head small { display: block; color: var(--faint); font-weight: 400; letter-spacing: .14em;
|
||||
font-size: 9.5px; margin-top: 5px; text-transform: uppercase; }
|
||||
#tabs { flex: 1; overflow-y: auto; padding: 6px 8px; }
|
||||
@@ -141,8 +158,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<nav id="rail">
|
||||
<div id="rail-head">
|
||||
<button id="railToggle" title="collapse the launcher to a toolstrip">☰</button>
|
||||
<span class="headtext">STUDIO<small>openrig studio · contract v0.1</small></span>
|
||||
</div>
|
||||
<div id="tabs"></div>
|
||||
<div id="manifest-health"></div>
|
||||
<div id="rail-foot">surfaces are rooms.<br/>this is the hallway.</div>
|
||||
</nav>
|
||||
<div id="maincol">
|
||||
<header id="topbar">
|
||||
<button id="railToggle" title="collapse the launcher to a toolstrip">☰</button>
|
||||
<div id="appHalf">
|
||||
<span class="title" id="topTitle">STUDIO</span>
|
||||
<span class="crumb" id="topCrumb"></span>
|
||||
@@ -154,12 +180,6 @@
|
||||
</div>
|
||||
</header>
|
||||
<div id="shellframes">
|
||||
<nav id="rail">
|
||||
<div id="rail-head">STUDIO<small>openrig studio · contract v0.1</small></div>
|
||||
<div id="tabs"></div>
|
||||
<div id="manifest-health"></div>
|
||||
<div id="rail-foot">surfaces are rooms.<br/>this is the hallway.</div>
|
||||
</nav>
|
||||
<main id="stage"><div id="empty">no surfaces in the manifest yet</div>
|
||||
</main>
|
||||
<aside id="agents">
|
||||
@@ -173,6 +193,7 @@
|
||||
<iframe allow="clipboard-read; clipboard-write" id="chatframe" title="agent seat"></iframe>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// The rail is full height and the header starts beside it.
|
||||
//
|
||||
// node --test 'test/*.test.mjs'
|
||||
//
|
||||
// Founder-directed: the header must not run the full width, the rail goes to the
|
||||
// TOP of the page, and the collapse control belongs on the rail rather than in the
|
||||
// header — a control belongs to the thing it acts on, and among MARKUP and AGENTS
|
||||
// it read as a studio-level control when it is only ever about the launcher.
|
||||
//
|
||||
// STRUCTURAL, and it says so: this repo has no browser dependency. The layout was
|
||||
// measured in a live shell at both states —
|
||||
// expanded : rail 212x720 (full viewport height), #topbar.left = 212
|
||||
// collapsed : body.rail-collapsed, rail 52x720, #topbar.left = 52, glyphs intact
|
||||
// — and an agent mark anchored to an element in the surface landed with a gap of
|
||||
// 0x0 after #stage moved 212px right, which is the thing the restructure could
|
||||
// most plausibly have broken.
|
||||
//
|
||||
// What is committed here is the SHAPE that produces that: reverting the nesting,
|
||||
// or moving the toggle back into the header, fails without needing a browser.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const shell = () => fs.readFileSync(path.join(REPO, "app", "shell.html"), "utf8");
|
||||
|
||||
test("#app is a ROW and the rail is its direct child", () => {
|
||||
const s = shell();
|
||||
assert.match(s, /#app \{[^}]*flex-direction: row/,
|
||||
"#app is not a row — the rail cannot be full height beside the header");
|
||||
assert.match(s, /<div id="app">\s*\n\s*<nav id="rail">/,
|
||||
"the rail is not the first child of #app, so it does not start at the top of the page");
|
||||
assert.match(s, /#maincol \{[^}]*flex-direction: column/,
|
||||
"#maincol is missing or is not a column");
|
||||
});
|
||||
|
||||
test("the header lives inside #maincol, not beside the rail's parent", () => {
|
||||
const s = shell();
|
||||
const main = s.match(/<div id="maincol">([\s\S]*?)<div id="shellframes">/);
|
||||
assert.ok(main, "#maincol does not open before #shellframes");
|
||||
assert.match(main[1], /<header id="topbar">/,
|
||||
"#topbar is not inside #maincol — it would span the full width again");
|
||||
});
|
||||
|
||||
test("the collapse toggle is ON THE RAIL", () => {
|
||||
const s = shell();
|
||||
const head = s.match(/<div id="rail-head">([\s\S]*?)<\/div>/);
|
||||
assert.ok(head, "#rail-head is no longer a block element with children");
|
||||
assert.match(head[1], /id="railToggle"/,
|
||||
"the collapse control is not in the rail head — a control belongs to the thing it acts on");
|
||||
const topbar = s.match(/<header id="topbar">([\s\S]*?)<\/header>/);
|
||||
assert.ok(topbar, "cannot find #topbar");
|
||||
assert.doesNotMatch(topbar[1], /id="railToggle"/,
|
||||
"the collapse control is back in the header");
|
||||
});
|
||||
|
||||
test("the rail's text is wrapped so collapsing hides it explicitly", () => {
|
||||
const s = shell();
|
||||
assert.match(s, /<span class="headtext">/,
|
||||
"the rail head text is not wrapped, so collapsing has to hide it by inherited font-size: 0");
|
||||
assert.match(s, /body\.rail-collapsed #rail-head \.headtext \{[^}]*display: none/,
|
||||
"collapsing does not hide the head text");
|
||||
});
|
||||
|
||||
test("ONE rule per selector — the merge studio-impl offered and I took", () => {
|
||||
// Their patch added a second #rail-head block beside the original and offered to
|
||||
// merge. Two rules for one selector is two places computing one property, which
|
||||
// this repo has already paid for three times; they are merged.
|
||||
const s = shell();
|
||||
const count = (sel) =>
|
||||
(s.match(new RegExp("^\\s*" + sel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\{", "gm")) || []).length;
|
||||
assert.equal(count("#rail-head"), 1, "#rail-head is declared more than once");
|
||||
assert.equal(count("body.rail-collapsed #rail-head"), 1, "the collapsed rail head is declared more than once");
|
||||
assert.equal(count("#railToggle"), 1, "#railToggle is declared more than once");
|
||||
});
|
||||
|
||||
test("the overlay still mounts on #stage, so annotations are unaffected", () => {
|
||||
// The restructure moves #app, #maincol and the rail. It must NOT reach the
|
||||
// overlay, which a shell feature attaches to — verified live (mark landed 0x0
|
||||
// off its element after #stage moved 212px), pinned structurally here.
|
||||
const s = shell();
|
||||
assert.match(s, /document\.getElementById\("stage"\)\.appendChild\(overlayHost\)/,
|
||||
"the overlay no longer mounts on #stage — the outer restructure has reached a shell feature");
|
||||
});
|
||||
|
||||
test("positive control: these checks can fail", () => {
|
||||
const s = shell();
|
||||
const reverted = s.replace(/#app \{[^}]*flex-direction: row/, "#app { display: flex; flex-direction: column");
|
||||
assert.doesNotMatch(reverted, /#app \{[^}]*flex-direction: row/,
|
||||
"the row could not be reverted, so the first assertion proves nothing");
|
||||
const moved = s.replace(/<div id="rail-head">[\s\S]*?<\/div>/, '<div id="rail-head">STUDIO</div>');
|
||||
const head = moved.match(/<div id="rail-head">([\s\S]*?)<\/div>/);
|
||||
assert.doesNotMatch(head[1], /railToggle/,
|
||||
"the toggle could not be removed from the head, so that assertion is untested");
|
||||
});
|
||||
Reference in new issue
Block a user