Initial commit: Folder Bookmark Tray

Windows tray app for bookmarking folders with quick-action buttons
(Explorer, Terminal, Claude, Visual Studio, Redeploy).

Features:
- Tabs containing columns of bookmarks
- Per-row launcher buttons configurable from a header settings panel
- Optional auto-start on Windows login
- In-app confirm dialogs for destructive actions
- Dark theme, frameless tray-anchored popup, pinnable
This commit is contained in:
David
2026-04-29 09:36:59 -04:00
commit 56ef96026b
22 changed files with 10717 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import BookmarkRow from './BookmarkRow.jsx';
import AddBookmarkForm from './AddBookmarkForm.jsx';
export default function Column({
column,
columns,
tabs,
bookmarks,
claudeAvailable,
inspectRevision,
recentColors,
buttonVisibility,
canMoveLeft,
canMoveRight,
onAdd,
onEdit,
onRemove,
onMoveBookmark,
onRenameColumn,
onRemoveColumn,
onMoveColumnToTab,
onMoveLeft,
onMoveRight,
onRequestConfirm,
}) {
const [renaming, setRenaming] = useState(false);
const [draftName, setDraftName] = useState(column.name);
const [menuOpen, setMenuOpen] = useState(false);
const [moveTabOpen, setMoveTabOpen] = useState(false);
const [addOpen, setAddOpen] = useState(false);
const menuRef = useRef(null);
const handleAddSubmitted = useCallback(async (payload) => {
await onAdd(payload);
setAddOpen(false);
}, [onAdd]);
useEffect(() => { setDraftName(column.name); }, [column.name]);
useEffect(() => {
if (!menuOpen) return undefined;
const onClick = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target)) {
setMenuOpen(false);
setMoveTabOpen(false);
}
};
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}, [menuOpen]);
const submitRename = async () => {
const name = draftName.trim();
if (!name || name === column.name) { setRenaming(false); setDraftName(column.name); return; }
await onRenameColumn(column.id, name);
setRenaming(false);
};
const handleDelete = () => {
setMenuOpen(false);
const count = bookmarks.length;
const otherCols = columns.filter((c) => c.id !== column.id);
const ask = onRequestConfirm || ((opts) => { if (window.confirm(`${opts.title}\n\n${opts.message || ''}`)) opts.onConfirm(); });
if (count === 0) {
ask({
title: `Delete column "${column.name}"?`,
message: 'This column has no bookmarks. It will be removed.',
confirmLabel: 'Delete column',
confirmKind: 'danger',
onConfirm: () => onRemoveColumn(column.id),
});
return;
}
if (otherCols.length === 0) {
const word = count === 1 ? 'bookmark' : 'bookmarks';
ask({
title: `Delete column "${column.name}"?`,
message: `This will permanently delete ${count} ${word}.`,
confirmLabel: 'Delete everything',
confirmKind: 'danger',
onConfirm: () => onRemoveColumn(column.id),
});
return;
}
const target = otherCols[0];
const word = count === 1 ? 'bookmark' : 'bookmarks';
ask({
title: `Delete column "${column.name}"?`,
message: `This column has ${count} ${word}. Move them to "${target.name}", or delete everything?`,
confirmLabel: `Move to "${target.name}"`,
confirmKind: 'primary',
extraActions: [
{
label: 'Delete with bookmarks',
kind: 'danger',
onClick: () => onRemoveColumn(column.id),
},
],
onConfirm: () => onRemoveColumn(column.id, target.id),
});
};
const otherTabs = (tabs || []).filter((t) => t.id !== column.tabId);
return (
<section className="column">
<header className="column-header">
{!renaming ? (
<h2
className="column-title"
title="Click to rename"
onClick={() => setRenaming(true)}
>{column.name}</h2>
) : (
<input
autoFocus
className="api-key-input small column-rename-input"
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
onBlur={submitRename}
onKeyDown={(e) => {
if (e.key === 'Enter') submitRename();
else if (e.key === 'Escape') { setRenaming(false); setDraftName(column.name); }
}}
/>
)}
<div className="column-overflow" ref={menuRef}>
<button
className="icon-button"
title="Column options"
onClick={() => { setMenuOpen((v) => !v); setMoveTabOpen(false); }}
></button>
{menuOpen && (
<div className="bookmark-menu">
<button onClick={() => { setMenuOpen(false); setRenaming(true); }}>Rename</button>
<button
onClick={() => { setMenuOpen(false); onMoveLeft(); }}
disabled={!canMoveLeft}
>Move left</button>
<button
onClick={() => { setMenuOpen(false); onMoveRight(); }}
disabled={!canMoveRight}
>Move right</button>
{otherTabs.length > 0 && onMoveColumnToTab && (
moveTabOpen ? (
<div className="bookmark-submenu">
<div className="bookmark-submenu-label">Move to tab</div>
{otherTabs.map((t) => (
<button
key={t.id}
onClick={() => {
setMoveTabOpen(false);
setMenuOpen(false);
onMoveColumnToTab(column.id, t.id);
}}
>{t.name}</button>
))}
<button
className="submenu-back"
onClick={() => setMoveTabOpen(false)}
> Back</button>
</div>
) : (
<button onClick={() => setMoveTabOpen(true)}>Move to tab </button>
)
)}
<button className="danger" onClick={handleDelete}>Delete column</button>
</div>
)}
</div>
</header>
{bookmarks.length === 0 ? (
<div className="empty-state column-empty">No bookmarks here yet.</div>
) : (
<ul className="bookmark-list">
{bookmarks.map((b, idx) => (
<BookmarkRow
key={b.id}
bookmark={b}
columns={columns}
claudeAvailable={claudeAvailable}
inspectRevision={inspectRevision}
recentColors={recentColors}
buttonVisibility={buttonVisibility}
canMoveUp={idx > 0}
canMoveDown={idx < bookmarks.length - 1}
onEdit={onEdit}
onRemove={onRemove}
onMoveUp={() => onMoveBookmark(b.id, -1)}
onMoveDown={() => onMoveBookmark(b.id, 1)}
/>
))}
</ul>
)}
{addOpen ? (
<AddBookmarkForm
columnId={column.id}
recentColors={recentColors}
onAdd={handleAddSubmitted}
onCancel={() => setAddOpen(false)}
/>
) : (
<button
type="button"
className="add-bookmark-trigger"
title="Add bookmark"
onClick={() => setAddOpen(true)}
>+</button>
)}
</section>
);
}