@chuvenger/devdock 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +171 -16
  2. package/dist/chunk-3MVFATRN.js +60 -0
  3. package/dist/chunk-3MVFATRN.js.map +1 -0
  4. package/dist/{chunk-F2KWDPMA.js → chunk-NXZNFF34.js} +238 -42
  5. package/dist/chunk-NXZNFF34.js.map +1 -0
  6. package/dist/{dock-CvTtAelh.d.ts → dock-P6l6pKjS.d.ts} +1 -1
  7. package/dist/{dock-Du6Im767.d.cts → dock-d3HqT25k.d.cts} +1 -1
  8. package/dist/index.cjs +235 -39
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +1 -1
  13. package/dist/{match-B6LN1RRP.d.cts → match-DDSJIRYW.d.cts} +1 -1
  14. package/dist/{match-C-mY8W8T.d.ts → match-DwGIF1Xt.d.ts} +1 -1
  15. package/dist/react-router.cjs +276 -47
  16. package/dist/react-router.cjs.map +1 -1
  17. package/dist/react-router.d.cts +2 -2
  18. package/dist/react-router.d.ts +2 -2
  19. package/dist/react-router.js +2 -2
  20. package/dist/react.cjs +277 -47
  21. package/dist/react.cjs.map +1 -1
  22. package/dist/react.d.cts +28 -8
  23. package/dist/react.d.ts +28 -8
  24. package/dist/react.js +2 -2
  25. package/dist/svelte.cjs +235 -39
  26. package/dist/svelte.cjs.map +1 -1
  27. package/dist/svelte.d.cts +3 -3
  28. package/dist/svelte.d.ts +3 -3
  29. package/dist/svelte.js +2 -2
  30. package/dist/svelte.js.map +1 -1
  31. package/dist/sveltekit.cjs +235 -39
  32. package/dist/sveltekit.cjs.map +1 -1
  33. package/dist/sveltekit.d.cts +4 -4
  34. package/dist/sveltekit.d.ts +4 -4
  35. package/dist/sveltekit.js +2 -2
  36. package/dist/sveltekit.js.map +1 -1
  37. package/dist/types-B_SpF32A.d.cts +157 -0
  38. package/dist/types-B_SpF32A.d.ts +157 -0
  39. package/package.json +1 -1
  40. package/dist/chunk-D5IQBV2K.js +0 -24
  41. package/dist/chunk-D5IQBV2K.js.map +0 -1
  42. package/dist/chunk-F2KWDPMA.js.map +0 -1
  43. package/dist/types-fhhRek-e.d.cts +0 -88
  44. package/dist/types-fhhRek-e.d.ts +0 -88
package/README.md CHANGED
@@ -9,10 +9,11 @@ routes and run custom commands.
9
9
 
10
10
  - 🔒 **Dev-gated by default** — renders only in a dev build served from a dev host (localhost/LAN), so it never ships to a real URL. Fully overridable.
11
11
  - 🧭 **Route jumper** — list routes manually, or auto-detect your `/dev` routes from React Router or SvelteKit.
12
- - ⚡ **Custom commands** — register any action (reset DB, toggle a flag, copy a token…).
12
+ - ⚡ **Custom commands** — register any action (reset DB, toggle a flag, copy a token…), with live labels and check marks for toggles.
13
+ - 📊 **Custom views** — mount your own UI in the popup for anything a list can't say: a frame-rate meter, a sync status, a segmented control.
13
14
  - 🔎 Filter box + keyboard nav (↑/↓/Enter/Esc) and an optional global hotkey.
14
15
  - 🎨 Zero CSS to import — inline-styled, high z-index, no runtime dependencies.
15
- - 🛡️ **Shadow DOM isolated** by default — the host page's CSS can't touch it (and vice versa).
16
+ - 🛡️ **Shadow DOM isolated** by default — the host page's CSS can't touch it (and vice versa), or `shadow: "inherit"` to let your design system in.
16
17
  - 🧩 **Framework-agnostic core** with thin **React** and **Svelte** adapters.
17
18
 
18
19
  ## Architecture
@@ -22,13 +23,13 @@ framework-free core (`createDevDock`) that builds DOM directly. The framework
22
23
  adapters are tiny wrappers that drive its `create → update → destroy`
23
24
  lifecycle:
24
25
 
25
- | Import | What you get |
26
- | ------ | ------------ |
27
- | `@chuvenger/devdock` | `createDevDock(options)` — vanilla core, works anywhere |
28
- | `@chuvenger/devdock/react` | `<DevDock />` React component |
29
- | `@chuvenger/devdock/react-router` | `<ReactRouterDevDock />` + route auto-detection |
30
- | `@chuvenger/devdock/svelte` | `use:devdock` Svelte action |
31
- | `@chuvenger/devdock/sveltekit` | `use:devdock` + route auto-detection for SvelteKit |
26
+ | Import | What you get | Custom views |
27
+ | ------ | ------------ | ------------ |
28
+ | `@chuvenger/devdock` | `createDevDock(options)` — vanilla core, works anywhere | `mount(host)` → teardown |
29
+ | `@chuvenger/devdock/react` | `<DevDock />` React component | `render: () => <JSX />`, portaled in |
30
+ | `@chuvenger/devdock/react-router` | `<ReactRouterDevDock />` + route auto-detection | same as `/react` |
31
+ | `@chuvenger/devdock/svelte` | `use:devdock` Svelte action | `mount(host)` → teardown |
32
+ | `@chuvenger/devdock/sveltekit` | `use:devdock` + route auto-detection for SvelteKit | `mount(host)` → teardown |
32
33
 
33
34
  `react`, `react-dom`, `react-router`, and `svelte` are all **optional** peer
34
35
  deps — install only what your adapter needs. The react-router adapter imports
@@ -226,12 +227,149 @@ dock.destroy(); // remove entirely
226
227
 
227
228
  Use this to build a Vue/Angular/web-component adapter, or wire it up by hand.
228
229
 
230
+ ## Stateful commands
231
+
232
+ A command is normally fire-and-forget. Two optional fields turn one into a
233
+ control that shows its own state — both are re-read on every render of the open
234
+ panel, so the row updates in place:
235
+
236
+ ```tsx
237
+ <DevDock
238
+ commands={[
239
+ // A function label: the row shows live state.
240
+ { id: "theme", label: () => `Theme: ${theme}`, keepOpen: true, run: toggleTheme },
241
+ // `checked` renders a check mark and makes the row a real toggle
242
+ // (role="checkbox" + aria-checked).
243
+ { id: "freeze", label: "Freeze animations", checked: () => frozen, keepOpen: true, run: freeze },
244
+ ]}
245
+ />
246
+ ```
247
+
248
+ Pair them with `keepOpen: true` so the popup stays put and you can watch the
249
+ value change. Give a command with a function `label` an explicit `id` — the id
250
+ is what keeps the row stable when the text changes.
251
+
252
+ ## Custom views
253
+
254
+ Some state has no row shape at all: a frame-rate meter, four lines of sync
255
+ facts, a segmented control. Pass those in as **views** — your own UI, mounted
256
+ inside the popup, keeping the dock's shell, hotkey, filter, route detection and
257
+ dev-gating.
258
+
259
+ Views are mounted when the popup **opens** and torn down when it **closes**,
260
+ which is what makes a `requestAnimationFrame` loop or an event listener inside
261
+ one correct rather than a leak.
262
+
263
+ ### React
264
+
265
+ ```tsx
266
+ <DevDock
267
+ shadow="inherit" // ← see below: views need the app's CSS
268
+ views={[
269
+ { id: "fps", label: "Frame rate", render: () => <FrameRate /> },
270
+ { id: "theme", label: "Theme", render: () => <ThemeControl value={theme} onChange={setTheme} /> },
271
+ // Two views under one "Session" heading, below the routes/commands list.
272
+ { id: "facts", group: "Session", order: "after", render: () => <SyncFacts /> },
273
+ { id: "signin", group: "Session", order: "after", render: () => <SignInButton /> },
274
+ ]}
275
+ />
276
+ ```
277
+
278
+ The component is rendered with `createPortal` into a host element the dock
279
+ owns, so it stays part of your React tree: context, hooks and state all work,
280
+ and it re-renders when the component holding `<DevDock>` does. A view whose
281
+ `id` is unchanged is **never remounted**, so re-rendering the parent is free.
282
+
283
+ ### Vanilla / Svelte
284
+
285
+ The core contract is framework-free: fill the host element, return a teardown.
286
+
287
+ ```ts
288
+ createDevDock({
289
+ views: [
290
+ {
291
+ id: "fps",
292
+ label: "Frame rate",
293
+ mount: (host) => {
294
+ let raf = requestAnimationFrame(function tick() {
295
+ host.textContent = `${fps()} fps`;
296
+ raf = requestAnimationFrame(tick);
297
+ });
298
+ return () => cancelAnimationFrame(raf); // ← runs when the popup closes
299
+ },
300
+ },
301
+ ],
302
+ });
303
+ ```
304
+
305
+ In Svelte the same field takes a component (Svelte 5 shown; on Svelte 4 use
306
+ `new MyPanel({ target: host })` and `component.$destroy()`):
307
+
308
+ ```svelte
309
+ <script lang="ts">
310
+ import { mount, unmount } from "svelte";
311
+ import { devdock } from "@chuvenger/devdock/svelte";
312
+ import SyncFacts from "./SyncFacts.svelte";
313
+
314
+ const options = {
315
+ shadow: "inherit",
316
+ views: [
317
+ {
318
+ id: "facts",
319
+ label: "Session",
320
+ mount: (host) => {
321
+ const view = mount(SyncFacts, { target: host });
322
+ return () => unmount(view);
323
+ },
324
+ },
325
+ ],
326
+ };
327
+ </script>
328
+
329
+ <div use:devdock={options}></div>
330
+ ```
331
+
332
+ ### Views and your CSS
333
+
334
+ The dock renders inside a shadow root, and **the document's stylesheet does not
335
+ cross that boundary** — a view that renders your app's own components renders
336
+ unstyled. If your view needs the host app's CSS, say so:
337
+
338
+ - `shadow="inherit"` — keep the shadow root, but copy the document's styles
339
+ into it (`document.adoptedStyleSheets` plus every `<style>` and
340
+ `<link rel="stylesheet">`, re-synced as your bundler injects or edits them).
341
+ - `shadow={false}` — render in the light DOM, where the page's CSS applies
342
+ directly.
343
+
344
+ Isolation stays the default. Two things to know about `"inherit"`:
345
+
346
+ - The page's CSS can now reach the dock's own chrome too, and your design
347
+ system's tokens arrive with the page's assumptions — a light-theme token
348
+ lands on the dock's dark panel. Scope the tokens you need onto the view's
349
+ own root.
350
+ - **The rules cross the boundary; the element tree does not.** A selector
351
+ anchored on an ancestor outside the shadow root —
352
+ `html[data-theme="dark"] .card`, `body.compact .row` — still won't match
353
+ inside it. Re-apply that state on the view's own root, where it can.
354
+
355
+ `shadow` is read once, when the dock is created: a shadow root can't be
356
+ detached, so `update()` ignores it.
357
+
358
+ > Testing a view with `@testing-library/user-event`? It can't type into a
359
+ > shadow root — it installs its value interceptor from a `focus` listener on
360
+ > the document, where the event has been retargeted to the shadow host. Render
361
+ > with `shadow: false` in tests, or write the value through the native setter
362
+ > and dispatch `input` yourself.
363
+
229
364
  ## Controlling when it shows
230
365
 
231
366
  By default the dock renders only when **both** of these hold:
232
367
 
233
- 1. **It's a dev build** — `process.env.NODE_ENV !== "production"`, the token
234
- every bundler inlines.
368
+ 1. **It's a dev build** — `import.meta.env.DEV` when the bundler is in the
369
+ Vite family (Vite, SvelteKit, React Router 7, Astro…), otherwise
370
+ `process.env.NODE_ENV !== "production"`, the token every bundler inlines.
371
+ Both are replaced statically, so a production bundle drops the dock's gate
372
+ to a constant `false`.
235
373
  2. **It's served from a dev host** — loopback (`localhost`, `127.0.0.1`,
236
374
  `::1`), an mDNS `.local` name, or a private LAN address (`10.x`,
237
375
  `172.16–31.x`, `192.168.x`, so hitting your dev server from a phone on the
@@ -268,9 +406,18 @@ tree-shakes it: `{import.meta.env.DEV && <DevDock … />}`.
268
406
  The dock is styled entirely with inline styles (no stylesheet to import) and,
269
407
  by default, renders inside an **open shadow root** attached to a host element in
270
408
  `document.body`. That means the host page's CSS can't leak in and the dock's
271
- styles can't leak out. Pass `shadow: false` to render in the light DOM instead
272
- (e.g. if you want to override its look from the page). The host element carries
273
- a `data-devdock` attribute either way.
409
+ styles can't leak out. The host element carries a `data-devdock` attribute in
410
+ every mode.
411
+
412
+ | `shadow` | Where it renders | The page's CSS |
413
+ | -------- | ---------------- | -------------- |
414
+ | `true` (default) | shadow root | kept out |
415
+ | `"inherit"` | shadow root | copied in, and kept in sync |
416
+ | `false` | light DOM | applies directly |
417
+
418
+ Use `"inherit"` (or `false`) when a [custom view](#views-and-your-css) renders
419
+ your app's own components and needs its stylesheet. Otherwise leave it alone —
420
+ isolation is the reason the dock survives arbitrary host CSS.
274
421
 
275
422
  ## API
276
423
 
@@ -280,6 +427,7 @@ a `data-devdock` attribute either way.
280
427
  | ------------ | ----------------------------- | ------------------------------------ | ----------- |
281
428
  | `routes` | `DevRoute[]` | `[]` | Navigable destinations. |
282
429
  | `commands` | `DevCommand[]` | `[]` | Custom actions. |
430
+ | `views` | `DevView[]` | `[]` | Custom panels, mounted while the popup is open. |
283
431
  | `enabled` | `boolean \| (() => boolean)` | dev build **and** dev host | Whether to render at all. Overrides both default checks. A predicate is re-checked on every update. |
284
432
  | `devHostOnly`| `boolean` | `true` | Also require a localhost/LAN hostname, so dev builds on real URLs stay hidden. Ignored when `enabled` is set. |
285
433
  | `onNavigate` | `(path: string) => void` | `window.location.assign` | How to navigate on route select. |
@@ -289,17 +437,24 @@ a `data-devdock` attribute either way.
289
437
  | `hotkey` | `string \| null` | `null` | Toggle shortcut, e.g. `"mod+."`. |
290
438
  | `zIndex` | `number` | `2147483000` | Base z-index. |
291
439
  | `container` | `HTMLElement` | `document.body` | Where to mount. |
292
- | `shadow` | `boolean` | `true` | Render in a shadow root (CSS-isolated). Set `false` for light DOM. |
440
+ | `shadow` | `boolean \| "inherit"` | `true` | Render in a shadow root (CSS-isolated). `"inherit"` copies the page's styles in; `false` renders in the light DOM. Read once, at creation. |
293
441
 
294
442
  `DevRoute`: `{ path: string; label?: string; group?: string }`
295
443
 
296
- `DevCommand`: `{ label: string; run: () => void | Promise<void>; id?: string; group?: string; keepOpen?: boolean }`
444
+ `DevCommand`: `{ label: string | (() => string); run: () => void | Promise<void>; checked?: () => boolean; id?: string; group?: string; keepOpen?: boolean }`
445
+
446
+ `DevView`: `{ mount: (host: HTMLElement) => void | (() => void); id?: string; label?: string; group?: string; order?: "before" | "after" }` — on the React adapter, `render: () => ReactNode` replaces `mount`.
297
447
 
298
448
  ### `@chuvenger/devdock` (core)
299
449
 
300
450
  - `createDevDock(options?): { update(options), destroy() }` — mounts the dock; no-ops during SSR (no `document`).
301
451
  - `matchHotkey(spec, event)` — the hotkey matcher (exported for reuse).
302
452
 
453
+ ### `@chuvenger/devdock/react`
454
+
455
+ - `DevDock(props)` — the component. Same options as the core, with `views` taking `render: () => ReactNode` instead of `mount`.
456
+ - `DevDockView` — the React view shape.
457
+
303
458
  ### `@chuvenger/devdock/react-router`
304
459
 
305
460
  - `ReactRouterDevDock(props)` — `DevDock` wired to `useNavigate()` + auto-detected routes. Extra props: `match?: RouteMatcher`, `staticRoutes?: boolean`.
@@ -0,0 +1,60 @@
1
+ import { keyViews, createDevDock } from './chunk-NXZNFF34.js';
2
+ import { useRef, useState, useMemo, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ var NO_VIEWS = [];
7
+ function DevDock({ views = NO_VIEWS, ...options }) {
8
+ const instance = useRef(null);
9
+ const [hosts, setHosts] = useState(
10
+ () => /* @__PURE__ */ new Map()
11
+ );
12
+ const alive = useRef(true);
13
+ const keyed = useMemo(() => keyViews(views), [views]);
14
+ const coreViews = useMemo(
15
+ () => keyed.map(({ key, view }) => ({
16
+ id: key,
17
+ label: view.label,
18
+ group: view.group,
19
+ order: view.order,
20
+ mount: (host) => {
21
+ setHosts((prev) => new Map(prev).set(key, host));
22
+ return () => {
23
+ if (!alive.current) return;
24
+ setHosts((prev) => {
25
+ const next = new Map(prev);
26
+ next.delete(key);
27
+ return next;
28
+ });
29
+ };
30
+ }
31
+ })),
32
+ [keyed]
33
+ );
34
+ const latest = useRef({ options, coreViews });
35
+ latest.current = { options, coreViews };
36
+ useEffect(() => {
37
+ alive.current = true;
38
+ const inst = createDevDock({
39
+ ...latest.current.options,
40
+ views: latest.current.coreViews
41
+ });
42
+ instance.current = inst;
43
+ return () => {
44
+ alive.current = false;
45
+ inst.destroy();
46
+ instance.current = null;
47
+ };
48
+ }, []);
49
+ useEffect(() => {
50
+ instance.current?.update({ ...options, views: coreViews });
51
+ });
52
+ return /* @__PURE__ */ jsx(Fragment, { children: keyed.map(({ key, view }) => {
53
+ const host = hosts.get(key);
54
+ return host ? createPortal(view.render(), host, key) : null;
55
+ }) });
56
+ }
57
+
58
+ export { DevDock };
59
+ //# sourceMappingURL=chunk-3MVFATRN.js.map
60
+ //# sourceMappingURL=chunk-3MVFATRN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/DevDock.tsx"],"names":[],"mappings":";;;;;AA6BA,IAAM,WAA0B,EAAC;AAQ1B,SAAS,QAAQ,EAAE,KAAA,GAAQ,QAAA,EAAU,GAAG,SAAQ,EAAiB;AACtE,EAAA,MAAM,QAAA,GAAW,OAA+B,IAAI,CAAA;AAIpD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,QAAA;AAAA,IACxB,0BAAU,GAAA;AAAI,GAChB;AAGA,EAAA,MAAM,KAAA,GAAQ,OAAO,IAAI,CAAA;AAIzB,EAAA,MAAM,KAAA,GAAQ,QAAQ,MAAM,QAAA,CAAS,KAAK,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEpD,EAAA,MAAM,SAAA,GAAY,OAAA;AAAA,IAChB,MACE,KAAA,CAAM,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,MAAK,MAAO;AAAA,MAC5B,EAAA,EAAI,GAAA;AAAA,MACJ,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,KAAA,EAAO,CAAC,IAAA,KAAS;AACf,QAAA,QAAA,CAAS,CAAC,SAAS,IAAI,GAAA,CAAI,IAAI,CAAA,CAAE,GAAA,CAAI,GAAA,EAAK,IAAI,CAAC,CAAA;AAC/C,QAAA,OAAO,MAAM;AACX,UAAA,IAAI,CAAC,MAAM,OAAA,EAAS;AACpB,UAAA,QAAA,CAAS,CAAC,IAAA,KAAS;AACjB,YAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,IAAI,CAAA;AACzB,YAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,YAAA,OAAO,IAAA;AAAA,UACT,CAAC,CAAA;AAAA,QACH,CAAA;AAAA,MACF;AAAA,KACF,CAAE,CAAA;AAAA,IACJ,CAAC,KAAK;AAAA,GACR;AAKA,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,EAAE,OAAA,EAAS,WAAW,CAAA;AAC5C,EAAA,MAAA,CAAO,OAAA,GAAU,EAAE,OAAA,EAAS,SAAA,EAAU;AAEtC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,KAAA,CAAM,OAAA,GAAU,IAAA;AAChB,IAAA,MAAM,OAAO,aAAA,CAAc;AAAA,MACzB,GAAG,OAAO,OAAA,CAAQ,OAAA;AAAA,MAClB,KAAA,EAAO,OAAO,OAAA,CAAQ;AAAA,KACvB,CAAA;AACD,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,OAAO,MAAM;AACX,MAAA,KAAA,CAAM,OAAA,GAAU,KAAA;AAChB,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAAA,IACrB,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,QAAA,CAAS,SAAS,MAAA,CAAO,EAAE,GAAG,OAAA,EAAS,KAAA,EAAO,WAAW,CAAA;AAAA,EAC3D,CAAC,CAAA;AAED,EAAA,uCAEK,QAAA,EAAA,KAAA,CAAM,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,MAAK,KAAM;AAC5B,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,OAAO,OAAO,YAAA,CAAa,IAAA,CAAK,QAAO,EAAG,IAAA,EAAM,GAAG,CAAA,GAAI,IAAA;AAAA,EACzD,CAAC,CAAA,EACH,CAAA;AAEJ","file":"chunk-3MVFATRN.js","sourcesContent":["import { useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { createDevDock, keyViews } from \"../core/dock\";\nimport type {\n DevDockInstance,\n DevDockOptions,\n DevView,\n} from \"../core/types\";\n\n/**\n * A custom panel rendered by React inside the dock's popup. Same contract as\n * the core's {@link DevView}, with `render` in place of `mount`: the component\n * is portaled into the dock, mounted when the popup opens and unmounted when\n * it closes.\n *\n * A view renders inside the dock's shadow root, which the document's\n * stylesheets don't cross — pass `shadow=\"inherit\"` (or `shadow={false}`) if\n * it uses your app's CSS.\n */\nexport interface DevDockView extends Omit<DevView, \"mount\"> {\n /** The view's content. Called on every render of the host `<DevDock>`. */\n render: () => ReactNode;\n}\n\nexport type DevDockProps = Omit<DevDockOptions, \"views\"> & {\n /** Custom panels rendered inside the popup. See {@link DevDockView}. */\n views?: DevDockView[];\n};\n\nconst NO_VIEWS: DevDockView[] = [];\n\n/**\n * React wrapper around the framework-agnostic core. The dock itself mounts\n * into `document.body` (or `container`) and is torn down on unmount; this\n * component renders only the portals for any `views` you pass, so with none it\n * renders nothing. Safe to leave mounted; it renders nothing in production.\n */\nexport function DevDock({ views = NO_VIEWS, ...options }: DevDockProps) {\n const instance = useRef<DevDockInstance | null>(null);\n // The host elements the core created for our views, keyed the same way it\n // keys them. React draws into these with a portal, which keeps the core free\n // of any React import.\n const [hosts, setHosts] = useState<ReadonlyMap<string, HTMLElement>>(\n () => new Map(),\n );\n // `destroy()` tears views down, which would otherwise ask an unmounting\n // component to set state.\n const alive = useRef(true);\n\n // Keyed exactly the way the core keys the host elements, so each portal\n // finds its own.\n const keyed = useMemo(() => keyViews(views), [views]);\n\n const coreViews = useMemo<DevView[]>(\n () =>\n keyed.map(({ key, view }) => ({\n id: key,\n label: view.label,\n group: view.group,\n order: view.order,\n mount: (host) => {\n setHosts((prev) => new Map(prev).set(key, host));\n return () => {\n if (!alive.current) return;\n setHosts((prev) => {\n const next = new Map(prev);\n next.delete(key);\n return next;\n });\n };\n },\n })),\n [keyed],\n );\n\n // The core reconciles views by id and no-ops when their shape is unchanged,\n // so handing it a fresh array on every render costs nothing and never\n // remounts a view.\n const latest = useRef({ options, coreViews });\n latest.current = { options, coreViews };\n\n useEffect(() => {\n alive.current = true;\n const inst = createDevDock({\n ...latest.current.options,\n views: latest.current.coreViews,\n });\n instance.current = inst;\n return () => {\n alive.current = false;\n inst.destroy();\n instance.current = null;\n };\n }, []);\n\n useEffect(() => {\n instance.current?.update({ ...options, views: coreViews });\n });\n\n return (\n <>\n {keyed.map(({ key, view }) => {\n const host = hosts.get(key);\n return host ? createPortal(view.render(), host, key) : null;\n })}\n </>\n );\n}\n"]}