@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.
- package/README.md +206 -16
- 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 +365 -52
- package/dist/react-router.cjs.map +1 -1
- package/dist/react-router.d.cts +58 -11
- package/dist/react-router.d.ts +58 -11
- package/dist/react-router.js +101 -8
- package/dist/react-router.js.map +1 -1
- 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
|
|
@@ -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.
|
|
275
|
-
|
|
276
|
-
|
|
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`
|
|
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"]}
|