# widget-box

The seam between the two SVG UI systems, so they **compose instead of competing**.

- [[widgets3d]] has the *controls* — `slider3d`, `toggle3d`, `select3d`, `list3d` —
  and they're what every `scenePanel` hook in this library is written against.
- [[box]] / [[surface]] have the *container*: text that re-wraps, scrolling, focus
  traversal, and (via `surface`) cascade menus plus persistent draggable, closable
  panels.

`widgetBox` puts the first inside the second. A `Widget3d` becomes a `BoxChild`, so
an existing panel's rows can be dropped into a floating panel — or a popup, or a
scrolling region — without rewriting them, and without `box` having to grow its own
slider.

## Demo

```js
import {
  b3d,
  b3dLight,
  panelScene,
  slider3d,
  toggle3d,
  button3d,
  label3d,
  ui,
} from 'tosijs-3d'
const { surface, widgetBox, box, textBlock, svgPoint } = ui
import { tosi, elements } from 'tosijs'
import { svgElements } from 'tosijs'

const { svg } = svgElements
const { div } = elements
const { wb } = tosi({ wb: { time: 14, fog: true } })

const W = 360
const H = 280

const readout = div({ style: 'margin:8px 4px;color:#8ea;font:13px system-ui' }, 'time 14.0 · fog on')
const update = () => {
  readout.textContent = `time ${Number(wb.time.value).toFixed(1)} · fog ${wb.fog.value ? 'on' : 'off'}`
}
wb.time.observe(update)
wb.fog.observe(update)

const s = surface({ width: W, height: H })
s.setContent(
  box(
    { width: W, height: H, padding: 12, gap: 8, background: '#12151c' },
    textBlock('widgets3d inside a surface panel', { font: { size: 14, weight: 600 }, color: '#e6e6e6' }),
    textBlock('The panel is draggable and closable; the controls are ordinary widgets3d.', { font: { size: 12 }, color: '#9fb0c3' })
  )
)
s.openPanel(
  { x: 14, y: 78 },
  widgetBox({ width: 300, padding: 10, gap: 4, background: '#0e1116' }, [
    label3d({ text: 'Scene', bold: true, compact: true }),
    slider3d({ label: 'time of day', value: wb.time, min: 0, max: 24, step: 0.5 }),
    toggle3d({ label: 'fog', value: wb.fog }),
    button3d({ label: 'Reset', handleClick: () => { wb.time.value = 14; wb.fog.value = true } }),
  ]),
  { title: 'Scene', draggable: true }
)

const svgEl = svg({ viewBox: `0 0 ${W} ${H}`, width: W, height: H, style: 'max-width:100%;touch-action:none' }, s.el)
const at = (e) => {
  // svgPoint, not rect arithmetic: the viewBox is letterboxed when the
  // container's aspect ratio differs, and a linear map drifts as it's resized.
  const p = svgPoint(svgEl, e.clientX, e.clientY)
  return [p.x, p.y]
}
svgEl.addEventListener('pointerdown', (e) => { s.handlePointer('down', ...at(e)); svgEl.setPointerCapture(e.pointerId) })
svgEl.addEventListener('pointermove', (e) => s.handlePointer('move', ...at(e)))
svgEl.addEventListener('pointerup', (e) => s.handlePointer('up', ...at(e)))

// 3D side — panelScene routes scene picks to the SAME surface, so the slider
// drags and toggles work identically on the plane (the path a VR ray takes).
const { plane, sceneCreated } = panelScene({ svg: svgEl, target: s })
const scene = b3d({ style: 'border-radius:8px;overflow:hidden', sceneCreated }, b3dLight({ intensity: 1 }), plane)

preview.append(
  div({ style: 'display:flex;flex-direction:column;height:100%;background:#0c0e14' },
    div({ style: 'display:flex;gap:20px;flex:1;min-height:0;padding:14px' },
      div({ style: 'color:#9ab;font:12px system-ui;display:flex;flex-direction:column;gap:6px;flex:1;min-width:0' }, 'DOM', svgEl),
      div({ style: 'color:#9ab;font:12px system-ui;display:flex;flex-direction:column;gap:6px;flex:1;min-width:0' }, '3D texture', scene)),
    readout)
)
```
```css
.preview { height: 100%; }
```

widgets3d controls living inside a `surface` panel — **drag the title bar** to move it,
× to close. The slider drags properly because `BoxChild.handlePointer` captures: the
gesture survives the pointer leaving the track.

> Drag the sliders in either pane — same panel, same drags. A drag that leaves
> the track keeps working, which is the whole reason `BoxChild.handlePointer`
> captures: on a texture there is no pointer capture to inherit.

## Why an adapter rather than a port

Porting the controls to `box` would mean rewriting `slider3d` and friends, and every
`scenePanel` written against them, to end up with the same widgets. The two protocols
differ only in activation: a `BoxChild` is activated *semantically*, which cannot
express a drag, while a slider needs `down → move* → up` with **capture**, so the drag
survives the pointer slipping off the track. `BoxChild.handlePointer` (raw, capturing)
covers that: `box` hands a raw child the whole gesture and keeps routing to it until
`up`, and `widgetBox` is the translation.
