The peek is the record page, at a size the reader picks - #35
The peek is the record page, at a size the reader picks#35nguyenngothuong wants to merge 2 commits into
Conversation
Opening a row gave a panel that listed the model's fields, drew the stages as tags and offered its own Edit. The record page drew the same record from the same model with a different header, a highlight strip, a stage path, four tabs and a different Edit. Two renderings of one thing drift, and this pair had already started: the peek showed the *row* the list had fetched, so a column the list does not select was missing from it with nothing saying so. `RecordDetail` is that rendering, once. `RecordScreen` is now `<Page>` around it, and the list's peek is a `Drawer` around it. A field added to the layout appears in both because there is only one place to add it to. WHAT THE READER GETS. Three sizes — the 452px strip it always was, a 920px column, and the whole window — on two buttons in the drawer header, and the choice is remembered per browser. `wide` is the size that makes a peek an answer rather than a preview: four sections of two-column fields do not fit in a strip, and in one they wrap to a line each and the reader scrolls a page that would have fitted on a screen. Escape steps down through the sizes before it closes, because a reader who went full screen and pressed it meant "give me the list back", and closing instead is not undoable — the peek does not remember which record it held. At the narrowest size the highlight strip and the stage path are dropped. Both are horizontal by nature and neither survives a 452px column: three highlights become four wrapped lines, which reads as damage rather than density. DRAWERS NOW PORTAL TO THE DOCUMENT. That became necessary rather than tidy: the peek draws `RecordDetail`, which has an Edit that opens a drawer of its own, and rendered in place the edit panel was laid out inside the peek and clipped by it. `.shell` is the only positioned ancestor and it fills the window, so `fixed` covers the rectangle `absolute` did — no pixel moves for the nine drawers that were already fine. Tests: `drawerSize` is a state machine and a stored preference, so the ends and the failure are what is pinned — widen and narrow saturate rather than wrap, an unknown stored value falls back, and a `localStorage` that throws does not take the record page with it. Safari in private mode throws on `setItem`, and a peek is not worth a blank screen. Verified against the running sample with Playwright: 452 → 920 → 1440 on a 1440 viewport, Widen disabled at the end, three Escapes to close, the size surviving a reload, and the nested edit drawer landing at the window edge rather than inside the panel. `npm test` is 140 passed with the one failure that is pre-existing on this commit's parent — `liveRecords.test.ts` asserts `28 Aug 2026` for a `T23:18:06Z` timestamp, which is 29 August anywhere east of UTC (votrongdao#30 covers the application-side half of that). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: nguyenngothuong <nguyenthuongtb12@gmail.com>
`EDITABLE` names the four kinds a custom field *may* be declared on, and the Edit button was offered whenever the record was one of them. Whether the tenant has declared anything is a different question, and nothing asked it. So on the seeded tenant — which declares one field, on accounts — Edit is live on every contact, lead and opportunity, and opens a drawer reading "Nothing has been declared on contacts" above a Save button offering "0 change(s)". A reader who presses Edit and is shown an empty form does not conclude that their tenant has declared no fields. They conclude the editor is broken, and the sentence explaining otherwise arrives after the click that cost them the trust. This repository already makes that argument about the Clone button two lines below — disabled, with the reason on it, rather than live and inert. The count comes from the schema the edit drawer itself reads, so the button and the panel behind it cannot disagree about whether there is anything to edit. Two reasons rather than one, because they are two different facts and only one is fixable by the person reading it: the kind takes no declared fields at all, or this tenant has not declared any yet — and the second says where to go and that it takes no deployment, which is the claim the sample exists to make. Found by opening Edit on a contact in the peek this branch adds, which is how the promise came to be made in two places instead of one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: nguyenngothuong <nguyenthuongtb12@gmail.com>
|
Added a second commit, found while testing the first.
A reader who presses Edit and gets an empty form does not conclude that their tenant has declared no fields — they conclude the editor is broken, and the sentence explaining otherwise arrives after the click that already cost the trust. It is the argument this file already makes about The count comes from the same schema the edit drawer reads, so the button and the panel behind it cannot disagree about whether there is anything to edit. Two reasons rather than one, because "this kind takes no declared fields" and "this tenant has declared none yet" are different facts and only the second is fixable by the reader — so that one names Setup and says it takes no deployment, which is the claim the sample exists to make. Verified: Edit disabled with the reason on a contact, live on an account. Happy to split this into its own PR if you would rather keep the two apart — it is only here because the peek is what made the promise appear in two places, and reviewing the peek means meeting it. |
Opening a row from a list gave a panel that listed the model’s fields, drew the stages as tags and offered its own Edit. The record page drew the same record from the same model with a different header, a highlight strip, a stage path, four tabs and a different Edit.
Two renderings of one thing drift, and this pair had already started: the peek showed the
rowthe list had fetched, not the record — so a column the list does not select was simply missing from it, with nothing saying so.What changed
RecordDetailis that rendering, once.RecordScreenis now<Page>around it; the list’s peek is aDraweraround it. A field added to the layout appears in both because there is one place to add it to.Three sizes on two buttons in the drawer header — the 452px strip it always was, a 920px column, and the whole window — remembered per browser.
wideis the size that makes a peek an answer rather than a preview: four sections of two-column fields wrap to a line each in a strip, and the reader scrolls a page that would have fitted on a screen.Escape steps down through the sizes before it closes. A reader who went full screen and pressed it meant “give me the list back”, and closing instead is not undoable — the peek does not remember which record it held.
At the narrowest size the highlight strip and the stage path are dropped. Both are horizontal by nature and neither survives a 452px column.
The one thing worth your eye
Drawers now portal to the document. That became necessary rather than tidy: the peek draws
RecordDetail, which has an Edit that opens a drawer of its own, and rendered in place the edit panel was laid out inside the peek and clipped by it — a control that reads as broken rather than nested..shellis the only positioned ancestor and it fills the window, sofixedcovers the rectangleabsolutedid. I believe this moves no pixel for the nine drawers that were already fine, and I checked the ones I could reach — but it is a change to a primitive every screen uses, so it is the part I would review first.If you would rather the primitive not grow a
sizeprop at all, the alternative I considered was a second component besideDrawer; I did not take it because two things that open over the page are two things to keep accessible, andDraweralready has the Escape and focus handling.Verified
Driven with Playwright against
docker compose up:drawerSizehas unit tests for the ends and the failure — widen/narrow saturate rather than wrap, an unknown stored value falls back, and alocalStoragethat throws does not take the record page with it (Safari in private mode throws onsetItem).npm testis 140 passed, with the one failure pre-existing ondev:liveRecords.test.tsasserts28 Aug 2026for aT23:18:06Ztimestamp, which is 29 August east of UTC. #30 covers the application-side half of that root cause.npm run lintcould not be run — noeslint.config.js(#33).Signed off under the DCO.