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:
commit
fdb5a8baa3
6 changed files with 133 additions and 49 deletions
|
|
@ -75,7 +75,7 @@ function Dashboard() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative" style={{ zIndex: 1 }}>
|
||||
<div className="relative" style={{ zIndex: 20 }}>
|
||||
<TopBar />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ const pageTitles: Record<string, string> = {
|
|||
'/infrastructure': 'Infrastructure',
|
||||
'/booknest': 'BookNest',
|
||||
'/terminal': 'Terminal',
|
||||
'/tunnels': 'Tunnels',
|
||||
'/files': 'Files',
|
||||
'/containers': 'Containers',
|
||||
'/remote-desktop': 'Remote Desktop',
|
||||
'/host-metrics': 'Host Metrics',
|
||||
'/settings': 'Settings',
|
||||
'/help': 'Help',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,29 @@ html, body {
|
|||
|
||||
/* Native <select> dropdown panels are OS/browser-rendered and ignore most
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import {
|
|||
Globe2,
|
||||
Container,
|
||||
X,
|
||||
Pencil,
|
||||
Trash2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
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 (
|
||||
<div className="flex items-center justify-between group" style={{ padding: '6px 0' }}>
|
||||
<a
|
||||
|
|
@ -195,38 +207,56 @@ function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleF
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
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' }} />
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{bookmark.title}</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={onToggleFavorite}
|
||||
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' }} />
|
||||
</button>
|
||||
<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
|
||||
onClick={onToggleFavorite}
|
||||
className="cursor-pointer bg-transparent border-none p-0 flex items-center"
|
||||
>
|
||||
<Star size={14} fill={bookmark.favorite ? '#C8A434' : 'none'} style={{ color: bookmark.favorite ? '#C8A434' : '#4A4D55' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AddBookmarkModal({
|
||||
function BookmarkModal({
|
||||
categories,
|
||||
bookmark,
|
||||
onClose,
|
||||
onCreated,
|
||||
onUpdated,
|
||||
onCategoryCreated,
|
||||
}: {
|
||||
categories: BookmarkCategory[]
|
||||
bookmark?: Bookmark
|
||||
onClose: () => void
|
||||
onCreated: (bookmark: Bookmark) => void
|
||||
onUpdated: (bookmark: Bookmark) => void
|
||||
onCategoryCreated: (category: BookmarkCategory) => void
|
||||
}) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [url, setUrl] = useState('')
|
||||
const [icon, setIcon] = useState('link2')
|
||||
const [iconMode, setIconMode] = useState<'auto' | 'manual'>('auto')
|
||||
const [categoryId, setCategoryId] = useState<number | 'new' | ''>('')
|
||||
const isEdit = !!bookmark
|
||||
const [title, setTitle] = useState(bookmark?.title ?? '')
|
||||
const [url, setUrl] = useState(bookmark?.url ?? '')
|
||||
const [icon, setIcon] = useState(bookmark?.icon ?? 'link2')
|
||||
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 [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
|
@ -258,26 +288,36 @@ function AddBookmarkModal({
|
|||
} else if (categoryId !== '') {
|
||||
resolvedCategoryId = categoryId
|
||||
}
|
||||
const { id } = await api.createBookmark({
|
||||
title: title.trim(),
|
||||
url: url.trim(),
|
||||
icon,
|
||||
categoryId: resolvedCategoryId,
|
||||
})
|
||||
onCreated({
|
||||
id,
|
||||
category_id: resolvedCategoryId,
|
||||
title: title.trim(),
|
||||
url: url.trim(),
|
||||
icon,
|
||||
favorite: 0,
|
||||
status: 'unknown',
|
||||
last_checked_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
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({
|
||||
title: title.trim(),
|
||||
url: url.trim(),
|
||||
icon,
|
||||
categoryId: resolvedCategoryId,
|
||||
})
|
||||
onCreated({
|
||||
id,
|
||||
category_id: resolvedCategoryId,
|
||||
title: title.trim(),
|
||||
url: url.trim(),
|
||||
icon,
|
||||
favorite: 0,
|
||||
status: 'unknown',
|
||||
last_checked_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
onClose()
|
||||
} 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 {
|
||||
setBusy(false)
|
||||
}
|
||||
|
|
@ -295,7 +335,7 @@ function AddBookmarkModal({
|
|||
style={{ ...cardBase, width: '420px', padding: '24px', gap: '14px' }}
|
||||
>
|
||||
<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">
|
||||
<X size={16} style={{ color: '#7A7D85' }} />
|
||||
</button>
|
||||
|
|
@ -395,7 +435,7 @@ function AddBookmarkModal({
|
|||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{busy ? 'Saving…' : 'Add Bookmark'}
|
||||
{busy ? 'Saving…' : isEdit ? 'Save Changes' : 'Add Bookmark'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -406,6 +446,7 @@ export default function BookNest() {
|
|||
const [bookmarks, setBookmarks] = useState<Bookmark[] | null>(null)
|
||||
const [categories, setCategories] = useState<BookmarkCategory[]>([])
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingBookmark, setEditingBookmark] = useState<Bookmark | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
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) {
|
||||
const next = bookmark.favorite ? false : true
|
||||
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 (
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto" style={{ scrollbarWidth: 'none' }}>
|
||||
{showAddModal && (
|
||||
<AddBookmarkModal
|
||||
{(showAddModal || editingBookmark) && (
|
||||
<BookmarkModal
|
||||
categories={categories}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
bookmark={editingBookmark ?? undefined}
|
||||
onClose={() => { setShowAddModal(false); setEditingBookmark(null) }}
|
||||
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])}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -549,7 +602,13 @@ export default function BookNest() {
|
|||
<h3 style={sectionTitle}>{group.title}</h3>
|
||||
<div className="flex flex-col" style={{ gap: '2px' }}>
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ export default function Containers() {
|
|||
</select>
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs"
|
||||
style={{ border: '1px solid rgba(255,255,255,0.1)', color: TEXT_PRIMARY }}
|
||||
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={13} className={loading ? 'animate-spin' : ''} /> Refresh
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -295,15 +295,15 @@ export default function Files() {
|
|||
))}
|
||||
</div>
|
||||
<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
|
||||
</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
|
||||
</button>
|
||||
<button
|
||||
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' }}
|
||||
>
|
||||
<Upload size={12} /> Upload
|
||||
|
|
@ -465,13 +465,13 @@ export default function Files() {
|
|||
Move (delete from source after copy)
|
||||
</label>
|
||||
<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
|
||||
</button>
|
||||
<button
|
||||
onClick={startTransfer}
|
||||
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 }}
|
||||
>
|
||||
<Send size={12} /> Transfer
|
||||
|
|
@ -492,7 +492,7 @@ export default function Files() {
|
|||
<button
|
||||
onClick={saveEdit}
|
||||
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 }}
|
||||
>
|
||||
<Save size={12} /> Save
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue