wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import { issues } from "../../stores/issues.svelte.js";
  import AppIcon from "../layout/AppIcon.svelte";
  import { remembered } from "../../utils/remember.svelte.js";

  let {
    repos,
    onclose,
  }: {
    repos: string[];
    onclose: () => void;
  } = $props();

  /// Which repository, kept — issues get filed against the same one for
  /// weeks. The title and body are never kept: a remembered issue title is one
  /// that gets filed again by accident, describing work it does not describe.
  const selectedRepo = remembered("issues.repo", "");
  let title = $state("");
  let body = $state("");
  let submitting = $state(false);
  let error: string | null = $state(null);

  $effect(() => {
    if (selectedRepo.value === "" && repos.length > 0) {
      selectedRepo.value = repos[0]!;
      return;
    }
    if (selectedRepo.value !== "" && !repos.includes(selectedRepo.value)) {
      selectedRepo.value = repos[0] ?? "";
    }
  });

  async function handleSubmit() {
    if (!selectedRepo.value || !title.trim()) return;
    submitting = true;
    error = null;
    try {
      const [owner, repo] = selectedRepo.value.split("/");
      await issues.createIssue(owner!, repo!, {
        title: title.trim(),
        body: body.trim() || undefined,
      });
      onclose();
    } catch (e) {
      error = e instanceof Error ? e.message : String(e);
    } finally {
      submitting = false;
    }
  }
</script>

<div class="modal-overlay" onclick={onclose} onkeydown={(e) => { if (e.key === "Escape") onclose(); }} role="presentation">
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
  <div
    class="modal-panel"
    onclick={(e) => e.stopPropagation()}
    onkeydown={(e) => { if (e.key === "Escape") onclose(); }}
    role="dialog"
    aria-label="Create Issue"
    tabindex="0"
  >
    <div class="modal-header">
      <h2 class="modal-title">New Issue</h2>
      <button class="modal-close" onclick={onclose} aria-label="Close">
        <AppIcon name="x" size={14} />
      </button>
    </div>
    <div class="modal-body">
      {#if error}
        <div class="modal-error">{error}</div>
      {/if}

      <label class="field">
        <span class="field-label">Repository</span>
        <select class="field-select" bind:value={selectedRepo.value}>
          {#each repos as repo}
            <option value={repo}>{repo}</option>
          {/each}
        </select>
      </label>

      <label class="field">
        <span class="field-label">Title</span>
        <input
          class="field-input"
          type="text"
          placeholder="Issue title"
          bind:value={title}
          onkeydown={(e) => { if (e.key === "Enter" && !e.shiftKey) handleSubmit(); }}
        />
      </label>

      <label class="field">
        <span class="field-label">Description (optional)</span>
        <textarea
          class="field-textarea"
          placeholder="Describe the issue (Markdown supported)"
          bind:value={body}
          rows="6"
        ></textarea>
      </label>

      <div class="modal-actions">
        <button class="btn-cancel" onclick={onclose}>Cancel</button>
        <button
          class="btn-submit"
          disabled={!title.trim() || !selectedRepo.value || submitting}
          onclick={handleSubmit}
        >
          {submitting ? "Creating..." : "Create Issue"}
        </button>
      </div>
    </div>
  </div>
</div>

<style>
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-4);
    margin-bottom: var(--wb-spacing-12);
  }

  .field-label {
    font-size: var(--wb-font-size-11);
    font-weight: 600;
    color: var(--vscode-foreground);
  }

  .field-input,
  .field-select,
  .field-textarea {
    padding: var(--wb-spacing-8) var(--wb-spacing-10);
    font-size: var(--wb-font-size-13);
    font-family: var(--wsb-font-sans);
    color: var(--vscode-editor-foreground);
    background: var(--vscode-input-background);
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wsb-radius-sm);
  }

  .field-input:focus,
  .field-select:focus,
  .field-textarea:focus {
    outline: none;
    border-color: var(--vscode-focusBorder);
  }

  .field-textarea {
    resize: vertical;
    min-height: var(--wb-control-size-80);
  }

  .modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--wb-spacing-8);
    margin-top: var(--wb-spacing-16);
  }

  .btn-cancel {
    height: var(--wb-control-size-32);
    padding: 0 var(--wb-spacing-14);
    font-size: var(--wb-font-size-12);
    font-weight: 500;
    color: var(--vscode-foreground);
    background: var(--vscode-input-background);
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wsb-radius-sm);
    cursor: pointer;
  }

  .btn-cancel:hover {
    background: var(--vscode-list-hoverBackground);
  }

  .btn-submit {
    height: var(--wb-control-size-32);
    padding: 0 var(--wb-spacing-14);
    font-size: var(--wb-font-size-12);
    font-weight: 600;
    color: white;
    background: var(--vscode-testing-iconPassed);
    border-radius: var(--wsb-radius-sm);
    cursor: pointer;
  }

  .btn-submit:disabled {
    opacity: 0.5;
    cursor: default;
  }

  .btn-submit:hover:not(:disabled) {
    opacity: 0.9;
  }
</style>