@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 +21 -0
- package/README.md +222 -0
- package/dist/chunk-NJ3FCNK6.js +391 -0
- package/dist/chunk-NJ3FCNK6.js.map +1 -0
- package/dist/chunk-SR3BZ3C5.js +24 -0
- package/dist/chunk-SR3BZ3C5.js.map +1 -0
- package/dist/dock-elbtMGsE.d.ts +10 -0
- package/dist/dock-mQl2WU8n.d.cts +10 -0
- package/dist/index.cjs +394 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react-router.cjs +481 -0
- package/dist/react-router.cjs.map +1 -0
- package/dist/react-router.d.cts +49 -0
- package/dist/react-router.d.ts +49 -0
- package/dist/react-router.js +71 -0
- package/dist/react-router.js.map +1 -0
- package/dist/react.cjs +416 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +12 -0
- package/dist/react.d.ts +12 -0
- package/dist/react.js +4 -0
- package/dist/react.js.map +1 -0
- package/dist/svelte.cjs +407 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.d.cts +27 -0
- package/dist/svelte.d.ts +27 -0
- package/dist/svelte.js +19 -0
- package/dist/svelte.js.map +1 -0
- package/dist/types-DYQBDvfK.d.cts +73 -0
- package/dist/types-DYQBDvfK.d.ts +73 -0
- package/package.json +105 -0
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
|