← Files DataARCHIVED FILE

templates/data-app/base/src/components/ui.jsx

28.9 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as Dropdown from "@radix-ui/react-dropdown-menu";
import React, { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

import { Icon } from "./Icon.jsx";
import { dropdownModel } from "./dropdown-model.js";
import { useTouchReleaseTrigger } from "./touch-release-trigger.js";

export function Button({ variant = "secondary", className = "", ...props }) {
  return <button type="button" {...props} className={`button ${variant} ${className}`.trim()} />;
}

export function useInputModality(target) {
  useEffect(() => {
    // Focus restoration after a pointer-operated menu must not paint a keyboard ring.
    // Keep this on the document so portalled menus/dialogs share the same modality.
    const root = target ?? document.documentElement;
    const doc = root.ownerDocument;
    const previous = root.getAttribute("data-input-modality");
    const pointer = () => { root.dataset.inputModality = "pointer"; };
    const keyboard = event => {
      if (!event.metaKey && !event.ctrlKey && !event.altKey
        && !["Shift", "Control", "Alt", "Meta"].includes(event.key)) root.dataset.inputModality = "keyboard";
    };
    doc.addEventListener("pointerdown", pointer, true);
    doc.addEventListener("keydown", keyboard, true);
    return () => {
      doc.removeEventListener("pointerdown", pointer, true);
      doc.removeEventListener("keydown", keyboard, true);
      if (previous === null) delete root.dataset.inputModality;
      else root.dataset.inputModality = previous;
    };
  }, [target]);
}

export function Checkbox({ children, checked, onChange, disabled = false, className = "" }) {
  return <label className={`form-checkbox ${className}`.trim()}>
    <span className="form-checkbox-control">
      <input type="checkbox" checked={checked} disabled={disabled} onChange={onChange} />
      <Icon name="checkboxUnchecked" size={20} />
      <Icon name="checkboxChecked" size={20} />
    </span>
    <span>{children}</span>
  </label>;
}

const tooltipWarmUntil = new WeakMap();

export function Tooltip({ children, className = "", portal = false, visible, ...props }) {
  const elementRef = useRef(null);
  useEffect(() => {
    const element = elementRef.current;
    const trigger = element?.closest("[data-delayed-tooltip]");
    if (!trigger) return undefined;
    const doc = trigger.ownerDocument;
    let timer, opened = false;
    const enter = () => {
      clearTimeout(timer);
      const warm = (tooltipWarmUntil.get(doc) ?? 0) > Date.now();
      trigger.toggleAttribute("data-tooltip-warm", warm);
      const value = doc.defaultView.getComputedStyle(trigger).getPropertyValue("--tooltip-delay").trim();
      const delay = warm ? 0 : (parseFloat(value) || 0) * (value.endsWith("ms") ? 1 : 1000);
      timer = setTimeout(() => { opened = true; tooltipWarmUntil.set(doc, Infinity); }, delay);
    };
    const leave = () => {
      clearTimeout(timer);
      if (opened) tooltipWarmUntil.set(doc, Date.now() + 300);
      opened = false;
      trigger.removeAttribute("data-tooltip-warm");
    };
    trigger.addEventListener("pointerenter", enter);
    trigger.addEventListener("pointerleave", leave);
    trigger.addEventListener("focusin", enter);
    trigger.addEventListener("focusout", leave);
    return () => {
      leave();
      trigger.removeEventListener("pointerenter", enter);
      trigger.removeEventListener("pointerleave", leave);
      trigger.removeEventListener("focusin", enter);
      trigger.removeEventListener("focusout", leave);
    };
  }, []);
  const tooltip = (
    <span
      {...props}
      ref={elementRef}
      className={["dashboard-tooltip", className].filter(Boolean).join(" ")}
      data-visible={visible == null ? undefined : String(visible)}
      role="tooltip"
    >
      {children}
    </span>
  );
  return portal && typeof document !== "undefined" ? createPortal(tooltip, document.body) : tooltip;
}

/** Reveal clipped text without adding tooltips to labels that already fit. */
export function TruncatedText({ as: Tag = "span", children, focusOnParent = false, anchorRef, ...props }) {
  const anchor = useRef(null), tip = useRef(null), timer = useRef(null);
  const [text, setText] = useState(null);
  const id = useId();
  const hide = () => { clearTimeout(timer.current); setText(null); };
  const show = () => {
    clearTimeout(timer.current);
    const element = anchor.current;
    if (!element || element.isContentEditable || element.scrollWidth <= element.clientWidth) return;
    timer.current = setTimeout(() => setText(element.textContent), 350);
  };
  useEffect(() => () => clearTimeout(timer.current), []);
  useEffect(() => {
    if (!focusOnParent) return undefined;
    const parent = anchor.current?.parentElement;
    if (!parent) return undefined;
    const dismiss = event => { if (event.key === "Escape") hide(); };
    parent.addEventListener("focus", show);
    parent.addEventListener("blur", hide);
    parent.addEventListener("keydown", dismiss);
    return () => { parent.removeEventListener("focus", show); parent.removeEventListener("blur", hide); parent.removeEventListener("keydown", dismiss); };
  });
  useLayoutEffect(() => {
    if (!text || !tip.current) return;
    const position = () => {
      const bounds = anchor.current.getBoundingClientRect(), tooltip = tip.current;
      const view = anchor.current.ownerDocument.defaultView;
      tooltip.style.maxWidth = `${Math.min(360, view.innerWidth - 24)}px`;
      const size = tooltip.getBoundingClientRect();
      tooltip.style.left = `${Math.max(12, Math.min(view.innerWidth - size.width - 12, bounds.left))}px`;
      tooltip.style.top = `${Math.max(12, bounds.bottom + size.height + 8 < view.innerHeight - 12
        ? bounds.bottom + 8 : bounds.top - size.height - 8)}px`;
    };
    position();
    const view = anchor.current.ownerDocument.defaultView;
    view.addEventListener("resize", hide);
    view.addEventListener("scroll", hide, true);
    return () => { view.removeEventListener("resize", hide); view.removeEventListener("scroll", hide, true); };
  }, [text]);
  return <><Tag {...props} ref={element => { anchor.current = element; anchorRef?.(element); }}
    aria-describedby={!focusOnParent && text ? id : props["aria-describedby"]}
    onPointerEnter={event => { props.onPointerEnter?.(event); show(); }}
    onPointerLeave={event => { props.onPointerLeave?.(event); hide(); }}
    onPointerDown={event => { props.onPointerDown?.(event); hide(); }}
    onFocus={event => { props.onFocus?.(event); show(); }} onBlur={event => { props.onBlur?.(event); hide(); }}
    onKeyDown={event => { props.onKeyDown?.(event); if (event.key === "Escape") hide(); }}>{children}</Tag>
    {text && createPortal(<span ref={tip} id={id} role="tooltip" aria-hidden={focusOnParent || undefined}
      data-tooltip-portal="true" className="info-tooltip truncated-text-tooltip">{text}</span>,
      anchor.current.getRootNode().host ? anchor.current.getRootNode() : anchor.current.ownerDocument.body)}
  </>;
}

export function Menu({ label, trigger, children, align = "end", side = "bottom", contentClassName = "", open, onOpenChange, contentProps = {}, portalContainer }) {
  const [localOpen, setLocalOpen] = useState(false);
  const isOpen = open ?? localOpen;
  const changeOpen = next => { if (open === undefined) setLocalOpen(next); onOpenChange?.(next); };
  const release = useTouchReleaseTrigger(update => changeOpen(typeof update === "function" ? update(isOpen) : update));
  const touchTrigger = React.cloneElement(trigger, {
    onPointerDownCapture: event => { trigger.props.onPointerDownCapture?.(event); release.onPointerDownCapture(event); },
    onPointerMove: event => { trigger.props.onPointerMove?.(event); release.onPointerMove(event); },
    onPointerCancel: event => { trigger.props.onPointerCancel?.(event); release.onPointerCancel(event); },
    onPointerUp: event => { trigger.props.onPointerUp?.(event); release.onPointerUp(event); },
  });
  return (
    <Dropdown.Root modal={false} open={isOpen} onOpenChange={changeOpen}>
      <Dropdown.Trigger asChild>{touchTrigger}</Dropdown.Trigger>
      <Dropdown.Portal container={portalContainer}>
        <Dropdown.Content
          {...contentProps}
          aria-label={label}
          align={align}
          side={side}
          className={["popover", contentClassName].filter(Boolean).join(" ")}
          collisionPadding={12}
          sideOffset={7}
          onCloseAutoFocus={(event) => {
            contentProps.onCloseAutoFocus?.(event);
            if (document.activeElement?.closest?.('[data-permalink-target="true"]')) event.preventDefault();
          }}
        >
          {children}
        </Dropdown.Content>
      </Dropdown.Portal>
    </Dropdown.Root>
  );
}

export function MenuItem({
  icon,
  leading,
  danger = false,
  children,
  subtext,
  onSelect,
  className = "",
  href,
  target,
  rel,
  ...props
}) {
  const itemProps = {
    ...props,
    className: `menu-item${subtext ? " has-subtext" : ""}${danger ? " danger" : ""}${className ? ` ${className}` : ""}`,
  };
  const content = (
    <>
      {leading ?? (icon && <Icon name={icon} />)}
      {subtext ? (
        <span className="menu-item-copy">
          <span className="menu-item-label">{children}</span>
          <span className="menu-item-subtext">{subtext}</span>
        </span>
      ) : (
        <span className="menu-item-label">{children}</span>
      )}
    </>
  );

  if (href)
    return (
      <Dropdown.Item
        asChild
        onSelect={(event) => {
          onSelect?.(event);
          event.preventDefault();
        }}
      >
        <a
          {...itemProps}
          href={href}
          target={target}
          rel={rel ?? "noopener noreferrer"}
          onKeyDownCapture={(event) => {
            if (event.key === "Enter") event.stopPropagation();
          }}
        >
          {content}
        </a>
      </Dropdown.Item>
    );

  return (
    <Dropdown.Item {...itemProps} onSelect={onSelect}>
      {content}
    </Dropdown.Item>
  );
}

export function MenuSub({ icon, leading, label, children, contentClassName = "", triggerClassName = "", portalContainer }) {
  return (
    <Dropdown.Sub>
      <Dropdown.SubTrigger className={["menu-item", triggerClassName].filter(Boolean).join(" ")}>
        {leading ?? (icon && <Icon name={icon} />)}
        <span className="menu-item-label">{label}</span>
        <Icon name="chevronRight" className="menu-chevron" />
      </Dropdown.SubTrigger>
      <Dropdown.Portal container={portalContainer}>
        <Dropdown.SubContent
          aria-label={label}
          className={["popover", "menu-sub-content", contentClassName].filter(Boolean).join(" ")}
          collisionPadding={12}
          sideOffset={6}
        >
          {children}
        </Dropdown.SubContent>
      </Dropdown.Portal>
    </Dropdown.Sub>
  );
}

export function MenuSeparator() {
  return <Dropdown.Separator className="menu-separator" />;
}

export function MenuGroup({ label, children }) {
  return (
    <Dropdown.Group>
      <Dropdown.Label className="menu-group-label">{label}</Dropdown.Label>
      {children}
    </Dropdown.Group>
  );
}

export function InfoTooltip({ label = "More information", children, portalContainer }) {
  const id = useId();
  const triggerRef = useRef(null);
  const tooltipRef = useRef(null);
  const [visible, setVisible] = useState(false);
  const touchRef = useRef(false);
  useEffect(() => {
    if (!visible) return undefined;
    const dismiss = event => {
      if (!triggerRef.current?.contains(event.target)) setVisible(false);
    };
    document.addEventListener("pointerdown", dismiss, true);
    return () => document.removeEventListener("pointerdown", dismiss, true);
  }, [visible]);

  useLayoutEffect(() => {
    if (!visible || typeof window === "undefined") return undefined;
    const trigger = triggerRef.current;
    const wrapper = trigger?.closest(".info-wrap");
    const tooltip = tooltipRef.current;
    if (!trigger || !wrapper || !tooltip) return undefined;

    function positionTooltip() {
      const viewport = { left: 12, right: window.innerWidth - 12 };
      const componentBounds = wrapper.closest("[data-component-id]")?.getBoundingClientRect();
      const componentRange = componentBounds && {
        left: Math.max(viewport.left, componentBounds.left + 12),
        right: Math.min(viewport.right, componentBounds.right - 12),
      };
      const range = componentRange && componentRange.right - componentRange.left >= 180 ? componentRange : viewport;
      tooltip.style.setProperty("--info-tooltip-max-width", `${Math.floor(range.right - range.left)}px`);
      const anchor = trigger.getBoundingClientRect();
      const bounds = tooltip.getBoundingClientRect();
      const above = anchor.top - bounds.height - 8 >= 12;
      const side = above ? "top" : "bottom";
      const center = anchor.left + anchor.width / 2;
      const left = Math.max(range.left, Math.min(range.right - bounds.width, center - bounds.width / 2));
      const preferredTop = above ? anchor.top - bounds.height - 8 : anchor.bottom + 8;
      const top = Math.max(12, Math.min(window.innerHeight - bounds.height - 12, preferredTop));
      wrapper.dataset.tooltipSide = side;
      tooltip.dataset.tooltipSide = side;
      tooltip.style.left = `${left}px`;
      tooltip.style.top = `${top}px`;
      tooltip.style.opacity = "1";
      tooltip.style.visibility = "visible";
    }

    positionTooltip();
    window.addEventListener("resize", positionTooltip);
    window.addEventListener("scroll", positionTooltip, true);
    return () => {
      window.removeEventListener("resize", positionTooltip);
      window.removeEventListener("scroll", positionTooltip, true);
    };
  }, [visible, children]);

  function hideTooltip() {
    if (touchRef.current) return;
    if (triggerRef.current?.matches(":focus")) return;
    setVisible(false);
  }

  return (
    <span className="info-wrap" onPointerEnter={event => {
      if (event.pointerType !== "touch") { touchRef.current = false; setVisible(true); }
    }} onPointerLeave={hideTooltip}>
      <button
        ref={triggerRef}
        type="button"
        className="info"
        aria-label={label}
        aria-describedby={visible ? id : undefined}
        onPointerDown={event => { touchRef.current = event.pointerType === "touch"; }}
        onClick={() => { if (touchRef.current) setVisible(current => !current); }}
        onFocus={() => { if (!touchRef.current) setVisible(true); }}
        onBlur={() => { touchRef.current = false; setVisible(false); }}
        onKeyDown={(event) => {
          touchRef.current = false;
          if (event.key === "Escape") event.currentTarget.blur();
        }}
      >
        <Icon name="info" size={16} />
      </button>
      {visible && typeof document !== "undefined" && createPortal(
        <span ref={tooltipRef} id={id} className="info-tooltip" data-tooltip-portal="true" role="tooltip">
          {children}
        </span>,
        portalContainer ?? document.body,
      )}
    </span>
  );
}

export function Select({
  label,
  value,
  choices,
  onChange,
  showLabel = false,
  allLabel = "All",
  formatChoice,
  triggerClassName = "filter-trigger",
  contentClassName = "",
  align = "start",
  modal = true,
  disabled = false,
  scrollToSelected = false,
  groups,
  multiple = false,
  portalContainer,
}) {
  const { display, displayedValue, isSelected, select } = dropdownModel({ value, multiple, allLabel, formatChoice });
  const [open, setOpen] = useState(false);
  const release = useTouchReleaseTrigger(setOpen);
  const optionGroups = groups ?? [{ choices }];
  const native = !multiple && (
    <span className={`${triggerClassName} select-trigger native-filter-shell`}>
      {showLabel && <span className="filter-label" aria-hidden="true">{label}</span>}
      <span className="select-value" aria-hidden="true">{displayedValue}</span>
      <Icon name="chevronDown" className="chevron" />
      <select className="native-filter-control" aria-label={label}
        value={value ?? optionGroups[0]?.choices[0] ?? ""} disabled={disabled}
        onChange={event => onChange?.(event.currentTarget.value)}
        onPointerDown={event => { event.currentTarget.dataset.pointerFocus = "true"; }}
        onBlur={event => { delete event.currentTarget.dataset.pointerFocus; }}
        onKeyDown={event => { delete event.currentTarget.dataset.pointerFocus; if (event.key === "Escape") event.stopPropagation(); }}>
        {optionGroups.map((group, index) => group.label
          ? <optgroup key={group.label} label={group.label}>{group.choices.map(choice =>
            <option key={choice} value={choice}>{display(choice)}</option>)}</optgroup>
          : <React.Fragment key={index}>{group.choices.map(choice =>
            <option key={choice} value={choice}>{display(choice)}</option>)}</React.Fragment>)}
      </select>
    </span>
  );

  return (
    <>
    {native}
    <span className={multiple ? "custom-select-host" : "custom-select-host has-native-picker"}>
    <Dropdown.Root modal={modal} open={open} onOpenChange={setOpen}>
      <Dropdown.Trigger className={`${triggerClassName} select-trigger`} aria-label={label} disabled={disabled}
        {...release}
        onPointerEnter={event => {
          const valueLabel = event.currentTarget.querySelector(".select-value");
          event.currentTarget.title = valueLabel?.scrollWidth > valueLabel?.clientWidth ? valueLabel.textContent : "";
        }}>
        {showLabel && <span className="filter-label">{label}</span>}
        <span className="select-value">{displayedValue}</span>
        <Icon name="chevronDown" className="chevron" />
      </Dropdown.Trigger>
      <Dropdown.Portal container={portalContainer}>
        <Dropdown.Content
          align={align}
          className={["popover", "select-content", contentClassName].filter(Boolean).join(" ")}
          collisionPadding={12}
          sideOffset={7}
          onOpenAutoFocus={(event) => {
            if (!scrollToSelected) return;
            const content = event.currentTarget;
            requestAnimationFrame(() =>
              content.querySelector('[data-state="checked"]')?.scrollIntoView({ block: "center" }),
            );
          }}
        >
          {multiple ? optionGroups.map((group, index) => (
            <React.Fragment key={group.label ?? index}>
              {group.label && (
                <Dropdown.Label className="menu-group-label select-group-label">{group.label}</Dropdown.Label>
              )}
              {group.choices.map((choice) => {
                const checked = isSelected(choice);
                return <Dropdown.CheckboxItem className="menu-item" key={choice || "none"}
                  checked={checked} onCheckedChange={() => onChange?.(select(choice))}
                  onSelect={(event) => event.preventDefault()}>
                  <span className="menu-item-label">{display(choice)}</span>
                  <Dropdown.ItemIndicator className="menu-check"><Icon name="check" /></Dropdown.ItemIndicator>
                </Dropdown.CheckboxItem>;
              })}
            </React.Fragment>
          )) : <Dropdown.RadioGroup value={value} onValueChange={onChange}>
            {optionGroups.map((group, index) => (
              <React.Fragment key={group.label ?? index}>
                {group.label && (
                  <Dropdown.Label className="menu-group-label select-group-label">{group.label}</Dropdown.Label>
                )}
                {group.choices.map((choice) => (
                  <Dropdown.RadioItem className="menu-item" key={choice || "none"} value={choice}>
                    <span className="menu-item-label">{display(choice)}</span>
                    <Dropdown.ItemIndicator className="menu-check">
                      <Icon name="check" />
                    </Dropdown.ItemIndicator>
                  </Dropdown.RadioItem>
                ))}
              </React.Fragment>
            ))}
          </Dropdown.RadioGroup>}
        </Dropdown.Content>
      </Dropdown.Portal>
    </Dropdown.Root>
    </span>
    </>
  );
}

function tabElementId(tabsId, tabId, kind) {
  return `${tabsId}-${kind}-${tabId}`;
}

// One measured underline moves between tabs; layout changes never rely on guessed widths.
export function TabIndicator({ navRef, value, inset = 0, motion }) {
  const indicatorRef = useRef(null);
  const previous = useRef(null);
  useLayoutEffect(() => {
    const indicator = indicatorRef.current;
    const nav = navRef.current ?? indicator?.parentElement;
    if (!nav || !indicator) return;
    const update = () => {
      const selected = nav.querySelector('[role="tab"][aria-selected="true"]');
      if (!selected) {
        indicator.hidden = true;
        return;
      }
      indicator.hidden = false;
      const bounds = selected.getBoundingClientRect();
      const parent = nav.getBoundingClientRect();
      const leftInset = selected.closest(".dashboard-tab-item") === nav.firstElementChild ? 0 : inset;
      const next = {
        x: bounds.left - parent.left + nav.scrollLeft + leftInset,
        width: Math.max(0, bounds.width - leftInset - inset),
      };
      if (motion) indicator.style.transition = window.matchMedia("(prefers-reduced-motion: reduce)").matches
        ? "none" : `transform ${motion.duration}ms ${motion.easing}, width ${motion.duration}ms ${motion.easing}`;
      indicator.style.width = `${next.width}px`;
      indicator.style.transform = `translateX(${next.x}px)`;
      const old = previous.current;
      if (
        !motion && old &&
        (old.x !== next.x || old.width !== next.width) &&
        !window.matchMedia("(prefers-reduced-motion: reduce)").matches
      ) {
        indicator.getAnimations?.().forEach((animation) => animation.cancel());
        indicator.animate(
          [
            { transform: `translateX(${old.x}px)`, width: `${old.width}px` },
            { transform: `translateX(${next.x}px)`, width: `${next.width}px` },
          ],
          { duration: 180, easing: "cubic-bezier(.22,1,.36,1)" },
        );
      }
      previous.current = next;
    };
    update();
    const observer = new ResizeObserver(update);
    observer.observe(nav);
    for (const tab of nav.querySelectorAll('[role="tab"]')) observer.observe(tab);
    return () => observer.disconnect();
  }, [navRef, value, inset, motion]);
  return <span ref={indicatorRef} className="tab-active-indicator" aria-hidden="true" />;
}

/** Keep tab overflow in its own strip and reveal only edges with hidden tabs. */
export function useTabOverflow(ref, selectionKey, enabled = true) {
  useLayoutEffect(() => {
    const strip = ref.current;
    if (!strip) return undefined;
    if (!enabled) {
      delete strip.dataset.scrollLeft;
      delete strip.dataset.scrollRight;
      return undefined;
    }

    const update = () => {
      const overflow = strip.scrollWidth > strip.clientWidth + 1;
      strip.dataset.scrollLeft = String(overflow && strip.scrollLeft > 1);
      strip.dataset.scrollRight = String(overflow && strip.scrollLeft + strip.clientWidth < strip.scrollWidth - 1);
    };
    const selected = strip.querySelector('[role="tab"][aria-selected="true"]');
    if (selected) {
      const tab = selected.getBoundingClientRect();
      const viewport = strip.getBoundingClientRect();
      if (tab.left < viewport.left) strip.scrollLeft += tab.left - viewport.left - 12;
      else if (tab.right > viewport.right) strip.scrollLeft += tab.right - viewport.right + 12;
    }
    update();
    const observer = new ResizeObserver(update);
    observer.observe(strip);
    for (const tab of strip.querySelectorAll('[role="tab"]')) observer.observe(tab);
    const wheel = event => {
      if (event.ctrlKey || event.metaKey || event.shiftKey || Math.abs(event.deltaX) >= Math.abs(event.deltaY)) return;
      const before = strip.scrollLeft;
      strip.scrollLeft += event.deltaY;
      if (Math.abs(strip.scrollLeft - before) > .5) {
        event.preventDefault();
        update();
      }
    };
    strip.addEventListener("scroll", update, { passive: true });
    strip.addEventListener("wheel", wheel, { passive: false });
    return () => {
      observer.disconnect();
      strip.removeEventListener("scroll", update);
      strip.removeEventListener("wheel", wheel);
    };
  }, [ref, selectionKey, enabled]);
}

export function Tabs({ id, label, items, value, onChange, className = "", variant = "underline", indicatorMotion }) {
  const generatedId = useId();
  const navRef = useRef(null);
  const tabsId = id ?? `tabs-${generatedId}`;
  useTabOverflow(navRef, `${value}:${items.map(({ id, label }) => `${id}:${label}`).join("|")}`);
  return (
    <nav
      ref={navRef}
      className={`tabs ${variant === "underline" ? "source-tabs" : ""} ${className}`.trim()}
      role="tablist"
      aria-label={label}
      aria-orientation="horizontal"
      onKeyDown={(event) => {
        if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key) || !items.length) return;
        event.preventDefault();
        const current = Math.max(
          0,
          items.findIndex((item) => item.id === value),
        );
        const next =
          event.key === "Home"
            ? 0
            : event.key === "End"
              ? items.length - 1
              : (current + (event.key === "ArrowRight" ? 1 : -1) + items.length) % items.length;
        onChange(items[next].id);
        event.currentTarget.querySelectorAll("[role='tab']")[next]?.focus();
      }}
    >
      {items.map((item) => (
        <button
          key={item.id}
          type="button"
          role="tab"
          aria-selected={value === item.id}
          id={tabElementId(tabsId, item.id, "tab")}
          aria-controls={tabElementId(tabsId, item.id, "panel")}
          tabIndex={value === item.id ? 0 : -1}
          onClick={() => onChange(item.id)}
        >
          {item.label}
        </button>
      ))}
      {(variant === "underline" || className.split(" ").includes("source-tabs")) && <TabIndicator navRef={navRef} value={value} motion={indicatorMotion} />}
    </nav>
  );
}

export function TabPanel({ tabsId, tabId, active, className = "", children, keepMounted = false }) {
  if (!active && !keepMounted) return null;
  return (
    <section
      className={`tab-panel ${className}`.trim()}
      role="tabpanel"
      id={tabElementId(tabsId, tabId, "panel")}
      aria-labelledby={tabElementId(tabsId, tabId, "tab")}
      hidden={!active || undefined}
      tabIndex={0}
    >
      {children}
    </section>
  );
}

export function Dialog({
  open = true,
  title,
  titleInfo,
  initialFocusSelector,
  expanded = false,
  className = "",
  style,
  headerActions,
  onClose,
  children,
  showClose = true,
  backdropClassName = "",
  portalContainer,
  contentRef,
  onKeyDown,
}) {
  const returnFocus = useRef(null);
  return (
    <DialogPrimitive.Root
      open={open}
      onOpenChange={(open) => {
        if (!open) onClose();
      }}
    >
      <DialogPrimitive.Portal container={portalContainer}>
        <DialogPrimitive.Overlay className={["dialog-backdrop", backdropClassName].filter(Boolean).join(" ")} />
        <DialogPrimitive.Content
          ref={contentRef}
          onKeyDown={onKeyDown}
          style={style}
          data-shared-dialog
          aria-describedby={undefined}
          onOpenAutoFocus={(event) => {
            returnFocus.current = event.currentTarget.ownerDocument.activeElement;
            if (!initialFocusSelector) return;
            const initial = event.currentTarget.querySelector(initialFocusSelector);
            if (!initial) return;
            event.preventDefault();
            initial.focus({ preventScroll: true });
          }}
          onCloseAutoFocus={(event) => {
            const routeTarget = event.currentTarget.ownerDocument.querySelector('[data-permalink-target="true"]');
            // Navigation owns focus when it moves away from this dialog's opener.
            if (routeTarget && !routeTarget.contains(returnFocus.current)) {
              event.preventDefault();
              routeTarget.focus({ preventScroll: true });
              return;
            }
            if (!returnFocus.current?.isConnected) return;
            event.preventDefault();
            returnFocus.current.focus({ preventScroll: true });
          }}
          className={["dialog", expanded ? "expanded" : "source-dialog", className].filter(Boolean).join(" ")}
        >
          <header className="dialog-header" tabIndex={initialFocusSelector === ".dialog-header" ? -1 : undefined}>
            <div className="dialog-title-group">
              <DialogPrimitive.Title>{title}</DialogPrimitive.Title>
              {titleInfo && <InfoTooltip label="How scheduling works">{titleInfo}</InfoTooltip>}
            </div>
            <div className="dialog-header-actions">
              {headerActions}
              {showClose && (
                <DialogPrimitive.Close className="icon-button" aria-label="Close">
                  <Icon name="cross" size={20} />
                </DialogPrimitive.Close>
              )}
            </div>
          </header>
          <div className="dialog-content">{children}</div>
        </DialogPrimitive.Content>
      </DialogPrimitive.Portal>
    </DialogPrimitive.Root>
  );
}

SHA-256: d07d561a0876bb899eb16ac68a70781a96fcd6a6427c34b1bc023091eaa472c8