@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.
Files changed (54) hide show
  1. package/README.md +186 -4
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
  5. package/dist/env-DfxpPfs9.js +28 -0
  6. package/dist/env-vEjyJfPW.d.ts +17 -0
  7. package/dist/force-state-BaqWsmrf.js +153 -0
  8. package/dist/force-state-CCqoTfVB.d.ts +23 -0
  9. package/dist/handler-WmE4Lu7l.d.ts +39 -0
  10. package/dist/index-BYsdS-7d.d.ts +55 -0
  11. package/dist/index.d.ts +6 -36
  12. package/dist/index.js +119 -202
  13. package/dist/marker-C66LxOhJ.d.ts +9 -0
  14. package/dist/marker-CMrvxPmS.d.ts +12 -0
  15. package/dist/marker-DjeGvL-F.js +9 -0
  16. package/dist/marker-uZYPOJ4M.js +12 -0
  17. package/dist/markers.d.ts +3 -0
  18. package/dist/markers.js +3 -0
  19. package/dist/next.d.ts +3 -18
  20. package/dist/next.js +3 -28
  21. package/dist/payload-CLKDVZWl.d.ts +68 -0
  22. package/dist/plugins/access/server.d.ts +122 -0
  23. package/dist/plugins/access/server.js +339 -0
  24. package/dist/plugins/access.d.ts +41 -0
  25. package/dist/plugins/access.js +250 -0
  26. package/dist/plugins/agent/server.d.ts +129 -0
  27. package/dist/plugins/agent/server.js +429 -0
  28. package/dist/plugins/agent.d.ts +37 -0
  29. package/dist/plugins/agent.js +411 -0
  30. package/dist/plugins/design.d.ts +125 -0
  31. package/dist/plugins/design.js +619 -0
  32. package/dist/plugins/observe.d.ts +64 -0
  33. package/dist/plugins/observe.js +617 -0
  34. package/dist/plugins/ops/server.d.ts +82 -0
  35. package/dist/plugins/ops/server.js +380 -0
  36. package/dist/plugins/ops.d.ts +26 -0
  37. package/dist/plugins/ops.js +362 -0
  38. package/dist/plugins/vitals.js +3 -86
  39. package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
  40. package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
  41. package/dist/review.d.ts +181 -0
  42. package/dist/review.js +1017 -0
  43. package/dist/selector-NSYSe7AZ.js +28 -0
  44. package/dist/server.d.ts +3 -39
  45. package/dist/server.js +1 -1
  46. package/dist/shortcut-U6ftVY06.js +222 -0
  47. package/dist/state-boundary.d.ts +26 -0
  48. package/dist/state-boundary.js +61 -0
  49. package/dist/styles--slk86ZQ.js +332 -0
  50. package/dist/types-DADBRiGk.d.ts +50 -0
  51. package/dist/types-QJUajwWq.js +35 -0
  52. package/dist/types-f3jlNxzw.d.ts +92 -0
  53. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  54. 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 HudPlugin, d as PinEdge, f as definePlugin, n as HudCapability, o as HudPluginContext, r as HudCommand, s as HudPluginProps, t as HudApiError } from "./plugins-1QmTmZyL.js";
2
- import { n as HudMountProps, r as defineHud, t as DefineHudOptions } from "./define-hud-BHQR8Lbv.js";
3
- //#region src/core/marker.d.ts
4
- /**
5
- * Literal a consumer's CI bundle gate greps for in production client chunks.
6
- * It is rendered as an attribute on the HUD root, so a bundler cannot drop it
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 resolvePlugins, i as definePlugin, n as HudApiError, r as createPluginContext } from "./plugins-CzUiTn_r.js";
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
- import { createPortal } from "react-dom";
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 = [...BUILTIN, ...commands].map((command) => registry.register(command));
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
- pin: (edge) => pin(plugin.id, edge)
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
- pin
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: useCallback((id) => panelsRef.current?.close(id), []),
1125
- pin: useCallback((id, edge) => panelsRef.current?.pin(id, edge), [])
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(() => slots.flatMap(({ plugin, ctx }) => (plugin.commands ?? []).map((command) => ({
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__ */ jsx(PanelManager, {
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
- function DefinedHud({ envLabel }) {
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 };
@@ -0,0 +1,3 @@
1
+ import { t as HUD_DEV_MARKER } from "./marker-CMrvxPmS.js";
2
+ import { t as HUD_REVIEW_MARKER } from "./marker-C66LxOhJ.js";
3
+ export { HUD_DEV_MARKER, HUD_REVIEW_MARKER };
@@ -0,0 +1,3 @@
1
+ import { t as HUD_DEV_MARKER } from "./marker-uZYPOJ4M.js";
2
+ import { t as HUD_REVIEW_MARKER } from "./marker-DjeGvL-F.js";
3
+ export { HUD_DEV_MARKER, HUD_REVIEW_MARKER };