@chuvenger/devdock 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruven Chu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # devdock
2
+
3
+ A tiny, dependency-free dev-only floating button. It sits in a corner of your
4
+ app, **only renders in development**, and opens a popup to jump between dev
5
+ routes and run custom commands.
6
+
7
+ - πŸ”’ **Dev-gated by default** β€” hidden when `process.env.NODE_ENV === "production"`. Fully overridable.
8
+ - 🧭 **Route jumper** β€” list routes manually, or auto-detect them from React Router.
9
+ - ⚑ **Custom commands** β€” register any action (reset DB, toggle a flag, copy a token…).
10
+ - πŸ”Ž Filter box + keyboard nav (↑/↓/Enter/Esc) and an optional global hotkey.
11
+ - 🎨 Zero CSS to import β€” inline-styled, high z-index, no runtime dependencies.
12
+ - πŸ›‘οΈ **Shadow DOM isolated** by default β€” the host page's CSS can't touch it (and vice versa).
13
+ - 🧩 **Framework-agnostic core** with thin **React** and **Svelte** adapters.
14
+
15
+ ## Architecture
16
+
17
+ The button, popup, filtering, keyboard handling, and dev-gating live in a
18
+ framework-free core (`createDevDock`) that builds DOM directly. The framework
19
+ adapters are tiny wrappers that drive its `create β†’ update β†’ destroy`
20
+ lifecycle:
21
+
22
+ | Import | What you get |
23
+ | ------ | ------------ |
24
+ | `@chuvenger/devdock` | `createDevDock(options)` β€” vanilla core, works anywhere |
25
+ | `@chuvenger/devdock/react` | `<DevDock />` React component |
26
+ | `@chuvenger/devdock/react-router` | `<ReactRouterDevDock />` + route auto-detection |
27
+ | `@chuvenger/devdock/svelte` | `use:devdock` Svelte action |
28
+
29
+ `react`, `react-dom`, `react-router-dom`, and `svelte` are all **optional** peer
30
+ deps β€” install only what your adapter needs.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ npm install @chuvenger/devdock
36
+ # or: bun add @chuvenger/devdock / pnpm add @chuvenger/devdock
37
+ ```
38
+
39
+ ## React
40
+
41
+ ```tsx
42
+ import { DevDock } from "@chuvenger/devdock/react";
43
+
44
+ export function App() {
45
+ return (
46
+ <>
47
+ {/* …your app… */}
48
+ <DevDock
49
+ routes={[
50
+ { path: "/admin", label: "Admin" },
51
+ { path: "/style-guide", label: "Style guide" },
52
+ ]}
53
+ commands={[
54
+ { label: "Reset onboarding", run: () => localStorage.clear() },
55
+ { label: "Log current user", run: async () => console.log(await getUser()) },
56
+ ]}
57
+ hotkey="mod+."
58
+ />
59
+ </>
60
+ );
61
+ }
62
+ ```
63
+
64
+ In production the component renders nothing, so it's safe to leave mounted.
65
+
66
+ ### React Router (auto-detected routes)
67
+
68
+ For a React Router **data router** (`createBrowserRouter`), use the dedicated
69
+ entry. It navigates through the SPA router (no full reload) and discovers your
70
+ routes automatically:
71
+
72
+ ```tsx
73
+ import { ReactRouterDevDock } from "@chuvenger/devdock/react-router";
74
+
75
+ function Layout() {
76
+ return (
77
+ <>
78
+ <Outlet />
79
+ <ReactRouterDevDock commands={[{ label: "Reload", run: () => location.reload() }]} />
80
+ </>
81
+ );
82
+ }
83
+ ```
84
+
85
+ Render it inside the router tree. Dynamic (`:id`) and splat (`*`) routes are
86
+ skipped since they need arguments. Customize per route via React Router's
87
+ `handle`:
88
+
89
+ ```tsx
90
+ { path: "reports", element: <Reports />, handle: { devLabel: "Reports", devGroup: "Internal", hidden: false } }
91
+ ```
92
+
93
+ **Scope which routes are detected** with `match` β€” a glob (`*` = any
94
+ characters), a `RegExp`, or a predicate. Manual `routes` are always included:
95
+
96
+ ```tsx
97
+ <ReactRouterDevDock match="/dev/*" /> // only routes under /dev
98
+ <ReactRouterDevDock match={/^\/(admin|dev)/} />
99
+ <ReactRouterDevDock match={(r) => r.path.length < 20} />
100
+ ```
101
+
102
+ ## Svelte
103
+
104
+ `@chuvenger/devdock/svelte` exports a Svelte action β€” no extra build step, works
105
+ in any Svelte 4/5 app:
106
+
107
+ ```svelte
108
+ <script lang="ts">
109
+ import { devdock } from "@chuvenger/devdock/svelte";
110
+
111
+ const options = {
112
+ routes: [{ path: "/admin", label: "Admin" }],
113
+ commands: [{ label: "Reset", run: () => localStorage.clear() }],
114
+ hotkey: "mod+.",
115
+ onNavigate: (path) => goto(path), // e.g. SvelteKit's goto
116
+ };
117
+ </script>
118
+
119
+ <!-- The dock mounts to document.body; this element is just an anchor. -->
120
+ <div use:devdock={options}></div>
121
+ ```
122
+
123
+ Reactive `options` flow through automatically (the action's `update` re-renders).
124
+
125
+ ## Vanilla / any framework
126
+
127
+ ```ts
128
+ import { createDevDock } from "@chuvenger/devdock";
129
+
130
+ const dock = createDevDock({
131
+ routes: [{ path: "/admin", label: "Admin" }],
132
+ commands: [{ label: "Reset", run: () => localStorage.clear() }],
133
+ });
134
+
135
+ // later…
136
+ dock.update({ enabled: false }); // hide
137
+ dock.destroy(); // remove entirely
138
+ ```
139
+
140
+ Use this to build a Vue/Angular/web-component adapter, or wire it up by hand.
141
+
142
+ ## Controlling when it shows
143
+
144
+ `enabled` overrides the default `NODE_ENV` check β€” pass a boolean or a
145
+ predicate (re-evaluated on every update):
146
+
147
+ ```tsx
148
+ // Show in dev, plus in prod when ?debug is present:
149
+ <DevDock enabled={import.meta.env.DEV || location.search.includes("debug")} />
150
+
151
+ // Or a callback, e.g. gate on a role or a runtime flag:
152
+ <DevDock enabled={() => currentUser?.isStaff ?? false} />
153
+ ```
154
+
155
+ To strip it from production bundles entirely, gate the element so the minifier
156
+ tree-shakes it: `{import.meta.env.DEV && <DevDock … />}`.
157
+
158
+ ## Styling & isolation
159
+
160
+ The dock is styled entirely with inline styles (no stylesheet to import) and,
161
+ by default, renders inside an **open shadow root** attached to a host element in
162
+ `document.body`. That means the host page's CSS can't leak in and the dock's
163
+ styles can't leak out. Pass `shadow: false` to render in the light DOM instead
164
+ (e.g. if you want to override its look from the page). The host element carries
165
+ a `data-devdock` attribute either way.
166
+
167
+ ## API
168
+
169
+ ### Options (shared by every adapter)
170
+
171
+ | Option | Type | Default | Description |
172
+ | ------------ | ----------------------------- | ------------------------------------ | ----------- |
173
+ | `routes` | `DevRoute[]` | `[]` | Navigable destinations. |
174
+ | `commands` | `DevCommand[]` | `[]` | Custom actions. |
175
+ | `enabled` | `boolean \| (() => boolean)` | `NODE_ENV !== "production"` | Whether to render at all. A predicate is re-checked on every update. |
176
+ | `onNavigate` | `(path: string) => void` | `window.location.assign` | How to navigate on route select. |
177
+ | `position` | `"bottom-left" \| "bottom-right" \| "top-left" \| "top-right"` | `"bottom-left"` | Corner. |
178
+ | `label` | `string` | `"DEV"` | Button text. |
179
+ | `title` | `string` | `"Dev Menu"` | Popup heading. |
180
+ | `hotkey` | `string \| null` | `null` | Toggle shortcut, e.g. `"mod+."`. |
181
+ | `zIndex` | `number` | `2147483000` | Base z-index. |
182
+ | `container` | `HTMLElement` | `document.body` | Where to mount. |
183
+ | `shadow` | `boolean` | `true` | Render in a shadow root (CSS-isolated). Set `false` for light DOM. |
184
+
185
+ `DevRoute`: `{ path: string; label?: string; group?: string }`
186
+
187
+ `DevCommand`: `{ label: string; run: () => void | Promise<void>; id?: string; group?: string; keepOpen?: boolean }`
188
+
189
+ ### `@chuvenger/devdock` (core)
190
+
191
+ - `createDevDock(options?): { update(options), destroy() }` β€” mounts the dock; no-ops during SSR (no `document`).
192
+ - `matchHotkey(spec, event)` β€” the hotkey matcher (exported for reuse).
193
+
194
+ ### `@chuvenger/devdock/react-router`
195
+
196
+ - `ReactRouterDevDock(props)` β€” `DevDock` wired to `useNavigate()` + auto-detected routes. Extra prop: `match?: RouteMatcher`.
197
+ - `useDetectedRoutes(match?): DevRoute[]` β€” the detected routes, optionally scoped (build your own UI).
198
+ - `flattenRoutes(routes, parent?): DevRoute[]` β€” pure helper that flattens a route tree.
199
+ - `RouteMatcher` β€” `string | RegExp | ((route: DevRoute) => boolean)`.
200
+
201
+ ## Examples
202
+
203
+ Runnable demos consuming the library straight from source live in
204
+ [`examples/`](./examples):
205
+
206
+ ```bash
207
+ cd examples/react && bun install && bun run dev # React + React Router
208
+ cd examples/svelte && bun install && bun run dev # Svelte 5 (use:devdock)
209
+ ```
210
+
211
+ ## Development
212
+
213
+ ```bash
214
+ bun install
215
+ bun run test # vitest (core + react + svelte + react-router)
216
+ bun run build # tsup β†’ dist (ESM + CJS + d.ts, one entry per adapter)
217
+ bun run typecheck
218
+ ```
219
+
220
+ ## License
221
+
222
+ MIT
@@ -0,0 +1,391 @@
1
+ // src/core/hotkey.ts
2
+ function matchHotkey(spec, e) {
3
+ const parts = spec.toLowerCase().split("+").map((p) => p.trim()).filter(Boolean);
4
+ if (parts.length === 0) return false;
5
+ const key = parts[parts.length - 1];
6
+ const mods = new Set(parts.slice(0, -1));
7
+ const isMac = typeof navigator !== "undefined" && /mac/i.test(navigator.platform);
8
+ const wantMeta = mods.has("meta") || mods.has("cmd") || mods.has("mod") && isMac;
9
+ const wantCtrl = mods.has("ctrl") || mods.has("control") || mods.has("mod") && !isMac;
10
+ const wantShift = mods.has("shift");
11
+ const wantAlt = mods.has("alt") || mods.has("option");
12
+ if (e.metaKey !== wantMeta) return false;
13
+ if (e.ctrlKey !== wantCtrl) return false;
14
+ if (e.shiftKey !== wantShift) return false;
15
+ if (e.altKey !== wantAlt) return false;
16
+ return e.key.toLowerCase() === key;
17
+ }
18
+
19
+ // src/core/dock.ts
20
+ var isDev = () => {
21
+ try {
22
+ return process.env.NODE_ENV !== "production";
23
+ } catch {
24
+ return false;
25
+ }
26
+ };
27
+ function el(tag, styles, props) {
28
+ const node = document.createElement(tag);
29
+ if (styles) Object.assign(node.style, styles);
30
+ if (props) Object.assign(node, props);
31
+ return node;
32
+ }
33
+ function cornerStyle(position) {
34
+ const gap = "16px";
35
+ switch (position) {
36
+ case "bottom-right":
37
+ return { bottom: gap, right: gap };
38
+ case "top-left":
39
+ return { top: gap, left: gap };
40
+ case "top-right":
41
+ return { top: gap, right: gap };
42
+ default:
43
+ return { bottom: gap, left: gap };
44
+ }
45
+ }
46
+ function panelAnchor(position) {
47
+ const isTop = position.startsWith("top");
48
+ const isRight = position.endsWith("right");
49
+ return {
50
+ position: "absolute",
51
+ [isTop ? "top" : "bottom"]: "calc(100% + 8px)",
52
+ [isRight ? "right" : "left"]: "0"
53
+ };
54
+ }
55
+ var FONT = "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
56
+ function createDevDock(initial = {}) {
57
+ if (typeof document === "undefined") {
58
+ return { update() {
59
+ }, destroy() {
60
+ } };
61
+ }
62
+ let opts = { ...initial };
63
+ let open = false;
64
+ let query = "";
65
+ let active = 0;
66
+ let mounted = false;
67
+ const host = el("div");
68
+ host.setAttribute("data-devdock", "");
69
+ const useShadow = (initial.shadow ?? true) && typeof host.attachShadow === "function";
70
+ const shadowRoot = useShadow ? host.attachShadow({ mode: "open" }) : null;
71
+ const mountPoint = shadowRoot ?? host;
72
+ const root = el("div", { fontFamily: FONT });
73
+ mountPoint.appendChild(root);
74
+ const panel = el("div", {
75
+ width: "280px",
76
+ maxHeight: "min(70vh, 480px)",
77
+ display: "flex",
78
+ flexDirection: "column",
79
+ background: "#16181d",
80
+ color: "#e7e9ee",
81
+ border: "1px solid #2a2e37",
82
+ borderRadius: "12px",
83
+ boxShadow: "0 12px 40px rgba(0,0,0,0.45)",
84
+ overflow: "hidden"
85
+ });
86
+ panel.setAttribute("role", "dialog");
87
+ const heading = el("div", {
88
+ padding: "10px 12px",
89
+ borderBottom: "1px solid #2a2e37",
90
+ fontSize: "12px",
91
+ fontWeight: "600",
92
+ letterSpacing: "0.4px",
93
+ textTransform: "uppercase",
94
+ color: "#8b909c"
95
+ });
96
+ const inputWrap = el("div", { padding: "8px" });
97
+ const input = el("input", {
98
+ width: "100%",
99
+ boxSizing: "border-box",
100
+ padding: "8px 10px",
101
+ fontSize: "13px",
102
+ color: "#e7e9ee",
103
+ background: "#0f1115",
104
+ border: "1px solid #2a2e37",
105
+ borderRadius: "8px",
106
+ outline: "none"
107
+ });
108
+ input.placeholder = "Filter\u2026";
109
+ inputWrap.appendChild(input);
110
+ const list = el("div", { overflowY: "auto", padding: "0 6px 8px" });
111
+ panel.append(heading, inputWrap, list);
112
+ const button = el("button", {
113
+ display: "inline-flex",
114
+ alignItems: "center",
115
+ gap: "6px",
116
+ padding: "8px 12px",
117
+ fontSize: "12px",
118
+ fontWeight: "700",
119
+ letterSpacing: "0.6px",
120
+ color: "#fff",
121
+ background: "#111317",
122
+ border: "1px solid #2a2e37",
123
+ borderRadius: "999px",
124
+ boxShadow: "0 4px 14px rgba(0,0,0,0.35)",
125
+ cursor: "pointer"
126
+ });
127
+ button.type = "button";
128
+ button.setAttribute("aria-label", "Toggle dev menu");
129
+ const dot = el("span", {
130
+ width: "8px",
131
+ height: "8px",
132
+ borderRadius: "999px",
133
+ background: "#22c55e",
134
+ display: "inline-block"
135
+ });
136
+ const buttonLabel = el("span");
137
+ button.append(dot, buttonLabel);
138
+ root.appendChild(button);
139
+ const enabled = () => {
140
+ const e = opts.enabled;
141
+ if (typeof e === "function") return e();
142
+ return e ?? isDev();
143
+ };
144
+ const navigate = (path) => {
145
+ if (opts.onNavigate) opts.onNavigate(path);
146
+ else if (typeof window !== "undefined") window.location.assign(path);
147
+ };
148
+ const buildItems = () => {
149
+ const out = [];
150
+ (opts.routes ?? []).forEach((r, i) => {
151
+ out.push({
152
+ kind: "route",
153
+ key: `route:${r.path}:${i}`,
154
+ label: r.label ?? r.path,
155
+ group: r.group ?? "Routes",
156
+ route: r
157
+ });
158
+ });
159
+ (opts.commands ?? []).forEach((c, i) => {
160
+ out.push({
161
+ kind: "command",
162
+ key: `cmd:${c.id ?? c.label}:${i}`,
163
+ label: c.label,
164
+ group: c.group ?? "Commands",
165
+ command: c
166
+ });
167
+ });
168
+ return out;
169
+ };
170
+ const getFiltered = () => {
171
+ const q = query.trim().toLowerCase();
172
+ const items = buildItems();
173
+ if (!q) return items;
174
+ return items.filter(
175
+ (it) => it.label.toLowerCase().includes(q) || it.kind === "route" && it.route.path.toLowerCase().includes(q)
176
+ );
177
+ };
178
+ const runItem = async (it) => {
179
+ if (it.kind === "route") {
180
+ navigate(it.route.path);
181
+ setOpen(false);
182
+ return;
183
+ }
184
+ try {
185
+ await it.command.run();
186
+ } finally {
187
+ if (!it.command.keepOpen) setOpen(false);
188
+ }
189
+ };
190
+ const renderList = () => {
191
+ const filtered = getFiltered();
192
+ active = Math.min(active, Math.max(0, filtered.length - 1));
193
+ list.replaceChildren();
194
+ if (filtered.length === 0) {
195
+ list.appendChild(
196
+ el(
197
+ "div",
198
+ { padding: "12px", fontSize: "13px", color: "#6b7280" },
199
+ { textContent: "Nothing here." }
200
+ )
201
+ );
202
+ return;
203
+ }
204
+ const groups = /* @__PURE__ */ new Map();
205
+ for (const it of filtered) {
206
+ const arr = groups.get(it.group) ?? [];
207
+ arr.push(it);
208
+ groups.set(it.group, arr);
209
+ }
210
+ for (const [groupName, groupItems] of groups) {
211
+ const section = el("div", { marginTop: "6px" });
212
+ section.appendChild(
213
+ el(
214
+ "div",
215
+ {
216
+ padding: "6px 8px 2px",
217
+ fontSize: "10px",
218
+ fontWeight: "700",
219
+ letterSpacing: "0.6px",
220
+ textTransform: "uppercase",
221
+ color: "#6b7280"
222
+ },
223
+ { textContent: groupName }
224
+ )
225
+ );
226
+ for (const it of groupItems) {
227
+ const idx = filtered.indexOf(it);
228
+ const isActive = idx === active;
229
+ const row = el("button", {
230
+ display: "flex",
231
+ alignItems: "center",
232
+ justifyContent: "space-between",
233
+ gap: "8px",
234
+ width: "100%",
235
+ textAlign: "left",
236
+ padding: "8px 10px",
237
+ fontSize: "13px",
238
+ color: "#e7e9ee",
239
+ background: isActive ? "#2563eb" : "transparent",
240
+ border: "none",
241
+ borderRadius: "8px",
242
+ cursor: "pointer"
243
+ });
244
+ row.type = "button";
245
+ const labelSpan = el(
246
+ "span",
247
+ {
248
+ overflow: "hidden",
249
+ textOverflow: "ellipsis",
250
+ whiteSpace: "nowrap"
251
+ },
252
+ { textContent: it.label }
253
+ );
254
+ row.appendChild(labelSpan);
255
+ if (it.kind === "route" && it.route.path !== it.label) {
256
+ row.appendChild(
257
+ el(
258
+ "span",
259
+ {
260
+ fontSize: "11px",
261
+ color: isActive ? "#cfe0ff" : "#6b7280",
262
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
263
+ flexShrink: "0"
264
+ },
265
+ { textContent: it.route.path }
266
+ )
267
+ );
268
+ }
269
+ row.addEventListener("mouseenter", () => {
270
+ active = idx;
271
+ renderList();
272
+ });
273
+ row.addEventListener("click", () => void runItem(it));
274
+ section.appendChild(row);
275
+ }
276
+ list.appendChild(section);
277
+ }
278
+ };
279
+ const renderShell = () => {
280
+ const position = opts.position ?? "bottom-left";
281
+ const zIndex = opts.zIndex ?? 2147483e3;
282
+ Object.assign(root.style, {
283
+ position: "fixed",
284
+ zIndex: String(zIndex),
285
+ top: "",
286
+ bottom: "",
287
+ left: "",
288
+ right: ""
289
+ });
290
+ Object.assign(root.style, cornerStyle(position));
291
+ heading.textContent = opts.title ?? "Dev Menu";
292
+ panel.setAttribute("aria-label", opts.title ?? "Dev Menu");
293
+ Object.assign(panel.style, panelAnchor(position));
294
+ buttonLabel.textContent = opts.label ?? "DEV";
295
+ button.style.background = open ? "#2563eb" : "#111317";
296
+ button.setAttribute("aria-expanded", String(open));
297
+ };
298
+ function setOpen(next) {
299
+ if (open === next) return;
300
+ open = next;
301
+ if (open) {
302
+ query = "";
303
+ input.value = "";
304
+ active = 0;
305
+ panel.isConnected || root.insertBefore(panel, button);
306
+ renderShell();
307
+ renderList();
308
+ requestAnimationFrame(() => input.focus());
309
+ } else if (panel.isConnected) {
310
+ panel.remove();
311
+ }
312
+ renderShell();
313
+ }
314
+ input.addEventListener("input", () => {
315
+ query = input.value;
316
+ active = 0;
317
+ renderList();
318
+ });
319
+ panel.addEventListener("keydown", (e) => {
320
+ const filtered = getFiltered();
321
+ if (e.key === "Escape") {
322
+ e.preventDefault();
323
+ setOpen(false);
324
+ } else if (e.key === "ArrowDown") {
325
+ e.preventDefault();
326
+ active = Math.min(active + 1, filtered.length - 1);
327
+ renderList();
328
+ } else if (e.key === "ArrowUp") {
329
+ e.preventDefault();
330
+ active = Math.max(active - 1, 0);
331
+ renderList();
332
+ } else if (e.key === "Enter") {
333
+ e.preventDefault();
334
+ const it = filtered[active];
335
+ if (it) void runItem(it);
336
+ }
337
+ });
338
+ button.addEventListener("click", () => setOpen(!open));
339
+ const onDocMouseDown = (e) => {
340
+ if (open && !e.composedPath().includes(root)) setOpen(false);
341
+ };
342
+ const onKeyDown = (e) => {
343
+ if (opts.hotkey && matchHotkey(opts.hotkey, e)) {
344
+ e.preventDefault();
345
+ setOpen(!open);
346
+ }
347
+ };
348
+ const addListeners = () => {
349
+ document.addEventListener("mousedown", onDocMouseDown);
350
+ window.addEventListener("keydown", onKeyDown);
351
+ };
352
+ const removeListeners = () => {
353
+ document.removeEventListener("mousedown", onDocMouseDown);
354
+ window.removeEventListener("keydown", onKeyDown);
355
+ };
356
+ const mount = () => {
357
+ if (mounted) return;
358
+ (opts.container ?? document.body).appendChild(host);
359
+ addListeners();
360
+ mounted = true;
361
+ };
362
+ const unmount = () => {
363
+ if (!mounted) return;
364
+ removeListeners();
365
+ host.remove();
366
+ mounted = false;
367
+ };
368
+ renderShell();
369
+ if (enabled()) mount();
370
+ return {
371
+ update(next) {
372
+ opts = { ...opts, ...next };
373
+ if (enabled()) {
374
+ mount();
375
+ renderShell();
376
+ if (open) renderList();
377
+ } else {
378
+ setOpen(false);
379
+ unmount();
380
+ }
381
+ },
382
+ destroy() {
383
+ setOpen(false);
384
+ unmount();
385
+ }
386
+ };
387
+ }
388
+
389
+ export { createDevDock, matchHotkey };
390
+ //# sourceMappingURL=chunk-NJ3FCNK6.js.map
391
+ //# sourceMappingURL=chunk-NJ3FCNK6.js.map