view design_system/src/components/collections.js @ 265:056790c4fb0d

add role-aware Epi assistant prompts Add verified June knowledge, guest/member/admin Copilot profiles, profile-isolated session recovery, animated Epi greetings, and a single authoritative runtime config workflow for inference. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 10:50:30 -0700
parents 2b6e732087ff
children
line wrap: on
line source

const HTMLElementBase = globalThis.HTMLElement || class {};

function rememberAttribute(state, element, name) {
  let attributes = state.get(element);
  if (!attributes) {
    attributes = new Map();
    state.set(element, attributes);
  }
  if (!attributes.has(name)) {
    attributes.set(name, element.getAttribute(name));
  }
}

function setManagedAttribute(state, element, name, value) {
  rememberAttribute(state, element, name);
  if (value === null) element.removeAttribute(name);
  else element.setAttribute(name, value);
}

function originalAttribute(state, element, name) {
  const attributes = state.get(element);
  return attributes?.has(name)
    ? attributes.get(name)
    : element.getAttribute(name);
}

function restoreElement(state, element) {
  const attributes = state.get(element);
  if (!attributes) return;
  for (const [name, value] of attributes) {
    if (value === null) element.removeAttribute(name);
    else element.setAttribute(name, value);
  }
  state.delete(element);
}

function restoreAttributes(state) {
  for (const element of [...state.keys()]) restoreElement(state, element);
}

function pruneAttributes(state, retained) {
  for (const element of [...state.keys()]) {
    if (!retained.has(element)) restoreElement(state, element);
  }
}

function setDefaultAttribute(state, element, name, value) {
  if (!element.hasAttribute(name) || state.get(element)?.has(name)) {
    setManagedAttribute(state, element, name, value);
  }
}

function eventElement(event) {
  const target = event.target;
  if (target?.nodeType === 1) return target;
  return target?.parentElement || null;
}

function ownedElement(host, selector) {
  return [...host.querySelectorAll(selector)].find(
    element => element.closest(host.localName) === host,
  ) || null;
}

function emit(element, type, detail) {
  element.dispatchEvent(new CustomEvent(type, {
    bubbles: true,
    detail,
  }));
}

function isDisabled(control) {
  return Boolean(control?.disabled) ||
    control?.getAttribute("aria-disabled") === "true";
}

/**
 * Enhances a native scroll-snap viewport with navigation controls.
 *
 * @extends {HTMLElement}
 */
export class ZenCarousel extends HTMLElementBase {
  connectedCallback() {
    this._managed ||= new Map();
    this._onClick ||= event => {
      const control = eventElement(event)?.closest(
        "[data-zen-prev], [data-zen-next]",
      );
      if (control === this._prev && !isDisabled(control)) this.move(-1);
      else if (control === this._next && !isDisabled(control)) this.move(1);
    };
    this._onKeydown ||= event => {
      if (event.altKey || event.ctrlKey || event.metaKey ||
          !["ArrowLeft", "ArrowRight"].includes(event.key)) {
        return;
      }
      const target = eventElement(event);
      if (target?.closest("input, textarea, select, [contenteditable]")) return;
      event.preventDefault();
      this.move(event.key === "ArrowLeft" ? -1 : 1);
    };
    this._onScroll ||= () => {
      globalThis.clearTimeout(this._scrollTimer);
      this._scrollTimer = globalThis.setTimeout(() => {
        this._scrollTimer = null;
        this.setIndex(this.currentIndex(), true);
      }, 100);
    };
    this.addEventListener("click", this._onClick);
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("keydown", this._onKeydown);
    this._viewport?.removeEventListener("scroll", this._onScroll);
    globalThis.clearTimeout(this._scrollTimer);
    restoreAttributes(this._managed);
    this._viewport = null;
    this._slides = [];
    this._prev = null;
    this._next = null;
    this._index = null;
    this._scrollTimer = null;
  }

  sync() {
    const viewport = ownedElement(this, "[data-zen-viewport]");
    if (viewport !== this._viewport) {
      this._viewport?.removeEventListener("scroll", this._onScroll);
      this._viewport = viewport;
      viewport?.addEventListener("scroll", this._onScroll, { passive: true });
    }
    this._slides = viewport ? [...viewport.children] : [];
    this._prev = ownedElement(this, "[data-zen-prev]");
    this._next = ownedElement(this, "[data-zen-next]");

    const retained = new Set([
      this,
      viewport,
      this._prev,
      this._next,
      ...this._slides,
    ].filter(Boolean));
    pruneAttributes(this._managed, retained);
    setDefaultAttribute(this._managed, this, "role", "region");
    setDefaultAttribute(
      this._managed,
      this,
      "aria-roledescription",
      "carousel",
    );
    setDefaultAttribute(this._managed, this, "aria-label", "Carousel");
    if (viewport) {
      setDefaultAttribute(this._managed, viewport, "tabindex", "0");
    }
    for (const [index, slide] of this._slides.entries()) {
      setDefaultAttribute(this._managed, slide, "role", "group");
      setDefaultAttribute(
        this._managed,
        slide,
        "aria-roledescription",
        "slide",
      );
      setDefaultAttribute(
        this._managed,
        slide,
        "aria-label",
        `Slide ${index + 1} of ${this._slides.length}`,
      );
    }

    const index = this.currentIndex();
    this._index = index;
    this.render();
  }

  currentIndex() {
    if (!this._viewport || !this._slides.length) return 0;
    const viewportLeft = this._viewport.getBoundingClientRect().left;
    let closest = 0;
    let distance = Infinity;
    for (const [index, slide] of this._slides.entries()) {
      const current = Math.abs(slide.getBoundingClientRect().left - viewportLeft);
      if (current < distance) {
        closest = index;
        distance = current;
      }
    }
    return closest;
  }

  move(offset) {
    if (!this._slides.length) return;
    const current = Number.isInteger(this._index)
      ? this._index
      : this.currentIndex();
    this.scrollTo(Math.max(0, Math.min(this._slides.length - 1, current + offset)));
  }

  scrollTo(index) {
    const slide = this._slides[index];
    if (!this._viewport || !slide || index === this._index) return;
    const viewportRect = this._viewport.getBoundingClientRect();
    const slideRect = slide.getBoundingClientRect();
    const left = this._viewport.scrollLeft + slideRect.left - viewportRect.left;
    if (typeof this._viewport.scrollTo === "function") {
      this._viewport.scrollTo({ left, behavior: "smooth" });
    } else if (typeof slide.scrollIntoView === "function") {
      slide.scrollIntoView({
        behavior: "smooth",
        block: "nearest",
        inline: "start",
      });
    }
    this.setIndex(index, true);
  }

  setIndex(index, notify) {
    const next = Math.max(
      0,
      Math.min(this._slides.length ? this._slides.length - 1 : 0, index),
    );
    if (next === this._index) {
      this.render();
      return;
    }
    this._index = next;
    this.render();
    if (notify) emit(this, "zen-change", { index: next });
  }

  render() {
    const last = Math.max(0, this._slides.length - 1);
    this.syncDisabled(this._prev, !this._slides.length || this._index <= 0);
    this.syncDisabled(
      this._next,
      !this._slides.length || this._index >= last,
    );
  }

  syncDisabled(control, disabled) {
    if (!control) return;
    const authored = originalAttribute(this._managed, control, "disabled");
    setManagedAttribute(
      this._managed,
      control,
      "disabled",
      disabled || authored !== null ? "" : null,
    );
  }
}

/**
 * Keeps a message viewport pinned while the reader remains near its end.
 *
 * @extends {HTMLElement}
 */
export class ZenMessageScroller extends HTMLElementBase {
  connectedCallback() {
    this._managed ||= new Map();
    this._onScroll ||= () => {
      this._nearEnd = this.isNearEnd();
      this.render();
    };
    this._onClick ||= event => {
      const button = eventElement(event)?.closest("[data-zen-scroll-end]");
      if (button === this._button && !isDisabled(button)) this.scrollEnd(true);
    };
    this._viewport = ownedElement(this, "[data-zen-viewport]") || this;
    this._button = ownedElement(this, "[data-zen-scroll-end]");
    this._nearEnd = this.isNearEnd();
    this.addEventListener("click", this._onClick);
    this._viewport.addEventListener("scroll", this._onScroll, { passive: true });
    this._observer ||= new MutationObserver(records => {
      if (!records.some(record => record.addedNodes.length)) return;
      const wasNearEnd = this._nearEnd;
      if (wasNearEnd) this.scrollEnd(false);
      else this.render();
    });
    this._observer.observe(this._viewport, { childList: true, subtree: true });
    this.render();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this._viewport?.removeEventListener("scroll", this._onScroll);
    restoreAttributes(this._managed);
    this._viewport = null;
    this._button = null;
  }

  isNearEnd() {
    if (!this._viewport) return true;
    return this._viewport.scrollHeight - this._viewport.scrollTop -
      this._viewport.clientHeight <= 24;
  }

  scrollEnd(smooth) {
    if (!this._viewport) return;
    if (typeof this._viewport.scrollTo === "function") {
      this._viewport.scrollTo({
        top: this._viewport.scrollHeight,
        behavior: smooth ? "smooth" : "auto",
      });
    } else {
      this._viewport.scrollTop = this._viewport.scrollHeight;
    }
    this._nearEnd = true;
    this.render();
  }

  render() {
    if (!this._button) return;
    setManagedAttribute(
      this._managed,
      this._button,
      "hidden",
      this._nearEnd ? "" : null,
    );
  }
}

/**
 * Adds pointer and keyboard resizing to two native content panels.
 *
 * @extends {HTMLElement}
 */
export class ZenResizable extends HTMLElementBase {
  static get observedAttributes() {
    return ["min", "max"];
  }

  connectedCallback() {
    this._managed ||= new Map();
    this._onPointerDown ||= event => {
      const handle = eventElement(event)?.closest("[data-zen-handle]");
      if (handle !== this._handle || (event.button !== 0 && event.button !== -1)) {
        return;
      }
      event.preventDefault();
      this._pointerId = event.pointerId;
      this._handle.setPointerCapture?.(event.pointerId);
    };
    this._onPointerMove ||= event => {
      if (event.pointerId !== this._pointerId) return;
      const rect = this.getBoundingClientRect();
      if (!rect.width) return;
      this.setPercent((event.clientX - rect.left) / rect.width * 100, true);
    };
    this._onPointerEnd ||= event => {
      if (event.pointerId !== this._pointerId) return;
      if (this._handle?.hasPointerCapture?.(event.pointerId)) {
        this._handle.releasePointerCapture(event.pointerId);
      }
      this._pointerId = null;
    };
    this._onKeydown ||= event => {
      const handle = eventElement(event)?.closest("[data-zen-handle]");
      if (handle !== this._handle ||
          !["ArrowLeft", "ArrowRight"].includes(event.key)) {
        return;
      }
      event.preventDefault();
      const step = Number(this.getAttribute("step"));
      const amount = Number.isFinite(step) && step > 0 ? step : 1;
      this.setPercent(
        this._percent + (event.key === "ArrowLeft" ? -amount : amount),
        true,
      );
    };
    this.addEventListener("pointerdown", this._onPointerDown);
    this.addEventListener("pointermove", this._onPointerMove);
    this.addEventListener("pointerup", this._onPointerEnd);
    this.addEventListener("pointercancel", this._onPointerEnd);
    this.addEventListener("keydown", this._onKeydown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("pointerdown", this._onPointerDown);
    this.removeEventListener("pointermove", this._onPointerMove);
    this.removeEventListener("pointerup", this._onPointerEnd);
    this.removeEventListener("pointercancel", this._onPointerEnd);
    this.removeEventListener("keydown", this._onKeydown);
    restoreAttributes(this._managed);
    this._first = null;
    this._second = null;
    this._handle = null;
    this._pointerId = null;
    this._percent = null;
  }

  attributeChangedCallback() {
    if (this.isConnected && Number.isFinite(this._percent)) {
      this.setPercent(this._percent, false);
    }
  }

  limits() {
    const minimum = this.getAttribute("min");
    const maximum = this.getAttribute("max");
    const min = minimum !== null && Number.isFinite(Number(minimum))
      ? Math.max(0, Math.min(100, Number(minimum)))
      : 0;
    const max = maximum !== null && Number.isFinite(Number(maximum))
      ? Math.max(min, Math.min(100, Number(maximum)))
      : 100;
    return { min, max };
  }

  sync() {
    const children = [...this.children];
    const handle = children.find(child => child.hasAttribute("data-zen-handle"));
    const handleIndex = children.indexOf(handle);
    const first = children.slice(0, handleIndex).reverse().find(
      child => child.hasAttribute("data-zen-panel"),
    );
    const second = children.slice(handleIndex + 1).find(
      child => child.hasAttribute("data-zen-panel"),
    );

    if (!first || !second || !handle) {
      if (this._first) restoreElement(this._managed, this._first);
      if (this._handle) restoreElement(this._managed, this._handle);
      this._first = null;
      this._second = null;
      this._handle = null;
      this._percent = null;
      return;
    }
    if (this._first && this._first !== first) restoreElement(this._managed, this._first);
    if (this._handle && this._handle !== handle) {
      restoreElement(this._managed, this._handle);
    }
    this._first = first;
    this._second = second;
    this._handle = handle;

    setManagedAttribute(this._managed, this._handle, "role", "separator");
    setManagedAttribute(
      this._managed,
      this._handle,
      "aria-orientation",
      "vertical",
    );
    setDefaultAttribute(this._managed, this._handle, "tabindex", "0");

    if (!Number.isFinite(this._percent)) {
      const containerWidth = this.getBoundingClientRect().width;
      const rendered = containerWidth
        ? this._first.getBoundingClientRect().width / containerWidth * 100
        : Number.parseFloat(this._first.style.flexBasis);
      this._percent = Number.isFinite(rendered) ? rendered : 50;
    }
    this.setPercent(this._percent, false);
  }

  setPercent(percent, notify) {
    if (!this._first || !this._handle) return;
    const { min, max } = this.limits();
    const next = Math.round(Math.max(min, Math.min(max, percent)) * 100) / 100;
    const changed = next !== this._percent;
    this._percent = next;
    rememberAttribute(this._managed, this._first, "style");
    this._first.style.flexBasis = `${next}%`;
    setManagedAttribute(this._managed, this._handle, "aria-valuemin", String(min));
    setManagedAttribute(this._managed, this._handle, "aria-valuemax", String(max));
    setManagedAttribute(this._managed, this._handle, "aria-valuenow", String(next));
    if (notify && changed) emit(this, "zen-resize", { percent: next });
  }
}

/**
 * Adds stable client-side sorting to a native HTML table.
 *
 * @extends {HTMLElement}
 */
export class ZenDataTable extends HTMLElementBase {
  connectedCallback() {
    this._managed ||= new Map();
    this._onClick ||= event => {
      const button = eventElement(event)?.closest("button[data-zen-sort]");
      if (!this._buttons.includes(button) || isDisabled(button)) return;
      this.sort(button);
    };
    this.addEventListener("click", this._onClick);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    restoreAttributes(this._managed);
    this._table = null;
    this._buttons = [];
  }

  sync() {
    this._table =
      this.querySelector(":scope > table") || this.querySelector("table");
    this._buttons = this._table
      ? [...this._table.querySelectorAll("button[data-zen-sort]")].filter(
        button => button.closest("table") === this._table &&
          button.closest("th"),
      )
      : [];
    pruneAttributes(
      this._managed,
      new Set(this._buttons.map(button => button.closest("th"))),
    );
  }

  sort(button) {
    const key = button.getAttribute("data-zen-sort");
    const header = button.closest("th");
    if (!this._table || !key || !header) return;
    const direction =
      header.getAttribute("aria-sort") === "ascending"
        ? "descending"
        : "ascending";

    for (const candidate of this._buttons) {
      const candidateHeader = candidate.closest("th");
      setManagedAttribute(
        this._managed,
        candidateHeader,
        "aria-sort",
        candidateHeader === header ? direction : null,
      );
    }

    for (const body of this._table.tBodies) {
      const entries = [...body.rows].map((row, index) => {
        const cell = [...row.cells].find(
          candidate => candidate.getAttribute("data-key") === key,
        );
        return { index, row, value: cell?.textContent?.trim() ?? "" };
      });
      const numeric = entries.every(
        entry => Number.isFinite(Number(entry.value)),
      );
      const factor = direction === "ascending" ? 1 : -1;
      entries.sort((left, right) => {
        const compared = numeric
          ? Number(left.value) - Number(right.value)
          : left.value.localeCompare(right.value);
        return compared ? compared * factor : left.index - right.index;
      });
      body.append(...entries.map(entry => entry.row));
    }
    emit(this, "zen-sort", { column: key, direction });
  }
}

const definitions = {
  "zen-carousel": ZenCarousel,
  "zen-data-table": ZenDataTable,
  "zen-message-scroller": ZenMessageScroller,
  "zen-resizable": ZenResizable,
};

if (globalThis.customElements) {
  for (const [name, definition] of Object.entries(definitions)) {
    if (!globalThis.customElements.get(name)) {
      globalThis.customElements.define(name, definition);
    }
  }
}