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

@devitup/capacitor-duo-plugin

v0.1.2

Published

Capability-based iPhone Duo hinge and reserved-region information for Capacitor

Readme

@devitup/capacitor-duo-plugin

A small Capacitor 8 plugin exposing UIKit hinge and reserved-region facts to web applications. Application layout stays in CSS. The package does not inspect device names, screen dimensions, or user agents to identify hardware.

Requirements and installation

  • Capacitor 8 (@capacitor/core peer dependency).
  • iOS deployment target 15 or later; building the iOS source requires Xcode 27.1 / iOS 27.1 SDK or newer while these APIs remain beta. At runtime, iOS below 27.1 returns neutral state.
  • Android v0.1 returns neutral state, including on foldables. Web also returns neutral state.

The contract describes capabilities and geometry rather than Apple model names, so a later Android foldable implementation can use the same consumer API.

npm install @devitup/capacitor-duo-plugin
npx cap sync

The package contains Swift Package Manager and CocoaPods metadata for iOS, plus a standard Android Capacitor library. Capacitor 8 generates a dependency on the Swift product DevitupCapacitorDuoPlugin, which this package exports. No application configuration or permissions are required.

API

import { Duo } from '@devitup/capacitor-duo-plugin';

const state = await Duo.getState();
if (state.isDuo) {
  // Choose an app-specific enhancement for the current Duo-capable hierarchy.
}

const listener = await Duo.addListener('layoutChanged', state => {
  // Read current native facts; preserve your existing app/component state.
});
await listener.remove();

DuoLayoutState has supported, isDuo, hasHinge, hinge, divisionRegions, occlusionRegions, hasDivisionRegion, divisionActive, divisionAxis, verticalBarEdge, and sequence.

  • supported means the current iOS runtime and view hierarchy can query the iOS 27.1 APIs. On ordinary iPhones running 27.1 this may be true while isDuo is false.
  • isDuo means a hinge callback supplies a hinge or a .division reserved region is visible to the WebView probe, including an inactive region. It describes the current layout hierarchy, not a permanent hardware identity. If a hierarchy withholds both signals, isDuo is false even on Duo hardware. The lower-level fields remain authoritative.
  • hinge.status is unknown, closed, partiallyOpen, or fullyOpen. angleRadians is in radians, following UIKit. Apple determines its update frequency and precision.
  • divisionRegions are places where content should divide. occlusionRegions are places obscured by hardware, such as a camera. Both include inactive regions. Each has an opaque session-local id, active, and frame.
  • divisionActive is true if any division region is active. A flat Duo may have isDuo: true, hasDivisionRegion: true, divisionActive: false, and a zero-width division. Do not reserve a visible hinge gap in that state.
  • divisionAxis is inferred from a region's long dimension; it is null for ambiguous geometry. It describes the line's orientation: a tall narrow division is vertical.
  • verticalBarEdge is UIKit's preferred logical edge for a vertical bar: 'leading', 'trailing', or null when UIKit reports unspecified. It is available on iOS 27.1+ and remains null on earlier iOS, Android, and web. UIKit can report a preference even when no system vertical bar is currently visible. This is a native layout fact, not an instruction to move app navigation.
  • Leading and trailing follow the current interface layout direction. In left-to-right layouts they usually correspond to left and right; in right-to-left layouts they reverse. Consumers can position their own controls with CSS logical properties such as inset-inline-start and inset-inline-end.
  • The edge and isDuo are independent. A Duo-capable hierarchy can have verticalBarEdge: null; a system preference outside Duo does not make isDuo true. Safe-area CSS env() values, division regions, and occlusion regions remain separate inputs to an app's layout.
  • sequence increases only when a meaningful native snapshot changes. The native side rounds geometry to 0.01 CSS px and angle to 0.001 radian, suppresses identical snapshots, and coalesces hinge/layout callbacks to about one query per 16.7 ms. A posture change can therefore appear up to one frame later.
  • Each layoutChanged callback already receives the complete current state. Apply that argument directly; do not call Duo.getState() inside every callback. The optional DOM helper calls getState() once during bootstrap, then uses event payloads.

On unsupported iOS, Android, and web, all flags are false, hinge, divisionAxis, and verticalBarEdge are null, both region lists are empty, and sequence is 0.

const state = await Duo.getState();

if (state.verticalBarEdge === 'trailing') {
  // The consuming app may choose to place custom controls
  // on the platform-provided trailing vertical-control edge.
}

The plugin exposes the native preference and does not prescribe navigation design.

Coordinates

Region frame values are CSS layout-viewport pixels measured from the WebView's upper-left edge. UIKit reports points in the probe's coordinates; the plugin first uses UIView.convert(_:to:) to map them into the WKWebView's bounds. It then checks the WebView bounds against window.innerWidth/innerHeight and visualViewport dimensions, scale, and offsets. UIKit points and CSS pixels map nearly 1:1 for the supported, settled Capacitor viewport (width=device-width, initial-scale=1, viewport-fit=cover), without multiplying by device pixel ratio. A WebView may have any stable rectangular size or orientation, but native content insets, pinch zoom, keyboard-shifted visual viewports, and transformed/offset viewports are outside this coordinate contract. Use CSS safe-area env() values separately.

During orientation, fold, or resize transitions, UIKit and WebKit can briefly report different layout frames. The plugin rejects zero, nonfinite, mismatched, rotated, and out-of-bounds samples as a whole. It retains the last valid state (including its sequence) and retries each frame for about half a second, then every 250 ms until a valid sample arrives or the view detaches. A new layout or hinge update brings the next retry forward. It never publishes a partially mapped region list. Debug builds log a short rejection reason at most once per second; release builds do not log it. If the first sample is invalid, getState() returns the neutral state until the first valid sample.

No ordinary viewport width, orientation, pixel density, or safe-area values are bridged; use CSS, ResizeObserver, visualViewport, and browser APIs for those.

Optional DOM projection

import { installDuoLayout } from '@devitup/capacitor-duo-plugin/dom';
const dispose = await installDuoLayout();
// When the owning app/service is destroyed:
await dispose();

The helper sets data-duo, data-duo-supported, data-duo-hinge, data-duo-division, and, when known, data-duo-division-axis on <html>. For the first active division it sets --duo-division-x, --duo-division-y, --duo-division-width, and --duo-division-height.

When UIKit specifies an edge, the helper also sets data-duo-vertical-bar-edge="leading" or "trailing". It removes the attribute when the edge becomes null, on unsupported state, and on disposal.

It also sets data-duo-occlusion="active|inactive" and data-duo-occlusion-count (the full active count). Up to four active occlusions, sorted by x, y, then opaque ID, receive --duo-occlusion-0-x/y/width/height through --duo-occlusion-3-x/y/width/height. The full region list remains available through Duo.getState() and event payloads. The helper removes stale properties when regions disappear, and removes all its attributes/properties on disposal or unsupported state. It subscribes before requesting state and rejects an older snapshot using sequence.

For multiple division regions, use Duo.getState() directly; the CSS variables describe the first active region only.

Sass and CSS

@use '@devitup/capacitor-duo-plugin/scss' as duo;

@include duo.device {
  @media (min-width: 700px) {
    .app-navigation { /* consumer-chosen wide Duo treatment */ }
  }
}

.content {
  @include duo.division-active {
    /* avoid or split around the real fold */
  }
}

@include duo.vertical-bar-trailing {
  .custom-control { inset-inline-end: var(--app-control-inline-offset); }
}

700px is an application-selected width, not an official Duo breakpoint. Mixins: device, hinge, hinge-active, hinge-fully-open, hinge-partially-open, division-active, division-vertical, division-horizontal, occlusion-active, vertical-bar, vertical-bar-leading, and vertical-bar-trailing. They compile to selectors for the DOM helper and work at the stylesheet root or inside a selector. The vertical-bar mixins add no widths or navigation styles.

For a full-viewport vertical division, a consumer might place its two panes in a grid using the actual geometry:

:root[data-duo-division='active'][data-duo-division-axis='vertical'] .split {
  display: grid;
  grid-template-columns: var(--duo-division-x) var(--duo-division-width) minmax(0, 1fr);
}
.split > :first-child { grid-column: 1; }
.split > :last-child { grid-column: 3; }

Horizontal divisions use --duo-division-y and --duo-division-height in grid-template-rows. Consumers should account for their own container offset and size; the variables are relative to the WebView viewport, not a nested element.

Verification and troubleshooting

Run npm test, npm run build, npm run verify:package, and an Xcode build/test of CapacitorDuoPlugin. If native state stays neutral, verify iOS 27.1, plugin registration after npx cap sync, and that the WebView is attached to a visible scene. An inactive division is expected when flat. Verify viewport meta settings if CSS geometry appears offset. The Duo simulator should be used for manual fold/flat transition checks before shipping; an SDK build alone cannot validate live hinge behavior.

The plugin itself collects or transmits no user data. Its JavaScript evaluation reads only the current layout viewport dimensions to convert native coordinates.

See SDK findings for the exact beta API names and differences from the earlier plan.

The release handoff lists the remaining remote setup and live Duo validation before publishing.