From 0fe75b280bc3989a5859445e15e47413d92ad844 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 27 Aug 2026 15:34:30 -0700 Subject: [PATCH] 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. --- web/src/styles/app.css | 5 +++++ web/src/ui/popover.tsx | 36 +++++++++++++++++++++++++++++++++--- web/src/views/AppShell.tsx | 3 ++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/web/src/styles/app.css b/web/src/styles/app.css index b9c3578..f3cbd4d 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -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; } diff --git a/web/src/ui/popover.tsx b/web/src/ui/popover.tsx index 518c77c..7e42dc2 100644 --- a/web/src/ui/popover.tsx +++ b/web/src/ui/popover.tsx @@ -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 ( - ); } diff --git a/web/src/views/AppShell.tsx b/web/src/views/AppShell.tsx index a6421ad..e8a9198 100644 --- a/web/src/views/AppShell.tsx +++ b/web/src/views/AppShell.tsx @@ -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 }) { + } label="Documentation" href="https://docs.ihasmail.org" external /> } label="Settings" onClick={() => navigate("/settings")} /> } label="Refresh" onClick={() => window.location.reload()} /> } label="Sign out" onClick={() => void logout()} />