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

  const CELL_SIZE = 16;
  const CELL_GAP = 2;
  const CELL_STEP = CELL_SIZE + CELL_GAP;
  const LABEL_WIDTH = 36;
  const HEADER_HEIGHT = 16;
  const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""];

  const LEVEL_COLORS_LIGHT = [
    "var(--vscode-input-background)",
    "#9be9a8",
    "#40c463",
    "#30a14e",
    "#216e39",
  ];

  const LEVEL_COLORS_DARK = [
    "var(--vscode-input-background)",
    "#0e4429",
    "#006d32",
    "#26a641",
    "#39d353",
  ];

  interface DayCell {
    date: string;
    value: number;
    level: number;
    dayOfWeek: number;
  }

  function levelColor(level: number): string {
    const isDark = document.documentElement.classList.contains(
      "dark",
    );
    const colors = isDark ? LEVEL_COLORS_DARK : LEVEL_COLORS_LIGHT;
    return colors[level] ?? colors[0]!;
  }

  let tooltip = $state<{
    x: number;
    y: number;
    text: string;
  } | null>(null);

  function handleCellHover(
    e: MouseEvent,
    cell: DayCell,
  ) {
    const rect = (
      e.currentTarget as HTMLElement
    ).getBoundingClientRect();
    const d = new Date(cell.date + "T00:00:00");
    const label = d.toLocaleDateString("en", {
      month: "short",
      day: "numeric",
      year: "numeric",
    });
    tooltip = {
      x: rect.left + rect.width / 2,
      y: rect.top - 4,
      text: `${label}: ${cell.value.toLocaleString()} ${analytics.metric}`,
    };
  }

  function handleCellLeave() {
    tooltip = null;
  }

  function handleCellClick(cell: DayCell) {
    analytics.selectDate(cell.date);
  }

  const grid = $derived.by(() => {
    const entries = analytics.heatmap?.entries;
    if (!entries || entries.length === 0) {
      return { cols: [] as DayCell[][], months: [] as { col: number; label: string }[] };
    }

    const cols: DayCell[][] = [];
    let currentCol: DayCell[] = [];
    let lastMonth = "";
    const monthLabels: { col: number; label: string }[] = [];

    for (let i = 0; i < entries.length; i++) {
      const entry = entries[i]!;
      const d = new Date(entry.date + "T00:00:00");
      const dow = d.getDay();
      const cell: DayCell = {
        date: entry.date,
        value: entry.value,
        level: entry.level,
        dayOfWeek: dow,
      };

      if (i > 0 && dow === 0) {
        cols.push(currentCol);
        currentCol = [];
      }

      const month = d.toLocaleString("en", { month: "short" });
      if (month !== lastMonth && dow <= 3) {
        monthLabels.push({ col: cols.length, label: month });
        lastMonth = month;
      }

      currentCol.push(cell);
    }
    if (currentCol.length > 0) {
      cols.push(currentCol);
    }

    return { cols, months: monthLabels };
  });

  const chartWidth = $derived(
    grid.cols.length * CELL_STEP + LABEL_WIDTH + 4,
  );
  const chartHeight = 7 * CELL_STEP + HEADER_HEIGHT + 4;
</script>

<div class="heatmap-container">
  <div class="heatmap-header">
    <h3 class="chart-title">Activity</h3>
    <div class="metric-toggle">
      <button
        class="toggle-btn"
        class:active={analytics.metric === "messages"}
        onclick={() => analytics.setMetric("messages")}
      >
        Messages
      </button>
      <button
        class="toggle-btn"
        class:active={analytics.metric === "sessions"}
        onclick={() => analytics.setMetric("sessions")}
      >
        Sessions
      </button>
    </div>
  </div>

  {#if !analytics.loaded.heatmap || analytics.loading.heatmap}
    <LoadingSpinner label="Loading heatmap" />
  {:else if analytics.errors.heatmap}
    <div class="error">
      {analytics.errors.heatmap}
      <button
        class="retry-btn"
        onclick={() => analytics.fetchHeatmap()}
      >
        Retry
      </button>
    </div>
  {:else if grid.cols.length > 0}
    {#if analytics.heatmap?.entries_from && analytics.heatmap.entries_from > analytics.from}
      <div class="clamp-note">Showing most recent year</div>
    {/if}
    <div class="heatmap-scroll">
      <div
        class="heatmap-chart"
        style={`width:${chartWidth}px;height:${chartHeight}px;`}
      >
        {#each DAY_LABELS as label, i}
          {#if label}
            <span
              class="day-label"
              style={`left:${LABEL_WIDTH - 4}px;top:${i * CELL_STEP + HEADER_HEIGHT + CELL_SIZE - 10}px;`}
            >
              {label}
            </span>
          {/if}
        {/each}

        {#each grid.months as month}
          <span
            class="month-label"
            style={`left:${month.col * CELL_STEP + LABEL_WIDTH}px;top:${HEADER_HEIGHT - 12}px;`}
          >
            {month.label}
          </span>
        {/each}

        {#each grid.cols as col, colIdx}
          {#each col as cell}
            <button
              class="heatmap-cell"
              class:clickable={cell.value > 0 || analytics.selectedDate === cell.date}
              class:selected={analytics.selectedDate === cell.date}
              style={`left:${colIdx * CELL_STEP + LABEL_WIDTH}px;top:${cell.dayOfWeek * CELL_STEP + HEADER_HEIGHT}px;background:${levelColor(cell.level)};`}
              tabindex="-1"
              aria-label={`${cell.date}: ${cell.value} ${analytics.metric}`}
              onmouseenter={(e) => handleCellHover(e, cell)}
              onmouseleave={handleCellLeave}
              onclick={() => {
                if (cell.value > 0 || analytics.selectedDate === cell.date)
                  handleCellClick(cell);
              }}
              onkeydown={(e) => {
                if (e.key === "Enter" && (cell.value > 0 || analytics.selectedDate === cell.date))
                  handleCellClick(cell);
              }}
            ></button>
          {/each}
        {/each}
      </div>
    </div>

    {#if tooltip}
      <div
        class="tooltip"
        style="left: {tooltip.x}px; top: {tooltip.y}px;"
      >
        {tooltip.text}
      </div>
    {/if}
  {:else}
    <div class="empty">No data for this period</div>
  {/if}
</div>

<style>
  .heatmap-container {
    position: relative;
    flex: 1;
  }

  .heatmap-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--wb-spacing-8);
  }

  .chart-title {
    font-size: var(--wb-font-size-12);
    font-weight: 600;
    color: var(--vscode-editor-foreground);
  }

  .metric-toggle {
    display: flex;
    gap: var(--wb-spacing-2);
  }

  .toggle-btn {
    height: var(--wb-control-size-22);
    padding: 0 var(--wb-spacing-8);
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-10);
    font-weight: 500;
    color: var(--vscode-disabledForeground);
    cursor: pointer;
    transition: background 0.1s, color 0.1s;
  }

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

  .toggle-btn.active {
    background: var(--vscode-input-background);
    color: var(--vscode-editor-foreground);
  }

  .heatmap-scroll {
    overflow-x: auto;
    padding-bottom: var(--wb-spacing-4);
  }

  .heatmap-chart {
    position: relative;
    margin: 0 auto;
  }

  .day-label,
  .month-label {
    position: absolute;
    font-size: var(--wb-font-size-9);
    color: var(--vscode-disabledForeground);
    font-family: var(--wsb-font-sans);
    white-space: nowrap;
  }

  .day-label {
    transform: translateX(-100%);
  }

  .heatmap-cell {
    position: absolute;
    width: var(--wb-control-size-16);
    height: var(--wb-control-size-16);
    border-radius: var(--wb-corner-radius-2);
    cursor: default;
  }

  .heatmap-cell.clickable {
    cursor: pointer;
  }

  .heatmap-cell.clickable:hover {
    opacity: 0.8;
    outline: 1px solid var(--vscode-disabledForeground);
  }

  .heatmap-cell.selected {
    outline: 2px solid var(--vscode-editor-foreground);
  }

  .tooltip {
    position: fixed;
    transform: translateX(-50%) translateY(-100%);
    padding: var(--wb-spacing-4) var(--wb-spacing-8);
    background: var(--vscode-editor-foreground);
    color: var(--vscode-editor-background);
    font-size: var(--wb-font-size-10);
    border-radius: var(--wsb-radius-sm);
    white-space: nowrap;
    pointer-events: none;
    z-index: 100;
  }

  .clamp-note {
    color: var(--vscode-disabledForeground);
    font-size: var(--wb-font-size-10);
    margin-bottom: var(--wb-spacing-4);
  }

  .empty {
    color: var(--vscode-disabledForeground);
    font-size: var(--wb-font-size-12);
    padding: var(--wb-spacing-24);
    text-align: center;
  }

  .error {
    color: var(--vscode-errorForeground);
    font-size: var(--wb-font-size-12);
    padding: var(--wb-spacing-12);
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-8);
  }

  .retry-btn {
    padding: var(--wb-spacing-2) var(--wb-spacing-8);
    border: 1px solid currentColor;
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-11);
    color: inherit;
    cursor: pointer;
  }
</style>