diff design_system/src/components/forms.js @ 254:2b6e732087ff

[ui] Add complete native component catalog Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 15:12:09 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design_system/src/components/forms.js	Tue Aug 04 15:12:09 2026 -0700
@@ -0,0 +1,1240 @@
+let nextFormId = 1;
+
+function uniqueId(prefix) {
+  let id;
+  do id = `${prefix}-${nextFormId++}`;
+  while (document.getElementById(id));
+  return id;
+}
+
+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 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 restoreAttribute(state, element, name) {
+  const attributes = state.get(element);
+  if (!attributes?.has(name)) return;
+  const value = attributes.get(name);
+  if (value === null) element.removeAttribute(name);
+  else element.setAttribute(name, value);
+  attributes.delete(name);
+  if (!attributes.size) state.delete(element);
+}
+
+function restoreAttributes(state) {
+  for (const element of [...state.keys()]) restoreElement(state, element);
+}
+
+function originalAttribute(state, element, name) {
+  const attributes = state.get(element);
+  return attributes?.has(name)
+    ? attributes.get(name)
+    : element.getAttribute(name);
+}
+
+function ensureId(state, element, prefix) {
+  const owner = element.id && document.getElementById(element.id);
+  if (!element.id || (owner && owner !== element)) {
+    setManagedAttribute(state, element, "id", uniqueId(prefix));
+  }
+  return element.id;
+}
+
+function emit(element, type, detail) {
+  element.dispatchEvent(new CustomEvent(type, {
+    bubbles: true,
+    detail,
+  }));
+}
+
+class NativeControl extends HTMLElement {
+  connectedCallback() {
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this._control = null;
+  }
+
+  sync() {
+    this._control = this.querySelector(this.controlSelector);
+  }
+}
+
+/**
+ * Provides presentation around a native checkbox input.
+ *
+ * @extends {NativeControl}
+ */
+export class ZenCheckbox extends NativeControl {
+  get controlSelector() {
+    return 'input[type="checkbox"]';
+  }
+}
+
+/**
+ * Provides presentation around a native select control.
+ *
+ * @extends {NativeControl}
+ */
+export class ZenSelect extends NativeControl {
+  get controlSelector() {
+    return "select";
+  }
+
+  sync() {
+    super.sync();
+    if (!this._control) return;
+    let icon = this.querySelector(
+      ":scope > zen-icon[data-zen-select-icon]",
+    );
+    if (!icon) {
+      icon = document.createElement("zen-icon");
+      icon.dataset.zenSelectIcon = "";
+      icon.dataset.zenGenerated = "true";
+      icon.setAttribute("name", "chevron-down");
+      this.append(icon);
+    }
+  }
+
+  disconnectedCallback() {
+    this.querySelector(
+      ':scope > zen-icon[data-zen-select-icon][data-zen-generated="true"]',
+    )?.remove();
+    super.disconnectedCallback();
+  }
+}
+
+/**
+ * Provides presentation around a native range input.
+ *
+ * @extends {NativeControl}
+ */
+export class ZenSlider extends NativeControl {
+  get controlSelector() {
+    return 'input[type="range"]';
+  }
+}
+
+function createDate(year, month, day) {
+  const date = new Date(0);
+  date.setHours(0, 0, 0, 0);
+  date.setFullYear(year, month, day);
+  return date;
+}
+
+function parseDate(value) {
+  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || "");
+  if (!match) return null;
+  const year = Number(match[1]);
+  const month = Number(match[2]) - 1;
+  const day = Number(match[3]);
+  const date = createDate(year, month, day);
+  if (date.getFullYear() !== year ||
+      date.getMonth() !== month ||
+      date.getDate() !== day) return null;
+  return date;
+}
+
+function dateKey(date) {
+  const year = String(date.getFullYear()).padStart(4, "0");
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
+}
+
+function addDays(date, amount) {
+  return createDate(
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate() + amount,
+  );
+}
+
+function addMonths(date, amount) {
+  const first = createDate(date.getFullYear(), date.getMonth() + amount, 1);
+  const last = createDate(
+    first.getFullYear(),
+    first.getMonth() + 1,
+    0,
+  ).getDate();
+  return createDate(
+    first.getFullYear(),
+    first.getMonth(),
+    Math.min(date.getDate(), last),
+  );
+}
+
+/**
+ * Renders a tokenized calendar backed by a native date input.
+ *
+ * @extends {HTMLElement}
+ */
+export class ZenCalendar extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    this._onClick ||= event => this.onClick(event);
+    this._onKeydown ||= event => this.onKeydown(event);
+    this._onInput ||= event => {
+      if (event.target !== this._input) return;
+      this.readInput();
+      if (this._input.validity.valid) this.setInvalid(false);
+      this.render();
+    };
+    this._onInvalid ||= event => {
+      if (event.target !== this._input) return;
+      event.preventDefault();
+      this.setInvalid(true);
+      queueMicrotask(() => {
+        if (!this.isConnected) return;
+        const target = this._trigger ||
+          this._grid?.querySelector('[tabindex="0"]:not(:disabled)');
+        target?.focus();
+      });
+    };
+    this._onDocumentPointerDown ||= event => {
+      if (this.isPicker && this._open && !this.contains(event.target)) {
+        this.setOpen(false);
+      }
+    };
+    this._onReset ||= () => queueMicrotask(() => {
+      if (!this.isConnected) return;
+      this.readInput();
+      this.render();
+    });
+    this.addEventListener("click", this._onClick);
+    this.addEventListener("keydown", this._onKeydown);
+    this.addEventListener("input", this._onInput);
+    this.addEventListener("change", this._onInput);
+    this.addEventListener("invalid", this._onInvalid, true);
+    document.addEventListener("pointerdown", this._onDocumentPointerDown);
+    this._observer ||= new MutationObserver(() => this.sync());
+    this._observer.observe(this, { childList: true });
+    this._inputObserver ||= new MutationObserver(() => {
+      this.readInput();
+      this.render();
+    });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this._inputObserver?.disconnect();
+    this._form?.removeEventListener("reset", this._onReset);
+    this.removeEventListener("click", this._onClick);
+    this.removeEventListener("keydown", this._onKeydown);
+    this.removeEventListener("input", this._onInput);
+    this.removeEventListener("change", this._onInput);
+    this.removeEventListener("invalid", this._onInvalid, true);
+    document.removeEventListener("pointerdown", this._onDocumentPointerDown);
+    this.release();
+  }
+
+  get isPicker() {
+    return this.localName === "zen-date-picker";
+  }
+
+  sync() {
+    const input = [...this.children].find(
+      child => child.matches?.('input[type="date"]'),
+    ) || null;
+    if (input === this._input) return;
+    this.release();
+    this._input = input;
+    if (!input) return;
+
+    setManagedAttribute(
+      this._managed,
+      input,
+      "data-zen-calendar-input",
+      "",
+    );
+    setManagedAttribute(this._managed, input, "tabindex", "-1");
+    setManagedAttribute(this._managed, input, "aria-hidden", "true");
+    this._inputObserver.observe(input, {
+      attributes: true,
+      attributeFilter: ["disabled", "max", "min", "readonly", "value"],
+    });
+    this._form = input.form;
+    this._form?.addEventListener("reset", this._onReset);
+    this.readInput();
+    this.createView();
+    this.render();
+  }
+
+  release() {
+    this._inputObserver?.disconnect();
+    this._form?.removeEventListener("reset", this._onReset);
+    this._generated?.remove();
+    this._generated = null;
+    this._view = null;
+    this._grid = null;
+    this._heading = null;
+    this._trigger = null;
+    this._panel = null;
+    this._labelText = "";
+    this._labelIds = [];
+    this._form = null;
+    this._open = false;
+    restoreAttributes(this._managed || new Map());
+    this._input = null;
+  }
+
+  readInput() {
+    const selected = parseDate(this._input?.value);
+    const today = new Date();
+    this._selected = selected;
+    this._focusDate = selected || this._focusDate || today;
+    this._month = createDate(
+      (selected || this._focusDate).getFullYear(),
+      (selected || this._focusDate).getMonth(),
+      1,
+    );
+  }
+
+  limits() {
+    return {
+      maximum: parseDate(this._input?.max),
+      minimum: parseDate(this._input?.min),
+    };
+  }
+
+  isEditable() {
+    return Boolean(this._input) &&
+      !this._input.disabled &&
+      !this._input.readOnly;
+  }
+
+  isAllowed(date) {
+    if (!date) return false;
+    const { maximum, minimum } = this.limits();
+    return (!minimum || date >= minimum) && (!maximum || date <= maximum);
+  }
+
+  clampDate(date) {
+    const { maximum, minimum } = this.limits();
+    if (minimum && date < minimum) return minimum;
+    if (maximum && date > maximum) return maximum;
+    return date;
+  }
+
+  setInvalid(invalid) {
+    setManagedAttribute(
+      this._managed,
+      this,
+      "data-invalid",
+      invalid ? "" : null,
+    );
+    if (this._trigger) {
+      this._trigger.setAttribute("aria-invalid", String(invalid));
+    }
+    if (this._grid) {
+      this._grid.setAttribute("aria-invalid", String(invalid));
+    }
+  }
+
+  createIcon(name) {
+    const icon = document.createElement("zen-icon");
+    icon.setAttribute("name", name);
+    return icon;
+  }
+
+  createControl(name, label) {
+    const button = document.createElement("button");
+    button.type = "button";
+    button.dataset[name] = "";
+    button.setAttribute("aria-label", label);
+    return button;
+  }
+
+  createView() {
+    const generated = document.createElement("div");
+    generated.dataset.zenGenerated = "calendar";
+    this._generated = generated;
+    const labels = [...this._input.labels];
+    const authoredLabelIds = (this._input.getAttribute("aria-labelledby") || "")
+      .split(/\s+/)
+      .filter(id => id && document.getElementById(id));
+    const associatedLabelIds = labels.map(label =>
+      ensureId(this._managed, label, "zen-date-label")
+    );
+    this._labelIds = authoredLabelIds.length
+      ? authoredLabelIds
+      : associatedLabelIds;
+    this._labelText = this._labelIds.map(id =>
+      document.getElementById(id)?.textContent.trim()
+    )
+      .filter(Boolean)
+      .join(", ") ||
+      labels.map(label => label.textContent.trim())
+      .filter(Boolean)
+      .join(", ") ||
+      this._input.getAttribute("aria-label") ||
+      "Date";
+
+    if (this.isPicker) {
+      const trigger = this.createControl(
+        "zenDateTrigger",
+        "Choose date",
+      );
+      trigger.id = uniqueId("zen-date-picker-trigger");
+      trigger.append(this.createIcon("calendar"));
+      const triggerText = document.createElement("span");
+      triggerText.dataset.zenDateValue = "";
+      triggerText.id = uniqueId("zen-date-picker-value");
+      trigger.append(triggerText);
+      this._trigger = trigger;
+
+      const panel = document.createElement("div");
+      panel.dataset.zenCalendarPanel = "";
+      panel.id = uniqueId("zen-date-picker-panel");
+      panel.setAttribute("role", "dialog");
+      panel.setAttribute("aria-label", `${this._labelText} calendar`);
+      panel.hidden = true;
+      trigger.setAttribute("aria-haspopup", "dialog");
+      trigger.setAttribute("aria-controls", panel.id);
+      if (this._input.hasAttribute("aria-describedby")) {
+        trigger.setAttribute(
+          "aria-describedby",
+          this._input.getAttribute("aria-describedby"),
+        );
+      }
+      if (this._labelIds.length) {
+        trigger.setAttribute(
+          "aria-labelledby",
+          [...this._labelIds, triggerText.id].join(" "),
+        );
+      }
+      for (const label of labels) {
+        if (label.htmlFor === this._input.id) {
+          setManagedAttribute(this._managed, label, "for", trigger.id);
+        }
+      }
+      this._panel = panel;
+      generated.append(trigger, panel);
+    }
+
+    const view = document.createElement("div");
+    view.dataset.zenCalendarView = "";
+    const header = document.createElement("header");
+    const previous = this.createControl(
+      "zenCalendarPrevious",
+      "Previous month",
+    );
+    previous.append(this.createIcon("chevron-left"));
+    const heading = document.createElement("strong");
+    heading.setAttribute("aria-live", "polite");
+    const next = this.createControl("zenCalendarNext", "Next month");
+    next.append(this.createIcon("chevron-right"));
+    header.append(previous, heading, next);
+
+    const weekdays = document.createElement("div");
+    weekdays.dataset.zenCalendarWeekdays = "";
+    weekdays.setAttribute("aria-hidden", "true");
+    const formatter = new Intl.DateTimeFormat(undefined, {
+      weekday: "narrow",
+    });
+    for (let day = 4; day < 11; day++) {
+      const label = document.createElement("span");
+      label.textContent = formatter.format(new Date(2026, 0, day));
+      weekdays.append(label);
+    }
+
+    const grid = document.createElement("div");
+    grid.dataset.zenCalendarGrid = "";
+    grid.setAttribute("role", "grid");
+    grid.setAttribute(
+      "aria-label",
+      this._labelText ? `${this._labelText} calendar` : "Calendar",
+    );
+    view.append(header, weekdays, grid);
+    this._view = view;
+    this._heading = heading;
+    this._grid = grid;
+    (this._panel || generated).append(view);
+    this.append(generated);
+    if (this.isPicker) this.setOpen(false);
+  }
+
+  render() {
+    if (!this._grid || !this._month) return;
+    this._focusDate = this.clampDate(this._focusDate);
+    this._heading.textContent = new Intl.DateTimeFormat(undefined, {
+      month: "long",
+      year: "numeric",
+    }).format(this._month);
+
+    if (this._trigger) {
+      const text = this._trigger.querySelector("[data-zen-date-value]");
+      text.textContent = this._selected
+        ? new Intl.DateTimeFormat(undefined, {
+          dateStyle: "medium",
+        }).format(this._selected)
+        : "Choose date";
+      if (this._labelIds.length) {
+        this._trigger.removeAttribute("aria-label");
+      } else {
+        this._trigger.setAttribute(
+          "aria-label",
+          `${this._labelText}, ${text.textContent}`,
+        );
+      }
+      this._trigger.disabled = !this.isEditable();
+    }
+    for (const control of this._view.querySelectorAll(
+      "[data-zen-calendar-previous], [data-zen-calendar-next]",
+    )) {
+      control.disabled = !this.isEditable();
+    }
+
+    const first = createDate(
+      this._month.getFullYear(),
+      this._month.getMonth(),
+      1,
+    );
+    const start = addDays(first, -first.getDay());
+    const { maximum, minimum } = this.limits();
+    const today = dateKey(new Date());
+    const selected = this._selected ? dateKey(this._selected) : "";
+    const focused = dateKey(this._focusDate);
+    const fragment = document.createDocumentFragment();
+    const formatter = new Intl.DateTimeFormat(undefined, {
+      dateStyle: "full",
+    });
+
+    for (let week = 0; week < 6; week++) {
+      const row = document.createElement("div");
+      row.setAttribute("role", "row");
+      for (let day = 0; day < 7; day++) {
+        const date = addDays(start, week * 7 + day);
+        const key = dateKey(date);
+        const button = document.createElement("button");
+        button.type = "button";
+        button.dataset.zenCalendarDay = key;
+        button.setAttribute("role", "gridcell");
+        button.setAttribute("aria-label", formatter.format(date));
+        button.setAttribute("aria-selected", String(key === selected));
+        const disabled = !this.isEditable() ||
+          Boolean((minimum && date < minimum) || (maximum && date > maximum));
+        button.tabIndex = key === focused && !disabled ? 0 : -1;
+        button.textContent = String(date.getDate());
+        button.toggleAttribute(
+          "data-outside",
+          date.getMonth() !== this._month.getMonth(),
+        );
+        button.toggleAttribute("data-today", key === today);
+        button.disabled = disabled;
+        row.append(button);
+      }
+      fragment.append(row);
+    }
+    this._grid.replaceChildren(fragment);
+  }
+
+  onClick(event) {
+    const target = event.target instanceof Element ? event.target : null;
+    if (!target) return;
+    if (target.closest("[data-zen-date-trigger]") === this._trigger) {
+      if (!this.isEditable()) return;
+      this.setOpen(!this._open);
+      return;
+    }
+    if (target.closest("[data-zen-calendar-previous]")) {
+      if (!this.isEditable()) return;
+      this.changeMonth(-1);
+      return;
+    }
+    if (target.closest("[data-zen-calendar-next]")) {
+      if (!this.isEditable()) return;
+      this.changeMonth(1);
+      return;
+    }
+    const day = target.closest("[data-zen-calendar-day]");
+    if (day && !day.disabled) this.selectDate(parseDate(day.dataset.zenCalendarDay));
+  }
+
+  onKeydown(event) {
+    if (event.key === "Escape" && this.isPicker && this._open) {
+      event.preventDefault();
+      this.setOpen(false);
+      this._trigger.focus();
+      return;
+    }
+    const day = event.target instanceof Element
+      ? event.target.closest("[data-zen-calendar-day]")
+      : null;
+    if (!day) return;
+    const current = parseDate(day.dataset.zenCalendarDay);
+    let next = null;
+    if (event.key === "ArrowLeft") next = addDays(current, -1);
+    else if (event.key === "ArrowRight") next = addDays(current, 1);
+    else if (event.key === "ArrowUp") next = addDays(current, -7);
+    else if (event.key === "ArrowDown") next = addDays(current, 7);
+    else if (event.key === "Home") next = addDays(current, -current.getDay());
+    else if (event.key === "End") next = addDays(current, 6 - current.getDay());
+    else if (event.key === "PageUp") next = addMonths(current, -1);
+    else if (event.key === "PageDown") next = addMonths(current, 1);
+    if (!next) return;
+    next = this.clampDate(next);
+    event.preventDefault();
+    this._focusDate = next;
+    this._month = createDate(next.getFullYear(), next.getMonth(), 1);
+    this.render();
+    this._grid.querySelector(
+      `[data-zen-calendar-day="${dateKey(next)}"]`,
+    )?.focus();
+  }
+
+  changeMonth(amount) {
+    if (!this.isEditable()) return;
+    this._month = addMonths(this._month, amount);
+    this._focusDate = this.clampDate(createDate(
+      this._month.getFullYear(),
+      this._month.getMonth(),
+      1,
+    ));
+    this._month = createDate(
+      this._focusDate.getFullYear(),
+      this._focusDate.getMonth(),
+      1,
+    );
+    this.render();
+    this._grid.querySelector('[tabindex="0"]')?.focus();
+  }
+
+  selectDate(date) {
+    if (!this.isEditable() || !this.isAllowed(date)) return;
+    this._selected = date;
+    this._focusDate = date;
+    this._month = createDate(date.getFullYear(), date.getMonth(), 1);
+    this._input.value = dateKey(date);
+    this._input.dispatchEvent(new Event("input", {
+      bubbles: true,
+      composed: true,
+    }));
+    this._input.dispatchEvent(new Event("change", { bubbles: true }));
+    emit(this, "zen-change", { value: this._input.value });
+    this.render();
+    if (this.isPicker) {
+      this.setOpen(false);
+      this._trigger.focus();
+    }
+  }
+
+  setOpen(open) {
+    if (!this.isPicker || !this._panel || !this._trigger) return;
+    this._open = Boolean(open) && this.isEditable();
+    this._panel.hidden = !this._open;
+    this._trigger.setAttribute("aria-expanded", String(this._open));
+    if (this._open) {
+      this._grid.querySelector('[tabindex="0"]')?.focus();
+    }
+  }
+}
+
+/**
+ * Presents the first-party calendar behind a tokenized disclosure trigger.
+ *
+ * @extends {ZenCalendar}
+ */
+export class ZenDatePicker extends ZenCalendar {}
+
+/**
+ * Adds radiogroup semantics when no native fieldset owns the radios.
+ *
+ * @extends {HTMLElement}
+ */
+export class ZenRadioGroup extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    restoreAttributes(this._managed);
+  }
+
+  sync() {
+    const radios = this.querySelectorAll('input[type="radio"]');
+    const needsRole = radios.length > 0 && !this.querySelector("fieldset");
+    if (needsRole && !this.hasAttribute("role")) {
+      setManagedAttribute(this._managed, this, "role", "radiogroup");
+    } else if (!needsRole) {
+      restoreElement(this._managed, this);
+    }
+  }
+}
+
+/**
+ * Synchronizes switch semantics onto a native checkbox.
+ *
+ * @extends {HTMLElement}
+ */
+export class ZenSwitch extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    this._onStateChange ||= event => {
+      if (event.target === this._control) this.updateState();
+    };
+    this._onReset ||= () => {
+      queueMicrotask(() => {
+        if (this.isConnected) this.updateState();
+      });
+    };
+    this.addEventListener("input", this._onStateChange);
+    this.addEventListener("change", this._onStateChange);
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this.removeEventListener("input", this._onStateChange);
+    this.removeEventListener("change", this._onStateChange);
+    this._form?.removeEventListener("reset", this._onReset);
+    restoreAttributes(this._managed);
+    this._control = null;
+    this._form = null;
+  }
+
+  sync() {
+    const control = this.querySelector('input[type="checkbox"]');
+    const form = control?.form || null;
+    if (control !== this._control) {
+      if (this._control) restoreElement(this._managed, this._control);
+      this._control = control;
+    }
+    if (form !== this._form) {
+      this._form?.removeEventListener("reset", this._onReset);
+      this._form = form;
+      form?.addEventListener("reset", this._onReset);
+    }
+    this.updateState();
+  }
+
+  updateState() {
+    if (!this._control) return;
+    setManagedAttribute(this._managed, this._control, "role", "switch");
+    setManagedAttribute(
+      this._managed,
+      this._control,
+      "aria-checked",
+      String(this._control.checked),
+    );
+  }
+}
+
+/**
+ * Reflects native form validity on the component boundary.
+ *
+ * @extends {HTMLElement}
+ */
+export class ZenForm extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    this._syncValidity ||= () => {
+      if (!this._form) return;
+      setManagedAttribute(
+        this._managed,
+        this,
+        "data-invalid",
+        this._form.checkValidity() ? null : "",
+      );
+    };
+    this._markInvalid ||= () => {
+      setManagedAttribute(this._managed, this, "data-invalid", "");
+    };
+    this._onReset ||= () => queueMicrotask(this._syncValidity);
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this.releaseForm();
+    restoreAttributes(this._managed);
+  }
+
+  releaseForm() {
+    this._form?.removeEventListener("submit", this._syncValidity);
+    this._form?.removeEventListener("input", this._syncValidity);
+    this._form?.removeEventListener("invalid", this._markInvalid, true);
+    this._form?.removeEventListener("reset", this._onReset);
+    this._form = null;
+  }
+
+  sync() {
+    const form = this.querySelector("form");
+    if (form === this._form) return;
+    this.releaseForm();
+    restoreElement(this._managed, this);
+    this._form = form;
+    form?.addEventListener("submit", this._syncValidity);
+    form?.addEventListener("input", this._syncValidity);
+    form?.addEventListener("invalid", this._markInvalid, true);
+    form?.addEventListener("reset", this._onReset);
+  }
+}
+
+class FilterableList extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    this._active = null;
+    this._onInput ||= event => {
+      if (event.target !== this._input) return;
+      if (this.isCombobox) this.setOpen(true);
+      this.filter();
+    };
+    this._onKeydown ||= event => this.onKeydown(event);
+    this._onClick ||= event => this.onClick(event);
+    this._onFocus ||= event => {
+      if (this.isCombobox && event.target === this._input) {
+        this.setOpen(true);
+        this.filter();
+      }
+    };
+    this.addEventListener("input", this._onInput);
+    this.addEventListener("keydown", this._onKeydown);
+    this.addEventListener("click", this._onClick);
+    this.addEventListener("focusin", this._onFocus);
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, { childList: true, subtree: true });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this.removeEventListener("input", this._onInput);
+    this.removeEventListener("keydown", this._onKeydown);
+    this.removeEventListener("click", this._onClick);
+    this.removeEventListener("focusin", this._onFocus);
+    restoreAttributes(this._managed);
+    this._input = null;
+    this._list = null;
+    this._items = [];
+    this._active = null;
+  }
+
+  get isCombobox() {
+    return false;
+  }
+
+  get listRole() {
+    return "menu";
+  }
+
+  get itemRole() {
+    return "menuitem";
+  }
+
+  sync() {
+    const input = this.querySelector("input");
+    const list = this.querySelector(`[role="${this.listRole}"]`);
+    if (input !== this._input || list !== this._list) {
+      restoreAttributes(this._managed);
+      this._input = input;
+      this._list = list;
+      this._items = [];
+      this._active = null;
+      this._open = false;
+    }
+    if (!input || !list) return;
+
+    const items = [...list.querySelectorAll(`[role="${this.itemRole}"]`)];
+    for (const item of this._items) {
+      if (!items.includes(item)) restoreElement(this._managed, item);
+    }
+    this._items = items;
+
+    const listId = ensureId(
+      this._managed,
+      list,
+      this.isCombobox ? "zen-listbox" : "zen-command-menu",
+    );
+    if (!input.hasAttribute("aria-controls")) {
+      setManagedAttribute(this._managed, input, "aria-controls", listId);
+    }
+    if (this.isCombobox) {
+      if (!input.hasAttribute("role")) {
+        setManagedAttribute(this._managed, input, "role", "combobox");
+      }
+      if (!input.hasAttribute("aria-autocomplete")) {
+        setManagedAttribute(
+          this._managed,
+          input,
+          "aria-autocomplete",
+          "list",
+        );
+      }
+      this.setOpen(this._open);
+    }
+    this.filter();
+  }
+
+  filter() {
+    if (!this._input || !this._list) return;
+    const query = this._input.value.trim().toLowerCase();
+    for (const item of this._items) {
+      const originallyHidden =
+        originalAttribute(this._managed, item, "hidden") !== null;
+      const matches = item.textContent.toLowerCase().includes(query);
+      setManagedAttribute(
+        this._managed,
+        item,
+        "hidden",
+        originallyHidden || !matches ? "" : null,
+      );
+    }
+    if (this._active?.hidden) this.setActive(null);
+  }
+
+  visibleItems() {
+    return this._items.filter(item => {
+      if (item.hidden || item.closest("[hidden]")) return false;
+      if (item.getAttribute("aria-disabled") === "true") return false;
+      return !("disabled" in item && item.disabled);
+    });
+  }
+
+  setActive(item) {
+    this._active = item;
+    for (const candidate of this._items) {
+      if (this.isCombobox) {
+        setManagedAttribute(
+          this._managed,
+          candidate,
+          "aria-selected",
+          String(candidate === item),
+        );
+      } else {
+        restoreAttribute(this._managed, candidate, "aria-selected");
+      }
+    }
+    if (!this._input) return;
+    if (item) {
+      setManagedAttribute(
+        this._managed,
+        this._input,
+        "aria-activedescendant",
+        ensureId(this._managed, item, "zen-option"),
+      );
+      item.scrollIntoView?.({ block: "nearest" });
+    } else {
+      setManagedAttribute(
+        this._managed,
+        this._input,
+        "aria-activedescendant",
+        null,
+      );
+    }
+  }
+
+  moveActive(offset) {
+    const items = this.visibleItems();
+    if (!items.length) {
+      this.setActive(null);
+      return;
+    }
+    const current = items.indexOf(this._active);
+    const index = current < 0
+      ? (offset > 0 ? 0 : items.length - 1)
+      : (current + offset + items.length) % items.length;
+    this.setActive(items[index]);
+  }
+
+  onKeydown(event) {
+    if (event.target !== this._input) return;
+    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
+      event.preventDefault();
+      if (this.isCombobox) this.setOpen(true);
+      this.moveActive(event.key === "ArrowDown" ? 1 : -1);
+      return;
+    }
+    if (event.key === "Enter") {
+      const item = this._active || this.visibleItems()[0];
+      if (!item) return;
+      event.preventDefault();
+      this.choose(item);
+    }
+  }
+
+  onClick(event) {
+    const target = event.target instanceof Element
+      ? event.target.closest(`[role="${this.itemRole}"]`)
+      : null;
+    if (!target || !this._list?.contains(target) || target.hidden) return;
+    if (target.getAttribute("aria-disabled") === "true" ||
+        ("disabled" in target && target.disabled)) return;
+    this.choose(target);
+  }
+
+  valueFor(item) {
+    return item.dataset.value ?? item.getAttribute("value") ??
+      item.textContent.trim();
+  }
+}
+
+/**
+ * Filters and selects native option elements from a text input.
+ *
+ * @extends {FilterableList}
+ */
+export class ZenCombobox extends FilterableList {
+  get isCombobox() {
+    return true;
+  }
+
+  get listRole() {
+    return "listbox";
+  }
+
+  get itemRole() {
+    return "option";
+  }
+
+  setOpen(open) {
+    this._open = Boolean(open);
+    if (!this._input || !this._list) return;
+    setManagedAttribute(
+      this._managed,
+      this._input,
+      "aria-expanded",
+      String(this._open),
+    );
+    setManagedAttribute(
+      this._managed,
+      this._list,
+      "hidden",
+      this._open ? null : "",
+    );
+    if (!this._open) this.setActive(null);
+  }
+
+  onKeydown(event) {
+    if (event.target === this._input && event.key === "Escape") {
+      if (this._open) event.preventDefault();
+      this.setOpen(false);
+      return;
+    }
+    super.onKeydown(event);
+  }
+
+  choose(item) {
+    const value = this.valueFor(item);
+    this._input.value = value;
+    this._input.dispatchEvent(new Event("input", {
+      bubbles: true,
+      composed: true,
+    }));
+    this._input.dispatchEvent(new Event("change", { bubbles: true }));
+    emit(this, "zen-change", { value });
+    this.setOpen(false);
+  }
+}
+
+/**
+ * Filters a keyboard-oriented command menu and emits selected commands.
+ *
+ * @extends {FilterableList}
+ */
+export class ZenCommand extends FilterableList {
+  choose(item) {
+    emit(this, "zen-command", { value: this.valueFor(item) });
+  }
+}
+
+/**
+ * Enhances one native one-time-code input with visual slots.
+ *
+ * @extends {HTMLElement}
+ */
+export class ZenInputOtp extends HTMLElement {
+  connectedCallback() {
+    this._managed ||= new Map();
+    this._complete ??= false;
+    this._onInput ||= event => {
+      if (event.target === this._input) this.update(true);
+    };
+    this.addEventListener("input", this._onInput);
+    if (!this._observer) {
+      this._observer = new MutationObserver(() => this.sync());
+    }
+    this._observer.observe(this, {
+      attributes: true,
+      attributeFilter: ["allow", "maxlength", "pattern"],
+      childList: true,
+      subtree: true,
+    });
+    this.sync();
+  }
+
+  disconnectedCallback() {
+    this._observer?.disconnect();
+    this.removeEventListener("input", this._onInput);
+    const input = this._input;
+    const complete = this._complete;
+    this.release();
+    this._previousInput = input;
+    this._complete = complete;
+  }
+
+  release() {
+    restoreAttributes(this._managed);
+    if (this._slots && this._originalSlots) {
+      this._slots.replaceChildren(...this._originalSlots);
+    }
+    this._input = null;
+    this._slots = null;
+    this._originalSlots = null;
+    this._complete = false;
+  }
+
+  sync() {
+    const inputs = [...this.querySelectorAll("input")];
+    const input = inputs.length === 1 ? inputs[0] : null;
+    const slots = this.querySelector("[data-zen-otp-slots]");
+    if (input !== this._input || slots !== this._slots) {
+      const remainedComplete =
+        (input === this._input || input === this._previousInput) &&
+        this._complete;
+      this.release();
+      this._input = input;
+      this._slots = slots;
+      this._originalSlots = slots ? [...slots.childNodes] : null;
+      this._complete = remainedComplete;
+      this._previousInput = null;
+    }
+    if (!this._input) return;
+
+    if (!this._input.hasAttribute("maxlength")) {
+      setManagedAttribute(this._managed, this._input, "maxlength", "6");
+    }
+    if (this.numericOnly() && !this._input.hasAttribute("inputmode")) {
+      setManagedAttribute(this._managed, this._input, "inputmode", "numeric");
+    } else if (!this.numericOnly()) {
+      restoreAttribute(this._managed, this._input, "inputmode");
+    }
+    this.update(true);
+  }
+
+  numericOnly() {
+    return !this.hasAttribute("allow") &&
+      !this.hasAttribute("pattern") &&
+      !this._input.hasAttribute("allow") &&
+      !this._input.hasAttribute("pattern");
+  }
+
+  length() {
+    const value = Number.parseInt(this._input.getAttribute("maxlength"), 10);
+    return Number.isFinite(value) && value > 0 ? value : 6;
+  }
+
+  update(notify) {
+    const length = this.length();
+    const before = this._input.value;
+    let value = this.numericOnly() ? before.replace(/\D/g, "") : before;
+    value = [...value].slice(0, length).join("");
+    if (value !== before) {
+      const selection = this._input.selectionStart;
+      this._input.value = value;
+      if (selection !== null) {
+        const preceding = before.slice(0, selection);
+        const position = this.numericOnly()
+          ? preceding.replace(/\D/g, "").length
+          : [...preceding].slice(0, length).join("").length;
+        try {
+          this._input.setSelectionRange(position, position);
+        } catch {
+          // Some native input types do not expose text selection.
+        }
+      }
+    }
+    this.renderSlots(value, length);
+
+    const complete = [...value].length === length;
+    if (notify && complete && !this._complete) {
+      emit(this, "zen-complete", { value });
+    }
+    this._complete = complete;
+  }
+
+  renderSlots(value, length) {
+    if (!this._slots) return;
+    const characters = [...value];
+    const current = [...this._slots.children];
+    const unchanged = current.length === length &&
+      current.every((slot, index) =>
+        slot.localName === "span" &&
+        slot.textContent === (characters[index] || ""));
+    if (unchanged) return;
+
+    const fragment = document.createDocumentFragment();
+    for (let index = 0; index < length; index++) {
+      const slot = document.createElement("span");
+      slot.textContent = characters[index] || "";
+      fragment.append(slot);
+    }
+    this._slots.replaceChildren(fragment);
+  }
+}
+
+const definitions = {
+  "zen-calendar": ZenCalendar,
+  "zen-checkbox": ZenCheckbox,
+  "zen-combobox": ZenCombobox,
+  "zen-command": ZenCommand,
+  "zen-date-picker": ZenDatePicker,
+  "zen-form": ZenForm,
+  "zen-input-otp": ZenInputOtp,
+  "zen-radio-group": ZenRadioGroup,
+  "zen-select": ZenSelect,
+  "zen-slider": ZenSlider,
+  "zen-switch": ZenSwitch,
+};
+
+if (globalThis.customElements) {
+  for (const [name, definition] of Object.entries(definitions)) {
+    if (!customElements.get(name)) customElements.define(name, definition);
+  }
+}