breakglass UI: foldable control sections for small screens
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Viktor: "make screens foldable so they can be viewed on small screens." The VM-control sheet packed Inspect + 4 power buttons + a long output dump into one scroll on a phone. Made the dense sections collapsible with native <details>/<summary> (zero-JS, accessible):
- Inspect and Power are foldable groups, open by default (nothing important hidden), tap the caret header to collapse the one you are not using.
- Command output (e.g. a long forensics dump) is a foldable block; its <pre> is capped at 46vh with internal scroll so it never runs off the page.

Verified via Playwright at 390x844: tapping Power collapses it to its header; the forensics output folds and scrolls within a bounded box. Works on desktop too (side panel stays expanded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-12 23:21:01 +00:00
parent aa054cac3f
commit 0e45445341
4 changed files with 60 additions and 18 deletions

View file

@ -6,8 +6,8 @@
<meta name="color-scheme" content="dark" /> <meta name="color-scheme" content="dark" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>devvm breakglass</title> <title>devvm breakglass</title>
<script type="module" crossorigin src="./assets/index-DFUUDy82.js"></script> <script type="module" crossorigin src="./assets/index-DjaW81Sq.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B_5XZQm9.css"> <link rel="stylesheet" crossorigin href="./assets/index-DWHIP1Zw.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View file

@ -106,9 +106,9 @@
<button class="retry" onclick={() => location.reload()}>Reload</button> <button class="retry" onclick={() => location.reload()}>Reload</button>
</div> </div>
{:else} {:else}
<!-- read-only actions --> <!-- read-only actions (foldable) -->
<div class="group"> <details class="group" open>
<div class="group-label">Inspect <span class="group-tag">read-only</span></div> <summary class="group-label">Inspect <span class="group-tag">read-only</span></summary>
<div class="btn-row"> <div class="btn-row">
{#each nonMutating as v (v.name)} {#each nonMutating as v (v.name)}
<button <button
@ -122,13 +122,13 @@
</button> </button>
{/each} {/each}
</div> </div>
</div> </details>
<!-- mutating / power actions --> <!-- mutating / power actions (foldable) -->
<div class="group"> <details class="group" open>
<div class="group-label group-label--danger"> <summary class="group-label group-label--danger">
Power <span class="group-tag group-tag--danger">affects the running VM</span> Power <span class="group-tag group-tag--danger">affects the running VM</span>
</div> </summary>
<div class="danger-list"> <div class="danger-list">
{#each mutating as v (v.name)} {#each mutating as v (v.name)}
<div class="danger-item {v.headline ? 'danger-item--headline' : ''}"> <div class="danger-item {v.headline ? 'danger-item--headline' : ''}">
@ -162,9 +162,9 @@
</div> </div>
{/each} {/each}
</div> </div>
</div> </details>
<!-- output --> <!-- output (foldable; a long forensics dump scrolls inside a capped box) -->
{#if actionError} {#if actionError}
<div class="block-error" role="alert"> <div class="block-error" role="alert">
⚠ Command failed to reach the host — {actionError} ⚠ Command failed to reach the host — {actionError}
@ -172,8 +172,8 @@
{/if} {/if}
{#if output} {#if output}
<div class="out {outputFailed ? 'out--fail' : 'out--ok'}"> <details class="out {outputFailed ? 'out--fail' : 'out--ok'}" open>
<div class="out-head"> <summary class="out-head">
<code class="out-verb">{output.verb}</code> <code class="out-verb">{output.verb}</code>
{#if output.rejected} {#if output.rejected}
<span class="out-status out-status--fail">rejected</span> <span class="out-status out-status--fail">rejected</span>
@ -182,7 +182,7 @@
exit {output.exit_code} exit {output.exit_code}
</span> </span>
{/if} {/if}
</div> </summary>
{#if output.stdout} {#if output.stdout}
<pre class="out-pre">{output.stdout}</pre> <pre class="out-pre">{output.stdout}</pre>
{/if} {/if}
@ -193,7 +193,7 @@
{#if !output.stdout && !output.stderr} {#if !output.stdout && !output.stderr}
<pre class="out-pre out-pre--empty">(no output)</pre> <pre class="out-pre out-pre--empty">(no output)</pre>
{/if} {/if}
</div> </details>
{/if} {/if}
{/if} {/if}
</div> </div>
@ -559,4 +559,46 @@
.retry:hover { .retry:hover {
background: rgba(255, 77, 77, 0.12); background: rgba(255, 77, 77, 0.12);
} }
/* ── foldable sections (native <details>) ───────────────────────────────
Each group + the output dump fold away on small screens. Open by default
so nothing important is hidden; tap the header to collapse. */
details.group > summary,
details.out > summary {
list-style: none;
cursor: pointer;
user-select: none;
}
details.group > summary::-webkit-details-marker,
details.out > summary::-webkit-details-marker {
display: none;
}
/* disclosure caret on the left of each foldable header */
details.group > summary::before,
details.out > summary::before {
content: "▾";
display: inline-block;
width: 11px;
margin-right: 4px;
color: var(--ink-faint);
font-size: 9px;
transition: transform 0.15s ease;
}
details.group:not([open]) > summary::before,
details.out:not([open]) > summary::before {
transform: rotate(-90deg);
}
/* roomier tap target for the fold header on touch */
details.group > summary {
padding: 3px 0;
}
/* keep the exit-status pinned to the right now that a caret leads the row */
.out-head .out-status {
margin-left: auto;
}
/* a long dump (e.g. forensics) scrolls inside a capped box, not the page */
.out-pre {
max-height: 46vh;
overflow: auto;
}
</style> </style>