@mcpware/claude-code-organizer 0.1.4 → 0.2.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/README.md CHANGED
@@ -2,9 +2,27 @@
2
2
 
3
3
  **Organize all your Claude Code memories, skills, MCP servers, and hooks — view by scope hierarchy, move between scopes via drag-and-drop.**
4
4
 
5
- Claude Code saves your customizations across scattered directories with encoded paths. This tool gives you a visual dashboard to see everything, understand which scope each item applies to, and move items between scopes with one click.
5
+ <video src="docs/demo.webm" autoplay loop muted playsinline width="100%"></video>
6
6
 
7
- ![Claude Code Organizer Dashboard](docs/dashboard.png)
7
+ ## The Problem
8
+
9
+ Ever asked Claude Code to "remember this", only to find it saved the memory to the wrong scope?
10
+
11
+ You're inside a project folder, you tell Claude to remember a preference — and it saves it to that project's scope. Switch to another project, and Claude has no idea. That memory is trapped.
12
+
13
+ The reverse happens too — skills or memories sitting in global scope that really only apply to one repo, leaking into everything else.
14
+
15
+ Want to fix it? You'd have to manually dig through `~/.claude/` and its encoded-path folders (`-home-user-projects-my-app/`), find the right file, and move it yourself.
16
+
17
+ **Claude Code Organizer fixes this.**
18
+
19
+ ### Example: Project → Global
20
+
21
+ You told Claude to remember "I prefer TypeScript + ESM" while inside a project, but that preference applies everywhere. Open the dashboard, drag that memory from Project scope to Global scope. Done.
22
+
23
+ ### Example: Global → Project
24
+
25
+ You have a deploy skill sitting in global, but it only makes sense for one repo. Drag it into that Project scope — other projects won't see it anymore.
8
26
 
9
27
  ---
10
28
 
@@ -30,6 +48,10 @@ npm install -g @mcpware/claude-code-organizer
30
48
  claude-code-organizer
31
49
  ```
32
50
 
51
+ Or paste this into Claude Code:
52
+
53
+ > Run `npx @mcpware/claude-code-organizer` — it's a dashboard for managing Claude Code settings. Tell me the URL when it's ready.
54
+
33
55
  Opens a dashboard at `http://localhost:3847`. Works with your real `~/.claude/` directory.
34
56
 
35
57
  ## What It Manages
@@ -47,12 +69,12 @@ Opens a dashboard at `http://localhost:3847`. Works with your real `~/.claude/`
47
69
  ## Scope Hierarchy
48
70
 
49
71
  ```
50
- Global <- applies everywhere
51
- AlltrueAi (workspace) <- applies to all sub-projects
52
- ai-security-control-plane <- project-specific
53
- rule-processor <- project-specific
54
- MyGithub (project) <- independent project
55
- Documents (project) <- independent project
72
+ Global <- applies everywhere
73
+ Company (workspace) <- applies to all sub-projects
74
+ CompanyRepo1 <- project-specific
75
+ CompanyRepo2 <- project-specific
76
+ SideProjects (project) <- independent project
77
+ Documents (project) <- independent project
56
78
  ```
57
79
 
58
80
  Child scopes inherit parent scope's memories, skills, and MCP servers.
@@ -120,6 +142,6 @@ MIT
120
142
 
121
143
  ## Author
122
144
 
123
- [mcpware](https://github.com/mcpware) — Building tools for the Claude Code ecosystem.
145
+ [ithiria894](https://github.com/ithiria894) — Building tools for the Claude Code ecosystem.
124
146
 
125
147
  See also: [@mcpware/instagram-mcp](https://github.com/mcpware/instagram-mcp)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpware/claude-code-organizer",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Organize all your Claude Code memories, skills, MCP servers, and hooks — view by scope hierarchy, move between scopes via drag-and-drop",
5
5
  "type": "module",
6
6
  "bin": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/mcpware/claude-code-organizer",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.3",
9
+ "version": "0.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@mcpware/claude-code-organizer",
14
- "version": "0.1.3",
14
+ "version": "0.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }
package/src/mover.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * Pure data module. No HTTP, no UI.
11
11
  */
12
12
 
13
- import { rename, mkdir, readFile, writeFile } from "node:fs/promises";
13
+ import { rename, mkdir, readFile, writeFile, rm, unlink } from "node:fs/promises";
14
14
  import { join, dirname, basename } from "node:path";
15
15
  import { homedir } from "node:os";
16
16
  import { existsSync } from "node:fs";
@@ -193,6 +193,66 @@ export async function moveItem(item, toScopeId, scopes) {
193
193
  }
194
194
  }
195
195
 
196
+ // ── Delete functions ─────────────────────────────────────────────────
197
+
198
+ async function deleteMemory(item) {
199
+ await unlink(item.path);
200
+ return { ok: true, deleted: item.path, message: `Deleted memory "${item.name}"` };
201
+ }
202
+
203
+ async function deleteSkill(item) {
204
+ await rm(item.path, { recursive: true, force: true });
205
+ return { ok: true, deleted: item.path, message: `Deleted skill "${item.name}"` };
206
+ }
207
+
208
+ async function deleteMcp(item) {
209
+ const mcpJson = item.path;
210
+ let content;
211
+ try {
212
+ content = JSON.parse(await readFile(mcpJson, "utf-8"));
213
+ } catch {
214
+ return { ok: false, error: `Cannot read .mcp.json: ${mcpJson}` };
215
+ }
216
+
217
+ if (!content.mcpServers?.[item.name]) {
218
+ return { ok: false, error: `Server "${item.name}" not found in ${mcpJson}` };
219
+ }
220
+
221
+ delete content.mcpServers[item.name];
222
+ await writeFile(mcpJson, JSON.stringify(content, null, 2) + "\n");
223
+
224
+ return { ok: true, deleted: mcpJson, message: `Deleted MCP server "${item.name}"` };
225
+ }
226
+
227
+ /**
228
+ * Delete an item permanently.
229
+ */
230
+ export async function deleteItem(item, scopes) {
231
+ if (item.locked) {
232
+ return { ok: false, error: `${item.name} is locked and cannot be deleted` };
233
+ }
234
+
235
+ const deletableCategories = ["memory", "skill", "mcp"];
236
+ if (!deletableCategories.includes(item.category)) {
237
+ return { ok: false, error: `${item.category} items cannot be deleted` };
238
+ }
239
+
240
+ try {
241
+ switch (item.category) {
242
+ case "memory":
243
+ return await deleteMemory(item);
244
+ case "skill":
245
+ return await deleteSkill(item);
246
+ case "mcp":
247
+ return await deleteMcp(item);
248
+ default:
249
+ return { ok: false, error: `Unknown category: ${item.category}` };
250
+ }
251
+ } catch (err) {
252
+ return { ok: false, error: `Delete failed: ${err.message}` };
253
+ }
254
+ }
255
+
196
256
  /**
197
257
  * Get valid destination scopes for an item.
198
258
  * Returns only scopes where this category of item can live.
package/src/scanner.mjs CHANGED
@@ -49,7 +49,7 @@ function parseFrontmatter(content) {
49
49
 
50
50
  /**
51
51
  * Resolve an encoded project dir name back to a real filesystem path.
52
- * E.g. "-home-nicole-AlltrueAi-ai-security-control-plane" → "/home/nicole/AlltrueAi/ai-security-control-plane"
52
+ * E.g. "-home-user-mycompany-repo1" → "/home/user/mycompany/repo1"
53
53
  *
54
54
  * Strategy: starting from root, greedily match the longest existing directory
55
55
  * at each level by consuming segments from the encoded name.
@@ -120,7 +120,7 @@ async function discoverScopes() {
120
120
 
121
121
  // Decode encoded path: try to find the real directory on disk.
122
122
  // The encoding replaces / with - and prepends -.
123
- // E.g. -home-nicole-AlltrueAi-ai-security-control-plane → /home/nicole/AlltrueAi/ai-security-control-plane
123
+ // E.g. -home-user-mycompany-repo1 → /home/user/mycompany/repo1
124
124
  // Since directory names can contain dashes, we resolve by checking which real path exists.
125
125
  const realPath = await resolveEncodedProjectPath(d.name);
126
126
  if (!realPath) continue;
package/src/server.mjs CHANGED
@@ -8,7 +8,7 @@ import { createServer } from "node:http";
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { join, extname } from "node:path";
10
10
  import { scan } from "./scanner.mjs";
11
- import { moveItem, getValidDestinations } from "./mover.mjs";
11
+ import { moveItem, deleteItem, getValidDestinations } from "./mover.mjs";
12
12
 
13
13
  const UI_DIR = join(import.meta.dirname, "ui");
14
14
 
@@ -84,6 +84,22 @@ async function handleRequest(req, res) {
84
84
  return json(res, result, result.ok ? 200 : 400);
85
85
  }
86
86
 
87
+ // POST /api/delete — delete an item
88
+ if (path === "/api/delete" && req.method === "POST") {
89
+ const { itemPath } = await readBody(req);
90
+
91
+ if (!cachedData) await freshScan();
92
+
93
+ const item = cachedData.items.find(i => i.path === itemPath && !i.locked);
94
+ if (!item) return json(res, { ok: false, error: "Item not found or locked" }, 400);
95
+
96
+ const result = await deleteItem(item, cachedData.scopes);
97
+
98
+ if (result.ok) await freshScan();
99
+
100
+ return json(res, result, result.ok ? 200 : 400);
101
+ }
102
+
87
103
  // GET /api/destinations?path=...&category=... — valid move destinations
88
104
  if (path === "/api/destinations" && req.method === "GET") {
89
105
  if (!cachedData) await freshScan();
@@ -95,6 +111,55 @@ async function handleRequest(req, res) {
95
111
  return json(res, { ok: true, destinations, currentScopeId: item.scopeId });
96
112
  }
97
113
 
114
+ // POST /api/restore — restore a deleted file (for undo)
115
+ if (path === "/api/restore" && req.method === "POST") {
116
+ const { filePath, content, isDir } = await readBody(req);
117
+ if (!filePath || !filePath.startsWith("/")) {
118
+ return json(res, { ok: false, error: "Invalid path" }, 400);
119
+ }
120
+ try {
121
+ const { mkdir, writeFile: wf } = await import("node:fs/promises");
122
+ const { dirname } = await import("node:path");
123
+ await mkdir(dirname(filePath), { recursive: true });
124
+ if (isDir) {
125
+ // For skills: restore SKILL.md inside the directory
126
+ await mkdir(filePath, { recursive: true });
127
+ const skillPath = join(filePath, "SKILL.md");
128
+ await wf(skillPath, content, "utf-8");
129
+ } else {
130
+ await wf(filePath, content, "utf-8");
131
+ }
132
+ await freshScan();
133
+ return json(res, { ok: true, message: "Restored successfully" });
134
+ } catch (err) {
135
+ return json(res, { ok: false, error: `Restore failed: ${err.message}` }, 400);
136
+ }
137
+ }
138
+
139
+ // POST /api/restore-mcp — restore a deleted MCP server entry
140
+ if (path === "/api/restore-mcp" && req.method === "POST") {
141
+ const { name, config, mcpJsonPath } = await readBody(req);
142
+ if (!name || !config || !mcpJsonPath) {
143
+ return json(res, { ok: false, error: "Missing name, config, or mcpJsonPath" }, 400);
144
+ }
145
+ try {
146
+ let content = { mcpServers: {} };
147
+ try {
148
+ content = JSON.parse(await readFile(mcpJsonPath, "utf-8"));
149
+ if (!content.mcpServers) content.mcpServers = {};
150
+ } catch { /* file doesn't exist, start fresh */ }
151
+ content.mcpServers[name] = config;
152
+ const { writeFile: wf, mkdir: mk } = await import("node:fs/promises");
153
+ const { dirname } = await import("node:path");
154
+ await mk(dirname(mcpJsonPath), { recursive: true });
155
+ await wf(mcpJsonPath, JSON.stringify(content, null, 2) + "\n");
156
+ await freshScan();
157
+ return json(res, { ok: true, message: `Restored MCP server "${name}"` });
158
+ } catch (err) {
159
+ return json(res, { ok: false, error: `Restore failed: ${err.message}` }, 400);
160
+ }
161
+ }
162
+
98
163
  // GET /api/file-content?path=... — read file content for detail panel
99
164
  if (path === "/api/file-content" && req.method === "GET") {
100
165
  const filePath = url.searchParams.get("path");
@@ -121,6 +186,12 @@ async function handleRequest(req, res) {
121
186
  return serveFile(res, join(UI_DIR, "app.js"));
122
187
  }
123
188
 
189
+ // Suppress favicon 404
190
+ if (path === "/favicon.ico") {
191
+ res.writeHead(204);
192
+ return res.end();
193
+ }
194
+
124
195
  // ── 404 ──
125
196
  res.writeHead(404);
126
197
  res.end("Not found");
@@ -128,7 +199,7 @@ async function handleRequest(req, res) {
128
199
 
129
200
  // ── Start server ─────────────────────────────────────────────────────
130
201
 
131
- export function startServer(port = 3847) {
202
+ export function startServer(port = 3847, maxRetries = 10) {
132
203
  const server = createServer(async (req, res) => {
133
204
  try {
134
205
  await handleRequest(req, res);
@@ -139,9 +210,25 @@ export function startServer(port = 3847) {
139
210
  }
140
211
  });
141
212
 
142
- server.listen(port, () => {
143
- console.log(`Claude Inventory running at http://localhost:${port}`);
213
+ let attempt = 0;
214
+ function tryListen(p) {
215
+ server.listen(p, () => {
216
+ console.log(`Claude Code Organizer running at http://localhost:${p}`);
217
+ });
218
+ }
219
+
220
+ server.on("error", (err) => {
221
+ if (err.code === "EADDRINUSE" && attempt < maxRetries) {
222
+ attempt++;
223
+ const nextPort = port + attempt;
224
+ console.log(`Port ${port + attempt - 1} in use, trying ${nextPort}...`);
225
+ tryListen(nextPort);
226
+ } else {
227
+ console.error(`Failed to start server: ${err.message}`);
228
+ process.exit(1);
229
+ }
144
230
  });
145
231
 
232
+ tryListen(port);
146
233
  return server;
147
234
  }
package/src/ui/app.js CHANGED
@@ -14,6 +14,10 @@ let data = null; // { scopes, items, counts }
14
14
  let activeFilters = new Set(); // empty = show all, or set of "memory", "skill", "mcp", etc.
15
15
  let selectedItem = null; // currently selected item object
16
16
  let pendingDrag = null; // { item, fromScopeId, toScopeId, revertFn }
17
+ let pendingDelete = null; // item to delete
18
+ let draggingItem = null; // item currently being dragged
19
+ let expandState = { scopes: new Set(), cats: new Set() }; // track expanded sections
20
+ let bulkSelected = new Set(); // paths of selected items for bulk ops
17
21
 
18
22
  // ── Category config ──────────────────────────────────────────────────
19
23
 
@@ -44,6 +48,9 @@ async function init() {
44
48
  setupSearch();
45
49
  setupDetailPanel();
46
50
  setupModals();
51
+ setupBulkBar();
52
+ setupScopeDropZones();
53
+ setupExpandToggle();
47
54
  }
48
55
 
49
56
  async function fetchJson(url) {
@@ -155,6 +162,17 @@ function renderScope(scope, depth) {
155
162
  (categories[item.category] ??= []).push(item);
156
163
  }
157
164
 
165
+ // Sort memory items: feedback last, then alphabetical within each subType
166
+ if (categories.memory) {
167
+ const subTypeOrder = { project: 0, reference: 1, user: 2, feedback: 3 };
168
+ categories.memory.sort((a, b) => {
169
+ const oa = subTypeOrder[a.subType] ?? 2;
170
+ const ob = subTypeOrder[b.subType] ?? 2;
171
+ if (oa !== ob) return oa - ob;
172
+ return a.name.localeCompare(b.name);
173
+ });
174
+ }
175
+
158
176
  // Count sub-projects
159
177
  const subInfo = childScopes.length > 0 ? `${childScopes.length} sub-projects` : "";
160
178
 
@@ -205,15 +223,20 @@ function renderItem(item) {
205
223
  const icon = ITEM_ICONS[item.category] || "📄";
206
224
  const locked = item.locked ? " locked" : "";
207
225
  const badgeClass = `b-${item.subType || item.category}`;
226
+ const checked = bulkSelected.has(item.path) ? " checked" : "";
227
+
228
+ const checkbox = item.locked ? "" : `<input type="checkbox" class="row-chk" data-path="${esc(item.path)}"${checked}>`;
208
229
 
209
230
  const actions = item.locked ? "" : `
210
231
  <span class="row-acts">
211
232
  <button class="rbtn" data-action="move">Move</button>
212
233
  <button class="rbtn" data-action="open">Open</button>
234
+ <button class="rbtn rbtn-danger" data-action="delete">Delete</button>
213
235
  </span>`;
214
236
 
215
237
  return `
216
238
  <div class="item-row${locked}" data-path="${esc(item.path)}" data-category="${item.category}">
239
+ ${checkbox}
217
240
  <span class="row-ico">${icon}</span>
218
241
  <span class="row-name">${esc(item.name)}</span>
219
242
  <span class="row-badge ${badgeClass}">${esc(item.subType || item.category)}</span>
@@ -241,10 +264,58 @@ function esc(s) {
241
264
 
242
265
  // ── SortableJS init ──────────────────────────────────────────────────
243
266
 
267
+ function saveExpandState() {
268
+ expandState.scopes.clear();
269
+ expandState.cats.clear();
270
+ document.querySelectorAll(".scope-hdr").forEach(hdr => {
271
+ const body = hdr.nextElementSibling;
272
+ if (body && !body.classList.contains("c")) {
273
+ expandState.scopes.add(hdr.dataset.scopeId);
274
+ }
275
+ });
276
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
277
+ const body = hdr.nextElementSibling;
278
+ const scopeId = hdr.closest(".scope-block")?.querySelector(".scope-hdr")?.dataset.scopeId || "";
279
+ const catKey = `${scopeId}::${hdr.dataset.cat}`;
280
+ if (body && !body.classList.contains("c")) {
281
+ expandState.cats.add(catKey);
282
+ }
283
+ });
284
+ }
285
+
286
+ function restoreExpandState() {
287
+ // Scopes default open — collapse those NOT in saved state (only if we have saved state)
288
+ const hasSavedState = expandState.scopes.size > 0 || expandState.cats.size > 0;
289
+ document.querySelectorAll(".scope-hdr").forEach(hdr => {
290
+ const body = hdr.nextElementSibling;
291
+ const tog = hdr.querySelector(".scope-tog");
292
+ if (hasSavedState && !expandState.scopes.has(hdr.dataset.scopeId)) {
293
+ body?.classList.add("c");
294
+ tog?.classList.add("c");
295
+ }
296
+ });
297
+
298
+ // Categories default collapsed — expand those in saved state
299
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
300
+ const body = hdr.nextElementSibling;
301
+ const tog = hdr.querySelector(".cat-tog");
302
+ const scopeId = hdr.closest(".scope-block")?.querySelector(".scope-hdr")?.dataset.scopeId || "";
303
+ const catKey = `${scopeId}::${hdr.dataset.cat}`;
304
+
305
+ if (expandState.cats.has(catKey)) {
306
+ body?.classList.remove("c");
307
+ tog?.classList.remove("c");
308
+ } else {
309
+ body?.classList.add("c");
310
+ tog?.classList.add("c");
311
+ }
312
+ });
313
+ }
314
+
244
315
  function initSortable() {
245
316
  document.querySelectorAll(".sortable-zone").forEach(el => {
246
317
  const group = el.dataset.group;
247
- if (!group || group === "none") return; // non-movable categories
318
+ if (!group || group === "none") return;
248
319
 
249
320
  Sortable.create(el, {
250
321
  group,
@@ -257,7 +328,15 @@ function initSortable() {
257
328
  scrollSensitivity: 100,
258
329
  scrollSpeed: 15,
259
330
  bubbleScroll: true,
331
+ onStart(evt) {
332
+ const itemPath = evt.item.dataset.path;
333
+ draggingItem = data.items.find(i => i.path === itemPath);
334
+ },
260
335
  onEnd(evt) {
336
+ draggingItem = null;
337
+ // Remove all drop-target highlights
338
+ document.querySelectorAll(".scope-block.drop-target").forEach(b => b.classList.remove("drop-target"));
339
+
261
340
  if (evt.from === evt.to) return;
262
341
 
263
342
  const itemEl = evt.item;
@@ -270,7 +349,6 @@ function initSortable() {
270
349
  const fromScope = data.scopes.find(s => s.id === fromScopeId);
271
350
  const toScope = data.scopes.find(s => s.id === toScopeId);
272
351
 
273
- // Revert function
274
352
  const oldParent = evt.from;
275
353
  const oldIndex = evt.oldIndex;
276
354
  const revertFn = () => {
@@ -278,14 +356,13 @@ function initSortable() {
278
356
  else oldParent.insertBefore(itemEl, oldParent.children[oldIndex]);
279
357
  };
280
358
 
281
- // Show confirm with full details
282
359
  pendingDrag = { item, fromScopeId, toScopeId, revertFn };
283
360
  showDragConfirm(item, fromScope, toScope);
284
361
  }
285
362
  });
286
363
  });
287
364
 
288
- // Scope header toggle — default OPEN (show structure)
365
+ // Scope header toggle — default OPEN
289
366
  document.querySelectorAll(".scope-hdr").forEach(hdr => {
290
367
  hdr.addEventListener("click", () => {
291
368
  const body = hdr.nextElementSibling;
@@ -295,14 +372,13 @@ function initSortable() {
295
372
  });
296
373
  });
297
374
 
298
- // Category toggle — default COLLAPSED (hide items)
299
- document.querySelectorAll(".cat-hdr").forEach(hdr => {
300
- const body = hdr.nextElementSibling;
301
- const tog = hdr.querySelector(".cat-tog");
302
- body.classList.add("c");
303
- tog.classList.add("c");
375
+ // Category toggle — restore state or default collapsed
376
+ restoreExpandState();
304
377
 
378
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
305
379
  hdr.addEventListener("click", () => {
380
+ const body = hdr.nextElementSibling;
381
+ const tog = hdr.querySelector(".cat-tog");
306
382
  body.classList.toggle("c");
307
383
  tog.classList.toggle("c");
308
384
  });
@@ -311,7 +387,7 @@ function initSortable() {
311
387
  // Item click → detail panel
312
388
  document.querySelectorAll(".item-row").forEach(row => {
313
389
  row.addEventListener("click", (e) => {
314
- if (e.target.closest(".rbtn")) return; // don't trigger on button click
390
+ if (e.target.closest(".rbtn")) return;
315
391
  const path = row.dataset.path;
316
392
  const item = data.items.find(i => i.path === path);
317
393
  if (item) showDetail(item, row);
@@ -332,6 +408,229 @@ function initSortable() {
332
408
  openMoveModal(item);
333
409
  } else if (btn.dataset.action === "open") {
334
410
  window.open(`vscode://file${item.path}`, "_blank");
411
+ } else if (btn.dataset.action === "delete") {
412
+ openDeleteModal(item);
413
+ }
414
+ });
415
+ });
416
+
417
+ // Checkbox handlers for bulk select
418
+ document.querySelectorAll(".row-chk").forEach(chk => {
419
+ chk.addEventListener("change", (e) => {
420
+ e.stopPropagation();
421
+ const path = chk.dataset.path;
422
+ if (chk.checked) bulkSelected.add(path);
423
+ else bulkSelected.delete(path);
424
+ updateBulkBar();
425
+ });
426
+ chk.addEventListener("click", (e) => e.stopPropagation());
427
+ });
428
+ }
429
+
430
+ // ── Bulk operations ─────────────────────────────────────────────────
431
+
432
+ function updateBulkBar() {
433
+ const bar = document.getElementById("bulkBar");
434
+ if (bulkSelected.size === 0) {
435
+ bar.classList.add("hidden");
436
+ return;
437
+ }
438
+ bar.classList.remove("hidden");
439
+ document.getElementById("bulkCount").textContent = `${bulkSelected.size} selected`;
440
+ }
441
+
442
+ function setupBulkBar() {
443
+ document.getElementById("bulkClear").addEventListener("click", () => {
444
+ bulkSelected.clear();
445
+ document.querySelectorAll(".row-chk").forEach(c => c.checked = false);
446
+ updateBulkBar();
447
+ });
448
+
449
+ document.getElementById("bulkDelete").addEventListener("click", async () => {
450
+ if (bulkSelected.size === 0) return;
451
+ const count = bulkSelected.size;
452
+ const paths = [...bulkSelected];
453
+
454
+ // Confirm
455
+ if (!confirm(`Delete ${count} item(s)? This cannot be undone.`)) return;
456
+
457
+ let ok = 0, fail = 0;
458
+ for (const p of paths) {
459
+ const result = await doDelete(p, true); // true = skip refresh
460
+ if (result.ok) ok++;
461
+ else fail++;
462
+ }
463
+
464
+ bulkSelected.clear();
465
+ await refreshUI();
466
+ toast(`Deleted ${ok} item(s)${fail ? `, ${fail} failed` : ""}`);
467
+ });
468
+
469
+ document.getElementById("bulkMove").addEventListener("click", async () => {
470
+ if (bulkSelected.size === 0) return;
471
+ const paths = [...bulkSelected];
472
+
473
+ // All selected items must be same category for move
474
+ const items = paths.map(p => data.items.find(i => i.path === p)).filter(Boolean);
475
+ const categories = new Set(items.map(i => i.category));
476
+ if (categories.size > 1) {
477
+ return toast("Cannot bulk-move items of different types", true);
478
+ }
479
+
480
+ // Use first item to get destinations, then move all
481
+ openBulkMoveModal(items);
482
+ });
483
+ }
484
+
485
+ async function openBulkMoveModal(items) {
486
+ const first = items[0];
487
+ const res = await fetchJson(`/api/destinations?path=${encodeURIComponent(first.path)}`);
488
+ if (!res.ok) return toast(res.error, true);
489
+
490
+ const listEl = document.getElementById("moveDestList");
491
+ const allScopeMap = {};
492
+ for (const s of data.scopes) allScopeMap[s.id] = s;
493
+ for (const s of res.destinations) allScopeMap[s.id] = s;
494
+
495
+ function getDepth(scope) {
496
+ let depth = 0, cur = scope;
497
+ while (cur.parentId) { depth++; cur = allScopeMap[cur.parentId] || { parentId: null }; }
498
+ return depth;
499
+ }
500
+
501
+ const ordered = [];
502
+ function addWithChildren(parentId) {
503
+ for (const s of res.destinations) {
504
+ if ((s.parentId || null) === parentId) { ordered.push(s); addWithChildren(s.id); }
505
+ }
506
+ }
507
+ addWithChildren(null);
508
+
509
+ listEl.innerHTML = ordered.map(scope => {
510
+ const depth = getDepth(scope);
511
+ const indentPx = depth > 0 ? ` style="padding-left:${depth * 28}px"` : "";
512
+ const icon = scope.id === "global" ? "🌐" : (SCOPE_ICONS[scope.type] || "📂");
513
+ return `<div class="dest" data-scope-id="${esc(scope.id)}"${indentPx}>
514
+ <span class="di">${icon}</span>
515
+ <span class="dn">${esc(scope.name)}</span>
516
+ <span class="dp">${esc(scope.tag)}</span>
517
+ </div>`;
518
+ }).join("");
519
+
520
+ let selectedDest = null;
521
+ listEl.querySelectorAll(".dest").forEach(d => {
522
+ d.addEventListener("click", () => {
523
+ listEl.querySelectorAll(".dest").forEach(x => x.classList.remove("sel"));
524
+ d.classList.add("sel");
525
+ selectedDest = d.dataset.scopeId;
526
+ document.getElementById("moveConfirm").disabled = false;
527
+ });
528
+ });
529
+
530
+ document.getElementById("moveConfirm").disabled = true;
531
+ document.getElementById("moveConfirm").onclick = async () => {
532
+ if (!selectedDest) return;
533
+ closeMoveModal();
534
+ let ok = 0, fail = 0;
535
+ for (const item of items) {
536
+ const result = await doMove(item.path, selectedDest, true); // true = skip refresh
537
+ if (result.ok) ok++;
538
+ else fail++;
539
+ }
540
+ bulkSelected.clear();
541
+ await refreshUI();
542
+ toast(`Moved ${ok} item(s)${fail ? `, ${fail} failed` : ""}`);
543
+ };
544
+
545
+ document.getElementById("moveModal").classList.remove("hidden");
546
+ }
547
+
548
+ // ── Scope card drop zones (document-level, bypass SortableJS) ────────
549
+ // SortableJS intercepts per-element dragover events on sortable containers.
550
+ // Using document-level listeners in capture phase ensures highlighting works.
551
+
552
+ function setupScopeDropZones() {
553
+ document.addEventListener("dragover", (e) => {
554
+ if (!draggingItem) return;
555
+
556
+ // Find the innermost scope-block under cursor
557
+ const scopeBlock = e.target.closest(".scope-block");
558
+
559
+ // Clear all highlights
560
+ document.querySelectorAll(".scope-block.drop-target").forEach(b => b.classList.remove("drop-target"));
561
+
562
+ if (scopeBlock) {
563
+ const hdr = scopeBlock.querySelector(":scope > .scope-hdr");
564
+ const scopeId = hdr?.dataset.scopeId;
565
+ if (scopeId && scopeId !== draggingItem.scopeId) {
566
+ e.preventDefault();
567
+ scopeBlock.classList.add("drop-target");
568
+ }
569
+ }
570
+ }, true); // capture phase
571
+
572
+ document.addEventListener("drop", (e) => {
573
+ if (!draggingItem) return;
574
+
575
+ const scopeBlock = e.target.closest(".scope-block");
576
+ document.querySelectorAll(".scope-block.drop-target").forEach(b => b.classList.remove("drop-target"));
577
+
578
+ if (!scopeBlock) return;
579
+ const hdr = scopeBlock.querySelector(":scope > .scope-hdr");
580
+ const scopeId = hdr?.dataset.scopeId;
581
+ if (!scopeId || scopeId === draggingItem.scopeId) return;
582
+
583
+ // If drop landed inside a sortable zone, SortableJS onEnd handles it
584
+ if (e.target.closest(".sortable-zone")) return;
585
+
586
+ e.preventDefault();
587
+ e.stopPropagation();
588
+
589
+ const item = draggingItem;
590
+ const fromScope = data.scopes.find(s => s.id === item.scopeId);
591
+ const toScope = data.scopes.find(s => s.id === scopeId);
592
+
593
+ pendingDrag = { item, fromScopeId: item.scopeId, toScopeId: scopeId, revertFn: () => {} };
594
+ showDragConfirm(item, fromScope, toScope);
595
+ draggingItem = null;
596
+ }, true); // capture phase
597
+
598
+ document.addEventListener("dragend", () => {
599
+ draggingItem = null;
600
+ document.querySelectorAll(".scope-block.drop-target").forEach(b => b.classList.remove("drop-target"));
601
+ }, true); // capture phase
602
+ }
603
+
604
+ // ── Expand/Collapse toggle ───────────────────────────────────────────
605
+
606
+ let allExpanded = false;
607
+
608
+ function setupExpandToggle() {
609
+ const btn = document.getElementById("expandToggle");
610
+ btn.addEventListener("click", () => {
611
+ allExpanded = !allExpanded;
612
+ btn.innerHTML = allExpanded
613
+ ? '<span class="toggle-icon expanded"></span> Collapse all'
614
+ : '<span class="toggle-icon"></span> Expand all';
615
+
616
+ // Scopes always stay open — only categories toggle
617
+ // Expand all = categories open; Collapse all = categories closed (default)
618
+ document.querySelectorAll(".scope-hdr").forEach(hdr => {
619
+ const body = hdr.nextElementSibling;
620
+ const tog = hdr.querySelector(".scope-tog");
621
+ body?.classList.remove("c");
622
+ tog?.classList.remove("c");
623
+ });
624
+
625
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
626
+ const body = hdr.nextElementSibling;
627
+ const tog = hdr.querySelector(".cat-tog");
628
+ if (allExpanded) {
629
+ body?.classList.remove("c");
630
+ tog?.classList.remove("c");
631
+ } else {
632
+ body?.classList.add("c");
633
+ tog?.classList.add("c");
335
634
  }
336
635
  });
337
636
  });
@@ -342,10 +641,63 @@ function initSortable() {
342
641
  function setupSearch() {
343
642
  document.getElementById("searchInput").addEventListener("input", function () {
344
643
  const q = this.value.toLowerCase();
644
+ const btn = document.getElementById("expandToggle");
645
+
646
+ // Auto-expand when searching, collapse back when cleared
647
+ if (q && !allExpanded) {
648
+ allExpanded = true;
649
+ btn.innerHTML = '<span class="toggle-icon expanded"></span> Collapse all';
650
+ document.querySelectorAll(".scope-hdr").forEach(hdr => {
651
+ hdr.nextElementSibling?.classList.remove("c");
652
+ hdr.querySelector(".scope-tog")?.classList.remove("c");
653
+ });
654
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
655
+ hdr.nextElementSibling?.classList.remove("c");
656
+ hdr.querySelector(".cat-tog")?.classList.remove("c");
657
+ });
658
+ } else if (!q && allExpanded) {
659
+ allExpanded = false;
660
+ btn.innerHTML = '<span class="toggle-icon"></span> Expand all';
661
+ document.querySelectorAll(".cat-hdr").forEach(hdr => {
662
+ hdr.nextElementSibling?.classList.add("c");
663
+ hdr.querySelector(".cat-tog")?.classList.add("c");
664
+ });
665
+ }
666
+
667
+ // 1. Show/hide individual item rows
345
668
  document.querySelectorAll(".item-row").forEach(row => {
346
669
  const text = row.textContent.toLowerCase();
347
670
  row.style.display = (!q || text.includes(q)) ? "" : "none";
348
671
  });
672
+
673
+ // 2. Hide category sections where all items are hidden
674
+ document.querySelectorAll(".cat-hdr").forEach(catHdr => {
675
+ const catBody = catHdr.nextElementSibling;
676
+ if (!catBody) return;
677
+ const rows = catBody.querySelectorAll(".item-row");
678
+ const anyVisible = rows.length === 0 || [...rows].some(r => r.style.display !== "none");
679
+ catHdr.style.display = anyVisible ? "" : "none";
680
+ catBody.style.display = anyVisible ? "" : "none";
681
+ });
682
+
683
+ // 3. Hide scope blocks bottom-up (deepest first so parents see child visibility)
684
+ const allBlocks = [...document.querySelectorAll(".scope-block")];
685
+ allBlocks.reverse().forEach(block => {
686
+ const hdr = block.querySelector(":scope > .scope-hdr");
687
+ const body = block.querySelector(":scope > .scope-body");
688
+ if (!hdr || !body) return;
689
+
690
+ // Check if any direct category content is visible
691
+ const catHdrs = body.querySelectorAll(":scope > .cat-hdr");
692
+ const anyCatVisible = [...catHdrs].some(ch => ch.style.display !== "none");
693
+
694
+ // Check if any child scope-block is visible
695
+ const childScopes = body.querySelectorAll(":scope > .child-scopes > .scope-block");
696
+ const anyChildVisible = [...childScopes].some(cb => cb.style.display !== "none");
697
+
698
+ const visible = !q || anyCatVisible || anyChildVisible;
699
+ block.style.display = visible ? "" : "none";
700
+ });
349
701
  });
350
702
  }
351
703
 
@@ -359,6 +711,9 @@ function setupDetailPanel() {
359
711
  document.getElementById("detailMove").addEventListener("click", () => {
360
712
  if (selectedItem && !selectedItem.locked) openMoveModal(selectedItem);
361
713
  });
714
+ document.getElementById("detailDelete").addEventListener("click", () => {
715
+ if (selectedItem && !selectedItem.locked) openDeleteModal(selectedItem);
716
+ });
362
717
  }
363
718
 
364
719
  function showDetail(item, rowEl) {
@@ -375,12 +730,56 @@ function showDetail(item, rowEl) {
375
730
  document.getElementById("detailDate").textContent = item.mtime || "—";
376
731
  document.getElementById("detailPath").textContent = item.path;
377
732
 
378
- // Show/hide move button
733
+ // Show/hide move and delete buttons
379
734
  document.getElementById("detailMove").style.display = item.locked ? "none" : "";
735
+ document.getElementById("detailDelete").style.display = item.locked ? "none" : "";
380
736
 
381
737
  // Highlight row
382
738
  document.querySelectorAll(".item-row.selected").forEach(r => r.classList.remove("selected"));
383
739
  if (rowEl) rowEl.classList.add("selected");
740
+
741
+ // Load preview
742
+ loadPreview(item);
743
+ }
744
+
745
+ async function loadPreview(item) {
746
+ const el = document.getElementById("previewContent");
747
+ el.textContent = "Loading...";
748
+
749
+ try {
750
+ // MCP: show config directly from item data
751
+ if (item.category === "mcp") {
752
+ el.textContent = JSON.stringify(item.mcpConfig || {}, null, 2);
753
+ return;
754
+ }
755
+
756
+ // Hook: show command/prompt inline
757
+ if (item.category === "hook") {
758
+ el.textContent = item.description || "(no content)";
759
+ return;
760
+ }
761
+
762
+ // Plugin: directory, no single file to preview
763
+ if (item.category === "plugin") {
764
+ el.textContent = `Plugin directory: ${item.path}`;
765
+ return;
766
+ }
767
+
768
+ // Skill: read SKILL.md inside the directory
769
+ let filePath = item.path;
770
+ if (item.category === "skill") {
771
+ filePath = item.path + "/SKILL.md";
772
+ }
773
+
774
+ const res = await fetchJson(`/api/file-content?path=${encodeURIComponent(filePath)}`);
775
+ if (res.ok) {
776
+ el.textContent = res.content;
777
+ } else {
778
+ el.textContent = res.error || "Cannot load preview";
779
+ }
780
+ } catch {
781
+ el.textContent = "Failed to load preview";
782
+ }
384
783
  }
385
784
 
386
785
  function closeDetail() {
@@ -427,6 +826,26 @@ function showDragConfirm(item, fromScope, toScope) {
427
826
 
428
827
  // ── Modals ───────────────────────────────────────────────────────────
429
828
 
829
+ function openDeleteModal(item) {
830
+ pendingDelete = item;
831
+ const catConfig = CATEGORIES[item.category] || { icon: "📄", label: item.category };
832
+ const scope = data.scopes.find(s => s.id === item.scopeId);
833
+
834
+ document.getElementById("deletePreview").innerHTML = `
835
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
836
+ <span style="font-size:1.1rem;">${catConfig.icon}</span>
837
+ <div>
838
+ <div style="font-weight:700;color:var(--text-primary);font-size:0.88rem;">${esc(item.name)}</div>
839
+ <div style="font-size:0.65rem;color:var(--text-muted);margin-top:2px;">${esc(scope?.name || item.scopeId)} · ${esc(item.category)}</div>
840
+ </div>
841
+ </div>
842
+ <div style="font-size:0.68rem;color:#dc2626;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-light);">
843
+ ${item.category === "skill" ? "This will delete the entire skill folder and all its files." : "This will permanently delete the file."}
844
+ </div>`;
845
+
846
+ document.getElementById("deleteModal").classList.remove("hidden");
847
+ }
848
+
430
849
  function setupModals() {
431
850
  // Drag confirm
432
851
  document.getElementById("dcCancel").addEventListener("click", () => {
@@ -455,6 +874,25 @@ function setupModals() {
455
874
  pendingDrag = null;
456
875
  }
457
876
  });
877
+
878
+ // Delete modal
879
+ document.getElementById("deleteCancel").addEventListener("click", () => {
880
+ document.getElementById("deleteModal").classList.add("hidden");
881
+ pendingDelete = null;
882
+ });
883
+ document.getElementById("deleteConfirm").addEventListener("click", async () => {
884
+ document.getElementById("deleteModal").classList.add("hidden");
885
+ if (pendingDelete) {
886
+ await doDelete(pendingDelete.path);
887
+ pendingDelete = null;
888
+ }
889
+ });
890
+ document.getElementById("deleteModal").addEventListener("click", (e) => {
891
+ if (e.target === document.getElementById("deleteModal")) {
892
+ document.getElementById("deleteModal").classList.add("hidden");
893
+ pendingDelete = null;
894
+ }
895
+ });
458
896
  }
459
897
 
460
898
  async function openMoveModal(item) {
@@ -545,7 +983,19 @@ function closeMoveModal() {
545
983
 
546
984
  // ── API calls ────────────────────────────────────────────────────────
547
985
 
548
- async function doMove(itemPath, toScopeId) {
986
+ async function refreshUI() {
987
+ saveExpandState();
988
+ data = await fetchJson("/api/scan");
989
+ renderPills();
990
+ renderTree();
991
+ closeDetail();
992
+ }
993
+
994
+ async function doMove(itemPath, toScopeId, skipRefresh = false) {
995
+ // Find item before move for undo info
996
+ const item = data.items.find(i => i.path === itemPath);
997
+ const fromScopeId = item?.scopeId;
998
+
549
999
  const response = await fetch("/api/move", {
550
1000
  method: "POST",
551
1001
  headers: { "Content-Type": "application/json" },
@@ -553,27 +1003,115 @@ async function doMove(itemPath, toScopeId) {
553
1003
  });
554
1004
  const result = await response.json();
555
1005
 
556
- if (result.ok) {
557
- toast(result.message);
558
- // Refresh everything
559
- data = await fetchJson("/api/scan");
560
- renderPills();
561
- renderTree();
562
- closeDetail();
563
- } else {
564
- toast(result.error, true);
1006
+ if (!skipRefresh) {
1007
+ if (result.ok) {
1008
+ const undoFn = async () => {
1009
+ // Move back: result.to is the new path, fromScopeId is where it came from
1010
+ const undoResult = await fetch("/api/move", {
1011
+ method: "POST",
1012
+ headers: { "Content-Type": "application/json" },
1013
+ body: JSON.stringify({ itemPath: result.to, toScopeId: fromScopeId }),
1014
+ }).then(r => r.json());
1015
+ if (undoResult.ok) { toast("Move undone"); await refreshUI(); }
1016
+ else toast(undoResult.error, true);
1017
+ };
1018
+ toast(result.message, false, undoFn);
1019
+ await refreshUI();
1020
+ } else {
1021
+ toast(result.error, true);
1022
+ }
565
1023
  }
566
1024
 
567
1025
  return result;
568
1026
  }
569
1027
 
570
- // ── Toast ────────────────────────────────────────────────────────────
1028
+ async function doDelete(itemPath, skipRefresh = false) {
1029
+ // Backup content before delete for undo
1030
+ const item = data.items.find(i => i.path === itemPath);
1031
+ let backupContent = null;
1032
+ let mcpBackup = null;
1033
+ if (item) {
1034
+ try {
1035
+ if (item.category === "mcp") {
1036
+ // MCP: backup the server config from item data
1037
+ mcpBackup = { name: item.name, config: item.mcpConfig, mcpJsonPath: item.path };
1038
+ } else {
1039
+ let readPath = item.path;
1040
+ if (item.category === "skill") readPath = item.path + "/SKILL.md";
1041
+ const backup = await fetchJson(`/api/file-content?path=${encodeURIComponent(readPath)}`);
1042
+ if (backup.ok) backupContent = backup.content;
1043
+ }
1044
+ } catch { /* best effort */ }
1045
+ }
571
1046
 
572
- function toast(msg, isError = false) {
1047
+ const response = await fetch("/api/delete", {
1048
+ method: "POST",
1049
+ headers: { "Content-Type": "application/json" },
1050
+ body: JSON.stringify({ itemPath }),
1051
+ });
1052
+ const result = await response.json();
1053
+
1054
+ if (!skipRefresh) {
1055
+ if (result.ok) {
1056
+ let undoFn = null;
1057
+ if (mcpBackup) {
1058
+ // MCP undo: re-add the server entry to .mcp.json
1059
+ undoFn = async () => {
1060
+ const restoreResult = await fetch("/api/restore-mcp", {
1061
+ method: "POST",
1062
+ headers: { "Content-Type": "application/json" },
1063
+ body: JSON.stringify(mcpBackup),
1064
+ }).then(r => r.json());
1065
+ if (restoreResult.ok) { toast("Delete undone"); await refreshUI(); }
1066
+ else toast(restoreResult.error, true);
1067
+ };
1068
+ } else if (backupContent) {
1069
+ undoFn = async () => {
1070
+ const restoreResult = await fetch("/api/restore", {
1071
+ method: "POST",
1072
+ headers: { "Content-Type": "application/json" },
1073
+ body: JSON.stringify({
1074
+ filePath: item.path,
1075
+ content: backupContent,
1076
+ isDir: item.category === "skill",
1077
+ }),
1078
+ }).then(r => r.json());
1079
+ if (restoreResult.ok) { toast("Delete undone"); await refreshUI(); }
1080
+ else toast(restoreResult.error, true);
1081
+ };
1082
+ }
1083
+ toast(result.message, false, undoFn);
1084
+ await refreshUI();
1085
+ } else {
1086
+ toast(result.error, true);
1087
+ }
1088
+ }
1089
+
1090
+ return result;
1091
+ }
1092
+
1093
+ // ── Toast with optional Undo ─────────────────────────────────────────
1094
+
1095
+ let toastTimer = null;
1096
+
1097
+ function toast(msg, isError = false, undoFn = null) {
573
1098
  const el = document.getElementById("toast");
574
- document.getElementById("toastMsg").textContent = msg;
575
- el.className = isError ? "toast error" : "toast";
576
- setTimeout(() => el.classList.add("hidden"), 4000);
1099
+ if (toastTimer) clearTimeout(toastTimer);
1100
+
1101
+ if (undoFn) {
1102
+ document.getElementById("toastMsg").innerHTML =
1103
+ `${esc(msg)} <button class="toast-undo" id="toastUndo">Undo</button>`;
1104
+ el.className = "toast";
1105
+ document.getElementById("toastUndo").onclick = async () => {
1106
+ el.classList.add("hidden");
1107
+ await undoFn();
1108
+ };
1109
+ toastTimer = setTimeout(() => el.classList.add("hidden"), 8000); // longer for undo
1110
+ } else {
1111
+ document.getElementById("toastMsg").textContent = msg;
1112
+ el.className = isError ? "toast error" : "toast";
1113
+ toastTimer = setTimeout(() => el.classList.add("hidden"), 4000);
1114
+ }
577
1115
  }
578
1116
 
579
1117
  // ── Start ────────────────────────────────────────────────────────────
package/src/ui/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>Claude Inventory</title>
6
+ <title>Claude Code Organizer</title>
7
7
  <link rel="stylesheet" href="/style.css">
8
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
9
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
@@ -13,13 +13,16 @@
13
13
  <div class="tree-area">
14
14
  <!-- Header -->
15
15
  <div class="page-hdr">
16
- <div class="logo">C</div>
17
- <h1>Claude Inventory</h1>
16
+ <div class="logo"><svg width="20" height="20" viewBox="0 0 248 248" fill="none"><path d="M52.4285 162.873L98.7844 136.879L99.5485 134.602L98.7844 133.334H96.4921L88.7237 132.862L62.2346 132.153L39.3113 131.207L17.0249 130.026L11.4214 128.844L6.2 121.873L6.7094 118.447L11.4214 115.257L18.171 115.847L33.0711 116.911L55.485 118.447L71.6586 119.392L95.728 121.873H99.5485L100.058 120.337L98.7844 119.392L97.7656 118.447L74.5877 102.732L49.4995 86.1905L36.3823 76.62L29.3779 71.7757L25.8121 67.2858L24.2839 57.3608L30.6515 50.2716L39.3113 50.8623L41.4763 51.4531L50.2636 58.1879L68.9842 72.7209L93.4357 90.6804L97.0015 93.6343L98.4374 92.6652L98.6571 91.9801L97.0015 89.2625L83.757 65.2772L69.621 40.8192L63.2534 30.6579L61.5978 24.632C60.9565 22.1032 60.579 20.0111 60.579 17.4246L67.8381 7.49965L71.9133 6.19995L81.7193 7.49965L85.7946 11.0443L91.9074 24.9865L101.714 46.8451L116.996 76.62L121.453 85.4816L123.873 93.6343L124.764 96.1155H126.292V94.6976L127.566 77.9197L129.858 57.3608L132.15 30.8942L132.915 23.4505L136.608 14.4708L143.994 9.62643L149.725 12.344L154.437 19.0788L153.8 23.4505L150.998 41.6463L145.522 70.1215L141.957 89.2625H143.994L146.414 86.7813L156.093 74.0206L172.266 53.698L179.398 45.6635L187.803 36.802L193.152 32.5484H203.34L210.726 43.6549L207.415 55.1159L196.972 68.3492L188.312 79.5739L175.896 96.2095L168.191 109.585L168.882 110.689L170.738 110.53L198.755 104.504L213.91 101.787L231.994 98.7149L240.144 102.496L241.036 106.395L237.852 114.311L218.495 119.037L195.826 123.645L162.07 131.592L161.696 131.893L162.137 132.547L177.36 133.925L183.855 134.279H199.774L229.447 136.524L237.215 141.605L241.8 147.867L241.036 152.711L229.065 158.737L213.019 154.956L175.45 145.977L162.587 142.787H160.805V143.85L171.502 154.366L191.242 172.089L215.82 195.011L217.094 200.682L213.91 205.172L210.599 204.699L188.949 188.394L180.544 181.069L161.696 165.118H160.422V166.772L164.752 173.152L187.803 207.771L188.949 218.405L187.294 221.832L181.308 223.959L174.813 222.777L161.187 203.754L147.305 182.486L136.098 163.345L134.745 164.2L128.075 235.42L125.019 239.082L117.887 241.8L111.902 237.31L108.718 229.984L111.902 215.452L115.722 196.547L118.779 181.541L121.58 162.873L123.291 156.636L123.14 156.219L121.773 156.449L107.699 175.752L86.304 204.699L69.3663 222.777L65.291 224.431L58.2867 220.768L58.9235 214.27L62.8713 208.48L86.304 178.705L100.44 160.155L109.551 149.507L109.462 147.967L108.959 147.924L46.6977 188.512L35.6182 189.93L30.7788 185.44L31.4156 178.115L33.7079 175.752L52.4285 162.873Z" fill="#fff"/></svg></div>
17
+ <h1>Claude Code Organizer</h1>
18
18
  <span class="spacer"></span>
19
19
  <div class="pills" id="pills">
20
20
  <!-- Rendered by app.js -->
21
21
  </div>
22
- <input class="search" placeholder="Search..." id="searchInput">
22
+ <div class="hdr-actions">
23
+ <input class="search" placeholder="Search name, type, description..." id="searchInput">
24
+ <button class="expand-toggle" id="expandToggle" title="Expand / Collapse all"><span class="toggle-icon"></span> Expand all</button>
25
+ </div>
23
26
  </div>
24
27
 
25
28
  <!-- Tree content — rendered by app.js -->
@@ -37,17 +40,24 @@
37
40
  <h2 id="detailTitle">—</h2>
38
41
  <button class="detail-x" id="detailClose">&times;</button>
39
42
  </div>
40
- <div class="detail-bd">
43
+ <div class="detail-meta">
41
44
  <div class="df"><div class="dl">Type</div><div class="dv" id="detailType">—</div></div>
42
45
  <div class="df"><div class="dl">Scope</div><div class="dv" id="detailScope">—</div></div>
43
46
  <div class="df"><div class="dl">Description</div><div class="dv" id="detailDesc">—</div></div>
44
- <div class="df"><div class="dl">Size</div><div class="dv" id="detailSize">—</div></div>
45
- <div class="df"><div class="dl">Modified</div><div class="dv" id="detailDate">—</div></div>
47
+ <div class="df-row">
48
+ <div class="df"><div class="dl">Size</div><div class="dv" id="detailSize">—</div></div>
49
+ <div class="df"><div class="dl">Modified</div><div class="dv" id="detailDate">—</div></div>
50
+ </div>
46
51
  <div class="df"><div class="dl">Path</div><div class="dv" id="detailPath" style="font-size:0.65rem;word-break:break-all;">—</div></div>
47
52
  </div>
53
+ <div class="detail-preview">
54
+ <div class="preview-hdr"><span class="preview-label">PREVIEW</span></div>
55
+ <pre class="preview-content" id="previewContent">Select an item to preview</pre>
56
+ </div>
48
57
  <div class="detail-ft">
49
58
  <button class="btn btn-p" id="detailOpen">Open in Editor</button>
50
59
  <button class="btn btn-s" id="detailMove">Move to...</button>
60
+ <button class="btn btn-danger" id="detailDelete">Delete</button>
51
61
  </div>
52
62
  </div>
53
63
  </div>
@@ -80,6 +90,29 @@
80
90
  </div>
81
91
  </div>
82
92
 
93
+ <!-- Delete confirm modal -->
94
+ <div class="modal-bg hidden" id="deleteModal">
95
+ <div class="modal">
96
+ <h3>Delete Item</h3>
97
+ <div class="modal-sub">This will permanently delete the file from disk</div>
98
+ <div class="move-preview" id="deletePreview">
99
+ <!-- Rendered by app.js -->
100
+ </div>
101
+ <div class="modal-btns">
102
+ <button class="btn btn-s" id="deleteCancel">Cancel</button>
103
+ <button class="btn btn-danger" id="deleteConfirm">Delete</button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Bulk action bar -->
109
+ <div class="bulk-bar hidden" id="bulkBar">
110
+ <span class="bulk-count" id="bulkCount">0 selected</span>
111
+ <button class="btn btn-p btn-sm" id="bulkMove">Move all</button>
112
+ <button class="btn btn-danger btn-sm" id="bulkDelete">Delete all</button>
113
+ <button class="btn btn-s btn-sm" id="bulkClear">Clear</button>
114
+ </div>
115
+
83
116
  <!-- Toast -->
84
117
  <div class="toast hidden" id="toast"><span id="toastMsg">Done</span></div>
85
118
 
package/src/ui/style.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ══════════════════════════════════════════════════════════
2
- Claude Inventory Manager — Styles
2
+ Claude Code Organizer — Styles
3
3
  Edit this file freely. No logic here.
4
4
  ══════════════════════════════════════════════════════════ */
5
5
 
@@ -52,7 +52,12 @@ body {
52
52
  .logo { width:32px; height:32px; background:var(--accent); border-radius:8px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:16px; font-weight:700; flex-shrink:0; }
53
53
  .page-hdr h1 { font-size:1.25rem; color:var(--text-primary); font-weight:700; letter-spacing:-0.4px; }
54
54
  .spacer { flex:1; }
55
- .search { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:7px 14px; color:var(--text-primary); font-size:0.78rem; width:200px; font-family:inherit; outline:none; transition:all 0.15s; }
55
+ .hdr-actions { display:flex; align-items:center; gap:6px; flex-shrink:0; }
56
+ .expand-toggle { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:7px 12px; color:var(--text-secondary); font-size:0.72rem; cursor:pointer; font-family:inherit; font-weight:500; transition:all 0.15s; white-space:nowrap; display:inline-flex; align-items:center; gap:4px; }
57
+ .expand-toggle:hover { background:var(--hover); border-color:var(--scope-border); }
58
+ .toggle-icon { display:inline-block; width:0; height:0; border-top:4px solid transparent; border-bottom:4px solid transparent; border-left:6px solid var(--text-muted); transition:transform 0.15s; }
59
+ .toggle-icon.expanded { transform:rotate(90deg); }
60
+ .search { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:7px 14px; color:var(--text-primary); font-size:0.78rem; width:260px; font-family:inherit; outline:none; transition:all 0.15s; }
56
61
  .search:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-light); }
57
62
  .search::placeholder { color:var(--text-faint); }
58
63
  .pills { display:flex; gap:4px; flex-wrap:wrap; }
@@ -89,6 +94,14 @@ body {
89
94
  .scope-body { padding:0 0 6px; }
90
95
  .scope-body.c { display:none; }
91
96
 
97
+ /* Drop zone highlighting — entire scope card lights up */
98
+ .scope-block.drop-target > .scope-hdr {
99
+ background:var(--accent-light);
100
+ border-color:var(--accent);
101
+ box-shadow:0 0 0 3px rgba(91,95,199,0.15);
102
+ transition:all 0.12s;
103
+ }
104
+
92
105
  .inherit {
93
106
  font-size:0.68rem; color:var(--accent-text); padding:6px 14px 10px 40px;
94
107
  font-weight:500; display:flex; align-items:center; gap:6px; flex-wrap:wrap;
@@ -153,6 +166,7 @@ body {
153
166
  .item-row:hover .row-acts { opacity:1; }
154
167
  .rbtn { background:var(--surface); border:1px solid var(--border); border-radius:5px; color:var(--text-secondary); padding:3px 8px; cursor:pointer; font-size:0.6rem; font-family:inherit; transition:all 0.12s; }
155
168
  .rbtn:hover { background:var(--accent); color:#fff; border-color:var(--accent); }
169
+ .rbtn-danger:hover { background:#dc2626; color:#fff; border-color:#dc2626; }
156
170
 
157
171
  /* ── Detail panel ── */
158
172
  .detail { width:340px; border-left:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; background:var(--surface); }
@@ -161,10 +175,16 @@ body {
161
175
  .detail-hdr h2 { font-size:0.88rem; color:var(--text-primary); flex:1; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
162
176
  .detail-x { background:none; border:none; color:var(--text-faint); cursor:pointer; font-size:1.1rem; transition:color 0.1s; }
163
177
  .detail-x:hover { color:var(--text-primary); }
164
- .detail-bd { flex:1; overflow-y:auto; padding:18px 20px; }
165
- .df { margin-bottom:16px; }
178
+ .detail-meta { padding:16px 20px 8px; border-bottom:1px solid var(--border-light); flex-shrink:0; }
179
+ .df { margin-bottom:12px; }
180
+ .df-row { display:flex; gap:20px; }
181
+ .df-row .df { flex:1; }
166
182
  .dl { font-size:0.6rem; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.8px; margin-bottom:4px; font-weight:600; }
167
183
  .dv { font-size:0.78rem; color:var(--text-secondary); line-height:1.4; }
184
+ .detail-preview { flex:1; display:flex; flex-direction:column; overflow:hidden; min-height:0; }
185
+ .preview-hdr { padding:12px 20px 6px; flex-shrink:0; }
186
+ .preview-label { font-size:0.6rem; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.8px; font-weight:600; }
187
+ .preview-content { flex:1; overflow-y:auto; padding:0 20px 16px; font-size:0.68rem; line-height:1.55; color:var(--text-secondary); white-space:pre-wrap; word-break:break-word; font-family:'SF Mono','Cascadia Code','Fira Code','Courier New',monospace; margin:0; background:transparent; }
168
188
  .detail-ft { padding:16px 20px; border-top:1px solid var(--border-light); display:flex; gap:8px; }
169
189
  .btn { padding:7px 16px; border-radius:8px; font-size:0.73rem; cursor:pointer; font-family:inherit; border:1px solid var(--border); font-weight:500; transition:all 0.15s; }
170
190
  .btn-p { background:var(--accent); color:#fff; border-color:var(--accent); }
@@ -172,6 +192,8 @@ body {
172
192
  .btn-p:disabled { opacity:0.4; cursor:not-allowed; }
173
193
  .btn-s { background:var(--surface); color:var(--text-secondary); }
174
194
  .btn-s:hover { background:var(--hover); }
195
+ .btn-danger { background:#dc2626; color:#fff; border-color:#dc2626; }
196
+ .btn-danger:hover { background:#b91c1c; }
175
197
 
176
198
  /* ── Modals ── */
177
199
  .modal-bg { position:fixed; inset:0; background:rgba(30,32,40,0.25); display:flex; align-items:center; justify-content:center; z-index:100; backdrop-filter:blur(3px); }
@@ -192,9 +214,18 @@ body {
192
214
  .dest .dp { color:var(--text-muted); font-size:0.6rem; margin-left:auto; }
193
215
  .dest-indent { margin-left:24px; }
194
216
 
217
+ /* ── Checkbox & Bulk bar ── */
218
+ .row-chk { width:14px; height:14px; cursor:pointer; accent-color:var(--accent); flex-shrink:0; margin:0; }
219
+ .bulk-bar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:10px 20px; display:flex; align-items:center; gap:12px; z-index:200; box-shadow:0 4px 20px rgba(0,0,0,0.12); }
220
+ .bulk-bar.hidden { display:none; }
221
+ .bulk-count { font-size:0.75rem; font-weight:600; color:var(--text-primary); }
222
+ .btn-sm { padding:5px 12px; font-size:0.68rem; }
223
+
195
224
  .toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--text-primary); color:#fff; padding:12px 28px; border-radius:12px; font-size:0.78rem; z-index:200; box-shadow:0 4px 20px rgba(0,0,0,0.12); font-weight:500; transition:opacity 0.3s; }
196
225
  .toast.hidden { display:none; }
197
226
  .toast.error { background:#dc2626; }
227
+ .toast-undo { background:none; border:1px solid rgba(255,255,255,0.4); color:#fff; padding:3px 10px; border-radius:6px; cursor:pointer; font-size:0.7rem; font-family:inherit; font-weight:600; margin-left:10px; transition:all 0.12s; }
228
+ .toast-undo:hover { background:rgba(255,255,255,0.2); border-color:#fff; }
198
229
 
199
230
  /* ── Scrollbar ── */
200
231
  ::-webkit-scrollbar { width:6px; }