diff --git a/src/app/components/stream-header/StreamHeader.css.ts b/src/app/components/stream-header/StreamHeader.css.ts
index cf1b8867..bb2da12d 100644
--- a/src/app/components/stream-header/StreamHeader.css.ts
+++ b/src/app/components/stream-header/StreamHeader.css.ts
@@ -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.
diff --git a/src/app/components/stream-header/StreamHeader.tsx b/src/app/components/stream-header/StreamHeader.tsx
index 2824e665..817a6ca1 100644
--- a/src/app/components/stream-header/StreamHeader.tsx
+++ b/src/app/components/stream-header/StreamHeader.tsx
@@ -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'}
/>
@@ -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'}
/>
>
diff --git a/src/app/components/stream-header/geometry.ts b/src/app/components/stream-header/geometry.ts
index b6f054a0..516fcd17 100644
--- a/src/app/components/stream-header/geometry.ts
+++ b/src/app/components/stream-header/geometry.ts
@@ -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
// ────────────────────────────────────────────────────────────────────
diff --git a/src/app/components/stream-header/useCurtainGesture.ts b/src/app/components/stream-header/useCurtainGesture.ts
index fb899e1f..facf87fb 100644
--- a/src/app/components/stream-header/useCurtainGesture.ts
+++ b/src/app/components/stream-header/useCurtainGesture.ts
@@ -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';
}
}
diff --git a/src/app/components/stream-header/useCurtainState.ts b/src/app/components/stream-header/useCurtainState.ts
index e8c37c46..d1a4398e 100644
--- a/src/app/components/stream-header/useCurtainState.ts
+++ b/src/app/components/stream-header/useCurtainState.ts
@@ -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 +