wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!--
  The bar at the top of a sidebar panel: what this panel is, and what can be
  done to it.

  One component because it is one convention. The explorer had it; the other
  panels each had their own — four with no title at all, five with a hand-drawn
  `↺`, one with its refresh in a floating strip halfway down — so the answer to
  "where is refresh" depended on which panel you were looking at.

  Actions go at the right end, in order of how often they are wanted, with
  refresh last: it is the one every panel has, so it is the one that should be
  in the same place in every panel.
-->
<script lang="ts">
  import type { Snippet } from "svelte";
  import AppIcon from "../layout/AppIcon.svelte";

  interface Props {
    /** What the panel is called. Shown as the workbench shows every panel. */
    title: string;
    /**
     * Reload whatever this panel reads. Omitted only by a panel that reads
     * nothing — a panel whose data can go stale and offers no way to re-read it
     * is one people reload the whole page for.
     */
    onrefresh?: () => void | Promise<void>;
    /** What refresh means here, in the words the panel would use. */
    refreshTitle?: string;
    /** Whether that reload is in flight, so the control can say so. */
    refreshing?: boolean;
    /** Anything this panel offers beyond refresh, placed before it. */
    actions?: Snippet;
  }

  let {
    title,
    onrefresh,
    refreshTitle = "Refresh",
    refreshing = false,
    actions,
  }: Props = $props();

  // A panel may expose a store-owned busy flag because the same work can be
  // started elsewhere. The header also owns the click it starts: several
  // panels used to discard their promise, so their refresh glyph never moved
  // even though a request was in flight. One interaction boundary means one
  // busy rule for every panel.
  let requested = $state(false);
  let refreshError = $state<string | null>(null);
  const busy = $derived(refreshing || requested);

  async function refresh(): Promise<void> {
    if (!onrefresh || busy) return;
    requested = true;
    refreshError = null;
    try {
      await onrefresh();
    } catch (failure) {
      // Stores normally render their domain error in the panel. A callback
      // that rejects before it can do that is still made visible here rather
      // than becoming an unhandled promise rejection with a stopped spinner.
      refreshError = failure instanceof Error ? failure.message : String(failure);
    } finally {
      requested = false;
    }
  }
</script>

<div class="panel-header">
  <span class="panel-title">{title}</span>
  <div class="panel-actions">
    {#if actions}{@render actions()}{/if}
    {#if onrefresh}
      <button
        class="panel-action-btn"
        class:spinning={busy}
        title={busy ? `${refreshTitle} — waiting for the response` : refreshTitle}
        aria-label={busy ? `${refreshTitle} — waiting for the response` : refreshTitle}
        aria-busy={busy}
        disabled={busy}
        onclick={() => void refresh()}
      >
        <AppIcon name="refresh" size={14} />
      </button>
    {/if}
  </div>
</div>
{#if refreshError}
  <div class="panel-refresh-error" role="alert">Refresh failed: {refreshError}</div>
{/if}