@hansenexus/hud 0.1.1 → 0.3.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 (100) hide show
  1. package/README.md +106 -25
  2. package/dist/define-hud-BHQR8Lbv.d.ts +31 -0
  3. package/dist/index.d.ts +36 -0
  4. package/dist/index.js +1198 -0
  5. package/dist/next.d.ts +48 -0
  6. package/dist/next.js +77 -0
  7. package/dist/plugins/vitals.d.ts +25 -0
  8. package/dist/plugins/vitals.js +109 -0
  9. package/dist/plugins-1QmTmZyL.d.ts +76 -0
  10. package/dist/plugins-CzUiTn_r.js +68 -0
  11. package/dist/server.d.ts +39 -0
  12. package/dist/server.js +106 -0
  13. package/package.json +55 -133
  14. package/chunk-20zke7cs.js +0 -574
  15. package/chunk-24vrg16w.js +0 -39
  16. package/chunk-50pq4sg6.js +0 -41
  17. package/chunk-5gtx3pza.js +0 -9
  18. package/chunk-6cdpasm9.js +0 -79
  19. package/chunk-6jt8yg4m.js +0 -117
  20. package/chunk-6ngkpwz7.js +0 -127
  21. package/chunk-71se605m.js +0 -85
  22. package/chunk-82nm9wcg.js +0 -316
  23. package/chunk-9hw5r9y1.js +0 -67
  24. package/chunk-c5y856ad.js +0 -98
  25. package/chunk-edq4jqch.js +0 -89
  26. package/chunk-emfnvz4p.js +0 -166
  27. package/chunk-h0hmjdqa.js +0 -264
  28. package/chunk-hc2g0n35.js +0 -24
  29. package/chunk-hex3wqag.js +0 -179
  30. package/chunk-kvxve0hx.js +0 -140
  31. package/chunk-kyeb6qeq.js +0 -95
  32. package/chunk-mz55rvw3.js +0 -172
  33. package/chunk-nm82wr8r.js +0 -58
  34. package/chunk-q15xwpy8.js +0 -48
  35. package/chunk-q96mkjve.js +0 -68
  36. package/chunk-tczzx083.js +0 -26
  37. package/chunk-tf774c4w.js +0 -115
  38. package/chunk-v086j53b.js +0 -19
  39. package/chunk-vjh0d29g.js +0 -52
  40. package/chunk-xezxydm3.js +0 -47
  41. package/chunk-xztw0xyt.js +0 -113
  42. package/chunk-yn1d38pb.js +0 -172
  43. package/core/access.d.mts +0 -53
  44. package/core/access.js +0 -27
  45. package/core/api-types.d.mts +0 -146
  46. package/core/api-types.js +0 -9
  47. package/core/capabilities.d.mts +0 -27
  48. package/core/capabilities.js +0 -14
  49. package/core/capture-buffer.d.mts +0 -37
  50. package/core/capture-buffer.js +0 -9
  51. package/core/force-state.d.mts +0 -68
  52. package/core/force-state.js +0 -21
  53. package/core/mock-cookie.d.mts +0 -30
  54. package/core/mock-cookie.js +0 -15
  55. package/core/types.d.mts +0 -59
  56. package/core/types.js +0 -9
  57. package/hud-provider.d.mts +0 -38
  58. package/hud-provider.js +0 -15
  59. package/hud.d.mts +0 -15
  60. package/hud.js +0 -18
  61. package/index.d.mts +0 -94
  62. package/index.js +0 -170
  63. package/lazy-panel.d.mts +0 -28
  64. package/lazy-panel.js +0 -11
  65. package/panels/cicd-panel.d.mts +0 -9
  66. package/panels/cicd-panel.js +0 -13
  67. package/panels/debug-info-panel.d.mts +0 -21
  68. package/panels/debug-info-panel.js +0 -9
  69. package/panels/dispatch-panel.d.mts +0 -10
  70. package/panels/dispatch-panel.js +0 -14
  71. package/panels/env-info-panel.d.mts +0 -9
  72. package/panels/env-info-panel.js +0 -12
  73. package/panels/feature-flags-panel.d.mts +0 -26
  74. package/panels/feature-flags-panel.js +0 -9
  75. package/panels/force-state-panel.d.mts +0 -11
  76. package/panels/force-state-panel.js +0 -113
  77. package/panels/info-row.d.mts +0 -21
  78. package/panels/info-row.js +0 -14
  79. package/panels/mock-seed-panel.d.mts +0 -15
  80. package/panels/mock-seed-panel.js +0 -14
  81. package/panels/network-console-panel.d.mts +0 -6
  82. package/panels/network-console-panel.js +0 -9
  83. package/panels/role-switcher-panel.d.mts +0 -28
  84. package/panels/role-switcher-panel.js +0 -9
  85. package/panels/sessions-panel.d.mts +0 -6
  86. package/panels/sessions-panel.js +0 -13
  87. package/panels/tokens-panel.d.mts +0 -38
  88. package/panels/tokens-panel.js +0 -9
  89. package/panels/unlock-panel.d.mts +0 -33
  90. package/panels/unlock-panel.js +0 -14
  91. package/panels/variants-panel.d.mts +0 -51
  92. package/panels/variants-panel.js +0 -12
  93. package/panels/vitals-panel.d.mts +0 -11
  94. package/panels/vitals-panel.js +0 -15
  95. package/server/guard.d.mts +0 -42
  96. package/server/guard.js +0 -12
  97. package/server/unlock.d.mts +0 -28
  98. package/server/unlock.js +0 -108
  99. package/state-boundary.d.mts +0 -25
  100. package/state-boundary.js +0 -54
package/dist/index.js ADDED
@@ -0,0 +1,1198 @@
1
+ import { a as resolvePlugins, i as definePlugin, n as HudApiError, r as createPluginContext } from "./plugins-CzUiTn_r.js";
2
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { createPortal } from "react-dom";
5
+ //#region src/shell/commands/context.ts
6
+ const RegistryContext = createContext(null);
7
+ function useRegistry() {
8
+ const registry = useContext(RegistryContext);
9
+ if (!registry) throw new Error("HUD commands used outside <CommandProvider>");
10
+ return registry;
11
+ }
12
+ function useCommands() {
13
+ const registry = useRegistry();
14
+ return useSyncExternalStore(registry.subscribe, registry.list, registry.list);
15
+ }
16
+ /** Registers `commands` while the caller is mounted. Pass a stable (memoised) array. */
17
+ function useRegisterCommands(commands) {
18
+ const registry = useRegistry();
19
+ useEffect(() => {
20
+ const unregister = commands.map((command) => registry.register(command));
21
+ return () => {
22
+ for (const off of unregister) off();
23
+ };
24
+ }, [registry, commands]);
25
+ }
26
+ //#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
+ //#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
+ /**
68
+ * Commands the shell and plugins contribute. Registering an id again
69
+ * replaces the earlier command; the first registered shortcut match wins.
70
+ */
71
+ var CommandRegistry = class {
72
+ #commands = /* @__PURE__ */ new Map();
73
+ #snapshot = [];
74
+ #listeners = /* @__PURE__ */ new Set();
75
+ register(command) {
76
+ this.#commands.set(command.id, command);
77
+ this.#emit();
78
+ return () => {
79
+ if (this.#commands.get(command.id) !== command) return;
80
+ this.#commands.delete(command.id);
81
+ this.#emit();
82
+ };
83
+ }
84
+ list = () => this.#snapshot;
85
+ subscribe = (listener) => {
86
+ this.#listeners.add(listener);
87
+ return () => this.#listeners.delete(listener);
88
+ };
89
+ run(id) {
90
+ const command = this.#commands.get(id);
91
+ command?.run();
92
+ return command !== void 0;
93
+ }
94
+ /** Runs the command bound to this key press, if any, and reports whether one ran. */
95
+ handleKey(event, apple) {
96
+ for (const command of this.#commands.values()) if (command.shortcut && matchesShortcut(event, parseShortcut(command.shortcut), apple)) {
97
+ command.run();
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ #emit() {
103
+ this.#snapshot = [...this.#commands.values()];
104
+ for (const listener of this.#listeners) listener();
105
+ }
106
+ };
107
+ //#endregion
108
+ //#region src/shell/commands/command-provider.tsx
109
+ const BUILTIN = [GRAB_COMMAND];
110
+ /** Owns one HUD's command registry and binds its shortcuts on `window`. */
111
+ function CommandProvider({ commands, children }) {
112
+ const [registry] = useState(() => new CommandRegistry());
113
+ useEffect(() => {
114
+ const unregister = [...BUILTIN, ...commands].map((command) => registry.register(command));
115
+ return () => {
116
+ for (const off of unregister) off();
117
+ };
118
+ }, [registry, commands]);
119
+ useEffect(() => {
120
+ const apple = isApplePlatform();
121
+ const onKeyDown = (event) => {
122
+ if (event.repeat || event.defaultPrevented) return;
123
+ if (registry.handleKey(event, apple)) event.preventDefault();
124
+ };
125
+ window.addEventListener("keydown", onKeyDown, true);
126
+ return () => window.removeEventListener("keydown", onKeyDown, true);
127
+ }, [registry]);
128
+ return /* @__PURE__ */ jsx(RegistryContext.Provider, {
129
+ value: registry,
130
+ children
131
+ });
132
+ }
133
+ //#endregion
134
+ //#region src/shell/panels/layout.ts
135
+ const EMPTY_LAYOUT = {
136
+ open: [],
137
+ floats: {},
138
+ pins: {},
139
+ hidden: false
140
+ };
141
+ /** A pinned edge never takes more than this share of the viewport. */
142
+ const MAX_SHARE = .4;
143
+ function visiblePanels(layout) {
144
+ return layout.hidden ? [] : layout.open;
145
+ }
146
+ function openPanel(layout, id, fallback) {
147
+ return {
148
+ ...layout,
149
+ open: [...layout.open.filter((open) => open !== id), id],
150
+ floats: id in layout.floats ? layout.floats : {
151
+ ...layout.floats,
152
+ [id]: fallback
153
+ },
154
+ hidden: false
155
+ };
156
+ }
157
+ function closePanel(layout, id) {
158
+ if (!layout.open.includes(id)) return layout;
159
+ return {
160
+ ...layout,
161
+ open: layout.open.filter((open) => open !== id)
162
+ };
163
+ }
164
+ function focusPanel(layout, id) {
165
+ if (layout.open.at(-1) === id || !layout.open.includes(id)) return layout;
166
+ return {
167
+ ...layout,
168
+ open: [...layout.open.filter((open) => open !== id), id]
169
+ };
170
+ }
171
+ function movePanel(layout, id, pos) {
172
+ return {
173
+ ...layout,
174
+ floats: {
175
+ ...layout.floats,
176
+ [id]: pos
177
+ }
178
+ };
179
+ }
180
+ /** Pins `id` to `edge`, at the end of that edge's stack; `null` floats it again. */
181
+ function pinPanel(layout, id, edge) {
182
+ const pins = Object.fromEntries(Object.entries(layout.pins).filter(([pinned]) => pinned !== id));
183
+ if (edge) pins[id] = edge;
184
+ return {
185
+ ...layout,
186
+ pins
187
+ };
188
+ }
189
+ /**
190
+ * ⌘⇧H: hide a shown layout, show a hidden one, and on an empty layout open
191
+ * `firstId` so the shortcut always shows something.
192
+ */
193
+ function toggleLayout(layout, firstId, fallback) {
194
+ if (layout.hidden) return {
195
+ ...layout,
196
+ hidden: false
197
+ };
198
+ if (layout.open.length > 0) return {
199
+ ...layout,
200
+ hidden: true
201
+ };
202
+ return firstId ? openPanel(layout, firstId, fallback) : layout;
203
+ }
204
+ /**
205
+ * Where pinned panels sit. Side pins run the full height and split it; bottom
206
+ * pins sit between the side pins and split the remaining width.
207
+ */
208
+ function pinGeometry(layout, viewport) {
209
+ const visible = new Set(visiblePanels(layout));
210
+ const stacks = {
211
+ left: [],
212
+ right: [],
213
+ bottom: []
214
+ };
215
+ for (const [id, edge] of Object.entries(layout.pins)) if (visible.has(id)) stacks[edge].push(id);
216
+ const side = Math.min(360, Math.round(viewport.w * MAX_SHARE));
217
+ const bottom = Math.min(280, Math.round(viewport.h * MAX_SHARE));
218
+ const reserve = {
219
+ left: stacks.left.length > 0 ? side : 0,
220
+ right: stacks.right.length > 0 ? side : 0,
221
+ bottom: stacks.bottom.length > 0 ? bottom : 0
222
+ };
223
+ const rects = {};
224
+ const split = (ids, length, place) => {
225
+ ids.forEach((id, i) => {
226
+ const start = Math.round(i * length / ids.length);
227
+ const end = Math.round((i + 1) * length / ids.length);
228
+ rects[id] = place(start, end - start);
229
+ });
230
+ };
231
+ split(stacks.left, viewport.h, (top, height) => ({
232
+ left: 0,
233
+ top,
234
+ width: side,
235
+ height
236
+ }));
237
+ split(stacks.right, viewport.h, (top, height) => ({
238
+ left: viewport.w - side,
239
+ top,
240
+ width: side,
241
+ height
242
+ }));
243
+ split(stacks.bottom, viewport.w - reserve.left - reserve.right, (start, width) => ({
244
+ left: reserve.left + start,
245
+ top: viewport.h - bottom,
246
+ width,
247
+ height: bottom
248
+ }));
249
+ return {
250
+ reserve,
251
+ rects
252
+ };
253
+ }
254
+ /** Keeps at least this much of the title bar on screen so a panel can always be dragged back. */
255
+ const GRIP = 40;
256
+ function clampPosition(pos, width, viewport) {
257
+ return {
258
+ x: Math.min(Math.max(pos.x, GRIP - width), viewport.w - GRIP),
259
+ y: Math.min(Math.max(pos.y, 0), viewport.h - GRIP)
260
+ };
261
+ }
262
+ function snapEdge(x, y, viewport) {
263
+ if (x <= 24) return "left";
264
+ if (x >= viewport.w - 24) return "right";
265
+ if (y >= viewport.h - 24) return "bottom";
266
+ return null;
267
+ }
268
+ //#endregion
269
+ //#region src/shell/panels/panel-frame.tsx
270
+ /** The narrowest a floating panel renders (`.panel { min-width }`). */
271
+ const MIN_WIDTH = 240;
272
+ const PIN_GLYPH = {
273
+ left: "◧",
274
+ right: "◨",
275
+ bottom: "⬓"
276
+ };
277
+ const PIN_ORDER = [
278
+ "left",
279
+ "bottom",
280
+ "right"
281
+ ];
282
+ /**
283
+ * One panel window: floating and draggable, or pinned to an edge. Dropping a
284
+ * drag near the left, right or bottom edge pins it there.
285
+ */
286
+ function PanelFrame({ title, z, viewport, position, pinned, onFocus, onClose, onMove, onPin, onSnapPreview, children }) {
287
+ const panelRef = useRef(null);
288
+ const drag = useRef(null);
289
+ const [dragPos, setDragPos] = useState(null);
290
+ const pos = dragPos ?? clampPosition(position, MIN_WIDTH, viewport);
291
+ const onPointerDown = (e) => {
292
+ if (pinned || e.target.closest("button")) return;
293
+ drag.current = {
294
+ dx: e.clientX - pos.x,
295
+ dy: e.clientY - pos.y,
296
+ pos,
297
+ edge: null
298
+ };
299
+ e.currentTarget.setPointerCapture(e.pointerId);
300
+ };
301
+ const onPointerMove = (e) => {
302
+ const d = drag.current;
303
+ if (!d) return;
304
+ const width = panelRef.current?.offsetWidth ?? MIN_WIDTH;
305
+ d.pos = clampPosition({
306
+ x: e.clientX - d.dx,
307
+ y: e.clientY - d.dy
308
+ }, width, viewport);
309
+ const edge = snapEdge(e.clientX, e.clientY, viewport);
310
+ if (edge !== d.edge) {
311
+ d.edge = edge;
312
+ onSnapPreview(edge);
313
+ }
314
+ setDragPos(d.pos);
315
+ };
316
+ const onPointerUp = (e) => {
317
+ const d = drag.current;
318
+ drag.current = null;
319
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
320
+ if (!d) return;
321
+ setDragPos(null);
322
+ if (d.edge) {
323
+ onSnapPreview(null);
324
+ onPin(d.edge);
325
+ } else if (d.pos.x !== position.x || d.pos.y !== position.y) onMove(d.pos);
326
+ };
327
+ const style = pinned ? {
328
+ ...pinned.rect,
329
+ zIndex: z
330
+ } : {
331
+ left: pos.x,
332
+ top: pos.y,
333
+ zIndex: z
334
+ };
335
+ return /* @__PURE__ */ jsxs("section", {
336
+ ref: panelRef,
337
+ className: "panel",
338
+ "aria-label": title,
339
+ "data-pin": pinned?.edge,
340
+ style,
341
+ onPointerDownCapture: onFocus,
342
+ children: [/* @__PURE__ */ jsxs("div", {
343
+ className: "panel-bar",
344
+ onPointerDown,
345
+ onPointerMove,
346
+ onPointerUp,
347
+ onPointerCancel: onPointerUp,
348
+ children: [/* @__PURE__ */ jsx("span", {
349
+ className: "panel-title",
350
+ children: title
351
+ }), /* @__PURE__ */ jsxs("span", {
352
+ className: "panel-actions",
353
+ children: [pinned ? /* @__PURE__ */ jsx("button", {
354
+ type: "button",
355
+ className: "panel-action",
356
+ "aria-label": `Unpin ${title}`,
357
+ title: "Unpin",
358
+ onClick: () => onPin(null),
359
+ children: "▢"
360
+ }) : PIN_ORDER.map((edge) => /* @__PURE__ */ jsx("button", {
361
+ type: "button",
362
+ className: "panel-action",
363
+ "aria-label": `Pin ${title} to the ${edge} edge`,
364
+ title: `Pin ${edge}`,
365
+ onClick: () => onPin(edge),
366
+ children: PIN_GLYPH[edge]
367
+ }, edge)), /* @__PURE__ */ jsx("button", {
368
+ type: "button",
369
+ className: "panel-action",
370
+ "aria-label": `Close ${title}`,
371
+ onClick: onClose,
372
+ children: "×"
373
+ })]
374
+ })]
375
+ }), /* @__PURE__ */ jsx("div", {
376
+ className: "panel-body",
377
+ children
378
+ })]
379
+ });
380
+ }
381
+ //#endregion
382
+ //#region src/shell/panels/panel-sheet.tsx
383
+ /** Below 768 px every open panel lives in one full-width bottom sheet, one tab each. */
384
+ function PanelSheet({ panels, active, onSelect, onClose, children }) {
385
+ return /* @__PURE__ */ jsxs("section", {
386
+ className: "sheet",
387
+ "aria-label": active.title,
388
+ children: [/* @__PURE__ */ jsxs("div", {
389
+ className: "sheet-bar",
390
+ children: [/* @__PURE__ */ jsx("div", {
391
+ className: "sheet-tabs",
392
+ role: "tablist",
393
+ "aria-label": "Open panels",
394
+ children: panels.map((panel) => /* @__PURE__ */ jsx("button", {
395
+ type: "button",
396
+ role: "tab",
397
+ className: "sheet-tab",
398
+ "aria-selected": panel === active,
399
+ onClick: () => onSelect(panel.id),
400
+ children: panel.title
401
+ }, panel.id))
402
+ }), /* @__PURE__ */ jsx("button", {
403
+ type: "button",
404
+ className: "panel-action",
405
+ "aria-label": `Close ${active.title}`,
406
+ onClick: () => onClose(active.id),
407
+ children: "×"
408
+ })]
409
+ }), /* @__PURE__ */ jsx("div", {
410
+ className: "panel-body",
411
+ role: "tabpanel",
412
+ children
413
+ })]
414
+ });
415
+ }
416
+ //#endregion
417
+ //#region src/shell/panels/panels-context.ts
418
+ const PanelsContext = createContext(null);
419
+ function usePanels() {
420
+ const api = useContext(PanelsContext);
421
+ if (!api) throw new Error("usePanels() used outside <PanelManager>");
422
+ return api;
423
+ }
424
+ //#endregion
425
+ //#region src/shell/panels/types.ts
426
+ const EDGES = [
427
+ "left",
428
+ "right",
429
+ "bottom"
430
+ ];
431
+ //#endregion
432
+ //#region src/shell/panels/reserve.ts
433
+ const PADDING = {
434
+ left: "padding-left",
435
+ right: "padding-right",
436
+ bottom: "padding-bottom"
437
+ };
438
+ /**
439
+ * Pushes the page away from pinned panels by adding padding to <html>, so
440
+ * normal-flow content reflows instead of being covered. Inline `!important`,
441
+ * like the HUD host, so a client stylesheet cannot zero it. The host's own
442
+ * padding is kept and added to.
443
+ *
444
+ * Limit: nothing short of an iframe narrows the viewport, so the host's
445
+ * `position: fixed` elements and its media queries still see the full window.
446
+ *
447
+ * Returns a function that restores the previous inline values.
448
+ */
449
+ function reservePageSpace(reserve, root = document.documentElement) {
450
+ const touched = EDGES.filter((edge) => reserve[edge] > 0).map((edge) => PADDING[edge]);
451
+ if (touched.length === 0) return () => {};
452
+ const computed = getComputedStyle(root);
453
+ const saved = touched.map((prop) => ({
454
+ prop,
455
+ value: root.style.getPropertyValue(prop),
456
+ priority: root.style.getPropertyPriority(prop)
457
+ }));
458
+ for (const edge of EDGES) {
459
+ if (reserve[edge] <= 0) continue;
460
+ const prop = PADDING[edge];
461
+ const base = Number.parseFloat(computed.getPropertyValue(prop)) || 0;
462
+ root.style.setProperty(prop, `${base + reserve[edge]}px`, "important");
463
+ }
464
+ return () => {
465
+ for (const { prop, value, priority } of saved) if (value) root.style.setProperty(prop, value, priority);
466
+ else root.style.removeProperty(prop);
467
+ };
468
+ }
469
+ //#endregion
470
+ //#region src/shell/panels/storage.ts
471
+ const VERSION = 1;
472
+ function layoutKey(appSlug) {
473
+ return `hansenexus-hud:layout:${appSlug}`;
474
+ }
475
+ /**
476
+ * Reads the stored layout for `appSlug`, dropping panels that are no longer
477
+ * registered. Storage can be missing, blocked or full of someone else's JSON;
478
+ * every failure yields an empty layout.
479
+ */
480
+ function loadLayout(appSlug, known) {
481
+ try {
482
+ const raw = window.localStorage.getItem(layoutKey(appSlug));
483
+ return raw ? sanitizeLayout(JSON.parse(raw), known) : EMPTY_LAYOUT;
484
+ } catch {
485
+ return EMPTY_LAYOUT;
486
+ }
487
+ }
488
+ function saveLayout(appSlug, layout) {
489
+ try {
490
+ window.localStorage.setItem(layoutKey(appSlug), JSON.stringify({
491
+ v: VERSION,
492
+ ...layout
493
+ }));
494
+ } catch {}
495
+ }
496
+ function isRecord(value) {
497
+ return typeof value === "object" && value !== null && !Array.isArray(value);
498
+ }
499
+ function isPosition(value) {
500
+ return isRecord(value) && Number.isFinite(value.x) && Number.isFinite(value.y);
501
+ }
502
+ function isEdge(value) {
503
+ return EDGES.some((edge) => edge === value);
504
+ }
505
+ function sanitizeLayout(value, known) {
506
+ if (!isRecord(value) || value.v !== VERSION) return EMPTY_LAYOUT;
507
+ const open = Array.isArray(value.open) ? [...new Set(value.open.filter((id) => typeof id === "string" && known.has(id)))] : [];
508
+ const floats = {};
509
+ if (isRecord(value.floats)) {
510
+ for (const [id, pos] of Object.entries(value.floats)) if (known.has(id) && isPosition(pos)) floats[id] = {
511
+ x: pos.x,
512
+ y: pos.y
513
+ };
514
+ }
515
+ const pins = {};
516
+ if (isRecord(value.pins)) {
517
+ for (const [id, edge] of Object.entries(value.pins)) if (known.has(id) && isEdge(edge)) pins[id] = edge;
518
+ }
519
+ return {
520
+ open,
521
+ floats,
522
+ pins,
523
+ hidden: value.hidden === true
524
+ };
525
+ }
526
+ function subscribeResize(listener) {
527
+ window.addEventListener("resize", listener);
528
+ return () => window.removeEventListener("resize", listener);
529
+ }
530
+ let cached = {
531
+ w: 0,
532
+ h: 0
533
+ };
534
+ /**
535
+ * The layout viewport without scrollbars: the box `position: fixed` resolves
536
+ * against. `clientWidth` of <html> is that box regardless of its padding.
537
+ */
538
+ function readViewport() {
539
+ const root = document.documentElement;
540
+ const w = root.clientWidth || window.innerWidth;
541
+ const h = root.clientHeight || window.innerHeight;
542
+ if (w !== cached.w || h !== cached.h) cached = {
543
+ w,
544
+ h
545
+ };
546
+ return cached;
547
+ }
548
+ function useViewport() {
549
+ return useSyncExternalStore(subscribeResize, readViewport, readViewport);
550
+ }
551
+ /** Window width including the scrollbar, the same width a `max-width` media query tests. */
552
+ const readNarrow = () => window.innerWidth < 768;
553
+ function useNarrow() {
554
+ return useSyncExternalStore(subscribeResize, readNarrow, readNarrow);
555
+ }
556
+ //#endregion
557
+ //#region src/shell/panels/panel-manager.tsx
558
+ /** Where a panel opens the first time: stacked up and right from above the pill. */
559
+ function defaultPosition(index) {
560
+ const step = 28 * index;
561
+ return {
562
+ x: 12 + step,
563
+ y: Math.max(12, window.innerHeight - 300 - step)
564
+ };
565
+ }
566
+ /**
567
+ * Window management for the HUD: open panels, z-order, drag, pin-to-edge with
568
+ * page push, per-app persistence, the mobile sheet, and the command registry
569
+ * (⌘⇧H toggles the layout).
570
+ */
571
+ function PanelManager({ commands, ...props }) {
572
+ return /* @__PURE__ */ jsx(CommandProvider, {
573
+ commands,
574
+ children: /* @__PURE__ */ jsx(Workspace, { ...props })
575
+ });
576
+ }
577
+ function Workspace({ appSlug, panels, apiRef, children }) {
578
+ const byId = useMemo(() => new Map(panels.map((panel) => [panel.id, panel])), [panels]);
579
+ const [layout, setLayout] = useState(() => loadLayout(appSlug, new Set(byId.keys())));
580
+ const viewport = useViewport();
581
+ const narrow = useNarrow();
582
+ const [snap, setSnap] = useState(null);
583
+ useEffect(() => saveLayout(appSlug, layout), [appSlug, layout]);
584
+ const { reserve, rects } = useMemo(() => pinGeometry(layout, viewport), [layout, viewport]);
585
+ const { left, right, bottom } = narrow ? {
586
+ left: 0,
587
+ right: 0,
588
+ bottom: 0
589
+ } : reserve;
590
+ useLayoutEffect(() => reservePageSpace({
591
+ left,
592
+ right,
593
+ bottom
594
+ }), [
595
+ left,
596
+ right,
597
+ bottom
598
+ ]);
599
+ const api = useMemo(() => ({
600
+ panels,
601
+ isOpen: (id) => !layout.hidden && layout.open.includes(id),
602
+ open: (id) => setLayout((l) => openPanel(l, id, defaultPosition(l.open.length))),
603
+ close: (id) => setLayout((l) => closePanel(l, id)),
604
+ toggle: (id) => setLayout((l) => !l.hidden && l.open.includes(id) ? closePanel(l, id) : openPanel(l, id, defaultPosition(l.open.length))),
605
+ pin: (id, edge) => setLayout((l) => pinPanel(l, id, edge))
606
+ }), [panels, layout]);
607
+ useLayoutEffect(() => {
608
+ if (!apiRef) return;
609
+ apiRef.current = api;
610
+ return () => {
611
+ apiRef.current = null;
612
+ };
613
+ }, [apiRef, api]);
614
+ useRegisterCommands(useMemo(() => [{
615
+ id: "hud.toggle-layout",
616
+ title: "Toggle layout",
617
+ shortcut: "mod+shift+h",
618
+ run: () => setLayout((l) => toggleLayout(l, panels[0]?.id, defaultPosition(l.open.length)))
619
+ }, {
620
+ id: "hud.reset-layout",
621
+ title: "Reset layout",
622
+ run: () => setLayout(EMPTY_LAYOUT)
623
+ }], [panels]));
624
+ const zOrder = visiblePanels(layout);
625
+ const visible = panels.filter((panel) => zOrder.includes(panel.id));
626
+ const active = byId.get(zOrder.at(-1) ?? "");
627
+ const controls = (id) => ({
628
+ id,
629
+ pinned: narrow ? null : layout.pins[id] ?? null,
630
+ narrow,
631
+ pin: (edge) => api.pin(id, edge),
632
+ close: () => api.close(id)
633
+ });
634
+ const snapRect = snap ? pinGeometry(pinPanel(layout, snap.id, snap.edge), viewport).rects[snap.id] : void 0;
635
+ const offsets = {
636
+ "--hud-reserve-left": `${left}px`,
637
+ "--hud-reserve-bottom": `${bottom}px`
638
+ };
639
+ return /* @__PURE__ */ jsx(PanelsContext.Provider, {
640
+ value: api,
641
+ children: /* @__PURE__ */ jsxs("div", {
642
+ className: "hud-layout",
643
+ style: offsets,
644
+ children: [
645
+ children,
646
+ narrow ? active && /* @__PURE__ */ jsx(PanelSheet, {
647
+ panels: visible,
648
+ active,
649
+ onSelect: (id) => setLayout((l) => focusPanel(l, id)),
650
+ onClose: api.close,
651
+ children: active.render(controls(active.id))
652
+ }) : visible.map((panel) => {
653
+ const edge = layout.pins[panel.id];
654
+ const rect = rects[panel.id];
655
+ return /* @__PURE__ */ jsx(PanelFrame, {
656
+ title: panel.title,
657
+ z: zOrder.indexOf(panel.id) + 1,
658
+ viewport,
659
+ position: layout.floats[panel.id] ?? defaultPosition(0),
660
+ pinned: edge && rect ? {
661
+ edge,
662
+ rect
663
+ } : null,
664
+ onFocus: () => setLayout((l) => focusPanel(l, panel.id)),
665
+ onClose: () => api.close(panel.id),
666
+ onMove: (pos) => setLayout((l) => movePanel(l, panel.id, pos)),
667
+ onPin: (next) => api.pin(panel.id, next),
668
+ onSnapPreview: (next) => setSnap(next ? {
669
+ id: panel.id,
670
+ edge: next
671
+ } : null),
672
+ children: panel.render(controls(panel.id))
673
+ }, panel.id);
674
+ }),
675
+ snapRect && /* @__PURE__ */ jsx("div", {
676
+ className: "snap-zone",
677
+ style: snapRect
678
+ })
679
+ ]
680
+ })
681
+ });
682
+ }
683
+ //#endregion
684
+ //#region src/shell/panels/pill-menu.tsx
685
+ /** The pill's "⋯" menu: every panel, every command with its shortcut, and Reset layout. */
686
+ function PillMenu() {
687
+ const panels = usePanels();
688
+ const commands = useCommands();
689
+ const apple = useMemo(isApplePlatform, []);
690
+ const [open, setOpen] = useState(false);
691
+ const ref = useRef(null);
692
+ useEffect(() => {
693
+ if (!open) return;
694
+ const onPointerDown = (e) => {
695
+ if (ref.current && !e.composedPath().includes(ref.current)) setOpen(false);
696
+ };
697
+ const onKeyDown = (e) => {
698
+ if (e.key === "Escape") setOpen(false);
699
+ };
700
+ window.addEventListener("pointerdown", onPointerDown, true);
701
+ window.addEventListener("keydown", onKeyDown, true);
702
+ return () => {
703
+ window.removeEventListener("pointerdown", onPointerDown, true);
704
+ window.removeEventListener("keydown", onKeyDown, true);
705
+ };
706
+ }, [open]);
707
+ return /* @__PURE__ */ jsxs("div", {
708
+ className: "menu-anchor",
709
+ ref,
710
+ children: [/* @__PURE__ */ jsx("button", {
711
+ type: "button",
712
+ className: "pill-item",
713
+ "aria-label": "HUD menu",
714
+ "aria-haspopup": "menu",
715
+ "aria-expanded": open,
716
+ onClick: () => setOpen((o) => !o),
717
+ children: "⋯"
718
+ }), open && /* @__PURE__ */ jsxs("div", {
719
+ className: "menu",
720
+ role: "menu",
721
+ "aria-label": "HUD menu",
722
+ children: [
723
+ /* @__PURE__ */ jsx("div", {
724
+ className: "menu-label",
725
+ children: "Panels"
726
+ }),
727
+ panels.panels.map((panel) => /* @__PURE__ */ jsxs("button", {
728
+ type: "button",
729
+ role: "menuitemcheckbox",
730
+ className: "menu-item",
731
+ "aria-checked": panels.isOpen(panel.id),
732
+ onClick: () => panels.toggle(panel.id),
733
+ children: [/* @__PURE__ */ jsx("span", { children: panel.title }), /* @__PURE__ */ jsx("span", {
734
+ className: "menu-check",
735
+ children: panels.isOpen(panel.id) ? "✓" : ""
736
+ })]
737
+ }, panel.id)),
738
+ /* @__PURE__ */ jsx("div", {
739
+ className: "menu-label",
740
+ children: "Commands"
741
+ }),
742
+ commands.map((command) => /* @__PURE__ */ jsxs("button", {
743
+ type: "button",
744
+ role: "menuitem",
745
+ className: "menu-item",
746
+ onClick: () => {
747
+ setOpen(false);
748
+ command.run();
749
+ },
750
+ children: [/* @__PURE__ */ jsx("span", { children: command.title }), command.shortcut && /* @__PURE__ */ jsx("kbd", { children: formatShortcut(parseShortcut(command.shortcut), apple) })]
751
+ }, command.id))
752
+ ]
753
+ })]
754
+ });
755
+ }
756
+ //#endregion
757
+ //#region src/shell/panels/styles.ts
758
+ /**
759
+ * Panel manager styles, appended to `HUD_CSS` inside the same shadow root and
760
+ * built on its `--hud-*` tokens.
761
+ */
762
+ const PANEL_CSS = `
763
+ .hud-layout { display: contents; }
764
+
765
+ /* Pinned panels push the page; the pill moves with it so it stays visible. */
766
+ .pill {
767
+ left: calc(12px + var(--hud-reserve-left, 0px));
768
+ bottom: calc(12px + var(--hud-reserve-bottom, 0px));
769
+ }
770
+
771
+ .menu-anchor { position: relative; display: flex; }
772
+
773
+ .menu {
774
+ position: absolute;
775
+ left: 0;
776
+ bottom: calc(100% + 8px);
777
+ display: flex;
778
+ flex-direction: column;
779
+ min-width: 220px;
780
+ padding: 4px;
781
+ background: var(--hud-bg);
782
+ border: 1px solid var(--hud-border);
783
+ border-radius: var(--hud-radius);
784
+ box-shadow: var(--hud-shadow);
785
+ font-family: var(--hud-font);
786
+ }
787
+
788
+ .menu-label {
789
+ padding: 6px 8px 2px;
790
+ font-size: 10px;
791
+ font-weight: 600;
792
+ letter-spacing: 0.06em;
793
+ text-transform: uppercase;
794
+ color: var(--hud-fg-muted);
795
+ }
796
+
797
+ .hud .menu-item {
798
+ display: flex;
799
+ align-items: center;
800
+ justify-content: space-between;
801
+ gap: 16px;
802
+ padding: 4px 8px;
803
+ border-radius: 4px;
804
+ text-align: left;
805
+ }
806
+
807
+ .hud .menu-item:hover { background: var(--hud-bg-subtle); }
808
+ .menu-check { color: var(--hud-accent); }
809
+ .menu kbd { font-family: var(--hud-mono); font-size: 11px; color: var(--hud-fg-muted); }
810
+
811
+ .panel {
812
+ position: fixed;
813
+ display: flex;
814
+ flex-direction: column;
815
+ min-width: 240px;
816
+ max-width: min(420px, calc(100vw - 24px));
817
+ max-height: calc(100vh - 24px);
818
+ background: var(--hud-bg);
819
+ border: 1px solid var(--hud-border);
820
+ border-radius: var(--hud-radius);
821
+ box-shadow: var(--hud-shadow);
822
+ pointer-events: auto;
823
+ overflow: hidden;
824
+ }
825
+
826
+ .panel[data-pin] {
827
+ min-width: 0;
828
+ max-width: none;
829
+ max-height: none;
830
+ border-radius: 0;
831
+ box-shadow: none;
832
+ }
833
+
834
+ .panel-bar {
835
+ display: flex;
836
+ align-items: center;
837
+ justify-content: space-between;
838
+ gap: 8px;
839
+ padding: 6px 6px 6px 10px;
840
+ border-bottom: 1px solid var(--hud-border);
841
+ background: var(--hud-bg-subtle);
842
+ cursor: grab;
843
+ user-select: none;
844
+ touch-action: none;
845
+ }
846
+
847
+ .panel-bar:active { cursor: grabbing; }
848
+ .panel[data-pin] .panel-bar { cursor: default; }
849
+ .panel-title { font-weight: 600; }
850
+ .panel-actions { display: flex; gap: 2px; }
851
+ .hud .panel-action {
852
+ width: 20px;
853
+ height: 20px;
854
+ border-radius: 4px;
855
+ color: var(--hud-fg-muted);
856
+ line-height: 20px;
857
+ text-align: center;
858
+ }
859
+ .hud .panel-action:hover { background: var(--hud-border); color: var(--hud-fg); }
860
+ .panel-body { flex: 1; padding: 8px 10px; overflow: auto; }
861
+
862
+ .snap-zone {
863
+ position: fixed;
864
+ z-index: 1000;
865
+ background: color-mix(in srgb, var(--hud-accent) 18%, transparent);
866
+ border: 2px dashed var(--hud-accent);
867
+ pointer-events: none;
868
+ }
869
+
870
+ .sheet {
871
+ position: fixed;
872
+ left: 0;
873
+ right: 0;
874
+ bottom: 0;
875
+ z-index: 1000;
876
+ display: flex;
877
+ flex-direction: column;
878
+ max-height: 60vh;
879
+ background: var(--hud-bg);
880
+ border-top: 1px solid var(--hud-border);
881
+ border-radius: var(--hud-radius) var(--hud-radius) 0 0;
882
+ box-shadow: var(--hud-shadow);
883
+ pointer-events: auto;
884
+ }
885
+
886
+ .sheet-bar {
887
+ display: flex;
888
+ align-items: center;
889
+ gap: 8px;
890
+ padding: 4px 6px;
891
+ border-bottom: 1px solid var(--hud-border);
892
+ background: var(--hud-bg-subtle);
893
+ border-radius: var(--hud-radius) var(--hud-radius) 0 0;
894
+ }
895
+
896
+ .sheet-tabs { display: flex; flex: 1; gap: 2px; overflow-x: auto; }
897
+ .hud .sheet-tab { padding: 4px 10px; border-radius: 4px; white-space: nowrap; color: var(--hud-fg-muted); }
898
+ .hud .sheet-tab[aria-selected="true"] { background: var(--hud-bg); color: var(--hud-fg); font-weight: 600; }
899
+ `;
900
+ //#endregion
901
+ //#region src/shell/plugin-slots.tsx
902
+ /** One stable context per plugin, so panels and pills can depend on `ctx` in effects. */
903
+ function usePluginSlots(plugins, { app, apiBase, can, close, pin }) {
904
+ return useMemo(() => plugins.map((plugin) => ({
905
+ plugin,
906
+ title: plugin.title ?? plugin.id,
907
+ ctx: createPluginContext(plugin, {
908
+ app,
909
+ apiBase,
910
+ can,
911
+ close: () => close(plugin.id),
912
+ pin: (edge) => pin(plugin.id, edge)
913
+ })
914
+ })), [
915
+ plugins,
916
+ app,
917
+ apiBase,
918
+ can,
919
+ close,
920
+ pin
921
+ ]);
922
+ }
923
+ /**
924
+ * A plugin's pill item. With a panel it is a toggle button; without one it
925
+ * only shows the plugin's `pill` content.
926
+ */
927
+ function PluginPillItem({ slot, open, onToggle }) {
928
+ const { plugin, ctx, title } = slot;
929
+ const Pill = plugin.pill;
930
+ const content = Pill ? /* @__PURE__ */ jsx(Pill, { ctx }) : title;
931
+ if (!plugin.panel) return Pill ? /* @__PURE__ */ jsx("span", {
932
+ className: "pill-item",
933
+ children: content
934
+ }) : null;
935
+ return /* @__PURE__ */ jsx("button", {
936
+ type: "button",
937
+ className: "pill-item",
938
+ "aria-pressed": open,
939
+ title,
940
+ onClick: onToggle,
941
+ children: content
942
+ });
943
+ }
944
+ function PluginPanelBody({ slot }) {
945
+ const Panel = slot.plugin.panel;
946
+ return Panel ? /* @__PURE__ */ jsx(Panel, { ctx: slot.ctx }) : null;
947
+ }
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
+ //#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;
1096
+ const NO_PLUGINS = [];
1097
+ const GRANT_ALL = () => true;
1098
+ /**
1099
+ * The HUD shell: a shadow-rooted status pill and floating panels.
1100
+ *
1101
+ * It renders unconditionally. Keeping it out of production builds is the
1102
+ * loader's job, at the import site, so a bundler can drop the whole module
1103
+ * (see `@hansenexus/hud/next`).
1104
+ */
1105
+ function Hud({ appSlug, envLabel = "DEV", plugins = NO_PLUGINS, apiBase = "/api/hud", can = GRANT_ALL }) {
1106
+ const resolved = useMemo(() => resolvePlugins(plugins, can), [plugins, can]);
1107
+ return /* @__PURE__ */ jsx(ShadowHost, {
1108
+ css: CSS,
1109
+ children: /* @__PURE__ */ jsx(Shell, {
1110
+ appSlug,
1111
+ envLabel,
1112
+ plugins: resolved,
1113
+ apiBase,
1114
+ can
1115
+ })
1116
+ });
1117
+ }
1118
+ function Shell({ appSlug, envLabel, plugins, apiBase, can }) {
1119
+ const panelsRef = useRef(null);
1120
+ const slots = usePluginSlots(plugins, {
1121
+ app: appSlug,
1122
+ apiBase,
1123
+ can,
1124
+ close: useCallback((id) => panelsRef.current?.close(id), []),
1125
+ pin: useCallback((id, edge) => panelsRef.current?.pin(id, edge), [])
1126
+ });
1127
+ const panels = useMemo(() => slots.filter((slot) => slot.plugin.panel).map((slot) => ({
1128
+ id: slot.plugin.id,
1129
+ title: slot.title,
1130
+ render: () => /* @__PURE__ */ jsx(PluginPanelBody, { slot })
1131
+ })), [slots]);
1132
+ const commands = useMemo(() => slots.flatMap(({ plugin, ctx }) => (plugin.commands ?? []).map((command) => ({
1133
+ id: `${plugin.id}.${command.id}`,
1134
+ title: command.title,
1135
+ shortcut: command.shortcut,
1136
+ run: () => command.run(ctx)
1137
+ }))), [slots]);
1138
+ return /* @__PURE__ */ jsx(PanelManager, {
1139
+ appSlug,
1140
+ panels,
1141
+ commands,
1142
+ apiRef: panelsRef,
1143
+ children: /* @__PURE__ */ jsx(Pill, {
1144
+ appSlug,
1145
+ envLabel,
1146
+ slots
1147
+ })
1148
+ });
1149
+ }
1150
+ function Pill({ appSlug, envLabel, slots }) {
1151
+ const panels = usePanels();
1152
+ return /* @__PURE__ */ jsxs("div", {
1153
+ className: "pill",
1154
+ role: "toolbar",
1155
+ "aria-label": `${appSlug} hud`,
1156
+ children: [
1157
+ /* @__PURE__ */ jsx("span", {
1158
+ className: "pill-item env",
1159
+ children: envLabel
1160
+ }),
1161
+ slots.map((slot) => /* @__PURE__ */ jsx(PluginPillItem, {
1162
+ slot,
1163
+ open: panels.isOpen(slot.plugin.id),
1164
+ onToggle: () => panels.toggle(slot.plugin.id)
1165
+ }, slot.plugin.id)),
1166
+ /* @__PURE__ */ jsx(PillMenu, {})
1167
+ ]
1168
+ });
1169
+ }
1170
+ //#endregion
1171
+ //#region src/core/define-hud.tsx
1172
+ /**
1173
+ * The app's one HUD file:
1174
+ *
1175
+ * ```tsx
1176
+ * export default defineHud({ app: "hansenexus", plugins: [vitals()] });
1177
+ * ```
1178
+ *
1179
+ * Returns a component. The plugin list is frozen here, so the shell sees a
1180
+ * stable array across renders.
1181
+ */
1182
+ function defineHud({ app, plugins, apiBase, can }) {
1183
+ resolvePlugins(plugins, () => true);
1184
+ const list = Object.freeze([...plugins]);
1185
+ function DefinedHud({ envLabel }) {
1186
+ return /* @__PURE__ */ jsx(Hud, {
1187
+ appSlug: app,
1188
+ plugins: list,
1189
+ ...envLabel === void 0 ? {} : { envLabel },
1190
+ ...apiBase === void 0 ? {} : { apiBase },
1191
+ ...can === void 0 ? {} : { can }
1192
+ });
1193
+ }
1194
+ DefinedHud.displayName = `Hud(${app})`;
1195
+ return DefinedHud;
1196
+ }
1197
+ //#endregion
1198
+ export { HUD_DEV_MARKER, HUD_HOST_TAG, Hud, HudApiError, defineHud, definePlugin };