clay-server 2.11.0-beta.8 → 2.11.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.
@@ -24,6 +24,27 @@ function showConfirmDialog(message, onConfirm) {
24
24
  });
25
25
  }
26
26
 
27
+ function showConfirmResetPin(name, onConfirm) {
28
+ var modal = document.createElement("div");
29
+ modal.className = "admin-modal-overlay";
30
+ var html = '<div class="admin-modal">' +
31
+ '<div class="admin-modal-body" style="padding:20px 16px 16px">' +
32
+ '<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">Reset PIN for <strong>' + escapeHtml(name) + '</strong>? A new temporary PIN will be generated and they will need to change it on next login.</p>' +
33
+ '</div>' +
34
+ '<div class="admin-modal-footer">' +
35
+ '<button class="admin-modal-save">Reset PIN</button>' +
36
+ '<button class="admin-modal-cancel">Cancel</button>' +
37
+ '</div></div>';
38
+ modal.innerHTML = html;
39
+ document.body.appendChild(modal);
40
+ modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
41
+ modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
42
+ modal.querySelector(".admin-modal-save").addEventListener("click", function () {
43
+ modal.remove();
44
+ onConfirm();
45
+ });
46
+ }
47
+
27
48
  var ctx = null;
28
49
  var cachedUsers = [];
29
50
  var cachedInvites = [];
@@ -111,8 +132,13 @@ function renderUsersTab(body) {
111
132
  html += '</div>';
112
133
  html += '<div class="admin-user-meta">' + escapeHtml(u.username) + ' · joined ' + created + '</div>';
113
134
  html += '</div>';
114
- if (!isMe && u.role !== "admin") {
115
- html += '<button class="admin-remove-btn" data-user-id="' + u.id + '" title="Remove user">' + iconHtml("trash-2") + '</button>';
135
+ if (!isMe) {
136
+ html += '<div style="display:flex;align-items:center;gap:2px">';
137
+ html += '<button class="admin-remove-btn admin-reset-pin-btn" data-user-id="' + u.id + '" title="Reset PIN">' + iconHtml("key-round") + '</button>';
138
+ if (u.role !== "admin") {
139
+ html += '<button class="admin-remove-btn" data-user-id="' + u.id + '" title="Remove user">' + iconHtml("trash-2") + '</button>';
140
+ }
141
+ html += '</div>';
116
142
  }
117
143
  html += '</div>';
118
144
  }
@@ -129,10 +155,32 @@ function renderUsersTab(body) {
129
155
  });
130
156
  }
131
157
 
158
+ // Bind reset PIN buttons
159
+ var resetBtns = body.querySelectorAll(".admin-reset-pin-btn");
160
+ for (var j = 0; j < resetBtns.length; j++) {
161
+ resetBtns[j].addEventListener("click", function (e) {
162
+ e.stopPropagation();
163
+ var userId = this.dataset.userId;
164
+ var user = cachedUsers.find(function (u) { return u.id === userId; });
165
+ var name = user ? (user.displayName || user.username) : "this user";
166
+ showConfirmResetPin(name, function () {
167
+ apiPost("/api/admin/users/" + userId + "/reset-pin").then(function (data) {
168
+ if (data.ok) {
169
+ showTempPinModal({ username: user.username, displayName: user.displayName || user.username }, data.tempPin);
170
+ } else {
171
+ showToast(data.error || "Failed to reset PIN");
172
+ }
173
+ }).catch(function () {
174
+ showToast("Failed to reset PIN");
175
+ });
176
+ });
177
+ });
178
+ }
179
+
132
180
  // Bind remove buttons
133
- var removeBtns = body.querySelectorAll(".admin-remove-btn");
134
- for (var j = 0; j < removeBtns.length; j++) {
135
- removeBtns[j].addEventListener("click", function () {
181
+ var removeBtns = body.querySelectorAll(".admin-remove-btn:not(.admin-reset-pin-btn)");
182
+ for (var k = 0; k < removeBtns.length; k++) {
183
+ removeBtns[k].addEventListener("click", function () {
136
184
  var userId = this.dataset.userId;
137
185
  var user = cachedUsers.find(function (u) { return u.id === userId; });
138
186
  var name = user ? (user.displayName || user.username) : "this user";
@@ -435,6 +435,16 @@ function renderSessionItem(s) {
435
435
  })(s.id, s.title, s.cliSessionId, moreBtn, s));
436
436
  el.appendChild(moreBtn);
437
437
 
438
+ // Unread badge
439
+ var unreadBadge = document.createElement("span");
440
+ unreadBadge.className = "session-unread-badge";
441
+ unreadBadge.dataset.sessionId = s.id;
442
+ if (s.unread > 0) {
443
+ unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
444
+ unreadBadge.classList.add("has-unread");
445
+ }
446
+ el.appendChild(unreadBadge);
447
+
438
448
  el.addEventListener("click", (function (id) {
439
449
  return function () {
440
450
  if (ctx.ws && ctx.connected) {
@@ -708,6 +718,13 @@ function renderSheetProjects(listEl) {
708
718
  el.appendChild(dot);
709
719
  }
710
720
 
721
+ if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
722
+ var mBadge = document.createElement("span");
723
+ mBadge.className = "mobile-project-unread";
724
+ mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
725
+ el.appendChild(mBadge);
726
+ }
727
+
711
728
  el.addEventListener("click", function () {
712
729
  if (ctx.switchProject) ctx.switchProject(p.slug);
713
730
  closeMobileSheet();
@@ -1387,6 +1404,74 @@ function hideIconTooltip() {
1387
1404
  }
1388
1405
  }
1389
1406
 
1407
+ // --- DM user context menu ---
1408
+ var userCtxMenu = null;
1409
+
1410
+ function closeUserCtxMenu() {
1411
+ if (userCtxMenu) {
1412
+ userCtxMenu.remove();
1413
+ userCtxMenu = null;
1414
+ }
1415
+ document.removeEventListener("click", handleUserCtxOutsideClick, true);
1416
+ }
1417
+
1418
+ function showUserCtxMenu(anchorEl, user) {
1419
+ closeUserCtxMenu();
1420
+ closeProjectCtxMenu();
1421
+
1422
+ var menu = document.createElement("div");
1423
+ menu.className = "project-ctx-menu";
1424
+
1425
+ var removeItem = document.createElement("button");
1426
+ removeItem.className = "project-ctx-item project-ctx-delete";
1427
+ removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
1428
+ removeItem.addEventListener("click", function (e) {
1429
+ e.stopPropagation();
1430
+ // Spawn dust particles at the user icon position
1431
+ var iconRect = anchorEl.getBoundingClientRect();
1432
+ spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
1433
+ closeUserCtxMenu();
1434
+ // Immediately mark as removed so strip re-render hides the icon,
1435
+ // even if the user was only visible via cachedDmConversations (not favorites)
1436
+ cachedDmRemovedUsers[user.id] = true;
1437
+ if (ctx.onDmRemoveUser) ctx.onDmRemoveUser(user.id);
1438
+ renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers);
1439
+ if (ctx.sendWs) {
1440
+ ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
1441
+ }
1442
+ });
1443
+ menu.appendChild(removeItem);
1444
+
1445
+ document.body.appendChild(menu);
1446
+ userCtxMenu = menu;
1447
+ refreshIcons();
1448
+
1449
+ requestAnimationFrame(function () {
1450
+ var rect = anchorEl.getBoundingClientRect();
1451
+ menu.style.position = "fixed";
1452
+ menu.style.left = (rect.right + 6) + "px";
1453
+ menu.style.top = rect.top + "px";
1454
+ var menuRect = menu.getBoundingClientRect();
1455
+ if (menuRect.right > window.innerWidth - 8) {
1456
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
1457
+ }
1458
+ if (menuRect.bottom > window.innerHeight - 8) {
1459
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
1460
+ }
1461
+ });
1462
+
1463
+ // Close on outside click
1464
+ setTimeout(function () {
1465
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
1466
+ }, 0);
1467
+ }
1468
+
1469
+ function handleUserCtxOutsideClick(e) {
1470
+ if (userCtxMenu && !userCtxMenu.contains(e.target)) {
1471
+ closeUserCtxMenu();
1472
+ }
1473
+ }
1474
+
1390
1475
  // --- Project context menu ---
1391
1476
  var projectCtxMenu = null;
1392
1477
 
@@ -1601,8 +1686,9 @@ function closeProjectCtxMenu() {
1601
1686
  }
1602
1687
  }
1603
1688
 
1604
- function showIconCtxMenu(anchorEl, slug) {
1689
+ function showIconCtxMenu(anchorEl, slug, name) {
1605
1690
  closeProjectCtxMenu();
1691
+ closeUserCtxMenu();
1606
1692
  closeEmojiPicker();
1607
1693
 
1608
1694
  var menu = document.createElement("div");
@@ -1618,6 +1704,24 @@ function showIconCtxMenu(anchorEl, slug) {
1618
1704
  });
1619
1705
  menu.appendChild(iconItem);
1620
1706
 
1707
+ // --- Separator ---
1708
+ var sep = document.createElement("div");
1709
+ sep.className = "project-ctx-separator";
1710
+ menu.appendChild(sep);
1711
+
1712
+ // --- Remove Project ---
1713
+ var removeItem = document.createElement("button");
1714
+ removeItem.className = "project-ctx-item project-ctx-delete";
1715
+ removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
1716
+ removeItem.addEventListener("click", function (e) {
1717
+ e.stopPropagation();
1718
+ closeProjectCtxMenu();
1719
+ if (ctx.ws && ctx.connected) {
1720
+ ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
1721
+ }
1722
+ });
1723
+ menu.appendChild(removeItem);
1724
+
1621
1725
  document.body.appendChild(menu);
1622
1726
  projectCtxMenu = menu;
1623
1727
  refreshIcons();
@@ -1639,6 +1743,7 @@ function showIconCtxMenu(anchorEl, slug) {
1639
1743
 
1640
1744
  function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
1641
1745
  closeProjectCtxMenu();
1746
+ closeUserCtxMenu();
1642
1747
  closeEmojiPicker();
1643
1748
 
1644
1749
  var menu = document.createElement("div");
@@ -1910,10 +2015,6 @@ function showTrashZone() {
1910
2015
  trash.classList.remove("drag-hover");
1911
2016
  var slug = e.dataTransfer.getData("text/plain");
1912
2017
  if (slug && ctx.ws && ctx.connected) {
1913
- // Spawn dust particles at trash position
1914
- var rect = trash.getBoundingClientRect();
1915
- spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
1916
- // Check for tasks before removing
1917
2018
  ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
1918
2019
  }
1919
2020
  });
@@ -1926,7 +2027,7 @@ function hideTrashZone() {
1926
2027
  if (addBtn) addBtn.style.display = "";
1927
2028
  }
1928
2029
 
1929
- function spawnDustParticles(cx, cy) {
2030
+ export function spawnDustParticles(cx, cy) {
1930
2031
  var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
1931
2032
  var count = 24;
1932
2033
  var container = document.createElement("div");
@@ -2113,6 +2214,15 @@ export function renderIconStrip(projects, currentSlug) {
2113
2214
  if (p.isProcessing) statusDot.classList.add("processing");
2114
2215
  el.appendChild(statusDot);
2115
2216
 
2217
+ // Unread badge (top-right)
2218
+ var projectBadge = document.createElement("span");
2219
+ projectBadge.className = "icon-strip-project-badge";
2220
+ if (p.unread > 0 && !isActive) {
2221
+ projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
2222
+ projectBadge.classList.add("has-unread");
2223
+ }
2224
+ el.appendChild(projectBadge);
2225
+
2116
2226
  // Tooltip on hover
2117
2227
  (function (name, elem) {
2118
2228
  elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
@@ -2128,13 +2238,13 @@ export function renderIconStrip(projects, currentSlug) {
2128
2238
  })(p.slug);
2129
2239
 
2130
2240
  // Right-click context menu (icon only)
2131
- (function (slug, elem) {
2241
+ (function (slug, name, elem) {
2132
2242
  elem.addEventListener("contextmenu", function (e) {
2133
2243
  e.preventDefault();
2134
2244
  e.stopPropagation();
2135
- showIconCtxMenu(elem, slug);
2245
+ showIconCtxMenu(elem, slug, name);
2136
2246
  });
2137
- })(p.slug, el);
2247
+ })(p.slug, p.name || p.slug, el);
2138
2248
 
2139
2249
  // Drag-and-drop reordering
2140
2250
  setupDragHandlers(el, p.slug);
@@ -2197,24 +2307,46 @@ export function getEmojiCategories() { return EMOJI_CATEGORIES; }
2197
2307
  // --- User strip (DM targets) ---
2198
2308
  var cachedAllUsers = [];
2199
2309
  var cachedOnlineUserIds = [];
2310
+ var cachedDmFavorites = [];
2311
+ var cachedDmConversations = [];
2312
+ var cachedDmUnread = {};
2313
+ var cachedMyUserId = null;
2200
2314
  var currentDmUserId = null;
2315
+ var dmPickerOpen = false;
2201
2316
 
2202
- export function renderUserStrip(allUsers, onlineUserIds, myUserId) {
2317
+ var cachedDmRemovedUsers = {};
2318
+
2319
+ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers) {
2203
2320
  cachedAllUsers = allUsers || [];
2204
2321
  cachedOnlineUserIds = onlineUserIds || [];
2322
+ cachedDmFavorites = dmFavorites || [];
2323
+ cachedDmConversations = dmConversations || [];
2324
+ cachedDmUnread = dmUnread || {};
2325
+ cachedDmRemovedUsers = dmRemovedUsers || {};
2326
+ cachedMyUserId = myUserId;
2205
2327
  var container = document.getElementById("icon-strip-users");
2206
2328
  if (!container) return;
2207
2329
 
2208
- // Filter out self, only show other users
2209
- var others = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
2330
+ // All other users
2331
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
2210
2332
 
2211
2333
  // Hide section if no other users (single-user mode or alone)
2212
- if (others.length === 0) {
2334
+ if (allOthers.length === 0) {
2213
2335
  container.innerHTML = "";
2214
2336
  container.classList.add("hidden");
2215
2337
  return;
2216
2338
  }
2217
2339
 
2340
+ // Filter to show only: favorites + users with unread + users with DM conversations
2341
+ // But exclude users explicitly removed from favorites
2342
+ var others = allOthers.filter(function (u) {
2343
+ if (cachedDmRemovedUsers[u.id]) return false;
2344
+ if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
2345
+ if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
2346
+ if (cachedDmConversations.indexOf(u.id) !== -1) return true;
2347
+ return false;
2348
+ });
2349
+
2218
2350
  container.classList.remove("hidden");
2219
2351
  container.innerHTML = "";
2220
2352
 
@@ -2254,21 +2386,141 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId) {
2254
2386
  if (ctx.openDm) ctx.openDm(u.id);
2255
2387
  });
2256
2388
 
2389
+ // Right-click: show context menu
2390
+ el.addEventListener("contextmenu", function (e) {
2391
+ e.preventDefault();
2392
+ e.stopPropagation();
2393
+ showUserCtxMenu(el, u);
2394
+ });
2395
+
2257
2396
  container.appendChild(el);
2258
2397
  })(others[i]);
2259
2398
  }
2260
2399
 
2261
- // Invite button at bottom of user strip (hidden for now)
2262
- // var inviteBtn = document.createElement("button");
2263
- // inviteBtn.className = "icon-strip-invite";
2264
- // inviteBtn.innerHTML = iconHtml("user-plus");
2265
- // inviteBtn.addEventListener("click", function () { triggerShare(); });
2266
- // inviteBtn.addEventListener("mouseenter", function () { showIconTooltip(inviteBtn, "Invite"); });
2267
- // inviteBtn.addEventListener("mouseleave", hideIconTooltip);
2268
- // container.appendChild(inviteBtn);
2400
+ // Add user (+) button
2401
+ var addBtn = document.createElement("button");
2402
+ addBtn.className = "icon-strip-invite";
2403
+ addBtn.innerHTML = iconHtml("user-plus");
2404
+ addBtn.addEventListener("click", function (e) {
2405
+ e.stopPropagation();
2406
+ toggleDmUserPicker(addBtn);
2407
+ });
2408
+ addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add DM favorite"); });
2409
+ addBtn.addEventListener("mouseleave", hideIconTooltip);
2410
+ container.appendChild(addBtn);
2269
2411
  refreshIcons();
2270
2412
  }
2271
2413
 
2414
+ function toggleDmUserPicker(anchorEl) {
2415
+ if (dmPickerOpen) {
2416
+ closeDmUserPicker();
2417
+ return;
2418
+ }
2419
+ dmPickerOpen = true;
2420
+
2421
+ var picker = document.createElement("div");
2422
+ picker.className = "dm-user-picker";
2423
+ picker.id = "dm-user-picker";
2424
+
2425
+ // Search input
2426
+ var searchInput = document.createElement("input");
2427
+ searchInput.className = "dm-user-picker-search";
2428
+ searchInput.type = "text";
2429
+ searchInput.placeholder = "Search users...";
2430
+ picker.appendChild(searchInput);
2431
+
2432
+ // Scrollable list
2433
+ var listEl = document.createElement("div");
2434
+ listEl.className = "dm-user-picker-list";
2435
+ picker.appendChild(listEl);
2436
+
2437
+ // Position the picker above the + button
2438
+ document.body.appendChild(picker);
2439
+ var rect = anchorEl.getBoundingClientRect();
2440
+ picker.style.left = (rect.right + 8) + "px";
2441
+ picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
2442
+
2443
+ function renderPickerList(filter) {
2444
+ listEl.innerHTML = "";
2445
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
2446
+ // Exclude already-favorited users
2447
+ var available = allOthers.filter(function (u) {
2448
+ return cachedDmFavorites.indexOf(u.id) === -1;
2449
+ });
2450
+ if (filter) {
2451
+ var lf = filter.toLowerCase();
2452
+ available = available.filter(function (u) {
2453
+ return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
2454
+ (u.username && u.username.toLowerCase().indexOf(lf) !== -1);
2455
+ });
2456
+ }
2457
+ if (available.length === 0) {
2458
+ var emptyEl = document.createElement("div");
2459
+ emptyEl.className = "dm-user-picker-empty";
2460
+ emptyEl.textContent = filter ? "No users found" : "No more users to add";
2461
+ listEl.appendChild(emptyEl);
2462
+ return;
2463
+ }
2464
+ for (var i = 0; i < available.length; i++) {
2465
+ (function (u) {
2466
+ var item = document.createElement("div");
2467
+ item.className = "dm-user-picker-item";
2468
+
2469
+ var av = document.createElement("img");
2470
+ av.className = "dm-user-picker-avatar";
2471
+ av.src = "https://api.dicebear.com/9.x/" + (u.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(u.avatarSeed || u.username) + "&size=28";
2472
+ av.alt = u.displayName;
2473
+ item.appendChild(av);
2474
+
2475
+ var name = document.createElement("span");
2476
+ name.className = "dm-user-picker-name";
2477
+ name.textContent = u.displayName;
2478
+ item.appendChild(name);
2479
+
2480
+ item.addEventListener("click", function () {
2481
+ if (ctx.sendWs) {
2482
+ ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
2483
+ }
2484
+ closeDmUserPicker();
2485
+ });
2486
+
2487
+ listEl.appendChild(item);
2488
+ })(available[i]);
2489
+ }
2490
+ }
2491
+
2492
+ renderPickerList("");
2493
+ searchInput.addEventListener("input", function () {
2494
+ renderPickerList(searchInput.value);
2495
+ });
2496
+
2497
+ // Focus search
2498
+ setTimeout(function () { searchInput.focus(); }, 50);
2499
+
2500
+ // Close on click outside
2501
+ function onDocClick(e) {
2502
+ if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
2503
+ closeDmUserPicker();
2504
+ document.removeEventListener("click", onDocClick, true);
2505
+ }
2506
+ }
2507
+ setTimeout(function () {
2508
+ document.addEventListener("click", onDocClick, true);
2509
+ }, 10);
2510
+ picker._docClickHandler = onDocClick;
2511
+ }
2512
+
2513
+ export function closeDmUserPicker() {
2514
+ dmPickerOpen = false;
2515
+ var picker = document.getElementById("dm-user-picker");
2516
+ if (picker) {
2517
+ if (picker._docClickHandler) {
2518
+ document.removeEventListener("click", picker._docClickHandler, true);
2519
+ }
2520
+ picker.remove();
2521
+ }
2522
+ }
2523
+
2272
2524
  export function setCurrentDmUser(userId) {
2273
2525
  currentDmUserId = userId;
2274
2526
  // Update active state on user icons immediately
@@ -2296,6 +2548,32 @@ export function updateDmBadge(userId, count) {
2296
2548
  }
2297
2549
  }
2298
2550
 
2551
+ export function updateSessionBadge(sessionId, count) {
2552
+ var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
2553
+ if (!badge) return;
2554
+ if (count > 0) {
2555
+ badge.textContent = count > 99 ? "99+" : String(count);
2556
+ badge.classList.add("has-unread");
2557
+ } else {
2558
+ badge.textContent = "";
2559
+ badge.classList.remove("has-unread");
2560
+ }
2561
+ }
2562
+
2563
+ export function updateProjectBadge(slug, count) {
2564
+ var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
2565
+ if (!icon) return;
2566
+ var badge = icon.querySelector(".icon-strip-project-badge");
2567
+ if (!badge) return;
2568
+ if (count > 0) {
2569
+ badge.textContent = count > 99 ? "99+" : String(count);
2570
+ badge.classList.add("has-unread");
2571
+ } else {
2572
+ badge.textContent = "";
2573
+ badge.classList.remove("has-unread");
2574
+ }
2575
+ }
2576
+
2299
2577
  export function initIconStrip(_ctx) {
2300
2578
  var addBtn = document.getElementById("icon-strip-add");
2301
2579
  if (addBtn) {
@@ -156,7 +156,7 @@ function dismissOnboarding() {
156
156
 
157
157
  // --- Visibility ---
158
158
 
159
- function showNotes() {
159
+ export function showNotes() {
160
160
  notesVisible = true;
161
161
  var container = document.getElementById("sticky-notes-container");
162
162
  var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
@@ -164,7 +164,7 @@ function showNotes() {
164
164
  if (toggleBtn) toggleBtn.classList.add("active");
165
165
  }
166
166
 
167
- function hideNotes() {
167
+ export function hideNotes() {
168
168
  notesVisible = false;
169
169
  var container = document.getElementById("sticky-notes-container");
170
170
  var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
@@ -260,7 +260,7 @@ function renderMiniMarkdown(text) {
260
260
 
261
261
  function syncTitle(noteEl, text) {
262
262
  var spacer = noteEl.querySelector(".sticky-note-spacer");
263
- if (spacer) spacer.textContent = getTitle(text);
263
+ if (spacer) spacer.textContent = getTitle(text) || "Untitled";
264
264
  }
265
265
 
266
266
  // --- HTML-to-Markdown reverse conversion (for contenteditable) ---
@@ -356,7 +356,7 @@ function renderNote(data) {
356
356
 
357
357
  var spacer = document.createElement("div");
358
358
  spacer.className = "sticky-note-spacer";
359
- spacer.textContent = getTitle(data.text);
359
+ spacer.textContent = getTitle(data.text) || "Untitled";
360
360
  header.appendChild(spacer);
361
361
 
362
362
  var addBtn = document.createElement("button");
@@ -644,7 +644,25 @@ function showFormatToolbar(rendered) {
644
644
  if (!sel.toString().trim()) return;
645
645
 
646
646
  var range = sel.getRangeAt(0);
647
- if (!rendered.contains(range.commonAncestorContainer)) return;
647
+ // When dragging outside the note, commonAncestorContainer may be a parent
648
+ // of rendered. Clamp the range to the rendered element so the toolbar shows.
649
+ if (!rendered.contains(range.commonAncestorContainer)) {
650
+ try {
651
+ var clampedRange = range.cloneRange();
652
+ if (range.startContainer === rendered || rendered.contains(range.startContainer)) {
653
+ clampedRange.selectNodeContents(rendered);
654
+ clampedRange.setStart(range.startContainer, range.startOffset);
655
+ } else if (range.endContainer === rendered || rendered.contains(range.endContainer)) {
656
+ clampedRange.selectNodeContents(rendered);
657
+ clampedRange.setEnd(range.endContainer, range.endOffset);
658
+ } else {
659
+ return;
660
+ }
661
+ range = clampedRange;
662
+ } catch (e) {
663
+ return;
664
+ }
665
+ }
648
666
 
649
667
  var toolbar = document.createElement("div");
650
668
  toolbar.className = "sn-format-toolbar";
@@ -1251,3 +1269,7 @@ export function closeArchive() {
1251
1269
  export function isArchiveOpen() {
1252
1270
  return archiveOpen;
1253
1271
  }
1272
+
1273
+ export function isNotesVisible() {
1274
+ return notesVisible;
1275
+ }