@chuvenger/devdock 0.4.1 → 0.6.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 (43) hide show
  1. package/README.md +206 -16
  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 +365 -52
  16. package/dist/react-router.cjs.map +1 -1
  17. package/dist/react-router.d.cts +58 -11
  18. package/dist/react-router.d.ts +58 -11
  19. package/dist/react-router.js +101 -8
  20. package/dist/react-router.js.map +1 -1
  21. package/dist/react.cjs +268 -47
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +28 -8
  24. package/dist/react.d.ts +28 -8
  25. package/dist/react.js +2 -2
  26. package/dist/svelte.cjs +226 -39
  27. package/dist/svelte.cjs.map +1 -1
  28. package/dist/svelte.d.cts +3 -3
  29. package/dist/svelte.d.ts +3 -3
  30. package/dist/svelte.js +2 -2
  31. package/dist/svelte.js.map +1 -1
  32. package/dist/sveltekit.cjs +226 -39
  33. package/dist/sveltekit.cjs.map +1 -1
  34. package/dist/sveltekit.d.cts +4 -4
  35. package/dist/sveltekit.d.ts +4 -4
  36. package/dist/sveltekit.js +2 -2
  37. package/dist/sveltekit.js.map +1 -1
  38. package/dist/{types-B-V99upW.d.cts → types-B_SpF32A.d.cts} +73 -5
  39. package/dist/{types-B-V99upW.d.ts → types-B_SpF32A.d.ts} +73 -5
  40. package/package.json +1 -1
  41. package/dist/chunk-P4ZUS4GB.js +0 -24
  42. package/dist/chunk-P4ZUS4GB.js.map +0 -1
  43. 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
@@ -132,6 +133,43 @@ arguments — to reach one, use a command that navigates to a known id:
132
133
  <ReactRouterDevDock staticRoutes match={(r) => r.path.length < 20} />
133
134
  ```
134
135
 
136
+ ### React Router framework mode (`app/routes.ts`)
137
+
138
+ Framework mode works, with one wrinkle: its routes are **lazy**. `handle` is an
139
+ export of the route *module*, so until that module loads it isn't on the route
140
+ object the adapter reads — a `handle.devLabel` on a page you haven't opened has
141
+ nothing to take effect on. Such routes still list by path convention (`/dev/*`),
142
+ labelled with their path.
143
+
144
+ `loadHandles` closes the gap by importing the lazy route modules to read their
145
+ `handle`:
146
+
147
+ ```tsx
148
+ <ReactRouterDevDock loadHandles />
149
+ ```
150
+
151
+ ```tsx
152
+ // app/routes/dev.style-guide.tsx
153
+ export const handle = { devLabel: "Style Guide", devGroup: "Internal" };
154
+ ```
155
+
156
+ It's opt-in because reading a handle up front means importing the module up
157
+ front — every lazy route's module, and any side effect its import carries. That
158
+ only ever happens in development (the dock doesn't render in production), but it
159
+ is real work on a dev page load. Leave it off if path labels are fine; turn it
160
+ on if you want `devLabel` / `devGroup`, or `handle.dev` to mark a route that
161
+ isn't under `/dev` (without it, such a route is never seen at all).
162
+
163
+ One thing it can't do: list a route the router hasn't discovered yet. Framework
164
+ mode fetches route branches as links render, so the dock watches the tree and
165
+ picks them up as they arrive — but a dev route nothing links to stays unknown
166
+ until you navigate there. Ship the whole tree up front with
167
+ `routeDiscovery: { mode: "initial" }` in `react-router.config.ts`.
168
+
169
+ `loadHandles` is not framework-only: a plain data router with
170
+ `lazy: () => import("./route")` hides `handle` exactly the same way, and the
171
+ flag reads it the same way.
172
+
135
173
  ## Svelte
136
174
 
137
175
  `@chuvenger/devdock/svelte` exports a Svelte action — no extra build step, works
@@ -226,6 +264,140 @@ dock.destroy(); // remove entirely
226
264
 
227
265
  Use this to build a Vue/Angular/web-component adapter, or wire it up by hand.
228
266
 
267
+ ## Stateful commands
268
+
269
+ A command is normally fire-and-forget. Two optional fields turn one into a
270
+ control that shows its own state — both are re-read on every render of the open
271
+ panel, so the row updates in place:
272
+
273
+ ```tsx
274
+ <DevDock
275
+ commands={[
276
+ // A function label: the row shows live state.
277
+ { id: "theme", label: () => `Theme: ${theme}`, keepOpen: true, run: toggleTheme },
278
+ // `checked` renders a check mark and makes the row a real toggle
279
+ // (role="checkbox" + aria-checked).
280
+ { id: "freeze", label: "Freeze animations", checked: () => frozen, keepOpen: true, run: freeze },
281
+ ]}
282
+ />
283
+ ```
284
+
285
+ Pair them with `keepOpen: true` so the popup stays put and you can watch the
286
+ value change. Give a command with a function `label` an explicit `id` — the id
287
+ is what keeps the row stable when the text changes.
288
+
289
+ ## Custom views
290
+
291
+ Some state has no row shape at all: a frame-rate meter, four lines of sync
292
+ facts, a segmented control. Pass those in as **views** — your own UI, mounted
293
+ inside the popup, keeping the dock's shell, hotkey, filter, route detection and
294
+ dev-gating.
295
+
296
+ Views are mounted when the popup **opens** and torn down when it **closes**,
297
+ which is what makes a `requestAnimationFrame` loop or an event listener inside
298
+ one correct rather than a leak.
299
+
300
+ ### React
301
+
302
+ ```tsx
303
+ <DevDock
304
+ shadow="inherit" // ← see below: views need the app's CSS
305
+ views={[
306
+ { id: "fps", label: "Frame rate", render: () => <FrameRate /> },
307
+ { id: "theme", label: "Theme", render: () => <ThemeControl value={theme} onChange={setTheme} /> },
308
+ // Two views under one "Session" heading, below the routes/commands list.
309
+ { id: "facts", group: "Session", order: "after", render: () => <SyncFacts /> },
310
+ { id: "signin", group: "Session", order: "after", render: () => <SignInButton /> },
311
+ ]}
312
+ />
313
+ ```
314
+
315
+ The component is rendered with `createPortal` into a host element the dock
316
+ owns, so it stays part of your React tree: context, hooks and state all work,
317
+ and it re-renders when the component holding `<DevDock>` does. A view whose
318
+ `id` is unchanged is **never remounted**, so re-rendering the parent is free.
319
+
320
+ ### Vanilla / Svelte
321
+
322
+ The core contract is framework-free: fill the host element, return a teardown.
323
+
324
+ ```ts
325
+ createDevDock({
326
+ views: [
327
+ {
328
+ id: "fps",
329
+ label: "Frame rate",
330
+ mount: (host) => {
331
+ let raf = requestAnimationFrame(function tick() {
332
+ host.textContent = `${fps()} fps`;
333
+ raf = requestAnimationFrame(tick);
334
+ });
335
+ return () => cancelAnimationFrame(raf); // ← runs when the popup closes
336
+ },
337
+ },
338
+ ],
339
+ });
340
+ ```
341
+
342
+ In Svelte the same field takes a component (Svelte 5 shown; on Svelte 4 use
343
+ `new MyPanel({ target: host })` and `component.$destroy()`):
344
+
345
+ ```svelte
346
+ <script lang="ts">
347
+ import { mount, unmount } from "svelte";
348
+ import { devdock } from "@chuvenger/devdock/svelte";
349
+ import SyncFacts from "./SyncFacts.svelte";
350
+
351
+ const options = {
352
+ shadow: "inherit",
353
+ views: [
354
+ {
355
+ id: "facts",
356
+ label: "Session",
357
+ mount: (host) => {
358
+ const view = mount(SyncFacts, { target: host });
359
+ return () => unmount(view);
360
+ },
361
+ },
362
+ ],
363
+ };
364
+ </script>
365
+
366
+ <div use:devdock={options}></div>
367
+ ```
368
+
369
+ ### Views and your CSS
370
+
371
+ The dock renders inside a shadow root, and **the document's stylesheet does not
372
+ cross that boundary** — a view that renders your app's own components renders
373
+ unstyled. If your view needs the host app's CSS, say so:
374
+
375
+ - `shadow="inherit"` — keep the shadow root, but copy the document's styles
376
+ into it (`document.adoptedStyleSheets` plus every `<style>` and
377
+ `<link rel="stylesheet">`, re-synced as your bundler injects or edits them).
378
+ - `shadow={false}` — render in the light DOM, where the page's CSS applies
379
+ directly.
380
+
381
+ Isolation stays the default. Two things to know about `"inherit"`:
382
+
383
+ - The page's CSS can now reach the dock's own chrome too, and your design
384
+ system's tokens arrive with the page's assumptions — a light-theme token
385
+ lands on the dock's dark panel. Scope the tokens you need onto the view's
386
+ own root.
387
+ - **The rules cross the boundary; the element tree does not.** A selector
388
+ anchored on an ancestor outside the shadow root —
389
+ `html[data-theme="dark"] .card`, `body.compact .row` — still won't match
390
+ inside it. Re-apply that state on the view's own root, where it can.
391
+
392
+ `shadow` is read once, when the dock is created: a shadow root can't be
393
+ detached, so `update()` ignores it.
394
+
395
+ > Testing a view with `@testing-library/user-event`? It can't type into a
396
+ > shadow root — it installs its value interceptor from a `focus` listener on
397
+ > the document, where the event has been retargeted to the shadow host. Render
398
+ > with `shadow: false` in tests, or write the value through the native setter
399
+ > and dispatch `input` yourself.
400
+
229
401
  ## Controlling when it shows
230
402
 
231
403
  By default the dock renders only when **both** of these hold:
@@ -271,9 +443,18 @@ tree-shakes it: `{import.meta.env.DEV && <DevDock … />}`.
271
443
  The dock is styled entirely with inline styles (no stylesheet to import) and,
272
444
  by default, renders inside an **open shadow root** attached to a host element in
273
445
  `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.
446
+ styles can't leak out. The host element carries a `data-devdock` attribute in
447
+ every mode.
448
+
449
+ | `shadow` | Where it renders | The page's CSS |
450
+ | -------- | ---------------- | -------------- |
451
+ | `true` (default) | shadow root | kept out |
452
+ | `"inherit"` | shadow root | copied in, and kept in sync |
453
+ | `false` | light DOM | applies directly |
454
+
455
+ Use `"inherit"` (or `false`) when a [custom view](#views-and-your-css) renders
456
+ your app's own components and needs its stylesheet. Otherwise leave it alone —
457
+ isolation is the reason the dock survives arbitrary host CSS.
277
458
 
278
459
  ## API
279
460
 
@@ -283,6 +464,7 @@ a `data-devdock` attribute either way.
283
464
  | ------------ | ----------------------------- | ------------------------------------ | ----------- |
284
465
  | `routes` | `DevRoute[]` | `[]` | Navigable destinations. |
285
466
  | `commands` | `DevCommand[]` | `[]` | Custom actions. |
467
+ | `views` | `DevView[]` | `[]` | Custom panels, mounted while the popup is open. |
286
468
  | `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
469
  | `devHostOnly`| `boolean` | `true` | Also require a localhost/LAN hostname, so dev builds on real URLs stay hidden. Ignored when `enabled` is set. |
288
470
  | `onNavigate` | `(path: string) => void` | `window.location.assign` | How to navigate on route select. |
@@ -292,21 +474,29 @@ a `data-devdock` attribute either way.
292
474
  | `hotkey` | `string \| null` | `null` | Toggle shortcut, e.g. `"mod+."`. |
293
475
  | `zIndex` | `number` | `2147483000` | Base z-index. |
294
476
  | `container` | `HTMLElement` | `document.body` | Where to mount. |
295
- | `shadow` | `boolean` | `true` | Render in a shadow root (CSS-isolated). Set `false` for light DOM. |
477
+ | `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
478
 
297
479
  `DevRoute`: `{ path: string; label?: string; group?: string }`
298
480
 
299
- `DevCommand`: `{ label: string; run: () => void | Promise<void>; id?: string; group?: string; keepOpen?: boolean }`
481
+ `DevCommand`: `{ label: string | (() => string); run: () => void | Promise<void>; checked?: () => boolean; id?: string; group?: string; keepOpen?: boolean }`
482
+
483
+ `DevView`: `{ mount: (host: HTMLElement) => void | (() => void); id?: string; label?: string; group?: string; order?: "before" | "after" }` — on the React adapter, `render: () => ReactNode` replaces `mount`.
300
484
 
301
485
  ### `@chuvenger/devdock` (core)
302
486
 
303
487
  - `createDevDock(options?): { update(options), destroy() }` — mounts the dock; no-ops during SSR (no `document`).
304
488
  - `matchHotkey(spec, event)` — the hotkey matcher (exported for reuse).
305
489
 
490
+ ### `@chuvenger/devdock/react`
491
+
492
+ - `DevDock(props)` — the component. Same options as the core, with `views` taking `render: () => ReactNode` instead of `mount`.
493
+ - `DevDockView` — the React view shape.
494
+
306
495
  ### `@chuvenger/devdock/react-router`
307
496
 
308
- - `ReactRouterDevDock(props)` — `DevDock` wired to `useNavigate()` + auto-detected routes. Extra props: `match?: RouteMatcher`, `staticRoutes?: boolean`.
309
- - `useDetectedRoutes(options?): DevRoute[]` — the detected routes (build your own UI). Takes `{ match?, staticRoutes? }`, or a bare matcher.
497
+ - `ReactRouterDevDock(props)` — `DevDock` wired to `useNavigate()` + auto-detected routes. Extra props: `match?: RouteMatcher`, `staticRoutes?: boolean`, `loadHandles?: boolean`.
498
+ - `useDetectedRoutes(options?): DevRoute[]` — the detected routes (build your own UI). Takes `{ match?, staticRoutes?, loadHandles? }`, or a bare matcher. Re-reads the tree as the router grows it.
499
+ - `loadHandles` — import lazy route modules to read their `handle`; needed for `handle` in [framework mode](#react-router-framework-mode-approutests).
310
500
  - `flattenRoutes(routes, parent?, options?): DevRoute[]` — pure helper that flattens a route tree.
311
501
  - `RouteMatcher` — `string | RegExp | ((route: DevRoute) => boolean)`.
312
502
 
@@ -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"]}