duet-ui 0.4.0-rc.1 → 0.4.0-rc.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 +175 -3
- package/dist/chrome.d.ts +6 -0
- package/dist/chrome.js +42 -12
- package/dist/duet.css +25 -3
- package/dist/geometry.d.ts +25 -0
- package/dist/geometry.js +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,15 +99,37 @@ How a new reading is committed:
|
|
|
99
99
|
|
|
100
100
|
Column mode infers from the window geometry that Safari's column is there; it never sees Safari's buttons. The 80 to 88px width difference is Safari's column (84px measured), the height difference is Safari's chrome below the page.
|
|
101
101
|
|
|
102
|
-
- `position: fixed` content
|
|
102
|
+
- `position: fixed` content stops at the page viewport (and a fixed box covering the middle of the viewport edge has its colour painted over the column, see [Modals and sheets in column mode](#modals-and-sheets-in-column-mode)), but WebKit paints in-document content that overflows the layout viewport into Safari's column. Duet renders the controls in an absolute layer at the top of the document, widens the document by the column's width, keeps the document itself from scrolling (the page scrolls in the inner scroller) and sets `touch-action: pan-y` on `<html>` so the page does not pan sideways. This is observed WebKit behaviour, not a supported API.
|
|
103
103
|
- Without `children` the document itself scrolls, and the layer follows it on a CSS scroll timeline (`animation-timeline: scroll(root)`). Safari runs the timeline with the scroll, so the controls hold through momentum and through smooth programmatic scrolls (`scrollIntoView({ behavior: "smooth" })`, `scrollTo({ behavior: "smooth" })`): you need no scroll listener and no `behavior: "auto"` workaround. Measured on the simulator: within 1px through flings and at rest, and one frame of up to 7pt at the end of a smooth scroll (a transform set on scroll ran up to 300pt off there). The keyframe ends at `scrollHeight - innerHeight`, Safari's own timeline range, which follows its URL bar as it collapses. Without scroll-timeline support, or when a page reset like `* { animation: none !important }` removes it, Duet falls back to a transform set on scroll, which lags in momentum. Children mode stays the steadier choice.
|
|
104
104
|
- The widened document can pan sideways when a swipe runs off the end of a horizontal scroller (a chip row), since `touch-action` stops at that scroller. Duet pins `scrollX` (0, or the column width when the document spans a left column) on every scroll and sets `overscroll-behavior-x: contain` on all elements at zero specificity (`:where()`), so your own value wins.
|
|
105
105
|
- Touches in the column reach the page but are not hit-tested to Duet's buttons, and Safari usually keeps the touch end. Duet listens for `pointerdown`, finds the button under the touch's coordinates and clicks it, so a column button fires on touch down.
|
|
106
106
|
- Placement comes from constants measured on the iPhone Duo simulator (iOS 27.1) and checked on a device: Safari's back and reader rows at the top of the column, its +/tabs pill at the bottom. A Safari update that moves its buttons or changes this painting behaviour needs new measurements.
|
|
107
107
|
|
|
108
|
+
### Modals and sheets in column mode
|
|
109
|
+
|
|
110
|
+
A modal or sheet on the web usually sits on a full-screen scrim: `position: fixed; inset: 0`. In Safari's column on the closed Duo that used to hide Duet's whole column layer, the close button included (#79).
|
|
111
|
+
|
|
112
|
+
- **What you see:** nothing to handle. While the overlay is open, the controls stay in the column and keep working. Safari's own buttons there are unaffected either way.
|
|
113
|
+
- **Cause:** Safari 26 hit-tests the middle of each viewport edge, 4px in, for a fixed or sticky box (WebKit's `LocalFrameView::fixedContainerEdges`). When one covers that point, Safari paints the box's colour, or plain chrome when the box has a `backdrop-filter`, over everything beyond that edge, Safari's column included. Duet's column layer is in-document content, so it disappears under that fill. Measured on the simulator: a scrim 3px short of the edge still hides the column, 6px short does not, and a scrim of 90% width or 40% height does not.
|
|
114
|
+
- **What Duet does:** in column mode it renders a small fixed guard (`.duet-guard`, 16x64px, transparent, `pointer-events: none`, top `z-index`) straddling that point on each edge that faces a column. Safari's hit test lands on the guard, which is too small to count as a container, so it finds none and keeps painting the column normally. The guard takes no taps and paints nothing.
|
|
115
|
+
- **What you must do:** nothing for visibility. For a native feel, pass `close` to `<Duet>` while the sheet is open, so the controls hold only its close button, and hide the sheet's own X in column mode:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<Duet tabs={tabs} close={open ? { label: "Close", onPress: () => setOpen(false) } : undefined}>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
html[data-duet-mode="column"] .sheet-x { display: none; }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The old workaround, making the scrim `position: absolute` in column mode, still works and is no longer needed.
|
|
126
|
+
- **Not covered:** `<dialog>.showModal()` puts its `::backdrop` in the browser's top layer, above any `z-index`, so it still hides the column (checked on the simulator), and the page outside a modal dialog is inert anyway, so Duet's buttons could not take taps. Use a fixed `<div>` scrim (or `dialog.show()` with your own scrim) when you want the column controls. A page box of your own with `z-index: 2147483647` over the middle of the column edge also beats the guard.
|
|
127
|
+
|
|
128
|
+
Try it on the [site](https://getduet.dev/sheet).
|
|
129
|
+
|
|
108
130
|
### Split view
|
|
109
131
|
|
|
110
|
-
A Safari split-view pane (469x669 window, 385x571 page) has Safari's column on the pane's outer edge, but no web signal says whether the pane is on the left or the right. In column mode Duet widens the document by a column on each side and draws the controls in both; each pane only shows its own column, and a tap there tells Duet which side it was. When a split pane falls back to `inline`, its side is remembered separately from the full-window side, so the user can move the controls next to Safari's column once. `useDuet().split` is true in a split pane.
|
|
132
|
+
A Safari split-view pane (469x669 window, 385x571 page) has Safari's column on the pane's outer edge, but no web signal says whether the pane is on the left or the right. In column mode Duet widens the document by a column on each side and draws the controls in both; each pane only shows its own column, and a tap there tells Duet which side it was. The document is kept scrolled across the left column, so a pane shows document x 84 to 469. When a split pane falls back to `inline`, its side is remembered separately from the full-window side, so the user can move the controls next to Safari's column once. `useDuet().split` is true in a split pane.
|
|
111
133
|
|
|
112
134
|
### Side
|
|
113
135
|
|
|
@@ -256,7 +278,17 @@ The page spans the whole window and the controls float over it, so backgrounds r
|
|
|
256
278
|
- `column(maxWidth, pad)`: a centred reading column. It centres on the whole window and moves aside from the controls only as far as it must. The `duet-column` class does the same in CSS (`--duet-max`, `--duet-pad`).
|
|
257
279
|
- `bleed(pad)`: a full-width band (hero, sticky bar, list row). The band runs under the controls, its content is padded back beside them. `bleed(false)` runs content itself edge to edge (a map, an image). The `duet-bleed` class does the same in CSS.
|
|
258
280
|
|
|
259
|
-
Duet sets the insets on `<html>` as `--duet-inset-left`, `--duet-inset-right` (the controls' width on their edge; in `column` mode without children
|
|
281
|
+
Duet sets the insets on `<html>` as `--duet-inset-left`, `--duet-inset-right` (the controls' width on their edge; 0 in `column` mode without children, see below), `--duet-inset-top` (28px in `strip` mode, for iOS's top blur in Home Screen apps, else 0) and `--duet-inset-bottom` (in `column` mode, Safari's chrome below the page: the page runs on under Safari's floating URL bar, and the scroller ends with that much space so the last content scrolls clear) and `--duet-safe-top-column` (in `column` mode, the top of the part of Safari's column clear of its back and reader buttons: keep popovers that cross the column below it). `--duet-vh` is `stableHeight` in px: size full-screen sections with `min-height: var(--duet-vh, 100svh)` so they keep their height while the URL bar collapses and expands. Inside `<DuetPanes>` each pane gets its own left and right insets: only a pane on the controls' edge is inset. `<html data-duet-mode>` holds the current mode for CSS.
|
|
282
|
+
|
|
283
|
+
### Column mode without children: Duet places the page
|
|
284
|
+
|
|
285
|
+
In `column` mode the controls sit in Safari's column, outside the layout viewport, so a page has nothing to keep clear of. Without `children` Duet places the page itself, so any page, including one that never calls `column()` or `bleed()`, starts just clear of the controls, keeps centred content centred in the space left and is never clipped:
|
|
286
|
+
|
|
287
|
+
- The insets are 0 and the page's layout viewport is exactly the part of the window beside Safari's column.
|
|
288
|
+
- Where the document spans a left column (split view, the closed Duo turned to 90), Duet keeps it scrolled across and gives `<html>` `margin-left` of the column width and the matching negative `margin-right`, so the page lands in the visible part of the window instead of under the column. `<html data-duet-reserve>` marks this. Do not set `margin` on `<html>` yourself, and do not reset `scrollX`: Duet owns it.
|
|
289
|
+
- `<Duet reserve={false}>` opts out: the page runs under the column as before, and `--duet-inset-left` / `--duet-inset-right` hold the column's width for `column()`, `bleed()` or your own padding.
|
|
290
|
+
|
|
291
|
+
With `children`, in `inline` and `strip` mode and in the emulated column, the page still runs under the controls and keeps clear through the insets above.
|
|
260
292
|
|
|
261
293
|
## Panes
|
|
262
294
|
|
|
@@ -293,6 +325,146 @@ export function Mail() {
|
|
|
293
325
|
|
|
294
326
|
Try it on the [site](https://getduet.dev/panes).
|
|
295
327
|
|
|
328
|
+
## Edge cases
|
|
329
|
+
|
|
330
|
+
Every iOS and Safari quirk Duet handles, what you would see without the handling, and what is left for you to do. Most need nothing from you. "Without children" means a page that renders `<Duet>` without `children`, so the document itself scrolls.
|
|
331
|
+
|
|
332
|
+
### Split view pane, or the closed Duo turned left
|
|
333
|
+
|
|
334
|
+
- **You see:** in column mode without children, the left 84px of the page is clipped, the controls draw over the content and an empty band runs down the right.
|
|
335
|
+
- **Cause:** Safari's column is on the pane's left edge (or on the left when the closed Duo is turned to 90). Duet keeps the document scrolled one column across so the left column can paint, but the page content still starts at x 0, under the column.
|
|
336
|
+
- **Duet does:** places the page itself: `margin-left` of the column width and the matching negative `margin-right` on `<html>`, marked `<html data-duet-reserve>`, with both insets 0. Centred content stays centred, nothing is clipped.
|
|
337
|
+
- **You do:** nothing. Don't set `margin` on `<html>` or reset `scrollX`, Duet owns both. `<Duet reserve={false}>` opts out: the page runs under the column and `--duet-inset-left` / `--duet-inset-right` hold its width for `column()`, `bleed()` or your own padding.
|
|
338
|
+
- **Status:** [#77](https://github.com/JayFarei/duet/issues/77), [PR #78](https://github.com/JayFarei/duet/pull/78), 0.4.0-rc.2. See [Split view](#split-view).
|
|
339
|
+
|
|
340
|
+
### Rubber band at the top or bottom of the page
|
|
341
|
+
|
|
342
|
+
- **You see:** in column mode without children, the controls leave their place as a group while the page bounces at either end (sliding up to 160pt over Safari's own column buttons, or out of the column), then come back once it settles.
|
|
343
|
+
- **Cause:** the controls follow the document on a CSS scroll timeline, which is clamped to the scroll range. The iOS rubber band pushes `scrollY` past that range, and the in-document layer moves with the bounce.
|
|
344
|
+
- **Duet does:** while the layer follows the document, `<html>` gets `overscroll-behavior-y: none`, so the document does not rubber-band, like a page with children. Only the Duo column is affected: other modes and other devices keep the bounce.
|
|
345
|
+
- **You do:** nothing. The rule has zero specificity (`:where()`), so a page's own `overscroll-behavior` still wins, and with it the controls ride the bounce again.
|
|
346
|
+
- **Status:** [#76](https://github.com/JayFarei/duet/issues/76), [PR #80](https://github.com/JayFarei/duet/pull/80), 0.4.0-rc.2.
|
|
347
|
+
|
|
348
|
+
### Full-screen `position: fixed` overlay or modal backdrop
|
|
349
|
+
|
|
350
|
+
- **You see:** without the handling, in column mode, while a modal or sheet scrim (`position: fixed; inset: 0`) is open, every Duet control in Safari's column disappears, the close button included, so the user has no way to close the sheet from the column.
|
|
351
|
+
- **Cause:** Safari 26 hit-tests a point 4px in from the middle of each viewport edge for a fixed or sticky box and, when one covers it, paints that box's colour over the inset beyond that edge, Safari's column included, instead of the page's in-document content.
|
|
352
|
+
- **Duet does:** in column mode renders `.duet-guard`, a small invisible fixed box (16x64px, `pointer-events: none`, top `z-index`) on each column edge, so that hit test lands on it and the column keeps showing the controls. Your scrim stays `position: fixed`.
|
|
353
|
+
- **You do:** nothing for a fixed `<div>` scrim. `<dialog>.showModal()` is not covered: its `::backdrop` sits in the top layer, above any `z-index`, so use a fixed `<div>` scrim instead. For one X, in the column, pass `close` to `<Duet>` while the sheet is open and hide the sheet's own X in column mode (`html[data-duet-mode="column"] .sheet-close { display: none; }`). See [Modals and sheets in column mode](#modals-and-sheets-in-column-mode).
|
|
354
|
+
- **Status:** [#79](https://github.com/JayFarei/duet/issues/79), [PR #83](https://github.com/JayFarei/duet/pull/83), 0.4.0-rc.2.
|
|
355
|
+
|
|
356
|
+
### Sideways swipe pans the page into the column
|
|
357
|
+
|
|
358
|
+
- **You see:** a sideways swipe moves the whole page, controls included, into Safari's column, typically when a swipe runs off the end of a carousel or code block.
|
|
359
|
+
- **Cause:** the controls widen the document by the column's width, so the document can scroll sideways, and `touch-action` on `<html>` stops at a horizontal scroller, whose overscroll chains to the document.
|
|
360
|
+
- **Duet does:** sets `touch-action: pan-y` on `<html>` (and on the inner scroller with children), `overscroll-behavior-x: contain` on every element at zero specificity, and pins `scrollX` (0, or the column width for a left column) on every scroll, instantly, so a page's `scroll-behavior: smooth` can't turn it into a slow creep. Leaving the column resets `scrollX` to 0. Carousels and code blocks still scroll sideways.
|
|
361
|
+
- **You do:** nothing. Your own `overscroll-behavior-x` wins over Duet's.
|
|
362
|
+
- **Status:** 0.2.5 ([PR #26](https://github.com/JayFarei/duet/pull/26)). See [Column mode](#column-mode-how-it-works).
|
|
363
|
+
|
|
364
|
+
### Back/forward cache restore
|
|
365
|
+
|
|
366
|
+
- **You see:** after going back to a page (`pageshow` with `persisted`), the controls start in the page and slide into the column over 2 to 3s, seen in a split left pane and on the closed Duo turned left.
|
|
367
|
+
- **Cause:** a back/forward cache restore keeps the page's old state but not the old window, and the `scrollX` pin was a plain `scrollTo`, which a page's `html { scroll-behavior: smooth }` animated and each scroll event restarted.
|
|
368
|
+
- **Duet does:** re-reads the signals on `pageshow`, re-pins `scrollX` with `behavior: "instant"`, and turns transitions off for 1.1s after a restore, as at first mount, so the controls land in place. Normal mode changes keep their ~250ms spring.
|
|
369
|
+
- **You do:** nothing.
|
|
370
|
+
- **Status:** [#66](https://github.com/JayFarei/duet/issues/66), [PR #67](https://github.com/JayFarei/duet/pull/67).
|
|
371
|
+
|
|
372
|
+
### In-app preview sheet
|
|
373
|
+
|
|
374
|
+
- **You see:** a link opened from a social or mail app in the in-app Safari preview's page sheet got Safari-tab placement on a real Duo.
|
|
375
|
+
- **Cause:** the sheet is a window inside the closed screen (450x670 on a real Duo), and its bottom safe-area inset reads 0 on the first sample and 34 about 100ms later, so the first reading looks like a tab.
|
|
376
|
+
- **Duet does:** recognises a window inside the closed screen with the column signature and a bottom inset as the preview, and lets a tab verdict rise to preview during a window's first second. Turned sideways in the sheet, where the free gap is about 26pt, it hides the controls (`useDuet().hidden` is `"preview"`) so the preview's own buttons stay usable.
|
|
377
|
+
- **You do:** nothing. Check `useDuet().preview` and `hidden` if your page lays out differently there.
|
|
378
|
+
- **Status:** [#66](https://github.com/JayFarei/duet/issues/66), [PR #67](https://github.com/JayFarei/duet/pull/67), [#69](https://github.com/JayFarei/duet/issues/69). See [In-app preview](#in-app-preview).
|
|
379
|
+
|
|
380
|
+
### Turning or folding mid-transition
|
|
381
|
+
|
|
382
|
+
- **You see:** without the handling, the controls flash at the wrong edge during a turn, fold, unfold or split move, the page insets jump (up to 296px on a turn, -485px on an unfold), or Safari zooms the page to fit (scale 1.14).
|
|
383
|
+
- **Cause:** there is no fold signal, and mid-transition the window passes through sizes no landed window has (inner and outer orientations disagree). The window size can land up to ~2s after the viewport, with no event. The column layer still carries the old posture's positions, which widen the document past the new window.
|
|
384
|
+
- **Duet does:** keeps the last placement and hides the controls (`hidden` and `confidence` are `"moving"`) until the window lands and holds for 150ms (`LAND_HOLD`). The insets keep their last values, and the column layer is clipped out of the document width, so it adds no width until the landing. After each event it polls every 100ms until the sizes hold for 500ms, for at most 2s.
|
|
385
|
+
- **You do:** nothing. If your own code reacts to `useDuet()`, ignore readings with `confidence: "moving"`.
|
|
386
|
+
- **Status:** [#68](https://github.com/JayFarei/duet/issues/68), [#69](https://github.com/JayFarei/duet/issues/69), [PR #70](https://github.com/JayFarei/duet/pull/70), 0.4.0-rc.1. See [Which mode](#which-mode).
|
|
387
|
+
|
|
388
|
+
### Text field focus and the on-screen keyboard
|
|
389
|
+
|
|
390
|
+
- **You see:** without the handling, focusing an input in column mode makes Safari zoom out (to about 0.94), and the keyboard covers the lower column and the controls in it.
|
|
391
|
+
- **Cause:** the column layer widens the document, and Safari zooms out to that width when a text field takes focus. The keyboard leaves Safari's column in place and only shrinks the visual viewport.
|
|
392
|
+
- **Duet does:** from the moment a field (`:read-write:focus`, so inputs, textareas and `contenteditable`) takes focus, clips the column layer out of the document width and hides it, before the zoom and the keyboard. The keyboard never changes the mode. `useDuet().keyboard` is true while it is up, and `<Duet>` sets `data-keyboard`. In column mode the controls come back when focus leaves.
|
|
393
|
+
- **You do:** nothing. Don't rely on Duet's column controls while the user types.
|
|
394
|
+
- **Status:** [#61](https://github.com/JayFarei/duet/issues/61), [PR #63](https://github.com/JayFarei/duet/pull/63).
|
|
395
|
+
|
|
396
|
+
### Pinch zoom
|
|
397
|
+
|
|
398
|
+
- **You see:** nothing changes while the page is zoomed: the mode and placement stay put.
|
|
399
|
+
- **Cause:** with `visualViewport.scale` not 1, `innerWidth` is in zoomed px, so the window signature is unreadable.
|
|
400
|
+
- **Duet does:** keeps the last context (`confidence: "held"`) until the scale returns to 1. Column mode also requires scale 1, so a page that loads zoomed starts `inline`.
|
|
401
|
+
- **You do:** nothing.
|
|
402
|
+
- **Status:** see [Which mode](#which-mode).
|
|
403
|
+
|
|
404
|
+
### Safari's URL bar collapses
|
|
405
|
+
|
|
406
|
+
- **You see:** `100vh` and `innerHeight` grow and shrink by 18px while the user scrolls, so full-screen sections jump.
|
|
407
|
+
- **Cause:** Safari collapses its URL bar on scroll, which changes the page height.
|
|
408
|
+
- **Duet does:** reports `useDuet().stableHeight`, the smallest page height seen at this window width and orientation, and sets it as `--duet-vh`. In column mode the page runs on under the floating URL bar, and `--duet-inset-bottom` reserves that space at the end of the scroller.
|
|
409
|
+
- **You do:** size full-screen sections with `min-height: var(--duet-vh, 100svh)`.
|
|
410
|
+
- **Status:** see [Laying out content](#laying-out-content).
|
|
411
|
+
|
|
412
|
+
### Live Activities and the top band
|
|
413
|
+
|
|
414
|
+
- **You see:** an empty gap at the top of the strip or column, even when no Live Activity runs.
|
|
415
|
+
- **Cause:** Live Activities grow the Dynamic Island down the strip and push Safari's buttons with it, and a page can't tell whether any are running.
|
|
416
|
+
- **Duet does:** always reserves the worst case measured on device (two activities). `leading` and `primary` may rise into that space before they fold, back, actions and "…" never do. Closed landscape and the in-app preview were measured with a bare island only.
|
|
417
|
+
- **You do:** nothing.
|
|
418
|
+
- **Status:** see [Claimable space](#claimable-space).
|
|
419
|
+
|
|
420
|
+
### Menus and popovers under Safari's buttons
|
|
421
|
+
|
|
422
|
+
- **You see:** without the handling, a menu that reaches into the column is drawn under Safari's back, reader or +/tabs buttons.
|
|
423
|
+
- **Cause:** Safari's native chrome always paints over the page, so `z-index` can't help.
|
|
424
|
+
- **Duet does:** keeps its own menus between `columnTop` and Safari's pill, or opens them wholly over the page when they are too tall.
|
|
425
|
+
- **You do:** keep your own popovers that cross the column below `--duet-safe-top-column` (0 outside column mode).
|
|
426
|
+
- **Status:** see [Claimable space](#claimable-space).
|
|
427
|
+
|
|
428
|
+
### Column buttons fire on touch down
|
|
429
|
+
|
|
430
|
+
- **You see:** buttons in Safari's column fire as the finger lands and show no native press state.
|
|
431
|
+
- **Cause:** touches in the column reach the page but are not hit-tested to elements outside the viewport, and Safari usually keeps the touch end.
|
|
432
|
+
- **Duet does:** listens for `pointerdown`, finds the button under the touch's coordinates and clicks it. In a split pane, the tap also tells Duet which side the pane is on.
|
|
433
|
+
- **You do:** nothing. Don't put anything that needs a long press or a drag in the column.
|
|
434
|
+
- **Status:** see [Column mode](#column-mode-how-it-works).
|
|
435
|
+
|
|
436
|
+
### Animations removed by a page reset
|
|
437
|
+
|
|
438
|
+
- **You see:** in column mode without children, the controls lag behind momentum scroll.
|
|
439
|
+
- **Cause:** a reset like `* { animation: none !important }` (common under reduced motion) removes the scroll timeline the layer follows the document on, and so does a browser without scroll-timeline support.
|
|
440
|
+
- **Duet does:** falls back to a transform set on every scroll event, which lags in momentum.
|
|
441
|
+
- **You do:** exclude `.duet` from the reset, or pass the page as `children` (the steadier choice in any case).
|
|
442
|
+
- **Status:** [PR #56](https://github.com/JayFarei/duet/pull/56). See [Column mode](#column-mode-how-it-works).
|
|
443
|
+
|
|
444
|
+
### Viewport meta and `viewport-fit=cover`
|
|
445
|
+
|
|
446
|
+
- **You see:** without the handling, Safari zooms a split pane out to fit the widened document, and `strip` mode is never detected.
|
|
447
|
+
- **Cause:** Safari shrinks the page to fit wide content unless the viewport meta says `shrink-to-fit=no`, and safe-area insets only reach the page with `viewport-fit=cover`. Frameworks add and rewrite viewport metas after load.
|
|
448
|
+
- **Duet does:** appends `shrink-to-fit=no` on import and `viewport-fit=cover` when `<Duet>` mounts, to every viewport meta, and keeps them on metas added or rewritten later. With no viewport meta it adds `width=device-width,initial-scale=1` first.
|
|
449
|
+
- **You do:** nothing, but don't set `viewport-fit=contain` or `shrink-to-fit=yes`: Duet only appends absent keys and leaves your values as they are.
|
|
450
|
+
- **Status:** 0.2.0, 0.2.1. See [Viewport meta](#viewport-meta).
|
|
451
|
+
|
|
452
|
+
### Mode flips back and forth
|
|
453
|
+
|
|
454
|
+
- **You see:** without the handling, the controls jump between the column and the page several times a second.
|
|
455
|
+
- **Cause:** column mode can feed back into the signals, for example when Safari zooms the page out to fit the wider document, the column signature disappears, `inline` removes the overflow, and the signature returns.
|
|
456
|
+
- **Duet does:** after 3 mode changes within 2s at the same window size, pauses column mode for 3s, doubling each time the loop resumes right after the pause, up to 60s. Changes while the window is still settling are not counted.
|
|
457
|
+
- **You do:** nothing.
|
|
458
|
+
- **Status:** 0.2.1. See [Oscillation guard](#oscillation-guard).
|
|
459
|
+
|
|
460
|
+
### Mode detection limits
|
|
461
|
+
|
|
462
|
+
- **You see:** `inline` where you expected the column or the strip, or a Duo layout on a device that is not a Duo.
|
|
463
|
+
- **Cause:** iOS gives a web page no API for the device, the fold or Safari's buttons. Duet infers everything from window and screen sizes measured on the Duo, safe-area insets, display mode and the user agent.
|
|
464
|
+
- **Duet does:** uses the column only in Safari (not Chrome, Firefox, Edge or other apps' browsers), never in an iframe, and gives in-app web views (a user agent without `Safari/`) `inline`. A Duo whose reported sizes differ (display zoom, a future iOS release) gets `inline`, and an iPad or another device with matching sizes may read as a Duo.
|
|
465
|
+
- **You do:** use `useDuet().duo` to switch to a Duo layout, not as proof of the device. Check `useDuet().browser` to tell why a page is `inline`. For server-rendered layouts, see [Server rendering](#server-rendering-and-hydration).
|
|
466
|
+
- **Status:** 0.2.2. See [Is this an iPhone Duo?](#is-this-an-iphone-duo) and [Which mode](#which-mode).
|
|
467
|
+
|
|
296
468
|
## Limitations
|
|
297
469
|
|
|
298
470
|
- **Duo detection is a heuristic** from screen and window sizes plus the user agent (see above). It can misfire on devices with matching sizes and miss a Duo whose reported sizes differ.
|
package/dist/chrome.d.ts
CHANGED
|
@@ -71,6 +71,12 @@ export interface DuetProps<T extends string = string> {
|
|
|
71
71
|
/** Page content. When given, it scrolls in an inner scroller and the document itself never moves, so
|
|
72
72
|
* the chrome (and in Safari the column layer) stays still during momentum scroll and rubber-banding. */
|
|
73
73
|
children?: ReactNode;
|
|
74
|
+
/** Without children, in Safari's column: Duet places the page beside the controls (default `true`). When the
|
|
75
|
+
* document spans a column on the left (a split pane, or the closed Duo turned left) the page is moved into the
|
|
76
|
+
* visible viewport with margins on `<html>`, and the insets are 0, so the page needs no padding of its own.
|
|
77
|
+
* `false` lets the page run under the column instead: it keeps clear by `--duet-inset-left/right`, through
|
|
78
|
+
* column() and bleed(). */
|
|
79
|
+
reserve?: boolean;
|
|
74
80
|
}
|
|
75
81
|
export declare function useDuet(): Context;
|
|
76
82
|
/** Tells the server whether this browser is an iPhone Duo, so it can render the Duo layout from the first byte.
|
package/dist/chrome.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { ArrowLeftRight, ChevronLeft, Ellipsis, Search, X } from "./icons.js";
|
|
5
5
|
import { glassTheme, themeVars } from "./theme.js";
|
|
6
|
-
import { DUO,
|
|
6
|
+
import { DUO, columnBand, isSplit, pageInsets, islandBox, magnet, morphBox, plan, stable } from "./geometry.js";
|
|
7
7
|
import { emulatedSignals, epoch, readSignals, settling, watchSignals } from "./signals.js";
|
|
8
8
|
import { GlassProvider } from "./glassyui/glass-provider.js";
|
|
9
9
|
import { GlassSurface } from "./glassyui/glass-surface.js";
|
|
@@ -201,7 +201,24 @@ function useColumnLayer(on, split, left, column, inColumn) {
|
|
|
201
201
|
sizes.observe(root);
|
|
202
202
|
sizes.observe(document.body);
|
|
203
203
|
}
|
|
204
|
-
|
|
204
|
+
// The layer's positions are document coordinates, from an absolute box at the document's left edge. A page whose
|
|
205
|
+
// <body> (or <html>) is positioned makes that its containing block, which body margins, or the margins that move
|
|
206
|
+
// the page beside a left column (pageInsets), move off the document's edge: measured, and undone with
|
|
207
|
+
// `translate` (the scroll timeline animates `transform`, which composes with it).
|
|
208
|
+
const align = () => {
|
|
209
|
+
refs.forEach((ref, i) => {
|
|
210
|
+
const el = ref.current;
|
|
211
|
+
if (!el)
|
|
212
|
+
return;
|
|
213
|
+
el.style.translate = "";
|
|
214
|
+
const off = Math.round(el.getBoundingClientRect().left + scrollX - (split && i ? column : 0));
|
|
215
|
+
if (off)
|
|
216
|
+
el.style.translate = `${-off}px 0`;
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
const follow = (e) => {
|
|
220
|
+
if (e?.type !== "scroll")
|
|
221
|
+
align();
|
|
205
222
|
// The layer widens the document, and a swipe that runs off the end of a sideways scroller chains to it
|
|
206
223
|
// (touch-action stops at the scroller), leaving page and controls panned off their rails: hold it.
|
|
207
224
|
// An emulating host already frames both columns; pinning there loops in iOS Safari, whose frame overflows a few px.
|
|
@@ -243,6 +260,7 @@ function useColumnLayer(on, split, left, column, inColumn) {
|
|
|
243
260
|
sizes?.disconnect();
|
|
244
261
|
for (const el of els) {
|
|
245
262
|
el.style.transform = "";
|
|
263
|
+
el.style.translate = "";
|
|
246
264
|
el.style.removeProperty("--duet-scroll-max");
|
|
247
265
|
delete el.dataset.follow;
|
|
248
266
|
}
|
|
@@ -385,20 +403,26 @@ export function Duet(props) {
|
|
|
385
403
|
// The copy whose column opened the menu draws it; the other copy's column is in the other pane.
|
|
386
404
|
const [menuSide, setMenuSide] = useState(ctx.side);
|
|
387
405
|
const inner = p.children !== undefined; // the inner scroller, which runs on under Safari's column
|
|
406
|
+
const reserve = p.reserve !== false;
|
|
388
407
|
useEffect(() => {
|
|
389
408
|
// The page spans the window and the controls float over it; these insets are how far text and buttons keep
|
|
390
409
|
// clear on each edge (layout.ts). In Safari's column the scroller runs on under the column (an emulated
|
|
391
|
-
// column's frame already spans it), so
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// left column has the document scrolled one column across, so the page runs under it.
|
|
410
|
+
// column's frame already spans it), so the inset there is the column's width; a tab has no safe areas, so the
|
|
411
|
+
// other edge is 0. Without children the document is the page, and Duet places it beside the column itself
|
|
412
|
+
// (pageInsets): nothing for the page to do.
|
|
395
413
|
const r = document.documentElement.style;
|
|
396
414
|
// Controls hidden for good in this window (the turned preview) take no room; hidden while moving keeps the
|
|
397
415
|
// last insets, so the page does not reflow twice in one turn.
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
416
|
+
const { left, right, shift } = pageInsets({
|
|
417
|
+
mode: ctx.mode, side: ctx.side, split, screenWidth: ctx.screenWidth, width: ctx.width, hiddenPreview: ctx.hidden === "preview",
|
|
418
|
+
inner, emulated: !!emulatedSignals(), reserve, env: envInset,
|
|
419
|
+
});
|
|
420
|
+
// The document is scrolled one column across (useColumnLayer): margins on <html> move the page into the visible
|
|
421
|
+
// viewport. <html>'s own margins, so the page's body margins, widths and max-widths are untouched, centred
|
|
422
|
+
// content centres in the visible pane, and position: fixed (already viewport relative) is unaffected.
|
|
423
|
+
r.marginLeft = shift ? `${shift}px` : "";
|
|
424
|
+
r.marginRight = shift ? `${-shift}px` : "";
|
|
425
|
+
document.documentElement.toggleAttribute("data-duet-reserve", !!shift);
|
|
402
426
|
// --duet-edge-*: the window's insets, kept for panes, which set --duet-inset-* to their own.
|
|
403
427
|
for (const [k, v] of [["right", right], ["left", left]]) {
|
|
404
428
|
r.setProperty(`--duet-inset-${k}`, v);
|
|
@@ -419,7 +443,13 @@ export function Duet(props) {
|
|
|
419
443
|
document.documentElement.dataset.duetMode = ctx.mode;
|
|
420
444
|
document.documentElement.toggleAttribute("data-duet-emulated", !!emulatedSignals());
|
|
421
445
|
document.documentElement.toggleAttribute("data-duet-split", split);
|
|
422
|
-
}, [ctx.mode, ctx.side, ctx.hidden === "preview", ctx.screenWidth, ctx.screenHeight, ctx.width, ctx.height, ctx.stableHeight, split, inner]);
|
|
446
|
+
}, [ctx.mode, ctx.side, ctx.hidden === "preview", ctx.screenWidth, ctx.screenHeight, ctx.width, ctx.height, ctx.stableHeight, split, inner, reserve]);
|
|
447
|
+
useEffect(() => () => {
|
|
448
|
+
const h = document.documentElement;
|
|
449
|
+
h.style.marginLeft = "";
|
|
450
|
+
h.style.marginRight = "";
|
|
451
|
+
h.removeAttribute("data-duet-reserve");
|
|
452
|
+
}, []);
|
|
423
453
|
// Mid-turn the layer is clipped out of the document (duet.css) and lets go of the sideways scroll; the landing
|
|
424
454
|
// takes it up again, scrolled one column across for a left column.
|
|
425
455
|
const [nav, nav2] = useColumnLayer(ctx.mode === "column" && ctx.hidden !== "moving", split, ctx.side === "left", column, ctx.mode === "column");
|
|
@@ -600,5 +630,5 @@ export function Duet(props) {
|
|
|
600
630
|
// are masked out under the lens (CSS), so only the enlarged copy shows through it.
|
|
601
631
|
_jsx("span", { className: "duet-lens", "aria-hidden": true, children: _jsx("span", { className: "duet-lens-view", children: _jsx("span", { className: "duet-lens-col", children: tabItems.map((t, i) => _jsx("span", { className: "duet-tab", "data-on": i === lensAt || undefined, style: tint(t), children: t.icon }, t.id)) }) }) })), !pl.tabs.min && _jsx("span", { className: "duet-pill-badges", "aria-hidden": true, children: tabItems.map((t) => _jsx("span", { children: badge(t) }, t.id)) }), pl.tabs.min ? tabItems.filter((t) => t.id === selected).map((t) => tabButton(t, true, 0)) : tabItems.map((t, i) => tabButton(t, false, i))] })), pl.searchButton && _jsx(GlassSurface, { preset: "control", className: "duet-glass duet-round", "data-duet-slot": "search", style: at(pl.searchButton), children: searchButton }), (!split || side === menuSide) && menuEl("more", anchor.more, more, false, menuAt, morphAt), p.tabs && (!split || side === menuSide) && menuEl("tabs", anchor.tabs, tabItems, true, menuAt, morphAt), p.accessory && pl.accessory && !i && (_jsx(GlassSurface, { preset: "control", className: "duet-glass duet-accessory", "data-duet-slot": "accessory", style: pl.accessory, "data-covered": menu === "tabs" || undefined, children: p.accessory }))] }, side));
|
|
602
632
|
};
|
|
603
|
-
return (_jsxs(GlassProvider, { defaultTheme: glassTheme(p.appearance, p.dark), children: [p.children !== undefined && _jsx("div", { ref: scroller, className: "duet-scroller", children: p.children }), sides.map(bar)] }));
|
|
633
|
+
return (_jsxs(GlassProvider, { defaultTheme: glassTheme(p.appearance, p.dark), children: [p.children !== undefined && _jsx("div", { ref: scroller, className: "duet-scroller", children: p.children }), sides.map(bar), ctx.mode === "column" && !emulatedSignals() && sides.map((side) => _jsx("span", { className: "duet-guard", "data-side": side, "aria-hidden": true }, side))] }));
|
|
604
634
|
}
|
package/dist/duet.css
CHANGED
|
@@ -254,9 +254,10 @@ html:has(> body .duet-scroller), html:has(> body .duet-scroller) > body { height
|
|
|
254
254
|
.duet-scroller { position: fixed; inset: 0; overflow-y: auto; overscroll-behavior: contain; box-sizing: border-box; }
|
|
255
255
|
/* The page spans the whole window and the controls float over it (blueprint rules 14/15): backgrounds run on
|
|
256
256
|
under the controls, and only text and buttons keep clear, by the insets (--duet-inset-left/right), through
|
|
257
|
-
column() or bleed(). Safari's column
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
column() or bleed(). Safari's column paints in-document content that overflows the layout viewport, but no
|
|
258
|
+
fixed box reaches it: a fixed box stops at the viewport edge, and one covering the middle of that edge has its
|
|
259
|
+
colour painted over the column instead (the guard below). So there the scroller is an in-document box that runs
|
|
260
|
+
on under the column. The document still never scrolls, so the chrome layer never moves. */
|
|
260
261
|
html[data-duet-mode="column"] .duet-scroller { position: absolute; width: calc(100% + var(--duet-inset-left) + var(--duet-inset-right)); bottom: auto; height: calc(100% + var(--duet-inset-bottom)); }
|
|
261
262
|
/* That overflow widens the document, and touch-action on <html> stops at this scroll container, so without its own
|
|
262
263
|
a sideways swipe pans the whole page, chrome and all, into the column. Carousels inside are scrollers of their own
|
|
@@ -283,6 +284,22 @@ html[data-duet-mode="column"][data-duet-emulated] .duet-scroller { width: 100%;
|
|
|
283
284
|
So from focus, before the zoom and the keyboard, the layer is clipped out of the document width. */
|
|
284
285
|
html:has(:read-write:focus) .duet[data-mode="column"] { overflow: clip; visibility: hidden; }
|
|
285
286
|
.duet[data-mode="column"] { position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 50; }
|
|
287
|
+
/* A page's full-screen fixed overlay (a modal scrim, `position: fixed; inset: 0`) would hide the whole column,
|
|
288
|
+
controls included: Safari 26 hit-tests the middle of each viewport edge, 4px in, for a fixed or sticky box
|
|
289
|
+
(LocalFrameView::fixedContainerEdges) and, when one covers it, paints that box's colour, or plain chrome behind a
|
|
290
|
+
backdrop-filter, over the inset beyond that edge, Safari's column included, instead of the page's in-document
|
|
291
|
+
content. The guard is a small fixed box straddling that point on the column's edge, above every page layer, so
|
|
292
|
+
the hit test lands on it instead: too small to count as a container itself, it has none above it, and the column
|
|
293
|
+
keeps showing the controls. That hit test skips layers with nothing fixed or sticky in them, so the guard is fixed
|
|
294
|
+
and outside the layer; it keeps a (transparent) background, since a box with no background, no backdrop and no
|
|
295
|
+
children counts as transparent and the hit test then retries without it, honouring pointer-events. It takes no
|
|
296
|
+
taps (simulator, 2026-10-27, #79). */
|
|
297
|
+
.duet-guard {
|
|
298
|
+
position: fixed; z-index: 2147483647; top: 50%; width: 16px; height: 64px; margin-top: -32px; pointer-events: none;
|
|
299
|
+
background-image: linear-gradient(transparent, transparent);
|
|
300
|
+
}
|
|
301
|
+
.duet-guard[data-side="right"] { right: -8px; }
|
|
302
|
+
.duet-guard[data-side="left"] { left: -8px; }
|
|
286
303
|
/* Mid-turn or mid-fold the layer still carries the last posture's positions (the old screen's column), which would
|
|
287
304
|
widen the document past the new window, and Safari then zooms the page to fit it (scale 1.14 unfolding from
|
|
288
305
|
closed landscape into the tall window, e2e round 2). Clipped, it adds no width until the window lands. */
|
|
@@ -292,6 +309,11 @@ html:has(:read-write:focus) .duet[data-mode="column"] { overflow: clip; visibili
|
|
|
292
309
|
data-follow and --duet-scroll-max, the document's scroll range, or a transform where this is unsupported). */
|
|
293
310
|
.duet[data-mode="column"][data-follow] { animation: duet-follow linear both; animation-timeline: scroll(root block); }
|
|
294
311
|
@keyframes duet-follow { from { transform: translateY(0); } to { transform: translateY(var(--duet-scroll-max, 0px)); } }
|
|
312
|
+
/* The rubber band at either end moves the document past its scroll range, where the timeline (clamped to it) cannot
|
|
313
|
+
follow, so the layer rode the bounce: up to 160pt over Safari's own column buttons, or out of the column (#76). The
|
|
314
|
+
document there does not rubber-band, like a page with children, whose document never scrolls. Zero specificity, so
|
|
315
|
+
a page's own overscroll-behavior wins. */
|
|
316
|
+
:where(html[data-duet-mode="column"]:has(> body .duet[data-follow])) { overscroll-behavior-y: none; }
|
|
295
317
|
/* A swipe that runs off the end of a sideways scroller chains to the widened document and pans it, chrome and
|
|
296
318
|
all, into the column. Zero specificity, so a page's own overscroll-behavior wins. */
|
|
297
319
|
:where(html[data-duet-mode="column"]) :where(*) { overscroll-behavior-x: contain; }
|
package/dist/geometry.d.ts
CHANGED
|
@@ -258,6 +258,31 @@ export declare function magnet(side: Side, dx: number): {
|
|
|
258
258
|
};
|
|
259
259
|
/** Width the inline strip takes from the page (same column width as Safari's). */
|
|
260
260
|
export declare const INLINE_WIDTH = 84;
|
|
261
|
+
/** Where the page goes beside the controls, and the insets left for it to keep clear by (chrome.tsx writes them on
|
|
262
|
+
* <html>). In Safari's column without children the document is the page, and Duet owns its layout (`reserve`, the
|
|
263
|
+
* default): when the document spans a column on the left (a split pane, which spans one on each side, or a left
|
|
264
|
+
* column closed) it is scrolled one column across, so the page is moved right by that column (`shift`, margins on
|
|
265
|
+
* <html>) and lands exactly in the visible viewport, beside the controls, with nothing to keep clear (insets 0).
|
|
266
|
+
* Before, a page that ignored the insets ran under the controls on the left and left a column-wide gap on the
|
|
267
|
+
* right (RegTail, split pane, 2026-10-27). With `reserve: false` the page runs under the column and keeps clear by
|
|
268
|
+
* the insets itself. With children the scroller runs on under the column and pads by the insets; an emulated
|
|
269
|
+
* column (the site's web Duo) already frames both columns and is never scrolled across. */
|
|
270
|
+
export declare function pageInsets(o: {
|
|
271
|
+
mode: Mode;
|
|
272
|
+
side: Side;
|
|
273
|
+
split: boolean;
|
|
274
|
+
screenWidth: number;
|
|
275
|
+
width: number;
|
|
276
|
+
hiddenPreview: boolean;
|
|
277
|
+
inner: boolean;
|
|
278
|
+
emulated: boolean;
|
|
279
|
+
reserve: boolean;
|
|
280
|
+
env: (side: Side) => string;
|
|
281
|
+
}): {
|
|
282
|
+
left: string;
|
|
283
|
+
right: string;
|
|
284
|
+
shift: number;
|
|
285
|
+
};
|
|
261
286
|
export interface Spec {
|
|
262
287
|
back: boolean;
|
|
263
288
|
leading: number;
|
package/dist/geometry.js
CHANGED
|
@@ -371,6 +371,30 @@ export function magnet(side, dx) {
|
|
|
371
371
|
}
|
|
372
372
|
/** Width the inline strip takes from the page (same column width as Safari's). */
|
|
373
373
|
export const INLINE_WIDTH = 84;
|
|
374
|
+
/** Where the page goes beside the controls, and the insets left for it to keep clear by (chrome.tsx writes them on
|
|
375
|
+
* <html>). In Safari's column without children the document is the page, and Duet owns its layout (`reserve`, the
|
|
376
|
+
* default): when the document spans a column on the left (a split pane, which spans one on each side, or a left
|
|
377
|
+
* column closed) it is scrolled one column across, so the page is moved right by that column (`shift`, margins on
|
|
378
|
+
* <html>) and lands exactly in the visible viewport, beside the controls, with nothing to keep clear (insets 0).
|
|
379
|
+
* Before, a page that ignored the insets ran under the controls on the left and left a column-wide gap on the
|
|
380
|
+
* right (RegTail, split pane, 2026-10-27). With `reserve: false` the page runs under the column and keeps clear by
|
|
381
|
+
* the insets itself. With children the scroller runs on under the column and pads by the insets; an emulated
|
|
382
|
+
* column (the site's web Duo) already frames both columns and is never scrolled across. */
|
|
383
|
+
export function pageInsets(o) {
|
|
384
|
+
const column = o.screenWidth - o.width, split = o.mode === "column" && o.split;
|
|
385
|
+
if (o.mode === "column" && !o.inner && !o.emulated && o.reserve)
|
|
386
|
+
return { left: "0px", right: "0px", shift: split || o.side === "left" ? column : 0 };
|
|
387
|
+
const inset = o.hiddenPreview ? "0px" : o.mode === "strip" ? o.env(o.side) : o.mode === "inline" ? `${INLINE_WIDTH}px` : `${column}px`;
|
|
388
|
+
const other = (side) => o.mode === "column" ? "0px" : o.env(side);
|
|
389
|
+
// Without children nothing runs under a right column: the document is laid out in the viewport (simulator,
|
|
390
|
+
// 2026-10-26: 84px of padding left a 100px gap).
|
|
391
|
+
const under = o.mode !== "column" || o.inner || o.emulated;
|
|
392
|
+
return {
|
|
393
|
+
right: (o.side === "right" || split) && under ? inset : other("right"),
|
|
394
|
+
left: o.side === "left" || split ? inset : other("left"),
|
|
395
|
+
shift: 0,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
374
398
|
/** Closed landscape Safari tab: its column's buttons sit apart from portrait's (SAFARI.landscape). */
|
|
375
399
|
const landscapeColumn = (c) => !c.split && !c.preview && c.screenWidth > c.screenHeight;
|
|
376
400
|
const groupHeight = (n) => DUO.control + (n - 1) * DUO.groupItem;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { epoch, readSignals, settling, watchSignals } from "./signals.ts";
|
|
|
4
4
|
export { bleed, column } from "./layout.ts";
|
|
5
5
|
export { DuetPanes, DuetPaneBar, usePanes, type DuetPanesProps, type DuetPaneBarProps } from "./duet-panes.tsx";
|
|
6
6
|
export { panes, backFrom, type PaneWindow } from "./panes.ts";
|
|
7
|
-
export { browserOf, detect, deviceOf, isDuo, isIOS, isPreview, isSplit, previewReason, plan, stable, PIN_MAX, PIN_TTL, columnBand, morphBox, islandBox, 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";
|
|
7
|
+
export { browserOf, detect, deviceOf, isDuo, isIOS, isPreview, isSplit, previewReason, plan, stable, PIN_MAX, PIN_TTL, columnBand, morphBox, islandBox, magnet, pageInsets, 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
|
@@ -4,4 +4,4 @@ export { epoch, readSignals, settling, watchSignals } from "./signals.js";
|
|
|
4
4
|
export { bleed, column } from "./layout.js";
|
|
5
5
|
export { DuetPanes, DuetPaneBar, usePanes } from "./duet-panes.js";
|
|
6
6
|
export { panes, backFrom } from "./panes.js";
|
|
7
|
-
export { browserOf, detect, deviceOf, isDuo, isIOS, isPreview, isSplit, previewReason, plan, stable, PIN_MAX, PIN_TTL, columnBand, morphBox, islandBox, magnet, DUO, INLINE_WIDTH, MAGNET, WIDE } from "./geometry.js";
|
|
7
|
+
export { browserOf, detect, deviceOf, isDuo, isIOS, isPreview, isSplit, previewReason, plan, stable, PIN_MAX, PIN_TTL, columnBand, morphBox, islandBox, magnet, pageInsets, 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.4.0-rc.
|
|
3
|
+
"version": "0.4.0-rc.2",
|
|
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",
|