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
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,4 +560,5 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-15 | codex/outstanding-work-cleanup | 0c56f27a37af88a073d2bb695d2cf4c05067ff4f + working-tree diff | repo-wide outstanding-work reconciliation and cleanup | No high-confidence P0/P1 remained in the locally executable scope. Fixed the stale architecture index for DSM/specifier routes, pruned redundant Knip configuration, removed the unused `SignedImage` default export, and reconciled maintained docs so completed/superseded plans no longer present as active work. Retained explicitly blocked work: provider-gated operator actions, the live-eval shadow reindex harness, deep-memory section-ownership design, and RAG follow-ups that require live evidence or product/security decisions. Full Knip findings were triaged rather than mass-deleted; unresolved-import scan is clean. | Offline `npm run verify:cheap` passed with 2,417 tests/1 skipped; docs links 806, script refs 264, codebase index 35/35; Knip unresolved scan clean; typecheck clean; focused Vitest 15/15; env-name parity clean; `git diff --check`. Provider-backed Supabase/OpenAI/GitHub/Railway, dependency audit, browser, build, drift, and release gates not run. |
| 2026-07-15 | codex/design-polish-pass | 0c56f27a37af88a073d2bb695d2cf4c05067ff4f + reviewed working diff | full live design, responsive, UX, accessibility, design-system, routing, performance, lint, testing, documentation, and release-readiness review | No P0/P1 reproduced. Fixed the P2 duplicate phone scroll surface in the shared standalone shell and added a regression test; fixed mode-menu Tab dismissal; explicitly hid decorative dynamic icons; restricted press scaling to motion-safe environments; moved sheet backdrops and forced-colour glass/backdrop behavior onto design tokens. External target fidelity remains blocked because no independent Figma/mockup source was supplied. | Live 30-route desktop + 21-route phone sweep; targeted 320/390/639/768/1440/1920 proofs; `npm run verify:cheap` (2417 passed/1 skipped); `npm run verify:ui` (175/175); focused keyboard 1/1; accessibility media/axe 5/5; scoped Prettier, ESLint, TypeScript, type-scale, and icon-scale passed. Provider-backed checks not run. |
| 2026-07-15 | codex/documents-closed-default | 49f63791bced2b1764a11ab723aea94b45b026b6 | documents viewer disclosure defaults and related defect hunt | Fixed the inconsistent default-open document viewer sections by making indexed text, high-yield summary, tables/diagrams, and indexing details a native mutually exclusive closed disclosure group. The section navigation opens its requested disclosure and deep-linked evidence still reveals its target. The hunt also removed the explicitly open nested table-review queue, preserved printable summary content through the browser print lifecycle, and added cold-server readiness guards to the affected viewer tests. No other high-confidence default-open defect remains in the live Documents scope. | `npm run verify:cheap`; TypeScript; focused ESLint/Prettier; clean-worktree mocked Chromium coverage for deep-linked evidence, structured summary, closed/mutually-exclusive disclosures, navigation opening, and print state restore; `git diff --check`. Turbopack could not run through the local external `node_modules` junction, so clean browser verification used Next's supported Webpack dev mode. No Supabase/OpenAI/live-provider checks run. |
| 2026-07-17 | codex/header-footer-scroll-timing-20260717 | 8298bfdcb40c207dbac1128e83c07b4aba782e32 | header and bottom-composer scroll timing, motion, responsive behavior, and merge readiness | Added deliberate hide/reveal travel thresholds with direction-reset handling, aligned header and composer easing/durations, and preserved reduced-motion and breakpoint behavior. No remaining high-confidence P0-P2 defect was found in the scoped diff or focused live behavior. | Focused Vitest 7/7; targeted Chromium UI 5/5; scoped ESLint; Prettier; full TypeScript; full lint; `git diff --check`. `verify:cheap` reached the aggregate Vitest phase, where two repository graph scans exceeded their 30-second test timeout under local disk contention; isolated assertions passed until the same timeout. No Supabase/OpenAI/live-provider checks run. |
| 2026-07-15 | HEAD / main snapshot (detached review worktree) | 0c56f27a37af88a073d2bb695d2cf4c05067ff4f | comprehensive repository review | Changes requested: two P1 defects (high-risk clinical claim support can accept a different trigger condition on lexical overlap; document-mode URL auto-run loops on navigation and leaves search loading indefinitely), one P2 supply-chain guard gap (the action-pin checker accepts mutable major tags and ignores SHA-pinned major versions), and one P3 orientation-doc gap (DSM and legacy specifier routes are absent from the codebase index). No P0 found. | Node 24/npm 11 and `npm ls --depth=0`; format, runtime, lint, TypeScript, sitemap/brand/icon/type-scale, docs links/scripts, CI/action/Codex guards; coverage 259 files passed/1 skipped and 2,417 tests passed/1 skipped; offline RAG 36 fixtures plus 282 tests; production build/client-secret scan; deployment boot smoke; critical Chromium 8/10 with two reproducible document-search failures; accessibility 5/5; viewport/focus checks through 1920x1080. Provider-backed governance, quality, drift, tenancy, hosted CI, and the cross-browser matrix were blocked or skipped by policy/targeted failures. |
26 changes: 22 additions & 4 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1243,7 +1243,12 @@ export function MasterSearchHeader({
usesCompactMobileBottomStyle && "document-mobile-search-compact",
showFooterSearchChips && "flex flex-col items-center gap-2.5",
shouldHideBottomOnScroll &&
"max-sm:transition-transform max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none",
cn(
"max-sm:transition-transform motion-reduce:transition-none",
bottomComposerHidden
? "max-sm:duration-[240ms] max-sm:ease-[cubic-bezier(0.4,0,0.2,1)]"
: "max-sm:duration-200 max-sm:ease-[cubic-bezier(0.22,1,0.36,1)]",
),
)}
>
{usesBottomComposerPlacement ? <div className="answer-footer-search-backdrop" aria-hidden="true" /> : null}
Expand Down Expand Up @@ -1516,8 +1521,18 @@ export function MasterSearchHeader({
// fixed-position mobile mode menu keeps the viewport as its containing block.
hideStrategy === "overlay" &&
(overlayAllBreakpoints
? "transition-transform duration-200 ease-out motion-reduce:transition-none"
: "max-sm:transition-transform max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none"),
? cn(
"transition-transform motion-reduce:transition-none",
headerChromeHidden
? "duration-[240ms] ease-[cubic-bezier(0.4,0,0.2,1)]"
: "duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
)
: cn(
"max-sm:transition-transform motion-reduce:transition-none",
headerChromeHidden
? "max-sm:duration-[240ms] max-sm:ease-[cubic-bezier(0.4,0,0.2,1)]"
: "max-sm:duration-200 max-sm:ease-[cubic-bezier(0.22,1,0.36,1)]",
)),
hideStrategy === "overlay" &&
headerChromeHidden &&
(overlayAllBreakpoints ? "-translate-y-full" : "max-sm:-translate-y-full"),
Expand Down Expand Up @@ -1734,7 +1749,10 @@ export function MasterSearchHeader({
data-scroll-hidden={headerChromeHidden ? "true" : undefined}
data-testid="universal-header-collapse"
className={cn(
"max-sm:grid max-sm:transition-[grid-template-rows] max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none",
"max-sm:grid max-sm:transition-[grid-template-rows] motion-reduce:transition-none",
headerChromeHidden
? "max-sm:duration-[240ms] max-sm:ease-[cubic-bezier(0.4,0,0.2,1)]"
: "max-sm:duration-200 max-sm:ease-[cubic-bezier(0.22,1,0.36,1)]",
headerChromeHidden ? "max-sm:[grid-template-rows:0fr]" : "max-sm:[grid-template-rows:1fr]",
)}
{...chromeFocusProps}
Expand Down
58 changes: 50 additions & 8 deletions src/components/clinical-dashboard/use-hide-on-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,58 @@ const topRevealOffset = 8;
// Minimum per-event delta (px) before we treat movement as intentional, to
// avoid jitter from momentum settling and fractional scroll positions.
const minimumDelta = 4;
// Once the header's activation band has passed, require a little more
// continuous downward travel before hiding. Reappearing should be easier, but
// still deliberate enough that trackpad/touch momentum cannot flicker the
// chrome at a direction change.
const hideIntentDistance = 24;
const revealIntentDistance = 12;

type ScrollDirection = "down" | "up" | null;

/** Pure scroll-direction evaluation used by the hook; exported for unit tests. */
export function computeScrollHideUpdate(params: { offset: number; lastOffset: number; currentlyHidden: boolean }): {
export function computeScrollHideUpdate(params: {
offset: number;
lastOffset: number;
currentlyHidden: boolean;
direction?: ScrollDirection;
directionTravel?: number;
}): {
hidden: boolean;
lastOffset: number;
direction: ScrollDirection;
directionTravel: number;
} {
const { offset, lastOffset, currentlyHidden } = params;
const { offset, lastOffset, currentlyHidden, direction = null, directionTravel = 0 } = params;
// Ignore iOS rubber-band overscroll at the top.
if (offset < 0) return { hidden: currentlyHidden, lastOffset };
if (offset < 0) return { hidden: currentlyHidden, lastOffset, direction, directionTravel };
const delta = offset - lastOffset;
if (offset <= topRevealOffset) {
return { hidden: false, lastOffset: offset };
return { hidden: false, lastOffset: offset, direction: null, directionTravel: 0 };
}
if (Math.abs(delta) < minimumDelta) {
return { hidden: currentlyHidden, lastOffset };
return { hidden: currentlyHidden, lastOffset, direction, directionTravel };
}
if (delta > 0) {
return { hidden: offset > hideActivationOffset, lastOffset: offset };

const nextDirection: Exclude<ScrollDirection, null> = delta > 0 ? "down" : "up";
const nextDirectionTravel = nextDirection === direction ? directionTravel + Math.abs(delta) : Math.abs(delta);
let hidden = currentlyHidden;

if (!currentlyHidden && nextDirection === "down" && offset > hideActivationOffset) {
// Only count travel beyond the activation band. This stops a single flick
// from the top hiding the chrome the instant it clears the header height.
const travelPastActivation = Math.min(nextDirectionTravel, offset - hideActivationOffset);
hidden = travelPastActivation >= hideIntentDistance;
} else if (currentlyHidden && nextDirection === "up" && nextDirectionTravel >= revealIntentDistance) {
hidden = false;
}
return { hidden: false, lastOffset: offset };

return {
hidden,
lastOffset: offset,
direction: nextDirection,
directionTravel: nextDirectionTravel,
};
}

function subscribeToPhoneMedia(onChange: () => void) {
Expand Down Expand Up @@ -67,6 +99,8 @@ export function useScrollHideReporter(disabled = false, allowAllBreakpoints = fa
const [hidden, setHidden] = useState(false);
const hiddenRef = useRef(false);
const lastOffsetRef = useRef(0);
const directionRef = useRef<ScrollDirection>(null);
const directionTravelRef = useRef(0);
const active = usePhoneScrollHideActive(disabled, allowAllBreakpoints);

const reportScroll = useCallback(
Expand All @@ -79,9 +113,13 @@ export function useScrollHideReporter(disabled = false, allowAllBreakpoints = fa
offset,
lastOffset,
currentlyHidden: hiddenRef.current,
direction: directionRef.current,
directionTravel: directionTravelRef.current,
});
lastOffsetRef.current = update.lastOffset;
hiddenRef.current = update.hidden;
directionRef.current = update.direction;
directionTravelRef.current = update.directionTravel;
setHidden(update.hidden);
},
[active],
Expand All @@ -91,6 +129,8 @@ export function useScrollHideReporter(disabled = false, allowAllBreakpoints = fa
if (active) return undefined;
hiddenRef.current = false;
lastOffsetRef.current = 0;
directionRef.current = null;
directionTravelRef.current = 0;
const frame = window.requestAnimationFrame(() => setHidden(false));
return () => window.cancelAnimationFrame(frame);
}, [active]);
Expand All @@ -104,6 +144,8 @@ export function useScrollHideReporter(disabled = false, allowAllBreakpoints = fa
useEffect(() => {
hiddenRef.current = false;
lastOffsetRef.current = 0;
directionRef.current = null;
directionTravelRef.current = 0;
const frame = window.requestAnimationFrame(() => setHidden(false));
return () => window.cancelAnimationFrame(frame);
}, [allowAllBreakpoints]);
Expand Down
82 changes: 76 additions & 6 deletions tests/use-hide-on-scroll.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,115 @@ describe("computeScrollHideUpdate", () => {
expect(computeScrollHideUpdate({ offset: 0, lastOffset: 0, currentlyHidden: true })).toEqual({
hidden: false,
lastOffset: 0,
direction: null,
directionTravel: 0,
});
expect(computeScrollHideUpdate({ offset: 8, lastOffset: 20, currentlyHidden: true })).toEqual({
hidden: false,
lastOffset: 8,
direction: null,
directionTravel: 0,
});
});

it("hides after scrolling down past the activation offset", () => {
expect(computeScrollHideUpdate({ offset: 80, lastOffset: 10, currentlyHidden: false })).toEqual({
it("waits for deliberate travel beyond the activation offset before hiding", () => {
const beforeThreshold = computeScrollHideUpdate({
offset: 92,
lastOffset: 60,
currentlyHidden: false,
direction: "down",
directionTravel: 60,
});
expect(beforeThreshold).toEqual({
hidden: false,
lastOffset: 92,
direction: "down",
directionTravel: 92,
});

expect(
computeScrollHideUpdate({
offset: 100,
lastOffset: beforeThreshold.lastOffset,
currentlyHidden: beforeThreshold.hidden,
direction: beforeThreshold.direction,
directionTravel: beforeThreshold.directionTravel,
}),
).toEqual({
hidden: true,
lastOffset: 80,
lastOffset: 100,
direction: "down",
directionTravel: 100,
});
});

it("stays visible when scrolling down but still within the activation band", () => {
expect(computeScrollHideUpdate({ offset: 40, lastOffset: 10, currentlyHidden: false })).toEqual({
hidden: false,
lastOffset: 40,
direction: "down",
directionTravel: 30,
});
});

it("reveals again on deliberate scroll up", () => {
expect(computeScrollHideUpdate({ offset: 120, lastOffset: 180, currentlyHidden: true })).toEqual({
it("reveals after a short but deliberate upward travel", () => {
const firstUpwardMove = computeScrollHideUpdate({
offset: 174,
lastOffset: 180,
currentlyHidden: true,
direction: "down",
directionTravel: 80,
});
expect(firstUpwardMove.hidden).toBe(true);

expect(
computeScrollHideUpdate({
offset: 166,
lastOffset: firstUpwardMove.lastOffset,
currentlyHidden: firstUpwardMove.hidden,
direction: firstUpwardMove.direction,
directionTravel: firstUpwardMove.directionTravel,
}),
).toEqual({
hidden: false,
lastOffset: 120,
lastOffset: 166,
direction: "up",
directionTravel: 14,
});
});

it("ignores rubber-band overscroll at the top", () => {
expect(computeScrollHideUpdate({ offset: -12, lastOffset: 4, currentlyHidden: true })).toEqual({
hidden: true,
lastOffset: 4,
direction: null,
directionTravel: 0,
});
});

it("ignores sub-threshold deltas", () => {
expect(computeScrollHideUpdate({ offset: 82, lastOffset: 80, currentlyHidden: false })).toEqual({
hidden: false,
lastOffset: 80,
direction: null,
directionTravel: 0,
});
});

it("resets intent when the user changes direction", () => {
expect(
computeScrollHideUpdate({
offset: 76,
lastOffset: 80,
currentlyHidden: false,
direction: "down",
directionTravel: 80,
}),
).toEqual({
hidden: false,
lastOffset: 76,
direction: "up",
directionTravel: 4,
});
});
});