clay-server 4.0.0-beta.8 → 4.0.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/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -19
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Sticky Notes browser: the bounded right workbench window over the board.
|
|
2
|
+
//
|
|
3
|
+
// This is the index/viewer surface, not the floating cards. The cards stay on
|
|
4
|
+
// the canvas over the conversation; this pane lists them, splits them into Open
|
|
5
|
+
// and Closed, and is where a note is completed or brought back.
|
|
6
|
+
//
|
|
7
|
+
// Completion here is Close, never delete. A closed note leaves the active
|
|
8
|
+
// canvas and moves to the Closed tab, where it stays readable and reversible.
|
|
9
|
+
// There is deliberately no destructive control on this surface at all.
|
|
10
|
+
|
|
11
|
+
import { store } from './store.js';
|
|
12
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
13
|
+
import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
|
|
14
|
+
import { enhanceClayLogLinks } from './clay-log-links.js';
|
|
15
|
+
import { send } from './sticky-notes-shared.js';
|
|
16
|
+
import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
|
|
17
|
+
|
|
18
|
+
var panel = null;
|
|
19
|
+
var gridEl = null;
|
|
20
|
+
var tabsEl = null;
|
|
21
|
+
|
|
22
|
+
// Registered by app.js so this module never imports the other right-pane tools
|
|
23
|
+
// and cannot form an import cycle with them.
|
|
24
|
+
var exclusiveClosers = [];
|
|
25
|
+
|
|
26
|
+
export function registerExclusiveClosers(fns) {
|
|
27
|
+
exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function closeOtherRightTools() {
|
|
31
|
+
for (var i = 0; i < exclusiveClosers.length; i++) {
|
|
32
|
+
try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isClosed(data) {
|
|
37
|
+
if (!data) return false;
|
|
38
|
+
if (data.state === "closed") return true;
|
|
39
|
+
if (data.state === "open") return false;
|
|
40
|
+
// Legacy notes predate the state field; `hidden` was the old spelling.
|
|
41
|
+
return data.hidden === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function openNotes(all) {
|
|
45
|
+
return (all || []).filter(function (data) { return !isClosed(data); });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function closedNotes(all) {
|
|
49
|
+
return (all || []).filter(isClosed);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function currentTab() {
|
|
53
|
+
return store.get('notesBrowserTab') === "closed" ? "closed" : "open";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Panel ---------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function ensurePanel() {
|
|
59
|
+
if (panel) return;
|
|
60
|
+
var panels = document.getElementById("main-panels");
|
|
61
|
+
if (!panels) return;
|
|
62
|
+
panel = document.createElement("section");
|
|
63
|
+
panel.id = "notes-browser";
|
|
64
|
+
panel.className = "notes-browser hidden";
|
|
65
|
+
panel.setAttribute("aria-label", "Sticky Notes");
|
|
66
|
+
panel.innerHTML =
|
|
67
|
+
'<header class="notes-browser-topbar">' +
|
|
68
|
+
'<span class="notes-browser-title">' + iconHtml("sticky-note") + 'Sticky Notes</span>' +
|
|
69
|
+
'<span class="notes-browser-subtitle">Open items on the board, and what has been closed</span>' +
|
|
70
|
+
'<div class="notes-browser-window-actions" aria-label="Sticky Notes window controls">' +
|
|
71
|
+
'<button id="notes-browser-wide" class="notes-browser-icon-btn" type="button" title="Widen panel" aria-label="Widen Sticky Notes panel" aria-pressed="false">' + iconHtml("chevrons-left-right") + '</button>' +
|
|
72
|
+
'<button id="notes-browser-fullscreen" class="notes-browser-icon-btn" type="button" title="Toggle fullscreen" aria-label="Toggle Sticky Notes fullscreen" aria-pressed="false">' + iconHtml("maximize-2") + '</button>' +
|
|
73
|
+
'<button id="notes-browser-close" class="notes-browser-icon-btn" type="button" title="Close Sticky Notes" aria-label="Close Sticky Notes">' + iconHtml("x") + '</button>' +
|
|
74
|
+
'</div>' +
|
|
75
|
+
'</header>' +
|
|
76
|
+
'<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
|
|
77
|
+
'<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
|
|
78
|
+
panels.appendChild(panel);
|
|
79
|
+
|
|
80
|
+
gridEl = panel.querySelector("#notes-browser-grid");
|
|
81
|
+
tabsEl = panel.querySelector("#notes-browser-tabs");
|
|
82
|
+
|
|
83
|
+
panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
|
|
84
|
+
panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
|
|
85
|
+
applyWindowState(!store.get('notesBrowserWide'), store.get('notesBrowserFullscreen'));
|
|
86
|
+
});
|
|
87
|
+
panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
|
|
88
|
+
applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
|
|
89
|
+
});
|
|
90
|
+
refreshIcons();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Width and fullscreen are transient view state, so they live in the store.
|
|
94
|
+
// `panel-fullscreen` is the shared class the document viewer, terminal, and
|
|
95
|
+
// Logs already use, so the geometry is the established one.
|
|
96
|
+
function applyWindowState(wide, fullscreen) {
|
|
97
|
+
store.set({ notesBrowserWide: !!wide, notesBrowserFullscreen: !!fullscreen });
|
|
98
|
+
if (!panel) return;
|
|
99
|
+
panel.classList.toggle("notes-browser-wide", !!wide && !fullscreen);
|
|
100
|
+
panel.classList.toggle("panel-fullscreen", !!fullscreen);
|
|
101
|
+
var wideBtn = panel.querySelector("#notes-browser-wide");
|
|
102
|
+
var fullBtn = panel.querySelector("#notes-browser-fullscreen");
|
|
103
|
+
if (wideBtn) {
|
|
104
|
+
wideBtn.setAttribute("aria-pressed", wide ? "true" : "false");
|
|
105
|
+
wideBtn.disabled = !!fullscreen;
|
|
106
|
+
}
|
|
107
|
+
if (fullBtn) fullBtn.setAttribute("aria-pressed", fullscreen ? "true" : "false");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Tabs ----------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function renderTabs(openCount, closedCount) {
|
|
113
|
+
if (!tabsEl) return;
|
|
114
|
+
var active = currentTab();
|
|
115
|
+
tabsEl.innerHTML = "";
|
|
116
|
+
var specs = [
|
|
117
|
+
{ key: "open", label: "Open", count: openCount },
|
|
118
|
+
{ key: "closed", label: "Closed", count: closedCount },
|
|
119
|
+
];
|
|
120
|
+
for (var i = 0; i < specs.length; i++) {
|
|
121
|
+
(function (spec) {
|
|
122
|
+
var tab = document.createElement("button");
|
|
123
|
+
tab.type = "button";
|
|
124
|
+
tab.className = "notes-browser-tab" + (spec.key === active ? " active" : "");
|
|
125
|
+
tab.setAttribute("role", "tab");
|
|
126
|
+
tab.setAttribute("aria-selected", spec.key === active ? "true" : "false");
|
|
127
|
+
tab.tabIndex = spec.key === active ? 0 : -1;
|
|
128
|
+
tab.dataset.tab = spec.key;
|
|
129
|
+
tab.innerHTML = '<span>' + spec.label + '</span><span class="notes-browser-tab-count">' + spec.count + '</span>';
|
|
130
|
+
tab.addEventListener("click", function () { selectTab(spec.key); });
|
|
131
|
+
tab.addEventListener("keydown", function (e) {
|
|
132
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
selectTab(spec.key === "open" ? "closed" : "open");
|
|
135
|
+
var next = tabsEl.querySelector('.notes-browser-tab.active');
|
|
136
|
+
if (next) next.focus();
|
|
137
|
+
});
|
|
138
|
+
tabsEl.appendChild(tab);
|
|
139
|
+
})(specs[i]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectTab(key) {
|
|
144
|
+
store.set({ notesBrowserTab: key === "closed" ? "closed" : "open" });
|
|
145
|
+
renderNotesBrowser();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Cards ---------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function closedMeta(data) {
|
|
151
|
+
var parts = [];
|
|
152
|
+
if (data.closedAt) {
|
|
153
|
+
var when = new Date(data.closedAt);
|
|
154
|
+
if (!isNaN(when.getTime())) parts.push("Closed " + when.toLocaleDateString());
|
|
155
|
+
} else {
|
|
156
|
+
parts.push("Closed");
|
|
157
|
+
}
|
|
158
|
+
var by = data.closedBy;
|
|
159
|
+
if (by && by.type === "user" && by.displayName) parts.push("by " + by.displayName);
|
|
160
|
+
else if (by && by.type === "session") parts.push("by an agent session");
|
|
161
|
+
return parts.join(" · ");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildCard(data) {
|
|
165
|
+
var card = document.createElement("article");
|
|
166
|
+
var closed = isClosed(data);
|
|
167
|
+
card.className = "notes-browser-card" + (closed ? " closed" : "");
|
|
168
|
+
card.setAttribute("role", "listitem");
|
|
169
|
+
card.dataset.color = data.color || "purple";
|
|
170
|
+
card.dataset.noteId = data.id;
|
|
171
|
+
|
|
172
|
+
var header = document.createElement("div");
|
|
173
|
+
header.className = "notes-browser-card-header";
|
|
174
|
+
|
|
175
|
+
var title = document.createElement("h3");
|
|
176
|
+
title.className = "notes-browser-card-title";
|
|
177
|
+
title.textContent = getTitle(data.text) || "Untitled";
|
|
178
|
+
header.appendChild(title);
|
|
179
|
+
|
|
180
|
+
// Exactly one lifecycle action per card, and it is never destructive.
|
|
181
|
+
var action = document.createElement("button");
|
|
182
|
+
action.type = "button";
|
|
183
|
+
action.className = "notes-browser-card-action";
|
|
184
|
+
if (closed) {
|
|
185
|
+
action.innerHTML = iconHtml("rotate-ccw") + "<span>Reopen</span>";
|
|
186
|
+
action.title = "Reopen this note and return it to the board";
|
|
187
|
+
action.setAttribute("aria-label", "Reopen note: " + title.textContent);
|
|
188
|
+
action.addEventListener("click", function (e) {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
send({ type: "note_reopen", id: data.id });
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
action.innerHTML = iconHtml("check") + "<span>Close</span>";
|
|
194
|
+
action.title = "Close this note; it stays on record and can be reopened";
|
|
195
|
+
action.setAttribute("aria-label", "Close note: " + title.textContent);
|
|
196
|
+
action.addEventListener("click", function (e) {
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
send({ type: "note_close", id: data.id });
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
header.appendChild(action);
|
|
202
|
+
card.appendChild(header);
|
|
203
|
+
|
|
204
|
+
var body = document.createElement("div");
|
|
205
|
+
body.className = "notes-browser-card-body";
|
|
206
|
+
var bodyLines = (data.text || "").split("\n").slice(1).join("\n").trim();
|
|
207
|
+
if (bodyLines) {
|
|
208
|
+
body.innerHTML = renderMiniMarkdown("_\n" + bodyLines).replace('<div class="sn-title">_</div>', "");
|
|
209
|
+
enhanceClayLogLinks(body);
|
|
210
|
+
}
|
|
211
|
+
card.appendChild(body);
|
|
212
|
+
|
|
213
|
+
if (closed) {
|
|
214
|
+
var meta = document.createElement("p");
|
|
215
|
+
meta.className = "notes-browser-card-meta";
|
|
216
|
+
meta.textContent = closedMeta(data);
|
|
217
|
+
card.appendChild(meta);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var strip = document.createElement("div");
|
|
221
|
+
strip.className = "notes-browser-card-color";
|
|
222
|
+
card.appendChild(strip);
|
|
223
|
+
|
|
224
|
+
// An open note can be jumped to on the canvas. A closed note has no card on
|
|
225
|
+
// the canvas, so reopening is the only way back and the body is read-only.
|
|
226
|
+
if (!closed) {
|
|
227
|
+
card.tabIndex = 0;
|
|
228
|
+
card.addEventListener("click", function () { focusNoteOnCanvas(data.id); });
|
|
229
|
+
card.addEventListener("keydown", function (e) {
|
|
230
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
focusNoteOnCanvas(data.id);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return card;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function emptyState(tab) {
|
|
239
|
+
var empty = document.createElement("div");
|
|
240
|
+
empty.className = "notes-browser-empty";
|
|
241
|
+
if (tab === "closed") {
|
|
242
|
+
empty.innerHTML = iconHtml("check") +
|
|
243
|
+
"<p>Nothing closed yet</p>" +
|
|
244
|
+
"<p class=\"notes-browser-empty-sub\">Closing a note takes it off the board without deleting it. Anything you close shows up here and can be reopened.</p>";
|
|
245
|
+
} else {
|
|
246
|
+
empty.innerHTML = iconHtml("sticky-note") +
|
|
247
|
+
"<p>Keep what matters across sessions</p>" +
|
|
248
|
+
"<p class=\"notes-browser-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p>";
|
|
249
|
+
}
|
|
250
|
+
return empty;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function renderNotesBrowser() {
|
|
254
|
+
if (!gridEl) return;
|
|
255
|
+
var all = listNoteData();
|
|
256
|
+
var open = openNotes(all);
|
|
257
|
+
var closed = closedNotes(all);
|
|
258
|
+
renderTabs(open.length, closed.length);
|
|
259
|
+
|
|
260
|
+
var tab = currentTab();
|
|
261
|
+
var shown = tab === "closed" ? closed : open;
|
|
262
|
+
// Newest first. The id is timestamp-prefixed, so it orders by creation.
|
|
263
|
+
shown = shown.slice().sort(function (a, b) {
|
|
264
|
+
var left = String(a.id || "");
|
|
265
|
+
var right = String(b.id || "");
|
|
266
|
+
return left < right ? 1 : left > right ? -1 : 0;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
gridEl.innerHTML = "";
|
|
270
|
+
if (shown.length === 0) {
|
|
271
|
+
gridEl.appendChild(emptyState(tab));
|
|
272
|
+
refreshIcons();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
for (var i = 0; i < shown.length; i++) gridEl.appendChild(buildCard(shown[i]));
|
|
276
|
+
refreshIcons();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- Lifecycle -----------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
// The conversation, composer, and title bar stay mounted and visible: the
|
|
282
|
+
// browser opens beside them in the single right workbench slot.
|
|
283
|
+
export function openNotesBrowser() {
|
|
284
|
+
ensurePanel();
|
|
285
|
+
if (!panel) return;
|
|
286
|
+
closeOtherRightTools();
|
|
287
|
+
// The floating canvas would otherwise sit on top of the pane.
|
|
288
|
+
hideNotes();
|
|
289
|
+
panel.classList.remove("hidden");
|
|
290
|
+
applyWindowState(store.get('notesBrowserWide'), false);
|
|
291
|
+
store.set({ notesBrowserOpen: true });
|
|
292
|
+
var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
|
|
293
|
+
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
294
|
+
renderNotesBrowser();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function closeNotesBrowser() {
|
|
298
|
+
if (!store.get('notesBrowserOpen')) return;
|
|
299
|
+
if (panel) panel.classList.add("hidden");
|
|
300
|
+
// Fullscreen is always dropped on close so the next open is a bounded pane.
|
|
301
|
+
applyWindowState(store.get('notesBrowserWide'), false);
|
|
302
|
+
store.set({ notesBrowserOpen: false });
|
|
303
|
+
var sidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
|
|
304
|
+
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
305
|
+
// The canvas comes back so the open notes are reachable again.
|
|
306
|
+
showNotes();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function isNotesBrowserOpen() {
|
|
310
|
+
return !!store.get('notesBrowserOpen');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function initNotesBrowser() {
|
|
314
|
+
document.addEventListener("keydown", function (e) {
|
|
315
|
+
if (e.key !== "Escape") return;
|
|
316
|
+
if (!store.get('notesBrowserOpen')) return;
|
|
317
|
+
closeNotesBrowser();
|
|
318
|
+
});
|
|
319
|
+
store.subscribe(function (state, previous) {
|
|
320
|
+
if (state.currentSlug === previous.currentSlug) return;
|
|
321
|
+
// A different project starts from a closed, bounded, Open-tab pane.
|
|
322
|
+
closeNotesBrowser();
|
|
323
|
+
applyWindowState(false, false);
|
|
324
|
+
store.set({ notesBrowserTab: "open" });
|
|
325
|
+
});
|
|
326
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// One floating sticky-note card: its markup, its drag and resize behaviour, and
|
|
2
|
+
// its header controls (colour, minimize, close, bring to front).
|
|
3
|
+
//
|
|
4
|
+
// The card knows how to build and wire itself. It does not own the board, so it
|
|
5
|
+
// never touches the notes map or the badges; the canvas module holds those and
|
|
6
|
+
// calls renderNote for each note.
|
|
7
|
+
|
|
8
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
9
|
+
import { VENDOR_AVATARS } from './app-rendering.js';
|
|
10
|
+
import { getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
|
|
11
|
+
import { enhanceClayLogLinks } from './clay-log-links.js';
|
|
12
|
+
import {
|
|
13
|
+
NOTE_COLORS,
|
|
14
|
+
isClosedNote,
|
|
15
|
+
send,
|
|
16
|
+
clampPos,
|
|
17
|
+
clampSize,
|
|
18
|
+
debouncedUpdate,
|
|
19
|
+
} from './sticky-notes-shared.js';
|
|
20
|
+
import { setupTextEdit } from './sticky-notes-editor.js';
|
|
21
|
+
|
|
22
|
+
var colorPickerEl = null;
|
|
23
|
+
|
|
24
|
+
// --- Note rendering ---
|
|
25
|
+
|
|
26
|
+
export function renderNote(data) {
|
|
27
|
+
var el = document.createElement("div");
|
|
28
|
+
el.className = "sticky-note";
|
|
29
|
+
el.dataset.noteId = data.id;
|
|
30
|
+
var clamped = clampPos(data.x, data.y, data.w, data.h);
|
|
31
|
+
el.style.left = clamped.x + "px";
|
|
32
|
+
el.style.top = clamped.y + "px";
|
|
33
|
+
el.style.width = data.w + "px";
|
|
34
|
+
el.style.height = data.h + "px";
|
|
35
|
+
el.style.zIndex = 100 + (data.zIndex || 0);
|
|
36
|
+
el.dataset.color = data.color || "purple";
|
|
37
|
+
|
|
38
|
+
if (data.minimized) el.classList.add("minimized");
|
|
39
|
+
if (isClosedNote(data)) el.classList.add("hidden");
|
|
40
|
+
|
|
41
|
+
// Header
|
|
42
|
+
var header = document.createElement("div");
|
|
43
|
+
header.className = "sticky-note-header";
|
|
44
|
+
|
|
45
|
+
if (data.origin && VENDOR_AVATARS[data.origin.vendor]) {
|
|
46
|
+
var originBadge = document.createElement("img");
|
|
47
|
+
originBadge.className = "sticky-note-agent-badge";
|
|
48
|
+
originBadge.src = VENDOR_AVATARS[data.origin.vendor];
|
|
49
|
+
originBadge.alt = "";
|
|
50
|
+
originBadge.title = "Created by " + data.origin.vendor;
|
|
51
|
+
header.appendChild(originBadge);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var closeBtn = document.createElement("button");
|
|
55
|
+
closeBtn.className = "sticky-note-btn sticky-note-close";
|
|
56
|
+
closeBtn.title = "Close";
|
|
57
|
+
closeBtn.innerHTML = iconHtml("x");
|
|
58
|
+
header.appendChild(closeBtn);
|
|
59
|
+
|
|
60
|
+
var minBtn = document.createElement("button");
|
|
61
|
+
minBtn.className = "sticky-note-btn sticky-note-min-btn";
|
|
62
|
+
minBtn.title = data.minimized ? "Expand" : "Minimize";
|
|
63
|
+
minBtn.innerHTML = data.minimized ? iconHtml("maximize-2") : iconHtml("minus");
|
|
64
|
+
header.appendChild(minBtn);
|
|
65
|
+
|
|
66
|
+
var spacer = document.createElement("div");
|
|
67
|
+
spacer.className = "sticky-note-spacer";
|
|
68
|
+
spacer.textContent = getTitle(data.text) || "Untitled";
|
|
69
|
+
header.appendChild(spacer);
|
|
70
|
+
|
|
71
|
+
var addBtn = document.createElement("button");
|
|
72
|
+
addBtn.className = "sticky-note-btn";
|
|
73
|
+
addBtn.title = "New note";
|
|
74
|
+
addBtn.innerHTML = iconHtml("plus");
|
|
75
|
+
addBtn.addEventListener("click", function (e) {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
createNote();
|
|
78
|
+
});
|
|
79
|
+
header.appendChild(addBtn);
|
|
80
|
+
|
|
81
|
+
var colorBtn = document.createElement("button");
|
|
82
|
+
colorBtn.className = "sticky-note-color-btn";
|
|
83
|
+
colorBtn.title = "Change color";
|
|
84
|
+
colorBtn.innerHTML = iconHtml("palette");
|
|
85
|
+
header.appendChild(colorBtn);
|
|
86
|
+
|
|
87
|
+
var mdBtn = document.createElement("button");
|
|
88
|
+
mdBtn.className = "sticky-note-btn sticky-note-md-btn";
|
|
89
|
+
mdBtn.title = "Edit markdown";
|
|
90
|
+
mdBtn.innerHTML = "<span class='sn-md-label'>MD</span>";
|
|
91
|
+
header.appendChild(mdBtn);
|
|
92
|
+
|
|
93
|
+
var opacityWrap = document.createElement("div");
|
|
94
|
+
opacityWrap.className = "sticky-note-opacity";
|
|
95
|
+
var opacitySlider = document.createElement("input");
|
|
96
|
+
opacitySlider.type = "range";
|
|
97
|
+
opacitySlider.min = "20";
|
|
98
|
+
opacitySlider.max = "100";
|
|
99
|
+
opacitySlider.value = String(Math.round((typeof data.opacity === "number" ? data.opacity : 0.64) * 100));
|
|
100
|
+
opacitySlider.className = "sticky-note-opacity-slider";
|
|
101
|
+
opacitySlider.title = "Opacity";
|
|
102
|
+
opacityWrap.appendChild(opacitySlider);
|
|
103
|
+
header.appendChild(opacityWrap);
|
|
104
|
+
|
|
105
|
+
// Apply saved opacity via CSS variable (not element opacity, so header stays visible on hover)
|
|
106
|
+
if (typeof data.opacity === "number") {
|
|
107
|
+
el.style.setProperty("--note-opacity", data.opacity);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
opacitySlider.addEventListener("input", function (e) {
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
var val = parseInt(opacitySlider.value, 10) / 100;
|
|
113
|
+
el.style.setProperty("--note-opacity", val);
|
|
114
|
+
});
|
|
115
|
+
opacitySlider.addEventListener("change", function (e) {
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
var val = parseInt(opacitySlider.value, 10) / 100;
|
|
118
|
+
el.style.setProperty("--note-opacity", val);
|
|
119
|
+
debouncedUpdate(data.id, { opacity: val }, 300);
|
|
120
|
+
});
|
|
121
|
+
opacitySlider.addEventListener("mousedown", function (e) { e.stopPropagation(); });
|
|
122
|
+
|
|
123
|
+
el.appendChild(header);
|
|
124
|
+
|
|
125
|
+
// Body
|
|
126
|
+
var body = document.createElement("div");
|
|
127
|
+
body.className = "sticky-note-body";
|
|
128
|
+
|
|
129
|
+
// Hidden textarea as markdown data store
|
|
130
|
+
var textarea = document.createElement("textarea");
|
|
131
|
+
textarea.className = "sticky-note-text";
|
|
132
|
+
textarea.value = data.text || "";
|
|
133
|
+
textarea.style.display = "none";
|
|
134
|
+
body.appendChild(textarea);
|
|
135
|
+
|
|
136
|
+
// Contenteditable rendered view (primary editing surface)
|
|
137
|
+
var rendered = document.createElement("div");
|
|
138
|
+
rendered.className = "sticky-note-rendered";
|
|
139
|
+
rendered.contentEditable = "true";
|
|
140
|
+
rendered.spellcheck = true;
|
|
141
|
+
if (data.text && data.text.trim()) {
|
|
142
|
+
rendered.innerHTML = renderMiniMarkdown(data.text);
|
|
143
|
+
enhanceClayLogLinks(rendered);
|
|
144
|
+
} else {
|
|
145
|
+
rendered.classList.add("is-empty");
|
|
146
|
+
}
|
|
147
|
+
body.appendChild(rendered);
|
|
148
|
+
|
|
149
|
+
el.appendChild(body);
|
|
150
|
+
|
|
151
|
+
// Resize handle
|
|
152
|
+
var resizeHandle = document.createElement("div");
|
|
153
|
+
resizeHandle.className = "sticky-note-resize";
|
|
154
|
+
el.appendChild(resizeHandle);
|
|
155
|
+
|
|
156
|
+
// --- Event handlers ---
|
|
157
|
+
setupDrag(el, spacer, data.id);
|
|
158
|
+
setupResize(el, resizeHandle, data.id);
|
|
159
|
+
setupTextEdit(textarea, rendered, data.id, mdBtn);
|
|
160
|
+
setupColorPicker(colorBtn, el, data.id);
|
|
161
|
+
setupMinimize(minBtn, el, data.id);
|
|
162
|
+
setupClose(closeBtn, el, data.id);
|
|
163
|
+
setupBringToFront(el, data.id);
|
|
164
|
+
|
|
165
|
+
refreshIcons();
|
|
166
|
+
return el;
|
|
167
|
+
}
|
|
168
|
+
// --- Drag ---
|
|
169
|
+
|
|
170
|
+
function setupDrag(noteEl, spacerEl, noteId) {
|
|
171
|
+
var dragging = false;
|
|
172
|
+
var pointerId = null;
|
|
173
|
+
var startX, startY, origX, origY;
|
|
174
|
+
|
|
175
|
+
spacerEl.addEventListener("pointerdown", function (e) {
|
|
176
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
dragging = true;
|
|
179
|
+
pointerId = e.pointerId;
|
|
180
|
+
startX = e.clientX;
|
|
181
|
+
startY = e.clientY;
|
|
182
|
+
origX = parseInt(noteEl.style.left) || 0;
|
|
183
|
+
origY = parseInt(noteEl.style.top) || 0;
|
|
184
|
+
noteEl.classList.add("dragging");
|
|
185
|
+
document.body.classList.add("sticky-note-interacting");
|
|
186
|
+
spacerEl.setPointerCapture(pointerId);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
function onMove(e) {
|
|
190
|
+
if (!dragging || e.pointerId !== pointerId) return;
|
|
191
|
+
var dx = e.clientX - startX;
|
|
192
|
+
var dy = e.clientY - startY;
|
|
193
|
+
var c = clampPos(origX + dx, origY + dy, noteEl.offsetWidth, noteEl.offsetHeight);
|
|
194
|
+
noteEl.style.left = c.x + "px";
|
|
195
|
+
noteEl.style.top = c.y + "px";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onUp(e) {
|
|
199
|
+
if (!dragging || (e && e.pointerId !== pointerId)) return;
|
|
200
|
+
dragging = false;
|
|
201
|
+
noteEl.classList.remove("dragging");
|
|
202
|
+
document.body.classList.remove("sticky-note-interacting");
|
|
203
|
+
if (pointerId !== null && spacerEl.hasPointerCapture(pointerId)) spacerEl.releasePointerCapture(pointerId);
|
|
204
|
+
pointerId = null;
|
|
205
|
+
debouncedUpdate(noteId, {
|
|
206
|
+
x: parseInt(noteEl.style.left),
|
|
207
|
+
y: parseInt(noteEl.style.top),
|
|
208
|
+
}, 200);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
spacerEl.addEventListener("pointermove", onMove);
|
|
212
|
+
spacerEl.addEventListener("pointerup", onUp);
|
|
213
|
+
spacerEl.addEventListener("pointercancel", onUp);
|
|
214
|
+
spacerEl.addEventListener("lostpointercapture", onUp);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Resize ---
|
|
218
|
+
|
|
219
|
+
function setupResize(noteEl, handle, noteId) {
|
|
220
|
+
var resizing = false;
|
|
221
|
+
var pointerId = null;
|
|
222
|
+
var startX, startY, origW, origH;
|
|
223
|
+
var MIN_W = 160;
|
|
224
|
+
var MIN_H = 80;
|
|
225
|
+
|
|
226
|
+
handle.addEventListener("pointerdown", function (e) {
|
|
227
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
resizing = true;
|
|
231
|
+
pointerId = e.pointerId;
|
|
232
|
+
startX = e.clientX;
|
|
233
|
+
startY = e.clientY;
|
|
234
|
+
origW = noteEl.offsetWidth;
|
|
235
|
+
origH = noteEl.offsetHeight;
|
|
236
|
+
noteEl.classList.add("resizing");
|
|
237
|
+
document.body.classList.add("sticky-note-interacting");
|
|
238
|
+
handle.setPointerCapture(pointerId);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function onMove(e) {
|
|
242
|
+
if (!resizing || e.pointerId !== pointerId) return;
|
|
243
|
+
var rawW = Math.max(MIN_W, origW + (e.clientX - startX));
|
|
244
|
+
var rawH = Math.max(MIN_H, origH + (e.clientY - startY));
|
|
245
|
+
var cs = clampSize(parseInt(noteEl.style.left) || 0, parseInt(noteEl.style.top) || 0, rawW, rawH);
|
|
246
|
+
noteEl.style.width = Math.max(MIN_W, cs.w) + "px";
|
|
247
|
+
noteEl.style.height = Math.max(MIN_H, cs.h) + "px";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function onUp(e) {
|
|
251
|
+
if (!resizing || (e && e.pointerId !== pointerId)) return;
|
|
252
|
+
resizing = false;
|
|
253
|
+
noteEl.classList.remove("resizing");
|
|
254
|
+
document.body.classList.remove("sticky-note-interacting");
|
|
255
|
+
if (pointerId !== null && handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
|
|
256
|
+
pointerId = null;
|
|
257
|
+
debouncedUpdate(noteId, {
|
|
258
|
+
w: noteEl.offsetWidth,
|
|
259
|
+
h: noteEl.offsetHeight,
|
|
260
|
+
}, 200);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handle.addEventListener("pointermove", onMove);
|
|
264
|
+
handle.addEventListener("pointerup", onUp);
|
|
265
|
+
handle.addEventListener("pointercancel", onUp);
|
|
266
|
+
handle.addEventListener("lostpointercapture", onUp);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Color picker ---
|
|
270
|
+
|
|
271
|
+
export function closeColorPicker() {
|
|
272
|
+
if (colorPickerEl) {
|
|
273
|
+
colorPickerEl.remove();
|
|
274
|
+
colorPickerEl = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function setupColorPicker(btn, noteEl, noteId) {
|
|
279
|
+
btn.addEventListener("click", function (e) {
|
|
280
|
+
e.stopPropagation();
|
|
281
|
+
showColorPicker(btn, noteEl, noteId);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function showColorPicker(anchor, noteEl, noteId) {
|
|
286
|
+
closeColorPicker();
|
|
287
|
+
|
|
288
|
+
var picker = document.createElement("div");
|
|
289
|
+
picker.className = "sticky-note-color-picker";
|
|
290
|
+
|
|
291
|
+
for (var i = 0; i < NOTE_COLORS.length; i++) {
|
|
292
|
+
(function (color) {
|
|
293
|
+
var dot = document.createElement("button");
|
|
294
|
+
dot.className = "sticky-note-color-dot";
|
|
295
|
+
dot.dataset.color = color;
|
|
296
|
+
if (noteEl.dataset.color === color) dot.classList.add("active");
|
|
297
|
+
dot.addEventListener("click", function (e) {
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
noteEl.dataset.color = color;
|
|
300
|
+
send({ type: "note_update", id: noteId, color: color });
|
|
301
|
+
closeColorPicker();
|
|
302
|
+
});
|
|
303
|
+
picker.appendChild(dot);
|
|
304
|
+
})(NOTE_COLORS[i]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
document.body.appendChild(picker);
|
|
308
|
+
colorPickerEl = picker;
|
|
309
|
+
|
|
310
|
+
// Position relative to anchor
|
|
311
|
+
var rect = anchor.getBoundingClientRect();
|
|
312
|
+
picker.style.left = rect.left + "px";
|
|
313
|
+
picker.style.top = (rect.bottom + 4) + "px";
|
|
314
|
+
|
|
315
|
+
// Close on outside click
|
|
316
|
+
setTimeout(function () {
|
|
317
|
+
document.addEventListener("click", function closeHandler() {
|
|
318
|
+
closeColorPicker();
|
|
319
|
+
document.removeEventListener("click", closeHandler);
|
|
320
|
+
});
|
|
321
|
+
}, 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- Minimize ---
|
|
325
|
+
|
|
326
|
+
function setupMinimize(btn, noteEl, noteId) {
|
|
327
|
+
btn.addEventListener("click", function (e) {
|
|
328
|
+
e.stopPropagation();
|
|
329
|
+
var isMinimized = noteEl.classList.toggle("minimized");
|
|
330
|
+
btn.innerHTML = isMinimized ? iconHtml("maximize-2") : iconHtml("minus");
|
|
331
|
+
btn.title = isMinimized ? "Expand" : "Minimize";
|
|
332
|
+
refreshIcons();
|
|
333
|
+
send({ type: "note_update", id: noteId, minimized: isMinimized });
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- Close note ---
|
|
338
|
+
//
|
|
339
|
+
// Completing a note closes it. The record is kept and stays reachable in the
|
|
340
|
+
// browser's Closed tab; nothing is ever deleted from this control.
|
|
341
|
+
|
|
342
|
+
function setupClose(btn, noteEl, noteId) {
|
|
343
|
+
btn.addEventListener("click", function (e) {
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
noteEl.classList.add("hidden");
|
|
346
|
+
send({ type: "note_close", id: noteId });
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- Bring to front ---
|
|
351
|
+
|
|
352
|
+
function setupBringToFront(noteEl, noteId) {
|
|
353
|
+
noteEl.addEventListener("mousedown", function (e) {
|
|
354
|
+
// Skip bring-to-front when clicking header buttons to avoid
|
|
355
|
+
// race condition where server response replaces innerHTML
|
|
356
|
+
// between mousedown and click, causing the click event to be lost
|
|
357
|
+
if (e.target.closest("button")) return;
|
|
358
|
+
send({ type: "note_bring_front", id: noteId });
|
|
359
|
+
});
|
|
360
|
+
}
|