clay-server 4.0.0-beta.12 → 4.0.0-beta.13

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.
@@ -1,238 +0,0 @@
1
- // Ambient discovery for Project Logs.
2
- //
3
- // The ledger accumulates alongside the work, so it should be reachable without
4
- // being hidden and noticeable without interrupting. Two affordances:
5
- //
6
- // - a slim handle on the right workbench edge that previews the pane on
7
- // hover or focus, and pins it open on any meaningful interaction
8
- // - a restrained unread marker on the toolbar button and the handle when the
9
- // Project Driver revises the ledger
10
- //
11
- // A preview is explicitly not an open: it closes itself when the pointer
12
- // leaves and never survives an Escape. Nothing here opens the pane on its own,
13
- // and an update never opens anything at all.
14
-
15
- import { store } from './store.js';
16
-
17
- var handle = null;
18
- var closeTimer = null;
19
- var hoverCapable = false;
20
-
21
- // Long enough to cross the gap from handle to panel without losing the
22
- // preview, short enough not to feel stuck.
23
- var CLOSE_DELAY_MS = 420;
24
-
25
- // Callbacks supplied by project-logs.js so this module never talks to the
26
- // WebSocket or decides navigation.
27
- var hooks = { reveal: null, hide: null, pin: null, isOpen: null, isPinned: null };
28
-
29
- function prefersReducedMotion() {
30
- try {
31
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
32
- } catch (e) {
33
- return false;
34
- }
35
- }
36
-
37
- // Hover preview is only offered to a device that genuinely hovers. Touch keeps
38
- // the explicit toolbar and navigation behaviour with no hover dependency.
39
- function detectHoverCapable() {
40
- try {
41
- return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
42
- } catch (e) {
43
- return false;
44
- }
45
- }
46
-
47
- function cancelClose() {
48
- if (closeTimer) {
49
- clearTimeout(closeTimer);
50
- closeTimer = null;
51
- }
52
- }
53
-
54
- function scheduleClose() {
55
- cancelClose();
56
- closeTimer = setTimeout(function () {
57
- closeTimer = null;
58
- // A pinned pane is a real open and is never closed by the pointer leaving.
59
- if (store.get('projectLogsPinned')) return;
60
- if (!store.get('projectLogsPreview')) return;
61
- hidePreview();
62
- }, CLOSE_DELAY_MS);
63
- }
64
-
65
- export function showPreview() {
66
- if (!hooks.reveal) return;
67
- if (store.get('projectLogsPinned') || store.get('projectLogsOpen')) return;
68
- cancelClose();
69
- if (store.get('projectLogsPreview')) return;
70
- store.set({ projectLogsPreview: true });
71
- hooks.reveal();
72
- syncHandle();
73
- }
74
-
75
- export function hidePreview() {
76
- cancelClose();
77
- if (!store.get('projectLogsPreview')) return;
78
- store.set({ projectLogsPreview: false });
79
- if (hooks.hide) hooks.hide();
80
- syncHandle();
81
- }
82
-
83
- // Any meaningful interaction turns a preview into a real open.
84
- export function pinFromPreview() {
85
- cancelClose();
86
- if (!hooks.pin) return;
87
- store.set({ projectLogsPreview: false });
88
- hooks.pin();
89
- syncHandle();
90
- }
91
-
92
- // The handle only exists while the pane is closed; a visible pane is its own
93
- // affordance.
94
- function syncHandle() {
95
- if (!handle) return;
96
- var open = !!(store.get('projectLogsOpen') || store.get('projectLogsPreview'));
97
- handle.classList.toggle("hidden", open);
98
- var unread = store.get('projectLogsUnread') || 0;
99
- handle.classList.toggle("has-unread", unread > 0);
100
- handle.setAttribute("aria-label", unread > 0
101
- ? "Open Project Logs, " + unread + (unread === 1 ? " new entry" : " new entries")
102
- : "Open Project Logs");
103
- handle.title = handle.getAttribute("aria-label");
104
- }
105
-
106
- // The toolbar button carries the same unread state, so the cue is visible
107
- // wherever the user is looking.
108
- function syncToolbar() {
109
- var button = document.getElementById("project-logs-btn");
110
- if (!button) return;
111
- var unread = store.get('projectLogsUnread') || 0;
112
- button.classList.toggle("project-logs-unread", unread > 0);
113
- var badge = document.getElementById("project-logs-count");
114
- if (badge) {
115
- badge.textContent = unread > 0 ? String(unread) : "";
116
- badge.classList.toggle("hidden", unread < 1);
117
- }
118
- }
119
-
120
- export function syncAmbient() {
121
- syncHandle();
122
- syncToolbar();
123
- }
124
-
125
- // A canonical revision arrived. Mark it, never open anything.
126
- //
127
- // Deduped by the server-derived reference plus revision number, so a replayed
128
- // or duplicated frame cannot inflate the count or re-trigger the flourish.
129
- export function noteCanonicalUpdate(msg) {
130
- if (!msg || !msg.ref || !msg.revision) return false;
131
- var seen = store.get('projectLogsSeenRevisions') || {};
132
- var key = msg.ref;
133
- if (seen[key] >= msg.revision) return false;
134
- var next = Object.assign({}, seen);
135
- next[key] = msg.revision;
136
-
137
- // An update while the pane is open is already visible, so it is acknowledged
138
- // rather than counted.
139
- var visible = !!(store.get('projectLogsOpen') || store.get('projectLogsPinned'));
140
- store.set({
141
- projectLogsSeenRevisions: next,
142
- projectLogsUnread: visible ? 0 : (store.get('projectLogsUnread') || 0) + 1,
143
- });
144
- syncAmbient();
145
- if (!visible) flourish();
146
- return true;
147
- }
148
-
149
- // A single restrained pulse on the two ambient surfaces. Never the whole
150
- // screen, and nothing at all when the user asked for reduced motion.
151
- function flourish() {
152
- if (prefersReducedMotion()) return;
153
- var targets = [handle, document.getElementById("project-logs-btn")];
154
- for (var i = 0; i < targets.length; i++) {
155
- var target = targets[i];
156
- if (!target) continue;
157
- target.classList.remove("project-logs-pulse");
158
- // Force a reflow so the animation restarts rather than being ignored.
159
- void target.offsetWidth;
160
- target.classList.add("project-logs-pulse");
161
- }
162
- }
163
-
164
- export function acknowledgeUpdates() {
165
- if (!store.get('projectLogsUnread')) {
166
- syncAmbient();
167
- return;
168
- }
169
- store.set({ projectLogsUnread: 0 });
170
- var targets = [handle, document.getElementById("project-logs-btn")];
171
- for (var i = 0; i < targets.length; i++) {
172
- if (targets[i]) targets[i].classList.remove("project-logs-pulse");
173
- }
174
- syncAmbient();
175
- }
176
-
177
- export function resetAmbient() {
178
- cancelClose();
179
- store.set({
180
- projectLogsUnread: 0,
181
- projectLogsSeenRevisions: {},
182
- projectLogsPreview: false,
183
- projectLogsPinned: false,
184
- });
185
- syncAmbient();
186
- }
187
-
188
- // Called by project-logs.js when the pane itself is entered or left, so moving
189
- // from the handle into the panel keeps the preview alive.
190
- export function bindPanelHover(panel) {
191
- if (!hoverCapable || !panel) return;
192
- panel.addEventListener("pointerenter", cancelClose);
193
- panel.addEventListener("pointerleave", function () {
194
- if (store.get('projectLogsPreview')) scheduleClose();
195
- });
196
- }
197
-
198
- export function initProjectLogsAmbient(callbacks) {
199
- hooks = callbacks || hooks;
200
- hoverCapable = detectHoverCapable();
201
-
202
- var main = document.getElementById("main-panels");
203
- if (!main) return;
204
-
205
- handle = document.createElement("button");
206
- handle.id = "project-logs-handle";
207
- handle.type = "button";
208
- handle.className = "project-logs-handle";
209
- handle.setAttribute("aria-label", "Open Project Logs");
210
- handle.title = "Open Project Logs";
211
- handle.innerHTML = '<span class="project-logs-handle-notch" aria-hidden="true"></span>';
212
- main.appendChild(handle);
213
-
214
- // Click and keyboard both pin, so the handle is a real control and not a
215
- // hover-only affordance.
216
- handle.addEventListener("click", function () {
217
- if (store.get('projectLogsOpen')) return;
218
- pinFromPreview();
219
- });
220
- handle.addEventListener("focus", function () {
221
- if (!store.get('projectLogsOpen')) showPreview();
222
- });
223
- handle.addEventListener("blur", function () {
224
- if (store.get('projectLogsPreview') && !store.get('projectLogsPinned')) scheduleClose();
225
- });
226
-
227
- if (hoverCapable) {
228
- handle.addEventListener("pointerenter", showPreview);
229
- handle.addEventListener("pointerleave", scheduleClose);
230
- }
231
-
232
- document.addEventListener("keydown", function (event) {
233
- if (event.key !== "Escape") return;
234
- if (store.get('projectLogsPreview')) hidePreview();
235
- });
236
-
237
- syncAmbient();
238
- }