tweak(stream-header): collapse peek1/peek2 into single peek snap so one drag reveals both action chips at once
This commit is contained in:
parent
0eb2e056c0
commit
bfd72dc1ff
5 changed files with 29 additions and 36 deletions
|
|
@ -161,16 +161,15 @@ export const segmentDot = recipe({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chip row — outer clip-strip. Each row reveals from underneath the
|
// Chip row — outer clip-strip. Both rows reveal together when the
|
||||||
// curtain when the user drags down to the corresponding peek stage.
|
// user drags the curtain down to the `peek` snap.
|
||||||
//
|
//
|
||||||
// The `marginBottom` math is load-bearing for the snap-top
|
// The `marginBottom` math is load-bearing for the snap-top
|
||||||
// calculation: the resting `top` of `peek1`/`peek2` lands the curtain
|
// calculation: the resting `top` of `peek` lands the curtain exactly
|
||||||
// exactly where the next row would have begun, so the breather never
|
// where the next row would have begun, so the breather never "steals"
|
||||||
// "steals" pixels from the next chip's paddingTop. Two different
|
// pixels from the next chip's paddingTop. Two different values:
|
||||||
// values:
|
|
||||||
// - default (chip-to-chip): `CHIP_GAP_PX` — tighter, so the two
|
// - 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,
|
// - `:last-child` (chip-to-curtain): `CURTAIN_BREATHER_PX` — wider,
|
||||||
// so the curtain's rounded top has comfortable air above the
|
// so the curtain's rounded top has comfortable air above the
|
||||||
// chip pill it lands above.
|
// chip pill it lands above.
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ export function StreamHeader({ scrollRef, children, bottomPinned }: StreamHeader
|
||||||
iconSrc={Icons.Search}
|
iconSrc={Icons.Search}
|
||||||
label={t('Search.search')}
|
label={t('Search.search')}
|
||||||
onClick={openSearch}
|
onClick={openSearch}
|
||||||
hidden={curtain.snap === 'closed'}
|
hidden={curtain.snap !== 'peek'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.chipRow}>
|
<div className={css.chipRow}>
|
||||||
|
|
@ -245,7 +245,7 @@ export function StreamHeader({ scrollRef, children, bottomPinned }: StreamHeader
|
||||||
iconSrc={Icons.Plus}
|
iconSrc={Icons.Plus}
|
||||||
label={t('Direct.create_chat')}
|
label={t('Direct.create_chat')}
|
||||||
onClick={openChat}
|
onClick={openChat}
|
||||||
hidden={curtain.snap !== 'peek2'}
|
hidden={curtain.snap !== 'peek'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
//
|
//
|
||||||
// Snap stops (curtain.top, px):
|
// Snap stops (curtain.top, px):
|
||||||
// closed = TABS_ROW_PX
|
// closed = TABS_ROW_PX
|
||||||
// peek1 = TABS_ROW_PX + CHIP_ROW_PX
|
// peek = TABS_ROW_PX + 2·CHIP_ROW_PX + CHIP_GAP_PX
|
||||||
// peek2 = TABS_ROW_PX + CHIP_ROW_PX * 2
|
// + CURTAIN_BREATHER_PX
|
||||||
// form:* = TABS_ROW_PX + formHeight + CURTAIN_BREATHER_PX
|
// form:* = TABS_ROW_PX + formHeight + CURTAIN_BREATHER_PX
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
DIRECTION_DEAD_ZONE_PX,
|
DIRECTION_DEAD_ZONE_PX,
|
||||||
RUBBER_BAND,
|
RUBBER_BAND,
|
||||||
} from './geometry';
|
} from './geometry';
|
||||||
import { CurtainSnap, isFormSnap, isPeekSnap } from './useCurtainState';
|
import { CurtainSnap, isFormSnap } from './useCurtainState';
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
// The scroll viewport that hosts the chat list inside the curtain.
|
// 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
|
// Touch-gesture driver for the curtain. Native-only: on web/PC the
|
||||||
// listeners aren't attached at all.
|
// listeners aren't attached at all.
|
||||||
//
|
//
|
||||||
// Peek path: drag down from `closed`/`peek1`/`peek2` rubber-bands the
|
// Peek path: drag down from `closed` rubber-bands the live delta and
|
||||||
// live delta and on release commits to the nearest stage. Drag UP from
|
// on release past the threshold commits to `peek` (both chips
|
||||||
// peek retreats toward `closed` via the same threshold logic.
|
// 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
|
// Form-close path: drag UP from a form snap tracks the finger 1:1; on
|
||||||
// release past `ACTIVE_CLOSE_THRESHOLD_PX` commits to `closed`.
|
// 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;
|
if (Math.abs(delta) < DIRECTION_DEAD_ZONE_PX) return;
|
||||||
direction = delta > 0 ? 'down' : 'up';
|
direction = delta > 0 ? 'down' : 'up';
|
||||||
|
|
||||||
// Direction guards: nothing higher than closed; nothing lower
|
// Direction guards: nothing higher than `closed`; nothing
|
||||||
// than peek2; form snaps only close (up).
|
// lower than `peek`; form snaps only close (up).
|
||||||
if (currentSnap === 'closed' && direction === 'up') {
|
if (currentSnap === 'closed' && direction === 'up') {
|
||||||
startY = null;
|
startY = null;
|
||||||
direction = null;
|
direction = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentSnap === 'peek2' && direction === 'down') {
|
if (currentSnap === 'peek' && direction === 'down') {
|
||||||
startY = null;
|
startY = null;
|
||||||
direction = null;
|
direction = null;
|
||||||
return;
|
return;
|
||||||
|
|
@ -143,17 +143,15 @@ export function useCurtainGesture({ scrollRef, snap, setLiveDrag, commit }: Args
|
||||||
if (Math.abs(lastDelta) >= ACTIVE_CLOSE_THRESHOLD_PX) {
|
if (Math.abs(lastDelta) >= ACTIVE_CLOSE_THRESHOLD_PX) {
|
||||||
next = 'closed';
|
next = 'closed';
|
||||||
}
|
}
|
||||||
} else if (isPeekSnap(currentSnap) || currentSnap === 'closed') {
|
} else {
|
||||||
const progressStages = lastDelta / CHIP_ROW_PX;
|
// Single-stage peek toggle. Threshold is COMMIT_THRESHOLD of a
|
||||||
let baseStage = 0;
|
// chip-row worth of rubber-banded drag — same touch effort as
|
||||||
if (currentSnap === 'peek1') baseStage = 1;
|
// the old per-stage commit, but now flips the WHOLE peek in
|
||||||
else if (currentSnap === 'peek2') baseStage = 2;
|
// one motion (`closed` ⇄ `peek`).
|
||||||
if (Math.abs(progressStages) >= COMMIT_THRESHOLD) {
|
const progress = lastDelta / CHIP_ROW_PX;
|
||||||
const target = Math.max(0, Math.min(2, Math.round(baseStage + progressStages)));
|
if (Math.abs(progress) >= COMMIT_THRESHOLD) {
|
||||||
let resolved: CurtainSnap = 'closed';
|
if (currentSnap === 'closed' && progress > 0) next = 'peek';
|
||||||
if (target === 1) resolved = 'peek1';
|
else if (currentSnap === 'peek' && progress < 0) next = 'closed';
|
||||||
else if (target === 2) resolved = 'peek2';
|
|
||||||
next = resolved;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ import {
|
||||||
// is no separate «active vs peek» mode flag — the snap encodes both.
|
// is no separate «active vs peek» mode flag — the snap encodes both.
|
||||||
export type CurtainSnap =
|
export type CurtainSnap =
|
||||||
| 'closed' // curtain flush under tabs row; nothing peeking
|
| 'closed' // curtain flush under tabs row; nothing peeking
|
||||||
| 'peek1' // search chip revealed
|
| 'peek' // both action chips (search + new chat) revealed
|
||||||
| 'peek2' // search + new-chat chips revealed
|
|
||||||
| 'form-search' // full search form revealed
|
| 'form-search' // full search form revealed
|
||||||
| 'form-chat'; // full new-chat form revealed
|
| 'form-chat'; // full new-chat form revealed
|
||||||
|
|
||||||
|
|
@ -23,8 +22,7 @@ export const isFormSnap = (
|
||||||
): snap is 'form-search' | 'form-chat' =>
|
): snap is 'form-search' | 'form-chat' =>
|
||||||
snap === 'form-search' || snap === 'form-chat';
|
snap === 'form-search' || snap === 'form-chat';
|
||||||
|
|
||||||
export const isPeekSnap = (snap: CurtainSnap): snap is 'peek1' | 'peek2' =>
|
export const isPeekSnap = (snap: CurtainSnap): snap is 'peek' => snap === 'peek';
|
||||||
snap === 'peek1' || snap === 'peek2';
|
|
||||||
|
|
||||||
// Form kind currently rendered in the header. Stays set during the
|
// Form kind currently rendered in the header. Stays set during the
|
||||||
// curtain's close transition so the form has content to slide behind;
|
// 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) {
|
switch (snap) {
|
||||||
case 'closed':
|
case 'closed':
|
||||||
return TABS_ROW_PX;
|
return TABS_ROW_PX;
|
||||||
case 'peek1':
|
case 'peek':
|
||||||
return TABS_ROW_PX + CHIP_ROW_PX + CURTAIN_BREATHER_PX;
|
|
||||||
case 'peek2':
|
|
||||||
return (
|
return (
|
||||||
TABS_ROW_PX +
|
TABS_ROW_PX +
|
||||||
CHIP_ROW_PX +
|
CHIP_ROW_PX +
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue