mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Keep the Patrol decision header pinned while a long decision scrolls (#2445)
On phones the Patrol attention detail header holds Back, Previous and Next issue. It was authored `sticky top-0`, but the inbox `<section>` used `overflow-hidden`, which made that section the sticky scroll container. Below `lg` the header moved exactly with the section, so on long decisions the queue controls scrolled away. - Inbox section: `overflow-hidden` → `overflow-clip`. The rounded corners are still clipped, but the section no longer creates a scroll container, so the header pins against `.app-scroll-shell`. - Header: adds the opaque `bg-surface/95` it was first authored with. PR #2443 removed it because Tailwind 3 never rendered it. Now that the header sticks, content would otherwise show through. Verified on a mock stack in light and dark: - **390px:** the header sits at 484/284/84/4/4px while the shell scrolls 0/200/400/600/max. Before the fix it followed the section off screen. It stops 4px down because of the shell's `py-1` padding. - **1440px:** with Evidence and history expanded, the header stays opaque at the top of the detail panel's own scroller. - **1024px:** checked as well. The receipt is `frontend-modern/browser-verification/56c38699b5a0cd36.json`. A regression test in `PatrolAttentionWorkbench.test.tsx` pins the class contract.
This commit is contained in:
@@ -11084,3 +11084,11 @@ the renamed Settings > Pulse Mobile section instead of Remote Access. Codes,
|
||||
severities and the response shape are unchanged. The API reference heading
|
||||
for the relay protocol records that Pulse Mobile retires on 31 March 2027, and
|
||||
the published copy stays identical to `docs/API.md`.
|
||||
|
||||
### Patrol attention detail header pinning
|
||||
|
||||
Pinning the Patrol attention detail header on phones
|
||||
(`frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx`) is a
|
||||
layout-only change. It reads no new attention fields, sends no new request,
|
||||
and leaves the attention projection, lifecycle, and queue ordering contracts
|
||||
above unchanged.
|
||||
|
||||
@@ -3433,6 +3433,15 @@ Patrol is off or its model needs setup, while retaining the existing setup task
|
||||
above it. Desktop and narrow browser checks must show the task and attention
|
||||
list together, then allow a selected decision to open and return without
|
||||
changing the disabled Patrol controls or overflowing the viewport.
|
||||
The selected-detail header (title, queue position, Back, and the phone
|
||||
Previous and Next issue controls) is `sticky top-0` with an opaque
|
||||
`bg-surface/95` background, and the `Patrol decision inbox` section clips its
|
||||
rounded corners with `overflow-clip`, never `overflow-hidden`. An
|
||||
`overflow-hidden` ancestor becomes the sticky scroll container, so on phones
|
||||
the header scrolled away with the page and took the queue controls with it.
|
||||
Below `lg` the header pins against `.app-scroll-shell`; at `lg` it pins inside
|
||||
the detail panel's own scroller. The class contract is pinned in
|
||||
`frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx`.
|
||||
The objective brief and optional-context fields in `PatrolObjectivesPanel`
|
||||
share the same `FormTextarea` ownership contract.
|
||||
|
||||
|
||||
@@ -1260,6 +1260,12 @@ proof, including failed verification and reduced motion, remains
|
||||
The durable decision and proof record is
|
||||
`internal/records/operational-trust-governed-docker-restart-2026-07-19.md`.
|
||||
|
||||
Reviewing a long decision on a phone must not lose the queue controls: the
|
||||
selected-detail header with Back, Previous, and Next issue stays pinned while
|
||||
the detail scrolls. The shell-level rule (opaque sticky header, `overflow-clip`
|
||||
inbox section) is owned by the frontend-primitives attention workbench shell
|
||||
contract.
|
||||
|
||||
## Current State
|
||||
|
||||
The Patrol 2.0 frontend now uses a one-task-at-a-time, decision-first
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "beb6c3f6a6f3fad74a1b9f76745bacdadcab7834",
|
||||
"verified_at": "2026-10-03T18:34:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "5d7c824c96e90ee8c4b8a8a92ac34e42ecd3f053925512de6fa0408f6bd0370e"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"mock stack, decision inbox with 50 decisions, light and dark color schemes",
|
||||
"390px touch: selected decision detail with Back to list, Previous and Next issue in the header; header top 484/284/84/4/4 while .app-scroll-shell scrolls 0/200/400/600/max, staying topmost at the shell padding edge",
|
||||
"390px: header background resolves to surface at 95% opacity in light and dark, scrolled content no longer shows through",
|
||||
"1440px: Evidence and history expanded so the detail panel overflows (1165/832); header stays at the panel top while the panel scrolls 0/300/333, opaque in both themes",
|
||||
"inbox section computed overflow clip with 12px corner radius, rounded corners still clipped at 1440"
|
||||
],
|
||||
"interactions": [
|
||||
"clicked Start review to open the highest-priority decision",
|
||||
"scrolled .app-scroll-shell at 390px and checked the header stays the topmost element at its position",
|
||||
"expanded Evidence and history and scrolled the lg detail panel at 1440px and 1024px",
|
||||
"stepped through all 50 decisions with Next decision at 1440px"
|
||||
]
|
||||
}
|
||||
@@ -384,7 +384,7 @@ export function PatrolAttentionWorkbench(
|
||||
return (
|
||||
<section
|
||||
aria-label="Patrol decision inbox"
|
||||
class="overflow-hidden rounded-xl border border-border bg-surface shadow-xs"
|
||||
class="overflow-clip rounded-xl border border-border bg-surface shadow-xs"
|
||||
>
|
||||
<div class="border-b border-border px-4 py-4 sm:px-6">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -877,7 +877,7 @@ function AttentionDetail(props: {
|
||||
aria-labelledby="attention-detail-title"
|
||||
aria-busy={props.loading}
|
||||
>
|
||||
<div class="sticky top-0 z-20 border-b border-border px-4 py-3 backdrop-blur-sm sm:px-5">
|
||||
<div class="sticky top-0 z-20 border-b border-border bg-surface/95 px-4 py-3 backdrop-blur-sm sm:px-5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -479,6 +479,33 @@ describe('PatrolAttentionWorkbench', () => {
|
||||
expect(window.location.search).toBe('?attention=record-1');
|
||||
});
|
||||
|
||||
it('keeps the detail header and queue controls pinned while a long decision scrolls', async () => {
|
||||
const active = item();
|
||||
apiMocks.getList.mockResolvedValue(
|
||||
listResponse([active], summary({ activeCount: 1, openCount: 1, calm: false })),
|
||||
);
|
||||
apiMocks.getDetail.mockResolvedValue(detail(active));
|
||||
renderWorkbench();
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: 'Open Database VM · Disk pressure' }),
|
||||
);
|
||||
const detailRegion = await screen.findByRole('complementary', {
|
||||
name: 'Database VM · Disk pressure',
|
||||
});
|
||||
const header = within(detailRegion)
|
||||
.getByRole('button', { name: 'Back to attention list' })
|
||||
.closest('div.sticky');
|
||||
expect(header).toHaveClass('top-0', 'bg-surface/95');
|
||||
|
||||
// overflow-hidden would make the inbox section the sticky scroll container,
|
||||
// so on phones the header scrolled away with the page; overflow-clip keeps the
|
||||
// rounded-corner clipping without creating a scroll container.
|
||||
const inbox = screen.getByRole('region', { name: 'Patrol decision inbox' });
|
||||
expect(inbox).toHaveClass('overflow-clip', 'rounded-xl');
|
||||
expect(inbox).not.toHaveClass('overflow-hidden');
|
||||
});
|
||||
|
||||
it('uses plain scan-row language while keeping unknown trust metadata in detail', async () => {
|
||||
const unknownTrust = item({
|
||||
state: 'unknown',
|
||||
|
||||
Reference in New Issue
Block a user