diff --git a/app/src/sim/ui/framelog.ts b/app/src/sim/ui/framelog.ts index d5cf2c2..93ac632 100644 --- a/app/src/sim/ui/framelog.ts +++ b/app/src/sim/ui/framelog.ts @@ -1,11 +1,12 @@ /// The frame inspector under the canvas. It merges the frame logs of every live -/// connection into one time-ordered list — a row per frame: connection (when -/// there is more than one), direction, kind, function, round-trip time (on the -/// reply, from its own request by id), byte length — expanding to the decoded -/// envelope, the framing name, and the raw bytes as hex or text. A gap in -/// *virtual* time over `IDLE_MS` inserts an `idle` separator. Every row kind and -/// every connection is filterable from the header. A refused connection gets its -/// own row. The pane's height is drag-resizable by its top grip. +/// connection into one time-ordered list under a labelled column header — a row +/// per frame: connection (when there is more than one), direction, kind, +/// function, round-trip time (on the reply, from its own request by id), byte +/// length — expanding to the decoded envelope, the framing name, and the raw +/// bytes as hex or text. A gap in *virtual* time over `IDLE_MS` inserts an +/// `idle` separator. Every row kind and every connection is filterable from the +/// header. A refused connection gets its own row. The pane's height is +/// drag-resizable by its top grip. /// /// The engine is the `comline-simulator` wasm now: frames are polled from the /// `Sim`, not pushed from a `Tap`. The view calls `poll()` after it advances the @@ -168,9 +169,27 @@ export function frameLog(): FrameLog { clear.textContent = "clear"; head.append(title, kindFilters, connFilters, clockBar, clear); + // column header — same grid as each row's `` (see the CSS) + const cols = document.createElement("div"); + cols.className = "sim-frames-cols"; + for (const [cls, label] of [ + ["frame-conn", "conn"], + ["frame-seq", "#"], + ["frame-dir", "direction"], + ["frame-kind", "kind"], + ["frame-fn", "fn"], + ["frame-delta", "rtt"], + ["frame-len", "bytes"], + ] as const) { + const s = document.createElement("span"); + s.className = cls; + s.textContent = label; + cols.append(s); + } + const list = document.createElement("div"); list.className = "sim-frames-list"; - el.append(grip, head, list); + el.append(grip, head, cols, list); let sources: LogSource[] = []; let api: FrameSource = { frames: () => [], detail: () => null }; @@ -321,6 +340,7 @@ export function frameLog(): FrameLog { sources = next; api = next_api; multi = next.length > 1; + cols.classList.toggle("has-conn", multi); labelByConn.clear(); for (const s of next) labelByConn.set(s.connId, s.label); renderConnFilters(); diff --git a/app/src/style.css b/app/src/style.css index 6ad9f7a..555fe4e 100644 --- a/app/src/style.css +++ b/app/src/style.css @@ -977,13 +977,45 @@ label.clock-btn { stroke: var(--err); stroke-dasharray: 3 3; } +/* one grid template shared by the column header and every row's summary */ +.sim-frames { + --frame-grid: 3ch minmax(6rem, auto) 4.5rem minmax(3rem, 1fr) 4.5rem 3.5rem; + --frame-grid-conn: 7rem 3ch minmax(6rem, auto) 4.5rem minmax(3rem, 1fr) 4.5rem 3.5rem; +} + +.sim-frames-cols { + display: grid; + grid-template-columns: var(--frame-grid); + gap: 0.5rem; + align-items: baseline; + padding: 0.3rem 0.4rem; + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted); + background: var(--bg-2); + border-bottom: 1px solid var(--border); +} +.sim-frames-cols.has-conn { + grid-template-columns: var(--frame-grid-conn); +} +.sim-frames-cols:not(.has-conn) .frame-conn { + display: none; +} +.sim-frames-cols > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .frame-row { border-bottom: 1px solid var(--border); font-size: 0.72rem; } details.frame-row > summary { display: grid; - grid-template-columns: 3ch minmax(6rem, 1fr) 5.5rem 5rem 5rem 4rem; + grid-template-columns: var(--frame-grid); gap: 0.5rem; align-items: baseline; padding: 0.2rem 0.4rem; @@ -991,7 +1023,7 @@ details.frame-row > summary { list-style: none; } details.frame-row.has-conn > summary { - grid-template-columns: 7rem 3ch minmax(6rem, 1fr) 5.5rem 5rem 5rem 4rem; + grid-template-columns: var(--frame-grid-conn); } .frame-conn { color: var(--accent); @@ -1022,6 +1054,9 @@ details.frame-row > summary > span { .frame-row .frame-delta, .frame-row .frame-len { color: var(--muted); +} +.frame-delta, +.frame-len { text-align: right; } .frame-handshake .frame-kind,