tweak(stream-header): collapse peek1/peek2 into single peek snap so one drag reveals both action chips at once

This commit is contained in:
heaven 2026-05-15 23:47:40 +03:00
parent 0eb2e056c0
commit bfd72dc1ff
5 changed files with 29 additions and 36 deletions

View file

@ -161,16 +161,15 @@ export const segmentDot = recipe({
},
});
// Chip row — outer clip-strip. Each row reveals from underneath the
// curtain when the user drags down to the corresponding peek stage.
// Chip row — outer clip-strip. Both rows reveal together when the
// user drags the curtain down to the `peek` snap.
//
// The `marginBottom` math is load-bearing for the snap-top
// calculation: the resting `top` of `peek1`/`peek2` lands the curtain
// exactly where the next row would have begun, so the breather never
// "steals" pixels from the next chip's paddingTop. Two different
// values:
// calculation: the resting `top` of `peek` lands the curtain exactly
// where the next row would have begun, so the breather never "steals"
// pixels from the next chip's paddingTop. Two different values:
// - default (chip-to-chip): `CHIP_GAP_PX` — tighter, so the two
// pills read as a related pair when both are revealed at peek2.
// pills read as a related pair when both are revealed.
// - `:last-child` (chip-to-curtain): `CURTAIN_BREATHER_PX` — wider,
// so the curtain's rounded top has comfortable air above the
// chip pill it lands above.

View file

@ -237,7 +237,7 @@ export function StreamHeader({ scrollRef, children, bottomPinned }: StreamHeader
iconSrc={Icons.Search}
label={t('Search.search')}
onClick={openSearch}
hidden={curtain.snap === 'closed'}
hidden={curtain.snap !== 'peek'}
/>
</div>
<div className={css.chipRow}>
@ -245,7 +245,7 @@ export function StreamHeader({ scrollRef, children, bottomPinned }: StreamHeader
iconSrc={Icons.Plus}
label={t('Direct.create_chat')}
onClick={openChat}
hidden={curtain.snap !== 'peek2'}
hidden={curtain.snap !== 'peek'}
/>
</div>
</>

View file

@ -10,8 +10,8 @@
//
// Snap stops (curtain.top, px):
// closed = TABS_ROW_PX
// peek1 = TABS_ROW_PX + CHIP_ROW_PX
// peek2 = TABS_ROW_PX + CHIP_ROW_PX * 2
// peek = TABS_ROW_PX + 2·CHIP_ROW_PX + CHIP_GAP_PX
// + CURTAIN_BREATHER_PX
// form:* = TABS_ROW_PX + formHeight + CURTAIN_BREATHER_PX
// ────────────────────────────────────────────────────────────────────

View file

@ -7,7 +7,7 @@ import {
DIRECTION_DEAD_ZONE_PX,
RUBBER_BAND,
} from './geometry';
import { CurtainSnap, isFormSnap, isPeekSnap } from './useCurtainState';
import { CurtainSnap, isFormSnap } from './useCurtainState';
type Args = {
// The scroll viewport that hosts the chat list inside the curtain.
@ -29,9 +29,9 @@ type Args = {
// Touch-gesture driver for the curtain. Native-only: on web/PC the
// listeners aren't attached at all.
//
// Peek path: drag down from `closed`/`peek1`/`peek2` rubber-bands the
// live delta and on release commits to the nearest stage. Drag UP from
// peek retreats toward `closed` via the same threshold logic.
// Peek path: drag down from `closed` rubber-bands the live delta and
// on release past the threshold commits to `peek` (both chips
// revealed in one motion). Drag UP from `peek` retreats to `closed`.
//
// Form-close path: drag UP from a form snap tracks the finger 1:1; on
// release past `ACTIVE_CLOSE_THRESHOLD_PX` commits to `closed`.
@ -92,14 +92,14 @@ export function useCurtainGesture({ scrollRef, snap, setLiveDrag, commit }: Args
if (Math.abs(delta) < DIRECTION_DEAD_ZONE_PX) return;
direction = delta > 0 ? 'down' : 'up';
// Direction guards: nothing higher than closed; nothing lower
// than peek2; form snaps only close (up).
// Direction guards: nothing higher than `closed`; nothing
// lower than `peek`; form snaps only close (up).
if (currentSnap === 'closed' && direction === 'up') {
startY = null;
direction = null;
return;
}
if (currentSnap === 'peek2' && direction === 'down') {
if (currentSnap === 'peek' && direction === 'down') {
startY = null;
direction = null;
return;
@ -143,17 +143,15 @@ export function useCurtainGesture({ scrollRef, snap, setLiveDrag, commit }: Args
if (Math.abs(lastDelta) >= ACTIVE_CLOSE_THRESHOLD_PX) {
next = 'closed';
}
} else if (isPeekSnap(currentSnap) || currentSnap === 'closed') {
const progressStages = lastDelta / CHIP_ROW_PX;
let baseStage = 0;
if (currentSnap === 'peek1') baseStage = 1;
else if (currentSnap === 'peek2') baseStage = 2;
if (Math.abs(progressStages) >= COMMIT_THRESHOLD) {
const target = Math.max(0, Math.min(2, Math.round(baseStage + progressStages)));
let resolved: CurtainSnap = 'closed';
if (target === 1) resolved = 'peek1';
else if (target === 2) resolved = 'peek2';
next = resolved;
} else {
// Single-stage peek toggle. Threshold is COMMIT_THRESHOLD of a
// chip-row worth of rubber-banded drag — same touch effort as
// the old per-stage commit, but now flips the WHOLE peek in
// one motion (`closed` ⇄ `peek`).
const progress = lastDelta / CHIP_ROW_PX;
if (Math.abs(progress) >= COMMIT_THRESHOLD) {
if (currentSnap === 'closed' && progress > 0) next = 'peek';
else if (currentSnap === 'peek' && progress < 0) next = 'closed';
}
}

View file

@ -13,8 +13,7 @@ import {
// is no separate «active vs peek» mode flag — the snap encodes both.
export type CurtainSnap =
| 'closed' // curtain flush under tabs row; nothing peeking
| 'peek1' // search chip revealed
| 'peek2' // search + new-chat chips revealed
| 'peek' // both action chips (search + new chat) revealed
| 'form-search' // full search form revealed
| 'form-chat'; // full new-chat form revealed
@ -23,8 +22,7 @@ export const isFormSnap = (
): snap is 'form-search' | 'form-chat' =>
snap === 'form-search' || snap === 'form-chat';
export const isPeekSnap = (snap: CurtainSnap): snap is 'peek1' | 'peek2' =>
snap === 'peek1' || snap === 'peek2';
export const isPeekSnap = (snap: CurtainSnap): snap is 'peek' => snap === 'peek';
// Form kind currently rendered in the header. Stays set during the
// curtain's close transition so the form has content to slide behind;
@ -81,9 +79,7 @@ export function snapTopPx(snap: CurtainSnap, formH: number | null): number {
switch (snap) {
case 'closed':
return TABS_ROW_PX;
case 'peek1':
return TABS_ROW_PX + CHIP_ROW_PX + CURTAIN_BREATHER_PX;
case 'peek2':
case 'peek':
return (
TABS_ROW_PX +
CHIP_ROW_PX +