("cmap");
```
The string must exactly match the Python trait name. A typo silently returns
`undefined`.
**`DataView` → `Float32Array`** — `Bytes` traitlets arrive as `DataView`.
Convert with `extractFloat32()` from `format.ts`.
**Canvas API** — `getContext("2d")`, `clearRect`, `drawImage`, `save`/`restore`.
Widgets render to offscreen canvas first, then `drawImage()` with zoom/pan
transforms.
**DPR** — `window.devicePixelRatio`. UI canvas uses `width={cssW * DPR}`,
`style={{ width: cssW }}`, `ctx.scale(DPR, DPR)` for crisp text on HiDPI.
**`ref` / `.current`** — Persistent DOM references via `useRef()`. Don't trigger
re-renders.
### MUI (Material UI)
- **`Box`** — styled `` with `sx` prop
- **`Stack`** — flexbox layout (`direction="row"` for horizontal)
- **`Typography`** — text with consistent styling
- **`sx`** — inline CSS as JS object (`fontSize: 10`, `mb: "4px"`, `bgcolor: "#fff"`)
- **`Switch`** — toggle (FFT, log scale); **`Select`** — dropdown (colormap); **`Button`** — action; **`Slider`** — range
### Build tools
**esbuild** — JS bundler. `npm run build` (one-shot, ~130ms) or `npm run dev` (watch mode).
**anywidget HMR** — Hot Module Replacement. With `npm run dev` + `ANYWIDGET_HMR=1`,
JS changes appear without kernel restart.
## Troubleshooting
**Trait exists in Python but JS reads `undefined`** — You forgot `.tag(sync=True)`.
**Python changes don't take effect** — Restart the kernel. Python code is loaded
at import time.
**Trait isn't syncing** — Check spelling. `useModelState("pos_row")` must exactly
match the Python trait name.
**Theme colors** — Use `colors` from `useTheme()` for all UI chrome:
```tsx
const { colors } = useTheme();
```
Available: `bg`, `bgAlt`, `text`, `textMuted`, `border`, `controlBg`, `accent`.
**Colormaps** — Applied entirely in JS. Python sends raw float32, JS maps via LUT:
```tsx
import { COLORMAPS, applyColormap } from "../colormaps";
applyColormap(floatData, rgbaBuffer, COLORMAPS["inferno"], vmin, vmax);
```
## Testing
### Unit tests
```bash
python -m pytest tests/ -v --ignore=tests/test_e2e_smoke.py # all
python -m pytest tests/test_widget_show4dstem.py -v # one widget
```
### End-to-end smoke tests
Requires Playwright + JupyterLab. Renders widgets in a real browser, captures
screenshots (light + dark theme), tests interactions. ~4 minutes:
```bash
python -m pytest tests/test_e2e_smoke.py -v # all
python -m pytest tests/test_e2e_smoke.py -v -k show2d # one widget
```
### Screenshot verification
After modifying widget UI: `npm run build` → run smoke tests → visually verify
`tests/screenshots/smoke/`.
| File | Purpose |
|------|---------|
| `test_widget_*.py` | Unit tests (traits, shapes, data, state, ROI, display) |
| `capture_*.py` | Screenshot capture scripts (run manually) |
| `test_e2e_smoke.py` | Full E2E via Playwright |
## Publishing
### Docs
```bash
pip install -e ".[docs]"
# One-shot build
sphinx-build docs docs/_build/html
# Live reload (rebuilds on file change, opens browser)
sphinx-autobuild docs docs/_build/html --open-browser --port 8322
```
### TestPyPI
Currently published on [TestPyPI](https://test.pypi.org/project/quantem-widget/) (not yet on PyPI).
1. Bump version in `pyproject.toml`
2. Tag and push: `git tag vX.Y.Z && git push origin main && git push origin vX.Y.Z`
3. CI builds and uploads to TestPyPI. Verify: `./scripts/verify_testpypi.sh X.Y.Z`
Install from TestPyPI:
```bash
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ quantem-widget
```