new console

This commit is contained in:
Kelorastus
2026-09-30 13:36:14 +02:00
parent feed0fa869
commit 3863809ae8
11 changed files with 513 additions and 14 deletions
+166
View File
@@ -0,0 +1,166 @@
<script lang="ts">
import Icon from "./Icon.svelte";
import { launcher } from "../lib/launcher.svelte";
let scroller: HTMLElement | undefined = $state();
const lines = $derived(launcher.consoleLines);
const running = $derived(launcher.consoleRunning);
const exit = $derived(launcher.consoleExit);
// Follow the output, but only while the reader is already at the bottom: scrolling back to
// read an error must not be yanked away by the next line that arrives.
let follow = $state(true);
function onScroll() {
const el = scroller;
if (!el) return;
follow = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
}
$effect(() => {
void lines.length;
if (follow && scroller) scroller.scrollTop = scroller.scrollHeight;
});
// A step header is worth picking out of a wall of build output, as is a failure.
function kind(line: string): string {
if (/^\s*={3}/.test(line)) return "step";
if (/^FAILED:|^\s*Erreur|error[: ]/i.test(line)) return "bad";
if (/nothing to (release|deploy)|No pack cut|Release finished/i.test(line)) return "note";
return "";
}
</script>
<section class="console glass" aria-label="Release output">
<header class="head">
<h2 class="eyebrow">Release</h2>
<span class="state mono" class:live={running} class:bad={!running && exit !== null && exit !== 0}>
{#if running}
running
{:else if exit === null}
idle
{:else if exit === 0}
finished
{:else}
failed ({exit})
{/if}
</span>
<span class="grow"></span>
<span class="total mono">{lines.length}</span>
<button class="icon-btn" aria-label="Close release output" onclick={() => launcher.closeConsole()}>
<Icon name="close" size={14} />
</button>
</header>
<!-- Both are deliberate: `log` is the role for streamed output, and it has to be
focusable or a keyboard cannot scroll back through a failed build. -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div bind:this={scroller} class="body mono selectable" onscroll={onScroll} tabindex="0" role="log">
{#if lines.length}
{#each lines as line, i (i)}
<div class="line {kind(line)}">{line || " "}</div>
{/each}
{:else}
<p class="empty">Waiting for the first line…</p>
{/if}
</div>
{#if !running && exit !== null}
<footer class="foot">
{exit === 0 ? "Closing this keeps the output until the next release." : "The full transcript is in state\\release\\release.log."}
</footer>
{/if}
</section>
<style>
.console {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
background: var(--glass-strong);
}
.head {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 8px 8px 16px;
border-bottom: 1px solid var(--line);
}
.grow {
flex: 1;
}
.total,
.state {
color: var(--muted);
font-size: var(--text-xs);
}
/* The dot earns its place: from across the desk it is the only thing that says whether a
release is still going. */
.state.live::before,
.state.bad::before {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 6px;
border-radius: 50%;
background: var(--accent, #4ade80);
content: "";
vertical-align: middle;
}
.state.live {
color: var(--ink-2);
}
.state.bad {
color: var(--danger);
}
.state.bad::before {
background: var(--danger);
}
.body {
flex: 1;
padding: 8px 0;
overflow-y: auto;
min-height: 0;
font-size: var(--text-xs);
line-height: 1.55;
}
.line {
padding: 0 16px;
color: var(--ink-2);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.line.step {
margin-top: 6px;
color: var(--ink);
font-weight: 600;
}
.line.bad {
color: var(--danger);
}
.line.note {
color: var(--warn);
}
.empty {
padding: 8px 16px;
color: var(--muted);
font-size: var(--text-sm);
}
.foot {
padding: 8px 16px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: var(--text-xs);
}
</style>