Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,78 @@ function applyZoom(z) {
// only ever truthy on a project that declares `stacks[]` (initPickers seeds
// `view.stack` from `info.stacks`; a project with none leaves it null), so a
// single-stack project's strip is unaffected.
// The zoom picker, back in the header as a control you can see and click.
//
// #73 moved the pickers into ⌘K and left the current value on the strip, on the
// principle "hide controls, never state". That reads well until someone opens
// behold for the first time: the strip says "zoom: components", it looks like a
// dropdown because it is a value next to other values, and clicking it does
// nothing — renderStatusbar() only ever sets textContent. The discoverable path
// to the single most useful control is a keyboard shortcut hinted by a "⌘K"
// glyph at the far end of the header.
//
// So zoom gets a real <select> and the palette keeps its entry. The other axes
// stay where #73 put them: zoom is the one you reach for constantly, and one
// visible control is not the header of selects that issue was about.
function renderZoomPicker() {
// #zoom-slot, top-left after the brand — not #pickers on the far right,
// where it sat beside the theme select and read as chrome rather than as the
// control. Falls back to #pickers so a static export built from older markup
// still gets a picker rather than none.
const slot = document.getElementById("zoom-slot") || document.getElementById("pickers");
if (!slot) return;
let sel = document.getElementById("zoom-picker");
if (!sel) {
sel = document.createElement("select");
sel.id = "zoom-picker";
sel.title = "Zoom — components, logical, composites, resources, attributes, runtime (also ⌘K)";
sel.addEventListener("change", () => {
applyZoom(sel.value);
renderStatusbar();
load();
});
// Before the theme picker, which mounts into the same slot at load.
slot.insertBefore(sel, slot.firstChild);
}
const current = zoomValue();
// Rebuilt each render because runtime is only meaningful with an env: it
// descends below the declaration boundary to owner-referenced children,
// which exist in a cluster and never in your source.
const opts = ZOOM_OPTS.filter(([, v]) => v !== "runtime" || view.env);
const want = opts.map(([label, v]) => `${v}:${label}`).join("|") + "@" + current;
if (sel.dataset.built !== want) {
sel.innerHTML = "";
for (const [label, v] of opts) {
const o = document.createElement("option");
o.value = v;
o.textContent = label;
if (v === current) o.selected = true;
sel.appendChild(o);
}
sel.dataset.built = want;
}
sel.value = current;
}

function renderStatusbar() {
renderZoomPicker();
const el = document.getElementById("statusbar");
if (!el) return;
const zoomLabel = (ZOOM_OPTS.find(([, v]) => v === zoomValue()) || ["zoom: " + zoomValue()])[0];
const parts = [zoomLabel, view.env ? `env: ${view.env}` : "env: (source)"];
// The strip looks clickable whether or not it is, so make it act like it:
// clicking opens the palette rather than doing nothing. env, stack and tier
// live only there, and this is the only affordance pointing at them.
if (!el.dataset.clickable) {
el.dataset.clickable = "1";
el.style.cursor = "pointer";
el.title = "env · stack · tier — click, or ⌘K, to change";
el.addEventListener("click", () => openPalette());
}
// No zoom here any more. #73 put the current zoom on the strip because the
// control had moved into the palette; with a real picker two slots along,
// the same value in both places reads as two pickers, one of which does not
// work — which is exactly how it was reported. The strip keeps the axes that
// still have no on-screen control.
const parts = [view.env ? `env: ${view.env}` : "env: (source)"];
if (view.stack) parts.push(`stack: ${view.stack}`);
if (axes.tier) parts.push(`tier: ${axes.tier}`);
if (view.radial && !view.components && !view.logical) parts.push("radial");
Expand Down
11 changes: 9 additions & 2 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@
#actions button:hover, #inspect button:hover { border-color: var(--pending); }
#actions button.approve { border-color: var(--managed); color: var(--managed); }
#inspect button { border-color: var(--foreign); color: var(--foreign); }
#pickers select { background: var(--panel); color: var(--fg); border: 1px solid var(--line);
#pickers select, #zoom-slot select { background: var(--panel); color: var(--fg); border: 1px solid var(--line);
border-radius: 6px; padding: 4px 8px; font-size: 12px; cursor: pointer; }
#pickers select:hover { border-color: var(--pending); }
#pickers select:hover, #zoom-slot select:hover { border-color: var(--pending); }
/* The observe → reconcile → apply dial (M2, #54): where the selected
target sits on the lifecycle progression. Its own row below the
header — a target-level control, not a per-node inspect detail. */
Expand Down Expand Up @@ -177,6 +177,13 @@
<div id="app">
<header>
<h1>behold</h1>
<!-- Zoom sits here, first after the brand, rather than in #pickers on
the far right with the theme select. It is the control you reach
for constantly — the one thing worth the chrome #73 otherwise
thinned — and in the right-hand cluster it read as chrome and got
missed. The ⌘K palette keeps its entry; this is the same action,
discoverable without knowing the shortcut exists. -->
<span id="zoom-slot" style="display:flex;align-self:center"></span>
<span class="meta" id="meta">loading…</span>
<span id="legend" style="display:none;gap:12px;font-size:11px;align-self:center">
<span style="color:var(--managed)">● managed</span>
Expand Down
Loading