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 workshop's projects: every project of `$PRJ` with its worktrees, the
  checkouts outside it, and the projects the workshop's notes and links name
  that are not cloned on this machine — each of those one click from being
  cloned.
-->
<script lang="ts">
  import { onMount } from "svelte";
  import { levels, actionKey } from "../../stores/levels.svelte.js";
  import { peers } from "../../stores/peers.svelte.js";
  import { router } from "../../stores/router.svelte.js";
  import { workbench } from "../../stores/workbench.svelte.js";
  import { opensInWorkbench } from "../../utils/open-link.js";
  import WorktreeRow from "./WorktreeRow.svelte";
  import NewWorktreeForm from "./NewWorktreeForm.svelte";

  onMount(() => {
    void levels.ensureLoaded();
    void peers.load();
  });

</script>

<div class="projects-page">
  <header>
    <h1>Projects</h1>
    <p class="subtle">
      {#if levels.prjRoot}
        In <code>{levels.prjRoot}</code>
      {:else if levels.loaded}
        No <code>$PRJ</code> on this machine: only worktrees outside it.
      {:else}
        Reading this machine…
      {/if}
    </p>
  </header>

  {#if levels.error}
    <p class="page-error" role="alert">{levels.error}</p>
  {/if}
  {#if levels.slotsError}
    <p class="page-error" role="alert">prj could not list its session slots: {levels.slotsError}</p>
  {/if}
  {#each levels.problems as problem (problem)}
    <p class="page-error" role="alert">{problem}</p>
  {/each}

  <section aria-label="Projects here">
    {#if levels.loaded && levels.projects.length === 0}
      <p class="empty">No project in <code>$PRJ</code> yet.</p>
    {/if}
    <div class="cards">
      {#each levels.projects as project (project.name)}
        <article class="card" data-project={project.name}>
          <header class="card-head">
            <a
              class="card-title"
              href={router.href({ level: "project", project: project.name })}
              onclick={opensInWorkbench((beside) => workbench.openProject(project.name, { beside }))}
            >
              {project.name}
            </a>
            <span class="subtle">
              {project.github ?? "no GitHub origin"} ·
              {project.worktrees.length} worktree{project.worktrees.length === 1 ? "" : "s"}
            </span>
          </header>
          {#each project.worktrees as worktree (worktree.id)}
            <WorktreeRow {worktree} />
          {/each}
          <div class="card-foot">
            <NewWorktreeForm project={project.name} />
          </div>
        </article>
      {/each}
    </div>
  </section>

  {#if levels.referenced.length > 0}
    <section aria-label="Not cloned here">
      <div class="section-title">Named by links, not cloned here</div>
      <ul class="referenced">
        {#each levels.referenced as named (named.project)}
          {@const outcome = levels.outcomes[actionKey.clone(named.project)]}
          <li>
            <a href="https://github.com/{named.project}" target="_blank" rel="noreferrer">{named.project}</a>
            <span class="subtle">named by {named.by.join(", ")}</span>
            <button
              class="secondary tiny"
              disabled={outcome?.running}
              onclick={() => void levels.clone(named.project)}
            >
              {outcome?.running ? "Cloning…" : "Clone"}
            </button>
            {#if outcome?.error}
              <span class="page-error" role="alert">{outcome.error}</span>
            {/if}
          </li>
        {/each}
      </ul>
    </section>
  {/if}

  {#if levels.standalone.length > 0}
    <section aria-label="Outside projects">
      <div class="section-title">Outside projects</div>
      <article class="card">
        {#each levels.standalone as worktree (worktree.id)}
          <WorktreeRow {worktree} />
        {/each}
      </article>
    </section>
  {/if}
</div>

<style>
  .projects-page {
    max-width: 1200px;
    margin: 0 auto;
    padding: var(--wb-spacing-24) var(--wb-spacing-20);
  }

  h1 {
    font-size: var(--wb-font-size-18);
    font-weight: 600;
    margin: 0;
    color: var(--vscode-editor-foreground);
  }

  code {
    font-family: var(--vscode-editor-font-family, monospace);
  }

  section {
    margin-top: var(--wb-spacing-24);
  }

  .page-error {
    color: var(--wb-error);
    font-size: var(--wb-font-size-12);
  }

  .cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
    gap: var(--wb-spacing-16);
  }

  .card {
    border: 1px solid var(--vscode-panel-border);
    border-radius: var(--wsb-radius-lg);
    background: var(--vscode-sideBar-background);
    overflow: hidden;
  }

  .card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding: var(--wb-spacing-10);
  }

  .card-title {
    font-size: var(--wb-font-size-14);
    font-weight: 600;
    color: var(--vscode-editor-foreground);
    text-decoration: none;
  }

  .card-title:hover {
    text-decoration: underline;
  }

  .card-foot {
    padding: var(--wb-spacing-8) var(--wb-spacing-10);
    border-top: 1px solid var(--vscode-panel-border);
  }

  .referenced {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-6);
  }

  .referenced li {
    display: flex;
    align-items: baseline;
    gap: var(--wb-spacing-8);
  }
</style>