// ThaiHao atoms — Button, Field primitives, Choice, Uploader, etc.
// All use design tokens. No hardcoded colors.

const { useState, useRef, useEffect, useId, useCallback } = React;

// ===== Button =====
function Button({
  variant = "primary",
  size = "default",
  icon,
  iconRight,
  children,
  className = "",
  ...rest
}) {
  const sizeClass = size === "sm" ? "btn--sm" : size === "lg" ? "btn--lg" : "";
  return (
    <button
      type="button"
      className={`btn btn--${variant} ${sizeClass} ${className}`}
      {...rest}
    >
      {icon && <i className={`ti ti-${icon}`} aria-hidden="true"></i>}
      {children && <span>{children}</span>}
      {iconRight && <i className={`ti ti-${iconRight}`} aria-hidden="true"></i>}
    </button>
  );
}

// ===== IconButton (icon only, with required aria-label) =====
function IconButton({ icon, label, className = "", ...rest }) {
  return (
    <button
      type="button"
      className={`btn btn--icon ${className}`}
      aria-label={label}
      title={label}
      {...rest}
    >
      <i className={`ti ti-${icon}`} aria-hidden="true"></i>
    </button>
  );
}

// ===== FieldLabel — label row above input =====
function FieldLabel({ children, htmlFor, required, icon, optional, recommended, t }) {
  return (
    <div className="field-label-row">
      <label className="field-label" htmlFor={htmlFor}>
        {icon && (
          <span className="label-with-icon">
            <i className={`ti ti-${icon}`} aria-hidden="true"></i>
            {children}
          </span>
        )}
        {!icon && children}
      </label>
      {required ? (
        <span className="field-required">{t.required}</span>
      ) : recommended ? (
        <span className="field-required">{t.recommended}</span>
      ) : optional ? (
        <span className="field-required">{t.optional}</span>
      ) : null}
    </div>
  );
}

// ===== FieldHelpOrError =====
function FieldHelpOrError({ help, error }) {
  if (error) {
    return (
      <div className="field-error" role="alert">
        <i className="ti ti-alert-circle" aria-hidden="true"></i>
        <span>{error}</span>
      </div>
    );
  }
  if (help) return <div className="field-help">{help}</div>;
  return null;
}

// ===== Field — vertical stack of label + input + help/error =====
function Field({
  label,
  required,
  optional,
  recommended,
  icon,
  help,
  error,
  htmlFor,
  children,
  t,
}) {
  return (
    <div>
      {label && (
        <FieldLabel
          htmlFor={htmlFor}
          required={required}
          optional={optional}
          recommended={recommended}
          icon={icon}
          t={t}
        >
          {label}
        </FieldLabel>
      )}
      {children}
      <FieldHelpOrError help={help} error={error} />
    </div>
  );
}

// ===== TextInput =====
function TextInput({ value, onChange, onBlur, error, mono, rightAlign, ...rest }) {
  return (
    <input
      type="text"
      className={`field-input ${mono ? "is-mono" : ""} ${rightAlign ? "is-right" : ""} ${error ? "is-error" : ""}`}
      value={value || ""}
      onChange={(e) => onChange(e.target.value)}
      onBlur={onBlur}
      {...rest}
    />
  );
}

// ===== Textarea =====
function Textarea({ value, onChange, onBlur, error, rows = 4, maxLength, showCounter, ...rest }) {
  const count = (value || "").length;
  const over = maxLength && count > maxLength;
  return (
    <div>
      <textarea
        className={`field-textarea ${error ? "is-error" : ""}`}
        value={value || ""}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        rows={rows}
        maxLength={maxLength}
        {...rest}
      />
      {showCounter && maxLength != null && (
        <div className={`char-counter ${over ? "is-over" : ""}`}>
          {count} / {maxLength}
        </div>
      )}
    </div>
  );
}

// ===== Select =====
function Select({ value, onChange, onBlur, error, options, placeholder, ...rest }) {
  return (
    <select
      className={`field-select ${error ? "is-error" : ""}`}
      value={value ?? ""}
      onChange={(e) => onChange(e.target.value)}
      onBlur={onBlur}
      {...rest}
    >
      {placeholder !== undefined && <option value="" disabled hidden>{placeholder}</option>}
      {options.map((opt) => {
        const o = typeof opt === "string" ? { value: opt, label: opt } : opt;
        return <option key={o.value} value={o.value}>{o.label}</option>;
      })}
    </select>
  );
}

// ===== InputWithPrefix (e.g. +66 phone) =====
function InputWithPrefix({ prefix, value, onChange, onBlur, error, mono, ...rest }) {
  return (
    <div className={`input-with-prefix ${error ? "is-error" : ""}`}>
      <span className="input-prefix">{prefix}</span>
      <input
        type="text"
        className={`field-input ${mono ? "is-mono" : ""}`}
        value={value || ""}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        {...rest}
      />
    </div>
  );
}

// ===== Choice (radio / checkbox row) =====
function Choice({ kind = "checkbox", checked, onChange, children, sub, name, value }) {
  const id = useId();
  return (
    <label className={`choice ${checked ? "is-checked" : ""}`} htmlFor={id}>
      <input
        id={id}
        type={kind === "radio" ? "radio" : "checkbox"}
        checked={!!checked}
        onChange={(e) => onChange(kind === "radio" ? value : e.target.checked)}
        name={name}
        value={value}
      />
      <span className={`choice-box ${kind === "radio" ? "radio" : "check"}`}>
        <span className="mark">{kind === "radio" ? null : <i className="ti ti-check" style={{fontSize:11}}></i>}</span>
      </span>
      <span style={{flex:1, minWidth:0}}>
        <span className="choice-text">{children}</span>
        {sub && <span className="choice-sub">{sub}</span>}
      </span>
    </label>
  );
}

// ===== ChoiceGrid — set of choices laid out in auto grid =====
function ChoiceGrid({ options, value, onChange, multi, columns }) {
  const isChecked = (opt) => multi ? (Array.isArray(value) && value.includes(opt)) : value === opt;
  const handle = (opt, checked) => {
    if (multi) {
      const cur = Array.isArray(value) ? value : [];
      onChange(checked ? [...cur, opt] : cur.filter((v) => v !== opt));
    } else {
      onChange(opt);
    }
  };
  const className = columns === 2 ? "choice-grid-2" : "choice-grid";
  return (
    <div className={className}>
      {options.map((opt) => {
        const label = typeof opt === "string" ? opt : opt.label;
        const v = typeof opt === "string" ? opt : opt.value;
        return (
          <Choice
            key={v}
            kind={multi ? "checkbox" : "radio"}
            value={v}
            checked={isChecked(v)}
            onChange={(next) => handle(v, multi ? next : true)}
          >
            {label}
          </Choice>
        );
      })}
    </div>
  );
}

// ===== Seg (segmented radio for yes/no/etc.) =====
function Seg({ options, value, onChange }) {
  return (
    <div className="seg-radio" role="radiogroup">
      {options.map((opt) => {
        const o = typeof opt === "string" ? { value: opt, label: opt } : opt;
        return (
          <button
            key={o.value}
            type="button"
            role="radio"
            aria-checked={value === o.value}
            className={value === o.value ? "is-active" : ""}
            onClick={() => onChange(o.value)}
          >
            {o.label}
          </button>
        );
      })}
    </div>
  );
}

// ===== TrustNote =====
function TrustNote({ children }) {
  return (
    <div className="trust-note">
      <i className="ti ti-shield-lock" aria-hidden="true"></i>
      <span className="trust-note-text">{children}</span>
    </div>
  );
}

// ===== Uploader =====
function bytesFmt(n) {
  if (n == null) return "";
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}

function Uploader({ accept, multiple, onAdd, dropLabel, helpLabel, icon = "upload" }) {
  const [drag, setDrag] = useState(false);
  const inputRef = useRef(null);

  const onFiles = (fileList) => {
    if (!fileList) return;
    const files = Array.from(fileList).map((f) => {
      const isImage = (f.type || "").startsWith("image/");
      return {
        id: `${f.name}_${f.size}_${Math.random().toString(36).slice(2, 7)}`,
        name: f.name,
        size: f.size,
        type: f.type,
        url: isImage ? URL.createObjectURL(f) : null,
        file: f,
      };
    });
    onAdd(files);
  };

  return (
    <div
      className={`uploader ${drag ? "is-dragging" : ""}`}
      role="button"
      tabIndex={0}
      onClick={() => inputRef.current && inputRef.current.click()}
      onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current && inputRef.current.click(); } }}
      onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
      onDragLeave={() => setDrag(false)}
      onDrop={(e) => { e.preventDefault(); setDrag(false); onFiles(e.dataTransfer.files); }}
    >
      <i className={`ti ti-${icon}`} aria-hidden="true"></i>
      <p className="body-md">{dropLabel}</p>
      {helpLabel && <p className="body-sm">{helpLabel}</p>}
      <input
        ref={inputRef}
        type="file"
        style={{ display: "none" }}
        accept={accept}
        multiple={multiple}
        onChange={(e) => { onFiles(e.target.files); e.target.value = ""; }}
      />
    </div>
  );
}

function fileIconFor(type, name) {
  const ext = (name || "").split(".").pop().toLowerCase();
  if ((type && type.includes("pdf")) || ext === "pdf") return "file-type-pdf";
  if ((type && type.startsWith("image/")) || ["png","jpg","jpeg","svg","webp"].includes(ext)) return "photo";
  return "file";
}

function UploadedFile({ file, onRemove }) {
  return (
    <div className="upload-file">
      <div className="upload-file-icon">
        <i className={`ti ti-${fileIconFor(file.type, file.name)}`} aria-hidden="true" style={{fontSize: 18}}></i>
      </div>
      <div className="upload-file-info">
        <div className="upload-file-name">{file.name}</div>
        <div className="upload-file-meta">{bytesFmt(file.size)}</div>
      </div>
      <div className="upload-file-actions">
        <IconButton icon="x" label="ลบไฟล์" onClick={onRemove} />
      </div>
    </div>
  );
}

// ===== ImageGrid (Step 4 product photos) — real thumbnails + drag-to-reorder =====
function ImageGrid({ images, onAdd, onRemove, onReorder, max = 10 }) {
  const inputRef = useRef(null);
  const [dragId, setDragId] = useState(null);
  const [overId, setOverId] = useState(null);

  const handle = (fileList) => {
    if (!fileList) return;
    const next = Array.from(fileList).map((f) => ({
      id: `${f.name}_${f.size}_${Math.random().toString(36).slice(2, 7)}`,
      name: f.name,
      size: f.size,
      type: f.type,
      url: URL.createObjectURL(f),
      file: f,
    }));
    onAdd(next);
  };
  const canAdd = images.length < max;

  const onDragStart = (e, id) => {
    setDragId(id);
    e.dataTransfer.effectAllowed = "move";
    try { e.dataTransfer.setData("text/plain", id); } catch (_) {}
  };
  const onDragOver = (e, id) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    if (overId !== id) setOverId(id);
  };
  const onDrop = (e, targetId) => {
    e.preventDefault();
    if (!dragId || dragId === targetId) { setDragId(null); setOverId(null); return; }
    const from = images.findIndex((i) => i.id === dragId);
    const to = images.findIndex((i) => i.id === targetId);
    if (from < 0 || to < 0) { setDragId(null); setOverId(null); return; }
    if (onReorder) onReorder(from, to);
    setDragId(null);
    setOverId(null);
  };
  const onDragEnd = () => { setDragId(null); setOverId(null); };

  return (
    <div>
      <div className="thumb-grid">
        {images.map((img, idx) => (
          <div
            className={`thumb ${dragId === img.id ? "is-dragging" : ""} ${overId === img.id && dragId && dragId !== img.id ? "is-drag-over" : ""}`}
            key={img.id}
            draggable={!!onReorder}
            onDragStart={(e) => onDragStart(e, img.id)}
            onDragOver={(e) => onDragOver(e, img.id)}
            onDrop={(e) => onDrop(e, img.id)}
            onDragEnd={onDragEnd}
          >
            {img.url ? (
              <img src={img.url} alt={img.name} draggable={false} />
            ) : (
              <span style={{
                position: "absolute", inset: 0,
                display: "grid", placeItems: "center",
                color: "var(--on-surface-variant)",
                background: "linear-gradient(135deg, var(--surface-container) 0%, var(--surface-container-high) 100%)"
              }}>
                <i className="ti ti-photo" style={{fontSize: 22}}></i>
              </span>
            )}
            <span className="thumb-num">{String(idx + 1).padStart(2, "0")}</span>
            {idx === 0 && <span className="thumb-hero-badge">หลัก</span>}
            <button
              type="button"
              className="thumb-remove"
              onClick={(e) => { e.stopPropagation(); onRemove(img.id); }}
              aria-label="ลบรูปภาพ"
            >
              <i className="ti ti-x" aria-hidden="true"></i>
            </button>
          </div>
        ))}
        {canAdd && (
          <button
            type="button"
            className="thumb-empty"
            onClick={() => inputRef.current && inputRef.current.click()}
            aria-label="เพิ่มรูปภาพ"
          >
            <i className="ti ti-plus"></i>
          </button>
        )}
      </div>
      <input
        ref={inputRef}
        type="file"
        style={{ display: "none" }}
        accept="image/*"
        multiple
        onChange={(e) => { handle(e.target.files); e.target.value = ""; }}
      />
    </div>
  );
}

// ===== Combobox (typeahead select) =====
function Combobox({ value, onChange, onBlur, options, placeholder, error, id, emptyLabel = "ไม่พบรายการ" }) {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [focusIdx, setFocusIdx] = useState(0);
  const wrapRef = useRef(null);
  const inputRef = useRef(null);
  const listRef = useRef(null);

  const display = open ? query : (value || "");
  const norm = (s) => (s || "").toLowerCase();
  const filtered = !open
    ? options
    : options.filter((o) => {
        const lbl = typeof o === "string" ? o : o.label;
        return norm(lbl).includes(norm(query));
      });

  useEffect(() => {
    const h = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) {
        setOpen(false);
        if (onBlur) onBlur();
      }
    };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [onBlur]);

  const choose = (opt) => {
    const v = typeof opt === "string" ? opt : opt.value;
    onChange(v);
    setOpen(false);
    setQuery("");
    if (onBlur) onBlur();
  };

  const onKey = (e) => {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      if (!open) setOpen(true);
      setFocusIdx((i) => Math.min(filtered.length - 1, i + 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setFocusIdx((i) => Math.max(0, i - 1));
    } else if (e.key === "Enter") {
      e.preventDefault();
      if (open && filtered[focusIdx]) choose(filtered[focusIdx]);
    } else if (e.key === "Escape") {
      setOpen(false);
    }
  };

  // Highlight matched substring
  const renderLabel = (label) => {
    if (!open || !query) return label;
    const i = norm(label).indexOf(norm(query));
    if (i < 0) return label;
    return (
      <span>
        {label.slice(0, i)}
        <mark>{label.slice(i, i + query.length)}</mark>
        {label.slice(i + query.length)}
      </span>
    );
  };

  return (
    <div className="combobox" ref={wrapRef}>
      <input
        id={id}
        ref={inputRef}
        type="text"
        role="combobox"
        aria-expanded={open}
        aria-controls={`${id}-list`}
        autoComplete="off"
        className={`field-input ${error ? "is-error" : ""}`}
        placeholder={placeholder}
        value={display}
        onFocus={() => { setOpen(true); setFocusIdx(0); }}
        onChange={(e) => { setQuery(e.target.value); setOpen(true); setFocusIdx(0); }}
        onKeyDown={onKey}
        style={{ paddingRight: value && !open ? 36 : 12 }}
      />
      {value && !open && (
        <button
          type="button"
          className="combobox-clear"
          aria-label="ล้างค่า"
          onClick={() => { onChange(""); setQuery(""); inputRef.current && inputRef.current.focus(); }}
        >
          <i className="ti ti-x" style={{fontSize: 14}}></i>
        </button>
      )}
      {open && (
        <div id={`${id}-list`} className="combobox-list" role="listbox" ref={listRef}>
          {filtered.length === 0 ? (
            <div className="combobox-empty">{emptyLabel}</div>
          ) : (
            filtered.map((opt, i) => {
              const lbl = typeof opt === "string" ? opt : opt.label;
              return (
                <div
                  key={typeof opt === "string" ? opt : opt.value}
                  role="option"
                  aria-selected={focusIdx === i}
                  className={`combobox-opt ${focusIdx === i ? "is-focused" : ""}`}
                  onMouseEnter={() => setFocusIdx(i)}
                  onMouseDown={(e) => { e.preventDefault(); choose(opt); }}
                >
                  <span>{renderLabel(lbl)}</span>
                  {(value === (typeof opt === "string" ? opt : opt.value)) && (
                    <i className="ti ti-check" style={{color: "var(--thaihao-primary)", fontSize: 14}}></i>
                  )}
                </div>
              );
            })
          )}
        </div>
      )}
    </div>
  );
}

// ===== Modal =====
function Modal({ title, onClose, children, footer }) {
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  return (
    <div className="modal-backdrop" role="dialog" aria-modal="true" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h2 className="title-sm">{title}</h2>
          <IconButton icon="x" label="ปิด" onClick={onClose} />
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

// ===== Validators =====
const Validators = {
  required: (v) => (v == null || v === "" || (Array.isArray(v) && v.length === 0)) ? "required" : null,
  tax13: (v) => {
    const digits = (v || "").replace(/\D/g, "");
    return digits.length === 13 ? null : "tax";
  },
  phoneTH: (v) => {
    const digits = (v || "").replace(/\D/g, "");
    // accept 8 or 9 digits after stripping leading 0
    const stripped = digits.replace(/^0+/, "");
    return stripped.length === 9 || digits.length === 9 ? null : "phone";
  },
  email: (v) => {
    if (!v) return null;
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : "email";
  },
  url: (v) => {
    if (!v) return null;
    return /^https?:\/\//.test(v) ? null : "url";
  },
  year: (v) => {
    if (!v) return null;
    const n = parseInt(v, 10);
    if (!Number.isFinite(n)) return "year";
    const cur = new Date().getFullYear();
    const inCE = n >= 1900 && n <= cur;          // ค.ศ.
    const inBE = n >= 2443 && n <= cur + 543;    // พ.ศ. (1900 CE = 2443 BE)
    return (inCE || inBE) ? null : "year";
  },
};

// ===== Formatters =====
const Format = {
  tax13: (v) => {
    const d = (v || "").replace(/\D/g, "").slice(0, 13);
    // X-XXXX-XXXXX-XX-X
    const a = d.slice(0, 1);
    const b = d.slice(1, 5);
    const c = d.slice(5, 10);
    const e = d.slice(10, 12);
    const f = d.slice(12, 13);
    return [a, b, c, e, f].filter(Boolean).join("-");
  },
  phoneTH: (v) => {
    const d = (v || "").replace(/\D/g, "").slice(0, 10);
    // 0X-XXXX-XXXX
    const stripped = d.startsWith("0") ? d.slice(1) : d;
    const limited = stripped.slice(0, 9);
    const a = limited.slice(0, 1);
    const b = limited.slice(1, 5);
    const c = limited.slice(5, 9);
    return `0${a}${b ? "-" + b : ""}${c ? "-" + c : ""}`;
  },
  money: (v) => {
    if (v === "" || v == null) return "";
    const n = Number((v || "").toString().replace(/[^0-9.]/g, ""));
    if (isNaN(n)) return "";
    return n.toLocaleString("en-US");
  },
  moneyRaw: (v) => (v || "").toString().replace(/[^0-9.]/g, ""),
  timestamp: (lang) => {
    const d = new Date();
    if (lang === "th") {
      const months = ["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];
      const day = d.getDate();
      const m = months[d.getMonth()];
      const y = d.getFullYear() + 543;
      const hh = String(d.getHours()).padStart(2,"0");
      const mm = String(d.getMinutes()).padStart(2,"0");
      return `${day} ${m} ${y} ${hh}:${mm} ICT`;
    } else {
      const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
      return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()} ${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")} ICT`;
    }
  },
};

// ===== Save pill =====
function SavePill({ state, when, t }) {
  if (state === "saving") {
    return (
      <span className="save-pill is-saving" aria-live="polite">
        <i className="ti ti-cloud-upload"></i>
        <span>{t.save_saving}</span>
      </span>
    );
  }
  if (state === "saved") {
    return (
      <span className="save-pill is-saved" aria-live="polite">
        <i className="ti ti-cloud-check"></i>
        <span>{t.save_saved(when)}</span>
      </span>
    );
  }
  return null;
}

// ===== Language toggle =====
function LanguageToggle({ lang, onChange }) {
  return (
    <div className="lang-toggle" role="radiogroup" aria-label="Language">
      <button
        type="button" role="radio" aria-checked={lang === "th"}
        className={lang === "th" ? "is-active" : ""}
        onClick={() => onChange("th")}
      >
        <span className="lang-flag" aria-hidden="true">🇹🇭</span>
        <span>TH</span>
      </button>
      <button
        type="button" role="radio" aria-checked={lang === "en"}
        className={lang === "en" ? "is-active" : ""}
        onClick={() => onChange("en")}
      >
        <span className="lang-flag" aria-hidden="true">🇬🇧</span>
        <span>EN</span>
      </button>
    </div>
  );
}

// ===== Step indicator =====
function StepIndicator({ steps, current, completed, onJump, t }) {
  return (
    <>
      <ol className="stepper" aria-label="Wizard progress">
        {steps.map((s, i) => {
          const isActive = i === current;
          const isDone = completed.has(i);
          const canJump = isDone || i <= current;
          return (
            <li
              key={s.key}
              className={`stepper-seg ${isActive ? "is-active" : ""} ${isDone ? "is-done" : ""}`}
              aria-current={isActive ? "step" : undefined}
            >
              <button
                type="button"
                disabled={!canJump}
                onClick={() => canJump && onJump(i)}
                style={{
                  background: "transparent", border: "none", padding: 0, cursor: canJump ? "pointer" : "default",
                  textAlign: "left", display: "flex", flexDirection: "column", gap: 4,
                  color: "inherit",
                }}
              >
                <span className="stepper-num">
                  {isDone ? <i className="ti ti-check" aria-hidden="true"></i> : <span>{String(i + 1).padStart(2, "0")}</span>}
                </span>
                <span className="stepper-label">{s.label}</span>
              </button>
            </li>
          );
        })}
      </ol>
      <div className="stepper-mobile">
        <div className="stepper-mobile-head">
          <span className="label-caps">{t.step_of(current + 1, steps.length)}</span>
          <span className="title-sm">{steps[current].label}</span>
        </div>
        <div className="stepper-mobile-bar"><span style={{ width: `${((current + 1) / steps.length) * 100}%` }}></span></div>
      </div>
    </>
  );
}

// Expose to global scope so other Babel scripts can use them
Object.assign(window, {
  Button, IconButton, FieldLabel, FieldHelpOrError, Field,
  TextInput, Textarea, Select, InputWithPrefix, Combobox,
  Choice, ChoiceGrid, Seg,
  TrustNote, Uploader, UploadedFile, ImageGrid, Modal,
  Validators, Format, bytesFmt,
  SavePill, LanguageToggle, StepIndicator,
});
