duet-ui 0.2.1 → 0.2.2
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/README.md +3 -1
- package/dist/chrome.js +3 -1
- package/dist/geometry.d.ts +13 -0
- package/dist/geometry.js +28 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,7 +67,9 @@ Each mode has a band the controls may use, and Duet keeps every control inside i
|
|
|
67
67
|
|
|
68
68
|
### Only on the Duo
|
|
69
69
|
|
|
70
|
-
`useDuet().duo` is true on an iPhone Duo in any posture, Safari or Home Screen, and false on other phones and desktops. It works outside `<Duet>`, so an existing app can switch to a Duo layout on it: render `<Duet>` only when it is true. `isDuo(signals)` is the same check for your own signals.
|
|
70
|
+
`useDuet().duo` is true on an iPhone Duo in any posture, Safari or Home Screen, and false on other phones and desktops. iOS never names the model, so it takes two checks: an iPhone user agent (or Safari's desktop-site Mac agent with touch), and a screen or window size only the Duo has. It works outside `<Duet>`, so an existing app can switch to a Duo layout on it: render `<Duet>` only when it is true. `isDuo(signals)` is the same check for your own signals.
|
|
71
|
+
|
|
72
|
+
`useDuet().browser` is `"safari"`, `"chrome"`, `"firefox"`, `"edge"`, `"webview"` (an in-app browser such as Instagram's) or `"other"`. Only Safari gets `column`: the others draw their own chrome where Safari leaves its column, so they get `inline`.
|
|
71
73
|
|
|
72
74
|
The server can't tell, so `<DuoCookie>` tells it: it keeps a `duet-duo` cookie in step with the browser, and a new device reloads once into the right layout. In the Next.js App Router:
|
|
73
75
|
|
package/dist/chrome.js
CHANGED
|
@@ -47,6 +47,8 @@ function signals() {
|
|
|
47
47
|
insetLeft: parseFloat(pad.paddingLeft) || 0,
|
|
48
48
|
framed: window.top !== window,
|
|
49
49
|
pixelRatio: devicePixelRatio,
|
|
50
|
+
userAgent: navigator.userAgent,
|
|
51
|
+
touchPoints: navigator.maxTouchPoints,
|
|
50
52
|
standalone: navigator.standalone === true || matchMedia("(display-mode: standalone)").matches,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -116,7 +118,7 @@ function subscribe(cb) {
|
|
|
116
118
|
visualViewport?.removeEventListener("resize", on);
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
|
-
const ssr = { mode: "column", duo: true, 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", layout: "narrow", side: "right", width: 382, height: 580, offsetTop: 0, screenWidth: 466, screenHeight: 678, insetTop: 0, insetBottom: 0 };
|
|
120
122
|
export function useDuet() {
|
|
121
123
|
return useSyncExternalStore(subscribe, read, () => ssr);
|
|
122
124
|
}
|
package/dist/geometry.d.ts
CHANGED
|
@@ -80,6 +80,10 @@ export interface Signals {
|
|
|
80
80
|
framed?: boolean;
|
|
81
81
|
/** devicePixelRatio: tells the in-app preview's 375x667 window from an iPhone SE's (2x). */
|
|
82
82
|
pixelRatio?: number;
|
|
83
|
+
/** navigator.userAgent and maxTouchPoints: tell iOS and Safari apart (see isIOS, browserOf). Unknown (an
|
|
84
|
+
* emulator's signals) restricts nothing. */
|
|
85
|
+
userAgent?: string;
|
|
86
|
+
touchPoints?: number;
|
|
83
87
|
}
|
|
84
88
|
export interface Context {
|
|
85
89
|
mode: Mode;
|
|
@@ -103,6 +107,8 @@ export interface Context {
|
|
|
103
107
|
/** A Safari split-view pane on the open Duo (see isSplit). In column mode nothing tells which pane it is, so
|
|
104
108
|
* the controls are drawn in both columns, each pane showing only its own (chrome.tsx); `side` is moot. */
|
|
105
109
|
split?: boolean;
|
|
110
|
+
/** The browser the page runs in (see browserOf). Only Safari gets the column. */
|
|
111
|
+
browser: Browser;
|
|
106
112
|
}
|
|
107
113
|
export type Layout = "narrow" | "wide";
|
|
108
114
|
/** Page width from which two panes fit. The page spans the window, controls floating over it: the open Duo gives
|
|
@@ -110,6 +116,13 @@ export type Layout = "narrow" | "wide";
|
|
|
110
116
|
export declare const WIDE = 600;
|
|
111
117
|
export declare const isPreview: (s: Signals) => boolean;
|
|
112
118
|
export declare const isDuo: (s: Signals) => boolean;
|
|
119
|
+
/** iOS has no model in its UA, so the sizes above say Duo; the UA says iPhone. Safari's "Request Desktop Website"
|
|
120
|
+
* sends a Mac UA, told from a Mac by touch. undefined when the signals carry no UA. */
|
|
121
|
+
export declare const isIOS: (s: Signals) => boolean | undefined;
|
|
122
|
+
export type Browser = "safari" | "chrome" | "firefox" | "edge" | "webview" | "other";
|
|
123
|
+
/** Every iOS browser is WebKit; the others name themselves in the UA. An in-app web view (Instagram, Facebook)
|
|
124
|
+
* drops the "Safari/" token; the in-app Safari preview keeps it, and is Safari. */
|
|
125
|
+
export declare function browserOf(ua: string | undefined): Browser;
|
|
113
126
|
/** A Safari split-view pane: its window is 469x669 in either pane. Nothing tells the left pane from the right
|
|
114
127
|
* (device probe 2026-10-25: screenX, availLeft, insets, visualViewport and touch screenX read the same in both),
|
|
115
128
|
* 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
|
@@ -53,11 +53,32 @@ export const isPreview = (s) => !s.standalone && s.pixelRatio === PREVIEW.pixelR
|
|
|
53
53
|
* Other phones and desktops never match, so apps can switch to a Duo layout on it alone. */
|
|
54
54
|
const DUO_SIZES = [[466, 678], [669, 951], [469, 669]];
|
|
55
55
|
const duoSize = (w, h) => DUO_SIZES.some(([a, b]) => Math.abs(Math.min(w, h) - a) <= 1 && Math.abs(Math.max(w, h) - b) <= 1);
|
|
56
|
-
export const isDuo = (s) => duoSize(s.screenWidth, s.screenHeight) || duoSize(s.outerWidth, s.outerHeight);
|
|
56
|
+
export const isDuo = (s) => isIOS(s) !== false && (duoSize(s.screenWidth, s.screenHeight) || duoSize(s.outerWidth, s.outerHeight));
|
|
57
|
+
/** iOS has no model in its UA, so the sizes above say Duo; the UA says iPhone. Safari's "Request Desktop Website"
|
|
58
|
+
* sends a Mac UA, told from a Mac by touch. undefined when the signals carry no UA. */
|
|
59
|
+
export const isIOS = (s) => s.userAgent === undefined ? undefined
|
|
60
|
+
: /iPhone|iPod/.test(s.userAgent) || (/Macintosh/.test(s.userAgent) && (s.touchPoints ?? 0) > 1);
|
|
61
|
+
/** Every iOS browser is WebKit; the others name themselves in the UA. An in-app web view (Instagram, Facebook)
|
|
62
|
+
* drops the "Safari/" token; the in-app Safari preview keeps it, and is Safari. */
|
|
63
|
+
export function browserOf(ua) {
|
|
64
|
+
if (ua === undefined)
|
|
65
|
+
return "safari";
|
|
66
|
+
if (/EdgiOS|Edg\//.test(ua))
|
|
67
|
+
return "edge";
|
|
68
|
+
if (/CriOS|Chrome\//.test(ua))
|
|
69
|
+
return "chrome";
|
|
70
|
+
if (/FxiOS|Firefox\//.test(ua))
|
|
71
|
+
return "firefox";
|
|
72
|
+
if (/AppleWebKit/.test(ua) && !/Safari\//.test(ua))
|
|
73
|
+
return "webview";
|
|
74
|
+
if (/Version\/[\d.]+.*Safari\//.test(ua) && !/OPiOS|OPT\/|GSA\/|Ddg\//.test(ua))
|
|
75
|
+
return "safari";
|
|
76
|
+
return "other";
|
|
77
|
+
}
|
|
57
78
|
/** A Safari split-view pane: its window is 469x669 in either pane. Nothing tells the left pane from the right
|
|
58
79
|
* (device probe 2026-10-25: screenX, availLeft, insets, visualViewport and touch screenX read the same in both),
|
|
59
80
|
* though Safari's column sits on the pane's outer edge. So the inline side is remembered apart for split view. */
|
|
60
|
-
export const isSplit = (s) => !s.standalone && Math.abs(s.outerWidth - 469) <= 1 && Math.abs(s.outerHeight - 669) <= 1;
|
|
81
|
+
export const isSplit = (s) => !s.standalone && isIOS(s) !== false && Math.abs(s.outerWidth - 469) <= 1 && Math.abs(s.outerHeight - 669) <= 1;
|
|
61
82
|
/** Measured on the Duo simulator: Safari's column is 84pt wide (outer − inner), the chrome below the
|
|
62
83
|
* page is 98pt (80 with the compact URL bar), the window is the closed screen's 678pt tall. A split-view
|
|
63
84
|
* pane on the open Duo (469x669 outer, 385x571 inner) has the same column on its outer edge, so either
|
|
@@ -70,13 +91,14 @@ const SAFARI_COLUMN = { width: [80, 88], chrome: [68, 102] };
|
|
|
70
91
|
const within = (v, [a, b]) => v >= a && v <= b;
|
|
71
92
|
/** preferred: the side the user picked for inline controls, if any. */
|
|
72
93
|
export function detect(s, preferred, allowColumn = true) {
|
|
73
|
-
const duo = isDuo(s);
|
|
94
|
+
const duo = isDuo(s), browser = browserOf(s.userAgent);
|
|
74
95
|
// Only a Home Screen app owns the strip: a Safari tab reports the column as an 84pt inset for a frame while folding.
|
|
75
96
|
if (s.standalone && (s.insetRight >= 60 || s.insetLeft >= 60)) {
|
|
76
|
-
return { mode: "strip", duo, 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 };
|
|
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 };
|
|
77
98
|
}
|
|
78
|
-
const base = { duo, width: s.innerWidth, height: s.innerHeight, offsetTop: 0, insetTop: s.insetTop, insetBottom: s.insetBottom };
|
|
79
|
-
|
|
99
|
+
const base = { duo, browser, width: s.innerWidth, height: s.innerHeight, offsetTop: 0, insetTop: s.insetTop, insetBottom: s.insetBottom };
|
|
100
|
+
// Other iOS browsers draw their own chrome where Safari has its column: inline for them.
|
|
101
|
+
const column = allowColumn && !s.standalone && !s.framed && browser === "safari"
|
|
80
102
|
&& within(s.outerWidth - s.innerWidth, SAFARI_COLUMN.width)
|
|
81
103
|
&& within(s.outerHeight - s.innerHeight, SAFARI_COLUMN.chrome)
|
|
82
104
|
&& s.innerHeight > s.innerWidth // landscape moves Safari's column buttons around
|
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 { detect, isDuo, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE, type Context, type Layout, type Mode, type Plan, type Side, type Signals, type Spec } from "./geometry.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";
|
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 { detect, isDuo, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE } from "./geometry.js";
|
|
5
|
+
export { browserOf, detect, isDuo, isIOS, isSplit, plan, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE } from "./geometry.js";
|
package/package.json
CHANGED