Fix favicon, dark select dropdowns, add brand bookmark icons and Help page
- Replace mislabeled Vite-logo favicon.svg with proper ArchNest mark extracted from the logo, generated at 32/48/256px PNGs - Force native <select>/<option> elements to render with the dark theme (color-scheme + explicit colors) so options are readable - Auto-detect real brand/service icons for bookmarks (AWS, Proxmox, Azure, Docker, etc.) via the dashboard-icons CDN, with manual override and graceful fallback to lucide icons - Add a Help page with a guided tour of every page, linked from the sidebar, top-bar search, and the user dropdown menu
This commit is contained in:
parent
3d9c4c65c2
commit
57086d2f6f
12 changed files with 384 additions and 27 deletions
|
|
@ -2,9 +2,11 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48.png" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>archnest</title>
|
||||
<title>ArchNest</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
BIN
public/favicon-32.png
Normal file
BIN
public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 805 B |
BIN
public/favicon-48.png
Normal file
BIN
public/favicon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
|
|
@ -12,6 +12,7 @@ import Containers from './pages/Containers'
|
|||
import RemoteDesktop from './pages/RemoteDesktop'
|
||||
import HostMetrics from './pages/HostMetrics'
|
||||
import Settings from './pages/Settings'
|
||||
import Help from './pages/Help'
|
||||
import Login from './pages/Login'
|
||||
import Enrollment from './pages/Enrollment'
|
||||
import { useAuth } from './lib/AuthContext'
|
||||
|
|
@ -93,6 +94,7 @@ function Dashboard() {
|
|||
<Route path="/remote-desktop" element={<RemoteDesktop />} />
|
||||
<Route path="/host-metrics" element={<HostMetrics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/help" element={<Help />} />
|
||||
</Routes>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
MonitorSmartphone,
|
||||
Gauge,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
|
|
@ -32,6 +33,7 @@ const navItems = [
|
|||
{ icon: MonitorSmartphone, label: 'Remote Desktop', route: '/remote-desktop' },
|
||||
{ icon: Gauge, label: 'Host Metrics', route: '/host-metrics' },
|
||||
{ icon: Settings, label: 'Settings', route: '/settings' },
|
||||
{ icon: HelpCircle, label: 'Help', route: '/help' },
|
||||
]
|
||||
|
||||
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const pageTitles: Record<string, string> = {
|
|||
'/booknest': 'BookNest',
|
||||
'/terminal': 'Terminal',
|
||||
'/settings': 'Settings',
|
||||
'/help': 'Help',
|
||||
}
|
||||
|
||||
const pageSubtitles: Record<string, string> = {
|
||||
|
|
@ -27,6 +28,7 @@ const staticPages: { name: string; path: string }[] = [
|
|||
{ name: 'Remote Desktop', path: '/remote-desktop' },
|
||||
{ name: 'Host Metrics', path: '/host-metrics' },
|
||||
{ name: 'Settings', path: '/settings' },
|
||||
{ name: 'Help', path: '/help' },
|
||||
]
|
||||
|
||||
type SearchResult =
|
||||
|
|
@ -225,10 +227,13 @@ export default function TopBar() {
|
|||
<Shield size={14} />
|
||||
<span>Security</span>
|
||||
</a>
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors no-underline">
|
||||
<button
|
||||
onClick={() => { setUserMenuOpen(false); navigate('/help') }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors cursor-pointer border-none bg-transparent text-left"
|
||||
>
|
||||
<HelpCircle size={14} />
|
||||
<span>Help & Support</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<div className="border-t border-border py-1">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -38,3 +38,15 @@ html, body {
|
|||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
select {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
select option {
|
||||
background-color: #141518;
|
||||
color: #E8E6E0;
|
||||
}
|
||||
|
|
|
|||
132
src/lib/serviceIcons.ts
Normal file
132
src/lib/serviceIcons.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Auto-suggests a real service/brand icon for a bookmark by matching its
|
||||
// title (and URL host) against a curated alias list, pointing at the
|
||||
// dashboard-icons CDN — the same icon pack used by most self-hosted
|
||||
// dashboards (Homarr, Homepage, etc.) for exactly this purpose.
|
||||
const ICON_CDN_BASE = 'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg'
|
||||
|
||||
// key: lowercase phrase to match in the bookmark title or URL host.
|
||||
// value: the icon slug in the dashboard-icons repo.
|
||||
const SERVICE_ALIASES: Record<string, string> = {
|
||||
aws: 'amazon-web-services',
|
||||
'amazon web services': 'amazon-web-services',
|
||||
ec2: 'amazon-web-services',
|
||||
azure: 'azure',
|
||||
'microsoft azure': 'azure',
|
||||
gcp: 'google-cloud',
|
||||
'google cloud': 'google-cloud',
|
||||
proxmox: 'proxmox',
|
||||
pve: 'proxmox',
|
||||
docker: 'docker',
|
||||
kubernetes: 'kubernetes',
|
||||
k8s: 'kubernetes',
|
||||
github: 'github',
|
||||
gitlab: 'gitlab',
|
||||
gitea: 'gitea',
|
||||
bitbucket: 'bitbucket',
|
||||
cloudflare: 'cloudflare',
|
||||
netbird: 'netbird',
|
||||
grafana: 'grafana',
|
||||
prometheus: 'prometheus',
|
||||
'uptime kuma': 'uptime-kuma',
|
||||
'uptime-kuma': 'uptime-kuma',
|
||||
nginx: 'nginx',
|
||||
'nginx proxy manager': 'nginx-proxy-manager',
|
||||
portainer: 'portainer',
|
||||
jellyfin: 'jellyfin',
|
||||
plex: 'plex',
|
||||
sonarr: 'sonarr',
|
||||
radarr: 'radarr',
|
||||
wordpress: 'wordpress',
|
||||
mysql: 'mysql',
|
||||
mariadb: 'mariadb',
|
||||
postgres: 'postgresql',
|
||||
postgresql: 'postgresql',
|
||||
mongodb: 'mongodb',
|
||||
redis: 'redis',
|
||||
'home assistant': 'home-assistant',
|
||||
'home-assistant': 'home-assistant',
|
||||
pihole: 'pi-hole',
|
||||
'pi-hole': 'pi-hole',
|
||||
traefik: 'traefik',
|
||||
'visual studio code': 'visual-studio-code',
|
||||
vscode: 'visual-studio-code',
|
||||
slack: 'slack',
|
||||
discord: 'discord',
|
||||
notion: 'notion',
|
||||
jira: 'jira',
|
||||
confluence: 'confluence',
|
||||
jenkins: 'jenkins',
|
||||
ansible: 'ansible',
|
||||
terraform: 'terraform',
|
||||
vault: 'vault',
|
||||
minio: 'minio',
|
||||
truenas: 'truenas',
|
||||
synology: 'synology',
|
||||
unifi: 'unifi',
|
||||
ubiquiti: 'unifi',
|
||||
pfsense: 'pfsense',
|
||||
opnsense: 'opnsense',
|
||||
vmware: 'vmware',
|
||||
vsphere: 'vmware',
|
||||
esxi: 'vmware',
|
||||
windows: 'windows',
|
||||
linux: 'linux',
|
||||
ubuntu: 'ubuntu',
|
||||
debian: 'debian',
|
||||
fedora: 'fedora',
|
||||
archlinux: 'archlinux',
|
||||
'arch linux': 'archlinux',
|
||||
rancher: 'rancher',
|
||||
jupyter: 'jupyter',
|
||||
gitlabci: 'gitlab',
|
||||
npm: 'npm',
|
||||
nodejs: 'nodejs',
|
||||
'node.js': 'nodejs',
|
||||
python: 'python',
|
||||
react: 'react',
|
||||
vaultwarden: 'vaultwarden',
|
||||
bitwarden: 'bitwarden',
|
||||
netdata: 'netdata',
|
||||
zabbix: 'zabbix',
|
||||
nextcloud: 'nextcloud',
|
||||
owncloud: 'owncloud',
|
||||
gitkraken: 'gitkraken',
|
||||
digitalocean: 'digitalocean',
|
||||
'digital ocean': 'digitalocean',
|
||||
linode: 'linode',
|
||||
vultr: 'vultr',
|
||||
heroku: 'heroku',
|
||||
vercel: 'vercel',
|
||||
netlify: 'netlify',
|
||||
cpanel: 'cpanel',
|
||||
webmin: 'webmin',
|
||||
pterodactyl: 'pterodactyl',
|
||||
rabbitmq: 'rabbitmq',
|
||||
kafka: 'kafka',
|
||||
elasticsearch: 'elasticsearch',
|
||||
kibana: 'kibana',
|
||||
logstash: 'logstash',
|
||||
}
|
||||
|
||||
function normalize(s: string): string {
|
||||
return s.toLowerCase().trim()
|
||||
}
|
||||
|
||||
/** Best-effort guess at a real brand icon for a bookmark, or null if nothing matched. */
|
||||
export function guessServiceIconUrl(title: string, url?: string): string | null {
|
||||
const haystack = [normalize(title)]
|
||||
if (url) {
|
||||
try {
|
||||
haystack.push(normalize(new URL(url).hostname))
|
||||
} catch {
|
||||
// not a valid absolute URL yet (e.g. still being typed) — ignore
|
||||
}
|
||||
}
|
||||
|
||||
for (const [alias, slug] of Object.entries(SERVICE_ALIASES)) {
|
||||
if (haystack.some((h) => h.includes(alias))) {
|
||||
return `${ICON_CDN_BASE}/${slug}.svg`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import {
|
|||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import { api, ApiError, type Bookmark, type BookmarkCategory } from '../lib/api'
|
||||
import { guessServiceIconUrl } from '../lib/serviceIcons'
|
||||
|
||||
const ICONS: Record<string, LucideIcon> = {
|
||||
server: Server,
|
||||
|
|
@ -85,6 +86,30 @@ function resolveIcon(name: string | null | undefined): LucideIcon {
|
|||
return ICONS[name.toLowerCase()] ?? Link2
|
||||
}
|
||||
|
||||
function isIconUrl(icon: string | null | undefined): boolean {
|
||||
return !!icon && /^https?:\/\//.test(icon)
|
||||
}
|
||||
|
||||
/** Renders a bookmark's icon — a real brand logo if `icon` is a CDN URL, falling
|
||||
back to the generic lucide set (and to a plain link icon if the image 404s). */
|
||||
function BookmarkIcon({ icon, size = 16, style }: { icon: string | null | undefined; size?: number; style?: React.CSSProperties }) {
|
||||
const [imgFailed, setImgFailed] = useState(false)
|
||||
if (isIconUrl(icon) && !imgFailed) {
|
||||
return (
|
||||
<img
|
||||
src={icon!}
|
||||
alt=""
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ flexShrink: 0, objectFit: 'contain', ...style }}
|
||||
onError={() => setImgFailed(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const Icon = resolveIcon(imgFailed ? null : icon)
|
||||
return <Icon size={size} style={{ flexShrink: 0, ...style }} />
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
online: '#2ECC71',
|
||||
warning: '#E67E22',
|
||||
|
|
@ -163,7 +188,6 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
|
|||
}
|
||||
|
||||
function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleFavorite: () => void }) {
|
||||
const Icon = resolveIcon(bookmark.icon)
|
||||
return (
|
||||
<div className="flex items-center justify-between group" style={{ padding: '6px 0' }}>
|
||||
<a
|
||||
|
|
@ -173,7 +197,7 @@ function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleF
|
|||
className="flex items-center gap-2.5 cursor-pointer"
|
||||
style={{ minWidth: 0, textDecoration: 'none' }}
|
||||
>
|
||||
<Icon size={16} style={{ color: '#C8A434', flexShrink: 0 }} />
|
||||
<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
|
||||
|
|
@ -201,11 +225,17 @@ function AddBookmarkModal({
|
|||
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 [newCategoryName, setNewCategoryName] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (iconMode !== 'auto') return
|
||||
setIcon(guessServiceIconUrl(title, url) ?? 'link2')
|
||||
}, [title, url, iconMode])
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
|
@ -282,14 +312,45 @@ function AddBookmarkModal({
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label style={{ fontSize: '11px', color: '#7A7D85' }}>Icon</label>
|
||||
<select style={inputStyle} value={icon} onChange={(e) => setIcon(e.target.value)}>
|
||||
{Object.keys(ICONS).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center justify-between">
|
||||
<label style={{ fontSize: '11px', color: '#7A7D85' }}>Icon</label>
|
||||
{iconMode === 'manual' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIconMode('auto')}
|
||||
className="cursor-pointer bg-transparent border-none p-0"
|
||||
style={{ fontSize: '11px', color: '#C8A434' }}
|
||||
>
|
||||
Auto-detect again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{iconMode === 'auto' ? (
|
||||
<div className="flex items-center gap-2.5" style={{ ...inputStyle, justifyContent: 'space-between' }}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BookmarkIcon icon={icon} size={16} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '12px', color: isIconUrl(icon) ? '#E8E6E0' : '#7A7D85' }}>
|
||||
{isIconUrl(icon) ? 'Detected from title/URL' : 'No match yet — type a title or pick one'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIconMode('manual')}
|
||||
className="cursor-pointer bg-transparent border-none p-0"
|
||||
style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Choose manually
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<select style={inputStyle} value={icon} onChange={(e) => setIcon(e.target.value)}>
|
||||
{Object.keys(ICONS).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
|
@ -390,7 +451,7 @@ export default function BookNest() {
|
|||
[...groups]
|
||||
.sort((a, b) => b.links.length - a.links.length)
|
||||
.slice(0, 5)
|
||||
.map((g) => ({ label: g.title, icons: g.links.slice(0, 5).map((l) => resolveIcon(l.icon)), count: g.links.length })),
|
||||
.map((g) => ({ label: g.title, icons: g.links.slice(0, 5).map((l) => l.icon), count: g.links.length })),
|
||||
[groups]
|
||||
)
|
||||
|
||||
|
|
@ -464,9 +525,9 @@ export default function BookNest() {
|
|||
<div key={qa.label} style={{ ...cardBase, padding: '14px' }} className="hover:!border-gold/20">
|
||||
<span style={{ fontSize: '11px', color: '#E8E6E0', fontWeight: 600, marginBottom: '10px' }}>{qa.label}</span>
|
||||
<div className="flex items-center gap-1.5" style={{ marginBottom: '8px' }}>
|
||||
{qa.icons.map((Icon, i) => (
|
||||
{qa.icons.map((icon, i) => (
|
||||
<div key={i} style={{ width: '22px', height: '22px', borderRadius: '6px', backgroundColor: 'rgba(200,164,52,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Icon size={12} style={{ color: '#C8A434' }} />
|
||||
<BookmarkIcon icon={icon} size={12} style={{ color: '#C8A434' }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -503,15 +564,12 @@ export default function BookNest() {
|
|||
<h3 style={{ ...sectionTitle, fontSize: '12px', marginBottom: '18px' }}>Favorites</h3>
|
||||
<div className="flex flex-col" style={{ gap: '4px' }}>
|
||||
{favorites.length > 0 ? (
|
||||
favorites.map((f) => {
|
||||
const Icon = resolveIcon(f.icon)
|
||||
return (
|
||||
<a key={f.id} href={f.url} target="_blank" rel="noreferrer" className="flex items-center gap-3" style={{ padding: '8px 0', textDecoration: 'none' }}>
|
||||
<Icon size={17} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>{f.title}</span>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
favorites.map((f) => (
|
||||
<a key={f.id} href={f.url} target="_blank" rel="noreferrer" className="flex items-center gap-3" style={{ padding: '8px 0', textDecoration: 'none' }}>
|
||||
<BookmarkIcon icon={f.icon} size={17} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>{f.title}</span>
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No favorites yet</p>
|
||||
)}
|
||||
|
|
|
|||
145
src/pages/Help.tsx
Normal file
145
src/pages/Help.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import {
|
||||
LayoutGrid,
|
||||
Server,
|
||||
Bookmark,
|
||||
Terminal,
|
||||
Waypoints,
|
||||
FolderOpen,
|
||||
Box,
|
||||
MonitorSmartphone,
|
||||
Gauge,
|
||||
Settings,
|
||||
Search,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '22px',
|
||||
}
|
||||
|
||||
interface GuideEntry {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
tips?: string[]
|
||||
}
|
||||
|
||||
const guideEntries: GuideEntry[] = [
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
title: 'Glance',
|
||||
description:
|
||||
'The home dashboard. Shows overall system health, a rollup of connected integrations, recent activity, and shortcuts into the rest of the app.',
|
||||
tips: ['Click "Connected Integrations" entries to jump straight to Infrastructure.'],
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: 'Infrastructure',
|
||||
description:
|
||||
'Lists every connected integration (Proxmox, AWS, Docker, NetBird, Cloudflare, Uptime Kuma, Weather, SSH hosts) and the live resources/health each one reports.',
|
||||
tips: ['Add new integrations from Settings → Integrations — they show up here automatically.'],
|
||||
},
|
||||
{
|
||||
icon: Bookmark,
|
||||
title: 'BookNest',
|
||||
description: 'A categorized bookmark manager for the links you use most — internal tools, dashboards, docs, anything.',
|
||||
tips: [
|
||||
'Icons are auto-detected from the title or URL (e.g. typing "Proxmox" picks up the real Proxmox logo) — pick "Choose manually" if it guesses wrong.',
|
||||
'Star a bookmark to pin it to the Favorites panel.',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
title: 'Terminal',
|
||||
description: 'A full SSH terminal to any host you\'ve added as an integration — supports tabs, split panes, jump hosts, and certificate auth.',
|
||||
tips: ['Session output can be logged; theme and font preferences are remembered between visits.'],
|
||||
},
|
||||
{
|
||||
icon: Waypoints,
|
||||
title: 'Tunnels',
|
||||
description: 'Local, remote, and dynamic (SOCKS5) SSH tunnels. Tunnels can be set to auto-start whenever the backend boots.',
|
||||
},
|
||||
{
|
||||
icon: FolderOpen,
|
||||
title: 'Files',
|
||||
description: 'Browse, edit, upload, and download files over SFTP on any connected SSH host — and transfer files directly between two hosts without round-tripping through your machine.',
|
||||
tips: ['Use the "Send to another host" action on a file row to start a host-to-host transfer; progress shows live in the panel at the bottom.'],
|
||||
},
|
||||
{
|
||||
icon: Box,
|
||||
title: 'Containers',
|
||||
description: 'Manage Docker containers on remote hosts — start, stop, view logs, and exec into a running container.',
|
||||
},
|
||||
{
|
||||
icon: MonitorSmartphone,
|
||||
title: 'Remote Desktop',
|
||||
description: 'RDP, VNC, and Telnet sessions to remote machines, streamed through the built-in Guacamole proxy — no separate client needed.',
|
||||
},
|
||||
{
|
||||
icon: Gauge,
|
||||
title: 'Host Metrics',
|
||||
description: 'Live CPU, memory, disk, network, listening-port, firewall, process, and login-activity widgets for any SSH-managed host.',
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: 'Settings',
|
||||
description: 'Your profile, integrations, appearance, notifications, and full data export/import (back up or migrate every integration, bookmark, and tunnel as a single JSON file).',
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
title: 'Search (top bar)',
|
||||
description: 'The search box at the top of every page looks across pages, integrations, and bookmarks at once — press Enter to jump to the top match.',
|
||||
},
|
||||
]
|
||||
|
||||
export default function Help() {
|
||||
return (
|
||||
<div className="p-8" style={{ maxWidth: '1100px' }}>
|
||||
<div style={{ marginBottom: '28px' }}>
|
||||
<h1 style={{ fontSize: '22px', color: '#E8E6E0', fontWeight: 700, marginBottom: '6px' }}>How ArchNest works</h1>
|
||||
<p style={{ fontSize: '13px', color: '#7A7D85' }}>
|
||||
A quick tour of every page and what it's for. Use the sidebar to navigate, or the search bar at the top to jump straight to something.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{guideEntries.map((entry) => {
|
||||
const Icon = entry.icon
|
||||
return (
|
||||
<div key={entry.title} style={cardBase}>
|
||||
<div className="flex items-center gap-3" style={{ marginBottom: '10px' }}>
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(200,164,52,0.1)',
|
||||
border: '1px solid rgba(200,164,52,0.18)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Icon size={18} style={{ color: '#C8A434' }} />
|
||||
</div>
|
||||
<h3 style={{ fontSize: '14px', color: '#E8E6E0', fontWeight: 600 }}>{entry.title}</h3>
|
||||
</div>
|
||||
<p style={{ fontSize: '12.5px', color: '#A8A6A0', lineHeight: 1.6 }}>{entry.description}</p>
|
||||
{entry.tips && entry.tips.length > 0 && (
|
||||
<ul style={{ marginTop: '10px', paddingLeft: '16px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{entry.tips.map((tip, i) => (
|
||||
<li key={i} style={{ fontSize: '11.5px', color: '#7A7D85', lineHeight: 1.5 }}>
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue