web/index.html
Ref: Size: 5.7 KiB History
<!doctype html>
<!-- `mux web`: the wall of devices (M-web Task 8). Served from memory by
the hub; everything dynamic lives in mux.js; the replica itself is
mux_core.wasm — the same ghostty-vt the daemon runs. -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>mux</title>
<style>
:root { --bg: #0d0f12; --chrome: #1a1d23; --fg: #c8ccd4; --dim: #6b7280; }
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font: 13px/1.4 ui-monospace, monospace;
}
#wall {
display: grid; gap: 10px; padding: 10px;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
}
.tile {
background: var(--chrome); border: 1px solid #262a33; border-radius: 4px;
overflow: hidden; cursor: pointer; display: flex; flex-direction: column;
}
.tile header {
display: flex; justify-content: space-between; align-items: center;
padding: 4px 8px; font-size: 12px; color: var(--dim);
border-bottom: 1px solid #262a33; user-select: none;
}
.tile header .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.copy-request {
display: none; margin: 0 8px; padding: 0 6px; border: 1px solid #4a5261;
border-radius: 3px; background: transparent; color: var(--fg);
font: inherit; font-size: 11px; cursor: pointer;
}
.copy-request.on { display: inline-block; }
.copy-request.error { border-color: #8b3d3d; background: #3d1a1a; color: #e07a7a; }
.badge { padding: 0 6px; border-radius: 3px; font-size: 11px; }
.badge.connecting, .badge.reconnecting { background: #4a3b12; color: #e8c35a; }
.badge.up { background: #16351f; color: #6fce8a; }
/* .stuck: the tile gave up replaying and is sending nothing further. */
.badge.gone, .badge.exited, .badge.refused, .badge.stuck { background: #3d1a1a; color: #e07a7a; }
.badge.scroll { background: #1a2c3d; color: #6ab0e0; }
/* mux.js sets width/height in CSS pixels and sizes the backing store to
that times devicePixelRatio, so glyphs rasterize at device resolution
instead of being a downscaled bitmap. No width:100% here: it would
stretch the backing store back out and undo exactly that. */
/* The canvas background is TERMINAL ground, not chrome: it is what
shows before the first frame lands and along the sub-pixel edge the
backing-store rounding leaves. mux.js publishes the theme's
background here, so a light theme does not flash black. */
.tile canvas { display: block; background: var(--term-bg, #000); }
/* The zoomed tile: same element, promoted. Keys go here.
INSET, not inset:0 — the ring of shade left visible around it is the
only way back out (clicking it unzooms), and a tile covering the
viewport made unzoom unreachable. Escape is deliberately NOT bound:
it belongs to the application, and vim needs it. */
.tile.zoomed {
position: fixed; inset: 3vh 4vw; z-index: 20; cursor: default;
}
.tile.zoomed canvas { margin: 0 auto; cursor: text; touch-action: none; }
/* Below the zoomed tile, above the wall: the click target for unzoom. */
#shade {
display: none; position: fixed; inset: 0; z-index: 10; background: #000a;
}
#shade.on { display: block; }
/* The settings tile: the wall's one standing control, and the only
element on it the hub did not put there. A tile-shaped panel, not a
toolbar — the wall stays a wall. Nothing here authors the wall: the
wall is the hosts file. It costs nothing to make the panel
unreachable while zoomed, too — the shade covers the wall — so an
open panel can never be holding the keyboard the terminal owns. */
.tile.settings-tile {
min-height: 80px; align-items: center; justify-content: center;
border-style: dashed;
}
.tile.settings-tile .settings { width: 90%; font-size: 11px; }
.tile.settings-tile .settings summary { color: var(--dim); cursor: pointer; list-style: none; }
.tile.settings-tile .settings summary::-webkit-details-marker { display: none; }
.tile.settings-tile .settings summary:hover { color: var(--fg); }
.tile.settings-tile .settings .row {
display: flex; align-items: center; gap: 6px; margin-top: 6px;
}
.tile.settings-tile .settings .row > span { color: var(--dim); }
.tile.settings-tile .settings input[type="text"],
.tile.settings-tile .settings input[type="number"] {
flex: 1; min-width: 0; padding: 2px 4px; border: 1px solid #4a5261;
border-radius: 3px; background: transparent; color: var(--fg);
font: inherit; font-size: 11px; text-align: left;
}
.tile.settings-tile .settings input[type="number"] { flex: 0 0 4.5em; }
.tile.settings-tile .settings button {
padding: 2px 6px; border: 1px solid #4a5261; border-radius: 3px;
background: transparent; color: var(--fg); font: inherit;
font-size: 11px; cursor: pointer;
}
.tile.settings-tile .settings .note, .tile.settings-tile .settings .caveat { color: var(--dim); font-size: 11px; }
.tile.settings-tile .settings .note.bad { color: #e07a7a; }
.tile header .spawn {
padding: 0 6px; border: 0; background: transparent; color: var(--dim);
font: inherit; cursor: pointer;
}
/* `+` = another session on this tile's host: an add, so it greens. */
.tile header .spawn:hover { color: #6fce8a; }
/* Zoomed = the terminal owns the header; wall management waits. */
.tile.zoomed header .spawn { display: none; }
/* IME target: focusable, invisible, never display:none (that kills IME). */
#ime {
position: fixed; left: -9999px; top: 0; width: 1px; height: 1px;
opacity: 0; border: 0; padding: 0;
}
</style>
</head>
<body>
<div id="shade"></div>
<div id="wall"></div>
<input id="ime" autocomplete="off" autocapitalize="off" spellcheck="false">
<script src="/mux.js"></script>
</body>
</html>