@hansenexus/hud 0.3.0 → 0.4.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 +186 -4
- package/dist/access-CNvJOwKM.d.ts +54 -0
- package/dist/capabilities-CjulXFQX.js +189 -0
- package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
- package/dist/env-DfxpPfs9.js +28 -0
- package/dist/env-vEjyJfPW.d.ts +17 -0
- package/dist/force-state-BaqWsmrf.js +153 -0
- package/dist/force-state-CCqoTfVB.d.ts +23 -0
- package/dist/handler-WmE4Lu7l.d.ts +39 -0
- package/dist/index-BYsdS-7d.d.ts +55 -0
- package/dist/index.d.ts +6 -36
- package/dist/index.js +119 -202
- package/dist/marker-C66LxOhJ.d.ts +9 -0
- package/dist/marker-CMrvxPmS.d.ts +12 -0
- package/dist/marker-DjeGvL-F.js +9 -0
- package/dist/marker-uZYPOJ4M.js +12 -0
- package/dist/markers.d.ts +3 -0
- package/dist/markers.js +3 -0
- package/dist/next.d.ts +3 -18
- package/dist/next.js +3 -28
- package/dist/payload-CLKDVZWl.d.ts +68 -0
- package/dist/plugins/access/server.d.ts +122 -0
- package/dist/plugins/access/server.js +339 -0
- package/dist/plugins/access.d.ts +41 -0
- package/dist/plugins/access.js +250 -0
- package/dist/plugins/agent/server.d.ts +129 -0
- package/dist/plugins/agent/server.js +429 -0
- package/dist/plugins/agent.d.ts +37 -0
- package/dist/plugins/agent.js +411 -0
- package/dist/plugins/design.d.ts +125 -0
- package/dist/plugins/design.js +619 -0
- package/dist/plugins/observe.d.ts +64 -0
- package/dist/plugins/observe.js +617 -0
- package/dist/plugins/ops/server.d.ts +82 -0
- package/dist/plugins/ops/server.js +380 -0
- package/dist/plugins/ops.d.ts +26 -0
- package/dist/plugins/ops.js +362 -0
- package/dist/plugins/vitals.js +3 -86
- package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
- package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
- package/dist/review.d.ts +181 -0
- package/dist/review.js +1017 -0
- package/dist/selector-NSYSe7AZ.js +28 -0
- package/dist/server.d.ts +3 -39
- package/dist/server.js +1 -1
- package/dist/shortcut-U6ftVY06.js +222 -0
- package/dist/state-boundary.d.ts +26 -0
- package/dist/state-boundary.js +61 -0
- package/dist/styles--slk86ZQ.js +332 -0
- package/dist/types-DADBRiGk.d.ts +50 -0
- package/dist/types-QJUajwWq.js +35 -0
- package/dist/types-f3jlNxzw.d.ts +92 -0
- package/dist/vitals-panel-Dvc5dnR-.js +87 -0
- package/package.json +23 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/plugins/design/force-state.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Force-state store: lets a reviewer (the States panel) or an agent
|
|
4
|
+
* (`?__state=`) render any registered state boundary as loading, empty or
|
|
5
|
+
* error without faking data.
|
|
6
|
+
*
|
|
7
|
+
* Development only. `StateBoundary` reaches this module behind a
|
|
8
|
+
* `NODE_ENV === "development"` constant, so production builds drop it; a
|
|
9
|
+
* consumer's bundle gate greps client chunks for FORCE_STATE_MARKER to prove it.
|
|
10
|
+
*/
|
|
11
|
+
declare const FORCED_STATES: readonly ["loading", "empty", "error"];
|
|
12
|
+
type ForcedState = (typeof FORCED_STATES)[number];
|
|
13
|
+
/** `?__state=loading` forces top-level boundaries; `?__state=<id>:error` one boundary. */
|
|
14
|
+
declare const FORCE_STATE_PARAM = "__state";
|
|
15
|
+
/**
|
|
16
|
+
* globalThis key of the store singleton, and the bundle-gate marker. Holding
|
|
17
|
+
* the store on globalThis keeps one instance even if the module is evaluated
|
|
18
|
+
* twice: HMR, or the app's `StateBoundary` chunk and the HUD's States panel
|
|
19
|
+
* chunk each carrying a copy.
|
|
20
|
+
*/
|
|
21
|
+
declare const FORCE_STATE_MARKER = "__HANSENEXUS_HUD_FORCE_STATE__";
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ForcedState as i, FORCE_STATE_MARKER as n, FORCE_STATE_PARAM as r, FORCED_STATES as t };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { a as HudHttpMethod, o as HudPlugin, r as HudCapability } from "./plugins-CQ_0w1ot.js";
|
|
2
|
+
//#region src/server/handler.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether a request may reach a plugin route. Return a `Response` to
|
|
5
|
+
* refuse it (404, 401, 403 ...) or `null` to let it through. The `access`
|
|
6
|
+
* plugin supplies one for deployed tiers.
|
|
7
|
+
*/
|
|
8
|
+
type HudGuard = (request: Request, capability: HudCapability | undefined) => Response | null | Promise<Response | null>;
|
|
9
|
+
interface HudHandlerOptions {
|
|
10
|
+
/** Path the catch-all route is mounted at. Default `/api/hud`. */
|
|
11
|
+
basePath?: string;
|
|
12
|
+
/** Default: `developmentOnly`, which answers 404 unless `NODE_ENV` is `development`. */
|
|
13
|
+
guard?: HudGuard;
|
|
14
|
+
/** Drops plugins server side, like `defineHud({ can })` does client side. Default: grant all. */
|
|
15
|
+
can?: (capability: HudCapability) => boolean;
|
|
16
|
+
}
|
|
17
|
+
type HudRequestHandler = (request: Request) => Promise<Response>;
|
|
18
|
+
/** One function per method, so a Next route can `export const { GET, POST } = createHudHandler(...)`. */
|
|
19
|
+
type HudHandler = {
|
|
20
|
+
handle: HudRequestHandler;
|
|
21
|
+
} & Record<HudHttpMethod, HudRequestHandler>;
|
|
22
|
+
/**
|
|
23
|
+
* The default guard. Core assumes development, so a deployed build that
|
|
24
|
+
* mounts the route without an `access` guard serves nothing and reveals
|
|
25
|
+
* nothing about which plugins exist.
|
|
26
|
+
*/
|
|
27
|
+
declare const developmentOnly: HudGuard;
|
|
28
|
+
/**
|
|
29
|
+
* Serves every plugin's routes from one catch-all route
|
|
30
|
+
* (`app/api/hud/[...hud]/route.ts` in Next). A request to
|
|
31
|
+
* `<basePath>/<plugin id>/<path>` reaches that plugin's `routes[path][method]`;
|
|
32
|
+
* unknown plugin or path answers 404, a known path with another method 405.
|
|
33
|
+
*
|
|
34
|
+
* Pass the server halves (`@hansenexus/hud/plugins/<id>/server`): they carry
|
|
35
|
+
* `routes` and no React.
|
|
36
|
+
*/
|
|
37
|
+
declare function createHudHandler(plugins: readonly HudPlugin[], options?: HudHandlerOptions): HudHandler;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { createHudHandler as a, HudRequestHandler as i, HudHandler as n, developmentOnly as o, HudHandlerOptions as r, HudGuard as t };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { o as HudPlugin, r as HudCapability } from "./plugins-CQ_0w1ot.js";
|
|
2
|
+
import "./define-hud-BBzsSl8b.js";
|
|
3
|
+
//#region src/core/grabber/picker.d.ts
|
|
4
|
+
interface PickPoint {
|
|
5
|
+
clientX: number;
|
|
6
|
+
clientY: number;
|
|
7
|
+
}
|
|
8
|
+
interface ElementPickerProps {
|
|
9
|
+
active: boolean;
|
|
10
|
+
onPick: (element: Element, point: PickPoint) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
/** Banner text while active. */
|
|
13
|
+
hint?: string;
|
|
14
|
+
/** Label next to the hover outline. Default: the tag name. */
|
|
15
|
+
describe?: (element: Element) => string;
|
|
16
|
+
/** Extra elements to leave alone, on top of the picker's own shadow host. */
|
|
17
|
+
ignore?: (element: Element) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Styles for the picker overlay. The picker renders where it is mounted, so
|
|
21
|
+
* the caller puts these into the same (shadow) root. Colours fall back when
|
|
22
|
+
* the `--hud-*` tokens are absent.
|
|
23
|
+
*/
|
|
24
|
+
declare const PICKER_CSS = "\n.grab-layer { position: fixed; inset: 0; pointer-events: none; }\n.grab-hint {\n position: fixed;\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n padding: 3px 12px;\n border-radius: 0 0 6px 6px;\n white-space: nowrap;\n background: var(--hud-accent, #0369a1);\n color: var(--hud-accent-fg, #ffffff);\n font: 600 11px/1.5 var(--hud-mono, ui-monospace, monospace);\n}\n.grab-outline {\n position: fixed;\n border: 1.5px solid var(--hud-accent, #0369a1);\n background: color-mix(in srgb, var(--hud-accent, #0369a1) 8%, transparent);\n border-radius: 3px;\n}\n.grab-label {\n position: fixed;\n max-width: min(480px, calc(100vw - 8px));\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding: 1px 6px;\n border-radius: 3px;\n background: var(--hud-accent, #0369a1);\n color: var(--hud-accent-fg, #ffffff);\n font: 600 10px/1.6 var(--hud-mono, ui-monospace, monospace);\n}\n";
|
|
25
|
+
/**
|
|
26
|
+
* Click-to-pick controller: crosshair, hover outline with a label, the next
|
|
27
|
+
* click picks, Escape cancels. Events inside the picker's own shadow host
|
|
28
|
+
* (the HUD's pill and panels) pass through untouched, so panels stay usable
|
|
29
|
+
* and mounted during a grab.
|
|
30
|
+
*/
|
|
31
|
+
declare function ElementPicker({ active, onPick, onCancel, hint, describe, ignore }: ElementPickerProps): import("react").JSX.Element;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/shell/hud.d.ts
|
|
34
|
+
interface HudProps {
|
|
35
|
+
/** App slug: labels the pill and keys the persisted layout. */
|
|
36
|
+
appSlug: string;
|
|
37
|
+
/** Badge shown at the start of the pill. Default "DEV". */
|
|
38
|
+
envLabel?: string;
|
|
39
|
+
/** Client halves of the plugins, in pill order. Keep the array stable across renders. */
|
|
40
|
+
plugins?: readonly HudPlugin[];
|
|
41
|
+
/** Base path of the plugin routes. Default "/api/hud". */
|
|
42
|
+
apiBase?: string;
|
|
43
|
+
/** Capability check. Default: grant all (core assumes development). Keep it stable. */
|
|
44
|
+
can?: (capability: HudCapability) => boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The HUD shell: a shadow-rooted status pill and floating panels.
|
|
48
|
+
*
|
|
49
|
+
* It renders unconditionally. Keeping it out of production builds is the
|
|
50
|
+
* loader's job, at the import site, so a bundler can drop the whole module
|
|
51
|
+
* (see `@hansenexus/hud/next`).
|
|
52
|
+
*/
|
|
53
|
+
declare function Hud({ appSlug, envLabel, plugins, apiBase, can }: HudProps): import("react").JSX.Element;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { PICKER_CSS as a, ElementPickerProps as i, HudProps as n, PickPoint as o, ElementPicker as r, Hud as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* while keeping the HUD, and it is unmistakably deliberate rather than an
|
|
8
|
-
* incidental string a refactor could rename away.
|
|
9
|
-
*/
|
|
10
|
-
export declare const HUD_DEV_MARKER = "__HANSENEXUS_HUD_DEV__";
|
|
11
|
-
/** Tag name of the element that hosts the HUD's shadow root. */
|
|
12
|
-
export declare const HUD_HOST_TAG = "hn-hud";
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/shell/hud.d.ts
|
|
15
|
-
interface HudProps {
|
|
16
|
-
/** App slug: labels the pill and keys the persisted layout. */
|
|
17
|
-
appSlug: string;
|
|
18
|
-
/** Badge shown at the start of the pill. Default "DEV". */
|
|
19
|
-
envLabel?: string;
|
|
20
|
-
/** Client halves of the plugins, in pill order. Keep the array stable across renders. */
|
|
21
|
-
plugins?: readonly HudPlugin[];
|
|
22
|
-
/** Base path of the plugin routes. Default "/api/hud". */
|
|
23
|
-
apiBase?: string;
|
|
24
|
-
/** Capability check. Default: grant all (core assumes development). Keep it stable. */
|
|
25
|
-
can?: (capability: HudCapability) => boolean;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* The HUD shell: a shadow-rooted status pill and floating panels.
|
|
29
|
-
*
|
|
30
|
-
* It renders unconditionally. Keeping it out of production builds is the
|
|
31
|
-
* loader's job, at the import site, so a bundler can drop the whole module
|
|
32
|
-
* (see `@hansenexus/hud/next`).
|
|
33
|
-
*/
|
|
34
|
-
export declare function Hud({ appSlug, envLabel, plugins, apiBase, can }: HudProps): import("react").JSX.Element;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { type DefineHudOptions, HudApiError, type HudCapability, type HudCommand, type HudMountProps, type HudPlugin, type HudPluginContext, type HudPluginProps, type HudProps, type PinEdge, defineHud, definePlugin };
|
|
1
|
+
import { a as GrabComponent, i as matchComponentHints, n as GrabAnchor, r as GrabPayload, t as ComponentHint } from "./payload-CLKDVZWl.js";
|
|
2
|
+
import { c as HudPluginProps, f as PinEdge, g as useGrab, h as HudGrabState, i as HudCommand, m as HudGrab, n as HudApiError, o as HudPlugin, p as definePlugin, r as HudCapability, s as HudPluginContext, t as HudAccessControl } from "./plugins-CQ_0w1ot.js";
|
|
3
|
+
import { n as HudMountProps, r as defineHud, t as DefineHudOptions } from "./define-hud-BBzsSl8b.js";
|
|
4
|
+
import { a as PICKER_CSS, i as ElementPickerProps, n as HudProps, o as PickPoint, r as ElementPicker, t as Hud } from "./index-BYsdS-7d.js";
|
|
5
|
+
import { n as HUD_HOST_TAG, t as HUD_DEV_MARKER } from "./marker-CMrvxPmS.js";
|
|
6
|
+
export { type ComponentHint, type DefineHudOptions, ElementPicker, type ElementPickerProps, type GrabAnchor, type GrabComponent, type GrabPayload, HUD_DEV_MARKER, HUD_HOST_TAG, Hud, type HudAccessControl, HudApiError, type HudCapability, type HudCommand, type HudGrab, type HudGrabState, type HudMountProps, type HudPlugin, type HudPluginContext, type HudPluginProps, type HudProps, PICKER_CSS, type PickPoint, type PinEdge, defineHud, definePlugin, matchComponentHints, useGrab };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as buildGrabPayload, c as createGrabber, i as parseShortcut, l as useGrab, n as isApplePlatform, o as describeElement, r as matchesShortcut, s as matchComponentHints, t as formatShortcut } from "./shortcut-U6ftVY06.js";
|
|
2
|
+
import { n as HUD_HOST_TAG, t as HUD_DEV_MARKER } from "./marker-uZYPOJ4M.js";
|
|
3
|
+
import { a as resolvePlugins, i as definePlugin, n as HudApiError, r as createPluginContext } from "./plugins-DVy_vaYr.js";
|
|
4
|
+
import { i as PICKER_CSS, n as ShadowHost, r as ElementPicker, t as HUD_CSS } from "./styles--slk86ZQ.js";
|
|
2
5
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
|
|
7
|
+
//#region src/shell/grabber.tsx
|
|
8
|
+
const GRAB_CSS = PICKER_CSS + `
|
|
9
|
+
.hud .grab-toggle { font-size: 14px; line-height: 1; padding: 2px 6px; }
|
|
10
|
+
.hud .grab-toggle[aria-pressed="true"] { color: var(--hud-accent); }
|
|
11
|
+
`;
|
|
12
|
+
/**
|
|
13
|
+
* Grab mode for the shell. It lives beside the panels, not inside one, so a
|
|
14
|
+
* grab never unmounts or closes a panel; the picker ignores clicks inside the
|
|
15
|
+
* HUD, so open panels stay usable while it is active.
|
|
16
|
+
*/
|
|
17
|
+
function Grabber({ app, store, slots }) {
|
|
18
|
+
const { grabbing } = useGrab(store);
|
|
19
|
+
const onPick = useCallback((element) => {
|
|
20
|
+
const payload = buildGrabPayload(element, { app });
|
|
21
|
+
store.complete(payload);
|
|
22
|
+
for (const { plugin, ctx } of slots) try {
|
|
23
|
+
plugin.onGrab?.(payload, ctx);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`hud: ${plugin.id} onGrab failed`, error);
|
|
26
|
+
}
|
|
27
|
+
}, [
|
|
28
|
+
app,
|
|
29
|
+
store,
|
|
30
|
+
slots
|
|
31
|
+
]);
|
|
32
|
+
return /* @__PURE__ */ jsx(ElementPicker, {
|
|
33
|
+
active: grabbing,
|
|
34
|
+
onPick,
|
|
35
|
+
onCancel: store.cancel,
|
|
36
|
+
describe: describeElement,
|
|
37
|
+
hint: "Grab: click an element · Esc cancels"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
5
41
|
//#region src/shell/commands/context.ts
|
|
6
42
|
const RegistryContext = createContext(null);
|
|
7
43
|
function useRegistry() {
|
|
@@ -24,46 +60,7 @@ function useRegisterCommands(commands) {
|
|
|
24
60
|
}, [registry, commands]);
|
|
25
61
|
}
|
|
26
62
|
//#endregion
|
|
27
|
-
//#region src/shell/commands/shortcut.ts
|
|
28
|
-
function parseShortcut(spec) {
|
|
29
|
-
const parts = spec.toLowerCase().split("+");
|
|
30
|
-
return {
|
|
31
|
-
key: parts.at(-1) ?? "",
|
|
32
|
-
mod: parts.includes("mod"),
|
|
33
|
-
shift: parts.includes("shift"),
|
|
34
|
-
alt: parts.includes("alt")
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function isApplePlatform() {
|
|
38
|
-
return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
39
|
-
}
|
|
40
|
-
function matchesShortcut(event, shortcut, apple) {
|
|
41
|
-
if ((apple ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey) !== shortcut.mod) return false;
|
|
42
|
-
if (event.shiftKey !== shortcut.shift || event.altKey !== shortcut.alt) return false;
|
|
43
|
-
if (event.key.toLowerCase() === shortcut.key) return true;
|
|
44
|
-
if (/^[a-z]$/.test(shortcut.key)) return event.code === `Key${shortcut.key.toUpperCase()}`;
|
|
45
|
-
if (/^\d$/.test(shortcut.key)) return event.code === `Digit${shortcut.key}`;
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
function formatShortcut(shortcut, apple) {
|
|
49
|
-
const key = shortcut.key.toUpperCase();
|
|
50
|
-
if (apple) return `${shortcut.mod ? "⌘" : ""}${shortcut.alt ? "⌥" : ""}${shortcut.shift ? "⇧" : ""}${key}`;
|
|
51
|
-
return [
|
|
52
|
-
shortcut.mod && "Ctrl",
|
|
53
|
-
shortcut.alt && "Alt",
|
|
54
|
-
shortcut.shift && "Shift",
|
|
55
|
-
key
|
|
56
|
-
].filter(Boolean).join("+");
|
|
57
|
-
}
|
|
58
|
-
//#endregion
|
|
59
63
|
//#region src/shell/commands/registry.ts
|
|
60
|
-
/** Element grab arrives with the grabber slice (hud#7); the shortcut is reserved now. */
|
|
61
|
-
const GRAB_COMMAND = {
|
|
62
|
-
id: "hud.grab",
|
|
63
|
-
title: "Grab element",
|
|
64
|
-
shortcut: "mod+shift+g",
|
|
65
|
-
run: () => {}
|
|
66
|
-
};
|
|
67
64
|
/**
|
|
68
65
|
* Commands the shell and plugins contribute. Registering an id again
|
|
69
66
|
* replaces the earlier command; the first registered shortcut match wins.
|
|
@@ -106,12 +103,11 @@ var CommandRegistry = class {
|
|
|
106
103
|
};
|
|
107
104
|
//#endregion
|
|
108
105
|
//#region src/shell/commands/command-provider.tsx
|
|
109
|
-
const BUILTIN = [GRAB_COMMAND];
|
|
110
106
|
/** Owns one HUD's command registry and binds its shortcuts on `window`. */
|
|
111
107
|
function CommandProvider({ commands, children }) {
|
|
112
108
|
const [registry] = useState(() => new CommandRegistry());
|
|
113
109
|
useEffect(() => {
|
|
114
|
-
const unregister =
|
|
110
|
+
const unregister = commands.map((command) => registry.register(command));
|
|
115
111
|
return () => {
|
|
116
112
|
for (const off of unregister) off();
|
|
117
113
|
};
|
|
@@ -900,7 +896,7 @@ const PANEL_CSS = `
|
|
|
900
896
|
//#endregion
|
|
901
897
|
//#region src/shell/plugin-slots.tsx
|
|
902
898
|
/** One stable context per plugin, so panels and pills can depend on `ctx` in effects. */
|
|
903
|
-
function usePluginSlots(plugins, { app, apiBase, can, close, pin }) {
|
|
899
|
+
function usePluginSlots(plugins, { app, apiBase, can, close, open, pin, grab }) {
|
|
904
900
|
return useMemo(() => plugins.map((plugin) => ({
|
|
905
901
|
plugin,
|
|
906
902
|
title: plugin.title ?? plugin.id,
|
|
@@ -909,7 +905,9 @@ function usePluginSlots(plugins, { app, apiBase, can, close, pin }) {
|
|
|
909
905
|
apiBase,
|
|
910
906
|
can,
|
|
911
907
|
close: () => close(plugin.id),
|
|
912
|
-
|
|
908
|
+
open: () => open(plugin.id),
|
|
909
|
+
pin: (edge) => pin(plugin.id, edge),
|
|
910
|
+
grab
|
|
913
911
|
})
|
|
914
912
|
})), [
|
|
915
913
|
plugins,
|
|
@@ -917,7 +915,9 @@ function usePluginSlots(plugins, { app, apiBase, can, close, pin }) {
|
|
|
917
915
|
apiBase,
|
|
918
916
|
can,
|
|
919
917
|
close,
|
|
920
|
-
|
|
918
|
+
open,
|
|
919
|
+
pin,
|
|
920
|
+
grab
|
|
921
921
|
]);
|
|
922
922
|
}
|
|
923
923
|
/**
|
|
@@ -946,153 +946,8 @@ function PluginPanelBody({ slot }) {
|
|
|
946
946
|
return Panel ? /* @__PURE__ */ jsx(Panel, { ctx: slot.ctx }) : null;
|
|
947
947
|
}
|
|
948
948
|
//#endregion
|
|
949
|
-
//#region src/core/marker.ts
|
|
950
|
-
/**
|
|
951
|
-
* Literal a consumer's CI bundle gate greps for in production client chunks.
|
|
952
|
-
* It is rendered as an attribute on the HUD root, so a bundler cannot drop it
|
|
953
|
-
* while keeping the HUD, and it is unmistakably deliberate rather than an
|
|
954
|
-
* incidental string a refactor could rename away.
|
|
955
|
-
*/
|
|
956
|
-
const HUD_DEV_MARKER = "__HANSENEXUS_HUD_DEV__";
|
|
957
|
-
/** Tag name of the element that hosts the HUD's shadow root. */
|
|
958
|
-
const HUD_HOST_TAG = "hn-hud";
|
|
959
|
-
//#endregion
|
|
960
|
-
//#region src/shell/shadow-host.tsx
|
|
961
|
-
/**
|
|
962
|
-
* Inline `!important` on the host element: author `!important` in a host
|
|
963
|
-
* stylesheet (`* { position: static !important }`) loses to inline
|
|
964
|
-
* `!important`, so this is the one set of rules a client site cannot override.
|
|
965
|
-
* The host covers the viewport but lets every click through; HUD surfaces opt
|
|
966
|
-
* back in with `pointer-events: auto`.
|
|
967
|
-
*/
|
|
968
|
-
const HOST_STYLE = {
|
|
969
|
-
all: "initial",
|
|
970
|
-
position: "fixed",
|
|
971
|
-
inset: "0",
|
|
972
|
-
"z-index": "2147483000",
|
|
973
|
-
"pointer-events": "none",
|
|
974
|
-
display: "block"
|
|
975
|
-
};
|
|
976
|
-
/**
|
|
977
|
-
* Renders children into a shadow root on a host element appended to <body>.
|
|
978
|
-
* Mounted in an effect, so server rendering emits nothing and hydration never
|
|
979
|
-
* differs.
|
|
980
|
-
*/
|
|
981
|
-
function ShadowHost({ css, children }) {
|
|
982
|
-
const [mount, setMount] = useState(null);
|
|
983
|
-
useLayoutEffect(() => {
|
|
984
|
-
const host = document.createElement(HUD_HOST_TAG);
|
|
985
|
-
for (const [prop, value] of Object.entries(HOST_STYLE)) host.style.setProperty(prop, value, "important");
|
|
986
|
-
const shadow = host.attachShadow({ mode: "open" });
|
|
987
|
-
const style = document.createElement("style");
|
|
988
|
-
style.textContent = css;
|
|
989
|
-
const root = document.createElement("div");
|
|
990
|
-
root.className = "hud";
|
|
991
|
-
root.setAttribute("data-hud-marker", HUD_DEV_MARKER);
|
|
992
|
-
shadow.append(style, root);
|
|
993
|
-
document.body.appendChild(host);
|
|
994
|
-
setMount(root);
|
|
995
|
-
return () => {
|
|
996
|
-
host.remove();
|
|
997
|
-
setMount(null);
|
|
998
|
-
};
|
|
999
|
-
}, [css]);
|
|
1000
|
-
return mount ? createPortal(children, mount) : null;
|
|
1001
|
-
}
|
|
1002
|
-
//#endregion
|
|
1003
949
|
//#region src/shell/hud.tsx
|
|
1004
|
-
const CSS =
|
|
1005
|
-
.hud {
|
|
1006
|
-
all: initial;
|
|
1007
|
-
--hud-bg: #ffffff;
|
|
1008
|
-
--hud-bg-subtle: #f4f4f5;
|
|
1009
|
-
--hud-fg: #18181b;
|
|
1010
|
-
--hud-fg-muted: #71717a;
|
|
1011
|
-
--hud-border: rgba(24, 24, 27, 0.14);
|
|
1012
|
-
--hud-accent: #0369a1;
|
|
1013
|
-
--hud-good: #15803d;
|
|
1014
|
-
--hud-warn: #b45309;
|
|
1015
|
-
--hud-poor: #b91c1c;
|
|
1016
|
-
--hud-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
1017
|
-
--hud-radius: 8px;
|
|
1018
|
-
--hud-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1019
|
-
--hud-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
1020
|
-
|
|
1021
|
-
font-family: var(--hud-font);
|
|
1022
|
-
font-size: 12px;
|
|
1023
|
-
line-height: 1.5;
|
|
1024
|
-
color: var(--hud-fg);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
@media (prefers-color-scheme: dark) {
|
|
1028
|
-
.hud {
|
|
1029
|
-
--hud-bg: #18181b;
|
|
1030
|
-
--hud-bg-subtle: #27272a;
|
|
1031
|
-
--hud-fg: #f4f4f5;
|
|
1032
|
-
--hud-fg-muted: #a1a1aa;
|
|
1033
|
-
--hud-border: rgba(244, 244, 245, 0.16);
|
|
1034
|
-
--hud-accent: #38bdf8;
|
|
1035
|
-
--hud-good: #4ade80;
|
|
1036
|
-
--hud-warn: #fbbf24;
|
|
1037
|
-
--hud-poor: #f87171;
|
|
1038
|
-
--hud-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
.hud *, .hud *::before, .hud *::after { box-sizing: border-box; }
|
|
1043
|
-
|
|
1044
|
-
.hud button {
|
|
1045
|
-
font: inherit;
|
|
1046
|
-
color: inherit;
|
|
1047
|
-
background: none;
|
|
1048
|
-
border: 0;
|
|
1049
|
-
margin: 0;
|
|
1050
|
-
padding: 0;
|
|
1051
|
-
cursor: pointer;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
.hud :focus-visible { outline: 2px solid var(--hud-accent); outline-offset: 1px; }
|
|
1055
|
-
|
|
1056
|
-
.pill {
|
|
1057
|
-
position: fixed;
|
|
1058
|
-
left: 12px;
|
|
1059
|
-
bottom: 12px;
|
|
1060
|
-
display: flex;
|
|
1061
|
-
align-items: center;
|
|
1062
|
-
gap: 2px;
|
|
1063
|
-
padding: 3px;
|
|
1064
|
-
background: var(--hud-bg);
|
|
1065
|
-
border: 1px solid var(--hud-border);
|
|
1066
|
-
border-radius: 999px;
|
|
1067
|
-
box-shadow: var(--hud-shadow);
|
|
1068
|
-
font-family: var(--hud-mono);
|
|
1069
|
-
pointer-events: auto;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
.hud .pill-item {
|
|
1073
|
-
display: flex;
|
|
1074
|
-
align-items: center;
|
|
1075
|
-
gap: 6px;
|
|
1076
|
-
padding: 2px 8px;
|
|
1077
|
-
border-radius: 999px;
|
|
1078
|
-
white-space: nowrap;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
.pill-item:hover, .pill-item[aria-pressed="true"] { background: var(--hud-bg-subtle); }
|
|
1082
|
-
|
|
1083
|
-
.env { font-weight: 600; color: var(--hud-accent); }
|
|
1084
|
-
|
|
1085
|
-
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--hud-fg-muted); }
|
|
1086
|
-
.dot[data-rating="good"] { background: var(--hud-good); }
|
|
1087
|
-
.dot[data-rating="needs-improvement"] { background: var(--hud-warn); }
|
|
1088
|
-
.dot[data-rating="poor"] { background: var(--hud-poor); }
|
|
1089
|
-
|
|
1090
|
-
.vitals { width: 100%; border-collapse: collapse; font-family: var(--hud-mono); }
|
|
1091
|
-
.vitals td { padding: 3px 0; }
|
|
1092
|
-
.vitals td:last-child { text-align: right; }
|
|
1093
|
-
.vitals .label { color: var(--hud-fg-muted); }
|
|
1094
|
-
.empty { color: var(--hud-fg-muted); }
|
|
1095
|
-
` + PANEL_CSS;
|
|
950
|
+
const CSS = HUD_CSS + PANEL_CSS + GRAB_CSS;
|
|
1096
951
|
const NO_PLUGINS = [];
|
|
1097
952
|
const GRANT_ALL = () => true;
|
|
1098
953
|
/**
|
|
@@ -1106,6 +961,7 @@ function Hud({ appSlug, envLabel = "DEV", plugins = NO_PLUGINS, apiBase = "/api/
|
|
|
1106
961
|
const resolved = useMemo(() => resolvePlugins(plugins, can), [plugins, can]);
|
|
1107
962
|
return /* @__PURE__ */ jsx(ShadowHost, {
|
|
1108
963
|
css: CSS,
|
|
964
|
+
marker: HUD_DEV_MARKER,
|
|
1109
965
|
children: /* @__PURE__ */ jsx(Shell, {
|
|
1110
966
|
appSlug,
|
|
1111
967
|
envLabel,
|
|
@@ -1117,38 +973,55 @@ function Hud({ appSlug, envLabel = "DEV", plugins = NO_PLUGINS, apiBase = "/api/
|
|
|
1117
973
|
}
|
|
1118
974
|
function Shell({ appSlug, envLabel, plugins, apiBase, can }) {
|
|
1119
975
|
const panelsRef = useRef(null);
|
|
976
|
+
const close = useCallback((id) => panelsRef.current?.close(id), []);
|
|
977
|
+
const open = useCallback((id) => panelsRef.current?.open(id), []);
|
|
978
|
+
const pin = useCallback((id, edge) => panelsRef.current?.pin(id, edge), []);
|
|
979
|
+
const [grab] = useState(createGrabber);
|
|
1120
980
|
const slots = usePluginSlots(plugins, {
|
|
1121
981
|
app: appSlug,
|
|
1122
982
|
apiBase,
|
|
1123
983
|
can,
|
|
1124
|
-
close
|
|
1125
|
-
|
|
984
|
+
close,
|
|
985
|
+
open,
|
|
986
|
+
pin,
|
|
987
|
+
grab
|
|
1126
988
|
});
|
|
1127
989
|
const panels = useMemo(() => slots.filter((slot) => slot.plugin.panel).map((slot) => ({
|
|
1128
990
|
id: slot.plugin.id,
|
|
1129
991
|
title: slot.title,
|
|
1130
992
|
render: () => /* @__PURE__ */ jsx(PluginPanelBody, { slot })
|
|
1131
993
|
})), [slots]);
|
|
1132
|
-
const commands = useMemo(() =>
|
|
994
|
+
const commands = useMemo(() => [{
|
|
995
|
+
id: "hud.grab",
|
|
996
|
+
title: "Grab element",
|
|
997
|
+
shortcut: "mod+shift+g",
|
|
998
|
+
run: () => grab.getState().grabbing ? grab.cancel() : grab.start()
|
|
999
|
+
}, ...slots.flatMap(({ plugin, ctx }) => (plugin.commands ?? []).map((command) => ({
|
|
1133
1000
|
id: `${plugin.id}.${command.id}`,
|
|
1134
1001
|
title: command.title,
|
|
1135
1002
|
shortcut: command.shortcut,
|
|
1136
1003
|
run: () => command.run(ctx)
|
|
1137
|
-
}))), [slots]);
|
|
1138
|
-
return /* @__PURE__ */
|
|
1004
|
+
})))], [slots, grab]);
|
|
1005
|
+
return /* @__PURE__ */ jsxs(PanelManager, {
|
|
1139
1006
|
appSlug,
|
|
1140
1007
|
panels,
|
|
1141
1008
|
commands,
|
|
1142
1009
|
apiRef: panelsRef,
|
|
1143
|
-
children: /* @__PURE__ */ jsx(Pill, {
|
|
1010
|
+
children: [/* @__PURE__ */ jsx(Pill, {
|
|
1144
1011
|
appSlug,
|
|
1145
1012
|
envLabel,
|
|
1013
|
+
slots,
|
|
1014
|
+
grab
|
|
1015
|
+
}), /* @__PURE__ */ jsx(Grabber, {
|
|
1016
|
+
app: appSlug,
|
|
1017
|
+
store: grab,
|
|
1146
1018
|
slots
|
|
1147
|
-
})
|
|
1019
|
+
})]
|
|
1148
1020
|
});
|
|
1149
1021
|
}
|
|
1150
|
-
function Pill({ appSlug, envLabel, slots }) {
|
|
1022
|
+
function Pill({ appSlug, envLabel, slots, grab }) {
|
|
1151
1023
|
const panels = usePanels();
|
|
1024
|
+
const { grabbing } = useGrab(grab);
|
|
1152
1025
|
return /* @__PURE__ */ jsxs("div", {
|
|
1153
1026
|
className: "pill",
|
|
1154
1027
|
role: "toolbar",
|
|
@@ -1163,6 +1036,15 @@ function Pill({ appSlug, envLabel, slots }) {
|
|
|
1163
1036
|
open: panels.isOpen(slot.plugin.id),
|
|
1164
1037
|
onToggle: () => panels.toggle(slot.plugin.id)
|
|
1165
1038
|
}, slot.plugin.id)),
|
|
1039
|
+
/* @__PURE__ */ jsx("button", {
|
|
1040
|
+
type: "button",
|
|
1041
|
+
className: "pill-item grab-toggle",
|
|
1042
|
+
"aria-label": "Grab element",
|
|
1043
|
+
"aria-pressed": grabbing,
|
|
1044
|
+
title: "Grab element",
|
|
1045
|
+
onClick: () => grabbing ? grab.cancel() : grab.start(),
|
|
1046
|
+
children: "⌖"
|
|
1047
|
+
}),
|
|
1166
1048
|
/* @__PURE__ */ jsx(PillMenu, {})
|
|
1167
1049
|
]
|
|
1168
1050
|
});
|
|
@@ -1182,7 +1064,19 @@ function Pill({ appSlug, envLabel, slots }) {
|
|
|
1182
1064
|
function defineHud({ app, plugins, apiBase, can }) {
|
|
1183
1065
|
resolvePlugins(plugins, () => true);
|
|
1184
1066
|
const list = Object.freeze([...plugins]);
|
|
1185
|
-
|
|
1067
|
+
const access = list.find((plugin) => plugin.access)?.access;
|
|
1068
|
+
function DefinedHud({ envLabel, env = "development" }) {
|
|
1069
|
+
if (env === "disabled") return null;
|
|
1070
|
+
if (access) return /* @__PURE__ */ jsx(AccessHud, {
|
|
1071
|
+
access,
|
|
1072
|
+
env,
|
|
1073
|
+
app,
|
|
1074
|
+
plugins: list,
|
|
1075
|
+
apiBase: apiBase ?? "/api/hud",
|
|
1076
|
+
...envLabel === void 0 ? {} : { envLabel },
|
|
1077
|
+
...can === void 0 ? {} : { can }
|
|
1078
|
+
});
|
|
1079
|
+
if (env !== "development") return null;
|
|
1186
1080
|
return /* @__PURE__ */ jsx(Hud, {
|
|
1187
1081
|
appSlug: app,
|
|
1188
1082
|
plugins: list,
|
|
@@ -1194,5 +1088,28 @@ function defineHud({ app, plugins, apiBase, can }) {
|
|
|
1194
1088
|
DefinedHud.displayName = `Hud(${app})`;
|
|
1195
1089
|
return DefinedHud;
|
|
1196
1090
|
}
|
|
1091
|
+
/**
|
|
1092
|
+
* The shell behind the access plugin's lock and capability matrix.
|
|
1093
|
+
*
|
|
1094
|
+
* Display only: hiding a plugin here protects nothing, since its code ships
|
|
1095
|
+
* in the bundle and its routes are one URL away. The server guard
|
|
1096
|
+
* (`accessGuard`) is what enforces the tier.
|
|
1097
|
+
*/
|
|
1098
|
+
function AccessHud({ access, env, app, plugins, apiBase, envLabel, can }) {
|
|
1099
|
+
const unlocked = access.useUnlocked(env, apiBase);
|
|
1100
|
+
const gated = useCallback((capability) => access.can(env, capability) && (can?.(capability) ?? true), [
|
|
1101
|
+
access,
|
|
1102
|
+
env,
|
|
1103
|
+
can
|
|
1104
|
+
]);
|
|
1105
|
+
const shown = useMemo(() => unlocked ? plugins : plugins.filter((plugin) => plugin.access), [unlocked, plugins]);
|
|
1106
|
+
return /* @__PURE__ */ jsx(Hud, {
|
|
1107
|
+
appSlug: app,
|
|
1108
|
+
plugins: shown,
|
|
1109
|
+
apiBase,
|
|
1110
|
+
can: gated,
|
|
1111
|
+
...envLabel === void 0 ? {} : { envLabel }
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1197
1114
|
//#endregion
|
|
1198
|
-
export { HUD_DEV_MARKER, HUD_HOST_TAG, Hud, HudApiError, defineHud, definePlugin };
|
|
1115
|
+
export { ElementPicker, HUD_DEV_MARKER, HUD_HOST_TAG, Hud, HudApiError, PICKER_CSS, defineHud, definePlugin, matchComponentHints, useGrab };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/review/marker.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Literal on the review entrypoint's root. A consumer's bundle gate expects it
|
|
4
|
+
* where the review widget is meant to ship and nowhere else; it never appears
|
|
5
|
+
* next to `HUD_DEV_MARKER`, which belongs to the dev shell alone.
|
|
6
|
+
*/
|
|
7
|
+
declare const HUD_REVIEW_MARKER = "__HANSENEXUS_HUD_REVIEW__";
|
|
8
|
+
//#endregion
|
|
9
|
+
export { HUD_REVIEW_MARKER as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/core/marker.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Literal a consumer's CI bundle gate greps for in production client chunks.
|
|
4
|
+
* It is rendered as an attribute on the HUD root, so a bundler cannot drop it
|
|
5
|
+
* while keeping the HUD, and it is unmistakably deliberate rather than an
|
|
6
|
+
* incidental string a refactor could rename away.
|
|
7
|
+
*/
|
|
8
|
+
declare const HUD_DEV_MARKER = "__HANSENEXUS_HUD_DEV__";
|
|
9
|
+
/** Tag name of the element that hosts the HUD's shadow root. */
|
|
10
|
+
declare const HUD_HOST_TAG = "hn-hud";
|
|
11
|
+
//#endregion
|
|
12
|
+
export { HUD_HOST_TAG as n, HUD_DEV_MARKER as t };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/review/marker.ts
|
|
2
|
+
/**
|
|
3
|
+
* Literal on the review entrypoint's root. A consumer's bundle gate expects it
|
|
4
|
+
* where the review widget is meant to ship and nowhere else; it never appears
|
|
5
|
+
* next to `HUD_DEV_MARKER`, which belongs to the dev shell alone.
|
|
6
|
+
*/
|
|
7
|
+
const HUD_REVIEW_MARKER = "__HANSENEXUS_HUD_REVIEW__";
|
|
8
|
+
//#endregion
|
|
9
|
+
export { HUD_REVIEW_MARKER as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/core/marker.ts
|
|
2
|
+
/**
|
|
3
|
+
* Literal a consumer's CI bundle gate greps for in production client chunks.
|
|
4
|
+
* It is rendered as an attribute on the HUD root, so a bundler cannot drop it
|
|
5
|
+
* while keeping the HUD, and it is unmistakably deliberate rather than an
|
|
6
|
+
* incidental string a refactor could rename away.
|
|
7
|
+
*/
|
|
8
|
+
const HUD_DEV_MARKER = "__HANSENEXUS_HUD_DEV__";
|
|
9
|
+
/** Tag name of the element that hosts the HUD's shadow root. */
|
|
10
|
+
const HUD_HOST_TAG = "hn-hud";
|
|
11
|
+
//#endregion
|
|
12
|
+
export { HUD_HOST_TAG as n, HUD_DEV_MARKER as t };
|
package/dist/markers.js
ADDED