taxa-server 0.1.0

axum web server for taxa: reproduces the HTTP contract + serves the embedded D3 frontend.
// Keyboard shortcuts — a declarative, easily-overridable keymap.
//
// Defaults to monocle's treemap-navigation set. The system is intentionally
// small and data-driven so it's trivial to change:
//
//   • MODIFY a binding   → edit DEFAULT_KEYMAP below.
//   • DISABLE everything → don't call installShortcuts() (or call it with []).
//   • CUSTOMIZE per app   → installShortcuts(myKeymap) with your own array.
//
// A binding is: {
//   keys:        [string]   // KeyboardEvent.key values that trigger it
//   when:        string?    // only fire on this tab id (null = any tab)
//   allowShift:  bool?      // permit Shift (needed for { } [ } on US layouts)
//   description: string     // for a future help overlay
//   run:         (e) => {}  // the action; receives the KeyboardEvent
// }
//
// Guards (shared by every binding): never fires while typing in an input /
// textarea / select / contenteditable, and never with Ctrl/Cmd/Alt held.

import {
  getActiveTab,
  cycleTreemapAxis,
  cycleTreemapMetric,
  setTreemapLevels,
} from "/static/selection.js";

// The monocle defaults: { } cycle Size-by, [ ] cycle Bucket-by (axis), digits
// set Levels. On US layouts Shift+[ yields { and Shift+] yields } — exactly the
// keys we want for Size-by — so those bindings allow Shift; the digit keys do
// not (matching monocle's "1–N without Shift").
export const DEFAULT_KEYMAP = [
  {
    keys: ["{", "}"],
    when: "treemap",
    allowShift: true,
    description: "Size by: previous / next metric",
    run: (e) => cycleTreemapMetric(e.key === "}" ? 1 : -1),
  },
  {
    keys: ["[", "]"],
    when: "treemap",
    allowShift: true,
    description: "Bucket by: previous / next axis",
    run: (e) => cycleTreemapAxis(e.key === "]" ? 1 : -1),
  },
  {
    keys: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
    when: "treemap",
    description: "Set treemap depth (Levels)",
    run: (e) => setTreemapLevels(e.key),
  },
];

function isTypingTarget(t) {
  return (
    t &&
    (t.tagName === "INPUT" ||
      t.tagName === "TEXTAREA" ||
      t.tagName === "SELECT" ||
      t.isContentEditable)
  );
}

/**
 * Install a single global keydown handler that dispatches to `keymap`.
 * Returns an uninstall function so a caller can tear it down.
 */
export function installShortcuts(keymap = DEFAULT_KEYMAP) {
  const onKey = (e) => {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    if (isTypingTarget(e.target)) return;
    const tab = getActiveTab();
    for (const b of keymap) {
      if (b.when && b.when !== tab) continue;
      if (!b.keys.includes(e.key)) continue;
      if (e.shiftKey && !b.allowShift) continue;
      b.run(e);
      e.preventDefault();
      return;
    }
  };
  document.addEventListener("keydown", onKey);
  return () => document.removeEventListener("keydown", onKey);
}