diff --git a/docs/redesign/09-ui-primitives-recipes.md b/docs/redesign/09-ui-primitives-recipes.md index 8d637f82a..eb50eb4af 100644 --- a/docs/redesign/09-ui-primitives-recipes.md +++ b/docs/redesign/09-ui-primitives-recipes.md @@ -43,21 +43,22 @@ Values are tokens; the target sizes follow the `-11`/`-12` spacing scale (L6). ### Interactive controls (tap targets) -| Recipe | Purpose | Tap | Hover | Active | Focus-visible | Disabled | -| ------------------------- | --------------------------------------- | ------------------- | ----------------- | ------ | ------------------------- | -------- | -| `controlBase` | Base for buttons (composed by others) | `min-h-11` | — (subclass adds) | ✅ | ✅ | ✅ | -| `primaryControl` | Primary command button | ✅ | ✅ | ✅ | ✅ | ✅ | -| `floatingControl` | Secondary / floating button | `min-h-11` | ✅ | ❌ | ✅ | ✅ | -| `toolbarButton` | Square icon button | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | -| `navPill` | Nav / segmented pill | `min-h-11` | ✅ | ❌ | ✅ | ✅ | -| `chatMicroAction` | Small chat action (copy, retry…) | `min-h-11 min-w-11` | ✅ | ❌ | ✅ | ✅ | -| `sourceCapsule` | Inline citation capsule (button shell) | `min-h-11` | ✅ | ❌ | ✅ (`focus-ring-premium`) | ❌ | -| `sourceCapsuleCountBadge` | Inset count chip inside `sourceCapsule` | — (non-tap) | ❌ | ❌ | ❌ | ❌ | -| `chatComposerIconButton` | Composer icon button | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | -| `chatSendButton` | Composer send (accent) | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | -| `sidebarItem` | Sidebar nav row | `min-h-11` | ✅ | ❌ | ✅ | ✅ | -| `sidebarToolTile` | Sidebar tool tile | `min-h-[64px]` | ✅ | ❌ | ✅ | ❌ | -| `shellChip` | Filter / mode chip | `min-h-11` | ✅ | ❌ | ❌ | ❌ | +| Recipe | Purpose | Tap | Hover | Active | Focus-visible | Disabled | +| ------------------------- | --------------------------------------------- | ------------------- | ----------------- | ------ | ---------------------------------------- | -------- | +| `controlBase` | Base for buttons (composed by others) | `min-h-11` | — (subclass adds) | ✅ | ✅ | ✅ | +| `primaryControl` | Primary command button | ✅ | ✅ | ✅ | ✅ | ✅ | +| `floatingControl` | Secondary / floating button | `min-h-11` | ✅ | ❌ | ✅ | ✅ | +| `toolbarButton` | Square icon button | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | +| `navPill` | Nav / segmented pill | `min-h-11` | ✅ | ❌ | ✅ | ✅ | +| `chatMicroAction` | Small chat action (copy, retry…) | `min-h-11 min-w-11` | ✅ | ❌ | ✅ | ✅ | +| `sourceCapsuleHit` | Citation capsule tap target (button) | `min-h-11` | ✅ (drives face) | ❌ | ✅ (`.source-capsule-hit:focus-visible`) | ❌ | +| `sourceCapsule` | Compact visible pill (`.source-capsule-face`) | — (inside hit) | ✅ (via hit) | ❌ | ✅ (via hit) | ❌ | +| `sourceCapsuleCountBadge` | Inset count chip inside `sourceCapsule` | — (non-tap) | ❌ | ❌ | ❌ | ❌ | +| `chatComposerIconButton` | Composer icon button | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | +| `chatSendButton` | Composer send (accent) | `h-11 w-11` | ✅ | ❌ | ✅ | ✅ | +| `sidebarItem` | Sidebar nav row | `min-h-11` | ✅ | ❌ | ✅ | ✅ | +| `sidebarToolTile` | Sidebar tool tile | `min-h-[64px]` | ✅ | ❌ | ✅ | ❌ | +| `shellChip` | Filter / mode chip | `min-h-11` | ✅ | ❌ | ❌ | ❌ | ### Form fields @@ -93,8 +94,10 @@ Overlay: **`sheetSurface`** / `sheetHandle` for the mobile bottom sheet ### Pills, badges, dots, tones - **Pills:** `metadataPill`, `subtleStatusPill` (`min-h-7`, non-tap labels). - Compose `sourceCapsule` + `sourceCapsuleCountBadge` for the answer sources - disclosure (icon, label, tabular count, chevron). + Compose `sourceCapsuleHit` (44px tap target) wrapping `sourceCapsule` + + `sourceCapsuleCountBadge` for the answer sources disclosure (icon, label, + tabular count, chevron). The visible pill reads compact while the hit target + keeps the WCAG touch size. - **Status dots:** `statusDotBase` + `statusDotReady` (success) / `statusDotReview` (warning) / `statusDotMuted`. - **Semantic tone triads** (border+bg+text, dark-mode-safe): `toneSuccess`, diff --git a/src/app/globals.css b/src/app/globals.css index 9ce9b7b87..288072954 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1947,29 +1947,42 @@ summary::-webkit-details-marker { box-shadow: none !important; } - /* Premium hover transitions for source capsules */ - .source-capsule-hover { + /* Source capsule: a compact visible face inside an invisible 44px tap target. + Border/shadow/transform live here (component layer) so the face reads small + while the hover, expanded, and focus chrome stay premium. Border colour is + owned here — the face carries no border-colour utility — so these rules win + without !important. */ + .source-capsule-face { + border-color: var(--border); transition: border-color 150ms ease, box-shadow 150ms ease, - transform 150ms ease !important; + transform 150ms ease; } - .source-capsule-hover:hover { - transform: translateY(-1px) !important; - box-shadow: var(--shadow-tight) !important; + .source-capsule-hit:hover .source-capsule-face { + transform: translateY(-1px); + border-color: var(--clinical-accent-border); + box-shadow: var(--shadow-tight); } - .source-capsule-hover[aria-expanded="true"] { + .source-capsule-hit[aria-expanded="true"] .source-capsule-face { border-color: var(--clinical-accent); box-shadow: var(--shadow-tight), var(--shadow-inset); } - .source-capsule-hover[aria-expanded="true"]:hover { - transform: translateY(-1px) !important; + .source-capsule-hit[aria-expanded="true"]:hover .source-capsule-face { + transform: translateY(-1px); + border-color: var(--clinical-accent); box-shadow: var(--shadow-tight), var(--shadow-inset); } + .source-capsule-hit:focus-visible .source-capsule-face { + outline: 2px solid var(--focus); + outline-offset: 2px; + box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent); + } + .polished-scroll { scrollbar-color: color-mix(in srgb, var(--border-strong) 65%, transparent) transparent; scrollbar-width: thin; @@ -2036,8 +2049,8 @@ summary::-webkit-details-marker { transition-duration: 0.01ms !important; } - .source-capsule-hover:hover, - .source-capsule-hover[aria-expanded="true"]:hover { + .source-capsule-hit:hover .source-capsule-face, + .source-capsule-hit[aria-expanded="true"]:hover .source-capsule-face { transform: none !important; } } diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx index f90a13d86..9fd15769b 100644 --- a/src/components/clinical-dashboard/answer-content.tsx +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -13,6 +13,7 @@ import { cn, sourceCapsule, sourceCapsuleCountBadge, + sourceCapsuleHit, statusDotMuted, statusDotReady, statusDotReview, @@ -602,23 +603,25 @@ export function NaturalLanguageAnswer({ ); diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index 1acd32e63..1fc77e3ba 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -74,10 +74,17 @@ export const chatActionRow = "flex min-h-tap flex-wrap items-center gap-1.5 text-xs font-semibold text-[color:var(--text-heading)] sm:min-h-8"; export const chatMicroAction = "inline-flex min-h-tap min-w-tap items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold text-[color:var(--text-muted)] transition hover:bg-[color:var(--clinical-accent-soft)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-50"; +// Answer "Sources" capsule. `sourceCapsuleHit` is an invisible 44px WCAG touch +// target that wraps the compact visible pill `sourceCapsule` (`.source-capsule-face`), +// so the control reads smaller and lighter without shrinking the tap area. Hover, +// expanded, and focus chrome are driven from the hit target's :hover / +// [aria-expanded] / :focus-visible in globals.css (@layer components). +export const sourceCapsuleHit = + "source-capsule-hit inline-flex min-h-tap w-fit items-center justify-center rounded-full outline-none"; export const sourceCapsule = - "source-capsule-hover focus-ring-premium inline-flex min-h-tap items-center gap-1.5 rounded-full border border-[color:var(--border)] bg-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] px-3 text-xs font-medium text-[color:var(--clinical-accent)] transition hover:border-[color:var(--clinical-accent-border)]"; + "source-capsule-face inline-flex items-center gap-1.5 rounded-full border bg-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] px-2.5 py-1 text-2xs font-medium text-[color:var(--clinical-accent)]"; export const sourceCapsuleCountBadge = - "nums inline-flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-[color:var(--surface-raised)] px-1.5 text-2xs font-semibold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]"; + "nums inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-[color:var(--surface-raised)] px-1 text-[10px] font-semibold leading-none text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]"; export const evidenceRow = "flex min-h-12 w-full items-center justify-between gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; export const clinicalNotesRow =