@glassly/miniapp 0.3.7-dev.894 → 0.3.7-dev.895

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/globals.d.ts CHANGED
@@ -16,6 +16,12 @@ export interface MiniappCapsuleMenuRect {
16
16
  left: number;
17
17
  width: number;
18
18
  height: number;
19
+ /**
20
+ * `corner` floats over the page's top-right; `rail` sits in the iPhone
21
+ * Duo's status rail, inside the side safe-area inset and clear of the page.
22
+ * Absent on hosts older than the field.
23
+ */
24
+ placement?: "corner" | "rail";
19
25
  }
20
26
  export type MiniappColorScheme = "light" | "dark";
21
27
  export interface HostGlobals {
@@ -29,12 +35,18 @@ export interface HostGlobals {
29
35
  miniappLocal?: boolean;
30
36
  /** True when the miniapp is running via the dev workflow (QR sideload). */
31
37
  miniappDeveloperMode?: boolean;
32
- /** Safe-area insets around the WebView content. Miniapps should pad accordingly. */
38
+ /**
39
+ * Safe-area insets around the WebView content. Miniapps should pad
40
+ * accordingly. On an iPhone Duo the status rail is an 84px side inset. The
41
+ * host updates this and dispatches `glassly:safe-area-change` when they
42
+ * change (a Duo folding or unfolding).
43
+ */
33
44
  safeAreaInsets?: MiniappSafeAreaInsets;
34
45
  /**
35
- * Bounding rect (in CSS pixels) of the floating capsule menu the host draws
36
- * over the WebView. The menu lives in the top-right and miniapps should
37
- * avoid placing interactive elements beneath it.
46
+ * Bounding rect (in CSS pixels) of the capsule menu the host draws over the
47
+ * WebView: the top-right corner, or the Duo's status rail (see `placement`).
48
+ * Miniapps should avoid placing interactive elements beneath it. Updated
49
+ * with `safeAreaInsets`.
38
50
  */
39
51
  capsuleMenu?: MiniappCapsuleMenuRect;
40
52
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAA;AAEjD,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,qFAAqF;IACrF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,2EAA2E;IAC3E,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,oFAAoF;IACpF,cAAc,CAAC,EAAE,qBAAqB,CAAA;IACtC;;;;OAIG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,OAAO,CAAC,EAAE,WAAW,CAAA;KACtB;CACF;AAED,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,WAAW,CAG5C"}
1
+ {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC9B;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAA;AAEjD,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,qFAAqF;IACrF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,2EAA2E;IAC3E,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,qBAAqB,CAAA;IACtC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,OAAO,CAAC,EAAE,WAAW,CAAA;KACtB;CACF;AAED,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,WAAW,CAG5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"globals.js","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqDH,wEAAwE;AACxE,MAAM,UAAU,cAAc;IAC5B,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,CAAA;IAC5C,OAAO,MAAM,CAAC,OAAO,IAAI,EAAE,CAAA;AAC7B,CAAC"}
1
+ {"version":3,"file":"globals.js","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiEH,wEAAwE;AACxE,MAAM,UAAU,cAAc;IAC5B,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,CAAA;IAC5C,OAAO,MAAM,CAAC,OAAO,IAAI,EAAE,CAAA;AAC7B,CAAC"}
@@ -1,3 +1,10 @@
1
- import type { MiniappCapsuleMenuRect } from "../globals";
1
+ import type { MiniappCapsuleMenuRect, MiniappSafeAreaInsets } from "../globals";
2
+ /**
3
+ * Whether the capsule menu floats over the page content: false when it sits
4
+ * in a side safe-area inset (the iPhone Duo's status rail), where it covers
5
+ * nothing the page lays out. Hosts that predate `placement` report a corner
6
+ * rect on the Duo, which lands inside the rail inset all the same.
7
+ */
8
+ export declare function capsuleOverContent(capsuleMenu: MiniappCapsuleMenuRect, viewportWidth: number, insets: MiniappSafeAreaInsets): boolean;
2
9
  export declare function getCapsuleHeaderPaddingRight(capsuleMenu: MiniappCapsuleMenuRect, viewportWidth: number, safeAreaRight: number, rightGap: number): number;
3
10
  //# sourceMappingURL=capsuleHeaderLayout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capsuleHeaderLayout.d.ts","sourceRoot":"","sources":["../../src/react/capsuleHeaderLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,YAAY,CAAA;AAEtD,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,sBAAsB,EACnC,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR"}
1
+ {"version":3,"file":"capsuleHeaderLayout.d.ts","sourceRoot":"","sources":["../../src/react/capsuleHeaderLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,sBAAsB,EAAE,qBAAqB,EAAC,MAAM,YAAY,CAAA;AAE7E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,sBAAsB,EACnC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAGT;AAED,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,sBAAsB,EACnC,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR"}
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Whether the capsule menu floats over the page content: false when it sits
3
+ * in a side safe-area inset (the iPhone Duo's status rail), where it covers
4
+ * nothing the page lays out. Hosts that predate `placement` report a corner
5
+ * rect on the Duo, which lands inside the rail inset all the same.
6
+ */
7
+ export function capsuleOverContent(capsuleMenu, viewportWidth, insets) {
8
+ if (capsuleMenu.placement === "rail")
9
+ return false;
10
+ return capsuleMenu.left < viewportWidth - insets.right && capsuleMenu.left + capsuleMenu.width > insets.left;
11
+ }
1
12
  export function getCapsuleHeaderPaddingRight(capsuleMenu, viewportWidth, safeAreaRight, rightGap) {
2
13
  const capsuleOffsetFromContentRight = viewportWidth - capsuleMenu.left - safeAreaRight;
3
14
  return Math.max(rightGap, capsuleOffsetFromContentRight + rightGap);
@@ -1 +1 @@
1
- {"version":3,"file":"capsuleHeaderLayout.js","sourceRoot":"","sources":["../../src/react/capsuleHeaderLayout.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,4BAA4B,CAC1C,WAAmC,EACnC,aAAqB,EACrB,aAAqB,EACrB,QAAgB;IAEhB,MAAM,6BAA6B,GAAG,aAAa,GAAG,WAAW,CAAC,IAAI,GAAG,aAAa,CAAA;IACtF,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,6BAA6B,GAAG,QAAQ,CAAC,CAAA;AACrE,CAAC"}
1
+ {"version":3,"file":"capsuleHeaderLayout.js","sourceRoot":"","sources":["../../src/react/capsuleHeaderLayout.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAmC,EACnC,aAAqB,EACrB,MAA6B;IAE7B,IAAI,WAAW,CAAC,SAAS,KAAK,MAAM;QAAE,OAAO,KAAK,CAAA;IAClD,OAAO,WAAW,CAAC,IAAI,GAAG,aAAa,GAAG,MAAM,CAAC,KAAK,IAAI,WAAW,CAAC,IAAI,GAAG,WAAW,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAA;AAC9G,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,WAAmC,EACnC,aAAqB,EACrB,aAAqB,EACrB,QAAgB;IAEhB,MAAM,6BAA6B,GAAG,aAAa,GAAG,WAAW,CAAC,IAAI,GAAG,aAAa,CAAA;IACtF,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,6BAA6B,GAAG,QAAQ,CAAC,CAAA;AACrE,CAAC"}
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * Use this when you want full control over header markup but want the
6
6
  * alignment math done for you. If you just want a stock header, use
7
- * <MiniappHeader> instead.
7
+ * <MiniappHeader> instead. When the capsule sits in the iPhone Duo's status
8
+ * rail, outside the page, the header takes the fallback height and margin
9
+ * and reserves nothing on the right.
8
10
  *
9
11
  * Example:
10
12
  * const style = useCapsuleHeaderStyle()
@@ -1 +1 @@
1
- {"version":3,"file":"useCapsuleHeaderStyle.d.ts","sourceRoot":"","sources":["../../src/react/useCapsuleHeaderStyle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAA;AAKxC,MAAM,WAAW,4BAA4B;IAC3C,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,aAAa,CAuB/F"}
1
+ {"version":3,"file":"useCapsuleHeaderStyle.d.ts","sourceRoot":"","sources":["../../src/react/useCapsuleHeaderStyle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAA;AAKxC,MAAM,WAAW,4BAA4B;IAC3C,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,aAAa,CAuB/F"}
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * Use this when you want full control over header markup but want the
6
6
  * alignment math done for you. If you just want a stock header, use
7
- * <MiniappHeader> instead.
7
+ * <MiniappHeader> instead. When the capsule sits in the iPhone Duo's status
8
+ * rail, outside the page, the header takes the fallback height and margin
9
+ * and reserves nothing on the right.
8
10
  *
9
11
  * Example:
10
12
  * const style = useCapsuleHeaderStyle()
@@ -1 +1 @@
1
- {"version":3,"file":"useCapsuleHeaderStyle.js","sourceRoot":"","sources":["../../src/react/useCapsuleHeaderStyle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAC,4BAA4B,EAAC,MAAM,uBAAuB,CAAA;AAClE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAA;AAazC,MAAM,UAAU,qBAAqB,CAAC,UAAwC,EAAE;IAC9E,MAAM,EAAC,WAAW,GAAG,EAAE,EAAE,QAAQ,GAAG,EAAE,EAAE,cAAc,GAAG,EAAE,EAAE,iBAAiB,GAAG,EAAE,EAAC,GAAG,OAAO,CAAA;IAC9F,MAAM,EAAC,MAAM,EAAE,WAAW,EAAC,GAAG,WAAW,EAAE,CAAA;IAE3C,MAAM,MAAM,GAAG,WAAW,EAAE,MAAM,IAAI,cAAc,CAAA;IACpD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,iBAAiB,GAAG,MAAM,GAAG,CAAC,CAAA;IACnH,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAA;IAClD,MAAM,YAAY,GAAG,WAAW;QAC9B,CAAC,CAAC,OAAO,MAAM,KAAK,WAAW;YAC7B,CAAC,CAAC,WAAW,CAAC,KAAK,GAAG,QAAQ;YAC9B,CAAC,CAAC,4BAA4B,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC;QACxF,CAAC,CAAC,WAAW,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,CAAC;QACb,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,eAAe;QAC/B,WAAW,EAAE,WAAW;QACxB,YAAY;QACZ,SAAS;QACT,MAAM;KACP,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"useCapsuleHeaderStyle.js","sourceRoot":"","sources":["../../src/react/useCapsuleHeaderStyle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAC,4BAA4B,EAAC,MAAM,uBAAuB,CAAA;AAClE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAA;AAazC,MAAM,UAAU,qBAAqB,CAAC,UAAwC,EAAE;IAC9E,MAAM,EAAC,WAAW,GAAG,EAAE,EAAE,QAAQ,GAAG,EAAE,EAAE,cAAc,GAAG,EAAE,EAAE,iBAAiB,GAAG,EAAE,EAAC,GAAG,OAAO,CAAA;IAC9F,MAAM,EAAC,MAAM,EAAE,WAAW,EAAC,GAAG,WAAW,EAAE,CAAA;IAE3C,MAAM,MAAM,GAAG,WAAW,EAAE,MAAM,IAAI,cAAc,CAAA;IACpD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,iBAAiB,GAAG,MAAM,GAAG,CAAC,CAAA;IACnH,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAA;IAClD,MAAM,YAAY,GAAG,WAAW;QAC9B,CAAC,CAAC,OAAO,MAAM,KAAK,WAAW;YAC7B,CAAC,CAAC,WAAW,CAAC,KAAK,GAAG,QAAQ;YAC9B,CAAC,CAAC,4BAA4B,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC;QACxF,CAAC,CAAC,WAAW,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,CAAC;QACb,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,eAAe;QAC/B,WAAW,EAAE,WAAW;QACxB,YAAY;QACZ,SAAS;QACT,MAAM;KACP,CAAA;AACH,CAAC"}
@@ -2,18 +2,24 @@
2
2
  * @fileoverview useSafeArea — React hook exposing the host's safe-area insets
3
3
  * and the capsule menu rect to miniapp UIs.
4
4
  *
5
- * The host injects these via window.Glassly before content loads. This hook
6
- * reads them once at mount — they don't currently change at runtime (the host
7
- * would have to force a reload to update them, e.g. on orientation change).
5
+ * The host injects these via window.Glassly before content loads and updates
6
+ * them when they change (an iPhone Duo folding moves its status rail),
7
+ * dispatching `glassly:safe-area-change`; the hook re-renders on that event
8
+ * and on viewport resizes.
8
9
  */
9
10
  import { type MiniappCapsuleMenuRect, type MiniappSafeAreaInsets } from "../globals";
10
11
  export interface UseSafeAreaResult {
11
- /** Pixel insets around the WebView content. Apply as padding on your root element. */
12
+ /**
13
+ * Pixel insets around the WebView content. Apply as padding on your root
14
+ * element. On an iPhone Duo the status rail is an 84px left or right inset:
15
+ * let backgrounds run under it and stop content at it.
16
+ */
12
17
  insets: MiniappSafeAreaInsets;
13
18
  /**
14
- * Bounding rect of the host's floating capsule menu (top-right overlay).
15
- * Null when the host doesn't render one (e.g. older builds). Use this to
16
- * avoid placing clickable content underneath the menu.
19
+ * Bounding rect of the host's capsule menu when it floats over the page
20
+ * (top-right). Null when the host doesn't render one (e.g. older builds) or
21
+ * draws it outside the content, in the Duo's status rail. Use this to avoid
22
+ * placing clickable content underneath the menu.
17
23
  */
18
24
  capsuleMenu: MiniappCapsuleMenuRect | null;
19
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useSafeArea.d.ts","sourceRoot":"","sources":["../../src/react/useSafeArea.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAiB,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,EAAC,MAAM,YAAY,CAAA;AAIlG,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,MAAM,EAAE,qBAAqB,CAAA;IAC7B;;;;OAIG;IACH,WAAW,EAAE,sBAAsB,GAAG,IAAI,CAAA;CAC3C;AAED,wBAAgB,WAAW,IAAI,iBAAiB,CAS/C"}
1
+ {"version":3,"file":"useSafeArea.d.ts","sourceRoot":"","sources":["../../src/react/useSafeArea.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAiB,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,EAAC,MAAM,YAAY,CAAA;AAKlG,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,EAAE,qBAAqB,CAAA;IAC7B;;;;;OAKG;IACH,WAAW,EAAE,sBAAsB,GAAG,IAAI,CAAA;CAC3C;AAmBD,wBAAgB,WAAW,IAAI,iBAAiB,CAoB/C"}
@@ -2,21 +2,50 @@
2
2
  * @fileoverview useSafeArea — React hook exposing the host's safe-area insets
3
3
  * and the capsule menu rect to miniapp UIs.
4
4
  *
5
- * The host injects these via window.Glassly before content loads. This hook
6
- * reads them once at mount — they don't currently change at runtime (the host
7
- * would have to force a reload to update them, e.g. on orientation change).
5
+ * The host injects these via window.Glassly before content loads and updates
6
+ * them when they change (an iPhone Duo folding moves its status rail),
7
+ * dispatching `glassly:safe-area-change`; the hook re-renders on that event
8
+ * and on viewport resizes.
8
9
  */
9
- import { useState } from "react";
10
+ import { useEffect, useState } from "react";
10
11
  import { getHostGlobals } from "../globals";
12
+ import { capsuleOverContent } from "./capsuleHeaderLayout";
11
13
  const EMPTY_INSETS = { top: 0, bottom: 0, left: 0, right: 0 };
14
+ function readSafeArea() {
15
+ const globals = getHostGlobals();
16
+ const insets = globals.safeAreaInsets ?? EMPTY_INSETS;
17
+ const capsuleMenu = globals.capsuleMenu ?? null;
18
+ const viewportWidth = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerWidth;
19
+ return {
20
+ insets,
21
+ capsuleMenu: capsuleMenu && capsuleOverContent(capsuleMenu, viewportWidth, insets) ? capsuleMenu : null,
22
+ };
23
+ }
24
+ function sameRect(a, b) {
25
+ if (a === b)
26
+ return true;
27
+ if (!a || !b)
28
+ return false;
29
+ return Object.keys(a).every((key) => a[key] === b[key]);
30
+ }
12
31
  export function useSafeArea() {
13
- const [result] = useState(() => {
14
- const globals = getHostGlobals();
15
- return {
16
- insets: globals.safeAreaInsets ?? EMPTY_INSETS,
17
- capsuleMenu: globals.capsuleMenu ?? null,
32
+ const [result, setResult] = useState(readSafeArea);
33
+ useEffect(() => {
34
+ const update = () => setResult((previous) => {
35
+ const next = readSafeArea();
36
+ return sameRect(previous.insets, next.insets) && sameRect(previous.capsuleMenu, next.capsuleMenu)
37
+ ? previous
38
+ : next;
39
+ });
40
+ // The host may have updated the globals between the first render and now.
41
+ update();
42
+ window.addEventListener("glassly:safe-area-change", update);
43
+ window.addEventListener("resize", update);
44
+ return () => {
45
+ window.removeEventListener("glassly:safe-area-change", update);
46
+ window.removeEventListener("resize", update);
18
47
  };
19
- });
48
+ }, []);
20
49
  return result;
21
50
  }
22
51
  //# sourceMappingURL=useSafeArea.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSafeArea.js","sourceRoot":"","sources":["../../src/react/useSafeArea.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAC,cAAc,EAA0D,MAAM,YAAY,CAAA;AAElG,MAAM,YAAY,GAA0B,EAAC,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAC,CAAA;AAalF,MAAM,UAAU,WAAW;IACzB,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAoB,GAAG,EAAE;QAChD,MAAM,OAAO,GAAG,cAAc,EAAE,CAAA;QAChC,OAAO;YACL,MAAM,EAAE,OAAO,CAAC,cAAc,IAAI,YAAY;YAC9C,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;SACzC,CAAA;IACH,CAAC,CAAC,CAAA;IACF,OAAO,MAAM,CAAA;AACf,CAAC"}
1
+ {"version":3,"file":"useSafeArea.js","sourceRoot":"","sources":["../../src/react/useSafeArea.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAC,cAAc,EAA0D,MAAM,YAAY,CAAA;AAClG,OAAO,EAAC,kBAAkB,EAAC,MAAM,uBAAuB,CAAA;AAExD,MAAM,YAAY,GAA0B,EAAC,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAC,CAAA;AAkBlF,SAAS,YAAY;IACnB,MAAM,OAAO,GAAG,cAAc,EAAE,CAAA;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,cAAc,IAAI,YAAY,CAAA;IACrD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAA;IAC/C,MAAM,aAAa,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAA;IAClG,OAAO;QACL,MAAM;QACN,WAAW,EAAE,WAAW,IAAI,kBAAkB,CAAC,WAAW,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI;KACxG,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAmB,CAAW,EAAE,CAAW;IAC1D,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACxB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1B,OAAQ,MAAM,CAAC,IAAI,CAAC,CAAC,CAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC1E,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAoB,YAAY,CAAC,CAAA;IACrE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,YAAY,EAAE,CAAA;YAC3B,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC;gBAC/F,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,IAAI,CAAA;QACV,CAAC,CAAC,CAAA;QACJ,0EAA0E;QAC1E,MAAM,EAAE,CAAA;QACR,MAAM,CAAC,gBAAgB,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAA;QAC3D,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACzC,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAA;YAC9D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QAC9C,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,OAAO,MAAM,CAAA;AACf,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/miniapp",
3
- "version": "0.3.7-dev.894",
3
+ "version": "0.3.7-dev.895",
4
4
  "description": "SDK for building Glassly local miniapps (browser/WebView)",
5
5
  "author": "Glassly Labs",
6
6
  "license": "MIT",
@@ -73,7 +73,7 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@glassly/cloud-protocol": "0.3.7-dev.894",
76
+ "@glassly/cloud-protocol": "0.3.7-dev.895",
77
77
  "eventemitter3": "^5.0.1"
78
78
  },
79
79
  "devDependencies": {
package/src/globals.ts CHANGED
@@ -18,6 +18,12 @@ export interface MiniappCapsuleMenuRect {
18
18
  left: number
19
19
  width: number
20
20
  height: number
21
+ /**
22
+ * `corner` floats over the page's top-right; `rail` sits in the iPhone
23
+ * Duo's status rail, inside the side safe-area inset and clear of the page.
24
+ * Absent on hosts older than the field.
25
+ */
26
+ placement?: "corner" | "rail"
21
27
  }
22
28
 
23
29
  export type MiniappColorScheme = "light" | "dark"
@@ -33,12 +39,18 @@ export interface HostGlobals {
33
39
  miniappLocal?: boolean
34
40
  /** True when the miniapp is running via the dev workflow (QR sideload). */
35
41
  miniappDeveloperMode?: boolean
36
- /** Safe-area insets around the WebView content. Miniapps should pad accordingly. */
42
+ /**
43
+ * Safe-area insets around the WebView content. Miniapps should pad
44
+ * accordingly. On an iPhone Duo the status rail is an 84px side inset. The
45
+ * host updates this and dispatches `glassly:safe-area-change` when they
46
+ * change (a Duo folding or unfolding).
47
+ */
37
48
  safeAreaInsets?: MiniappSafeAreaInsets
38
49
  /**
39
- * Bounding rect (in CSS pixels) of the floating capsule menu the host draws
40
- * over the WebView. The menu lives in the top-right and miniapps should
41
- * avoid placing interactive elements beneath it.
50
+ * Bounding rect (in CSS pixels) of the capsule menu the host draws over the
51
+ * WebView: the top-right corner, or the Duo's status rail (see `placement`).
52
+ * Miniapps should avoid placing interactive elements beneath it. Updated
53
+ * with `safeAreaInsets`.
42
54
  */
43
55
  capsuleMenu?: MiniappCapsuleMenuRect
44
56
  /**
@@ -1,4 +1,19 @@
1
- import type {MiniappCapsuleMenuRect} from "../globals"
1
+ import type {MiniappCapsuleMenuRect, MiniappSafeAreaInsets} from "../globals"
2
+
3
+ /**
4
+ * Whether the capsule menu floats over the page content: false when it sits
5
+ * in a side safe-area inset (the iPhone Duo's status rail), where it covers
6
+ * nothing the page lays out. Hosts that predate `placement` report a corner
7
+ * rect on the Duo, which lands inside the rail inset all the same.
8
+ */
9
+ export function capsuleOverContent(
10
+ capsuleMenu: MiniappCapsuleMenuRect,
11
+ viewportWidth: number,
12
+ insets: MiniappSafeAreaInsets,
13
+ ): boolean {
14
+ if (capsuleMenu.placement === "rail") return false
15
+ return capsuleMenu.left < viewportWidth - insets.right && capsuleMenu.left + capsuleMenu.width > insets.left
16
+ }
2
17
 
3
18
  export function getCapsuleHeaderPaddingRight(
4
19
  capsuleMenu: MiniappCapsuleMenuRect,
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * Use this when you want full control over header markup but want the
6
6
  * alignment math done for you. If you just want a stock header, use
7
- * <MiniappHeader> instead.
7
+ * <MiniappHeader> instead. When the capsule sits in the iPhone Duo's status
8
+ * rail, outside the page, the header takes the fallback height and margin
9
+ * and reserves nothing on the right.
8
10
  *
9
11
  * Example:
10
12
  * const style = useCapsuleHeaderStyle()
@@ -2,35 +2,70 @@
2
2
  * @fileoverview useSafeArea — React hook exposing the host's safe-area insets
3
3
  * and the capsule menu rect to miniapp UIs.
4
4
  *
5
- * The host injects these via window.Glassly before content loads. This hook
6
- * reads them once at mount — they don't currently change at runtime (the host
7
- * would have to force a reload to update them, e.g. on orientation change).
5
+ * The host injects these via window.Glassly before content loads and updates
6
+ * them when they change (an iPhone Duo folding moves its status rail),
7
+ * dispatching `glassly:safe-area-change`; the hook re-renders on that event
8
+ * and on viewport resizes.
8
9
  */
9
10
 
10
- import {useState} from "react"
11
+ import {useEffect, useState} from "react"
11
12
 
12
13
  import {getHostGlobals, type MiniappCapsuleMenuRect, type MiniappSafeAreaInsets} from "../globals"
14
+ import {capsuleOverContent} from "./capsuleHeaderLayout"
13
15
 
14
16
  const EMPTY_INSETS: MiniappSafeAreaInsets = {top: 0, bottom: 0, left: 0, right: 0}
15
17
 
16
18
  export interface UseSafeAreaResult {
17
- /** Pixel insets around the WebView content. Apply as padding on your root element. */
19
+ /**
20
+ * Pixel insets around the WebView content. Apply as padding on your root
21
+ * element. On an iPhone Duo the status rail is an 84px left or right inset:
22
+ * let backgrounds run under it and stop content at it.
23
+ */
18
24
  insets: MiniappSafeAreaInsets
19
25
  /**
20
- * Bounding rect of the host's floating capsule menu (top-right overlay).
21
- * Null when the host doesn't render one (e.g. older builds). Use this to
22
- * avoid placing clickable content underneath the menu.
26
+ * Bounding rect of the host's capsule menu when it floats over the page
27
+ * (top-right). Null when the host doesn't render one (e.g. older builds) or
28
+ * draws it outside the content, in the Duo's status rail. Use this to avoid
29
+ * placing clickable content underneath the menu.
23
30
  */
24
31
  capsuleMenu: MiniappCapsuleMenuRect | null
25
32
  }
26
33
 
34
+ function readSafeArea(): UseSafeAreaResult {
35
+ const globals = getHostGlobals()
36
+ const insets = globals.safeAreaInsets ?? EMPTY_INSETS
37
+ const capsuleMenu = globals.capsuleMenu ?? null
38
+ const viewportWidth = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerWidth
39
+ return {
40
+ insets,
41
+ capsuleMenu: capsuleMenu && capsuleOverContent(capsuleMenu, viewportWidth, insets) ? capsuleMenu : null,
42
+ }
43
+ }
44
+
45
+ function sameRect<T extends object>(a: T | null, b: T | null): boolean {
46
+ if (a === b) return true
47
+ if (!a || !b) return false
48
+ return (Object.keys(a) as (keyof T)[]).every((key) => a[key] === b[key])
49
+ }
50
+
27
51
  export function useSafeArea(): UseSafeAreaResult {
28
- const [result] = useState<UseSafeAreaResult>(() => {
29
- const globals = getHostGlobals()
30
- return {
31
- insets: globals.safeAreaInsets ?? EMPTY_INSETS,
32
- capsuleMenu: globals.capsuleMenu ?? null,
52
+ const [result, setResult] = useState<UseSafeAreaResult>(readSafeArea)
53
+ useEffect(() => {
54
+ const update = () =>
55
+ setResult((previous) => {
56
+ const next = readSafeArea()
57
+ return sameRect(previous.insets, next.insets) && sameRect(previous.capsuleMenu, next.capsuleMenu)
58
+ ? previous
59
+ : next
60
+ })
61
+ // The host may have updated the globals between the first render and now.
62
+ update()
63
+ window.addEventListener("glassly:safe-area-change", update)
64
+ window.addEventListener("resize", update)
65
+ return () => {
66
+ window.removeEventListener("glassly:safe-area-change", update)
67
+ window.removeEventListener("resize", update)
33
68
  }
34
- })
69
+ }, [])
35
70
  return result
36
71
  }