Skip to content

Commit c18bf8c

Browse files
ithiria894claude
andcommitted
feat: sidebar footer CTA + drag handle on hover
- Sidebar footer: star link, feedback/issues link, /cco hint - Drag handle (⠿) appears on hover for draggable items - Locked items hide the handle - Fix flaky skill count test (CCO installs /cco skill globally) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3f37fc5 commit c18bf8c

4 files changed

Lines changed: 2293 additions & 6 deletions

File tree

src/ui/app.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ function setupUi() {
114114
setupScopeDropZones();
115115
setupThemeToggle();
116116
setupCcActions();
117+
setupExport();
117118
setupResizers();
118119
}
119120

@@ -667,8 +668,11 @@ function renderItem(item) {
667668
${canDeleteItem(item) ? `<button type="button" class="act-btn act-del" data-action="delete">Del</button>` : ""}
668669
</span>`;
669670

671+
const dragHandle = item.locked ? "" : `<span class="drag-handle" title="Drag to move">⠿</span>`;
672+
670673
return `
671674
<div class="item${item.locked ? " locked" : ""}${isSelected ? " selected" : ""}" data-item-key="${esc(key)}" data-path="${esc(item.path)}" data-category="${esc(item.category)}">
675+
${dragHandle}
672676
${checkbox}
673677
<span class="item-ico">${icon}</span>
674678
<span class="item-name">${esc(item.name)}</span>
@@ -842,6 +846,51 @@ function setupCcActions() {
842846
});
843847
}
844848

849+
function setupExport() {
850+
const modal = document.getElementById("exportModal");
851+
const input = document.getElementById("exportPath");
852+
const confirmBtn = document.getElementById("exportConfirm");
853+
const cancelBtn = document.getElementById("exportCancel");
854+
855+
document.getElementById("exportBtn").addEventListener("click", () => {
856+
input.value = input.value || "/tmp";
857+
modal.classList.remove("hidden");
858+
input.focus();
859+
input.select();
860+
});
861+
862+
cancelBtn.addEventListener("click", () => modal.classList.add("hidden"));
863+
modal.addEventListener("click", (e) => { if (e.target === modal) modal.classList.add("hidden"); });
864+
865+
confirmBtn.addEventListener("click", async () => {
866+
const exportDir = input.value.trim();
867+
if (!exportDir || !exportDir.startsWith("/")) {
868+
toast("Please enter an absolute path", true);
869+
return;
870+
}
871+
confirmBtn.disabled = true;
872+
confirmBtn.textContent = "Exporting...";
873+
try {
874+
const raw = await fetch("/api/export", {
875+
method: "POST",
876+
headers: { "Content-Type": "application/json" },
877+
body: JSON.stringify({ exportDir }),
878+
});
879+
const res = await raw.json();
880+
if (res.ok) {
881+
modal.classList.add("hidden");
882+
toast(`${res.copied} items exported to ${res.path}`);
883+
} else {
884+
toast(res.error || "Export failed", true);
885+
}
886+
} catch {
887+
toast("Export failed", true);
888+
}
889+
confirmBtn.disabled = false;
890+
confirmBtn.textContent = "Export";
891+
});
892+
}
893+
845894
function renderBreadcrumb(scope) {
846895
if (!scope) return `<span class="crumb-pill">Unknown scope</span>`;
847896
const scopes = [...getScopeChain(scope), scope];

src/ui/index.html

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424

2525
<div class="sidebar-tree" id="sidebarTree"></div>
2626
<div class="sidebar-footer">
27-
<a href="https://github.com/mcpware" target="_blank" rel="noopener">proudly presented by @mcpware</a>
27+
<a href="https://github.com/mcpware/claude-code-organizer" target="_blank" rel="noopener">⭐ Star me on GitHub</a>
28+
<a href="https://github.com/mcpware/claude-code-organizer/issues" target="_blank" rel="noopener">💡 Bug? Feedback? Same-day fix promise</a>
29+
<span class="sidebar-footer-hint">Type <code>/cco</code> in Claude Code to reopen</span>
2830
</div>
2931
</div>
3032

@@ -36,6 +38,7 @@
3638
<span class="content-title" id="contentTitle">Organizer</span>
3739
<span class="content-scope-tag" id="contentScopeTag">global</span>
3840
<div class="content-inherit" id="contentInherit"></div>
41+
<button class="export-btn" id="exportBtn" type="button" title="Export all configs to a folder">Export</button>
3942
</div>
4043
<div class="filter-bar" id="pills"></div>
4144
</div>
@@ -108,6 +111,22 @@ <h2 id="detailTitle">Select an item</h2>
108111
</div>
109112
</div>
110113

114+
<div class="modal-bg hidden" id="exportModal">
115+
<div class="modal">
116+
<h3>Export Backup</h3>
117+
<div class="modal-sub">Copy all scanned configs to a folder</div>
118+
<div class="export-input-row">
119+
<label class="export-label" for="exportPath">Export to:</label>
120+
<input class="export-input" id="exportPath" type="text" placeholder="/home/user/backups" />
121+
</div>
122+
<div class="export-hint">A timestamped folder will be created inside this path</div>
123+
<div class="modal-btns">
124+
<button class="d-btn d-btn-open" id="exportCancel" type="button">Cancel</button>
125+
<button class="d-btn d-btn-move" id="exportConfirm" type="button">Export</button>
126+
</div>
127+
</div>
128+
</div>
129+
111130
<div class="modal-bg hidden" id="dragConfirmModal">
112131
<div class="modal">
113132
<h3>Confirm Move</h3>

src/ui/style.css

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,19 +224,36 @@ body {
224224
.s-cat:hover { color: var(--sidebar-text); background: var(--sidebar-hover); }
225225

226226
.sidebar-footer {
227-
padding: 10px 18px;
227+
padding: 10px 14px;
228228
border-top: 1px solid var(--sidebar-border);
229229
flex-shrink: 0;
230-
text-align: center;
230+
display: flex;
231+
flex-direction: column;
232+
gap: 4px;
231233
}
232234
.sidebar-footer a {
233-
font-size: 0.62rem;
235+
font-size: 0.68rem;
234236
color: var(--sidebar-text-dim);
235237
text-decoration: none;
236238
font-weight: 600;
237-
letter-spacing: 0.3px;
239+
letter-spacing: 0.2px;
240+
padding: 4px 8px;
241+
border-radius: 4px;
242+
transition: background 120ms, color 120ms;
243+
}
244+
.sidebar-footer a:hover { color: var(--sidebar-text-bright); background: var(--sidebar-hover); }
245+
.sidebar-footer-hint {
246+
font-size: 0.68rem;
247+
color: var(--sidebar-text-dim);
248+
font-weight: 600;
249+
letter-spacing: 0.2px;
250+
padding: 4px 8px;
251+
}
252+
.sidebar-footer-hint code {
253+
background: var(--sidebar-hover);
254+
padding: 1px 5px;
255+
border-radius: 3px;
238256
}
239-
.sidebar-footer a:hover { color: var(--sidebar-text); }
240257
.s-cat-ico { font-size: 0.78rem; }
241258
.s-cat-nm { font-weight: 400; flex: 1; }
242259
.s-cat-cnt { font-size: 0.65rem; }
@@ -416,8 +433,25 @@ body {
416433
transition: all 120ms;
417434
border: 1px solid var(--border-light);
418435
background: var(--surface);
436+
position: relative;
419437
}
420438

439+
.item .drag-handle {
440+
position: absolute;
441+
left: -20px;
442+
top: 50%;
443+
transform: translateY(-50%);
444+
font-size: 12px;
445+
color: var(--text-muted);
446+
opacity: 0;
447+
transition: opacity 120ms;
448+
cursor: grab;
449+
letter-spacing: 1px;
450+
user-select: none;
451+
}
452+
.item:hover .drag-handle { opacity: 1; }
453+
.item.locked .drag-handle { display: none; }
454+
421455
.item:hover { border-color: var(--border); box-shadow: 0 2px 6px oklch(0.20 0.01 70 / 0.08); }
422456
.item.selected { background: oklch(0.97 0.015 160); border-color: oklch(0.90 0.03 160); }
423457
.item.locked { opacity: 0.45; cursor: default; }
@@ -571,6 +605,20 @@ body {
571605
.d-btn:disabled { opacity: 0.45; cursor: not-allowed; }
572606
.d-btn:disabled:hover { background: inherit; color: inherit; }
573607

608+
.export-btn {
609+
margin-left: auto; padding: 4px 12px; border-radius: 6px; font-size: 0.7rem; font-weight: 600;
610+
border: 1px solid var(--border); background: var(--surface-inset); color: var(--text-secondary);
611+
cursor: pointer; font-family: inherit; transition: all 100ms; white-space: nowrap;
612+
}
613+
.export-btn:hover { border-color: var(--accent); color: var(--accent-text); }
614+
.export-input-row { display: flex; align-items: center; gap: 8px; margin: 12px 0 6px; }
615+
.export-label { font-size: 0.72rem; font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
616+
.export-input {
617+
flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border);
618+
background: var(--surface); color: var(--text-primary); font-size: 0.75rem; font-family: monospace;
619+
}
620+
.export-input:focus { outline: none; border-color: var(--accent); }
621+
.export-hint { font-size: 0.65rem; color: var(--text-muted); margin-bottom: 12px; }
574622
.detail-cc-actions { padding: 12px 20px; border-top: 1px solid var(--border-light); }
575623
.detail-cc-actions .d-label { margin-bottom: 8px; }
576624
.cc-btn-row { display: flex; flex-wrap: wrap; gap: 6px; }

0 commit comments

Comments
 (0)