Fix page titles, dropdown stacking, bookmark editing, and button/select polish
- Add missing pageTitles entries so Tunnels, Files, Containers, Remote Desktop, and Host Metrics no longer show "Glance" in the top bar - Raise the TopBar's stacking context above the page-content section so the user-menu dropdown no longer renders behind the hero banner - Support editing and deleting bookmarks (not just adding), via a shared BookmarkModal and per-row edit/delete actions in BookNest - Standardize <select> styling globally (gold-tinted border, hover/ focus glow) instead of three inconsistent inline style definitions - Widen cramped button padding/borders in Files and Containers
This commit is contained in:
parent
57086d2f6f
commit
fc9d685651
6 changed files with 133 additions and 49 deletions
|
|
@ -75,7 +75,7 @@ function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative" style={{ zIndex: 1 }}>
|
<div className="relative" style={{ zIndex: 20 }}>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
<button
|
<div className="flex items-center gap-1.5" style={{ flexShrink: 0 }}>
|
||||||
onClick={onToggleFavorite}
|
<button
|
||||||
className="cursor-pointer bg-transparent border-none p-0 flex items-center"
|
onClick={(e) => { e.preventDefault(); onEdit() }}
|
||||||
style={{ flexShrink: 0 }}
|
className="cursor-pointer bg-transparent border-none p-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
>
|
>
|
||||||
<Star size={14} fill={bookmark.favorite ? '#C8A434' : 'none'} style={{ color: bookmark.favorite ? '#C8A434' : '#4A4D55' }} />
|
<Pencil size={12} style={{ color: '#7A7D85' }} />
|
||||||
</button>
|
</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>
|
</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,26 +288,36 @@ function AddBookmarkModal({
|
||||||
} else if (categoryId !== '') {
|
} else if (categoryId !== '') {
|
||||||
resolvedCategoryId = categoryId
|
resolvedCategoryId = categoryId
|
||||||
}
|
}
|
||||||
const { id } = await api.createBookmark({
|
if (isEdit && bookmark) {
|
||||||
title: title.trim(),
|
await api.updateBookmark(bookmark.id, {
|
||||||
url: url.trim(),
|
title: title.trim(),
|
||||||
icon,
|
url: url.trim(),
|
||||||
categoryId: resolvedCategoryId,
|
icon,
|
||||||
})
|
categoryId: resolvedCategoryId,
|
||||||
onCreated({
|
})
|
||||||
id,
|
onUpdated({ ...bookmark, title: title.trim(), url: url.trim(), icon, category_id: resolvedCategoryId })
|
||||||
category_id: resolvedCategoryId,
|
} else {
|
||||||
title: title.trim(),
|
const { id } = await api.createBookmark({
|
||||||
url: url.trim(),
|
title: title.trim(),
|
||||||
icon,
|
url: url.trim(),
|
||||||
favorite: 0,
|
icon,
|
||||||
status: 'unknown',
|
categoryId: resolvedCategoryId,
|
||||||
last_checked_at: null,
|
})
|
||||||
created_at: new Date().toISOString(),
|
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()
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue