From 99f5a854dbb956a54a56f852f56532ddcdf7590a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Costa?= Date: Fri, 16 Oct 2026 15:55:15 -0300 Subject: [PATCH 01/24] Give status tiles a tooltip label, add the mail glyphs, and leave text fields without a focus ring Text fields and editors now mark focus with the caret alone; buttons, links and other controls keep the keyboard-only ring. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/icons/mail-icons.tsx | 260 +++++++++++++++++- apps/web/src/components/icons/nav-icons.tsx | 2 +- .../components/maily-editor/maily-theme.css | 22 +- apps/web/src/components/status-tile.tsx | 21 +- apps/web/src/styles/components.css | 56 +--- apps/web/src/styles/tokens/motion.css | 1 - 6 files changed, 281 insertions(+), 81 deletions(-) diff --git a/apps/web/src/components/icons/mail-icons.tsx b/apps/web/src/components/icons/mail-icons.tsx index e6c7ba669..068682efe 100644 --- a/apps/web/src/components/icons/mail-icons.tsx +++ b/apps/web/src/components/icons/mail-icons.tsx @@ -1,13 +1,17 @@ /** - * Thread-action glyphs (move, label) — path data from Lucide 1.31.0 - * (© Lucide contributors, ISC license: https://lucide.dev/license), drawn on - * the nav glyphs' frame and stroke. 14px: they sit in buttons and menu rows. + * Mail glyphs — thread actions, row kinds, screening verdicts and received + * events. Path data from Lucide 1.31.0 (© Lucide contributors, ISC license: + * https://lucide.dev/license), drawn on the nav glyphs' 24-unit frame. 14px by + * default; a tile passes a heavier stroke so the glyph keeps the weight of the + * 16-unit envelope and event glyphs beside it. */ import { Svg } from "./nav-icons"; -export function FolderInputIcon({ size = 14 }: { size?: number }) { +type IconProps = { size?: number; strokeWidth?: number | undefined }; + +export function FolderInputIcon({ size = 14, strokeWidth }: IconProps) { return ( - + @@ -15,9 +19,9 @@ export function FolderInputIcon({ size = 14 }: { size?: number }) { ); } -export function ArchiveIcon({ size = 14 }: { size?: number }) { +export function ArchiveIcon({ size = 14, strokeWidth }: IconProps) { return ( - + @@ -26,9 +30,9 @@ export function ArchiveIcon({ size = 14 }: { size?: number }) { } /** lucide octagon-alert. */ -export function SpamIcon({ size = 14 }: { size?: number }) { +export function SpamIcon({ size = 14, strokeWidth }: IconProps) { return ( - + @@ -37,9 +41,9 @@ export function SpamIcon({ size = 14 }: { size?: number }) { } /** lucide trash-2. */ -export function TrashIcon({ size = 14 }: { size?: number }) { +export function TrashIcon({ size = 14, strokeWidth }: IconProps) { return ( - + @@ -49,11 +53,241 @@ export function TrashIcon({ size = 14 }: { size?: number }) { ); } -export function TagIcon({ size = 14 }: { size?: number }) { +export function TagIcon({ size = 14, strokeWidth }: IconProps) { return ( - + ); } + +/** A static lucide inbox; the nav's InboxesGlyph animates. */ +export function InboxIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function MailsIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + ); +} + +export function AsteriskIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + ); +} + +export function BotIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + + + + ); +} + +/** A static lucide users; the nav's audience glyph animates. */ +export function UsersIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + + ); +} + +export function BugIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + + + + + + + + + ); +} + +export function LockIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function LockOpenIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function ClockIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +const SHIELD = + "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"; + +export function ShieldCheckIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function ShieldAlertIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + ); +} + +export function ShieldHalfIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function ScanEyeIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + + + + ); +} + +export function PaperclipIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + ); +} + +export function ReplyIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function ForwardIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +/** A static lucide webhook; the nav's webhooks glyph animates. */ +export function WebhookIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + + ); +} + +/** lucide undo-2 — Restore, out of Trash. */ +export function RestoreIcon({ size = 14, strokeWidth }: IconProps) { + return ( + + + + + ); +} + +export function XIcon({ size = 14, strokeWidth = 1.8 }: IconProps) { + return ( + + + + + ); +} + +export function PencilIcon({ size = 13, strokeWidth = 1.6 }: IconProps) { + return ( + + + + + ); +} + +export function ChevronLeftIcon({ size = 14, strokeWidth = 1.8 }: IconProps) { + return ( + + + + ); +} + +export function ImageOffIcon({ size = 14, strokeWidth = 1.6 }: IconProps) { + return ( + + + + + + + + + ); +} diff --git a/apps/web/src/components/icons/nav-icons.tsx b/apps/web/src/components/icons/nav-icons.tsx index 2406b16de..b74a8e2a2 100644 --- a/apps/web/src/components/icons/nav-icons.tsx +++ b/apps/web/src/components/icons/nav-icons.tsx @@ -25,7 +25,7 @@ export function Svg({ children, }: { size: number; - strokeWidth?: number; + strokeWidth?: number | undefined; children: React.ReactNode; }) { return ( diff --git a/apps/web/src/components/maily-editor/maily-theme.css b/apps/web/src/components/maily-editor/maily-theme.css index 918dfc666..d20d1aab2 100644 --- a/apps/web/src/components/maily-editor/maily-theme.css +++ b/apps/web/src/components/maily-editor/maily-theme.css @@ -148,13 +148,8 @@ body:has(.ms-modal-overlay):not(:has(.ms-modal-overlay .ms-maily)) .ms-maily-compact .ms-maily-surface { min-height: 120px; } -/* The body's frame takes the ring a textarea would. Tiptap gives the body - tabindex="0", so the global [tabindex="0"] ring would draw a second, square - ring inside the frame's, its corners past the frame's rounded ones. */ -.ms-maily .ms-maily-surface:focus-within { - border-color: var(--ms-focus-border); - box-shadow: var(--ms-focus-ring); -} +/* A text field marks focus with its caret alone. Tiptap gives the body + tabindex="0", which the global [tabindex="0"] ring would otherwise box. */ .ms-maily .ProseMirror:focus-visible { box-shadow: none; } @@ -266,9 +261,6 @@ body:has(.ms-modal-overlay):not(:has(.ms-modal-overlay .ms-maily)) padding: 5px 8px; outline: none; } -.ms-varpop-input:focus { - border-color: var(--ms-line-strong); -} .ms-varpop-input::placeholder { color: var(--ms-faint); } @@ -415,10 +407,6 @@ body:has(.ms-modal-overlay):not(:has(.ms-modal-overlay .ms-maily)) .ms-maily-picker-search input::placeholder { color: var(--ms-faint); } -/* The popover is the focus surface; the global input ring would draw a box inside it. */ -.ms-maily-picker-search input:focus-visible { - box-shadow: none; -} .ms-maily-picker-list { max-height: 320px; overflow-y: auto; @@ -488,9 +476,6 @@ body:has(.ms-modal-overlay):not(:has(.ms-modal-overlay .ms-maily)) font-size: 12px; outline: none; } -.ms-maily-picker-fallback:focus { - border-color: var(--ms-line-strong); -} .ms-maily-picker-fallback::placeholder { color: var(--ms-faint); } @@ -522,9 +507,6 @@ body:has(.ms-modal-overlay):not(:has(.ms-modal-overlay .ms-maily)) font-size: 12.5px; outline: none; } -.ms-maily-linkbox input:focus { - border-color: var(--ms-line-strong); -} /* ---- Maily's tippy popups (slash + variable suggestions) --------------- */ /* Maily scopes its Tailwind preflight to #mly-editor, but these popups mount diff --git a/apps/web/src/components/status-tile.tsx b/apps/web/src/components/status-tile.tsx index 71020241d..61e784ac4 100644 --- a/apps/web/src/components/status-tile.tsx +++ b/apps/web/src/components/status-tile.tsx @@ -1,5 +1,6 @@ import type { NavIconName } from "./icons/nav-icons"; import { NavGlyph } from "./icons/nav-icons"; +import { Tooltip } from "./tooltip"; /** Tile tint per badge tone, so a row's tile and its status pill read as one. */ export const TONE_COLOR = { @@ -14,19 +15,24 @@ export const TONE_COLOR = { * Rounded-square tile tinted by a status color, framing a small glyph: the * envelope on email rows, a section's nav glyph on the other lists. One frame * and one tint rule, so every list leads with the same mark. + * + * A `label` names what the tile means ("Quarantined", "Agent inbox") in a + * hover tooltip and to screen readers; without one the tile is decoration. */ export function StatusTile({ color, size = 28, + label, children, }: { color: string; size?: number; + label?: string | undefined; children: React.ReactNode; }) { - return ( + const tile = (