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

Fix favicon, dark select dropdowns, add brand bookmark icons and Help…
This commit is contained in:
Samuel James 2026-06-19 17:20:06 -04:00 committed by GitHub
commit 3e41571dd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 384 additions and 27 deletions

View file

@ -2,9 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>archnest</title> <title>ArchNest</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

BIN
public/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
public/favicon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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

View file

@ -12,6 +12,7 @@ import Containers from './pages/Containers'
import RemoteDesktop from './pages/RemoteDesktop' import RemoteDesktop from './pages/RemoteDesktop'
import HostMetrics from './pages/HostMetrics' import HostMetrics from './pages/HostMetrics'
import Settings from './pages/Settings' import Settings from './pages/Settings'
import Help from './pages/Help'
import Login from './pages/Login' import Login from './pages/Login'
import Enrollment from './pages/Enrollment' import Enrollment from './pages/Enrollment'
import { useAuth } from './lib/AuthContext' import { useAuth } from './lib/AuthContext'
@ -93,6 +94,7 @@ function Dashboard() {
<Route path="/remote-desktop" element={<RemoteDesktop />} /> <Route path="/remote-desktop" element={<RemoteDesktop />} />
<Route path="/host-metrics" element={<HostMetrics />} /> <Route path="/host-metrics" element={<HostMetrics />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/help" element={<Help />} />
</Routes> </Routes>
</section> </section>
</main> </main>

View file

@ -11,6 +11,7 @@ import {
MonitorSmartphone, MonitorSmartphone,
Gauge, Gauge,
Settings, Settings,
HelpCircle,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
} from 'lucide-react' } from 'lucide-react'
@ -32,6 +33,7 @@ const navItems = [
{ icon: MonitorSmartphone, label: 'Remote Desktop', route: '/remote-desktop' }, { icon: MonitorSmartphone, label: 'Remote Desktop', route: '/remote-desktop' },
{ icon: Gauge, label: 'Host Metrics', route: '/host-metrics' }, { icon: Gauge, label: 'Host Metrics', route: '/host-metrics' },
{ icon: Settings, label: 'Settings', route: '/settings' }, { icon: Settings, label: 'Settings', route: '/settings' },
{ icon: HelpCircle, label: 'Help', route: '/help' },
] ]
export default function Sidebar({ collapsed, onToggle }: SidebarProps) { export default function Sidebar({ collapsed, onToggle }: SidebarProps) {

View file

@ -10,6 +10,7 @@ const pageTitles: Record<string, string> = {
'/booknest': 'BookNest', '/booknest': 'BookNest',
'/terminal': 'Terminal', '/terminal': 'Terminal',
'/settings': 'Settings', '/settings': 'Settings',
'/help': 'Help',
} }
const pageSubtitles: Record<string, string> = { const pageSubtitles: Record<string, string> = {
@ -27,6 +28,7 @@ const staticPages: { name: string; path: string }[] = [
{ name: 'Remote Desktop', path: '/remote-desktop' }, { name: 'Remote Desktop', path: '/remote-desktop' },
{ name: 'Host Metrics', path: '/host-metrics' }, { name: 'Host Metrics', path: '/host-metrics' },
{ name: 'Settings', path: '/settings' }, { name: 'Settings', path: '/settings' },
{ name: 'Help', path: '/help' },
] ]
type SearchResult = type SearchResult =
@ -225,10 +227,13 @@ export default function TopBar() {
<Shield size={14} /> <Shield size={14} />
<span>Security</span> <span>Security</span>
</a> </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} /> <HelpCircle size={14} />
<span>Help & Support</span> <span>Help & Support</span>
</a> </button>
</div> </div>
<div className="border-t border-border py-1"> <div className="border-t border-border py-1">
<button <button

View file

@ -38,3 +38,15 @@ html, body {
width: 100vw; width: 100vw;
overflow: hidden; 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
View 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
}

View file

@ -42,6 +42,7 @@ import {
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'
import { guessServiceIconUrl } from '../lib/serviceIcons'
const ICONS: Record<string, LucideIcon> = { const ICONS: Record<string, LucideIcon> = {
server: Server, server: Server,
@ -85,6 +86,30 @@ function resolveIcon(name: string | null | undefined): LucideIcon {
return ICONS[name.toLowerCase()] ?? Link2 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> = { const statusColors: Record<string, string> = {
online: '#2ECC71', online: '#2ECC71',
warning: '#E67E22', warning: '#E67E22',
@ -163,7 +188,6 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
} }
function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleFavorite: () => void }) { function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleFavorite: () => void }) {
const Icon = resolveIcon(bookmark.icon)
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
@ -173,7 +197,7 @@ function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleF
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' }}
> >
<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> <span style={{ fontSize: '13px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{bookmark.title}</span>
</a> </a>
<button <button
@ -201,11 +225,17 @@ function AddBookmarkModal({
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
const [icon, setIcon] = useState('link2') const [icon, setIcon] = useState('link2')
const [iconMode, setIconMode] = useState<'auto' | 'manual'>('auto')
const [categoryId, setCategoryId] = useState<number | 'new' | ''>('') const [categoryId, setCategoryId] = useState<number | 'new' | ''>('')
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)
useEffect(() => {
if (iconMode !== 'auto') return
setIcon(guessServiceIconUrl(title, url) ?? 'link2')
}, [title, url, iconMode])
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault() e.preventDefault()
setError('') setError('')
@ -282,7 +312,37 @@ function AddBookmarkModal({
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<label style={{ fontSize: '11px', color: '#7A7D85' }}>Icon</label> <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)}> <select style={inputStyle} value={icon} onChange={(e) => setIcon(e.target.value)}>
{Object.keys(ICONS).map((key) => ( {Object.keys(ICONS).map((key) => (
<option key={key} value={key}> <option key={key} value={key}>
@ -290,6 +350,7 @@ function AddBookmarkModal({
</option> </option>
))} ))}
</select> </select>
)}
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
@ -390,7 +451,7 @@ export default function BookNest() {
[...groups] [...groups]
.sort((a, b) => b.links.length - a.links.length) .sort((a, b) => b.links.length - a.links.length)
.slice(0, 5) .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] [groups]
) )
@ -464,9 +525,9 @@ export default function BookNest() {
<div key={qa.label} style={{ ...cardBase, padding: '14px' }} className="hover:!border-gold/20"> <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> <span style={{ fontSize: '11px', color: '#E8E6E0', fontWeight: 600, marginBottom: '10px' }}>{qa.label}</span>
<div className="flex items-center gap-1.5" style={{ marginBottom: '8px' }}> <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' }}> <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>
))} ))}
</div> </div>
@ -503,15 +564,12 @@ export default function BookNest() {
<h3 style={{ ...sectionTitle, fontSize: '12px', marginBottom: '18px' }}>Favorites</h3> <h3 style={{ ...sectionTitle, fontSize: '12px', marginBottom: '18px' }}>Favorites</h3>
<div className="flex flex-col" style={{ gap: '4px' }}> <div className="flex flex-col" style={{ gap: '4px' }}>
{favorites.length > 0 ? ( {favorites.length > 0 ? (
favorites.map((f) => { 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' }}> <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' }} /> <BookmarkIcon icon={f.icon} size={17} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>{f.title}</span> <span style={{ fontSize: '13px', color: '#E8E6E0' }}>{f.title}</span>
</a> </a>
) ))
})
) : ( ) : (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No favorites yet</p> <p style={{ fontSize: '12px', color: '#7A7D85' }}>No favorites yet</p>
)} )}

145
src/pages/Help.tsx Normal file
View 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>
)
}