diff --git a/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md index d4db6e0..4134e23 100644 --- a/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md +++ b/.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/22-UI-SPEC.md @@ -62,13 +62,26 @@ Declared values (existing Pulse scale, multiples of 4 — see `DESIGN.md` §3): | Role | Size | Weight | Line Height | |------|------|--------|-------------| | Body | 14px (`text-sm`) | 400 (regular) | 1.5 | -| Label | 12px (`text-xs font-medium`) | 500 (medium) | 1.4 | -| Heading (card title) | 18px (`text-lg font-semibold`) | 600 (semibold) | 1.2 | +| Label | 12px (`text-xs text-muted-foreground`) | 400 (regular) | 1.4 | +| Heading (card title) | 18px (`text-lg font-bold`) | 700 (bold) | 1.2 | | Display (page title) | 24px (`text-2xl font-bold tracking-tight`) | 700 (bold) | 1.2 | -This phase introduces two new weights of its own: **400 (regular)** for body/evidence text and **600 (semibold)** for card titles/labels — matching `DESIGN.md`'s existing `text-lg font-semibold` card-title convention. The **700 (bold)** Display row is the pre-existing, unchanged `PageHeader` H1 (`components/navigation/page-header.tsx`), reused as-is on both new pages — not a new weight this phase introduces, called out here only because the template requires all four roles documented. +**Exactly two font weights are used on this phase's pages: 400 (regular) and +700 (bold).** Label is differentiated from Body by size (12px vs 14px) and +color (`text-muted-foreground`) — not by weight — so both share 400. Heading +(card title) is differentiated from Display (page title) by size (18px vs +24px) alone — both use 700, matching the pre-existing, unchanged `PageHeader` +H1 (`components/navigation/page-header.tsx`) reused as-is on both new pages. +This intentionally deviates from `DESIGN.md`'s incidental `text-lg +font-semibold` (600) card-title example elsewhere in the app: shadcn's +`CardTitle` primitive defaults to `font-semibold` (`components/ui/card.tsx` +line 35), so every `` on this phase's pages must pass an explicit +`className="font-bold"` override (Tailwind/`cn()` merge lets the later class +win) to hit the declared 700 weight. Do this only inside +`components/phishing/*` — do not change the shared `CardTitle` primitive's +default, which other pages still rely on. -Monospace (IBM Plex Mono, `font-mono`) is mandatory for: Message-ID, Return-Path, sender/recipient email addresses in header dumps, extracted URLs (D-09), attachment hashes (D-10), and all timestamps — per `DESIGN.md` §2's "mono for numerics/IDs/timestamps" rule. +Monospace (IBM Plex Mono, `font-mono`) is mandatory for: Message-ID, Return-Path, sender/recipient email addresses in header dumps, extracted URLs (D-09), attachment hashes (D-10), and all timestamps — per `DESIGN.md` §2's "mono for numerics/IDs/timestamps" rule. Monospace is a font-family distinction, not a weight — it does not count against the 2-weight budget above. --- @@ -121,7 +134,7 @@ Monospace (IBM Plex Mono, `font-mono`) is mandatory for: Message-ID, Return-Path | Primary CTA (Action Area) | "Approve selected" — disabled until ≥1 checkbox is checked | | Secondary CTA (Action Area) | "Remediate approved actions" | | Destructive CTA (Action Area) | "Mark as false positive" | -| Reclassify CTA (Classification card) | "Reclassify" | +| Reclassify CTA (Classification card) | "Reclassify ticket" | | D-07 empty state (no report yet) heading | "Not yet triaged" | | D-07 empty state body | "This ticket hasn't been scanned for phishing indicators yet. Run analysis now to extract evidence and see a classification." | | D-07 empty state CTA | "Analyze this ticket" → `POST /api/phishing/tickets/{ticket_id}/analyze`, then refetch | @@ -206,7 +219,7 @@ Single-column stack (`space-y-6`) at ` ``` -Each section is a shadcn `` with a `` (18px/600 per Typography) carrying a leading `lucide-react` icon (`h-4 w-4 mr-2`, inline per `DESIGN.md` §6). +Each section is a shadcn `` with a `` (18px/700 per Typography — the explicit `font-bold` override is required since `CardTitle` defaults to `font-semibold`) carrying a leading `lucide-react` icon (`h-4 w-4 mr-2`, inline per `DESIGN.md` §6). ### Campaigns list page — layout @@ -261,18 +274,21 @@ If `campaigns` list is empty: render ``: each extracted URL as `{url}` with a trailing icon-only Copy button (`lucide-react` `Copy`, `size="icon"` `variant="ghost"`) that calls `navigator.clipboard.writeText(url)` + `toast.success('Copied')`. **Never** an ``/`href`, never a click handler that navigates. Empty state: "No URLs found in this message." (size="sm" inline text, not the full `EmptyState` component — this is a tab body, not a page section). -3. **Attachments (D-10)** — shadcn `Table`: filename, content-type (`StatusBadge variantClass="bg-slate-500/15 text-slate-600"`), size (human-readable, e.g. "482 KB"), hash (`font-mono text-xs`, truncated with a `title` attribute showing the full value, same Copy-button affordance as URLs — copying a hash carries no click risk, so this one is decorative convenience, not a safety requirement). No content is ever fetched or rendered — filenames/hashes only, per EVID-04. +2. **URLs (D-09)** — ``: each extracted URL as `{url}` with a trailing icon-only Copy button (`lucide-react` `Copy`, `size="icon"` `variant="ghost"`, `aria-label="Copy URL"`) that calls `navigator.clipboard.writeText(url)` + `toast.success('Copied')`. **Never** an ``/`href`, never a click handler that navigates. Empty state: "No URLs found in this message." (size="sm" inline text, not the full `EmptyState` component — this is a tab body, not a page section). +3. **Attachments (D-10)** — shadcn `Table`: filename, content-type (`StatusBadge variantClass="bg-slate-500/15 text-slate-600"`), size (human-readable, e.g. "482 KB"), hash (`font-mono text-xs`, truncated with a `title` attribute showing the full value, same icon-only Copy button affordance as URLs, `aria-label="Copy hash"` — copying a hash carries no click risk, so this one is decorative convenience, not a safety requirement). No content is ever fetched or rendered — filenames/hashes only, per EVID-04. 4. **Body preview** — `
{bodyPreview}
`. Plain text only — never `dangerouslySetInnerHTML`, never rendered as HTML even if the source email was HTML. This is the sanitized/truncated preview column (`messages.body_preview`), not the raw body. 5. **Blast Radius** — reads `getBlastRadius()` output (planner's call whether fetched fresh per page load or read from the latest classification's persisted `reasons`, per CONTEXT.md discretion). If `status === 'unavailable'`: render the copy from the Copywriting Contract table above (reason-specific), no data table, no CTA — this is informational only. If `status === 'ok'`: a stat row of labeled numbers (Matched / Delivered / Held / Rejected / Clicked, each `.num` utility) above a per-recipient `Table` (recipient email `font-mono text-xs`, status `StatusBadge` per the color table). +All icon-only affordances in this card (Copy buttons above) must carry an +explicit `aria-label` — never rely on the icon alone for accessibility. + --- ## Classification Display Spec (REVIEW-04) `ClassificationCard` — read-only display of the latest `classifications` row (`ORDER BY created_at DESC LIMIT 1`). -- Header row: verdict `StatusBadge` (per color table) + `{confidence}% confidence` (`.num` inline) + right-aligned "Reclassify" button (`variant="outline"`, `size="sm"`), gated: `hasPermission(session.user.role, 'phishing', 'analyze')` — hidden (not disabled) when absent, since re-triggering analysis is a distinct, lower-stakes action than approve/remediate and REVIEW-06's disabled-vs-hidden requirement only names approve/remediate explicitly. +- Header row: verdict `StatusBadge` (per color table) + `{confidence}% confidence` (`.num` inline) + right-aligned "Reclassify ticket" button (`variant="outline"`, `size="sm"`), gated: `hasPermission(session.user.role, 'phishing', 'analyze')` — hidden (not disabled) when absent, since re-triggering analysis is a distinct, lower-stakes action than approve/remediate and REVIEW-06's disabled-vs-hidden requirement only names approve/remediate explicitly. - Summary: `classifications.summary`, `text-sm`. - Reasons: `
    `, one `
  • ` per `reasons` array entry. - Recommended actions: informational chip row only (`StatusBadge` per action-status color, muted/slate tone since these aren't yet approved) — humanized labels (`block_sender` → "Block sender", `purge_message` → "Purge message", etc.). This card never renders checkboxes or editable params — that interactive surface lives entirely in `ActionAreaCard` below, so there is exactly one place an operator can approve an action, not two.