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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user