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

react-native-duo-layout

v0.2.2

Published

Fold-aware layout hooks and a tappable status-strip toolbar for the iPhone Duo (and other foldables) in React Native.

Readme

react-native-duo-layout

Fold-aware layout hooks and a tappable status-strip toolbar for the iPhone Duo (and other foldables) in React Native. Written in TypeScript, plus one small iOS module for the hinge.

  • useScreenLayout(): compact vs expanded layout from the window size (re-renders on fold/unfold/rotate).
  • useStatusStrip(): where the Duo's status bar strip is: 'right', 'top' or null.
  • useHinge(): the hinge's position and angle, e.g. to detect when the Duo is half-folded.
  • StatusStripProvider + StatusStripSafeAreaView + StatusStripPortal: put Back buttons and tabs inside the strip from any screen and have them receive taps.
  • Every layout value (expanded width, strip offsets) has a sensible default you can change.

Install

npm install react-native-duo-layout react-native-safe-area-context

Peer dependencies: react, react-native, react-native-safe-area-context >= 4.

useHinge() uses native code, so run pod install in ios/ after installing and rebuild the app.

Usage

import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
  StatusStripProvider,
  StatusStripSafeAreaView,
  StatusStripPortal,
  useScreenLayout,
  useStatusStrip,
} from 'react-native-duo-layout';

export default function App() {
  return (
    <SafeAreaProvider>
      <StatusStripProvider>
        {/* Replaces SafeAreaView at the root */}
        <StatusStripSafeAreaView style={{ backgroundColor: 'white' }}>
          <Home />
        </StatusStripSafeAreaView>
      </StatusStripProvider>
    </SafeAreaProvider>
  );
}

function Home() {
  const { isExpanded } = useScreenLayout();
  const strip = useStatusStrip(); // 'right' | 'top' | null

  return (
    <>
      {isExpanded ? <TwoPanes /> : <OnePane />}

      {/* Shown in the strip when there is one... */}
      <StatusStripPortal>
        <MyTabs direction={strip === 'right' ? 'column' : 'row'} />
      </StatusStripPortal>

      {/* ...and somewhere else when there isn't. */}
      {!strip && <BottomTabBar />}
    </>
  );
}

With react-navigation

Screens in a stack stay mounted when another screen is pushed on top. Pass the focus state so only the visible screen fills the strip:

import { useIsFocused } from '@react-navigation/native';

function ChatScreen({ navigation }) {
  const isFocused = useIsFocused();

  return (
    <>
      <StatusStripPortal enabled={isFocused}>
        <BackButton onPress={navigation.goBack} />
      </StatusStripPortal>
      <Messages />
    </>
  );
}

Put StatusStripSafeAreaView outside the NavigationContainer's screens (e.g. around the whole container), so it covers the full window.

API

useScreenLayout(options?)

Returns { width, height, isExpanded, orientation }. isExpanded is true when the window is at least expandedMinWidth wide (default 600pt):

| Device | Size (pt) | isExpanded | |---|---|---| | iPhone Duo cover screen (folded) | 466 × 678 | false | | iPhone Duo inner screen (unfolded) | 669 × 951 | true | | iPhone 17 portrait / landscape | 402 × 874 / 874 × 402 | false / true |

options.expandedMinWidth overrides the provider's value for one call.

useStatusStrip()

Reads the safe-area insets:

  • 'right': only the right edge is inset (Duo folded, or unfolded landscape).
  • 'top': top inset on an expanded, non-iPad screen (Duo unfolded portrait).
  • null: no usable strip (normal phones, iPads).

useHinge()

Returns { status, angle } and re-renders when the hinge moves:

| status | Meaning | |---|---| | 'closed' | Folded shut. | | 'partiallyOpen' | Half-folded, e.g. standing like a book. | | 'fullyOpen' | Opened flat. | | 'unknown' | No reading yet, or not supported (see below). | | 'unavailable' | The app moved off the screen that reports the hinge. |

angle is in degrees (180 when flat), or null with no reading. How often it updates is up to the system, so prefer status when you only need the position.

const { status } = useHinge();
const { width, orientation } = useScreenLayout();

// Half-folded in landscape, end the list at the hinge.
const listWidth =
  status === 'partiallyOpen' && orientation === 'landscape' ? width / 2 : width / 3;

It reads UIHingeInteraction, which needs iOS 27.1 or later. On older iOS, on devices without a hinge and on Android the status stays 'unknown'.

Tip: give a pane an explicit width that changes, rather than switching between width and flex styles when the hinge moves; switching style keys on a mounted view can leave the old size behind.

<StatusStripProvider expandedMinWidth? iconsOffset? topStripLeftGap?>

Wrap the app once, inside SafeAreaProvider. All props are optional:

| Prop | Default | What it controls | |---|---|---| | expandedMinWidth | 600 (DEFAULT_EXPANDED_MIN_WIDTH) | Minimum window width (pt) for the expanded layout. | | iconsOffset | 160 (DEFAULT_STATUS_ICONS_OFFSET) | How far down the right strip its content starts, below the camera, time and wifi. | | topStripLeftGap | 12 (DEFAULT_TOP_STRIP_LEFT_GAP) | Gap from the left edge to the content when the strip runs across the top. |

<StatusStripProvider expandedMinWidth={700} iconsOffset={140} topStripLeftGap={20}>

useScreenLayout() and useStatusStrip() also work without a provider; they then use the defaults. StatusStripPortal needs a provider (it warns in development without one).

<StatusStripSafeAreaView {...ViewProps}>

Use it as the root view. It fills the window, pads its children into the safe area, and renders the current strip content in the padding: as a column under the wifi icon ('right'), or as a row beside the clock ('top'). With no strip it renders nothing extra.

<StatusStripPortal enabled?>

Use this to render something on the status strip, from any screen:

<StatusStripPortal>
  <BackButton onPress={goBack} />
</StatusStripPortal>

Set enabled={false} to hide it.

Constants

import {
  DEFAULT_EXPANDED_MIN_WIDTH,  // 600
  DEFAULT_STATUS_ICONS_OFFSET, // 160
  DEFAULT_TOP_STRIP_LEFT_GAP,  // 12
} from 'react-native-duo-layout';

Use the defaults to adjust relative to them, e.g. iconsOffset={DEFAULT_STATUS_ICONS_OFFSET + 20}.

License

MIT