@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.
- package/NOTICE +7 -0
- package/README.md +2 -2
- package/dist/actions/actions-dom.js +23 -15
- package/dist/actions/actions.d.ts +17 -0
- package/dist/actions/actions.js +132 -29
- package/dist/actions/synthetic-input.d.ts +9 -0
- package/dist/actions/synthetic-input.js +40 -0
- package/dist/commands/commands.js +18 -1
- package/dist/connect-options.d.ts +9 -9
- package/dist/dom/a11y.d.ts +9 -0
- package/dist/dom/a11y.js +19 -0
- package/dist/dom/desktop-capture.js +20 -3
- package/dist/dom/dom-ignore.d.ts +19 -6
- package/dist/dom/dom-ignore.js +20 -13
- package/dist/dom/query.d.ts +2 -2
- package/dist/dom/query.js +85 -45
- package/dist/dom/refs.d.ts +28 -0
- package/dist/dom/refs.js +87 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +34 -0
- package/dist/dom/source.d.ts +14 -0
- package/dist/dom/source.js +16 -0
- package/dist/dom/theme.d.ts +33 -10
- package/dist/dom/theme.js +109 -50
- package/dist/observers/console.js +5 -2
- package/dist/observers/install-all.js +4 -1
- package/dist/observers/ipc.d.ts +16 -0
- package/dist/observers/ipc.js +19 -1
- package/dist/observers/network.d.ts +8 -0
- package/dist/observers/network.js +29 -6
- package/dist/observers/state.js +21 -2
- package/dist/presenter/presenter-brand.d.ts +24 -7
- package/dist/presenter/presenter-brand.js +23 -6
- package/dist/presenter/presenter-config.d.ts +102 -9
- package/dist/presenter/presenter-config.js +94 -8
- package/dist/presenter/presenter-controls.d.ts +10 -10
- package/dist/presenter/presenter-controls.js +175 -85
- package/dist/presenter/presenter-dock-layout.d.ts +6 -0
- package/dist/presenter/presenter-dock-layout.js +170 -0
- package/dist/presenter/presenter-drag.d.ts +48 -0
- package/dist/presenter/presenter-drag.js +198 -0
- package/dist/presenter/presenter-effects.d.ts +1 -1
- package/dist/presenter/presenter-effects.js +1 -1
- package/dist/presenter/presenter-glow.d.ts +5 -5
- package/dist/presenter/presenter-glow.js +7 -7
- package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
- package/dist/presenter/presenter-heroicons-data.js +38 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
- package/dist/presenter/presenter-hud-chrome.js +41 -0
- package/dist/presenter/presenter-icons.d.ts +44 -0
- package/dist/presenter/presenter-icons.js +100 -0
- package/dist/presenter/presenter-log.d.ts +27 -5
- package/dist/presenter/presenter-log.js +114 -28
- package/dist/presenter/presenter-report-copy.d.ts +83 -0
- package/dist/presenter/presenter-report-copy.js +127 -0
- package/dist/presenter/presenter-report-styles.d.ts +7 -0
- package/dist/presenter/presenter-report-styles.js +102 -0
- package/dist/presenter/presenter-report.d.ts +23 -0
- package/dist/presenter/presenter-report.js +217 -0
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-settings-styles.d.ts +2 -0
- package/dist/presenter/presenter-settings-styles.js +153 -0
- package/dist/presenter/presenter-settings.d.ts +82 -0
- package/dist/presenter/presenter-settings.js +494 -0
- package/dist/presenter/presenter-shell-styles.d.ts +1 -0
- package/dist/presenter/presenter-shell-styles.js +375 -0
- package/dist/presenter/presenter-shell.d.ts +48 -0
- package/dist/presenter/presenter-shell.js +408 -0
- package/dist/presenter/presenter-styles.d.ts +9 -4
- package/dist/presenter/presenter-styles.js +61 -125
- package/dist/presenter/presenter-tally.d.ts +8 -11
- package/dist/presenter/presenter-tally.js +48 -23
- package/dist/presenter/presenter-test-helpers.d.ts +1 -1
- package/dist/presenter/presenter-test-helpers.js +1 -1
- package/dist/presenter/presenter-verbs.js +2 -2
- package/dist/presenter/presenter-workspace.d.ts +15 -0
- package/dist/presenter/presenter-workspace.js +180 -0
- package/dist/presenter/presenter.d.ts +15 -10
- package/dist/presenter/presenter.js +194 -46
- package/dist/registry/emitter.d.ts +2 -2
- package/dist/reticle.d.ts +2 -2
- package/dist/reticle.js +27 -25
- package/dist/review/annotator-styles.d.ts +6 -0
- package/dist/review/annotator-styles.js +81 -0
- package/dist/review/annotator.d.ts +29 -17
- package/dist/review/annotator.js +477 -146
- package/dist/timers/clock.js +8 -5
- package/dist/transport/unreachable-message.d.ts +12 -0
- package/dist/transport/unreachable-message.js +20 -0
- package/dist/util/captured-method.d.ts +37 -0
- package/dist/util/captured-method.js +45 -0
- 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
|
-
|
|
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
|
|
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
|
|
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-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
[data-reticle-
|
|
44
|
-
[data-reticle-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[data-reticle-
|
|
51
|
-
[data-reticle-
|
|
52
|
-
[data-reticle-
|
|
53
|
-
|
|
54
|
-
[data-reticle-
|
|
55
|
-
[data-reticle-
|
|
56
|
-
|
|
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-
|
|
60
|
-
[data-reticle-
|
|
61
|
-
[data-reticle-
|
|
62
|
-
[data-reticle-
|
|
63
|
-
[data-reticle-
|
|
64
|
-
background:var(--reticle-accent);color:#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[data-reticle-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
[data-reticle-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[data-reticle-overlay][data-reticle-state="ended"] [data-reticle-
|
|
75
|
-
[data-reticle-
|
|
76
|
-
|
|
77
|
-
[data-reticle-overlay][data-reticle-state="
|
|
78
|
-
box-shadow:inset 0 0 0
|
|
79
|
-
[data-reticle-overlay][data-reticle-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
[data-reticle-overlay][data-reticle-tone="
|
|
86
|
-
[data-reticle-overlay][data-reticle-tone="
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
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"]{
|
|
97
|
-
box-shadow:inset 0 0 0 2px rgba(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
[data-reticle-
|
|
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-
|
|
115
|
-
[data-reticle-
|
|
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
|
-
/**
|
|
118
|
-
|
|
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
|
-
/**
|
|
124
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
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;
|