From a8439d4c1b166f08b39d10ca43de3ea0627f844f Mon Sep 17 00:00:00 2001 From: Jake Tracey Date: Wed, 2 Sep 2026 07:23:43 +1000 Subject: [PATCH] Lift the answer-quality popover above the follow-up cards; redraw the quality icons The dropdown that opens from the answer-quality trigger was painted under the "Ask next" cards that arrive after it, whatever z-index it carried. Every late-arriving block fades up with `rp-answer-tail`, whose animation filled `both`; a filled-forward opacity/transform interpolation leaves the block a permanent stacking context, so the popover inside the actions row could never rise above the sibling blocks below it. The keyframes only declare `from`, so `backwards` ends on identical pixels and releases the context once the entrance is over. Same change to `rp-answer-in`, the answer body, for the same reason. The confidence icons move to the 24-unit grid and 1.7 stroke of the answer-action icons beside them, with the stroke thinning as the box grows so each size lands at the same optical weight. The exclamation dots are zero-length round-capped strokes rather than 0.15-radius filled circles, which rendered below a pixel and left the warning triangle reading as a bare outline. Verified in Chrome on a local build: dropdown hit-tested on top of all three follow-up cards at 1280px, bottom sheet at a true 390px layout viewport with no horizontal overflow, both on the default and Observatory (dark) palettes; forcing the old fill mode back on reproduces the trapped panel. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GYuuU1yPFeGKZpkwW2mLs6 --- apps/web/src/components/QualityGauge.tsx | 126 ++++++++++------------- apps/web/src/styles.css | 15 ++- 2 files changed, 69 insertions(+), 72 deletions(-) diff --git a/apps/web/src/components/QualityGauge.tsx b/apps/web/src/components/QualityGauge.tsx index 45ed091..2bc777e 100644 --- a/apps/web/src/components/QualityGauge.tsx +++ b/apps/web/src/components/QualityGauge.tsx @@ -45,24 +45,68 @@ function bandColour(score: number): string { return 'var(--rp-bad-ink)' } -function ShieldCheckIcon({ className = 'h-3.5 w-3.5' }: { className?: string }) { +// --------------------------------------------------------------------------- +// Icons. One 24-unit grid, round caps and joins, and a stroke that thins as the +// box grows so every size lands at about the same optical weight - the grid and +// the 1.7 stroke of the answer-action icons (`ActionIcon` in AskPage), so the +// quality trigger reads as one of that row rather than a glyph pasted in from a +// different set. The exclamation dots are zero-length strokes (`h.01`): with +// round caps a stroke draws a dot exactly one stroke-width across at any render +// size, where the filled 0.15-radius circles these replaced vanished below a +// pixel and left the triangle reading as a bare outline. +// --------------------------------------------------------------------------- + +type GlyphSize = 'sm' | 'md' | 'lg' + +const GLYPH_SIZE: Record = { + sm: { box: 'h-3.5 w-3.5', stroke: 2 }, + md: { box: 'h-4 w-4', stroke: 1.8 }, + lg: { box: 'h-[1.15rem] w-[1.15rem]', stroke: 1.7 }, +} + +const GLYPH = { + shieldCheck: + 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 01-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 011-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 011.52 0C14.51 3.81 17 5 19 5a1 1 0 011 1zM9 12l2 2 4-4', + checkCircle: 'M22 12a10 10 0 11-20 0 10 10 0 0120 0zM9 12l2 2 4-4', + info: 'M22 12a10 10 0 11-20 0 10 10 0 0120 0zM12 16v-4M12 8h.01', + alertTriangle: + 'M21.73 18l-8-14a2 2 0 00-3.48 0l-8 14A2 2 0 004 21h16a2 2 0 001.73-3zM12 9v4M12 17h.01', +} + +function Glyph({ d, size }: { d: string; size: GlyphSize }) { + const { box, stroke } = GLYPH_SIZE[size] return ( ) } +function ShieldCheckIcon({ size = 'sm' }: { size?: GlyphSize }) { + return +} + +function CheckCircleIcon({ size = 'sm' }: { size?: GlyphSize }) { + return +} + +function InfoCircleIcon({ size = 'sm' }: { size?: GlyphSize }) { + return +} + +function AlertTriangleIcon({ size = 'sm' }: { size?: GlyphSize }) { + return +} + function MiniMeter( { label, description, score }: { label: string; description: string; score: number }, ) { @@ -148,62 +192,6 @@ export function TrustSignals({ quality, showLabel = true }: TrustSignalsProps) { // apps/web/src/lib/confidence.ts) so this and the mini-meters never disagree. // --------------------------------------------------------------------------- -function CheckCircleIcon() { - return ( - - ) -} - -function InfoCircleIcon({ className = 'h-3.5 w-3.5' }: { className?: string }) { - return ( - - ) -} - -function AlertTriangleIcon({ className = 'h-4 w-4' }: { className?: string }) { - return ( - - ) -} - /** * The sentence that elaborates each confidence level. Held in one place so the * inline pill (`ConfidenceIndicator`, still used by search and the docs @@ -322,7 +310,7 @@ function ConfidencePill( tone === 'bad' ? 'rp-badge-bad' : 'rp-badge-warn' }`} > - + {label} {open @@ -505,12 +493,12 @@ export function AnswerQualityDisclosure( const { tone, labelled } = TRIGGER_TONE[confidence.state] const loud = tone !== 'quiet' - const glyph = (size: string) => + const glyph = (size: GlyphSize) => loud - ? + ? : confidence.state === 'high' - ? - : + ? + : const body = ( <> @@ -519,7 +507,7 @@ export function AnswerQualityDisclosure( className='mt-px shrink-0' style={{ color: loud ? `var(--rp-${tone}-ink)` : 'var(--rp-ink-3)' }} > - {glyph('h-4 w-4')} + {glyph('md')}

{confidence.label}

@@ -650,7 +638,7 @@ export function AnswerQualityDisclosure( } : undefined} > - {glyph('h-[1.15rem] w-[1.15rem]')} + {glyph('lg')} {labelled ? {confidence.label} : null} {open ? panel : null} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index d5bccdb..da2738a 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1903,7 +1903,7 @@ label.rp-btn:has(input:focus-visible) { /* The answer body rising into place after the steps clear. */ .rp-answer-in { - animation: rp-answer-in 420ms var(--rp-ease) both; + animation: rp-answer-in 420ms var(--rp-ease) backwards; } @keyframes rp-answer-in { @@ -1944,9 +1944,18 @@ label.rp-btn:has(input:focus-visible) { * the whole card; now the card's treatment never changes and these simply fade * up under the answer, in the order they read. Same 4px rise, easing and * --rp-stage-i stagger as the timeline they hand over from, held a beat longer - * because nobody is waiting on them. */ + * because nobody is waiting on them. + * + * Fill mode is `backwards`, not `both`, on purpose. The keyframes only declare + * `from`, so `both` and `backwards` end on the same pixels - but `both` keeps + * the finished animation applied to opacity and transform forever, and Chrome + * treats that as a permanent stacking context on every one of these blocks. + * The answer-quality dropdown lives inside the actions row and was being + * painted under the follow-up cards that arrive after it, whatever z-index it + * carried. Releasing the fill once the entrance is over releases the context + * too, so a popover in one tail block stacks over the tail blocks below it. */ .rp-answer-tail { - animation: rp-stage-in 320ms var(--rp-ease) both; + animation: rp-stage-in 320ms var(--rp-ease) backwards; animation-delay: calc(var(--rp-stage-i, 0) * 60ms); }