synabun 1.3.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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Memory Explorer
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Docked sidebar that organizes memories by parent → child
|
|
6
|
+
// category hierarchy. Slides in from the left and pushes
|
|
7
|
+
// the main content area to the right via --explorer-width.
|
|
8
|
+
|
|
9
|
+
import { state, emit, on } from './state.js';
|
|
10
|
+
import { catColor } from './colors.js';
|
|
11
|
+
import { openMemoryCard } from './ui-detail.js';
|
|
12
|
+
import { truncate } from './utils.js';
|
|
13
|
+
import { scheduleGraphRemoval, cancelScheduledRemoval, buildCategorySidebar, openColorPicker, editCategoryUI, deleteCategoryUI } from './ui-sidebar.js';
|
|
14
|
+
import { updateCategory as apiUpdateCategory } from './api.js';
|
|
15
|
+
import { KEYS } from './constants.js';
|
|
16
|
+
import { registerAction } from './ui-keybinds.js';
|
|
17
|
+
import { storage } from './storage.js';
|
|
18
|
+
import { isGuest, hasPermission } from './ui-sync.js';
|
|
19
|
+
|
|
20
|
+
const $ = (id) => document.getElementById(id);
|
|
21
|
+
|
|
22
|
+
/** True when guest and memories permission is disabled */
|
|
23
|
+
function guestReadOnly() { return isGuest() && !hasPermission('memories'); }
|
|
24
|
+
|
|
25
|
+
// ─── Local state ────────────────────────
|
|
26
|
+
let _sortMode = 'newest'; // 'newest' | 'oldest' | 'alpha'
|
|
27
|
+
let _collapsed = new Set(); // category names that are collapsed
|
|
28
|
+
let _filterText = '';
|
|
29
|
+
let _visible = false;
|
|
30
|
+
let _width = 300; // sidebar width in px
|
|
31
|
+
const MIN_WIDTH = 280;
|
|
32
|
+
const MAX_WIDTH = 600;
|
|
33
|
+
|
|
34
|
+
// ─── SVG icons ────────────────────────
|
|
35
|
+
const EYE_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M2.06 12c.86-2.15 3.64-7 9.94-7s9.08 4.85 9.94 7c-.86 2.15-3.64 7-9.94 7s-9.08-4.85-9.94-7z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
36
|
+
const EYE_SLASH = '<svg viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2l20 20"/><path d="M6.71 6.71C3.94 8.7 2.5 11.27 2.06 12c.86 2.15 3.64 7 9.94 7 2.08 0 3.82-.6 5.23-1.49"/><path d="M10 5.07A9.77 9.77 0 0 1 12 5c6.3 0 9.08 4.85 9.94 7-.35.87-.85 1.86-1.65 2.82"/><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/></svg>';
|
|
37
|
+
const SVG_PENCIL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>';
|
|
38
|
+
const SVG_TRASH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-.7 12.1a2 2 0 0 1-2 1.9H7.7a2 2 0 0 1-2-1.9L5 6"/></svg>';
|
|
39
|
+
|
|
40
|
+
// ─── Context menu singleton ──────────────
|
|
41
|
+
let _activeContextMenu = null;
|
|
42
|
+
|
|
43
|
+
function dismissContextMenu() {
|
|
44
|
+
if (_activeContextMenu) {
|
|
45
|
+
_activeContextMenu.remove();
|
|
46
|
+
_activeContextMenu = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function showCategoryContextMenu(btnEl, item, isParent, eyeState) {
|
|
51
|
+
dismissContextMenu();
|
|
52
|
+
|
|
53
|
+
const menu = document.createElement('div');
|
|
54
|
+
menu.className = 'explorer-ctx-menu';
|
|
55
|
+
_activeContextMenu = menu;
|
|
56
|
+
|
|
57
|
+
const isActive = state.activeCategories.has(item.name);
|
|
58
|
+
|
|
59
|
+
// ── Toggle visibility ──
|
|
60
|
+
const toggleItem = document.createElement('button');
|
|
61
|
+
toggleItem.className = 'explorer-ctx-item';
|
|
62
|
+
toggleItem.innerHTML = `<span class="explorer-ctx-icon">${isActive ? EYE_SLASH : EYE_OPEN}</span><span>${isActive ? 'Hide from graph' : 'Show on graph'}</span>`;
|
|
63
|
+
toggleItem.addEventListener('click', () => {
|
|
64
|
+
dismissContextMenu();
|
|
65
|
+
eyeState.toggle();
|
|
66
|
+
});
|
|
67
|
+
menu.appendChild(toggleItem);
|
|
68
|
+
|
|
69
|
+
// ── Edit category ──
|
|
70
|
+
const editItem = document.createElement('button');
|
|
71
|
+
editItem.className = 'explorer-ctx-item';
|
|
72
|
+
editItem.innerHTML = `<span class="explorer-ctx-icon">${SVG_PENCIL}</span><span>Edit category</span>`;
|
|
73
|
+
if (guestReadOnly()) {
|
|
74
|
+
editItem.disabled = true;
|
|
75
|
+
editItem.style.opacity = '0.35';
|
|
76
|
+
editItem.style.pointerEvents = 'none';
|
|
77
|
+
} else {
|
|
78
|
+
editItem.addEventListener('click', () => {
|
|
79
|
+
dismissContextMenu();
|
|
80
|
+
editCategoryUI(item.name);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
menu.appendChild(editItem);
|
|
84
|
+
|
|
85
|
+
// ── Separator ──
|
|
86
|
+
const sep = document.createElement('div');
|
|
87
|
+
sep.className = 'explorer-ctx-sep';
|
|
88
|
+
menu.appendChild(sep);
|
|
89
|
+
|
|
90
|
+
// ── Delete category ──
|
|
91
|
+
const memCount = countMemories(item);
|
|
92
|
+
const deleteItem = document.createElement('button');
|
|
93
|
+
deleteItem.className = 'explorer-ctx-item explorer-ctx-item--danger';
|
|
94
|
+
deleteItem.innerHTML = `<span class="explorer-ctx-icon">${SVG_TRASH}</span><span>Delete category</span>`;
|
|
95
|
+
if (guestReadOnly()) {
|
|
96
|
+
deleteItem.disabled = true;
|
|
97
|
+
deleteItem.style.opacity = '0.35';
|
|
98
|
+
deleteItem.style.pointerEvents = 'none';
|
|
99
|
+
} else {
|
|
100
|
+
deleteItem.addEventListener('click', () => {
|
|
101
|
+
dismissContextMenu();
|
|
102
|
+
deleteCategoryUI(item.name, memCount);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
menu.appendChild(deleteItem);
|
|
106
|
+
|
|
107
|
+
// Position relative to the button
|
|
108
|
+
document.body.appendChild(menu);
|
|
109
|
+
const btnRect = btnEl.getBoundingClientRect();
|
|
110
|
+
let top = btnRect.bottom + 4;
|
|
111
|
+
let left = btnRect.right - menu.offsetWidth;
|
|
112
|
+
|
|
113
|
+
// Keep within viewport
|
|
114
|
+
if (top + menu.offsetHeight > window.innerHeight - 8) {
|
|
115
|
+
top = btnRect.top - menu.offsetHeight - 4;
|
|
116
|
+
}
|
|
117
|
+
if (left < 8) left = 8;
|
|
118
|
+
|
|
119
|
+
menu.style.top = top + 'px';
|
|
120
|
+
menu.style.left = left + 'px';
|
|
121
|
+
|
|
122
|
+
// Dismiss on outside click (next tick so the current click doesn't trigger it)
|
|
123
|
+
requestAnimationFrame(() => {
|
|
124
|
+
const dismiss = (e) => {
|
|
125
|
+
if (!menu.contains(e.target)) {
|
|
126
|
+
dismissContextMenu();
|
|
127
|
+
document.removeEventListener('pointerdown', dismiss, true);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
document.addEventListener('pointerdown', dismiss, true);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ═══════════════════════════════════════════
|
|
135
|
+
// PERSISTENCE
|
|
136
|
+
// ═══════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
function loadPersistedState() {
|
|
139
|
+
try {
|
|
140
|
+
const raw = storage.getItem(KEYS.EXPLORER_COLLAPSED);
|
|
141
|
+
if (raw) _collapsed = new Set(JSON.parse(raw));
|
|
142
|
+
} catch {}
|
|
143
|
+
try {
|
|
144
|
+
const s = storage.getItem(KEYS.EXPLORER_SORT);
|
|
145
|
+
if (s && ['newest', 'oldest', 'alpha'].includes(s)) _sortMode = s;
|
|
146
|
+
} catch {}
|
|
147
|
+
try {
|
|
148
|
+
const v = storage.getItem(KEYS.EXPLORER_VISIBLE);
|
|
149
|
+
if (v === 'true') _visible = true;
|
|
150
|
+
} catch {}
|
|
151
|
+
try {
|
|
152
|
+
const w = parseInt(storage.getItem(KEYS.EXPLORER_WIDTH), 10);
|
|
153
|
+
if (w >= MIN_WIDTH && w <= MAX_WIDTH) _width = w;
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function saveCollapsed() {
|
|
158
|
+
try { storage.setItem(KEYS.EXPLORER_COLLAPSED, JSON.stringify([..._collapsed])); } catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function saveSort() {
|
|
162
|
+
try { storage.setItem(KEYS.EXPLORER_SORT, _sortMode); } catch {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function saveVisible() {
|
|
166
|
+
try { storage.setItem(KEYS.EXPLORER_VISIBLE, String(_visible)); } catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function saveWidth() {
|
|
170
|
+
try { storage.setItem(KEYS.EXPLORER_WIDTH, String(_width)); } catch {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Explorer category order persistence ──
|
|
174
|
+
function getExplorerCatOrder() {
|
|
175
|
+
try { return JSON.parse(storage.getItem(KEYS.EXPLORER_CAT_ORDER) || '{}'); }
|
|
176
|
+
catch { return {}; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function saveExplorerCatOrder(order) {
|
|
180
|
+
try { storage.setItem(KEYS.EXPLORER_CAT_ORDER, JSON.stringify(order)); }
|
|
181
|
+
catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Sort tree items using saved order, appending unseen items alphabetically.
|
|
186
|
+
*/
|
|
187
|
+
function applySavedOrder(items, savedOrder) {
|
|
188
|
+
if (!savedOrder || !savedOrder.length) return items;
|
|
189
|
+
const inOrder = savedOrder.filter(n => items.some(i => i.name === n));
|
|
190
|
+
const ordered = inOrder.map(n => items.find(i => i.name === n));
|
|
191
|
+
const remaining = items.filter(i => !savedOrder.includes(i.name));
|
|
192
|
+
return [...ordered, ...remaining];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// ═══════════════════════════════════════════
|
|
197
|
+
// CSS VARIABLE — drives all layout shifts
|
|
198
|
+
// ═══════════════════════════════════════════
|
|
199
|
+
|
|
200
|
+
function applyExplorerWidth() {
|
|
201
|
+
const w = _visible ? `${_width}px` : '0px';
|
|
202
|
+
document.documentElement.style.setProperty('--explorer-width', w);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════
|
|
207
|
+
// SORT HELPERS
|
|
208
|
+
// ═══════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
function sortNodes(nodes) {
|
|
211
|
+
const sorted = [...nodes];
|
|
212
|
+
switch (_sortMode) {
|
|
213
|
+
case 'newest':
|
|
214
|
+
sorted.sort((a, b) => {
|
|
215
|
+
const da = a.payload.updated_at || a.payload.created_at || '';
|
|
216
|
+
const db = b.payload.updated_at || b.payload.created_at || '';
|
|
217
|
+
return db.localeCompare(da);
|
|
218
|
+
});
|
|
219
|
+
break;
|
|
220
|
+
case 'oldest':
|
|
221
|
+
sorted.sort((a, b) => {
|
|
222
|
+
const da = a.payload.created_at || '';
|
|
223
|
+
const db = b.payload.created_at || '';
|
|
224
|
+
return da.localeCompare(db);
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
case 'alpha':
|
|
228
|
+
sorted.sort((a, b) => {
|
|
229
|
+
const la = getMemoryLabel(a).toLowerCase();
|
|
230
|
+
const lb = getMemoryLabel(b).toLowerCase();
|
|
231
|
+
return la.localeCompare(lb);
|
|
232
|
+
});
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
return sorted;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getMemoryLabel(node) {
|
|
239
|
+
const content = node.payload.content || '';
|
|
240
|
+
// Use first meaningful line
|
|
241
|
+
const lines = content.split('\n');
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
const trimmed = line.replace(/^#+\s*/, '').trim();
|
|
244
|
+
if (trimmed) return trimmed;
|
|
245
|
+
}
|
|
246
|
+
return node.id.slice(0, 8);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cycleSortMode() {
|
|
250
|
+
const modes = ['newest', 'oldest', 'alpha'];
|
|
251
|
+
const idx = modes.indexOf(_sortMode);
|
|
252
|
+
_sortMode = modes[(idx + 1) % modes.length];
|
|
253
|
+
saveSort();
|
|
254
|
+
|
|
255
|
+
// Update tooltip
|
|
256
|
+
const sortBtn = $('explorer-sort-btn');
|
|
257
|
+
if (sortBtn) {
|
|
258
|
+
const labels = { newest: 'Sort: newest first', oldest: 'Sort: oldest first', alpha: 'Sort: A-Z' };
|
|
259
|
+
sortBtn.setAttribute('data-tooltip', labels[_sortMode]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
buildTree();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
// ═══════════════════════════════════════════
|
|
267
|
+
// TREE DATA
|
|
268
|
+
// ═══════════════════════════════════════════
|
|
269
|
+
|
|
270
|
+
function buildTreeData() {
|
|
271
|
+
const saved = getExplorerCatOrder();
|
|
272
|
+
|
|
273
|
+
// Group memories by category
|
|
274
|
+
const byCat = {};
|
|
275
|
+
for (const node of state.allNodes) {
|
|
276
|
+
const cat = node.payload.category || 'uncategorized';
|
|
277
|
+
if (!byCat[cat]) byCat[cat] = [];
|
|
278
|
+
byCat[cat].push(node);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const tree = [];
|
|
282
|
+
const usedCats = new Set();
|
|
283
|
+
|
|
284
|
+
// Parent categories first
|
|
285
|
+
for (const [name, meta] of Object.entries(state.categoryMetadata)) {
|
|
286
|
+
if (!meta.is_parent) continue;
|
|
287
|
+
|
|
288
|
+
const children = [];
|
|
289
|
+
for (const [cName, cMeta] of Object.entries(state.categoryMetadata)) {
|
|
290
|
+
if (cMeta.parent !== name) continue;
|
|
291
|
+
usedCats.add(cName);
|
|
292
|
+
children.push({
|
|
293
|
+
type: 'category',
|
|
294
|
+
name: cName,
|
|
295
|
+
meta: cMeta,
|
|
296
|
+
memories: sortNodes(byCat[cName] || []),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// Apply saved child order (fallback: alphabetical)
|
|
300
|
+
const childOrder = (saved.children || {})[name];
|
|
301
|
+
if (childOrder && childOrder.length) {
|
|
302
|
+
children.splice(0, children.length, ...applySavedOrder(children, childOrder));
|
|
303
|
+
} else {
|
|
304
|
+
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
usedCats.add(name);
|
|
308
|
+
tree.push({
|
|
309
|
+
type: 'parent',
|
|
310
|
+
name,
|
|
311
|
+
meta,
|
|
312
|
+
children,
|
|
313
|
+
memories: sortNodes(byCat[name] || []),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Standalone categories (no parent, not a parent)
|
|
318
|
+
for (const [name, meta] of Object.entries(state.categoryMetadata)) {
|
|
319
|
+
if (usedCats.has(name)) continue;
|
|
320
|
+
usedCats.add(name);
|
|
321
|
+
tree.push({
|
|
322
|
+
type: 'category',
|
|
323
|
+
name,
|
|
324
|
+
meta,
|
|
325
|
+
memories: sortNodes(byCat[name] || []),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Orphan memories (category not in metadata)
|
|
330
|
+
for (const [cat, nodes] of Object.entries(byCat)) {
|
|
331
|
+
if (usedCats.has(cat)) continue;
|
|
332
|
+
tree.push({
|
|
333
|
+
type: 'category',
|
|
334
|
+
name: cat,
|
|
335
|
+
meta: { color: null, is_parent: false },
|
|
336
|
+
memories: sortNodes(nodes),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Apply saved top-level order (fallback: alphabetical)
|
|
341
|
+
if (saved.topLevel && saved.topLevel.length) {
|
|
342
|
+
tree.splice(0, tree.length, ...applySavedOrder(tree, saved.topLevel));
|
|
343
|
+
} else {
|
|
344
|
+
tree.sort((a, b) => a.name.localeCompare(b.name));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return tree;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
// ═══════════════════════════════════════════
|
|
352
|
+
// FILTER
|
|
353
|
+
// ═══════════════════════════════════════════
|
|
354
|
+
|
|
355
|
+
function matchesFilter(node) {
|
|
356
|
+
if (!_filterText) return true;
|
|
357
|
+
const q = _filterText.toLowerCase();
|
|
358
|
+
const p = node.payload;
|
|
359
|
+
if ((p.content || '').toLowerCase().includes(q)) return true;
|
|
360
|
+
if ((p.category || '').toLowerCase().includes(q)) return true;
|
|
361
|
+
if (p.tags && p.tags.some(t => t.toLowerCase().includes(q))) return true;
|
|
362
|
+
if ((p.subcategory || '').toLowerCase().includes(q)) return true;
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function countMemories(treeItem) {
|
|
367
|
+
let count = 0;
|
|
368
|
+
if (treeItem.memories) count += treeItem.memories.length;
|
|
369
|
+
if (treeItem.children) {
|
|
370
|
+
for (const child of treeItem.children) count += countMemories(child);
|
|
371
|
+
}
|
|
372
|
+
return count;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function countFilteredMemories(treeItem) {
|
|
376
|
+
let count = 0;
|
|
377
|
+
if (treeItem.memories) count += treeItem.memories.filter(matchesFilter).length;
|
|
378
|
+
if (treeItem.children) {
|
|
379
|
+
for (const child of treeItem.children) count += countFilteredMemories(child);
|
|
380
|
+
}
|
|
381
|
+
return count;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
// ═══════════════════════════════════════════
|
|
386
|
+
// DOM RENDERING
|
|
387
|
+
// ═══════════════════════════════════════════
|
|
388
|
+
|
|
389
|
+
function buildTree() {
|
|
390
|
+
const container = $('explorer-tree');
|
|
391
|
+
if (!container) return;
|
|
392
|
+
|
|
393
|
+
const treeData = buildTreeData();
|
|
394
|
+
container.innerHTML = '';
|
|
395
|
+
|
|
396
|
+
let totalMemories = 0;
|
|
397
|
+
let visibleMemories = 0;
|
|
398
|
+
|
|
399
|
+
for (const item of treeData) {
|
|
400
|
+
const { el, total, visible } = renderTreeItem(item, 0);
|
|
401
|
+
if (el) container.appendChild(el);
|
|
402
|
+
totalMemories += total;
|
|
403
|
+
visibleMemories += visible;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Update footer
|
|
407
|
+
const countEl = $('explorer-count');
|
|
408
|
+
if (countEl) {
|
|
409
|
+
if (_filterText) {
|
|
410
|
+
countEl.textContent = `${visibleMemories} / ${totalMemories} memories`;
|
|
411
|
+
} else {
|
|
412
|
+
countEl.textContent = `${totalMemories} memories`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Highlight selected node
|
|
417
|
+
if (state.selectedNodeId) {
|
|
418
|
+
highlightNode(state.selectedNodeId, false);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Wire drag-and-drop (only once per container element)
|
|
422
|
+
if (!container._dragWired) {
|
|
423
|
+
initExplorerDrag(container);
|
|
424
|
+
container._dragWired = true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderTreeItem(item, depth) {
|
|
429
|
+
const isParent = item.type === 'parent';
|
|
430
|
+
|
|
431
|
+
// For categories, check if any memories match filter
|
|
432
|
+
const filteredMemories = item.memories ? item.memories.filter(matchesFilter) : [];
|
|
433
|
+
const filteredChildCount = item.children
|
|
434
|
+
? item.children.reduce((sum, c) => sum + countFilteredMemories(c), 0)
|
|
435
|
+
: 0;
|
|
436
|
+
const totalFiltered = filteredMemories.length + filteredChildCount;
|
|
437
|
+
|
|
438
|
+
const totalMemories = countMemories(item);
|
|
439
|
+
|
|
440
|
+
// Skip if filter active and nothing matches
|
|
441
|
+
if (_filterText && totalFiltered === 0) {
|
|
442
|
+
return { el: null, total: totalMemories, visible: 0 };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const el = document.createElement('div');
|
|
446
|
+
el.className = `explorer-node ${isParent ? 'explorer-parent' : 'explorer-category'}`;
|
|
447
|
+
el.dataset.cat = item.name;
|
|
448
|
+
|
|
449
|
+
const expanded = !_collapsed.has(item.name);
|
|
450
|
+
el.dataset.expanded = String(expanded);
|
|
451
|
+
|
|
452
|
+
// Category row
|
|
453
|
+
const row = document.createElement('div');
|
|
454
|
+
row.className = 'explorer-row';
|
|
455
|
+
row.style.paddingLeft = `${10 + depth * 16}px`;
|
|
456
|
+
|
|
457
|
+
// Chevron
|
|
458
|
+
const hasContent = (filteredMemories.length > 0) ||
|
|
459
|
+
(item.children && item.children.some(c => !_filterText || countFilteredMemories(c) > 0));
|
|
460
|
+
const chevron = document.createElement('span');
|
|
461
|
+
chevron.className = 'explorer-chevron';
|
|
462
|
+
chevron.textContent = hasContent ? '\u25B6' : '';
|
|
463
|
+
row.appendChild(chevron);
|
|
464
|
+
|
|
465
|
+
// Color dot (click to change color)
|
|
466
|
+
const dot = document.createElement('span');
|
|
467
|
+
dot.className = 'explorer-dot';
|
|
468
|
+
const color = catColor(item.name);
|
|
469
|
+
dot.style.background = color;
|
|
470
|
+
dot.setAttribute('data-tooltip', 'Color');
|
|
471
|
+
dot.setAttribute('data-tooltip-pos', 'right');
|
|
472
|
+
dot.style.cursor = 'pointer';
|
|
473
|
+
dot.addEventListener('click', (e) => {
|
|
474
|
+
e.stopPropagation();
|
|
475
|
+
openColorPicker(item.name, el);
|
|
476
|
+
});
|
|
477
|
+
row.appendChild(dot);
|
|
478
|
+
|
|
479
|
+
// Label
|
|
480
|
+
const label = document.createElement('span');
|
|
481
|
+
label.className = 'explorer-label';
|
|
482
|
+
label.textContent = item.name;
|
|
483
|
+
label.setAttribute('data-tooltip', item.meta?.description || item.name);
|
|
484
|
+
label.setAttribute('data-tooltip-pos', 'right');
|
|
485
|
+
row.appendChild(label);
|
|
486
|
+
|
|
487
|
+
// Badge (count)
|
|
488
|
+
const badge = document.createElement('span');
|
|
489
|
+
badge.className = 'explorer-badge';
|
|
490
|
+
if (_filterText) {
|
|
491
|
+
badge.textContent = `${totalFiltered}/${totalMemories}`;
|
|
492
|
+
} else {
|
|
493
|
+
badge.textContent = String(totalMemories);
|
|
494
|
+
}
|
|
495
|
+
row.appendChild(badge);
|
|
496
|
+
|
|
497
|
+
// Visibility state indicator (dimmed row when hidden)
|
|
498
|
+
const isActive = state.activeCategories.has(item.name);
|
|
499
|
+
if (!isActive) el.classList.add('explorer-hidden-cat');
|
|
500
|
+
|
|
501
|
+
// "..." context menu button
|
|
502
|
+
const moreBtn = document.createElement('button');
|
|
503
|
+
moreBtn.className = 'explorer-more';
|
|
504
|
+
moreBtn.textContent = '\u22EF'; // ⋯
|
|
505
|
+
moreBtn.setAttribute('data-tooltip', 'Options');
|
|
506
|
+
moreBtn.setAttribute('data-tooltip-pos', 'left');
|
|
507
|
+
moreBtn.addEventListener('click', (e) => {
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
const eyeState = {
|
|
510
|
+
toggle: () => {
|
|
511
|
+
const catName = item.name;
|
|
512
|
+
const childNames = isParent
|
|
513
|
+
? Object.entries(state.categoryMetadata)
|
|
514
|
+
.filter(([, m]) => m.parent === catName)
|
|
515
|
+
.map(([n]) => n)
|
|
516
|
+
: [];
|
|
517
|
+
const targets = [catName, ...childNames];
|
|
518
|
+
|
|
519
|
+
if (state.activeCategories.has(catName)) {
|
|
520
|
+
targets.forEach(c => state.activeCategories.delete(c));
|
|
521
|
+
el.classList.add('explorer-hidden-cat');
|
|
522
|
+
scheduleGraphRemoval();
|
|
523
|
+
} else {
|
|
524
|
+
targets.forEach(c => state.activeCategories.add(c));
|
|
525
|
+
el.classList.remove('explorer-hidden-cat');
|
|
526
|
+
cancelScheduledRemoval();
|
|
527
|
+
emit('graph:refresh');
|
|
528
|
+
}
|
|
529
|
+
emit('categories-changed');
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
showCategoryContextMenu(moreBtn, item, isParent, eyeState);
|
|
533
|
+
});
|
|
534
|
+
row.appendChild(moreBtn);
|
|
535
|
+
|
|
536
|
+
el.appendChild(row);
|
|
537
|
+
|
|
538
|
+
// Click to toggle collapse
|
|
539
|
+
row.addEventListener('click', (e) => {
|
|
540
|
+
if (e.target.closest('.explorer-more')) return;
|
|
541
|
+
if (e.target.closest('.explorer-dot')) return;
|
|
542
|
+
e.stopPropagation();
|
|
543
|
+
const wasExpanded = el.dataset.expanded === 'true';
|
|
544
|
+
el.dataset.expanded = String(!wasExpanded);
|
|
545
|
+
if (wasExpanded) {
|
|
546
|
+
_collapsed.add(item.name);
|
|
547
|
+
} else {
|
|
548
|
+
_collapsed.delete(item.name);
|
|
549
|
+
}
|
|
550
|
+
saveCollapsed();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Children container
|
|
554
|
+
const childrenEl = document.createElement('div');
|
|
555
|
+
childrenEl.className = 'explorer-children';
|
|
556
|
+
|
|
557
|
+
let visibleCount = 0;
|
|
558
|
+
|
|
559
|
+
// Render child categories
|
|
560
|
+
if (item.children) {
|
|
561
|
+
for (const child of item.children) {
|
|
562
|
+
const { el: childEl, visible } = renderTreeItem(child, depth + 1);
|
|
563
|
+
if (childEl) childrenEl.appendChild(childEl);
|
|
564
|
+
visibleCount += visible;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Render memories directly in this category
|
|
569
|
+
for (const mem of filteredMemories) {
|
|
570
|
+
const memEl = renderMemoryItem(mem, depth + 1);
|
|
571
|
+
childrenEl.appendChild(memEl);
|
|
572
|
+
visibleCount++;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
el.appendChild(childrenEl);
|
|
576
|
+
|
|
577
|
+
return { el, total: totalMemories, visible: visibleCount };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderMemoryItem(node, depth) {
|
|
581
|
+
const el = document.createElement('div');
|
|
582
|
+
el.className = 'explorer-node explorer-memory';
|
|
583
|
+
el.dataset.id = node.id;
|
|
584
|
+
|
|
585
|
+
if (node.id === state.selectedNodeId) {
|
|
586
|
+
el.classList.add('selected');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const row = document.createElement('div');
|
|
590
|
+
row.className = 'explorer-row';
|
|
591
|
+
row.style.paddingLeft = `${10 + depth * 16}px`;
|
|
592
|
+
|
|
593
|
+
// Memory icon (small dot/circle)
|
|
594
|
+
const icon = document.createElement('span');
|
|
595
|
+
icon.className = 'explorer-memory-icon';
|
|
596
|
+
icon.textContent = '\u25CB'; // ○
|
|
597
|
+
row.appendChild(icon);
|
|
598
|
+
|
|
599
|
+
// Label (first line of content)
|
|
600
|
+
const label = document.createElement('span');
|
|
601
|
+
label.className = 'explorer-label';
|
|
602
|
+
const preview = getMemoryLabel(node);
|
|
603
|
+
label.textContent = truncate(preview, 60);
|
|
604
|
+
const created = node.payload.created_at ? new Date(node.payload.created_at).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';
|
|
605
|
+
row.setAttribute('data-tooltip', created ? `${preview}\n${created}` : preview);
|
|
606
|
+
row.setAttribute('data-tooltip-pos', 'right');
|
|
607
|
+
row.appendChild(label);
|
|
608
|
+
|
|
609
|
+
// Importance indicator
|
|
610
|
+
if (node.payload.importance && node.payload.importance >= 5) {
|
|
611
|
+
const imp = document.createElement('span');
|
|
612
|
+
imp.className = 'explorer-importance';
|
|
613
|
+
const val = node.payload.importance;
|
|
614
|
+
imp.textContent = '\u25CF'.repeat(Math.min(val, 10));
|
|
615
|
+
imp.setAttribute('data-tooltip', `Importance: ${val}/10`);
|
|
616
|
+
imp.setAttribute('data-tooltip-pos', 'right');
|
|
617
|
+
if (val >= 8) imp.classList.add('explorer-importance--high');
|
|
618
|
+
row.appendChild(imp);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
el.appendChild(row);
|
|
622
|
+
|
|
623
|
+
// Click to open detail panel + navigate graph
|
|
624
|
+
row.addEventListener('click', (e) => {
|
|
625
|
+
e.stopPropagation();
|
|
626
|
+
state.selectedNodeId = node.id;
|
|
627
|
+
openMemoryCard(node);
|
|
628
|
+
emit('graph:navigate', { node, zoom: 'close' });
|
|
629
|
+
emit('node-selected', node);
|
|
630
|
+
highlightNode(node.id, false);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Drag to terminal — feed memory as context to CLI
|
|
634
|
+
if (!guestReadOnly()) {
|
|
635
|
+
el.draggable = true;
|
|
636
|
+
el.addEventListener('dragstart', (e) => {
|
|
637
|
+
e.dataTransfer.setData('application/x-synabun-memory', node.id);
|
|
638
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
639
|
+
el.classList.add('dragging');
|
|
640
|
+
});
|
|
641
|
+
el.addEventListener('dragend', () => {
|
|
642
|
+
el.classList.remove('dragging');
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return el;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
// ═══════════════════════════════════════════
|
|
651
|
+
// HIGHLIGHT / SCROLL-INTO-VIEW
|
|
652
|
+
// ═══════════════════════════════════════════
|
|
653
|
+
|
|
654
|
+
function highlightNode(nodeId, scrollIntoView = true) {
|
|
655
|
+
const container = $('explorer-tree');
|
|
656
|
+
if (!container) return;
|
|
657
|
+
|
|
658
|
+
// Remove previous highlight
|
|
659
|
+
container.querySelectorAll('.explorer-memory.selected').forEach(el => {
|
|
660
|
+
el.classList.remove('selected');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (!nodeId) return;
|
|
664
|
+
|
|
665
|
+
// Find and highlight
|
|
666
|
+
const memEl = container.querySelector(`.explorer-memory[data-id="${CSS.escape(nodeId)}"]`);
|
|
667
|
+
if (!memEl) return;
|
|
668
|
+
|
|
669
|
+
memEl.classList.add('selected');
|
|
670
|
+
|
|
671
|
+
if (scrollIntoView) {
|
|
672
|
+
// Expand parent categories if needed
|
|
673
|
+
let parent = memEl.parentElement;
|
|
674
|
+
while (parent && parent !== container) {
|
|
675
|
+
if (parent.classList.contains('explorer-node') && parent.dataset.expanded === 'false') {
|
|
676
|
+
parent.dataset.expanded = 'true';
|
|
677
|
+
_collapsed.delete(parent.dataset.cat);
|
|
678
|
+
}
|
|
679
|
+
parent = parent.parentElement;
|
|
680
|
+
}
|
|
681
|
+
saveCollapsed();
|
|
682
|
+
|
|
683
|
+
// Scroll into view
|
|
684
|
+
memEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
// ═══════════════════════════════════════════
|
|
690
|
+
// TOGGLE / SHOW / HIDE
|
|
691
|
+
// ═══════════════════════════════════════════
|
|
692
|
+
|
|
693
|
+
export function toggleExplorer() {
|
|
694
|
+
const panel = $('explorer-panel');
|
|
695
|
+
if (!panel) return;
|
|
696
|
+
|
|
697
|
+
_visible = !_visible;
|
|
698
|
+
panel.classList.toggle('open', _visible);
|
|
699
|
+
saveVisible();
|
|
700
|
+
|
|
701
|
+
// Toggle unified title bar style + solid containers
|
|
702
|
+
const titleBar = document.getElementById('title-bar');
|
|
703
|
+
if (titleBar) titleBar.classList.toggle('explorer-active', _visible);
|
|
704
|
+
document.body.classList.toggle('explorer-open', _visible);
|
|
705
|
+
|
|
706
|
+
// Set CSS variable to push content
|
|
707
|
+
applyExplorerWidth();
|
|
708
|
+
|
|
709
|
+
// Tell graphs to resize after transition
|
|
710
|
+
setTimeout(() => {
|
|
711
|
+
window.dispatchEvent(new Event('resize'));
|
|
712
|
+
}, 320);
|
|
713
|
+
|
|
714
|
+
// Sync menu checkmark
|
|
715
|
+
const menuEl = $('menu-toggle-explorer');
|
|
716
|
+
if (menuEl) menuEl.classList.toggle('active', _visible);
|
|
717
|
+
const tbBtn = $('topright-memory-explorer-btn');
|
|
718
|
+
if (tbBtn) tbBtn.classList.toggle('active', _visible);
|
|
719
|
+
|
|
720
|
+
// Build tree on first show
|
|
721
|
+
if (_visible && !panel.dataset.built) {
|
|
722
|
+
buildTree();
|
|
723
|
+
panel.dataset.built = '1';
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ═══════════════════════════════════════════
|
|
728
|
+
// COLLAPSE ALL / EXPAND ALL
|
|
729
|
+
// ═══════════════════════════════════════════
|
|
730
|
+
|
|
731
|
+
function collapseAll() {
|
|
732
|
+
const container = $('explorer-tree');
|
|
733
|
+
if (!container) return;
|
|
734
|
+
container.querySelectorAll('.explorer-node[data-cat]').forEach(el => {
|
|
735
|
+
el.dataset.expanded = 'false';
|
|
736
|
+
_collapsed.add(el.dataset.cat);
|
|
737
|
+
});
|
|
738
|
+
saveCollapsed();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function expandAll() {
|
|
742
|
+
const container = $('explorer-tree');
|
|
743
|
+
if (!container) return;
|
|
744
|
+
container.querySelectorAll('.explorer-node[data-cat]').forEach(el => {
|
|
745
|
+
el.dataset.expanded = 'true';
|
|
746
|
+
});
|
|
747
|
+
_collapsed.clear();
|
|
748
|
+
saveCollapsed();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
// ═══════════════════════════════════════════
|
|
753
|
+
// RESIZE HANDLE (right edge drag)
|
|
754
|
+
// ═══════════════════════════════════════════
|
|
755
|
+
|
|
756
|
+
function initResizeHandle() {
|
|
757
|
+
const handle = $('explorer-resize-handle');
|
|
758
|
+
const panel = $('explorer-panel');
|
|
759
|
+
if (!handle || !panel) return;
|
|
760
|
+
|
|
761
|
+
let dragging = false;
|
|
762
|
+
|
|
763
|
+
handle.addEventListener('mousedown', (e) => {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
dragging = true;
|
|
766
|
+
handle.classList.add('active');
|
|
767
|
+
document.body.style.cursor = 'col-resize';
|
|
768
|
+
document.body.style.userSelect = 'none';
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
document.addEventListener('mousemove', (e) => {
|
|
772
|
+
if (!dragging) return;
|
|
773
|
+
const newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, e.clientX));
|
|
774
|
+
_width = newWidth;
|
|
775
|
+
panel.style.width = `${_width}px`;
|
|
776
|
+
applyExplorerWidth();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
document.addEventListener('mouseup', () => {
|
|
780
|
+
if (!dragging) return;
|
|
781
|
+
dragging = false;
|
|
782
|
+
handle.classList.remove('active');
|
|
783
|
+
document.body.style.cursor = '';
|
|
784
|
+
document.body.style.userSelect = '';
|
|
785
|
+
saveWidth();
|
|
786
|
+
// Trigger graph resize
|
|
787
|
+
window.dispatchEvent(new Event('resize'));
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
// ═══════════════════════════════════════════
|
|
793
|
+
// DRAG-AND-DROP REORDERING
|
|
794
|
+
// ═══════════════════════════════════════════
|
|
795
|
+
|
|
796
|
+
let _dragEl = null; // .explorer-node being dragged
|
|
797
|
+
let _dragType = null; // 'parent' | 'category'
|
|
798
|
+
let _dragName = null; // category name from data-cat
|
|
799
|
+
let _startY = 0;
|
|
800
|
+
let _startX = 0;
|
|
801
|
+
let _cloneEl = null; // floating clone (body-appended)
|
|
802
|
+
let _initialRect = null;
|
|
803
|
+
let _didDragMove = false;
|
|
804
|
+
let _reparenting = false; // guard for concurrent API calls
|
|
805
|
+
|
|
806
|
+
function wouldCreateCycle(draggedCat, targetParent) {
|
|
807
|
+
if (draggedCat === targetParent) return true;
|
|
808
|
+
let cur = targetParent;
|
|
809
|
+
while (cur) {
|
|
810
|
+
if (cur === draggedCat) return true;
|
|
811
|
+
cur = (state.categoryMetadata[cur] || {}).parent;
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function clearDragIndicators(container) {
|
|
817
|
+
container.querySelectorAll('.explorer-drop-before, .explorer-drop-after').forEach(el => {
|
|
818
|
+
el.classList.remove('explorer-drop-before', 'explorer-drop-after');
|
|
819
|
+
});
|
|
820
|
+
container.querySelectorAll('.explorer-drop-reparent').forEach(el => {
|
|
821
|
+
el.classList.remove('explorer-drop-reparent');
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function createExplorerDragClone(row) {
|
|
826
|
+
const rect = row.getBoundingClientRect();
|
|
827
|
+
const clone = document.createElement('div');
|
|
828
|
+
clone.className = 'explorer-drag-clone';
|
|
829
|
+
// Copy relevant children (dot + label)
|
|
830
|
+
const dot = row.querySelector('.explorer-dot');
|
|
831
|
+
const label = row.querySelector('.explorer-label');
|
|
832
|
+
if (dot) clone.appendChild(dot.cloneNode(true));
|
|
833
|
+
if (label) {
|
|
834
|
+
const lbl = label.cloneNode(true);
|
|
835
|
+
lbl.style.maxWidth = '180px';
|
|
836
|
+
clone.appendChild(lbl);
|
|
837
|
+
}
|
|
838
|
+
clone.style.width = Math.min(rect.width, 240) + 'px';
|
|
839
|
+
clone.style.left = rect.left + 'px';
|
|
840
|
+
clone.style.top = rect.top + 'px';
|
|
841
|
+
clone.style.transform = 'translate(0px, 0px) scale(1.04) rotate(0.8deg)';
|
|
842
|
+
document.body.appendChild(clone);
|
|
843
|
+
return clone;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function getExplorerDropTarget(container, y) {
|
|
847
|
+
const topLevelNodes = [...container.querySelectorAll(':scope > .explorer-node[data-cat]')];
|
|
848
|
+
|
|
849
|
+
if (_dragType === 'parent') {
|
|
850
|
+
// Dragging a parent — reorder among top-level siblings only
|
|
851
|
+
for (const node of topLevelNodes) {
|
|
852
|
+
if (node === _dragEl) continue;
|
|
853
|
+
const row = node.querySelector(':scope > .explorer-row');
|
|
854
|
+
if (!row) continue;
|
|
855
|
+
const rect = row.getBoundingClientRect();
|
|
856
|
+
const mid = rect.top + rect.height / 2;
|
|
857
|
+
if (y >= rect.top - 8 && y <= rect.bottom + 8) {
|
|
858
|
+
return { type: 'reorder-toplevel', node, position: y < mid ? 'before' : 'after' };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Dragging a child category — can reorder among siblings or reparent
|
|
865
|
+
const currentParentEl = _dragEl.parentElement?.closest('.explorer-node[data-cat]');
|
|
866
|
+
const currentParentName = currentParentEl?.dataset.cat || null;
|
|
867
|
+
|
|
868
|
+
// Check: is cursor above all top-level items? → make top-level
|
|
869
|
+
if (topLevelNodes.length > 0) {
|
|
870
|
+
const firstRow = topLevelNodes[0]?.querySelector(':scope > .explorer-row');
|
|
871
|
+
if (firstRow) {
|
|
872
|
+
const firstRect = firstRow.getBoundingClientRect();
|
|
873
|
+
const containerRect = container.getBoundingClientRect();
|
|
874
|
+
if (y < firstRect.top && y >= containerRect.top) {
|
|
875
|
+
return { type: 'make-toplevel' };
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Check all visible category nodes
|
|
881
|
+
const allCatNodes = [...container.querySelectorAll('.explorer-node[data-cat]')];
|
|
882
|
+
for (const node of allCatNodes) {
|
|
883
|
+
if (node === _dragEl) continue;
|
|
884
|
+
const row = node.querySelector(':scope > .explorer-row');
|
|
885
|
+
if (!row) continue;
|
|
886
|
+
const rect = row.getBoundingClientRect();
|
|
887
|
+
if (y < rect.top - 8 || y > rect.bottom + 8) continue;
|
|
888
|
+
|
|
889
|
+
const mid = rect.top + rect.height / 2;
|
|
890
|
+
const nodeCat = node.dataset.cat;
|
|
891
|
+
const nodeMeta = state.categoryMetadata[nodeCat] || {};
|
|
892
|
+
const isParentNode = node.classList.contains('explorer-parent');
|
|
893
|
+
|
|
894
|
+
// If hovering a parent node → reparent into it
|
|
895
|
+
if (isParentNode && !wouldCreateCycle(_dragName, nodeCat)) {
|
|
896
|
+
return { type: 'reparent', node, targetName: nodeCat };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// If hovering a sibling (same parent) → reorder
|
|
900
|
+
const nodeParentEl = node.parentElement?.closest('.explorer-node[data-cat]');
|
|
901
|
+
const nodeParentName = nodeParentEl?.dataset.cat || null;
|
|
902
|
+
if (nodeParentName === currentParentName) {
|
|
903
|
+
return { type: 'reorder-sibling', node, position: y < mid ? 'before' : 'after', parentName: currentParentName };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// If hovering a category under a different parent → reparent to that parent
|
|
907
|
+
if (nodeParentName && nodeParentName !== currentParentName && !wouldCreateCycle(_dragName, nodeParentName)) {
|
|
908
|
+
return { type: 'reparent', node: nodeParentEl, targetName: nodeParentName };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// If hovering a top-level standalone category → reorder at top level
|
|
912
|
+
if (!nodeParentName && !isParentNode) {
|
|
913
|
+
return { type: 'reorder-toplevel', node, position: y < mid ? 'before' : 'after' };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function initExplorerDrag(container) {
|
|
921
|
+
container.addEventListener('pointerdown', (e) => {
|
|
922
|
+
// Only trigger on grip handle
|
|
923
|
+
const gripEl = e.target.closest('.explorer-grip');
|
|
924
|
+
if (!gripEl) return;
|
|
925
|
+
|
|
926
|
+
const nodeEl = gripEl.closest('.explorer-node[data-cat]');
|
|
927
|
+
if (!nodeEl) return;
|
|
928
|
+
if (nodeEl.classList.contains('explorer-memory')) return;
|
|
929
|
+
if (_filterText) return; // no drag during search
|
|
930
|
+
if (_reparenting) return; // API call in flight
|
|
931
|
+
|
|
932
|
+
e.preventDefault();
|
|
933
|
+
e.stopPropagation();
|
|
934
|
+
|
|
935
|
+
_dragEl = nodeEl;
|
|
936
|
+
_dragType = nodeEl.classList.contains('explorer-parent') ? 'parent' : 'category';
|
|
937
|
+
_dragName = nodeEl.dataset.cat;
|
|
938
|
+
_startY = e.clientY;
|
|
939
|
+
_startX = e.clientX;
|
|
940
|
+
_didDragMove = false;
|
|
941
|
+
|
|
942
|
+
nodeEl.setPointerCapture(e.pointerId);
|
|
943
|
+
|
|
944
|
+
function onMove(ev) {
|
|
945
|
+
if (!_dragEl) return;
|
|
946
|
+
if (!_didDragMove && Math.abs(ev.clientY - _startY) < 5 && Math.abs(ev.clientX - _startX) < 5) return;
|
|
947
|
+
|
|
948
|
+
if (!_didDragMove) {
|
|
949
|
+
_didDragMove = true;
|
|
950
|
+
_initialRect = _dragEl.getBoundingClientRect();
|
|
951
|
+
|
|
952
|
+
// Create floating clone from the row
|
|
953
|
+
const row = _dragEl.querySelector(':scope > .explorer-row');
|
|
954
|
+
_cloneEl = createExplorerDragClone(row);
|
|
955
|
+
|
|
956
|
+
// Ghost the source
|
|
957
|
+
_dragEl.classList.add('explorer-drag-source');
|
|
958
|
+
container.classList.add('explorer-dragging');
|
|
959
|
+
|
|
960
|
+
// Set gap size based on row height
|
|
961
|
+
const rowH = row.getBoundingClientRect().height + 4;
|
|
962
|
+
container.style.setProperty('--explorer-drag-gap', rowH + 'px');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Move clone
|
|
966
|
+
const dx = ev.clientX - _startX;
|
|
967
|
+
const dy = ev.clientY - _startY;
|
|
968
|
+
_cloneEl.style.transform = `translate(${dx}px, ${dy}px) scale(1.04) rotate(0.8deg)`;
|
|
969
|
+
|
|
970
|
+
// Show drop indicator
|
|
971
|
+
clearDragIndicators(container);
|
|
972
|
+
const target = getExplorerDropTarget(container, ev.clientY);
|
|
973
|
+
if (target) {
|
|
974
|
+
if (target.type === 'reparent') {
|
|
975
|
+
target.node.classList.add('explorer-drop-reparent');
|
|
976
|
+
} else if (target.type === 'reorder-toplevel' || target.type === 'reorder-sibling') {
|
|
977
|
+
target.node.classList.add(target.position === 'before' ? 'explorer-drop-before' : 'explorer-drop-after');
|
|
978
|
+
} else if (target.type === 'make-toplevel') {
|
|
979
|
+
const first = container.querySelector(':scope > .explorer-node[data-cat]');
|
|
980
|
+
if (first && first !== _dragEl) first.classList.add('explorer-drop-before');
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function onUp(ev) {
|
|
986
|
+
nodeEl.removeEventListener('pointermove', onMove);
|
|
987
|
+
nodeEl.removeEventListener('pointerup', onUp);
|
|
988
|
+
nodeEl.removeEventListener('pointercancel', onUp);
|
|
989
|
+
|
|
990
|
+
if (!_dragEl) return;
|
|
991
|
+
|
|
992
|
+
const droppedName = _dragName;
|
|
993
|
+
const didMove = _didDragMove;
|
|
994
|
+
|
|
995
|
+
if (didMove) {
|
|
996
|
+
const target = getExplorerDropTarget(container, ev.clientY);
|
|
997
|
+
|
|
998
|
+
// ── Spring-snap animation ──
|
|
999
|
+
if (_cloneEl && target && _initialRect) {
|
|
1000
|
+
let gapEl = null, gapPosition = null;
|
|
1001
|
+
if (target.type === 'reorder-toplevel' || target.type === 'reorder-sibling') {
|
|
1002
|
+
gapEl = target.node;
|
|
1003
|
+
gapPosition = target.position;
|
|
1004
|
+
} else if (target.type === 'make-toplevel') {
|
|
1005
|
+
gapEl = container.querySelector(':scope > .explorer-node[data-cat]');
|
|
1006
|
+
gapPosition = 'before';
|
|
1007
|
+
} else if (target.type === 'reparent') {
|
|
1008
|
+
gapEl = target.node;
|
|
1009
|
+
gapPosition = 'after';
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (gapEl) {
|
|
1013
|
+
const gapRect = gapEl.getBoundingClientRect();
|
|
1014
|
+
const rowH = _initialRect.height + 4;
|
|
1015
|
+
const targetTop = gapPosition === 'before'
|
|
1016
|
+
? gapRect.top - rowH
|
|
1017
|
+
: gapRect.bottom;
|
|
1018
|
+
const targetDy = targetTop - _initialRect.top;
|
|
1019
|
+
const targetDx = gapRect.left - _initialRect.left;
|
|
1020
|
+
|
|
1021
|
+
_cloneEl.style.transition = 'transform 0.22s cubic-bezier(0.22, 1.15, 0.36, 1), opacity 0.14s ease 0.06s';
|
|
1022
|
+
_cloneEl.style.transform = `translate(${targetDx}px, ${targetDy}px) scale(1) rotate(0deg)`;
|
|
1023
|
+
_cloneEl.style.opacity = '0';
|
|
1024
|
+
await new Promise(r => setTimeout(r, 220));
|
|
1025
|
+
}
|
|
1026
|
+
} else if (_cloneEl && _initialRect) {
|
|
1027
|
+
// No valid target — snap back
|
|
1028
|
+
_cloneEl.style.transition = 'transform 0.2s cubic-bezier(0.2, 0, 0, 1), opacity 0.12s ease';
|
|
1029
|
+
_cloneEl.style.transform = 'translate(0px, 0px) scale(1) rotate(0deg)';
|
|
1030
|
+
_cloneEl.style.opacity = '0';
|
|
1031
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ── Execute drop action ──
|
|
1035
|
+
if (target) {
|
|
1036
|
+
const saved = getExplorerCatOrder();
|
|
1037
|
+
|
|
1038
|
+
if (target.type === 'reorder-toplevel') {
|
|
1039
|
+
// Read current top-level order from DOM
|
|
1040
|
+
const topNames = [...container.querySelectorAll(':scope > .explorer-node[data-cat]')]
|
|
1041
|
+
.map(el => el.dataset.cat)
|
|
1042
|
+
.filter(n => n !== droppedName);
|
|
1043
|
+
const refIdx = topNames.indexOf(target.node.dataset.cat);
|
|
1044
|
+
const insertIdx = target.position === 'before' ? refIdx : refIdx + 1;
|
|
1045
|
+
topNames.splice(insertIdx, 0, droppedName);
|
|
1046
|
+
saved.topLevel = topNames;
|
|
1047
|
+
saveExplorerCatOrder(saved);
|
|
1048
|
+
buildTree();
|
|
1049
|
+
|
|
1050
|
+
} else if (target.type === 'reorder-sibling' && target.parentName) {
|
|
1051
|
+
// Read current child order within the parent
|
|
1052
|
+
const parentEl = container.querySelector(`.explorer-node[data-cat="${CSS.escape(target.parentName)}"]`);
|
|
1053
|
+
if (parentEl) {
|
|
1054
|
+
const childContainer = parentEl.querySelector(':scope > .explorer-children');
|
|
1055
|
+
if (childContainer) {
|
|
1056
|
+
const childNames = [...childContainer.querySelectorAll(':scope > .explorer-node[data-cat]')]
|
|
1057
|
+
.map(el => el.dataset.cat)
|
|
1058
|
+
.filter(n => n !== droppedName);
|
|
1059
|
+
const refIdx = childNames.indexOf(target.node.dataset.cat);
|
|
1060
|
+
const insertIdx = target.position === 'before' ? refIdx : refIdx + 1;
|
|
1061
|
+
childNames.splice(insertIdx, 0, droppedName);
|
|
1062
|
+
if (!saved.children) saved.children = {};
|
|
1063
|
+
saved.children[target.parentName] = childNames;
|
|
1064
|
+
saveExplorerCatOrder(saved);
|
|
1065
|
+
buildTree();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
} else if (target.type === 'reparent') {
|
|
1070
|
+
const newParent = target.targetName;
|
|
1071
|
+
const currentMeta = state.categoryMetadata[droppedName] || {};
|
|
1072
|
+
if (currentMeta.parent !== newParent) {
|
|
1073
|
+
_reparenting = true;
|
|
1074
|
+
try {
|
|
1075
|
+
const data = await apiUpdateCategory(droppedName, { parent: newParent });
|
|
1076
|
+
// Apply response to state
|
|
1077
|
+
if (data && data.categories) {
|
|
1078
|
+
state.allCategoryNames = data.categories.map(c => c.name);
|
|
1079
|
+
state.categoryDescriptions = {};
|
|
1080
|
+
state.categoryMetadata = {};
|
|
1081
|
+
data.categories.forEach(c => {
|
|
1082
|
+
state.categoryDescriptions[c.name] = c.description;
|
|
1083
|
+
state.categoryMetadata[c.name] = {
|
|
1084
|
+
parent: c.parent,
|
|
1085
|
+
color: c.color,
|
|
1086
|
+
is_parent: c.is_parent,
|
|
1087
|
+
logo: c.logo,
|
|
1088
|
+
};
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
const presentCats = new Set(state.allNodes.map(n => n.payload.category));
|
|
1092
|
+
buildCategorySidebar(presentCats);
|
|
1093
|
+
emit('graph:refresh');
|
|
1094
|
+
buildTree();
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
console.error('Explorer: error reparenting category:', err);
|
|
1097
|
+
} finally {
|
|
1098
|
+
_reparenting = false;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
} else if (target.type === 'make-toplevel') {
|
|
1103
|
+
const currentMeta = state.categoryMetadata[droppedName] || {};
|
|
1104
|
+
if (currentMeta.parent) {
|
|
1105
|
+
_reparenting = true;
|
|
1106
|
+
try {
|
|
1107
|
+
const data = await apiUpdateCategory(droppedName, { parent: '' });
|
|
1108
|
+
if (data && data.categories) {
|
|
1109
|
+
state.allCategoryNames = data.categories.map(c => c.name);
|
|
1110
|
+
state.categoryDescriptions = {};
|
|
1111
|
+
state.categoryMetadata = {};
|
|
1112
|
+
data.categories.forEach(c => {
|
|
1113
|
+
state.categoryDescriptions[c.name] = c.description;
|
|
1114
|
+
state.categoryMetadata[c.name] = {
|
|
1115
|
+
parent: c.parent,
|
|
1116
|
+
color: c.color,
|
|
1117
|
+
is_parent: c.is_parent,
|
|
1118
|
+
logo: c.logo,
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
const presentCats = new Set(state.allNodes.map(n => n.payload.category));
|
|
1123
|
+
buildCategorySidebar(presentCats);
|
|
1124
|
+
emit('graph:refresh');
|
|
1125
|
+
buildTree();
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
console.error('Explorer: error making category top-level:', err);
|
|
1128
|
+
} finally {
|
|
1129
|
+
_reparenting = false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ── Cleanup ──
|
|
1137
|
+
clearDragIndicators(container);
|
|
1138
|
+
if (_dragEl) _dragEl.classList.remove('explorer-drag-source');
|
|
1139
|
+
if (_cloneEl) { _cloneEl.remove(); _cloneEl = null; }
|
|
1140
|
+
container.classList.remove('explorer-dragging');
|
|
1141
|
+
container.style.removeProperty('--explorer-drag-gap');
|
|
1142
|
+
_dragEl = null;
|
|
1143
|
+
_dragType = null;
|
|
1144
|
+
_dragName = null;
|
|
1145
|
+
_initialRect = null;
|
|
1146
|
+
|
|
1147
|
+
// ── Settle animation ──
|
|
1148
|
+
if (didMove && droppedName) {
|
|
1149
|
+
const settled = container.querySelector(`.explorer-node[data-cat="${CSS.escape(droppedName)}"]`);
|
|
1150
|
+
if (settled) {
|
|
1151
|
+
settled.classList.add('explorer-just-dropped');
|
|
1152
|
+
settled.addEventListener('animationend', () => settled.classList.remove('explorer-just-dropped'), { once: true });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
nodeEl.addEventListener('pointermove', onMove);
|
|
1158
|
+
nodeEl.addEventListener('pointerup', onUp);
|
|
1159
|
+
nodeEl.addEventListener('pointercancel', onUp);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
// ═══════════════════════════════════════════
|
|
1165
|
+
// INIT
|
|
1166
|
+
// ═══════════════════════════════════════════
|
|
1167
|
+
|
|
1168
|
+
export function initExplorer() {
|
|
1169
|
+
loadPersistedState();
|
|
1170
|
+
|
|
1171
|
+
// Apply persisted width to the panel
|
|
1172
|
+
const panel = $('explorer-panel');
|
|
1173
|
+
if (panel) {
|
|
1174
|
+
panel.style.width = `${_width}px`;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Menu toggle
|
|
1178
|
+
const menuEl = $('menu-toggle-explorer');
|
|
1179
|
+
if (menuEl) {
|
|
1180
|
+
menuEl.addEventListener('click', () => toggleExplorer());
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Logo click toggles explorer
|
|
1184
|
+
const logo = $('titlebar-logo');
|
|
1185
|
+
if (logo) {
|
|
1186
|
+
logo.addEventListener('click', () => toggleExplorer());
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Toolbar button
|
|
1190
|
+
const toolbarBtn = $('topright-memory-explorer-btn');
|
|
1191
|
+
if (toolbarBtn) {
|
|
1192
|
+
toolbarBtn.addEventListener('click', () => toggleExplorer());
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Collapse all / expand all
|
|
1196
|
+
const collapseAllBtn = $('explorer-collapse-all');
|
|
1197
|
+
if (collapseAllBtn) collapseAllBtn.addEventListener('click', collapseAll);
|
|
1198
|
+
|
|
1199
|
+
const expandAllBtn = $('explorer-expand-all');
|
|
1200
|
+
if (expandAllBtn) expandAllBtn.addEventListener('click', expandAll);
|
|
1201
|
+
|
|
1202
|
+
// Sort button
|
|
1203
|
+
const sortBtn = $('explorer-sort-btn');
|
|
1204
|
+
if (sortBtn) {
|
|
1205
|
+
// Set initial tooltip
|
|
1206
|
+
const labels = { newest: 'Sort: newest first', oldest: 'Sort: oldest first', alpha: 'Sort: A-Z' };
|
|
1207
|
+
sortBtn.setAttribute('data-tooltip', labels[_sortMode]);
|
|
1208
|
+
sortBtn.addEventListener('click', cycleSortMode);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Filter input
|
|
1212
|
+
const filterInput = $('explorer-filter-input');
|
|
1213
|
+
const filterClear = $('explorer-filter-clear');
|
|
1214
|
+
if (filterInput) {
|
|
1215
|
+
filterInput.addEventListener('input', () => {
|
|
1216
|
+
_filterText = filterInput.value.trim();
|
|
1217
|
+
filterClear.style.display = _filterText ? '' : 'none';
|
|
1218
|
+
buildTree();
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
if (filterClear) {
|
|
1222
|
+
filterClear.style.display = 'none';
|
|
1223
|
+
filterClear.addEventListener('click', () => {
|
|
1224
|
+
_filterText = '';
|
|
1225
|
+
if (filterInput) filterInput.value = '';
|
|
1226
|
+
filterClear.style.display = 'none';
|
|
1227
|
+
buildTree();
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Label toggle
|
|
1232
|
+
const labelToggle = $('explorer-label-toggle');
|
|
1233
|
+
if (labelToggle) {
|
|
1234
|
+
// Sync initial style
|
|
1235
|
+
labelToggle.style.color = state.labelsVisible ? 'var(--t-primary)' : 'var(--t-muted)';
|
|
1236
|
+
labelToggle.addEventListener('click', () => {
|
|
1237
|
+
state.labelsVisible = !state.labelsVisible;
|
|
1238
|
+
labelToggle.style.color = state.labelsVisible ? 'var(--t-primary)' : 'var(--t-muted)';
|
|
1239
|
+
emit('graph:refresh');
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Label size button + slider
|
|
1244
|
+
const labelSizeBtn = $('explorer-label-size');
|
|
1245
|
+
const sliderRow = $('explorer-label-slider-row');
|
|
1246
|
+
const slider = $('explorer-label-slider');
|
|
1247
|
+
if (labelSizeBtn && sliderRow && slider) {
|
|
1248
|
+
slider.value = state.labelSizeMultiplier;
|
|
1249
|
+
labelSizeBtn.addEventListener('click', () => {
|
|
1250
|
+
const open = sliderRow.style.display !== 'none';
|
|
1251
|
+
sliderRow.style.display = open ? 'none' : '';
|
|
1252
|
+
});
|
|
1253
|
+
slider.addEventListener('input', () => {
|
|
1254
|
+
state.labelSizeMultiplier = parseFloat(slider.value);
|
|
1255
|
+
emit('graph:refresh');
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Select all / deselect all categories
|
|
1260
|
+
const selectAllBtn = $('explorer-select-all');
|
|
1261
|
+
const deselectAllBtn = $('explorer-deselect-all');
|
|
1262
|
+
if (selectAllBtn) {
|
|
1263
|
+
selectAllBtn.addEventListener('click', () => {
|
|
1264
|
+
Object.keys(state.categoryMetadata).forEach(c => state.activeCategories.add(c));
|
|
1265
|
+
cancelScheduledRemoval();
|
|
1266
|
+
emit('graph:refresh');
|
|
1267
|
+
emit('categories-changed');
|
|
1268
|
+
buildTree();
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
if (deselectAllBtn) {
|
|
1272
|
+
deselectAllBtn.addEventListener('click', () => {
|
|
1273
|
+
state.activeCategories.clear();
|
|
1274
|
+
scheduleGraphRemoval();
|
|
1275
|
+
emit('categories-changed');
|
|
1276
|
+
buildTree();
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Right-edge resize
|
|
1281
|
+
initResizeHandle();
|
|
1282
|
+
|
|
1283
|
+
// Keyboard shortcut: toggle explorer (via central keybinds)
|
|
1284
|
+
registerAction('toggle-explorer', () => toggleExplorer());
|
|
1285
|
+
|
|
1286
|
+
// ── Event bus listeners ──
|
|
1287
|
+
|
|
1288
|
+
// Rebuild tree when data changes
|
|
1289
|
+
on('data-loaded', () => {
|
|
1290
|
+
if (_visible) buildTree();
|
|
1291
|
+
else {
|
|
1292
|
+
if (panel) delete panel.dataset.built;
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
on('data:reload', () => {
|
|
1297
|
+
if (_visible) buildTree();
|
|
1298
|
+
else {
|
|
1299
|
+
if (panel) delete panel.dataset.built;
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
on('categories-changed', () => {
|
|
1304
|
+
if (_visible) buildTree();
|
|
1305
|
+
else {
|
|
1306
|
+
if (panel) delete panel.dataset.built;
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// Also listen to sidebar's colon-separated event (edit/delete/create)
|
|
1311
|
+
on('categories:changed', () => {
|
|
1312
|
+
if (_visible) buildTree();
|
|
1313
|
+
else {
|
|
1314
|
+
if (panel) delete panel.dataset.built;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Rebuild when permissions arrive or change (context menu items may differ)
|
|
1319
|
+
on('session:info', () => {
|
|
1320
|
+
if (_visible) buildTree();
|
|
1321
|
+
});
|
|
1322
|
+
on('permissions:changed', () => {
|
|
1323
|
+
if (_visible) buildTree();
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
// Sync selection from graph click
|
|
1327
|
+
on('node-selected', (node) => {
|
|
1328
|
+
if (node && _visible) {
|
|
1329
|
+
highlightNode(node.id, true);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// Search integration
|
|
1334
|
+
on('search:apply', () => {
|
|
1335
|
+
if (_visible) buildTree();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
on('search:clear', () => {
|
|
1339
|
+
if (_visible) buildTree();
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// Restore visibility if it was open last time
|
|
1343
|
+
if (_visible) {
|
|
1344
|
+
if (panel) {
|
|
1345
|
+
panel.classList.add('open');
|
|
1346
|
+
applyExplorerWidth();
|
|
1347
|
+
const titleBar = document.getElementById('title-bar');
|
|
1348
|
+
if (titleBar) titleBar.classList.add('explorer-active');
|
|
1349
|
+
document.body.classList.add('explorer-open');
|
|
1350
|
+
const menuItem = $('menu-toggle-explorer');
|
|
1351
|
+
if (menuItem) menuItem.classList.add('active');
|
|
1352
|
+
const tbBtnInit = $('topright-memory-explorer-btn');
|
|
1353
|
+
if (tbBtnInit) tbBtnInit.classList.add('active');
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|