// kova/kraft-gallery.jsx — KRAFT · Designer gallery (split: 3D · filters)
// ----------------------------------------------------------------
// Designer feedback: "It's missing gallery. It should contain and split
// 3D and filters gallery."
//
// The chat surface (kraft.jsx) is conversational — great for generating,
// poor for *curating*. After a few sessions, designs are buried in the
// thread. This adds a second view on the same KRAFT page: a workspace-wide
// GALLERY that aggregates every design and splits it into:
//   • Filters (2D concepts) — browse all flat renders with a filter rail
//     (metal / motif / stone / weight / status), search, sort, multi-select.
//   • 3D models — the subset that has been converted to 3D, shown in a
//     rotatable preview (faux-3D placeholder; a real model-viewer swaps in
//     when GET /api/kraft/designs/:id/model returns a GLTF/USDZ url).
//
// Non-breaking: this is an additive view behind a Chat | Gallery toggle.
// Default stays Chat. Single source of truth = the same `allTurns` the chat
// holds, so chat-generated designs appear in the gallery instantly and the
// launch-pack (which reads `starred`) stays consistent.
//
// Atomic discipline: reuses Chip, Seg, Checkbox, Search, Skeleton, Card,
// EmptyState, Drawer, Btn, IconBtn, Icon. New presentational pieces here:
// ThreeDStage, GalleryCard, GalleryFilterRail, GalleryToolbar,
// GallerySelectionBar, DesignInspectPanel — all resolve to tokens.css.
// ----------------------------------------------------------------

const {
  Btn, IconBtn, Chip, Icon, Card, Seg, Checkbox, Search, Skeleton,
  EmptyState, Drawer, ConfidenceMeter,
} = window.K;

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

// TileSvg is exported by kraft.jsx; fall back defensively.
const KTileSvg = (props) => (window.K.TileSvg ? window.K.TileSvg(props) : null);


/* ============================================================
   METADATA DERIVATION
   Designs in chat carry only { id, tag, hue, starred }. The gallery needs
   filterable facets. We derive them deterministically from tag + hue + id
   so there is no second source of truth to drift.
   ============================================================ */
const hashStr = (s) => { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return h; };
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);

const deriveMetal = (tag, hue) => {
  const t = tag.toLowerCase();
  if (/white/.test(t)) return 'White gold';
  if (/rose/.test(t)) return 'Rose gold';
  if (/platinum/.test(t)) return 'Platinum';
  if (/22k/.test(t)) return '22k yellow';
  if (/18k/.test(t)) return '18k yellow';
  if (hue >= 120) return 'White gold';   // emerald/green settings read as white metal
  if (hue <= 16) return 'Rose gold';
  if (hue <= 44) return '22k yellow';
  return '18k yellow';
};

const MOTIF_MAP = [
  [/peacock/, 'Peacock'], [/paisley|mango/, 'Paisley'], [/floral|petal|flower/, 'Floral'],
  [/halo/, 'Halo'], [/solitaire|floating/, 'Solitaire'], [/signet|monogram|face/, 'Signet'],
  [/bezel/, 'Bezel'], [/pav[eé]|milgrain/, 'Pavé'], [/openwork|layered|haram/, 'Openwork'],
  [/trillion|cathedral|hexagonal|east-west/, 'Geometric'], [/lakshmi|temple|kemp/, 'Temple'],
];
const deriveMotif = (tag) => { const t = tag.toLowerCase(); const hit = MOTIF_MAP.find(([re]) => re.test(t)); return hit ? hit[1] : 'Contemporary'; };

const deriveStone = (tag) => {
  const t = tag.toLowerCase();
  if (t.includes('emerald')) return 'Emerald';
  if (t.includes('kemp')) return 'Kemp';
  if (/diamond|solitaire|pav[eé]|halo|brilliant|trillion|bezel|bar-set|floating|drop/.test(t)) return 'Diamond';
  return 'Gold only';
};

const deriveWeight = (tag, motif) => {
  const m = tag.match(/([\d.]+)\s*g/);
  if (m) { const g = parseFloat(m[1]); return { g, band: g < 6 ? 'Light' : g <= 12 ? 'Medium' : 'Heavy' }; }
  if (motif === 'Openwork' || motif === 'Temple') return { g: null, band: 'Heavy' };
  if (motif === 'Solitaire' || motif === 'Floral' || motif === 'Halo') return { g: null, band: 'Light' };
  return { g: null, band: 'Medium' };
};

const STATUS_META = {
  concept: { label: 'Concept', variant: 'mute' },
  '3d':    { label: '3D ready', variant: 'info' },
  rhino:   { label: 'Rhino sent', variant: 'accent' },
};

function enrich(design, ctx) {
  const tag = design.tag || design.id;
  const metal = deriveMetal(tag, design.hue ?? 40);
  const motif = deriveMotif(tag);
  const stone = deriveStone(tag);
  const weight = deriveWeight(tag, motif);
  const h = hashStr(design.id);
  // converted3D (set when the designer runs Convert to 3D) always wins; otherwise
  // a deterministic subset is pre-seeded as already-3D so the demo has both.
  const is3D = !!design.converted3D || h % 3 === 0;
  const status = !is3D ? 'concept'
    : (design.converted3D && h % 3 !== 0) ? '3d'
    : (h % 6 === 0 ? 'rhino' : '3d');
  return {
    ...design, tag, metal, motif, stone, weightBand: weight.band, weightG: weight.g,
    is3D, status,
    sessionId: ctx.sessionId, sessionTitle: ctx.sessionTitle,
    runId: ctx.runId, version: design.version || 1, parentDesignId: design.parentDesignId || null,
    facets: 32 + (h % 28),                 // faux model stat for the 3D card
    verts: 1200 + (h % 1800),
  };
}

// Flatten every assistant turn's designs across the workspace.
function buildItems(allTurns, sessions) {
  const titleOf = (sid) => (sessions.find(s => s.id === sid)?.title) || sid;
  const out = [];
  Object.entries(allTurns || {}).forEach(([sid, turns]) => {
    (turns || []).forEach(t => {
      if (t.role !== 'assistant' || !t.designs) return;
      t.designs.forEach(d => out.push(enrich(d, { sessionId: sid, sessionTitle: titleOf(sid), runId: t.runId })));
    });
  });
  return out;
}


/* ============================================================
   ATOM/MOLECULE · ThreeDStage — faux-3D rotatable preview
   Drag to rotate · toggle turntable spin. A real <model-viewer> mounts
   here in K6 once the model URL endpoint exists.
   ============================================================ */
const ThreeDStage = ({ design, size = 'md' }) => {
  const [ry, setRy] = useState(-22);
  const [rx, setRx] = useState(8);
  const [spin, setSpin] = useState(false);
  const drag = useRef(null);

  useEffect(() => {
    if (!spin) return;
    let raf, last = null;
    const tick = (ts) => {
      if (last != null) setRy(v => v + (ts - last) * 0.03);
      last = ts; raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [spin]);

  const onDown = (e) => { drag.current = { x: e.clientX, y: e.clientY, ry, rx }; setSpin(false); e.currentTarget.setPointerCapture?.(e.pointerId); };
  const onMove = (e) => {
    if (!drag.current) return;
    setRy(drag.current.ry + (e.clientX - drag.current.x) * 0.5);
    setRx(Math.max(-30, Math.min(38, drag.current.rx - (e.clientY - drag.current.y) * 0.3)));
  };
  const onUp = () => { drag.current = null; };

  return (
    <div className={`krg-3d krg-3d-${size}`}>
      <div className="krg-3d-stage"
           onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerLeave={onUp}>
        <div className="krg-3d-obj" style={{ transform: `rotateX(${rx}deg) rotateY(${ry}deg)` }}>
          <div className="krg-3d-face krg-3d-front"><KTileSvg hue={design.hue} /></div>
          <div className="krg-3d-face krg-3d-back"><KTileSvg hue={design.hue} /></div>
        </div>
        <div className="krg-3d-floor" />
      </div>
      <div className="krg-3d-bar">
        <Chip variant="info" dot>3D</Chip>
        <span className="krg-3d-hint t-meta">drag to rotate</span>
        <button className={`krg-3d-spinbtn${spin ? ' is-on' : ''}`} onClick={() => setSpin(s => !s)}>
          {spin ? '◼ Stop' : '↻ Spin'}
        </button>
      </div>
    </div>
  );
};


/* ============================================================
   MOLECULE · GalleryCard — one design tile in the grid
   ============================================================ */
const GalleryCard = ({ item, selected, onSelect, onInspect, onAction, onToggleStar }) => {
  const st = STATUS_META[item.status];
  return (
    <div className={`krg-card${selected ? ' is-selected' : ''}`}>
      <div className="krg-card-thumb" role="button" tabIndex={0}
           onClick={() => onInspect(item)}
           onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onInspect(item); } }}>
        <KTileSvg hue={item.hue} />
        <div className="krg-card-toptags">
          {item.is3D && <Chip variant="info" dot>3D</Chip>}
          {item.version > 1 && <Chip variant="mute">v{item.version}</Chip>}
        </div>
        <label className="krg-card-select" onClick={(e) => e.stopPropagation()}>
          <Checkbox checked={selected} onChange={() => onSelect(item.id)} />
        </label>
        <button className={`krg-card-star${item.starred ? ' is-on' : ''}`}
                onClick={(e) => { e.stopPropagation(); onToggleStar(item.id); }}
                title={item.starred ? 'Unstar' : 'Star'}>{item.starred ? '★' : '☆'}</button>
        <span className="krg-card-id t-mono">{item.id}</span>
      </div>
      <div className="krg-card-body">
        <div className="krg-card-tag">{item.tag}</div>
        <div className="krg-card-meta">
          <span>{item.metal}</span><span className="krg-dot">·</span>
          <span>{item.stone}</span><span className="krg-dot">·</span>
          <span>{item.weightBand}</span>
        </div>
        <div className="krg-card-foot">
          <Chip variant={st.variant} dot={item.status !== 'concept'}>{st.label}</Chip>
          <span style={{ flex: 1 }} />
          <button className="krg-card-act" onClick={() => onAction('refine', item)} title="Refine">✎</button>
          {!item.is3D
            ? <button className="krg-card-act" onClick={() => onAction('3d', item)} title="Convert to 3D">◫</button>
            : <button className="krg-card-act" onClick={() => onAction('rhino', item)} title="Send to Rhino">⤴</button>}
        </div>
      </div>
    </div>
  );
};


/* ============================================================
   MOLECULE · ThreeDCard — a 3D-model tile (rotatable)
   ============================================================ */
const ThreeDCard = ({ item, onInspect, onAction }) => (
  <div className="krg-card krg-card-3d">
    <ThreeDStage design={item} />
    <div className="krg-card-body">
      <div className="krg-card-tag">{item.tag}</div>
      <div className="krg-card-meta">
        <span>{item.metal}</span><span className="krg-dot">·</span>
        <span className="t-mono">{item.facets} facets</span><span className="krg-dot">·</span>
        <span>{item.status === 'rhino' ? 'Rhino sent' : '3D ready'}</span>
      </div>
      <div className="krg-card-foot">
        <Btn size="sm" variant="ghost" onClick={() => onInspect(item)}>Inspect</Btn>
        <span style={{ flex: 1 }} />
        <Btn size="sm" variant="primary" onClick={() => onAction('rhino', item)}>⤴ Rhino</Btn>
      </div>
    </div>
  </div>
);


/* ============================================================
   MOLECULE · FilterGroup — label + chip multiselect
   ============================================================ */
const FilterGroup = ({ label, options, selected, onToggle }) => (
  <div className="krg-filtergroup">
    <div className="krg-filtergroup-label">{label}</div>
    <div className="krg-filtergroup-chips">
      {options.map(o => (
        <Chip key={o.value} variant={selected.has(o.value) ? 'solid' : 'mute'} onClick={() => onToggle(o.value)}>
          {o.value} <span className="krg-chip-count">{o.count}</span>
        </Chip>
      ))}
    </div>
  </div>
);


/* ============================================================
   ORGANISM · GalleryFilterRail
   ============================================================ */
const FACETS = [
  { key: 'metal',      label: 'Metal' },
  { key: 'motif',      label: 'Motif' },
  { key: 'stone',      label: 'Stone' },
  { key: 'weightBand', label: 'Weight' },
  { key: 'status',     label: 'Status' },
];
const GalleryFilterRail = ({ items, filters, onToggle, onClear, activeCount }) => {
  const groups = useMemo(() => FACETS.map(f => {
    const counts = {};
    items.forEach(it => { const v = f.key === 'status' ? STATUS_META[it.status].label : it[f.key]; counts[v] = (counts[v] || 0) + 1; });
    return { ...f, options: Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([value, count]) => ({ value, count })) };
  }), [items]);

  return (
    <aside className="krg-rail">
      <div className="krg-rail-head">
        <span className="t-eyebrow">Filters</span>
        {activeCount > 0 && <button className="krg-rail-clear" onClick={onClear}>Clear ({activeCount})</button>}
      </div>
      {groups.map(g => (
        <FilterGroup key={g.key} label={g.label} options={g.options}
                     selected={filters[g.key]} onToggle={(v) => onToggle(g.key, v)} />
      ))}
    </aside>
  );
};


/* ============================================================
   MOLECULE · GalleryToolbar — split control + search + sort + density
   ============================================================ */
const SORTS = [
  { id: 'new',     label: 'Newest' },
  { id: 'starred', label: 'Starred first' },
  { id: 'metal',   label: 'Metal' },
  { id: 'weight',  label: 'Weight' },
];
const GalleryToolbar = ({ split, onSplit, scope, onScope, q, onQ, sort, onSort, density, onDensity, starredOnly, onStarred, counts }) => (
  <div className="krg-toolbar">
    <Seg value={split} onChange={onSplit} items={[
      { id: 'all', label: `All · ${counts.all}` },
      { id: '2d',  label: `2D · ${counts.twoD}` },
      { id: '3d',  label: `3D · ${counts.threeD}` },
    ]} />
    <div className="krg-toolbar-search">
      <Icon name="search" size={13} />
      <input value={q} onChange={(e) => onQ(e.target.value)} placeholder="Search designs, motifs, sessions…" />
      {q && <button className="krg-toolbar-clearq" onClick={() => onQ('')} aria-label="Clear">×</button>}
    </div>
    <span style={{ flex: 1 }} />
    <button className={`krg-toolbar-toggle${starredOnly ? ' is-on' : ''}`} onClick={() => onStarred(!starredOnly)}>
      {starredOnly ? '★' : '☆'} Starred
    </button>
    <Seg value={scope} onChange={onScope} items={[{ id: 'all', label: 'All sessions' }, { id: 'session', label: 'This session' }]} />
    <div className="krg-select">
      <select value={sort} onChange={(e) => onSort(e.target.value)}>
        {SORTS.map(s => <option key={s.id} value={s.id}>Sort · {s.label}</option>)}
      </select>
    </div>
    <div className="krg-density">
      <button className={density === 'comfortable' ? 'is-on' : ''} onClick={() => onDensity('comfortable')} title="Comfortable" aria-label="Comfortable">▦</button>
      <button className={density === 'compact' ? 'is-on' : ''} onClick={() => onDensity('compact')} title="Compact" aria-label="Compact">▤</button>
    </div>
  </div>
);


/* ============================================================
   MOLECULE · GallerySelectionBar — batch actions
   ============================================================ */
const GallerySelectionBar = ({ count, onStar, onConvert3D, onLaunch, onClear }) => (
  <div className="krg-selbar" role="status">
    <span className="krg-selbar-count">{count} selected</span>
    <span style={{ flex: 1 }} />
    <Btn size="sm" variant="ghost" onClick={onStar}>★ Star</Btn>
    <Btn size="sm" variant="ghost" onClick={onConvert3D}>◫ Convert to 3D</Btn>
    <Btn size="sm" variant="primary" onClick={onLaunch}>→ Add to launch pack</Btn>
    <IconBtn icon="cross" onClick={onClear} label="Clear selection" />
  </div>
);


/* ============================================================
   ORGANISM · DesignInspectPanel — drawer body for one design
   ============================================================ */
const InspectStat = ({ label, value }) => (
  <div className="krg-inspect-stat">
    <div className="krg-inspect-stat-l">{label}</div>
    <div className="krg-inspect-stat-v">{value}</div>
  </div>
);
const DesignInspectPanel = ({ item, onAction, onToggleStar }) => (
  <>
    <div className="krg-inspect-stage">
      {item.is3D ? <ThreeDStage design={item} size="lg" /> : (
        <div className="krg-inspect-flat"><KTileSvg hue={item.hue} /></div>
      )}
    </div>
    <div className="krg-drawer-section">
      <div className="krg-drawer-eyebrow">Specification</div>
      <div className="krg-inspect-grid">
        <InspectStat label="Metal" value={item.metal} />
        <InspectStat label="Stone" value={item.stone} />
        <InspectStat label="Weight" value={item.weightG ? `${item.weightG} g` : item.weightBand} />
        <InspectStat label="Motif" value={item.motif} />
        <InspectStat label="Status" value={STATUS_META[item.status].label} />
        <InspectStat label="Session" value={item.sessionTitle} />
      </div>
    </div>
    <div className="krg-drawer-section">
      <div className="krg-drawer-eyebrow">Lineage</div>
      <div className="krg-lineage">
        <span className="t-mono">{item.parentDesignId || 'origin'}</span>
        <span className="krg-lineage-arrow">→</span>
        <span className="t-mono krg-lineage-cur">{item.id}</span>
        <span className="krg-lineage-v">v{item.version}</span>
      </div>
      <div className="t-meta" style={{ marginTop: 6 }}>Run <span className="t-mono">{item.runId}</span> · {item.facets} facets · {item.verts.toLocaleString()} verts</div>
    </div>
  </>
);


/* ============================================================
   ORGANISM · KraftGalleryView — the page-level gallery
   ============================================================ */
const emptyFilters = () => ({ metal: new Set(), motif: new Set(), stone: new Set(), weightBand: new Set(), status: new Set() });

const KraftGalleryView = ({ allTurns, sessions, activeSessionId, onDesignAction, onToggleStar, onAddToLaunch, onConvert3D, pushToast }) => {
  const [split, setSplit] = useState('all');
  const [scope, setScope] = useState('all');
  const [q, setQ] = useState('');
  const [sort, setSort] = useState('new');
  const [density, setDensity] = useState('comfortable');
  const [starredOnly, setStarredOnly] = useState(false);
  const [filters, setFilters] = useState(emptyFilters);
  const [selected, setSelected] = useState(new Set());
  const [inspect, setInspect] = useState(null);

  const allItems = useMemo(() => buildItems(allTurns, sessions), [allTurns, sessions]);
  const scoped = useMemo(() => scope === 'session' ? allItems.filter(i => i.sessionId === activeSessionId) : allItems, [allItems, scope, activeSessionId]);

  const toggleFilter = useCallback((key, val) => setFilters(f => {
    const next = new Set(f[key]); next.has(val) ? next.delete(val) : next.add(val);
    return { ...f, [key]: next };
  }), []);
  const clearFilters = useCallback(() => { setFilters(emptyFilters()); setStarredOnly(false); setQ(''); }, []);
  const activeFilterCount = Object.values(filters).reduce((n, s) => n + s.size, 0) + (starredOnly ? 1 : 0) + (q ? 1 : 0);

  // apply filter rail + search + starred (NOT the 2D/3D split — split is per-section)
  const matched = useMemo(() => scoped.filter(it => {
    if (starredOnly && !it.starred) return false;
    if (q) { const hay = `${it.tag} ${it.metal} ${it.motif} ${it.stone} ${it.sessionTitle} ${it.id}`.toLowerCase(); if (!hay.includes(q.toLowerCase())) return false; }
    for (const f of FACETS) {
      const set = filters[f.key]; if (!set.size) continue;
      const v = f.key === 'status' ? STATUS_META[it.status].label : it[f.key];
      if (!set.has(v)) return false;
    }
    return true;
  }), [scoped, filters, q, starredOnly]);

  const sorted = useMemo(() => {
    const xs = [...matched];
    if (sort === 'starred') xs.sort((a, b) => (b.starred ? 1 : 0) - (a.starred ? 1 : 0));
    else if (sort === 'metal') xs.sort((a, b) => a.metal.localeCompare(b.metal));
    else if (sort === 'weight') { const o = { Light: 0, Medium: 1, Heavy: 2 }; xs.sort((a, b) => o[a.weightBand] - o[b.weightBand]); }
    return xs;
  }, [matched, sort]);

  const twoD = sorted.filter(i => !i.is3D);
  const threeD = sorted.filter(i => i.is3D);
  const counts = { all: sorted.length, twoD: twoD.length, threeD: threeD.length };

  const toggleSelect = useCallback((id) => setSelected(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }), []);
  const clearSelect = useCallback(() => setSelected(new Set()), []);

  const batchStar = () => { selected.forEach(id => onToggleStar(id)); pushToast?.({ kind: 'success', title: `★ Starred ${selected.size}`, body: 'PATCH /api/kraft/designs/star (batch)' }); clearSelect(); };
  const batchConvert = () => { onConvert3D?.([...selected]); pushToast?.({ kind: 'info', title: `Converting ${selected.size} to 3D…`, body: 'POST /api/kraft/designs:batchDerive3d — they move to the 3D tab when ready' }); clearSelect(); };
  const batchLaunch = () => { onAddToLaunch?.([...selected]); pushToast?.({ kind: 'success', title: `Added ${selected.size} to launch pack`, body: 'Starred + queued for CEO sign-off' }); clearSelect(); };

  const cardProps = (it) => ({
    item: it, selected: selected.has(it.id),
    onSelect: toggleSelect, onInspect: setInspect,
    onAction: onDesignAction, onToggleStar,
  });

  const grid2D = (list) => (
    <div className={`krg-grid${density === 'compact' ? ' is-compact' : ''}`}>
      {list.map(it => <GalleryCard key={it.id} {...cardProps(it)} />)}
    </div>
  );
  const grid3D = (list) => (
    <div className={`krg-grid krg-grid-3d${density === 'compact' ? ' is-compact' : ''}`}>
      {list.map(it => <ThreeDCard key={it.id} item={it} onInspect={setInspect} onAction={onDesignAction} />)}
    </div>
  );

  const isEmpty = sorted.length === 0;

  return (
    <div className="krg">
      <GalleryToolbar
        split={split} onSplit={setSplit} scope={scope} onScope={setScope}
        q={q} onQ={setQ} sort={sort} onSort={setSort}
        density={density} onDensity={setDensity}
        starredOnly={starredOnly} onStarred={setStarredOnly} counts={counts} />

      <div className="krg-body">
        <GalleryFilterRail items={scoped} filters={filters} onToggle={toggleFilter} onClear={clearFilters} activeCount={activeFilterCount} />

        <div className="krg-main">
          {selected.size > 0 && (
            <GallerySelectionBar count={selected.size} onStar={batchStar} onConvert3D={batchConvert} onLaunch={batchLaunch} onClear={clearSelect} />
          )}

          {isEmpty ? (
            <Card>
              <EmptyState mark="K"
                title="No designs match these filters."
                body={activeFilterCount > 0 ? 'Try clearing filters, or generate more directions in Chat.' : 'Generate designs in Chat — they appear here automatically.'}
                cta={activeFilterCount > 0 ? <Btn size="sm" variant="primary" onClick={clearFilters}>Clear filters</Btn> : null} />
            </Card>
          ) : split === '3d' ? (
            threeD.length ? grid3D(threeD) : <Card><EmptyState mark="◫" title="No 3D models yet." body="Convert any concept to 3D from its card or the inspector." /></Card>
          ) : split === '2d' ? (
            grid2D(twoD)
          ) : (
            <>
              {threeD.length > 0 && (
                <section className="krg-section">
                  <div className="krg-section-head"><span className="t-eyebrow">3D models · {threeD.length}</span>
                    <button className="krg-section-link" onClick={() => setSplit('3d')}>View all 3D ›</button></div>
                  {grid3D(threeD)}
                </section>
              )}
              <section className="krg-section">
                <div className="krg-section-head"><span className="t-eyebrow">2D concepts · {twoD.length}</span>
                  <button className="krg-section-link" onClick={() => setSplit('2d')}>View all 2D ›</button></div>
                {grid2D(twoD)}
              </section>
            </>
          )}
        </div>
      </div>

      <Drawer
        open={!!inspect}
        onClose={() => setInspect(null)}
        eyebrow={inspect ? `Design · ${inspect.id}` : ''}
        title={inspect?.tag}
        footer={inspect && (
          <>
            <Btn variant="ghost" onClick={() => onDesignAction('export', inspect)}>⤓ Export</Btn>
            <span style={{ flex: 1 }} />
            <Btn variant="ghost" onClick={() => { onToggleStar(inspect.id); }}>{inspect.starred ? '★ Starred' : '☆ Star'}</Btn>
            {inspect.is3D
              ? <Btn variant="primary" onClick={() => onDesignAction('rhino', inspect)}>⤴ Send to Rhino</Btn>
              : <Btn variant="primary" onClick={() => onDesignAction('3d', inspect)}>◫ Convert to 3D</Btn>}
          </>
        )}>
        {inspect && <DesignInspectPanel item={inspect} onAction={onDesignAction} onToggleStar={onToggleStar} />}
      </Drawer>
    </div>
  );
};


/* ============================================================
   EXPORT
   ============================================================ */
Object.assign(window.K, {
  KraftGalleryView, GalleryCard, ThreeDCard, ThreeDStage,
  GalleryToolbar, GalleryFilterRail, GallerySelectionBar, DesignInspectPanel,
  buildKraftGalleryItems: buildItems,
});
