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 { inSessionSearch } from "../../stores/inSessionSearch.svelte.js";
  import { tick } from "svelte";
  import AppIcon from "../layout/AppIcon.svelte";

  let inputRef: HTMLInputElement | undefined = $state(undefined);

  const isMac =
    typeof navigator !== "undefined" &&
    navigator.platform.toUpperCase().includes("MAC");

  $effect(() => {
    if (inSessionSearch.isOpen) {
      tick().then(() => inputRef?.focus());
    }
  });

  function handleKeydown(e: KeyboardEvent) {
    if (e.key === "Escape") {
      e.stopPropagation();
      inSessionSearch.close();
    } else if (e.key === "Enter") {
      e.preventDefault();
      if (e.shiftKey) {
        inSessionSearch.prev();
      } else {
        inSessionSearch.next();
      }
    }
  }

  let hasQuery = $derived(inSessionSearch.query.trim().length > 0);
  let hasMatches = $derived(inSessionSearch.matches.length > 0);
  let noResults = $derived(hasQuery && !hasMatches && !inSessionSearch.loading);

  let counterText = $derived.by(() => {
    if (!hasQuery) return "";
    if (inSessionSearch.loading) return "…";
    if (inSessionSearch.matches.length === 0) return "No results";
    return `${inSessionSearch.currentMatchIndex + 1} of ${inSessionSearch.matches.length}`;
  });
</script>

{#if inSessionSearch.isOpen}
  <div class="find-bar" role="search" aria-label="Find in session">
    <AppIcon class="find-icon" name="search" size={13} />

    <input
      bind:this={inputRef}
      class="find-input"
      class:no-results={noResults}
      type="text"
      placeholder="Find in session…"
      spellcheck="false"
      autocomplete="off"
      value={inSessionSearch.query}
      oninput={(e) =>
        (inSessionSearch.query = (e.currentTarget as HTMLInputElement).value)}
      onkeydown={handleKeydown}
      aria-label="Search query"
    />

    {#if hasQuery}
      <span class="counter" class:no-results={noResults} aria-live="polite">
        {counterText}
      </span>
    {/if}

    <div class="nav-buttons">
      <button
        class="nav-btn"
        title="Previous match (Shift+Enter)"
        disabled={!hasMatches}
        onclick={() => inSessionSearch.prev()}
        tabindex="0"
        aria-label="Previous match"
      >
        <AppIcon name="chevron-up" size={11} />
      </button>
      <button
        class="nav-btn"
        title="Next match (Enter)"
        disabled={!hasMatches}
        onclick={() => inSessionSearch.next()}
        tabindex="0"
        aria-label="Next match"
      >
        <AppIcon name="chevron-down" size={11} />
      </button>
    </div>

    <div class="divider"></div>

    <button
      class="close-btn"
      title="Close (Esc)"
      onclick={() => inSessionSearch.close()}
      tabindex="0"
      aria-label="Close find bar"
    >
      <AppIcon name="x" size={12} />
    </button>
  </div>
{/if}

<style>
  .find-bar {
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-6);
    padding: var(--wb-spacing-6) var(--wb-spacing-16);
    border-bottom: 1px solid var(--vscode-widget-border);
    background: var(--vscode-sideBar-background);
    flex-shrink: 0;
    animation: slide-down 0.12s ease-out;
  }

  @keyframes slide-down {
    from {
      opacity: 0;
      transform: translateY(-8px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  :global(.find-icon) {
    color: var(--vscode-disabledForeground);
    flex-shrink: 0;
  }

  .find-input {
    flex: 1;
    min-width: 0;
    height: var(--wb-control-size-26);
    padding: 0 var(--wb-spacing-8);
    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-panel-border);
    border-radius: var(--wsb-radius-sm);
    outline: none;
    transition:
      border-color 0.15s,
      background 0.15s;
  }

  .find-input:focus {
    border-color: var(--vscode-focusBorder);
    background: var(--vscode-sideBar-background);
  }

  .find-input.no-results:focus {
    border-color: var(--wb-accent-rose);
  }

  .counter {
    font-size: var(--wb-font-size-11);
    color: var(--vscode-disabledForeground);
    white-space: nowrap;
    flex-shrink: 0;
    min-width: var(--wb-control-size-72);
    text-align: right;
  }

  .counter.no-results {
    color: var(--wb-accent-rose);
  }

  .nav-buttons {
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-2);
    flex-shrink: 0;
  }

  .nav-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--wb-control-size-24);
    height: var(--wb-control-size-24);
    border-radius: var(--wsb-radius-sm);
    color: var(--vscode-foreground);
    transition:
      background 0.12s,
      color 0.12s;
  }

  .nav-btn:not(:disabled):hover {
    background: var(--vscode-list-hoverBackground);
    color: var(--vscode-editor-foreground);
  }

  .nav-btn:not(:disabled):active {
    transform: scale(0.9);
  }

  .divider {
    width: 1px;
    height: var(--wb-control-size-16);
    background: var(--vscode-widget-border);
    flex-shrink: 0;
    margin: 0 var(--wb-spacing-2);
  }

  .close-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--wb-control-size-24);
    height: var(--wb-control-size-24);
    border-radius: var(--wsb-radius-sm);
    color: var(--vscode-disabledForeground);
    flex-shrink: 0;
    transition:
      background 0.12s,
      color 0.12s;
  }

  .close-btn:hover {
    background: var(--vscode-list-hoverBackground);
    color: var(--vscode-editor-foreground);
  }

  .close-btn:active {
    transform: scale(0.9);
  }
</style>