Merge pull request #5 from SamuelSJames/claude/wonderful-faraday-qxym5t

Fix page titles, dropdown stacking, bookmark editing, and UI polish
This commit is contained in:
Samuel James 2026-06-19 17:36:15 -04:00 committed by GitHub
commit fdb5a8baa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 133 additions and 49 deletions

View file

@ -75,7 +75,7 @@ function Dashboard() {
</div> </div>
)} )}
<div className="relative" style={{ zIndex: 1 }}> <div className="relative" style={{ zIndex: 20 }}>
<TopBar /> <TopBar />
</div> </div>

View file

@ -9,6 +9,11 @@ const pageTitles: Record<string, string> = {
'/infrastructure': 'Infrastructure', '/infrastructure': 'Infrastructure',
'/booknest': 'BookNest', '/booknest': 'BookNest',
'/terminal': 'Terminal', '/terminal': 'Terminal',
'/tunnels': 'Tunnels',
'/files': 'Files',
'/containers': 'Containers',
'/remote-desktop': 'Remote Desktop',
'/host-metrics': 'Host Metrics',
'/settings': 'Settings', '/settings': 'Settings',
'/help': 'Help', '/help': 'Help',
} }

View file

@ -41,9 +41,29 @@ html, body {
/* Native <select> dropdown panels are OS/browser-rendered and ignore most /* Native <select> dropdown panels are OS/browser-rendered and ignore most
component styling without this, options render with a white background component styling without this, options render with a white background
and near-white text, making them unreadable against this dark theme. */ and near-white text, making them unreadable against this dark theme.
The !important rules below standardize every select's closed-state look
(gold-tinted border, consistent padding/radius) on top of whatever
per-page inline style happens to be set, so dropdowns read as "premium"
and consistent app-wide without having to touch every page. */
select { select {
color-scheme: dark; color-scheme: dark;
border: 1px solid rgba(200, 164, 52, 0.18) !important;
border-radius: 8px !important;
padding: 9px 12px !important;
background-color: rgba(255, 255, 255, 0.04) !important;
cursor: pointer;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
select:hover {
border-color: rgba(200, 164, 52, 0.35) !important;
}
select:focus {
outline: none;
border-color: rgba(200, 164, 52, 0.55) !important;
box-shadow: 0 0 0 3px rgba(200, 164, 52, 0.1);
} }
select option { select option {

View file

@ -39,6 +39,8 @@ import {
Globe2, Globe2,
Container, Container,
X, X,
Pencil,
Trash2,
type LucideIcon, type LucideIcon,
} from 'lucide-react' } from 'lucide-react'
import { api, ApiError, type Bookmark, type BookmarkCategory } from '../lib/api' import { api, ApiError, type Bookmark, type BookmarkCategory } from '../lib/api'
@ -187,7 +189,17 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
) )
} }
function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleFavorite: () => void }) { function LinkRow({
bookmark,
onToggleFavorite,
onEdit,
onDelete,
}: {
bookmark: Bookmark
onToggleFavorite: () => void
onEdit: () => void
onDelete: () => void
}) {
return ( return (
<div className="flex items-center justify-between group" style={{ padding: '6px 0' }}> <div className="flex items-center justify-between group" style={{ padding: '6px 0' }}>
<a <a
@ -195,38 +207,56 @@ function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleF
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center gap-2.5 cursor-pointer" className="flex items-center gap-2.5 cursor-pointer"
style={{ minWidth: 0, textDecoration: 'none' }} style={{ minWidth: 0, textDecoration: 'none', flex: 1 }}
> >
<BookmarkIcon icon={bookmark.icon} size={16} style={{ color: '#C8A434' }} /> <BookmarkIcon icon={bookmark.icon} size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '13px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{bookmark.title}</span> <span style={{ fontSize: '13px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{bookmark.title}</span>
</a> </a>
<div className="flex items-center gap-1.5" style={{ flexShrink: 0 }}>
<button
onClick={(e) => { e.preventDefault(); onEdit() }}
className="cursor-pointer bg-transparent border-none p-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity"
>
<Pencil size={12} style={{ color: '#7A7D85' }} />
</button>
<button
onClick={(e) => { e.preventDefault(); onDelete() }}
className="cursor-pointer bg-transparent border-none p-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash2 size={12} style={{ color: '#7A7D85' }} />
</button>
<button <button
onClick={onToggleFavorite} onClick={onToggleFavorite}
className="cursor-pointer bg-transparent border-none p-0 flex items-center" className="cursor-pointer bg-transparent border-none p-0 flex items-center"
style={{ flexShrink: 0 }}
> >
<Star size={14} fill={bookmark.favorite ? '#C8A434' : 'none'} style={{ color: bookmark.favorite ? '#C8A434' : '#4A4D55' }} /> <Star size={14} fill={bookmark.favorite ? '#C8A434' : 'none'} style={{ color: bookmark.favorite ? '#C8A434' : '#4A4D55' }} />
</button> </button>
</div> </div>
</div>
) )
} }
function AddBookmarkModal({ function BookmarkModal({
categories, categories,
bookmark,
onClose, onClose,
onCreated, onCreated,
onUpdated,
onCategoryCreated, onCategoryCreated,
}: { }: {
categories: BookmarkCategory[] categories: BookmarkCategory[]
bookmark?: Bookmark
onClose: () => void onClose: () => void
onCreated: (bookmark: Bookmark) => void onCreated: (bookmark: Bookmark) => void
onUpdated: (bookmark: Bookmark) => void
onCategoryCreated: (category: BookmarkCategory) => void onCategoryCreated: (category: BookmarkCategory) => void
}) { }) {
const [title, setTitle] = useState('') const isEdit = !!bookmark
const [url, setUrl] = useState('') const [title, setTitle] = useState(bookmark?.title ?? '')
const [icon, setIcon] = useState('link2') const [url, setUrl] = useState(bookmark?.url ?? '')
const [iconMode, setIconMode] = useState<'auto' | 'manual'>('auto') const [icon, setIcon] = useState(bookmark?.icon ?? 'link2')
const [categoryId, setCategoryId] = useState<number | 'new' | ''>('') const [iconMode, setIconMode] = useState<'auto' | 'manual'>(isIconUrl(bookmark?.icon) || !bookmark ? 'auto' : 'manual')
const [categoryId, setCategoryId] = useState<number | 'new' | ''>(bookmark?.category_id ?? '')
const [newCategoryName, setNewCategoryName] = useState('') const [newCategoryName, setNewCategoryName] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
@ -258,6 +288,15 @@ function AddBookmarkModal({
} else if (categoryId !== '') { } else if (categoryId !== '') {
resolvedCategoryId = categoryId resolvedCategoryId = categoryId
} }
if (isEdit && bookmark) {
await api.updateBookmark(bookmark.id, {
title: title.trim(),
url: url.trim(),
icon,
categoryId: resolvedCategoryId,
})
onUpdated({ ...bookmark, title: title.trim(), url: url.trim(), icon, category_id: resolvedCategoryId })
} else {
const { id } = await api.createBookmark({ const { id } = await api.createBookmark({
title: title.trim(), title: title.trim(),
url: url.trim(), url: url.trim(),
@ -275,9 +314,10 @@ function AddBookmarkModal({
last_checked_at: null, last_checked_at: null,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}) })
}
onClose() onClose()
} catch (err) { } catch (err) {
setError(err instanceof ApiError ? err.message : 'Failed to create bookmark') setError(err instanceof ApiError ? err.message : `Failed to ${isEdit ? 'update' : 'create'} bookmark`)
} finally { } finally {
setBusy(false) setBusy(false)
} }
@ -295,7 +335,7 @@ function AddBookmarkModal({
style={{ ...cardBase, width: '420px', padding: '24px', gap: '14px' }} style={{ ...cardBase, width: '420px', padding: '24px', gap: '14px' }}
> >
<div className="flex items-center justify-between" style={{ marginBottom: '4px' }}> <div className="flex items-center justify-between" style={{ marginBottom: '4px' }}>
<h3 style={{ fontSize: '14px', color: '#E8E6E0', fontWeight: 600 }}>Add Bookmark</h3> <h3 style={{ fontSize: '14px', color: '#E8E6E0', fontWeight: 600 }}>{isEdit ? 'Edit Bookmark' : 'Add Bookmark'}</h3>
<button type="button" onClick={onClose} className="cursor-pointer bg-transparent border-none p-0"> <button type="button" onClick={onClose} className="cursor-pointer bg-transparent border-none p-0">
<X size={16} style={{ color: '#7A7D85' }} /> <X size={16} style={{ color: '#7A7D85' }} />
</button> </button>
@ -395,7 +435,7 @@ function AddBookmarkModal({
marginTop: '4px', marginTop: '4px',
}} }}
> >
{busy ? 'Saving…' : 'Add Bookmark'} {busy ? 'Saving…' : isEdit ? 'Save Changes' : 'Add Bookmark'}
</button> </button>
</form> </form>
</div> </div>
@ -406,6 +446,7 @@ export default function BookNest() {
const [bookmarks, setBookmarks] = useState<Bookmark[] | null>(null) const [bookmarks, setBookmarks] = useState<Bookmark[] | null>(null)
const [categories, setCategories] = useState<BookmarkCategory[]>([]) const [categories, setCategories] = useState<BookmarkCategory[]>([])
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false)
const [editingBookmark, setEditingBookmark] = useState<Bookmark | null>(null)
useEffect(() => { useEffect(() => {
Promise.all([api.listBookmarks(), api.listBookmarkCategories()]).then(([b, c]) => { Promise.all([api.listBookmarks(), api.listBookmarkCategories()]).then(([b, c]) => {
@ -414,6 +455,16 @@ export default function BookNest() {
}) })
}, []) }, [])
async function deleteBookmark(bookmark: Bookmark) {
if (!window.confirm(`Delete "${bookmark.title}"?`)) return
setBookmarks((prev) => prev?.filter((b) => b.id !== bookmark.id) ?? prev)
try {
await api.deleteBookmark(bookmark.id)
} catch {
setBookmarks((prev) => (prev ? [...prev, bookmark] : prev))
}
}
async function toggleFavorite(bookmark: Bookmark) { async function toggleFavorite(bookmark: Bookmark) {
const next = bookmark.favorite ? false : true const next = bookmark.favorite ? false : true
setBookmarks((prev) => prev?.map((b) => (b.id === bookmark.id ? { ...b, favorite: next ? 1 : 0 } : b)) ?? prev) setBookmarks((prev) => prev?.map((b) => (b.id === bookmark.id ? { ...b, favorite: next ? 1 : 0 } : b)) ?? prev)
@ -478,11 +529,13 @@ export default function BookNest() {
return ( return (
<div className="flex h-full w-full flex-col overflow-y-auto" style={{ scrollbarWidth: 'none' }}> <div className="flex h-full w-full flex-col overflow-y-auto" style={{ scrollbarWidth: 'none' }}>
{showAddModal && ( {(showAddModal || editingBookmark) && (
<AddBookmarkModal <BookmarkModal
categories={categories} categories={categories}
onClose={() => setShowAddModal(false)} bookmark={editingBookmark ?? undefined}
onClose={() => { setShowAddModal(false); setEditingBookmark(null) }}
onCreated={(b) => setBookmarks((prev) => [b, ...(prev ?? [])])} onCreated={(b) => setBookmarks((prev) => [b, ...(prev ?? [])])}
onUpdated={(b) => setBookmarks((prev) => prev?.map((x) => (x.id === b.id ? b : x)) ?? prev)}
onCategoryCreated={(c) => setCategories((prev) => [...prev, c])} onCategoryCreated={(c) => setCategories((prev) => [...prev, c])}
/> />
)} )}
@ -549,7 +602,13 @@ export default function BookNest() {
<h3 style={sectionTitle}>{group.title}</h3> <h3 style={sectionTitle}>{group.title}</h3>
<div className="flex flex-col" style={{ gap: '2px' }}> <div className="flex flex-col" style={{ gap: '2px' }}>
{group.links.map((link) => ( {group.links.map((link) => (
<LinkRow key={link.id} bookmark={link} onToggleFavorite={() => toggleFavorite(link)} /> <LinkRow
key={link.id}
bookmark={link}
onToggleFavorite={() => toggleFavorite(link)}
onEdit={() => setEditingBookmark(link)}
onDelete={() => deleteBookmark(link)}
/>
))} ))}
</div> </div>
</div> </div>

View file

@ -146,8 +146,8 @@ export default function Containers() {
</select> </select>
<button <button
onClick={refresh} onClick={refresh}
className="flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs" className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs"
style={{ border: '1px solid rgba(255,255,255,0.1)', color: TEXT_PRIMARY }} style={{ border: '1px solid rgba(200,164,52,0.2)', color: TEXT_PRIMARY }}
> >
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} /> Refresh <RefreshCw size={13} className={loading ? 'animate-spin' : ''} /> Refresh
</button> </button>

View file

@ -295,15 +295,15 @@ export default function Files() {
))} ))}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={refresh} className="flex items-center gap-1 rounded-md px-2.5 py-1 text-xs" style={{ border: '1px solid rgba(255,255,255,0.1)', color: TEXT_PRIMARY }}> <button onClick={refresh} className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs" style={{ border: '1px solid rgba(200,164,52,0.2)', color: TEXT_PRIMARY }}>
<RefreshCw size={12} /> Refresh <RefreshCw size={12} /> Refresh
</button> </button>
<button onClick={handleMkdir} className="flex items-center gap-1 rounded-md px-2.5 py-1 text-xs" style={{ border: '1px solid rgba(255,255,255,0.1)', color: TEXT_PRIMARY }}> <button onClick={handleMkdir} className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs" style={{ border: '1px solid rgba(200,164,52,0.2)', color: TEXT_PRIMARY }}>
<FolderPlus size={12} /> New Folder <FolderPlus size={12} /> New Folder
</button> </button>
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-1 rounded-md px-2.5 py-1 text-xs" className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs"
style={{ backgroundColor: GOLD, color: '#0A0A0C' }} style={{ backgroundColor: GOLD, color: '#0A0A0C' }}
> >
<Upload size={12} /> Upload <Upload size={12} /> Upload
@ -465,13 +465,13 @@ export default function Files() {
Move (delete from source after copy) Move (delete from source after copy)
</label> </label>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button onClick={() => setTransferEntry(null)} className="rounded-md px-3 py-1 text-xs" style={{ border: '1px solid rgba(255,255,255,0.1)', color: TEXT_PRIMARY }}> <button onClick={() => setTransferEntry(null)} className="rounded-md px-3.5 py-1.5 text-xs" style={{ border: '1px solid rgba(200,164,52,0.2)', color: TEXT_PRIMARY }}>
Cancel Cancel
</button> </button>
<button <button
onClick={startTransfer} onClick={startTransfer}
disabled={!transferDestId || !transferDestPath} disabled={!transferDestId || !transferDestPath}
className="flex items-center gap-1 rounded-md px-3 py-1 text-xs" className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs"
style={{ backgroundColor: GOLD, color: '#0A0A0C', opacity: !transferDestId || !transferDestPath ? 0.5 : 1 }} style={{ backgroundColor: GOLD, color: '#0A0A0C', opacity: !transferDestId || !transferDestPath ? 0.5 : 1 }}
> >
<Send size={12} /> Transfer <Send size={12} /> Transfer
@ -492,7 +492,7 @@ export default function Files() {
<button <button
onClick={saveEdit} onClick={saveEdit}
disabled={savingEdit || editingEncoding === 'base64'} disabled={savingEdit || editingEncoding === 'base64'}
className="flex items-center gap-1 rounded-md px-2.5 py-1 text-xs" className="flex items-center gap-1 rounded-md px-3.5 py-1.5 text-xs"
style={{ backgroundColor: GOLD, color: '#0A0A0C', opacity: editingEncoding === 'base64' ? 0.5 : 1 }} style={{ backgroundColor: GOLD, color: '#0A0A0C', opacity: editingEncoding === 'base64' ? 0.5 : 1 }}
> >
<Save size={12} /> Save <Save size={12} /> Save