Link the documentation from the profile menu

docs.ihasmail.org is where installing, configuring and using ihasmail
are explained, and nothing in the app pointed at it. The profile menu is
where someone looks for the things that are about the app rather than
about their mail, so it goes there, above Settings, and opens in a new
tab: reading the docs is something you do beside your mail, not instead
of it.

`MenuItem` renders a real anchor when given an href, rather than a button
calling window.open. The browser's own handling of a link comes with it --
middle-click, a modifier-click, "open in new tab", the address on hover,
copying it -- none of which a button offers however carefully it is
scripted, and all of which someone expects of a menu entry that leaves the
app. Items without an href are the button they always were.

It also needed a line of CSS. The global rule for `a` coloured and
underlined the one entry that is a link, so the menu had a blue underlined
item among four plain ones, which reads as a mistake rather than a
distinction.

Verified against the mock: the entry sits above Settings, is an anchor to
https://docs.ihasmail.org with target=_blank and rel=noopener noreferrer,
and computes to the same colour, size and decoration as Settings beside
it.
This commit is contained in:
2026-08-27 15:34:30 -07:00
parent 05be820be4
commit 0fe75b280b
3 changed files with 40 additions and 4 deletions
+5
View File
@@ -282,6 +282,11 @@ img { max-width: 100%; }
.menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border-radius: var(--radius-sm); text-align: left; color: var(--fg); white-space: nowrap; }
.menu-item:hover, .menu-item.active { background: var(--bg-hover); }
.menu-item:disabled { opacity: .5; cursor: default; }
/* A menu entry that is a link still looks like a menu entry. The global rule
for `a` would otherwise colour and underline the one item that leaves the
app, which reads as a mistake rather than a distinction. */
a.menu-item { text-decoration: none; color: var(--fg); cursor: pointer; }
a.menu-item:hover { color: var(--fg); }
.menu-item.danger { color: var(--danger); }
.menu-item .menu-kbd { margin-left: auto; color: var(--fg-faint); font-size: .85em; }
.menu-item svg { color: var(--fg-muted); flex: 0 0 auto; }
+33 -3
View File
@@ -120,14 +120,44 @@ export interface MenuItemProps {
kbd?: string;
active?: boolean;
checked?: boolean;
/** Renders the item as a link. An external one gets a new tab. */
href?: string;
external?: boolean;
}
export function MenuItem({ icon, label, onClick, disabled, danger, kbd, active, checked }: MenuItemProps) {
return (
<button type="button" className={`menu-item ${danger ? "danger" : ""} ${active ? "active" : ""}`} onClick={onClick} disabled={disabled} role="menuitem">
export function MenuItem({ icon, label, onClick, disabled, danger, kbd, active, checked, href, external }: MenuItemProps) {
const inner = (
<>
{checked !== undefined ? <span style={{ width: 16, display: "inline-flex" }}>{checked ? "✓" : ""}</span> : icon}
<span className="grow truncate">{label}</span>
{kbd && <span className="menu-kbd">{kbd}</span>}
</>
);
const className = `menu-item ${danger ? "danger" : ""} ${active ? "active" : ""}`;
/*
* A real anchor when there is somewhere to go, rather than a button that
* calls window.open. The browser's own handling of a link comes with it --
* middle-click, a modifier-click, "open in new tab", the address on hover,
* copying it -- none of which a button offers however carefully it is
* scripted, and all of which someone expects from a menu entry that leaves
* the app.
*/
if (href) {
return (
<a
className={className}
href={href}
role="menuitem"
onClick={onClick}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
>
{inner}
</a>
);
}
return (
<button type="button" className={className} onClick={onClick} disabled={disabled} role="menuitem">
{inner}
</button>
);
}
+2 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, type ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { Calendar, ChevronsUpDown, FolderOpen, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users } from "lucide-react";
import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users } from "lucide-react";
import { useSession } from "@/store/session";
import { toggleTarget, useEffectiveTheme, useSettings } from "@/store/settings";
import { useMail } from "@/store/mail";
@@ -101,6 +101,7 @@ export function AppShell({ children }: { children: ReactNode }) {
</div>
</div>
<MenuSep />
<MenuItem icon={<BookOpen size={16} />} label="Documentation" href="https://docs.ihasmail.org" external />
<MenuItem icon={<Settings size={16} />} label="Settings" onClick={() => navigate("/settings")} />
<MenuItem icon={<RefreshCw size={16} />} label="Refresh" onClick={() => window.location.reload()} />
<MenuItem icon={<LogOut size={16} />} label="Sign out" onClick={() => void logout()} />