diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 4b7d68ce1d..d53cf10292 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -68,3 +68,10 @@ This document turns the current process review into phased, durable repo practic - **Known follow-up debts (documented, not actioned):** - Live migration history has duplicate-version churn (two each of `api_rate_limits`, `audit_logs`, `rag_queries_retention`, `audit_logs_service_role_policy`, `indexing_reliability_recovery`) from the same raw-apply habit. Do not rewrite history; treat as a caution for future applies. - Auth server is capped at 10 absolute DB connections (Supabase advisor); switch to percentage-based allocation in the dashboard before scaling instance size (not settable via SQL/MCP). + +## CSS cascade layering (2026-07-02) + +- The custom component classes in `src/app/globals.css` predate cascade layers, so they sat unlayered and silently beat Tailwind v4 utilities (which live in `@layer utilities`) on the same element. This caused three shipped UI bugs: the header source ledger ignoring responsive `hidden`, the composer clear button covering typed text (`pr-*` defeated), and the standalone-home status chips sliding under the mode pill. +- Conflict-free helper classes (`app-edge-backdrop`, `mobile-app-shell`, `mobile-popover-scroll`, `citation-link`, `animate-skeleton-shimmer`, `focus-ring-premium`, `source-capsule-hover`, `polished-scroll`) now live in `@layer components`, so utilities override them normally. Their call sites were audited for same-property utility collisions before the move. +- **Remaining debt:** the chrome classes (`edge-glass-header`, `universal-header-*`, `answer-footer-search-*`, `*-composer-edge`, `desktop-home-search-*`, `document-mobile-search-*`) stay intentionally unlayered because call sites stack utilities that set the same properties and today rely on the class winning (e.g. footer input font-size/padding, pill min-height, header shadow). Layering them requires reconciling each call site so rendered output is unchanged. Until then: when adding a utility to an element carrying one of these classes, check the class body first — the class wins. +- `tests/ui-overlap.spec.ts` is the standing regression guard for the visible symptom (overlapping header controls, composer clear-button geometry) across 640-1536px widths. diff --git a/playwright.config.ts b/playwright.config.ts index 73b3ee1e4b..b219750d9c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,7 +5,7 @@ const baseURL = getPlaywrightBaseUrl(); export default defineConfig({ testDir: "./tests", - testMatch: /.*ui-(smoke|stress|accessibility|tools)\.spec\.ts/, + testMatch: /.*ui-(smoke|stress|accessibility|tools|overlap)\.spec\.ts/, timeout: 60_000, retries: process.env.CI ? 1 : 0, expect: { diff --git a/src/app/globals.css b/src/app/globals.css index a9effb3d22..763a8a958f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -430,11 +430,12 @@ summary::-webkit-details-marker { color: #ecfeff; } -/* Layout utilities */ -.app-edge-backdrop { - background: var(--background); -} - +/* + * Layout chrome — INTENTIONALLY UNLAYERED: these classes beat Tailwind + * utilities on the same element (unlayered CSS wins over all layers). Their + * call sites stack utilities that set the same properties and rely on the + * class winning; the conflict-free helpers live in @layer components below. + */ .edge-glass-header { isolation: isolate; padding-left: max(0.75rem, var(--safe-area-left)); @@ -538,13 +539,6 @@ summary::-webkit-details-marker { opacity: 1; } -.answer-footer-search-mic { - height: 2.75rem; - width: 2.75rem; - min-width: 44px; - color: var(--text-muted); -} - .answer-footer-search-divider { display: none; height: 2.25rem; @@ -591,11 +585,6 @@ summary::-webkit-details-marker { color: var(--text-muted); } -.mobile-app-shell { - min-height: 100svh; - height: 100svh; -} - .dashboard-composer-edge { left: max(0.75rem, var(--safe-area-left)); right: max(0.75rem, var(--safe-area-right)); @@ -644,21 +633,6 @@ summary::-webkit-details-marker { font-weight: 560; } -.mobile-popover-scroll { - max-height: min(70svh, 28rem); -} - -@supports (height: 100dvh) { - .mobile-app-shell { - min-height: 100dvh; - height: 100dvh; - } - - .mobile-popover-scroll { - max-height: min(70dvh, 28rem); - } -} - @media (min-width: 640px) { .edge-glass-header { padding-left: max(1rem, var(--safe-area-left)); @@ -693,11 +667,6 @@ summary::-webkit-details-marker { width: 3.3rem; } - .answer-footer-search-mic { - height: 3.3rem; - width: 3.3rem; - } - .answer-footer-search-divider { display: block; } @@ -855,72 +824,108 @@ summary::-webkit-details-marker { } } -/* Scroll and print helpers */ -.citation-link { - position: relative; -} +/* + * Helper classes with no same-property utility conflicts at any call site + * live in @layer components so Tailwind utilities can override them. + * Audited 2026-07-02: the chrome classes above (edge-glass-header, + * universal-header-*, answer-footer-search-*, *-composer-edge, + * desktop-home-search-*) DO conflict with call-site utilities and stay + * unlayered deliberately — layering them changes rendered pixels. When + * adding a utility to an element carrying one of those classes, check the + * class body first; the class wins. + */ +@layer components { + .app-edge-backdrop { + background: var(--background); + } -.citation-link::after { - content: ""; - position: absolute; - top: -10px; - bottom: -10px; - left: -10px; - right: -10px; -} + .mobile-app-shell { + min-height: 100svh; + height: 100svh; + } -/* Premium Skeleton Shimmer animation with custom easing */ -.animate-skeleton-shimmer { - animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; -} + .mobile-popover-scroll { + max-height: min(70svh, 28rem); + } -@keyframes skeleton-pulse { - 0%, - 100% { - opacity: 1; + @supports (height: 100dvh) { + .mobile-app-shell { + min-height: 100dvh; + height: 100dvh; + } + + .mobile-popover-scroll { + max-height: min(70dvh, 28rem); + } } - 50% { - opacity: 0.35; + + /* Scroll and print helpers */ + .citation-link { + position: relative; } -} -/* Premium Double-Ring Focus style */ -.focus-ring-premium { - outline: none; -} -.focus-ring-premium:focus-visible { - outline: 2px solid var(--focus) !important; - outline-offset: 2px !important; - box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important; -} + .citation-link::after { + content: ""; + position: absolute; + top: -10px; + bottom: -10px; + left: -10px; + right: -10px; + } -/* Premium Hover Transitions for Source Capsules and Action row chips */ -.source-capsule-hover { - transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1) !important; -} + /* Premium Skeleton Shimmer animation with custom easing */ + .animate-skeleton-shimmer { + animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; + } -.source-capsule-hover:hover { - transform: translateY(-1px) scale(1.015) !important; - box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 8%, transparent) !important; -} + /* Premium Double-Ring Focus style */ + .focus-ring-premium { + outline: none; + } + .focus-ring-premium:focus-visible { + outline: 2px solid var(--focus) !important; + outline-offset: 2px !important; + box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important; + } -.polished-scroll { - scrollbar-color: color-mix(in srgb, var(--border-strong) 80%, transparent) transparent; - scrollbar-width: thin; -} + /* Premium Hover Transitions for Source Capsules and Action row chips */ + .source-capsule-hover { + transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1) !important; + } -.polished-scroll::-webkit-scrollbar { - height: 0.5rem; - width: 0.5rem; -} + .source-capsule-hover:hover { + transform: translateY(-1px) scale(1.015) !important; + box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 8%, transparent) !important; + } -.polished-scroll::-webkit-scrollbar-thumb { - background: color-mix(in srgb, var(--border-strong) 72%, transparent); - border-radius: 999px; + .polished-scroll { + scrollbar-color: color-mix(in srgb, var(--border-strong) 80%, transparent) transparent; + scrollbar-width: thin; + } + + .polished-scroll::-webkit-scrollbar { + height: 0.5rem; + width: 0.5rem; + } + + .polished-scroll::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--border-strong) 72%, transparent); + border-radius: 999px; + } + + .polished-scroll::-webkit-scrollbar-track { + background: transparent; + } } -.polished-scroll::-webkit-scrollbar-track { - background: transparent; +@keyframes skeleton-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.35; + } } /* User preference and alternate output modes */ diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 79d6a77e08..b2ffbf54ea 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -19,7 +19,6 @@ import { Maximize2, Menu, Minimize2, - Mic, Minus, Plus, Quote, @@ -362,7 +361,9 @@ function DocumentImage({ image }: { image: ImageRow }) { const showImageCaptionLine = cleanCaption && cleanCaption !== tableCaption; const displayLabels = smartEvidenceTags( image.labels, - [tableHeading, cleanCaption, image.tableTextSnippet ? sourceTextForCompactDisplay(image.tableTextSnippet) : null].filter(Boolean).join(" "), + [tableHeading, cleanCaption, image.tableTextSnippet ? sourceTextForCompactDisplay(image.tableTextSnippet) : null] + .filter(Boolean) + .join(" "), ); return ( @@ -2967,13 +2968,6 @@ export function DocumentViewer({ className="min-h-[44px] min-w-0 flex-1 bg-transparent px-2 text-base font-medium text-[color:var(--text)] outline-none placeholder:text-[color:var(--text-soft)]" /> - {usesUniversalFooterStyle ?