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:
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()} />
|
||||
|
||||
Reference in New Issue
Block a user