@reticlehq/browser 2.7.0 → 2.9.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 (92) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +2 -2
  3. package/dist/actions/actions-dom.js +23 -15
  4. package/dist/actions/actions.d.ts +17 -0
  5. package/dist/actions/actions.js +132 -29
  6. package/dist/actions/synthetic-input.d.ts +9 -0
  7. package/dist/actions/synthetic-input.js +40 -0
  8. package/dist/commands/commands.js +18 -1
  9. package/dist/connect-options.d.ts +9 -9
  10. package/dist/dom/a11y.d.ts +9 -0
  11. package/dist/dom/a11y.js +19 -0
  12. package/dist/dom/desktop-capture.js +20 -3
  13. package/dist/dom/dom-ignore.d.ts +19 -6
  14. package/dist/dom/dom-ignore.js +20 -13
  15. package/dist/dom/query.d.ts +2 -2
  16. package/dist/dom/query.js +85 -45
  17. package/dist/dom/refs.d.ts +28 -0
  18. package/dist/dom/refs.js +87 -0
  19. package/dist/dom/snapshot.d.ts +5 -0
  20. package/dist/dom/snapshot.js +34 -0
  21. package/dist/dom/source.d.ts +14 -0
  22. package/dist/dom/source.js +16 -0
  23. package/dist/dom/theme.d.ts +33 -10
  24. package/dist/dom/theme.js +109 -50
  25. package/dist/observers/console.js +5 -2
  26. package/dist/observers/install-all.js +4 -1
  27. package/dist/observers/ipc.d.ts +16 -0
  28. package/dist/observers/ipc.js +19 -1
  29. package/dist/observers/network.d.ts +8 -0
  30. package/dist/observers/network.js +29 -6
  31. package/dist/observers/state.js +21 -2
  32. package/dist/presenter/presenter-brand.d.ts +24 -7
  33. package/dist/presenter/presenter-brand.js +23 -6
  34. package/dist/presenter/presenter-config.d.ts +102 -9
  35. package/dist/presenter/presenter-config.js +94 -8
  36. package/dist/presenter/presenter-controls.d.ts +10 -10
  37. package/dist/presenter/presenter-controls.js +175 -85
  38. package/dist/presenter/presenter-dock-layout.d.ts +6 -0
  39. package/dist/presenter/presenter-dock-layout.js +170 -0
  40. package/dist/presenter/presenter-drag.d.ts +48 -0
  41. package/dist/presenter/presenter-drag.js +198 -0
  42. package/dist/presenter/presenter-effects.d.ts +1 -1
  43. package/dist/presenter/presenter-effects.js +1 -1
  44. package/dist/presenter/presenter-glow.d.ts +5 -5
  45. package/dist/presenter/presenter-glow.js +7 -7
  46. package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
  47. package/dist/presenter/presenter-heroicons-data.js +38 -0
  48. package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
  49. package/dist/presenter/presenter-hud-chrome.js +41 -0
  50. package/dist/presenter/presenter-icons.d.ts +44 -0
  51. package/dist/presenter/presenter-icons.js +100 -0
  52. package/dist/presenter/presenter-log.d.ts +27 -5
  53. package/dist/presenter/presenter-log.js +114 -28
  54. package/dist/presenter/presenter-report-copy.d.ts +83 -0
  55. package/dist/presenter/presenter-report-copy.js +127 -0
  56. package/dist/presenter/presenter-report-styles.d.ts +7 -0
  57. package/dist/presenter/presenter-report-styles.js +102 -0
  58. package/dist/presenter/presenter-report.d.ts +23 -0
  59. package/dist/presenter/presenter-report.js +217 -0
  60. package/dist/presenter/presenter-run-state.d.ts +1 -1
  61. package/dist/presenter/presenter-settings-styles.d.ts +2 -0
  62. package/dist/presenter/presenter-settings-styles.js +153 -0
  63. package/dist/presenter/presenter-settings.d.ts +82 -0
  64. package/dist/presenter/presenter-settings.js +494 -0
  65. package/dist/presenter/presenter-shell-styles.d.ts +1 -0
  66. package/dist/presenter/presenter-shell-styles.js +375 -0
  67. package/dist/presenter/presenter-shell.d.ts +48 -0
  68. package/dist/presenter/presenter-shell.js +408 -0
  69. package/dist/presenter/presenter-styles.d.ts +9 -4
  70. package/dist/presenter/presenter-styles.js +61 -125
  71. package/dist/presenter/presenter-tally.d.ts +8 -11
  72. package/dist/presenter/presenter-tally.js +48 -23
  73. package/dist/presenter/presenter-test-helpers.d.ts +1 -1
  74. package/dist/presenter/presenter-test-helpers.js +1 -1
  75. package/dist/presenter/presenter-verbs.js +2 -2
  76. package/dist/presenter/presenter-workspace.d.ts +15 -0
  77. package/dist/presenter/presenter-workspace.js +180 -0
  78. package/dist/presenter/presenter.d.ts +15 -10
  79. package/dist/presenter/presenter.js +194 -46
  80. package/dist/registry/emitter.d.ts +2 -2
  81. package/dist/reticle.d.ts +2 -2
  82. package/dist/reticle.js +27 -25
  83. package/dist/review/annotator-styles.d.ts +6 -0
  84. package/dist/review/annotator-styles.js +81 -0
  85. package/dist/review/annotator.d.ts +29 -17
  86. package/dist/review/annotator.js +477 -146
  87. package/dist/timers/clock.js +8 -5
  88. package/dist/transport/unreachable-message.d.ts +12 -0
  89. package/dist/transport/unreachable-message.js +20 -0
  90. package/dist/util/captured-method.d.ts +37 -0
  91. package/dist/util/captured-method.js +45 -0
  92. package/package.json +9 -6
@@ -1,13 +1,17 @@
1
1
  import { HumanControlKind, PresenterTone, SessionState } from '@reticlehq/core';
2
2
  import { nativeSetTimeout, nativeClearTimeout } from '../timers/native-timers.js';
3
- // Live-control panel: the two-way control surface inside the floating HUD — Pause/Resume + End
3
+ import { CHAT_TOGGLE_ATTR, CLEAR_MARKS_ATTR, MARKERS_BTN_ATTR } from './presenter-config.js';
4
+ import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon, hiIconHtml, hiToggleIconHtml, } from './presenter-icons.js';
5
+ import { getPresenterSettings } from './presenter-settings.js';
6
+ import { mountWorkspaceSelector, workspaceRowHtml } from './presenter-workspace.js';
7
+ // Live-control panel: the two-way control surface inside the floating HUD - Pause/Resume + End
4
8
  // (header), a message input + Send (footer), and the data-reticle-state visual machine. Split out of
5
9
  // presenter.ts to keep both files under the 500-line cap (mirrors the presenter-log.ts split).
6
10
  // All nodes carry data-reticle-* attrs so they're excluded from snapshots (see dom-ignore.ts). The
7
11
  // strings here are presenter-only UI; the control kinds + state values reuse protocol constants.
8
12
  /** data-reticle-state attribute on the overlay root; its value is always a SessionState. */
9
13
  const DATA_RETICLE_STATE = 'data-reticle-state';
10
- /** data-reticle-tone on the overlay root — waiting/ask/warn distinguishes how the agent handed back. */
14
+ /** data-reticle-tone on the overlay root - waiting/ask/warn distinguishes how the agent handed back. */
11
15
  const DATA_RETICLE_TONE = 'data-reticle-tone';
12
16
  const DATA_ON = 'data-on';
13
17
  const GLOW_OFF = '0';
@@ -22,6 +26,7 @@ const INPUT_PLACEHOLDER = 'Tell the agent something…';
22
26
  /** Accessible name for the composer (a placeholder is not an accessible name). */
23
27
  const INPUT_ARIA_LABEL = 'Message to the agent';
24
28
  const PAUSED_BADGE_TEXT = 'PAUSED';
29
+ export const PAUSED_BADGE_LABEL = PAUSED_BADGE_TEXT;
25
30
  const ENDED_BANNER_TEXT = 'Session ended';
26
31
  const COPY_LABEL = 'Copy run';
27
32
  const EXPORT_LABEL = 'Export';
@@ -32,98 +37,154 @@ const RUN_FILENAME = 'reticle-run.json';
32
37
  /** Border fade-out delay after a session ends (native timer; presenter-only tunable). */
33
38
  export const ENDED_FADE_MS = 4000;
34
39
  /** Max composer height (px) before it scrolls. One source for both the CSS cap and the JS auto-grow
35
- * clamp — they measure the same border-box, so the scrollbar appears exactly when growth stops. */
40
+ * clamp - they measure the same border-box, so the scrollbar appears exactly when growth stops. */
36
41
  const MSG_MAX_H = 96;
37
42
  /** CSS for the control surface (injected with the rest of the presenter stylesheet). */
38
43
  export const CONTROLS_CSS = `
39
- [data-reticle-hud] .reticle-ctl{pointer-events:auto;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;
40
- height:26px;padding:0 11px;border-radius:8px;border:1px solid var(--reticle-line);background:rgba(255,255,255,.04);
41
- color:var(--reticle-muted);font-family:var(--reticle-font);font-size:11px;font-weight:500;letter-spacing:.01em;line-height:1;
42
- transition:background .15s,color .15s,border-color .15s,transform .1s;}
43
- [data-reticle-hud] .reticle-ctl:hover{color:var(--reticle-fg);background:rgba(255,255,255,.09);}
44
- [data-reticle-hud] .reticle-ctl:active{transform:scale(.95);}
45
- [data-reticle-hud] .reticle-ctl:disabled{opacity:.35;cursor:default;}
46
- [data-reticle-hud] [data-reticle-end]{color:#ff9aa2;border-color:rgba(255,107,107,.22);}
47
- [data-reticle-hud] [data-reticle-end]:hover{color:#ff7a7a;border-color:rgba(255,107,107,.5);background:rgba(255,107,107,.1);}
48
- [data-reticle-hud] .reticle-badge{display:none;align-items:center;flex:none;font-weight:600;letter-spacing:.1em;font-size:9px;
49
- color:var(--reticle-accent);border:1px solid var(--reticle-accent);background:var(--reticle-accent-soft);padding:2px 8px;border-radius:999px;}
50
- [data-reticle-overlay][data-reticle-state="paused"] [data-reticle-badge]{display:inline-flex;}
51
- [data-reticle-hud] [data-reticle-foot]{flex:none;padding:10px 12px 12px;border-top:1px solid var(--reticle-line2);background:rgba(0,0,0,.16);}
52
- [data-reticle-hud] .reticle-composer{display:flex;align-items:flex-end;gap:6px;background:rgba(255,255,255,.05);
53
- border:1px solid var(--reticle-line);border-radius:14px;padding:5px 6px 5px 14px;transition:border-color .15s,box-shadow .15s;}
54
- [data-reticle-hud] .reticle-composer:focus-within{border-color:var(--reticle-accent);box-shadow:0 0 0 3px var(--reticle-accent-soft);}
55
- [data-reticle-hud] .reticle-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;resize:none;
56
- box-sizing:border-box;color:var(--reticle-fg);font-family:var(--reticle-font);font-size:13px;line-height:18px;
44
+ [data-reticle-chat-panel] [data-reticle-foot]{flex:none;padding:8px 10px 10px;border-top:1px solid rgba(255,255,255,.07);
45
+ background:linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,.55) 100%);pointer-events:auto;}
46
+ [data-reticle-chat-panel] .reticle-hud-log-well{margin:0 0 4px;}
47
+ [data-reticle-chat-panel] .reticle-composer-stack{display:flex;flex-direction:column;gap:6px;}
48
+ [data-reticle-chat-panel] .reticle-workspace-wrap{position:relative;align-self:flex-start;max-width:100%;}
49
+ [data-reticle-chat-panel] .reticle-workspace{
50
+ display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:3px 8px 3px 6px;border-radius:999px;cursor:pointer;
51
+ border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:rgba(255,255,255,.78);
52
+ font:inherit;font-size:11px;font-weight:500;line-height:1.2;
53
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05);transition:background .15s,border-color .15s,color .15s;}
54
+ [data-reticle-chat-panel] .reticle-workspace:hover{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.14);color:#fff;}
55
+ [data-reticle-chat-panel] .reticle-workspace[aria-expanded="true"]{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.16);color:#fff;}
56
+ [data-reticle-chat-panel] .reticle-workspace-icon,
57
+ [data-reticle-chat-panel] .reticle-workspace-caret{display:inline-flex;align-items:center;opacity:.72;flex:none;}
58
+ [data-reticle-chat-panel] .reticle-workspace-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:min(200px,calc(100vw - 120px));}
59
+ [data-reticle-chat-panel] .reticle-workspace[aria-expanded="true"] .reticle-workspace-caret{transform:rotate(180deg);}
60
+ [data-reticle-chat-panel] .reticle-workspace-caret{transition:transform .18s ease;}
61
+ [data-reticle-chat-panel] .reticle-workspace-menu{
62
+ position:absolute;left:0;bottom:calc(100% + 6px);z-index:8;min-width:min(268px,calc(100vw - 48px));max-width:min(300px,calc(100vw - 48px));
63
+ padding:10px 12px;border-radius:12px;background:#000;color:rgba(255,255,255,.86);font-size:11px;line-height:1.35;
64
+ box-shadow:0 12px 32px rgba(0,0,0,.58),inset 0 1px 0 rgba(255,255,255,.07),0 0 0 1px rgba(255,255,255,.1);
65
+ transform:translateY(4px) scale(.98);opacity:0;pointer-events:none;visibility:hidden;
66
+ transition:opacity .16s ease,transform .2s cubic-bezier(.19,1,.22,1),visibility .16s;}
67
+ [data-reticle-chat-panel] .reticle-workspace[aria-expanded="true"] + .reticle-workspace-menu,
68
+ [data-reticle-chat-panel] .reticle-workspace-menu[aria-hidden="false"]{
69
+ transform:translateY(0) scale(1);opacity:1;pointer-events:auto;visibility:visible;}
70
+ [data-reticle-chat-panel] .reticle-workspace-menu-head{
71
+ display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
72
+ [data-reticle-chat-panel] .reticle-workspace-menu-title{
73
+ color:rgba(255,255,255,.42);font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;}
74
+ [data-reticle-chat-panel] .reticle-workspace-copy{
75
+ display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:6px;
76
+ background:rgba(255,255,255,.06);color:rgba(255,255,255,.7);cursor:pointer;line-height:0;transition:background .12s,color .12s;}
77
+ [data-reticle-chat-panel] .reticle-workspace-copy:hover:not(:disabled){background:rgba(255,255,255,.12);color:#fff;}
78
+ [data-reticle-chat-panel] .reticle-workspace-copy:disabled{opacity:.35;cursor:not-allowed;}
79
+ [data-reticle-chat-panel] .reticle-workspace-menu-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:4px 0;}
80
+ [data-reticle-chat-panel] .reticle-workspace-menu-k{flex:none;color:rgba(255,255,255,.42);}
81
+ [data-reticle-chat-panel] .reticle-workspace-menu-v{min-width:0;text-align:right;color:rgba(255,255,255,.9);font-weight:500;word-break:break-all;}
82
+ [data-reticle-chat-panel] .reticle-composer{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.03);
83
+ border:1px solid rgba(255,255,255,.1);border-radius:999px;padding:4px 4px 4px 14px;
84
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05);transition:border-color .2s,box-shadow .2s,background .2s;}
85
+ [data-reticle-chat-panel] .reticle-composer:focus-within{
86
+ border-color:color-mix(in srgb,var(--reticle-accent) 50%,transparent);background:rgba(255,255,255,.06);
87
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.06),
88
+ 0 0 0 1px color-mix(in srgb,var(--reticle-accent) 50%,transparent);}
89
+ [data-reticle-chat-panel] .reticle-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;resize:none;
90
+ box-sizing:border-box;color:var(--reticle-fg);font-family:var(--reticle-font);font-size:12.5px;line-height:18px;
57
91
  height:28px;min-height:28px;max-height:${MSG_MAX_H}px;padding:5px 0;overflow-y:auto;
58
92
  scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.14) transparent;}
59
- [data-reticle-hud] .reticle-msg::-webkit-scrollbar{width:9px;}
60
- [data-reticle-hud] .reticle-msg::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
61
- [data-reticle-hud] .reticle-msg::placeholder{color:var(--reticle-faint);}
62
- [data-reticle-hud] .reticle-msg:disabled{opacity:.5;}
63
- [data-reticle-hud] .reticle-send{flex:none;width:30px;height:30px;padding:0;border-radius:10px;border:none;cursor:pointer;pointer-events:auto;
64
- background:var(--reticle-accent);color:#0b0d14;display:inline-flex;align-items:center;justify-content:center;transition:filter .15s,transform .1s;}
65
- [data-reticle-hud] .reticle-send svg{display:block;}
66
- [data-reticle-hud] .reticle-send:hover{filter:brightness(1.12);}
67
- [data-reticle-hud] .reticle-send:active{transform:scale(.9);}
68
- [data-reticle-hud] .reticle-send:disabled{opacity:.4;cursor:default;}
69
- [data-reticle-hud] .reticle-banner{display:none;flex:none;padding:8px 15px;color:var(--reticle-accent);
70
- font-size:11.5px;font-weight:500;border-bottom:1px solid var(--reticle-line2);background:var(--reticle-accent-soft);}
71
- [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-banner]{display:block;}
72
- /* Export row: hidden during a live session; revealed when ended so the run can be copied/saved. */
73
- [data-reticle-hud] .reticle-export{display:none;align-items:center;gap:8px;margin-top:9px;}
74
- [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-hud] .reticle-export{display:flex;}
75
- [data-reticle-hud] .reticle-export-msg{color:var(--reticle-ok);font-size:11px;opacity:0;transition:opacity .15s;}
76
- [data-reticle-hud] .reticle-export-msg[data-show="1"]{opacity:1;}
77
- [data-reticle-overlay][data-reticle-state="paused"] [data-reticle-glow][data-on="1"]{animation:none;
78
- box-shadow:inset 0 0 0 3px rgba(246,180,76,.9),inset 0 0 30px 6px rgba(246,180,76,.4);}
79
- [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-glow][data-on="1"]{animation:none;
80
- box-shadow:inset 0 0 0 2px rgba(61,215,166,.55);}
81
- /* Handoff tones tell the human the agent's mode at a glance. waiting = calm teal "your turn" (no
82
- alarm); ask = amber "answer me" with a pulse; warn = amber "agent crashed" with a pulse. Each leads
83
- the banner with an icon and overrides the calm ended-green accent. */
84
- [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-hud]{--reticle-accent:#38bdf8;--reticle-accent-soft:rgba(56,189,248,.16);}
85
- [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-banner]{font-weight:600;color:#7dd3fc;}
86
- [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-banner]::before{content:"\\270B ";}
87
- [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-glow][data-on="1"]{animation:none;
88
- box-shadow:inset 0 0 0 2px rgba(56,189,248,.5);}
89
- [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-hud],
90
- [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-hud]{--reticle-accent:#fb923c;--reticle-accent-soft:rgba(251,146,60,.18);}
93
+ [data-reticle-chat-panel] .reticle-msg::-webkit-scrollbar{width:9px;}
94
+ [data-reticle-chat-panel] .reticle-msg::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
95
+ [data-reticle-chat-panel] .reticle-msg::placeholder{color:var(--reticle-faint);}
96
+ [data-reticle-chat-panel] .reticle-msg:disabled{opacity:.5;}
97
+ [data-reticle-chat-panel] .reticle-send{flex:none;width:28px;height:28px;padding:0;border-radius:50%;border:none;cursor:pointer;pointer-events:auto;
98
+ background:var(--reticle-accent);color:#ffffff;display:inline-flex;align-items:center;justify-content:center;
99
+ transition:background .15s,transform .1s,box-shadow .15s;
100
+ box-shadow:0 1px 3px color-mix(in srgb,var(--reticle-accent) 30%,transparent);}
101
+ [data-reticle-chat-panel] .reticle-send:hover{
102
+ background:color-mix(in srgb,var(--reticle-accent) 85%,#000);
103
+ box-shadow:0 2px 6px color-mix(in srgb,var(--reticle-accent) 40%,transparent);}
104
+ [data-reticle-chat-panel] .reticle-send:active{transform:scale(.92);}
105
+ [data-reticle-chat-panel] .reticle-send:disabled{background:#374151;color:#9ca3af;box-shadow:none;opacity:.5;cursor:default;}
106
+ [data-reticle-chat-panel] .reticle-banner{display:none;flex:none;align-items:center;gap:8px;padding:10px 14px;color:var(--reticle-fg);
107
+ font-size:12px;font-weight:500;border-bottom:1px solid var(--reticle-line2);background:var(--reticle-surface);}
108
+ [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-chat-panel] .reticle-banner{display:block;}
109
+ [data-reticle-overlay][data-reticle-state="paused"] [data-reticle-glow][data-on="1"]{
110
+ box-shadow:inset 0 0 0 2px rgba(255,255,255,.2);}
111
+ [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-glow][data-on="1"]{
112
+ box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);}
113
+ [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-chat-panel]{
114
+ --reticle-accent:var(--reticle-state);
115
+ --reticle-accent-soft:color-mix(in srgb,var(--reticle-state) 18%,transparent);}
116
+ [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-banner]{font-weight:500;color:var(--reticle-fg);}
117
+ [data-reticle-overlay][data-reticle-tone="waiting"] [data-reticle-glow][data-on="1"]{
118
+ box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);}
119
+ [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-chat-panel],
120
+ [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-chat-panel]{
121
+ --reticle-accent:var(--reticle-state);
122
+ --reticle-accent-soft:color-mix(in srgb,var(--reticle-state) 18%,transparent);}
91
123
  [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-banner],
92
- [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-banner]{font-weight:600;color:#fdba74;}
93
- [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-banner]::before{content:"\\2753 ";}
94
- [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-banner]::before{content:"\\26A0\\FE0F ";}
124
+ [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-banner]{font-weight:500;color:var(--reticle-fg);}
95
125
  [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-glow][data-on="1"],
96
- [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-glow][data-on="1"]{animation:reticle-warn-pulse 1.5s ease-in-out infinite;
97
- box-shadow:inset 0 0 0 2px rgba(251,146,60,.7);}
98
- @keyframes reticle-warn-pulse{0%,100%{box-shadow:inset 0 0 0 2px rgba(251,146,60,.32);}
99
- 50%{box-shadow:inset 0 0 0 3px rgba(251,146,60,.85),inset 0 0 26px 5px rgba(251,146,60,.34);}}
100
- /* Replay-a-flow row: the human re-runs a saved flow with no agent. Hidden until flows are pushed.
101
- Bounded + self-scrolling: it sits between the flex:1 log and the flex:none composer, so without a
102
- height cap a long flow list would squeeze the log to nothing and push the message input past the
103
- panel's overflow:hidden clip. flex:none + max-height + overflow-y keep the log and input always
104
- visible; extra flow chips scroll inside this section instead of growing the panel. */
105
- [data-reticle-hud] .reticle-flows{display:none;flex:none;flex-wrap:wrap;align-content:flex-start;gap:6px;
106
- padding:9px 12px;border-top:1px solid var(--reticle-line2);max-height:88px;overflow-y:auto;overscroll-behavior:contain;}
107
- [data-reticle-hud] .reticle-flows[data-has="1"]{display:flex;}
108
- [data-reticle-hud] .reticle-flows::-webkit-scrollbar{width:9px;}
109
- [data-reticle-hud] .reticle-flows::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
110
- [data-reticle-hud] .reticle-flows-cap{flex:0 0 100%;margin-bottom:1px;color:var(--reticle-faint);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;}
111
- [data-reticle-hud] .reticle-flow{pointer-events:auto;cursor:pointer;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 10px;
126
+ [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-glow][data-on="1"]{
127
+ box-shadow:inset 0 0 0 2px rgba(255,255,255,.22);}
128
+ [data-reticle-chat-panel] .reticle-flows{display:none;flex:none;flex-wrap:wrap;align-content:flex-start;gap:6px;
129
+ padding:9px 12px;border-top:1px solid var(--reticle-line2);max-height:88px;overflow-y:auto;overscroll-behavior:contain;
130
+ pointer-events:auto;touch-action:pan-y;}
131
+ [data-reticle-chat-panel] .reticle-flows[data-has="1"]{display:flex;}
132
+ [data-reticle-chat-panel] .reticle-flows::-webkit-scrollbar{width:9px;}
133
+ [data-reticle-chat-panel] .reticle-flows::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
134
+ [data-reticle-chat-panel] .reticle-flows-cap{flex:0 0 100%;margin-bottom:1px;color:var(--reticle-faint);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;}
135
+ [data-reticle-chat-panel] .reticle-flow{pointer-events:auto;cursor:pointer;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 10px;
112
136
  border-radius:7px;border:1px solid var(--reticle-line);background:rgba(255,255,255,.04);color:var(--reticle-muted);
113
137
  font-family:var(--reticle-font);font-size:11px;font-weight:500;transition:background .15s,color .15s,border-color .15s,transform .1s;}
114
- [data-reticle-hud] .reticle-flow:hover{color:var(--reticle-fg);background:var(--reticle-accent-soft);border-color:var(--reticle-accent);}
115
- [data-reticle-hud] .reticle-flow:active{transform:scale(.95);}
138
+ [data-reticle-chat-panel] .reticle-flow:hover{color:var(--reticle-fg);background:var(--reticle-accent-soft);border-color:var(--reticle-accent);}
139
+ [data-reticle-chat-panel] .reticle-flow:active{transform:scale(.95);}
140
+ [data-reticle-overlay][data-reticle-state="paused"] [data-reticle-hud] .reticle-tb-btn--primary{
141
+ box-shadow:inset 0 0 0 1px rgba(255,255,255,.16),0 0 12px rgba(255,255,255,.04);}
142
+ [data-reticle-hud] .reticle-export-msg{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
143
+ overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;}
116
144
  `;
117
- /** Header markup (controls + badge) injected into .reticle-hud-head, after the expand button. */
118
- export const CONTROLS_HEAD_HTML = `<button type="button" data-reticle-pause class="reticle-ctl">${CONTROL_LABEL.PAUSE}</button><button type="button" data-reticle-end class="reticle-ctl">${CONTROL_LABEL.END}</button><span data-reticle-badge class="reticle-badge">${PAUSED_BADGE_TEXT}</span>`;
145
+ /** Swap pause/resume icon + accessible label without visible text clutter in the toolbar. */
146
+ function paintPauseBtn(btn, paused) {
147
+ const iconName = paused ? PresenterIcon.PLAY : PresenterIcon.PAUSE;
148
+ const labelText = paused ? CONTROL_LABEL.RESUME : CONTROL_LABEL.PAUSE;
149
+ btn.setAttribute('aria-label', labelText);
150
+ btn.setAttribute('title', labelText);
151
+ btn.replaceChildren(hiIcon(iconName, PRESENTER_ICON_SIZE.TOOLBAR));
152
+ btn.classList.toggle('reticle-tb-btn--primary', paused);
153
+ btn.setAttribute('data-active', paused ? '1' : '0');
154
+ }
155
+ const CHAT_LABEL = 'Agent chat';
156
+ const MARKERS_LABEL = 'Hide markers';
157
+ const CLEAR_MARKS_LABEL = 'Clear all';
158
+ const TB = PRESENTER_ICON_SIZE.TOOLBAR;
159
+ function tbWrap(btn, tip, kbd) {
160
+ const shortcut = kbd === undefined ? '' : `<span class="reticle-tb-kbd">${kbd}</span>`;
161
+ return `<div class="reticle-tb-wrap">${btn}<span class="reticle-tb-tip">${tip}${shortcut}</span></div>`;
162
+ }
163
+ function pauseToolbarHtml() {
164
+ const btn = `<button type="button" data-reticle-pause class="reticle-tb-btn" title="${CONTROL_LABEL.PAUSE}" aria-label="${CONTROL_LABEL.PAUSE}">${hiIconHtml(PresenterIcon.PAUSE, TB)}</button>`;
165
+ const badge = `<span data-reticle-badge class="reticle-pause-badge">${PAUSED_BADGE_TEXT}</span>`;
166
+ const tip = `<span class="reticle-tb-tip">${CONTROL_LABEL.PAUSE}</span>`;
167
+ return `<div class="reticle-tb-wrap reticle-tb-wrap--pause">${btn}${badge}${tip}</div>`;
168
+ }
169
+ /** Icon toolbar: pause, chat, markers, end, clear - copy/export appear after a run ends. */
170
+ export const CONTROLS_TOOLBAR_HTML = [
171
+ pauseToolbarHtml(),
172
+ tbWrap(`<button type="button" ${CHAT_TOGGLE_ATTR} class="reticle-tb-btn reticle-tb-btn--toggle" title="${CHAT_LABEL}" aria-label="${CHAT_LABEL}" aria-pressed="false" data-active="0">${hiToggleIconHtml(PresenterIcon.MESSAGE, TB)}</button>`, CHAT_LABEL),
173
+ tbWrap(`<button type="button" ${MARKERS_BTN_ATTR} class="reticle-tb-btn reticle-tb-btn--toggle" title="${MARKERS_LABEL}" aria-label="${MARKERS_LABEL}" aria-pressed="false" data-active="0" disabled>${hiToggleIconHtml(PresenterIcon.VIEW, TB)}</button>`, MARKERS_LABEL, 'H'),
174
+ tbWrap(`<button type="button" data-reticle-end class="reticle-tb-btn" title="${CONTROL_LABEL.END}" aria-label="${CONTROL_LABEL.END}">${hiIconHtml(PresenterIcon.STOP, TB)}</button>`, CONTROL_LABEL.END),
175
+ tbWrap(`<button type="button" ${CLEAR_MARKS_ATTR} class="reticle-tb-btn" title="${CLEAR_MARKS_LABEL}" aria-label="${CLEAR_MARKS_LABEL}" data-danger disabled>${hiIconHtml(PresenterIcon.TRASH, TB)}</button>`, CLEAR_MARKS_LABEL, 'X'),
176
+ `<button type="button" data-reticle-copy class="reticle-tb-btn reticle-tb-btn--export" title="${COPY_LABEL}" aria-label="${COPY_LABEL}" hidden>${hiIconHtml(PresenterIcon.COPY, TB)}</button>`,
177
+ `<button type="button" data-reticle-export class="reticle-tb-btn reticle-tb-btn--export" title="${EXPORT_LABEL}" aria-label="${EXPORT_LABEL}" hidden>${hiIconHtml(PresenterIcon.DOWNLOAD, TB)}</button>`,
178
+ '<span data-reticle-export-msg class="reticle-export-msg" aria-live="polite"></span>',
179
+ ].join('');
180
+ /** @deprecated Legacy head markup - replaced by CONTROLS_TOOLBAR_HTML in the morphing shell. */
181
+ export const CONTROLS_HEAD_HTML = CONTROLS_TOOLBAR_HTML;
119
182
  /** Banner markup (between head and log, hidden unless ended). */
120
183
  export const CONTROLS_BANNER_HTML = `<div data-reticle-banner class="reticle-banner">${ENDED_BANNER_TEXT}</div>`;
121
184
  /** Replay-a-flow row (between log and footer); buttons are filled in by setFlows once flows arrive. */
122
185
  export const CONTROLS_FLOWS_HTML = `<div data-reticle-flows class="reticle-flows"><span class="reticle-flows-cap">${FLOWS_LABEL}</span></div>`;
123
- /** Aesthetic send glyph (Feather "send" paper-plane). Inline SVG so it's crisp at any DPI. */
124
- const SEND_ICON = `<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>`;
125
- /** Footer markup: a rounded composer pill (input + icon Send), appended after the log div. */
126
- export const CONTROLS_FOOT_HTML = `<div data-reticle-foot><div class="reticle-composer"><textarea data-reticle-input class="reticle-msg" rows="1" aria-label="${INPUT_ARIA_LABEL}" placeholder="${INPUT_PLACEHOLDER}"></textarea><button type="button" data-reticle-send class="reticle-send" aria-label="${CONTROL_LABEL.SEND}">${SEND_ICON}</button></div><div class="reticle-export"><button type="button" data-reticle-copy class="reticle-ctl">${COPY_LABEL}</button><button type="button" data-reticle-export class="reticle-ctl">${EXPORT_LABEL}</button><span data-reticle-export-msg class="reticle-export-msg"></span></div></div>`;
186
+ /** Footer markup: composer only (export icons live in the toolbar). */
187
+ export const CONTROLS_FOOT_HTML = `<div data-reticle-foot><div class="reticle-composer-stack">${workspaceRowHtml()}<div class="reticle-composer"><textarea data-reticle-input class="reticle-msg" rows="1" aria-label="${INPUT_ARIA_LABEL}" placeholder="${INPUT_PLACEHOLDER}"></textarea><button type="button" data-reticle-send class="reticle-send" aria-label="${CONTROL_LABEL.SEND}">${hiIconHtml(PresenterIcon.SEND, PRESENTER_ICON_SIZE.SEND)}</button></div></div></div>`;
127
188
  function queryControlRefs(root) {
128
189
  return {
129
190
  pauseBtn: root.querySelector('[data-reticle-pause]') ?? undefined,
@@ -161,6 +222,7 @@ export class ControlPanel {
161
222
  #glow;
162
223
  /** The full replayable-flow list from the last push; re-filtered per page on route change. */
163
224
  #flowItems = [];
225
+ #workspaceTeardown;
164
226
  #host;
165
227
  constructor(host) {
166
228
  this.#host = host;
@@ -173,6 +235,10 @@ export class ControlPanel {
173
235
  this.#root = root;
174
236
  this.#glow = glow;
175
237
  this.#refs = queryControlRefs(root);
238
+ const pauseBtn = this.#refs.pauseBtn;
239
+ if (pauseBtn !== undefined) {
240
+ paintPauseBtn(pauseBtn, this.#state === SessionState.PAUSED);
241
+ }
176
242
  this.#refs.pauseBtn?.addEventListener('click', () => this.#onPauseToggle());
177
243
  this.#refs.endBtn?.addEventListener('click', () => this.#onEnd());
178
244
  this.#refs.sendBtn?.addEventListener('click', () => this.#onSend());
@@ -196,6 +262,7 @@ export class ControlPanel {
196
262
  });
197
263
  this.#refs.copyBtn?.addEventListener('click', () => this.#onCopy());
198
264
  this.#refs.exportBtn?.addEventListener('click', () => this.#onExport());
265
+ this.#workspaceTeardown = mountWorkspaceSelector(root);
199
266
  this.setState(SessionState.ACTIVE);
200
267
  }
201
268
  /** Serialize the run state to pretty JSON for Copy/Export. */
@@ -205,6 +272,9 @@ export class ControlPanel {
205
272
  /** Copy the run state to the clipboard (with a brief "Copied ✓" flash). */
206
273
  #onCopy() {
207
274
  void navigator.clipboard?.writeText(this.#runJson());
275
+ if (getPresenterSettings().clearOnCopy) {
276
+ this.#host.clearRunLog?.();
277
+ }
208
278
  const msg = this.#refs.exportMsg;
209
279
  if (msg !== undefined) {
210
280
  msg.textContent = COPIED_TEXT;
@@ -221,9 +291,14 @@ export class ControlPanel {
221
291
  a.download = RUN_FILENAME;
222
292
  a.click();
223
293
  URL.revokeObjectURL(url);
294
+ if (getPresenterSettings().clearOnCopy) {
295
+ this.#host.clearRunLog?.();
296
+ }
224
297
  }
225
298
  /** Clear any pending ended-fade timer (called from Presenter.destroy). */
226
299
  teardown() {
300
+ this.#workspaceTeardown?.();
301
+ this.#workspaceTeardown = undefined;
227
302
  if (this.#fadeTimer !== undefined)
228
303
  nativeClearTimeout(this.#fadeTimer);
229
304
  this.#fadeTimer = undefined;
@@ -252,11 +327,14 @@ export class ControlPanel {
252
327
  return;
253
328
  this.#host.emit(HumanControlKind.MESSAGE, text);
254
329
  this.#host.logHuman(text);
330
+ if (getPresenterSettings().clearOnCopy) {
331
+ this.#host.clearRunLog?.();
332
+ }
255
333
  if (this.#refs.input !== undefined)
256
334
  this.#refs.input.value = '';
257
335
  this.#autosize();
258
336
  }
259
- /** Grow the composer to fit its content (up to the CSS max-height), then shrink back — soothing,
337
+ /** Grow the composer to fit its content (up to the CSS max-height), then shrink back - soothing,
260
338
  * no scrollbar until it's genuinely long. Driven on input and after a send clears the field. */
261
339
  #autosize() {
262
340
  const el = this.#refs.input;
@@ -289,7 +367,7 @@ export class ControlPanel {
289
367
  /**
290
368
  * Re-render the replay chips for the CURRENT page. A flow "starts here" iff its first step's anchor
291
369
  * (a testid `start` hint) is present in the live DOM; flows with no start hint (signal/role-first,
292
- * un-checkable) always show. Called on connect and on every route change so the list tracks the page —
370
+ * un-checkable) always show. Called on connect and on every route change so the list tracks the page -
293
371
  * so you never see (or click) a flow that can't replay from where you are. Existing flows benefit
294
372
  * without re-recording, since the hint is derived from the first step, not stored on the flow.
295
373
  */
@@ -315,7 +393,7 @@ export class ControlPanel {
315
393
  el.setAttribute('data-has', visible.length > 0 ? '1' : '0');
316
394
  }
317
395
  /**
318
- * Drive the panel's visual state. Idempotent; NEVER emits a control — the shared path for both the
396
+ * Drive the panel's visual state. Idempotent; NEVER emits a control - the shared path for both the
319
397
  * optimistic local click and the authoritative server PRESENTER echo. Only the ended-border fade
320
398
  * touches a clock, via the injected native timer.
321
399
  */
@@ -335,12 +413,23 @@ export class ControlPanel {
335
413
  const refs = this.#refs;
336
414
  const ended = state === SessionState.ENDED;
337
415
  if (refs.pauseBtn !== undefined) {
338
- refs.pauseBtn.textContent =
339
- state === SessionState.PAUSED ? CONTROL_LABEL.RESUME : CONTROL_LABEL.PAUSE;
416
+ paintPauseBtn(refs.pauseBtn, state === SessionState.PAUSED);
340
417
  refs.pauseBtn.disabled = ended;
341
418
  }
342
419
  if (refs.endBtn !== undefined)
343
420
  refs.endBtn.disabled = ended;
421
+ if (refs.copyBtn !== undefined) {
422
+ if (ended)
423
+ refs.copyBtn.removeAttribute('hidden');
424
+ else
425
+ refs.copyBtn.setAttribute('hidden', '');
426
+ }
427
+ if (refs.exportBtn !== undefined) {
428
+ if (ended)
429
+ refs.exportBtn.removeAttribute('hidden');
430
+ else
431
+ refs.exportBtn.setAttribute('hidden', '');
432
+ }
344
433
  if (refs.sendBtn !== undefined)
345
434
  refs.sendBtn.disabled = ended;
346
435
  if (refs.input !== undefined)
@@ -362,5 +451,6 @@ export class ControlPanel {
362
451
  glow?.setAttribute(DATA_ON, GLOW_OFF);
363
452
  }, this.#host.endedFadeMs);
364
453
  }
454
+ this.#host.onStateChange?.(state);
365
455
  }
366
456
  }
@@ -0,0 +1,6 @@
1
+ /** Flip floating panels and reclamp a dragged dock so everything stays on-screen. */
2
+ export declare function syncDockLayout(dock: HTMLElement, overlay: HTMLElement): void;
3
+ /** Batch dock layout work on the next frame (resize, panel open, drag end). */
4
+ export declare function scheduleSyncDockLayout(dock: HTMLElement, overlay: HTMLElement): void;
5
+ /** Resolve the dock node from an overlay root. */
6
+ export declare function findDock(overlay: HTMLElement): HTMLElement | undefined;
@@ -0,0 +1,170 @@
1
+ import { CHAT_ATTR, CHAT_PANEL_ATTR, CHAT_PLACEMENT_ATTR, DOCK_ALIGN_ATTR, DOCK_ATTR, HUD_DOCK_MARGIN_PX, Placement, SETTINGS_ATTR, SETTINGS_PANEL_ATTR, SETTINGS_PLACEMENT_ATTR, } from './presenter-config.js';
2
+ import { applyHudPosition, isHudDragged, readHudPosition, relayoutHudPosition, } from './presenter-drag.js';
3
+ const HUD_ATTR = 'data-reticle-hud';
4
+ const DOCK_PANEL_GAP_PX = 8;
5
+ const DOCK_SETTINGS_GAP_PX = 12;
6
+ const HUD_BAR_HEIGHT_PX = 44;
7
+ const CHAT_MIN_HEIGHT_PX = 160;
8
+ const CHAT_DEFAULT_MAX_HEIGHT_PX = 420;
9
+ const SETTINGS_DEFAULT_MAX_HEIGHT_PX = 520;
10
+ const SETTINGS_MIN_HEIGHT_PX = 200;
11
+ function unionRects(rects) {
12
+ if (0 === rects.length)
13
+ return undefined;
14
+ let left = rects[0]?.left ?? 0;
15
+ let top = rects[0]?.top ?? 0;
16
+ let right = rects[0]?.right ?? 0;
17
+ let bottom = rects[0]?.bottom ?? 0;
18
+ for (const rect of rects.slice(1)) {
19
+ left = Math.min(left, rect.left);
20
+ top = Math.min(top, rect.top);
21
+ right = Math.max(right, rect.right);
22
+ bottom = Math.max(bottom, rect.bottom);
23
+ }
24
+ return { left, top, right, bottom };
25
+ }
26
+ function pickVerticalPlacement(spaceAbove, spaceBelow, needPx) {
27
+ if (spaceAbove >= needPx)
28
+ return Placement.ABOVE;
29
+ if (spaceBelow >= needPx)
30
+ return Placement.BELOW;
31
+ return spaceBelow > spaceAbove ? Placement.BELOW : Placement.ABOVE;
32
+ }
33
+ function availableHeight(placement, spaceAbove, spaceBelow, gapPx) {
34
+ const raw = Placement.ABOVE === placement ? spaceAbove - gapPx : spaceBelow - gapPx;
35
+ return Math.max(0, raw);
36
+ }
37
+ function setMaxHeight(el, cssVar, px) {
38
+ const pxText = `${String(px)}px`;
39
+ el.style.setProperty(cssVar, pxText);
40
+ el.style.maxHeight = pxText;
41
+ }
42
+ function clearMaxHeight(el, cssVar) {
43
+ el.style.removeProperty(cssVar);
44
+ el.style.removeProperty('max-height');
45
+ }
46
+ function visiblePanel(el) {
47
+ if (null === el)
48
+ return false;
49
+ const style = window.getComputedStyle(el);
50
+ return 'none' !== style.display && 'hidden' !== style.visibility;
51
+ }
52
+ /** Resolve the HUD bar inside a dock (or the node itself in minimal fixtures). */
53
+ function resolveHudBar(dock) {
54
+ const nested = dock.querySelector(`[${HUD_ATTR}]`);
55
+ if (nested instanceof HTMLElement)
56
+ return nested;
57
+ if (dock.hasAttribute(HUD_ATTR))
58
+ return dock;
59
+ if (isHudDragged(dock))
60
+ return dock;
61
+ return undefined;
62
+ }
63
+ /** Flip floating panels and reclamp a dragged dock so everything stays on-screen. */
64
+ export function syncDockLayout(dock, overlay) {
65
+ const hud = resolveHudBar(dock);
66
+ if (hud === undefined)
67
+ return;
68
+ const chatOpen = '1' === overlay.getAttribute(CHAT_ATTR);
69
+ const settingsOpen = '1' === overlay.getAttribute(SETTINGS_ATTR);
70
+ const chatPanel = dock.querySelector(`[${CHAT_PANEL_ATTR}]`);
71
+ const settingsPanel = dock.querySelector(`[${SETTINGS_PANEL_ATTR}]`);
72
+ const margin = HUD_DOCK_MARGIN_PX;
73
+ const viewportW = window.innerWidth;
74
+ const viewportH = window.innerHeight;
75
+ const hudRect = hud.getBoundingClientRect();
76
+ const spaceAbove = hudRect.top - margin;
77
+ const spaceBelow = viewportH - hudRect.bottom - margin;
78
+ if (chatOpen && chatPanel instanceof HTMLElement && visiblePanel(chatPanel)) {
79
+ const chatHeight = chatPanel.getBoundingClientRect().height || CHAT_DEFAULT_MAX_HEIGHT_PX;
80
+ const placement = pickVerticalPlacement(spaceAbove, spaceBelow, chatHeight + DOCK_PANEL_GAP_PX);
81
+ dock.setAttribute(CHAT_PLACEMENT_ATTR, placement);
82
+ const maxH = Math.min(CHAT_DEFAULT_MAX_HEIGHT_PX, Math.max(CHAT_MIN_HEIGHT_PX, availableHeight(placement, spaceAbove, spaceBelow, DOCK_PANEL_GAP_PX)));
83
+ setMaxHeight(chatPanel, '--reticle-chat-max-h', maxH);
84
+ }
85
+ else {
86
+ dock.removeAttribute(CHAT_PLACEMENT_ATTR);
87
+ if (chatPanel instanceof HTMLElement)
88
+ clearMaxHeight(chatPanel, '--reticle-chat-max-h');
89
+ }
90
+ if (settingsOpen && settingsPanel instanceof HTMLElement && visiblePanel(settingsPanel)) {
91
+ const settingsHeight = settingsPanel.getBoundingClientRect().height || SETTINGS_DEFAULT_MAX_HEIGHT_PX;
92
+ const placement = pickVerticalPlacement(spaceAbove, spaceBelow, settingsHeight + HUD_BAR_HEIGHT_PX + DOCK_SETTINGS_GAP_PX);
93
+ dock.setAttribute(SETTINGS_PLACEMENT_ATTR, placement);
94
+ const maxH = Math.min(SETTINGS_DEFAULT_MAX_HEIGHT_PX, Math.max(SETTINGS_MIN_HEIGHT_PX, availableHeight(placement, spaceAbove, spaceBelow, DOCK_SETTINGS_GAP_PX)));
95
+ setMaxHeight(settingsPanel, '--reticle-settings-max-h', maxH);
96
+ }
97
+ else {
98
+ dock.removeAttribute(SETTINGS_PLACEMENT_ATTR);
99
+ if (settingsPanel instanceof HTMLElement) {
100
+ clearMaxHeight(settingsPanel, '--reticle-settings-max-h');
101
+ }
102
+ }
103
+ const panelWidth = Math.max(chatOpen && chatPanel instanceof HTMLElement ? chatPanel.getBoundingClientRect().width : 0, settingsOpen && settingsPanel instanceof HTMLElement
104
+ ? settingsPanel.getBoundingClientRect().width
105
+ : 0);
106
+ if (chatOpen || settingsOpen) {
107
+ const panelLeft = hudRect.right - panelWidth;
108
+ const panelRight = hudRect.left + panelWidth;
109
+ if (panelLeft < margin || hudRect.left < margin) {
110
+ dock.setAttribute(DOCK_ALIGN_ATTR, 'start');
111
+ }
112
+ else if (panelRight > viewportW - margin || hudRect.right > viewportW - margin) {
113
+ dock.setAttribute(DOCK_ALIGN_ATTR, 'end');
114
+ }
115
+ else {
116
+ dock.setAttribute(DOCK_ALIGN_ATTR, 'end');
117
+ }
118
+ }
119
+ else {
120
+ dock.removeAttribute(DOCK_ALIGN_ATTR);
121
+ }
122
+ if (!chatOpen && !settingsOpen) {
123
+ if (isHudDragged(dock))
124
+ relayoutHudPosition(dock);
125
+ return;
126
+ }
127
+ const tracked = [hudRect];
128
+ if (chatOpen && chatPanel instanceof HTMLElement && visiblePanel(chatPanel)) {
129
+ tracked.push(chatPanel.getBoundingClientRect());
130
+ }
131
+ if (settingsOpen && settingsPanel instanceof HTMLElement && visiblePanel(settingsPanel)) {
132
+ tracked.push(settingsPanel.getBoundingClientRect());
133
+ }
134
+ const bounds = unionRects(tracked);
135
+ if (bounds === undefined)
136
+ return;
137
+ let dx = 0;
138
+ let dy = 0;
139
+ if (bounds.left < margin)
140
+ dx = margin - bounds.left;
141
+ else if (bounds.right > viewportW - margin)
142
+ dx = viewportW - margin - bounds.right;
143
+ if (bounds.top < margin)
144
+ dy = margin - bounds.top;
145
+ else if (bounds.bottom > viewportH - margin)
146
+ dy = viewportH - margin - bounds.bottom;
147
+ if (0 === dx && 0 === dy)
148
+ return;
149
+ const dockRect = dock.getBoundingClientRect();
150
+ const base = isHudDragged(dock)
151
+ ? readHudPosition(dock)
152
+ : { left: dockRect.left, top: dockRect.top };
153
+ applyHudPosition(dock, base.left + dx, base.top + dy);
154
+ }
155
+ let syncRafId;
156
+ /** Batch dock layout work on the next frame (resize, panel open, drag end). */
157
+ export function scheduleSyncDockLayout(dock, overlay) {
158
+ if (syncRafId !== undefined)
159
+ cancelAnimationFrame(syncRafId);
160
+ syncRafId = requestAnimationFrame(() => {
161
+ syncRafId = undefined;
162
+ syncDockLayout(dock, overlay);
163
+ requestAnimationFrame(() => syncDockLayout(dock, overlay));
164
+ });
165
+ }
166
+ /** Resolve the dock node from an overlay root. */
167
+ export function findDock(overlay) {
168
+ const dock = overlay.querySelector(`[${DOCK_ATTR}]`);
169
+ return dock instanceof HTMLElement ? dock : undefined;
170
+ }
@@ -0,0 +1,48 @@
1
+ export interface HudDragCallbacks {
2
+ onDragMove?: () => void;
3
+ /** Fires when a drag gesture ends; `moved` is true when the panel actually changed position. */
4
+ onDragEnd?: (moved: boolean) => void;
5
+ }
6
+ /** Whether the HUD is off the default bottom-right dock. */
7
+ export declare function isHudDragged(hud: HTMLElement): boolean;
8
+ /** Clamp a HUD position so the full panel stays inside the viewport. */
9
+ export declare function clampHudPosition(left: number, top: number, width: number, height: number, viewportWidth: number, viewportHeight: number, margin?: number): {
10
+ left: number;
11
+ top: number;
12
+ };
13
+ /** Paint an explicit left/top position (switches the HUD off the default bottom-right dock). */
14
+ export declare function applyHudPosition(hud: HTMLElement, left: number, top: number): void;
15
+ /** Return the HUD to the default bottom-right dock. */
16
+ export declare function resetHudDockPosition(hud: HTMLElement): void;
17
+ /** Return the HUD's laid-out box (rounded to whole pixels for stable clamping). */
18
+ export declare function hudLayoutBox(hud: HTMLElement): {
19
+ left: number;
20
+ top: number;
21
+ width: number;
22
+ height: number;
23
+ };
24
+ /** Read the dragged HUD's authored position (CSS vars), falling back to layout box. */
25
+ export declare function readHudPosition(hud: HTMLElement): {
26
+ left: number;
27
+ top: number;
28
+ };
29
+ /**
30
+ * Re-clamp a dragged HUD after resize, minimise/expand, or any layout-driven size change.
31
+ * No-op when the panel is still on the default dock.
32
+ */
33
+ export declare function relayoutHudPosition(hud: HTMLElement): void;
34
+ /**
35
+ * Keep a dragged HUD inside the viewport when the window or panel size changes
36
+ * (minimise ↔ expand, tally appearing, viewport resize).
37
+ */
38
+ export declare function installHudPositionGuards(hud: HTMLElement, overlay: HTMLElement): () => void;
39
+ /**
40
+ * Drag the presenter HUD by its header. Returns a teardown that removes listeners.
41
+ * Uses pointer capture so the drag stays smooth even when the cursor leaves the head.
42
+ */
43
+ export declare function installHudDrag(hud: HTMLElement, head: HTMLElement, callbacks?: HudDragCallbacks): () => void;
44
+ /**
45
+ * Drag the presenter HUD from multiple handles (FAB when collapsed, toolbar brand when expanded).
46
+ * Returns a teardown that removes listeners from every handle.
47
+ */
48
+ export declare function installHudDragHandles(hud: HTMLElement, handles: HTMLElement[], callbacks?: HudDragCallbacks): () => void;