Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/process-hardening.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: {
Expand Down
185 changes: 95 additions & 90 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
Expand DownExpand Up@@ -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;
Expand DownExpand Up@@ -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));
Expand DownExpand Up@@ -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));
Expand DownExpand Up@@ -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;
}
Expand DownExpand Up@@ -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 */
Expand Down
12 changes: 3 additions & 9 deletions src/components/DocumentViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,6 @@ import {
Maximize2,
Menu,
Minimize2,
Mic,
Minus,
Plus,
Quote,
Expand DownExpand Up@@ -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 (
Expand DownExpand Up@@ -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)]"
/>
</label>
<button
type="button"
className="grid h-[44px] w-[44px] shrink-0 place-items-center rounded-full text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]"
aria-label="Voice input"
>
<Mic className="h-4.5 w-4.5" />
</button>
<button
type="submit"
disabled={!canSummarizeDocument}
Expand Down
10 changes: 0 additions & 10 deletions src/components/applications-launcher-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
FileText,
Grid2X2,
Globe2,
Mic,
Menu,
MoreVertical,
Pill,
Expand DownExpand Up@@ -1065,15 +1064,6 @@ export function ApplicationsLauncherWorkspace({
className={cn(chatComposerInput, "w-full min-w-0")}
/>
</label>
<button
type="button"
className={cn(chatComposerIconButton, "disabled:cursor-not-allowed")}
aria-label="Voice input unavailable"
title="Voice input unavailable"
disabled
>
<Mic className="h-4.5 w-4.5" />
</button>
<button
type="submit"
disabled={filteredApps.length === 0}
Expand Down
9 changes: 0 additions & 9 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@
Loader2,
Menu,
MessageSquarePlus,
Mic,
Moon,
MoreHorizontal,
Pill,
Expand DownExpand Up@@ -57,7 +56,7 @@
} from "@/components/clinical-dashboard/mode-action-popup";
import {
cn,
chatComposerIconButton,

Check warning on line 59 in src/components/clinical-dashboard/master-search-header.tsx

View workflow job for this annotation

GitHub Actions/ verify

'chatComposerIconButton' is defined but never used
chatComposerInput,
chatComposerShell,
chatSendButton,
Expand DownExpand Up@@ -923,14 +922,6 @@
</button>
)}
</label>
<button
type="button"
className={cn(chatComposerIconButton, usesUniversalFooterStyle && "answer-footer-search-mic")}
aria-label="Voice input"
title="Voice input"
>
<Mic className="h-4.5 w-4.5" />
</button>
{usesUniversalFooterStyle ? <span className="answer-footer-search-divider" aria-hidden="true" /> : null}
<button
type="submit"
Expand Down
4 changes: 1 addition & 3 deletions src/components/forms/forms-search-results-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
FileText,
Home,
Menu,
Mic,
Moon,
MoreVertical,
Plus,
Expand DownExpand Up@@ -794,10 +793,9 @@ function BottomSearch({
<input
value={query}
onChange={(event) => onQueryChange(event.target.value)}
className="h-12 w-full rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-5 pr-20 text-sm font-medium text-[color:var(--text)] outline-none focus:border-[color:var(--clinical-accent)] focus:ring-4 focus:ring-[color:var(--clinical-accent)]/12"
className="h-12 w-full rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-5 pr-12 text-sm font-medium text-[color:var(--text)] outline-none focus:border-[color:var(--clinical-accent)] focus:ring-4 focus:ring-[color:var(--clinical-accent)]/12"
placeholder="Ask or search forms..."
/>
<Mic className="absolute right-12 top-1/2 h-5 w-5 -translate-y-1/2 text-[color:var(--text-muted)]" />
{query ? (
<button
type="button"
Expand Down
3 changes: 2 additions & 1 deletion src/lib/clinical-search.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1410,7 +1410,8 @@ export function clinicalRankExplanation(query: string, result: SearchResult): Se
// the same result.
const nextStepActionQuery = /\b(?:next step|step after|action)\b/i.test(query);
const riskFlowchartSource = nextStepActionQuery
? riskFlowchartZoneActionSource || (riskFlowchartLexicalSource && riskZoneActionPattern.test(riskFlowchartEvidenceText))
? riskFlowchartZoneActionSource ||
(riskFlowchartLexicalSource && riskZoneActionPattern.test(riskFlowchartEvidenceText))
: riskFlowchartLexicalSource || riskFlowchartZoneActionSource;
const riskFlowchartCanonicalTitle =
riskFlowchartQuery &&
Expand Down
5 changes: 4 additions & 1 deletion src/lib/rag.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3308,7 +3308,10 @@ function hasRiskFlowchartActionEvidence(query: string, results: SearchResult[],
const evidenceText = evidenceTextForGate(result);
if (!riskZoneActionPattern.test(evidenceText)) return false;
if (zonePhrasePattern.test(evidenceText)) return true;
return ["risk_matrix_cell", "flowchart_step", "diagram_decision"].includes(result.index_unit?.unit_type ?? "") && bareColourPattern.test(evidenceText);
return (
["risk_matrix_cell", "flowchart_step", "diagram_decision"].includes(result.index_unit?.unit_type ?? "") &&
bareColourPattern.test(evidenceText)
);
});
}

Expand Down
Loading
Loading