167 lines
4.1 KiB
Svelte
167 lines
4.1 KiB
Svelte
<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>
|