@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.
- package/README.md +166 -14
- package/dist/chunk-3MVFATRN.js +60 -0
- package/dist/chunk-3MVFATRN.js.map +1 -0
- package/dist/{chunk-WVRXVSEL.js → chunk-NXZNFF34.js} +229 -42
- package/dist/chunk-NXZNFF34.js.map +1 -0
- package/dist/{dock-DOWeFZJ5.d.ts → dock-P6l6pKjS.d.ts} +1 -1
- package/dist/{dock-dq6OS0Jm.d.cts → dock-d3HqT25k.d.cts} +1 -1
- package/dist/index.cjs +226 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{match-BZoH6rW7.d.cts → match-DDSJIRYW.d.cts} +1 -1
- package/dist/{match-D1am9aTf.d.ts → match-DwGIF1Xt.d.ts} +1 -1
- package/dist/react-router.cjs +267 -47
- package/dist/react-router.cjs.map +1 -1
- package/dist/react-router.d.cts +2 -2
- package/dist/react-router.d.ts +2 -2
- package/dist/react-router.js +2 -2
- package/dist/react.cjs +268 -47
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +28 -8
- package/dist/react.d.ts +28 -8
- package/dist/react.js +2 -2
- package/dist/svelte.cjs +226 -39
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.d.cts +3 -3
- package/dist/svelte.d.ts +3 -3
- package/dist/svelte.js +2 -2
- package/dist/svelte.js.map +1 -1
- package/dist/sveltekit.cjs +226 -39
- package/dist/sveltekit.cjs.map +1 -1
- package/dist/sveltekit.d.cts +4 -4
- package/dist/sveltekit.d.ts +4 -4
- package/dist/sveltekit.js +2 -2
- package/dist/sveltekit.js.map +1 -1
- package/dist/{types-B-V99upW.d.cts → types-B_SpF32A.d.cts} +73 -5
- package/dist/{types-B-V99upW.d.ts → types-B_SpF32A.d.ts} +73 -5
- package/package.json +1 -1
- package/dist/chunk-P4ZUS4GB.js +0 -24
- package/dist/chunk-P4ZUS4GB.js.map +0 -1
- 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.
|
|
275
|
-
|
|
276
|
-
|
|
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`
|
|
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"]}
|