/* ===== ui.jsx — primitives ===== */
(function () {
  const { cls, fmtDate, fmtRel, ALARM_META, smartDate } = window;

  /* ---------- Icons (lucide-ish, stroke 1.6) ---------- */
  const I = (path, opts = {}) => (props) => (
    <svg
      width={props.size || 16} height={props.size || 16}
      viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={opts.sw || 1.6}
      strokeLinecap="round" strokeLinejoin="round"
      className={props.className}
      style={props.style}
      aria-hidden="true"
    >{path}</svg>
  );

  const Icon = {
    search:    I(<><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></>),
    plus:      I(<><path d="M12 5v14M5 12h14"/></>),
    chevDown:  I(<><path d="m6 9 6 6 6-6"/></>),
    chevRight: I(<><path d="m9 6 6 6-6 6"/></>),
    chevLeft:  I(<><path d="m15 6-6 6 6 6"/></>),
    x:         I(<><path d="M18 6 6 18M6 6l12 12"/></>),
    check:     I(<><path d="M20 6 9 17l-5-5"/></>),
    edit:      I(<><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5Z"/></>),
    trash:     I(<><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></>),
    filter:    I(<><path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/></>),
    home:      I(<><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2h-4v-7H10v7H6a2 2 0 0 1-2-2V9z"/></>),
    bell:      I(<><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></>),
    box:       I(<><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="m3 8 9 5 9-5"/><path d="M12 13v8"/></>),
    flask:     I(<><path d="M9 3v6L4.5 19a2 2 0 0 0 1.7 3h11.6a2 2 0 0 0 1.7-3L15 9V3"/><path d="M8 3h8"/><path d="M7 15h10"/></>),
    cpu:      I(<><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2"/></>),
    users:     I(<><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>),
    truck:     I(<><path d="M10 17h4V5H2v12h3"/><path d="M14 9h4l4 4v4h-2"/><circle cx="7.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></>),
    building:  I(<><rect x="4" y="2" width="16" height="20" rx="1"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01M12 6h.01M16 6h.01M8 10h.01M12 10h.01M16 10h.01M8 14h.01M12 14h.01M16 14h.01"/></>),
    settings:  I(<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>),
    arrowR:    I(<><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></>),
    arrowL:    I(<><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></>),
    download:  I(<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></>),
    cal:       I(<><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></>),
    pkg:       I(<><path d="M16.5 9.4 7.55 4.24"/><path d="M21 8v8a2 2 0 0 1-1 1.73l-7 4a2 2 0 0 1-2 0l-7-4A2 2 0 0 1 3 16V8a2 2 0 0 1 1-1.73l7-4a2 2 0 0 1 2 0l7 4A2 2 0 0 1 21 8z"/><path d="m3.27 6.96 8.73 5.05 8.73-5.05"/><path d="M12 22.08V12"/></>),
    user:      I(<><circle cx="12" cy="7" r="4"/><path d="M5.5 21a8.38 8.38 0 0 1 13 0"/></>),
    history:   I(<><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></>),
    eye:       I(<><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></>),
    archive:   I(<><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></>),
    moreH:     I(<><circle cx="5" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="19" cy="12" r="1.4" fill="currentColor"/></>, {sw:0}),
    moreV:     I(<><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/></>, {sw:0}),
    sort:      I(<><path d="M3 6h13"/><path d="M3 12h9"/><path d="M3 18h5"/><path d="m17 17 4-4-4-4"/></>),
    alert:     I(<><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></>),
    tag:       I(<><path d="M20.59 13.41 13.42 20.58a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><path d="M7 7h.01"/></>),
    pin:       I(<><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></>),
    refresh:   I(<><path d="M21 12a9 9 0 0 0-15-6.7L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"/><path d="M21 21v-5h-5"/></>),
    tool:      I(<><path d="M14.7 6.3a4 4 0 0 0-5.6 5.6L2 19l3 3 7.1-7.1a4 4 0 0 0 5.6-5.6L14.6 12 12 9.4l2.7-3.1z"/></>),
    inbox:    I(<><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></>),
    layers:   I(<><path d="m12 2 9 5-9 5-9-5 9-5z"/><path d="m3 12 9 5 9-5"/><path d="m3 17 9 5 9-5"/></>),
    sparkle:   I(<><path d="m12 3 1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"/></>),
    keyboard:  I(<><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/></>),
    arrowDownR:I(<><path d="m6 6 12 12"/><path d="M6 18V8h10"/></>),
    cornerR:   I(<><polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></>),
    paperclip: I(<><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></>),
    grid:      I(<><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></>),
    list:      I(<><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></>),
    menu:      I(<><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></>),
    ext:       I(<><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>),
  };

  /* ---------- Button ---------- */
  function Button({ variant = 'default', size = 'md', icon: IconC, iconRight: IconR, children, className, ...rest }) {
    const base = 'inline-flex items-center justify-center font-medium rounded-lg transition-all whitespace-nowrap select-none focus-ring disabled:opacity-40 disabled:pointer-events-none';
    const variants = {
      default: 'bg-ink-900 text-white hover:bg-ink-800 active:bg-ink-950 shadow-sm',
      primary: 'bg-ink-900 text-white hover:bg-ink-800 active:bg-ink-950 shadow-sm',
      secondary:'bg-white text-ink-900 ring-1 ring-ink-200 hover:bg-ink-50 hover:ring-ink-300 active:bg-ink-100',
      ghost:   'text-ink-700 hover:bg-ink-100 hover:text-ink-900',
      danger:  'bg-red-600 text-white hover:bg-red-700 active:bg-red-800 shadow-sm',
      link:    'text-ink-900 hover:underline underline-offset-4',
    };
    const sizes = {
      xs: 'text-xs h-7 px-2.5 gap-1',
      sm: 'text-sm h-8 px-3 gap-1.5',
      md: 'text-sm h-9 px-3.5 gap-1.5',
      lg: 'text-[15px] h-10 px-4 gap-2',
    };
    return (
      <button className={cls(base, variants[variant], sizes[size], className)} {...rest}>
        {IconC && <IconC size={size === 'xs' ? 13 : size === 'sm' ? 14 : 15} />}
        {children}
        {IconR && <IconR size={size === 'xs' ? 13 : size === 'sm' ? 14 : 15} />}
      </button>
    );
  }

  function IconBtn({ icon: IconC, label, size = 'md', variant = 'ghost', className, ...rest }) {
    const sizes = { sm: 'h-7 w-7', md: 'h-8 w-8', lg: 'h-9 w-9' };
    const variants = {
      ghost:   'text-ink-600 hover:bg-ink-100 hover:text-ink-900',
      solid:   'bg-ink-900 text-white hover:bg-ink-800',
      outline: 'ring-1 ring-ink-200 hover:bg-ink-50 text-ink-700',
    };
    return (
      <button title={label} aria-label={label}
        className={cls('inline-flex items-center justify-center rounded-md transition-colors focus-ring', sizes[size], variants[variant], className)} {...rest}>
        <IconC size={size === 'sm' ? 14 : 16} />
      </button>
    );
  }

  /* ---------- Inputs ---------- */
  function Field({ label, required, hint, error, children, className }) {
    return (
      <label className={cls('flex flex-col gap-1.5', className)}>
        {label && (
          <span className="text-xs font-medium text-ink-700 flex items-center gap-1">
            {label}{required && <span className="text-red-600">*</span>}
          </span>
        )}
        {children}
        {hint && !error && <span className="text-xs text-ink-500">{hint}</span>}
        {error && <span className="text-xs text-red-600">{error}</span>}
      </label>
    );
  }

  function Input({ invalid, required, className, leadingIcon: LI, ...rest }) {
    return (
      <div className="relative w-full">
        {LI && <LI size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-400" />}
        <input
          className={cls(
            'w-full h-9 rounded-lg bg-white text-sm text-ink-900 placeholder:text-ink-400',
            'ring-1 transition-shadow focus:outline-none focus:ring-2',
            LI && 'pl-8 pr-3',
            !LI && 'px-3',
            invalid || required
              ? 'ring-red-300 focus:ring-red-500'
              : 'ring-ink-200 focus:ring-ink-900',
            className
          )}
          {...rest}
        />
      </div>
    );
  }

  function Textarea({ invalid, className, rows = 3, ...rest }) {
    return (
      <textarea rows={rows}
        className={cls(
          'w-full rounded-lg bg-white text-sm text-ink-900 placeholder:text-ink-400 px-3 py-2',
          'ring-1 transition-shadow focus:outline-none focus:ring-2',
          invalid ? 'ring-red-300 focus:ring-red-500' : 'ring-ink-200 focus:ring-ink-900',
          className
        )} {...rest} />
    );
  }

  function DateField({ value, onChange, required, ...rest }) {
    const [v, setV] = React.useState(value || '');
    React.useEffect(() => setV(value || ''), [value]);
    const handle = (raw) => {
      setV(raw);
      onChange?.(raw);
    };
    const handleBlur = () => {
      const sd = smartDate(v);
      if (sd !== v) { setV(sd); onChange?.(sd); }
    };
    return (
      <Input
        leadingIcon={Icon.cal}
        value={v}
        onChange={(e) => handle(e.target.value)}
        onBlur={handleBlur}
        placeholder="YYYY-MM-DD（可输入 0901）"
        required={required}
        {...rest}
      />
    );
  }

  /* ---------- SearchableSelect ---------- */
  // options: [{id, label, sub, group, recent: boolean}]
  function SearchableSelect({
    options = [], value, onChange, placeholder = '搜索或选择',
    required, allowCreate, onCreate, recentIds = [], renderOption, className, dropdownClass,
  }) {
    const [open, setOpen] = React.useState(false);
    const [q, setQ] = React.useState('');
    const [hi, setHi] = React.useState(0);
    const ref = React.useRef(null);
    React.useEffect(() => {
      const on = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
      document.addEventListener('mousedown', on);
      return () => document.removeEventListener('mousedown', on);
    }, []);
    const sel = options.find((o) => o.id === value);
    const ql = q.toLowerCase();
    const filtered = options.filter((o) =>
      !q || o.label.toLowerCase().includes(ql) || (o.sub || '').toLowerCase().includes(ql)
    );
    const recents = filtered.filter((o) => recentIds.includes(o.id));
    const others = filtered.filter((o) => !recentIds.includes(o.id));
    const showCreate = allowCreate && q && !options.some((o) => o.label.toLowerCase() === ql);

    return (
      <div ref={ref} className={cls('relative w-full', className)}>
        <button type="button" onClick={() => setOpen((x) => !x)}
          className={cls(
            'w-full h-9 flex items-center justify-between text-sm rounded-lg bg-white text-ink-900 px-3',
            'ring-1 transition-shadow hover:ring-ink-300',
            required && !value ? 'ring-red-300' : 'ring-ink-200',
            open && 'ring-2 ring-ink-900'
          )}>
          <span className={cls('truncate', !sel && 'text-ink-400')}>
            {sel ? sel.label : placeholder}
          </span>
          <Icon.chevDown size={14} className="text-ink-400 ml-2 shrink-0" />
        </button>

        {open && (
          <div className={cls(
            'absolute z-50 mt-1 w-full bg-white rounded-lg shadow-pop ring-1 ring-ink-200 overflow-hidden animate-fadein',
            dropdownClass
          )}>
            <div className="p-2 border-b border-ink-100">
              <Input autoFocus leadingIcon={Icon.search} value={q}
                onChange={(e) => { setQ(e.target.value); setHi(0); }}
                placeholder="搜索..." />
            </div>
            <div className="max-h-[280px] overflow-y-auto py-1">
              {recents.length > 0 && (
                <>
                  <div className="px-3 pt-1.5 pb-1 text-[10px] uppercase tracking-wider text-ink-400 flex items-center gap-1">
                    <Icon.history size={11} /> 最近使用
                  </div>
                  {recents.map((o) => (
                    <OptionRow key={o.id} o={o} selected={o.id === value}
                      onClick={() => { onChange?.(o.id); setOpen(false); setQ(''); }}
                      renderOption={renderOption} />
                  ))}
                  {others.length > 0 && <div className="my-1 border-t border-ink-100" />}
                </>
              )}
              {others.map((o) => (
                <OptionRow key={o.id} o={o} selected={o.id === value}
                  onClick={() => { onChange?.(o.id); setOpen(false); setQ(''); }}
                  renderOption={renderOption} />
              ))}
              {filtered.length === 0 && !showCreate && (
                <div className="px-3 py-6 text-center text-sm text-ink-400">无匹配结果</div>
              )}
              {showCreate && (
                <button type="button"
                  onClick={() => { onCreate?.(q); setOpen(false); setQ(''); }}
                  className="w-full text-left px-3 py-2.5 hover:bg-ink-50 flex items-center gap-2 border-t border-ink-100">
                  <span className="h-6 w-6 rounded-full bg-ink-900 text-white flex items-center justify-center">
                    <Icon.plus size={12} />
                  </span>
                  <span className="text-sm">
                    新建「<span className="font-medium">{q}</span>」
                  </span>
                </button>
              )}
            </div>
          </div>
        )}
      </div>
    );
  }

  function OptionRow({ o, selected, onClick, renderOption }) {
    return (
      <button type="button" onClick={onClick}
        className={cls(
          'w-full text-left px-3 py-2 hover:bg-ink-50 flex items-center gap-2 text-sm',
          selected && 'bg-ink-50'
        )}>
        {renderOption ? renderOption(o) : (
          <>
            <div className="flex-1 min-w-0">
              <div className="truncate text-ink-900">{o.label}</div>
              {o.sub && <div className="truncate text-xs text-ink-500">{o.sub}</div>}
            </div>
            {selected && <Icon.check size={14} className="text-ink-900 shrink-0" />}
          </>
        )}
      </button>
    );
  }

  /* ---------- Card / Section ---------- */
  function Card({ className, children, ...rest }) {
    return <div className={cls('bg-white rounded-xl ring-1 ring-ink-200 shadow-card', className)} {...rest}>{children}</div>;
  }
  function SectionH({ title, hint, right, className }) {
    return (
      <div className={cls('flex items-end justify-between gap-3 mb-3', className)}>
        <div>
          <h2 className="text-base font-semibold text-ink-900 tracking-tight">{title}</h2>
          {hint && <p className="text-xs text-ink-500 mt-0.5">{hint}</p>}
        </div>
        {right}
      </div>
    );
  }

  /* ---------- Badge / Alarm chips ---------- */
  function Badge({ children, tone = 'neutral', className, dot }) {
    const tones = {
      neutral: 'bg-ink-100 text-ink-700 ring-ink-200',
      red:     'bg-red-50 text-red-700 ring-red-200',
      orange:  'bg-orange-50 text-orange-700 ring-orange-200',
      amber:   'bg-amber-50 text-amber-800 ring-amber-200',
      sky:     'bg-sky-50 text-sky-700 ring-sky-200',
      zinc:    'bg-zinc-100 text-zinc-700 ring-zinc-300',
      rose:    'bg-rose-50 text-rose-700 ring-rose-200',
      violet:  'bg-violet-50 text-violet-700 ring-violet-200',
      green:   'bg-emerald-50 text-emerald-700 ring-emerald-200',
      blue:    'bg-blue-50 text-blue-700 ring-blue-200',
    };
    return (
      <span className={cls('inline-flex items-center gap-1 rounded-md text-[11px] px-1.5 py-0.5 ring-1 font-medium', tones[tone], className)}>
        {dot && <span className={cls('h-1.5 w-1.5 rounded-full', dot)} />}
        {children}
      </span>
    );
  }

  function AlarmChip({ type, detail, size = 'sm' }) {
    const m = ALARM_META[type];
    if (!m) return null;
    return (
      <span className={cls('inline-flex items-center gap-1 rounded-md ring-1 font-medium',
        m.tw,
        size === 'sm' ? 'text-[11px] px-1.5 py-0.5' : 'text-xs px-2 py-1')}>
        <span className={cls('h-1.5 w-1.5 rounded-full', m.dot)} />
        {m.label}
        {detail && <span className="opacity-70 font-normal">· {detail}</span>}
      </span>
    );
  }

  /* ---------- Modal ---------- */
  function Modal({ open, onClose, title, children, size = 'md', footer }) {
    React.useEffect(() => {
      if (!open) return;
      const onEsc = (e) => e.key === 'Escape' && onClose?.();
      window.addEventListener('keydown', onEsc);
      const prevOverflow = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => { window.removeEventListener('keydown', onEsc); document.body.style.overflow = prevOverflow; };
    }, [open]);
    if (!open) return null;
    const widths = { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-2xl', xl: 'max-w-4xl' };
    return (
      <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 animate-fadein" onClick={onClose}>
        <div className="absolute inset-0 bg-ink-950/30 backdrop-blur-[2px]"></div>
        <div onClick={(e) => e.stopPropagation()}
          className={cls('relative bg-white rounded-2xl shadow-pop ring-1 ring-ink-200 w-full animate-pop', widths[size])}>
          {title && (
            <div className="flex items-center justify-between px-5 pt-4 pb-3 border-b border-ink-100">
              <h3 className="text-[15px] font-semibold tracking-tight">{title}</h3>
              <IconBtn icon={Icon.x} label="关闭" onClick={onClose} />
            </div>
          )}
          <div className="px-5 py-4">{children}</div>
          {footer && <div className="px-5 py-3 border-t border-ink-100 flex justify-end gap-2 bg-ink-50/40 rounded-b-2xl">{footer}</div>}
        </div>
      </div>
    );
  }

  /* ---------- Drawer (mobile-friendly) ---------- */
  function Drawer({ open, onClose, title, children, side = 'right', width = 420 }) {
    if (!open) return null;
    return (
      <div className="fixed inset-0 z-[100] animate-fadein" onClick={onClose}>
        <div className="absolute inset-0 bg-ink-950/30"></div>
        <div onClick={(e) => e.stopPropagation()}
          className={cls(
            'absolute top-0 bottom-0 bg-white shadow-pop ring-1 ring-ink-200 animate-slidein flex flex-col',
            side === 'right' ? 'right-0' : 'left-0'
          )}
          style={{ width }}>
          {title && (
            <div className="flex items-center justify-between px-4 h-12 border-b border-ink-100">
              <h3 className="text-sm font-semibold">{title}</h3>
              <IconBtn icon={Icon.x} label="关闭" onClick={onClose} />
            </div>
          )}
          <div className="flex-1 overflow-y-auto">{children}</div>
        </div>
      </div>
    );
  }

  /* ---------- Tabs ---------- */
  function Tabs({ tabs, active, onChange, className }) {
    return (
      <div className={cls('flex items-center gap-1 border-b border-ink-200', className)}>
        {tabs.map((t) => (
          <button key={t.id} onClick={() => onChange(t.id)}
            className={cls(
              'px-3 h-9 text-sm font-medium relative transition-colors',
              active === t.id ? 'text-ink-900' : 'text-ink-500 hover:text-ink-800'
            )}>
            {t.label}
            {t.count != null && (
              <span className={cls(
                'ml-1.5 inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1 text-[10px] rounded-full',
                active === t.id ? 'bg-ink-900 text-white' : 'bg-ink-100 text-ink-600'
              )}>{t.count}</span>
            )}
            {active === t.id && <span className="absolute left-0 right-0 -bottom-px h-0.5 bg-ink-900 rounded-full"></span>}
          </button>
        ))}
      </div>
    );
  }

  /* ---------- Table ---------- */
  function Table({ columns, rows, onRow, empty, dense, rowKey = 'id' }) {
    if (!rows || rows.length === 0) {
      return empty || <EmptyState title="暂无数据" />;
    }
    return (
      <div className="overflow-x-auto">
        <table className="w-full border-collapse text-sm">
          <thead>
            <tr className="bg-ink-50/60">
              {columns.map((c, i) => (
                <th key={i} style={{ width: c.width }}
                  className={cls('text-left text-[11px] uppercase tracking-wider text-ink-500 font-medium px-3 py-2 border-b border-ink-200',
                    c.align === 'right' && 'text-right', c.align === 'center' && 'text-center'
                  )}>
                  {c.header}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((r, ri) => (
              <tr key={r[rowKey] ?? ri}
                onClick={() => onRow?.(r)}
                className={cls(
                  'border-b border-ink-100 group',
                  onRow && 'cursor-pointer hover:bg-ink-50'
                )}>
                {columns.map((c, i) => (
                  <td key={i} className={cls('px-3 align-middle', dense ? 'py-2' : 'py-3',
                    c.align === 'right' && 'text-right', c.align === 'center' && 'text-center')}>
                    {c.cell ? c.cell(r) : r[c.key]}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  /* ---------- EmptyState ---------- */
  function EmptyState({ icon: IconC = Icon.inbox, title = '空空如也', hint, action, compact }) {
    return (
      <div className={cls('flex flex-col items-center justify-center text-center', compact ? 'py-8' : 'py-16')}>
        <div className="h-12 w-12 rounded-full bg-ink-100 text-ink-400 flex items-center justify-center mb-3">
          <IconC size={22} />
        </div>
        <div className="text-sm font-medium text-ink-800">{title}</div>
        {hint && <div className="text-xs text-ink-500 mt-1 max-w-xs">{hint}</div>}
        {action && <div className="mt-4">{action}</div>}
      </div>
    );
  }

  /* ---------- Skeleton ---------- */
  function Skeleton({ className }) {
    return <div className={cls('bg-ink-100 rounded animate-pulse', className)} />;
  }

  /* ---------- Avatar ---------- */
  function Avatar({ name, size = 28, tone }) {
    const tones = ['bg-ink-200 text-ink-700', 'bg-amber-100 text-amber-800', 'bg-sky-100 text-sky-700', 'bg-emerald-100 text-emerald-700', 'bg-violet-100 text-violet-700', 'bg-rose-100 text-rose-700'];
    const idx = (name || '').charCodeAt(0) % tones.length;
    return (
      <span style={{ width: size, height: size, fontSize: size * 0.42 }}
        className={cls('inline-flex items-center justify-center rounded-full font-semibold', tones[idx])}>
        {name?.slice(0, 2)}
      </span>
    );
  }

  /* ---------- Kbd ---------- */
  function Kbd({ children, className }) {
    return (
      <kbd className={cls('inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 text-[10px] font-mono font-medium text-ink-600 bg-ink-100 ring-1 ring-ink-200 rounded shadow-[0_1px_0_0_#d4d4d8]', className)}>
        {children}
      </kbd>
    );
  }

  /* ---------- Toasts ---------- */
  function ToastHost() {
    const { toasts } = window.useStore();
    return (
      <div className="fixed bottom-4 right-4 z-[200] flex flex-col gap-2 items-end">
        {toasts.map((t) => (
          <div key={t.id}
            className={cls(
              'rounded-lg shadow-pop ring-1 px-3.5 py-2.5 text-sm bg-white animate-pop max-w-sm',
              t.kind === 'error' ? 'ring-red-200 text-red-800' : 'ring-ink-200 text-ink-900'
            )}>
            <div className="flex items-start gap-2">
              {t.kind === 'error'
                ? <Icon.alert size={16} className="mt-0.5 text-red-600 shrink-0" />
                : <Icon.check size={16} className="mt-0.5 text-emerald-600 shrink-0" />}
              <span>{t.text}</span>
            </div>
          </div>
        ))}
      </div>
    );
  }

  /* ---------- Divider w/ text ---------- */
  function DividerText({ children }) {
    return (
      <div className="flex items-center gap-3 my-3">
        <div className="flex-1 h-px bg-ink-200"></div>
        <span className="text-[10px] uppercase tracking-wider text-ink-400">{children}</span>
        <div className="flex-1 h-px bg-ink-200"></div>
      </div>
    );
  }

  Object.assign(window, {
    Icon, Button, IconBtn, Field, Input, Textarea, DateField,
    SearchableSelect, Card, SectionH, Badge, AlarmChip,
    Modal, Drawer, Tabs, Table, EmptyState, Skeleton, Avatar, Kbd,
    ToastHost, DividerText,
  });
})();
