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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,461 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Image Gallery
3
+ // Browse, filter, favorite, and manage images in data/images/.
4
+ // Draggable/resizable floating window (Session Monitor pattern).
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { emit, on } from './state.js';
8
+ import { fetchImages, toggleImageFavorite, deleteImage } from './api.js';
9
+ import { addImageToWhiteboard } from './ui-whiteboard.js';
10
+
11
+ const $ = (id) => document.getElementById(id);
12
+
13
+ // ── State ──
14
+ let _panel = null;
15
+ let _backdrop = null;
16
+ let _images = [];
17
+ let _filter = 'all'; // 'all' | 'screenshot' | 'attachment' | 'whiteboard' | 'paste'
18
+ let _search = '';
19
+ let _sort = 'newest'; // 'newest' | 'oldest' | 'name' | 'size'
20
+ let _favOnly = false;
21
+ let _lightboxImg = null; // filename currently in lightbox
22
+ let isVisible = false;
23
+
24
+ // ── Drag state ──
25
+ let _dragging = false;
26
+ let _dragOff = { x: 0, y: 0 };
27
+
28
+ // ── Icons ──
29
+ const ICON_CLOSE = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
30
+ const ICON_REFRESH = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 019.2-4M13.5 8a5.5 5.5 0 01-9.2 4"/><path d="M11.5 2v2.5H14M4.5 14v-2.5H2"/></svg>';
31
+ const ICON_HEART = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13.7C4.3 11 2 8.7 2 6.3A3.3 3.3 0 018 4.6a3.3 3.3 0 016 1.7c0 2.4-2.3 4.7-6 7.4z"/></svg>';
32
+ const ICON_HEART_FILL = '<svg viewBox="0 0 16 16" fill="currentColor" stroke="none"><path d="M8 13.7C4.3 11 2 8.7 2 6.3A3.3 3.3 0 018 4.6a3.3 3.3 0 016 1.7c0 2.4-2.3 4.7-6 7.4z"/></svg>';
33
+ const ICON_TRASH = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V2.5h6V4M3.5 4v9.5a1 1 0 001 1h7a1 1 0 001-1V4"/></svg>';
34
+ const ICON_DOWNLOAD = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9M5 8l3 3 3-3"/><path d="M2 12v2h12v-2"/></svg>';
35
+ const ICON_WHITEBOARD = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2" width="13" height="12" rx="1.5"/><path d="M4 7h8M4 10h5"/></svg>';
36
+ const ICON_GALLERY = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2.5" width="13" height="11" rx="2"/><circle cx="5.5" cy="6.5" r="1.5"/><path d="M1.5 11l3.5-3.5 2.5 2.5 2-1.5L14.5 13"/></svg>';
37
+
38
+ const TYPE_LABELS = {
39
+ all: 'All',
40
+ screenshot: 'Screenshots',
41
+ attachment: 'Attachments',
42
+ whiteboard: 'Whiteboard',
43
+ paste: 'Pastes',
44
+ };
45
+
46
+ // ── Helpers ──
47
+
48
+ function filtered() {
49
+ let list = _images;
50
+ if (_filter !== 'all') list = list.filter(i => i.type === _filter);
51
+ if (_favOnly) list = list.filter(i => i.favorite);
52
+ if (_search) {
53
+ const q = _search.toLowerCase();
54
+ list = list.filter(i => i.filename.toLowerCase().includes(q));
55
+ }
56
+ switch (_sort) {
57
+ case 'oldest': list = [...list].sort((a, b) => a.modifiedAt - b.modifiedAt); break;
58
+ case 'name': list = [...list].sort((a, b) => a.filename.localeCompare(b.filename)); break;
59
+ case 'size': list = [...list].sort((a, b) => b.size - a.size); break;
60
+ default: list = [...list].sort((a, b) => b.modifiedAt - a.modifiedAt); break;
61
+ }
62
+ return list;
63
+ }
64
+
65
+ function formatSize(bytes) {
66
+ if (bytes < 1024) return bytes + ' B';
67
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
68
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
69
+ }
70
+
71
+ function formatDate(ms) {
72
+ const d = new Date(ms);
73
+ const now = new Date();
74
+ const diff = now - d;
75
+ if (diff < 60000) return 'Just now';
76
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
77
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
78
+ if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
79
+ return d.toLocaleDateString();
80
+ }
81
+
82
+ function imageUrl(filename) {
83
+ return `/api/images/file/${encodeURIComponent(filename)}`;
84
+ }
85
+
86
+ // ── Build HTML ──
87
+
88
+ function buildPanelHTML() {
89
+ const list = filtered();
90
+ const counts = { all: _images.length };
91
+ for (const img of _images) counts[img.type] = (counts[img.type] || 0) + 1;
92
+
93
+ return `
94
+ <div class="ig-header" id="ig-header">
95
+ <div class="ig-header-left">
96
+ <span class="ig-icon">${ICON_GALLERY}</span>
97
+ <span class="ig-title">Image Gallery</span>
98
+ <span class="ig-count">${list.length}${list.length !== _images.length ? ' / ' + _images.length : ''}</span>
99
+ </div>
100
+ <div class="ig-header-right">
101
+ <button class="ig-btn" id="ig-refresh" data-tooltip="Refresh">${ICON_REFRESH}</button>
102
+ <button class="backdrop-toggle-btn" id="ig-backdrop-toggle" data-tooltip="Toggle backdrop">
103
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
104
+ </button>
105
+ <button class="ig-btn" id="ig-close" data-tooltip="Close">${ICON_CLOSE}</button>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="ig-toolbar">
110
+ <input type="text" class="ig-search" id="ig-search" placeholder="Search images..." data-tooltip="Search by filename">
111
+ <select class="ig-filter" id="ig-filter" data-tooltip="Filter by type">
112
+ ${Object.entries(TYPE_LABELS).map(([k, v]) => `<option value="${k}"${_filter === k ? ' selected' : ''}>${v}${counts[k] ? ' (' + counts[k] + ')' : ''}</option>`).join('')}
113
+ </select>
114
+ <select class="ig-sort" id="ig-sort" data-tooltip="Sort order">
115
+ <option value="newest"${_sort === 'newest' ? ' selected' : ''}>Newest</option>
116
+ <option value="oldest"${_sort === 'oldest' ? ' selected' : ''}>Oldest</option>
117
+ <option value="name"${_sort === 'name' ? ' selected' : ''}>Name</option>
118
+ <option value="size"${_sort === 'size' ? ' selected' : ''}>Size</option>
119
+ </select>
120
+ <button class="ig-btn ig-fav-toggle${_favOnly ? ' active' : ''}" id="ig-fav-toggle" data-tooltip="Favorites only">${ICON_HEART}</button>
121
+ </div>
122
+
123
+ <div class="ig-body" id="ig-body">
124
+ ${list.length === 0
125
+ ? `<div class="ig-empty">${_images.length === 0 ? 'No images yet' : 'No matches'}</div>`
126
+ : `<div class="ig-grid">${list.map(img => buildCard(img)).join('')}</div>`
127
+ }
128
+ </div>
129
+ `;
130
+ }
131
+
132
+ function buildCard(img) {
133
+ return `
134
+ <div class="ig-card" data-filename="${img.filename}">
135
+ <div class="ig-thumb-wrap">
136
+ <img class="ig-thumb" src="${imageUrl(img.filename)}" loading="lazy" alt="${img.filename}">
137
+ <div class="ig-card-actions">
138
+ <button class="ig-card-btn ig-card-fav${img.favorite ? ' active' : ''}" data-action="fav" data-tooltip="${img.favorite ? 'Unfavorite' : 'Favorite'}">${img.favorite ? ICON_HEART_FILL : ICON_HEART}</button>
139
+ <button class="ig-card-btn ig-card-wb" data-action="whiteboard" data-tooltip="Add to Whiteboard">${ICON_WHITEBOARD}</button>
140
+ <button class="ig-card-btn ig-card-dl" data-action="download" data-tooltip="Download">${ICON_DOWNLOAD}</button>
141
+ <button class="ig-card-btn ig-card-del" data-action="delete" data-tooltip="Delete">${ICON_TRASH}</button>
142
+ </div>
143
+ </div>
144
+ <div class="ig-card-info">
145
+ <span class="ig-card-name">${img.filename.length > 30 ? img.filename.slice(0, 27) + '...' : img.filename}</span>
146
+ <span class="ig-card-meta">${formatSize(img.size)} · ${formatDate(img.modifiedAt)}</span>
147
+ </div>
148
+ </div>`;
149
+ }
150
+
151
+ // ── Lightbox ──
152
+
153
+ function openLightbox(filename) {
154
+ _lightboxImg = filename;
155
+ let lb = document.getElementById('ig-lightbox');
156
+ if (!lb) {
157
+ lb = document.createElement('div');
158
+ lb.id = 'ig-lightbox';
159
+ lb.className = 'ig-lightbox';
160
+ document.body.appendChild(lb);
161
+ }
162
+ const img = _images.find(i => i.filename === filename);
163
+ lb.innerHTML = `
164
+ <div class="ig-lb-overlay" id="ig-lb-overlay"></div>
165
+ <div class="ig-lb-content">
166
+ <img class="ig-lb-img" src="${imageUrl(filename)}" alt="${filename}">
167
+ <div class="ig-lb-bar">
168
+ <span class="ig-lb-name">${filename}</span>
169
+ ${img ? `<span class="ig-lb-size">${formatSize(img.size)}</span>` : ''}
170
+ <div class="ig-lb-actions">
171
+ <button class="ig-btn" id="ig-lb-fav" data-tooltip="Toggle favorite">${img?.favorite ? ICON_HEART_FILL : ICON_HEART}</button>
172
+ <button class="ig-btn" id="ig-lb-wb" data-tooltip="Add to Whiteboard">${ICON_WHITEBOARD}</button>
173
+ <button class="ig-btn" id="ig-lb-dl" data-tooltip="Download">${ICON_DOWNLOAD}</button>
174
+ <button class="ig-btn" id="ig-lb-del" data-tooltip="Delete">${ICON_TRASH}</button>
175
+ <button class="ig-btn" id="ig-lb-close" data-tooltip="Close">${ICON_CLOSE}</button>
176
+ </div>
177
+ </div>
178
+ <button class="ig-lb-nav ig-lb-prev" id="ig-lb-prev">&#8249;</button>
179
+ <button class="ig-lb-nav ig-lb-next" id="ig-lb-next">&#8250;</button>
180
+ </div>
181
+ `;
182
+ lb.classList.add('open');
183
+ wireLightbox();
184
+ }
185
+
186
+ function closeLightbox() {
187
+ _lightboxImg = null;
188
+ const lb = document.getElementById('ig-lightbox');
189
+ if (lb) { lb.classList.remove('open'); lb.innerHTML = ''; }
190
+ }
191
+
192
+ function navigateLightbox(dir) {
193
+ const list = filtered();
194
+ const idx = list.findIndex(i => i.filename === _lightboxImg);
195
+ if (idx === -1) return;
196
+ const next = idx + dir;
197
+ if (next >= 0 && next < list.length) openLightbox(list[next].filename);
198
+ }
199
+
200
+ function wireLightbox() {
201
+ const lb = document.getElementById('ig-lightbox');
202
+ if (!lb) return;
203
+
204
+ // Overlay click disabled — close only via ESC or close button
205
+ lb.querySelector('#ig-lb-close')?.addEventListener('click', closeLightbox);
206
+ lb.querySelector('#ig-lb-prev')?.addEventListener('click', () => navigateLightbox(-1));
207
+ lb.querySelector('#ig-lb-next')?.addEventListener('click', () => navigateLightbox(1));
208
+
209
+ lb.querySelector('#ig-lb-fav')?.addEventListener('click', async () => {
210
+ const img = _images.find(i => i.filename === _lightboxImg);
211
+ if (!img) return;
212
+ await toggleImageFavorite(img.filename, !img.favorite);
213
+ img.favorite = !img.favorite;
214
+ render();
215
+ openLightbox(img.filename);
216
+ });
217
+
218
+ lb.querySelector('#ig-lb-wb')?.addEventListener('click', () => {
219
+ if (_lightboxImg) {
220
+ addImageToWhiteboard(imageUrl(_lightboxImg));
221
+ closeLightbox();
222
+ closePanel();
223
+ }
224
+ });
225
+
226
+ lb.querySelector('#ig-lb-dl')?.addEventListener('click', () => {
227
+ if (_lightboxImg) downloadImage(_lightboxImg);
228
+ });
229
+
230
+ lb.querySelector('#ig-lb-del')?.addEventListener('click', async () => {
231
+ if (!_lightboxImg) return;
232
+ if (!confirm(`Delete ${_lightboxImg}?`)) return;
233
+ await deleteImage(_lightboxImg);
234
+ _images = _images.filter(i => i.filename !== _lightboxImg);
235
+ closeLightbox();
236
+ render();
237
+ });
238
+ }
239
+
240
+ // ── Actions ──
241
+
242
+ function downloadImage(filename) {
243
+ const a = document.createElement('a');
244
+ a.href = imageUrl(filename);
245
+ a.download = filename;
246
+ document.body.appendChild(a);
247
+ a.click();
248
+ a.remove();
249
+ }
250
+
251
+ async function refreshImages() {
252
+ try {
253
+ const data = await fetchImages();
254
+ _images = data.images || [];
255
+ render();
256
+ } catch (err) {
257
+ console.error('[image-gallery] refresh failed:', err);
258
+ }
259
+ }
260
+
261
+ // ── Render ──
262
+
263
+ function render() {
264
+ if (!_panel) return;
265
+ const body = _panel.querySelector('#ig-body');
266
+ const countEl = _panel.querySelector('.ig-count');
267
+ const list = filtered();
268
+
269
+ if (countEl) {
270
+ countEl.textContent = list.length + (list.length !== _images.length ? ' / ' + _images.length : '');
271
+ }
272
+
273
+ if (!body) return;
274
+
275
+ if (list.length === 0) {
276
+ body.innerHTML = `<div class="ig-empty">${_images.length === 0 ? 'No images yet' : 'No matches'}</div>`;
277
+ return;
278
+ }
279
+
280
+ body.innerHTML = `<div class="ig-grid">${list.map(img => buildCard(img)).join('')}</div>`;
281
+ }
282
+
283
+ // ── Panel lifecycle ──
284
+
285
+ function wirePanel() {
286
+ _panel.querySelector('#ig-close')?.addEventListener('click', closePanel);
287
+ _panel.querySelector('#ig-refresh')?.addEventListener('click', refreshImages);
288
+
289
+ const igBdToggle = _panel.querySelector('#ig-backdrop-toggle');
290
+ if (igBdToggle) {
291
+ igBdToggle.addEventListener('click', () => {
292
+ if (_backdrop) {
293
+ _backdrop.classList.toggle('backdrop-hidden');
294
+ igBdToggle.classList.toggle('active', _backdrop.classList.contains('backdrop-hidden'));
295
+ }
296
+ });
297
+ }
298
+
299
+ // Search
300
+ const searchInput = _panel.querySelector('#ig-search');
301
+ if (searchInput) {
302
+ let debounce = null;
303
+ searchInput.addEventListener('input', () => {
304
+ clearTimeout(debounce);
305
+ debounce = setTimeout(() => { _search = searchInput.value; render(); }, 200);
306
+ });
307
+ }
308
+
309
+ // Filter
310
+ _panel.querySelector('#ig-filter')?.addEventListener('change', (e) => {
311
+ _filter = e.target.value;
312
+ render();
313
+ });
314
+
315
+ // Sort
316
+ _panel.querySelector('#ig-sort')?.addEventListener('change', (e) => {
317
+ _sort = e.target.value;
318
+ render();
319
+ });
320
+
321
+ // Favorites toggle
322
+ _panel.querySelector('#ig-fav-toggle')?.addEventListener('click', (e) => {
323
+ _favOnly = !_favOnly;
324
+ e.currentTarget.classList.toggle('active', _favOnly);
325
+ render();
326
+ });
327
+
328
+ // Grid clicks (delegate)
329
+ _panel.querySelector('#ig-body')?.addEventListener('click', async (e) => {
330
+ const btn = e.target.closest('[data-action]');
331
+ const card = e.target.closest('.ig-card');
332
+ if (!card) return;
333
+
334
+ const filename = card.dataset.filename;
335
+ if (!filename) return;
336
+
337
+ if (btn) {
338
+ e.stopPropagation();
339
+ const action = btn.dataset.action;
340
+ if (action === 'fav') {
341
+ const img = _images.find(i => i.filename === filename);
342
+ if (!img) return;
343
+ await toggleImageFavorite(filename, !img.favorite);
344
+ img.favorite = !img.favorite;
345
+ render();
346
+ } else if (action === 'whiteboard') {
347
+ addImageToWhiteboard(imageUrl(filename));
348
+ closePanel();
349
+ } else if (action === 'download') {
350
+ downloadImage(filename);
351
+ } else if (action === 'delete') {
352
+ if (!confirm(`Delete ${filename}?`)) return;
353
+ await deleteImage(filename);
354
+ _images = _images.filter(i => i.filename !== filename);
355
+ render();
356
+ }
357
+ } else {
358
+ openLightbox(filename);
359
+ }
360
+ });
361
+
362
+ // Drag
363
+ const header = _panel.querySelector('#ig-header');
364
+ if (header) {
365
+ header.addEventListener('mousedown', (e) => {
366
+ if (e.target.closest('button')) return;
367
+ _dragging = true;
368
+ _panel.classList.add('dragging');
369
+ const rect = _panel.getBoundingClientRect();
370
+ _dragOff = { x: e.clientX - rect.left, y: e.clientY - rect.top };
371
+ e.preventDefault();
372
+ });
373
+ }
374
+
375
+ // Keyboard in lightbox
376
+ document.addEventListener('keydown', handleKey);
377
+ }
378
+
379
+ function handleKey(e) {
380
+ if (e.key === 'Escape') {
381
+ if (_lightboxImg) { closeLightbox(); e.stopPropagation(); }
382
+ else if (_panel) { closePanel(); e.stopPropagation(); }
383
+ return;
384
+ }
385
+ if (!_lightboxImg) return;
386
+ if (e.key === 'ArrowLeft') navigateLightbox(-1);
387
+ else if (e.key === 'ArrowRight') navigateLightbox(1);
388
+ }
389
+
390
+ function openPanel() {
391
+ if (_panel) { _panel.focus(); return; }
392
+
393
+ _backdrop = document.createElement('div');
394
+ _backdrop.className = 'ig-backdrop';
395
+ // Backdrop click disabled — close only via ESC or close button
396
+ document.body.appendChild(_backdrop);
397
+
398
+ _panel = document.createElement('div');
399
+ _panel.className = 'image-gallery-panel glass resizable';
400
+ _panel.id = 'image-gallery-panel';
401
+ _panel.innerHTML = buildPanelHTML();
402
+ document.body.appendChild(_panel);
403
+
404
+ _panel.style.left = Math.max(20, (window.innerWidth - 780) / 2) + 'px';
405
+ _panel.style.top = Math.max(48, (window.innerHeight - 560) / 2) + 'px';
406
+
407
+ wirePanel();
408
+ isVisible = true;
409
+ refreshImages();
410
+
411
+ // Global mouse handlers for drag
412
+ const onMouseMove = (e) => {
413
+ if (!_dragging) return;
414
+ _panel.style.left = (e.clientX - _dragOff.x) + 'px';
415
+ _panel.style.top = (e.clientY - _dragOff.y) + 'px';
416
+ };
417
+ const onMouseUp = () => {
418
+ _dragging = false;
419
+ if (_panel) _panel.classList.remove('dragging');
420
+ };
421
+ document.addEventListener('mousemove', onMouseMove);
422
+ document.addEventListener('mouseup', onMouseUp);
423
+
424
+ // Store cleanup ref
425
+ _panel._cleanup = () => {
426
+ document.removeEventListener('mousemove', onMouseMove);
427
+ document.removeEventListener('mouseup', onMouseUp);
428
+ document.removeEventListener('keydown', handleKey);
429
+ };
430
+
431
+ requestAnimationFrame(() => {
432
+ _backdrop.classList.add('open');
433
+ _panel.classList.add('open');
434
+ });
435
+ }
436
+
437
+ function closePanel() {
438
+ if (!_panel) return;
439
+ closeLightbox();
440
+ if (_panel._cleanup) _panel._cleanup();
441
+ if (_backdrop) { _backdrop.remove(); _backdrop = null; }
442
+ _panel.remove();
443
+ _panel = null;
444
+ isVisible = false;
445
+ _search = '';
446
+ _filter = 'all';
447
+ _sort = 'newest';
448
+ _favOnly = false;
449
+ emit('image-gallery:closed');
450
+ }
451
+
452
+ // ── Exports ──
453
+
454
+ export function toggleImageGallery() {
455
+ if (_panel) closePanel();
456
+ else openPanel();
457
+ }
458
+
459
+ export function initImageGallery() {
460
+ // Nothing needed at init — panel is created on toggle
461
+ }