docs(22): fix typography weight overflow in UI design contract
Checker flagged 4 distinct font weights in use together (400/500/600/700) against a 2-weight max. Consolidate to 400 (Body+Label, differentiated by size/color) and 700 (Heading+Display, differentiated by size only) — requires an explicit font-bold override on CardTitle since shadcn's default is font-semibold. Also addresses two non-blocking flags: "Reclassify" -> "Reclassify ticket", and aria-label on icon-only Copy buttons (URL/hash). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
This commit is contained in:
parent
fd2280ab2c
commit
5d10f66f6b
1 changed files with 25 additions and 9 deletions
|
|
@ -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 `<CardTitle>` 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 `<md`; a 2-column grid for the bottom two r
|
|||
</main>
|
||||
```
|
||||
|
||||
Each section is a shadcn `<Card>` with a `<CardHeader><CardTitle>` (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 `<Card>` with a `<CardHeader><CardTitle className="font-bold">` (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 `<EmptyState icon={ShieldAlert} title="No c
|
|||
`EvidenceCard` renders one message's parsed evidence. If the campaign has more than one linked report/message, an outer `<Select>` ("Report: Ticket #{ticketNumber} — {relative date}") chooses which message's evidence populates the tabs below; default selection = most recently linked report. Tabs (`components/ui/tabs.tsx`):
|
||||
|
||||
1. **Headers** — a 2-column definition-list grid (`grid grid-cols-2 gap-x-6 gap-y-2 text-sm`) of From / Display name / Sender domain / Reply-To / Return-Path / To / Cc / Subject / Date / Message-ID / Received chain (collapsed under an `Accordion` item since it can be long) / SPF / DKIM / DMARC. SPF/DKIM/DMARC render as `StatusBadge` (pass=green, fail=red, none=slate, per the color table). Message-ID, Return-Path, and all email addresses render `font-mono text-xs`.
|
||||
2. **URLs (D-09)** — `<UrlList>`: each extracted URL as `<code className="font-mono text-xs truncate">{url}</code>` 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 `<a>`/`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)** — `<UrlList>`: each extracted URL as `<code className="font-mono text-xs truncate">{url}</code>` 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 `<a>`/`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** — `<pre className="max-h-96 overflow-y-auto whitespace-pre-wrap rounded-md bg-muted p-4 font-mono text-xs">{bodyPreview}</pre>`. 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: `<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">`, one `<li>` 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue