duet-ui 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chrome.js CHANGED
@@ -118,7 +118,7 @@ function subscribe(cb) {
118
118
  visualViewport?.removeEventListener("resize", on);
119
119
  };
120
120
  }
121
- const ssr = { mode: "column", duo: true, browser: "safari", layout: "narrow", side: "right", width: 382, height: 580, offsetTop: 0, screenWidth: 466, screenHeight: 678, insetTop: 0, insetBottom: 0 };
121
+ const ssr = { mode: "column", duo: true, browser: "safari", device: "duo", standalone: false, layout: "narrow", side: "right", width: 382, height: 580, offsetTop: 0, screenWidth: 466, screenHeight: 678, insetTop: 0, insetBottom: 0 };
122
122
  export function useDuet() {
123
123
  return useSyncExternalStore(subscribe, read, () => ssr);
124
124
  }
package/dist/duet.css CHANGED
@@ -228,6 +228,10 @@ html:has(> body .duet-scroller), html:has(> body .duet-scroller) > body { height
228
228
  edge, so there the scroller is an in-document box that runs on under the column. The document still never
229
229
  scrolls, so the chrome layer never moves. */
230
230
  html[data-duet-mode="column"] .duet-scroller { position: absolute; width: calc(100% + var(--duet-inset-right)); bottom: auto; height: calc(100% + var(--duet-inset-bottom)); }
231
+ /* That overflow widens the document, and touch-action on <html> stops at this scroll container, so without its own
232
+ a sideways swipe pans the whole page, chrome and all, into the column. Carousels inside are scrollers of their own
233
+ and still pan. */
234
+ html[data-duet-mode="column"] .duet-scroller { touch-action: pan-y; }
231
235
  /* Safari's URL pill floats under the page's bottom edge: the scroller runs on under it (above) and this spacer lets
232
236
  the last content scroll clear of it. */
233
237
  .duet-scroller::after { content: ""; display: block; height: var(--duet-inset-bottom, 0px); }
@@ -109,6 +109,10 @@ export interface Context {
109
109
  split?: boolean;
110
110
  /** The browser the page runs in (see browserOf). Only Safari gets the column. */
111
111
  browser: Browser;
112
+ /** The device the page runs on (see deviceOf). */
113
+ device: Device;
114
+ /** Opened from the Home Screen as a web app, not in a browser tab. */
115
+ standalone: boolean;
112
116
  }
113
117
  export type Layout = "narrow" | "wide";
114
118
  /** Page width from which two panes fit. The page spans the window, controls floating over it: the open Duo gives
@@ -123,6 +127,11 @@ export type Browser = "safari" | "chrome" | "firefox" | "edge" | "webview" | "ot
123
127
  /** Every iOS browser is WebKit; the others name themselves in the UA. An in-app web view (Instagram, Facebook)
124
128
  * drops the "Safari/" token; the in-app Safari preview keeps it, and is Safari. */
125
129
  export declare function browserOf(ua: string | undefined): Browser;
130
+ export type Device = "duo" | "iphone" | "ipad" | "android" | "desktop";
131
+ /** The Duo by its sizes (isDuo), other iOS devices by their screen's short side (an iPad's is 744pt or more,
132
+ * and iPadOS sends a Mac UA with touch), Android by its UA. Without a UA (an emulator's signals) only the
133
+ * sizes count. */
134
+ export declare function deviceOf(s: Signals): Device;
126
135
  /** A Safari split-view pane: its window is 469x669 in either pane. Nothing tells the left pane from the right
127
136
  * (device probe 2026-10-25: screenX, availLeft, insets, visualViewport and touch screenX read the same in both),
128
137
  * though Safari's column sits on the pane's outer edge. So the inline side is remembered apart for split view. */
package/dist/geometry.js CHANGED
@@ -75,6 +75,21 @@ export function browserOf(ua) {
75
75
  return "safari";
76
76
  return "other";
77
77
  }
78
+ /** The Duo by its sizes (isDuo), other iOS devices by their screen's short side (an iPad's is 744pt or more,
79
+ * and iPadOS sends a Mac UA with touch), Android by its UA. Without a UA (an emulator's signals) only the
80
+ * sizes count. */
81
+ export function deviceOf(s) {
82
+ if (isDuo(s))
83
+ return "duo";
84
+ const ua = s.userAgent ?? "";
85
+ if (/iPad/.test(ua) || (isIOS(s) && Math.min(s.screenWidth, s.screenHeight) >= 700))
86
+ return "ipad";
87
+ if (isIOS(s))
88
+ return "iphone";
89
+ if (/Android/.test(ua))
90
+ return "android";
91
+ return "desktop";
92
+ }
78
93
  /** A Safari split-view pane: its window is 469x669 in either pane. Nothing tells the left pane from the right
79
94
  * (device probe 2026-10-25: screenX, availLeft, insets, visualViewport and touch screenX read the same in both),
80
95
  * though Safari's column sits on the pane's outer edge. So the inline side is remembered apart for split view. */
@@ -91,12 +106,12 @@ const SAFARI_COLUMN = { width: [80, 88], chrome: [68, 102] };
91
106
  const within = (v, [a, b]) => v >= a && v <= b;
92
107
  /** preferred: the side the user picked for inline controls, if any. */
93
108
  export function detect(s, preferred, allowColumn = true) {
94
- const duo = isDuo(s), browser = browserOf(s.userAgent);
109
+ const duo = isDuo(s), browser = browserOf(s.userAgent), device = deviceOf(s), standalone = s.standalone;
95
110
  // Only a Home Screen app owns the strip: a Safari tab reports the column as an 84pt inset for a frame while folding.
96
111
  if (s.standalone && (s.insetRight >= 60 || s.insetLeft >= 60)) {
97
- return { mode: "strip", duo, browser, layout: layoutOf(s.innerWidth), side: s.insetRight >= 60 ? "right" : "left", width: s.innerWidth, height: s.innerHeight, offsetTop: Math.max(0, s.screenHeight - s.innerHeight), screenWidth: s.screenWidth, screenHeight: s.screenHeight, insetTop: s.insetTop, insetBottom: s.insetBottom };
112
+ return { mode: "strip", duo, browser, device, standalone, layout: layoutOf(s.innerWidth), side: s.insetRight >= 60 ? "right" : "left", width: s.innerWidth, height: s.innerHeight, offsetTop: Math.max(0, s.screenHeight - s.innerHeight), screenWidth: s.screenWidth, screenHeight: s.screenHeight, insetTop: s.insetTop, insetBottom: s.insetBottom };
98
113
  }
99
- const base = { duo, browser, width: s.innerWidth, height: s.innerHeight, offsetTop: 0, insetTop: s.insetTop, insetBottom: s.insetBottom };
114
+ const base = { duo, browser, device, standalone, width: s.innerWidth, height: s.innerHeight, offsetTop: 0, insetTop: s.insetTop, insetBottom: s.insetBottom };
100
115
  // Other iOS browsers draw their own chrome where Safari has its column: inline for them.
101
116
  const column = allowColumn && !s.standalone && !s.framed && browser === "safari"
102
117
  && within(s.outerWidth - s.innerWidth, SAFARI_COLUMN.width)
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { Duet, DuoCookie, useDuet, type DuetProps, type DuetItem, type DuetTab }
2
2
  export { bleed, column } from "./layout.ts";
3
3
  export { DuetPanes, DuetPaneBar, usePanes, type DuetPanesProps, type DuetPaneBarProps } from "./duet-panes.tsx";
4
4
  export { panes, backFrom, type PaneWindow } from "./panes.ts";
5
- export { browserOf, detect, isDuo, isIOS, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE, type Browser, type Context, type Layout, type Mode, type Plan, type Side, type Signals, type Spec } from "./geometry.ts";
5
+ export { browserOf, detect, deviceOf, isDuo, isIOS, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE, type Browser, type Context, type Device, type Layout, type Mode, type Plan, type Side, type Signals, type Spec } from "./geometry.ts";
package/dist/index.js CHANGED
@@ -2,4 +2,4 @@ export { Duet, DuoCookie, useDuet } from "./chrome.js";
2
2
  export { bleed, column } from "./layout.js";
3
3
  export { DuetPanes, DuetPaneBar, usePanes } from "./duet-panes.js";
4
4
  export { panes, backFrom } from "./panes.js";
5
- export { browserOf, detect, isDuo, isIOS, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE } from "./geometry.js";
5
+ export { browserOf, detect, deviceOf, isDuo, isIOS, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE } from "./geometry.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duet-ui",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Native-feeling controls for React web apps on the foldable iPhone: the side strip, Safari's column, split view and Home Screen apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",