@chuvenger/devdock 0.4.1 → 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 (42) hide show
  1. package/README.md +166 -14
  2. package/dist/chunk-3MVFATRN.js +60 -0
  3. package/dist/chunk-3MVFATRN.js.map +1 -0
  4. package/dist/{chunk-WVRXVSEL.js → chunk-NXZNFF34.js} +229 -42
  5. package/dist/chunk-NXZNFF34.js.map +1 -0
  6. package/dist/{dock-DOWeFZJ5.d.ts → dock-P6l6pKjS.d.ts} +1 -1
  7. package/dist/{dock-dq6OS0Jm.d.cts → dock-d3HqT25k.d.cts} +1 -1
  8. package/dist/index.cjs +226 -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-BZoH6rW7.d.cts → match-DDSJIRYW.d.cts} +1 -1
  14. package/dist/{match-D1am9aTf.d.ts → match-DwGIF1Xt.d.ts} +1 -1
  15. package/dist/react-router.cjs +267 -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 +268 -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 +226 -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 +226 -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-V99upW.d.cts → types-B_SpF32A.d.cts} +73 -5
  38. package/dist/{types-B-V99upW.d.ts → types-B_SpF32A.d.ts} +73 -5
  39. package/package.json +1 -1
  40. package/dist/chunk-P4ZUS4GB.js +0 -24
  41. package/dist/chunk-P4ZUS4GB.js.map +0 -1
  42. package/dist/chunk-WVRXVSEL.js.map +0 -1
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,6 +227,140 @@ 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:
@@ -271,9 +406,18 @@ tree-shakes it: `{import.meta.env.DEV && <DevDock … />}`.
271
406
  The dock is styled entirely with inline styles (no stylesheet to import) and,
272
407
  by default, renders inside an **open shadow root** attached to a host element in
273
408
  `document.body`. That means the host page's CSS can't leak in and the dock's
274
- styles can't leak out. Pass `shadow: false` to render in the light DOM instead
275
- (e.g. if you want to override its look from the page). The host element carries
276
- 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.
277
421
 
278
422
  ## API
279
423
 
@@ -283,6 +427,7 @@ a `data-devdock` attribute either way.
283
427
  | ------------ | ----------------------------- | ------------------------------------ | ----------- |
284
428
  | `routes` | `DevRoute[]` | `[]` | Navigable destinations. |
285
429
  | `commands` | `DevCommand[]` | `[]` | Custom actions. |
430
+ | `views` | `DevView[]` | `[]` | Custom panels, mounted while the popup is open. |
286
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. |
287
432
  | `devHostOnly`| `boolean` | `true` | Also require a localhost/LAN hostname, so dev builds on real URLs stay hidden. Ignored when `enabled` is set. |
288
433
  | `onNavigate` | `(path: string) => void` | `window.location.assign` | How to navigate on route select. |
@@ -292,17 +437,24 @@ a `data-devdock` attribute either way.
292
437
  | `hotkey` | `string \| null` | `null` | Toggle shortcut, e.g. `"mod+."`. |
293
438
  | `zIndex` | `number` | `2147483000` | Base z-index. |
294
439
  | `container` | `HTMLElement` | `document.body` | Where to mount. |
295
- | `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. |
296
441
 
297
442
  `DevRoute`: `{ path: string; label?: string; group?: string }`
298
443
 
299
- `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`.
300
447
 
301
448
  ### `@chuvenger/devdock` (core)
302
449
 
303
450
  - `createDevDock(options?): { update(options), destroy() }` — mounts the dock; no-ops during SSR (no `document`).
304
451
  - `matchHotkey(spec, event)` — the hotkey matcher (exported for reuse).
305
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
+
306
458
  ### `@chuvenger/devdock/react-router`
307
459
 
308
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"]}