npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sigx/lynx-sheet

v0.27.0

Published

Unified bottom sheet for sigx-lynx — detent model, drag/snap math, keyboard-aware sheets (engine + standalone component)

Readme

@sigx/lynx-sheet

The unified bottom sheet for SignalX for Lynx — one detent model, one drag/snap engine, usable with or without routes.

Status: landing in stages (tracking issue signalxjs/lynx#774). The detent model, the shared engine/pan, and the standalone <BottomSheet> component ship today; @sigx/lynx-navigation's presentation: 'sheet' rebuilds on the same engine next.

<BottomSheet>

A bottom-anchored panel that snaps between detents, follows the finger, rides above the keyboard, and (optionally) dims what's behind it and drag-dismisses — no route required, place it in your own layout:

import { BottomSheet } from '@sigx/lynx-sheet';

<BottomSheet
    detents={[120, { fraction: 0.45 }, { fraction: 0.9 }]}
    open={open}
    animate                 // default false: JUMP, so external motion (keyboard) does the reveal
    dismissible             // drag/fling below half the floor → parks at 0 + `dismiss` event
    backdrop                // dim tracks the reveal; tap dismisses; inert while parked
    dragMode="surface"      // whole panel drags, arbitrating with an inner gestures <ScrollView>
    topOffset={insets.top + HEADER_H}
    onSnap={(i) => {}}
    onDismiss={() => { open = false; }}   // the sheet only PARKS; the consumer closes it
    slots={{ handle: () => <Grabber />, default: () => <Body /> }}
/>
  • Persistent mode (default): the floor detent is a hard floor — a composer accessory. open toggles floor ↔ openDetentIndex; pass liftSV (useKeyboardLiftSV()) so the sheet rides above the keyboard, and openToLift to open at the exact live keyboard height (captured on the main thread — the WhatsApp dip-free swap).
  • Dismissible mode: add dismissible (+ backdrop) for the modal tray; closing (open: false) parks it hidden at reveal 0.
  • Android touch guard: a plain backdrop blocks Lynx handlers beneath it, but on Android the raw platform touch still reaches native views — an EditText under the dim grabs focus + keyboard (#787). Fix it by rendering the dim as @sigx/lynx-gestures' native touch-guard element: backdrop={{ guardTag: TOUCH_GUARD_TAG }} (import { TOUCH_GUARD_TAG } from '@sigx/lynx-gestures'). The tag arrives as a string so @sigx/lynx-sheet itself stays pure JS; the guard element requires sigx prebuild.
  • Drag modes (mount-constant): 'handle' (pan on the handle slot only — default, safe with raw <list> bodies), 'surface' (full-surface drag with the 8-step scroll arbitration; this component provides the ScrollDragHost an inner @sigx/lynx <ScrollView> adopts), 'grabber', 'none'. The always-drags chrome strip height is grabberPx (default 28) — size it to a whole input row for WhatsApp-style sheets.
  • Stacking: Lynx has no z-index/portal — render the sheet as the LAST child of a full-surface positioned container so the backdrop dims the whole screen.
  • Safe area: detent resolution reads useSafeAreaInsets()/useKeyboardLift() — mount a @sigx/lynx-safe-area <SafeAreaProvider> above the sheet, or { keyboard: true } detents and inset corrections degrade to zero insets (with a dev warning).
  • Rotation: geometry follows it. screenH comes from useScreen() (#856), so fraction detents and the bottom-edge anchor re-resolve on a rotation or window resize instead of staying pinned to the launch orientation (#791).

Detent model

A sheet declares its resting heights as DetentSpec[] and resolves them against a DetentEnv to plain ascending px values:

import { resolveDetents } from '@sigx/lynx-sheet';

const detents = resolveDetents(
    [
        64,                                    // px: the collapsed floor (input row)
        { keyboard: true, fallbackPx: 320 },   // floor riding on the remembered keyboard height
        { fraction: 0.92 },                    // share of screen height
    ],
    {
        screenH: 800,   // `<BottomSheet>` passes the LIVE height (useScreen())
        topOffset: 80,       // top inset + header the sheet must never slide under
        bottomInset: 24,     // added back onto keyboard detents (lift values are inset-discounted)
        keyboardPx: 300,     // max observed BG-reactive keyboard lift, 0 if never seen
    },
); // → ascending px, deduped, capped at screenH - topOffset

{ keyboard: true } owns the math apps used to hand-roll: the remembered keyboard height needs the bottom safe-area inset added back (keyboard lift values are inset-discounted while the sheet reaches the true screen bottom), and it must come from a BG-reactive keyboard source — never from reading a main-thread-written SharedValue on the background thread, which stays at its seed value.

The inset is added back only by however much of it the sheet still has to covermax(0, bottomInset - bottomOffset). A sheet reaching the true screen bottom (bottomOffset: 0) covers all of it; one whose ancestor already pads the gesture bar (bottomOffset: insets.bottom) covers none, and adding it back there would open the sheet a gesture bar taller than the keyboard it replaces — visible as the composer's input row jumping on every keyboard↔panel swap, because that inflated detent is also openToLift's floor and clamps away the live main-thread capture (#811).

Pinning content to the visible bottom edge

The panel is laid out as tall as the top detent and slid down, so its own bottom edge is off-screen at every rest below the top detentposition: absolute; bottom: 0 pins to a place nobody can see. Pass pinnedBottomRef and the sheet binds that element to the inverse of the slide, so it sits last in flow yet paints flush with the bottom of the revealed slice, on the main thread, every frame of a drag or keyboard lift:

const tabsRef = useMainThreadRef<MainThread.Element | null>(null);

<BottomSheet pinnedBottomRef={tabsRef} …>
    {/* body stays FULL panel height so a drag never opens a gap under it */}
    <EmojiPicker tabPlacement="bottom" tabBarRef={tabsRef} … />
</BottomSheet>

Keeping the body at full panel height is what avoids a gap mid-drag — but it also means the body extends below the fold, so scrollable content needs to know how much is hidden. onRest reports the sheet's settled visible height in px (mount, open toggle, drag settle, dismiss) for exactly that:

<BottomSheet onRest={(px) => { restH.value = px; }} … />
// … then e.g. <EmojiPicker gridBottomInset={panelH - restH.value} />

onSnap says which detent; onRest says how tall.

For the live height — tracking the finger frame-for-frame, not just settles — onReveal hands out a SharedValue<number> of the effective visible height (max(dragged reveal, floor + keyboard lift)). It may be called on re-renders; the SharedValue's identity is stable, so capturing it is idempotent. Its flagship consumer is a chat thread behind the sheet: bind it straight into @sigx/lynx-list's bottomInset and the newest messages ride the sheet frame-synced through keyboard rises and detent drags (#844):

const occluderSV = signal<{ sv: SharedValue<number> | null }>({ sv: null });

<List inverted bottomInset={occluderSV.sv ?? floorH} … />
<BottomSheet onReveal={(sv) => { occluderSV.sv = sv; }} … />

Release math and drag arbitration

Worklet-safe pure functions in reveal-px space (reveal = visible sheet height, 0 = hidden):

  • projectReveal(revealPx, velocityY) — where a release lands if the finger's velocity carries it for PROJECTION_SEC. Position projection instead of raw velocity thresholds: a genuine fling projects past the dismiss line from anywhere; a controlled fast drag projects near a detent and settles there.
  • shouldDismiss(revealPx, velocityY, floorPx) — projected landing under half the floor detent dismisses.
  • nearestDetentIndex(revealPx, velocityY, candidatesPx) — the settle target on a non-dismiss release.
  • revealDurationSec(heightFraction, fullSlideDurationSec) — transition duration velocity-matched to the card/modal slide.
  • decideDragOwner(input) — the full 8-step UNDECIDED → SHEET | CONTENT arbitration for full-surface drags over an inner scrollable (grabber chrome zone, web horizontal-axis gate, rest-lock, at-max scroll cooperation, and the one-way content→sheet mid-gesture handoff).

License

MIT