@ttt-productions/mobile-core 0.0.1 → 0.0.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.
Files changed (73) hide show
  1. package/dist/env.d.ts +5 -0
  2. package/dist/env.d.ts.map +1 -0
  3. package/{src/env.ts → dist/env.js} +1 -0
  4. package/dist/env.js.map +1 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/{src/index.ts → dist/index.js} +1 -5
  8. package/dist/index.js.map +1 -0
  9. package/dist/ios/useIosSafariFixes.d.ts +8 -0
  10. package/dist/ios/useIosSafariFixes.d.ts.map +1 -0
  11. package/{src/ios/useIosSafariFixes.ts → dist/ios/useIosSafariFixes.js} +7 -7
  12. package/dist/ios/useIosSafariFixes.js.map +1 -0
  13. package/dist/ios/useNoRubberBand.d.ts +6 -0
  14. package/dist/ios/useNoRubberBand.d.ts.map +1 -0
  15. package/dist/ios/useNoRubberBand.js +19 -0
  16. package/dist/ios/useNoRubberBand.js.map +1 -0
  17. package/dist/keyboard/KeyboardAvoidingView.d.ts +18 -0
  18. package/dist/keyboard/KeyboardAvoidingView.d.ts.map +1 -0
  19. package/dist/keyboard/KeyboardAvoidingView.js +11 -0
  20. package/dist/keyboard/KeyboardAvoidingView.js.map +1 -0
  21. package/dist/keyboard/focusOrder.d.ts +2 -0
  22. package/dist/keyboard/focusOrder.d.ts.map +1 -0
  23. package/dist/keyboard/focusOrder.js +18 -0
  24. package/dist/keyboard/focusOrder.js.map +1 -0
  25. package/dist/keyboard/useInputNavigation.d.ts +17 -0
  26. package/dist/keyboard/useInputNavigation.d.ts.map +1 -0
  27. package/dist/keyboard/useInputNavigation.js +35 -0
  28. package/dist/keyboard/useInputNavigation.js.map +1 -0
  29. package/dist/keyboard/useKeepFocusedInputVisible.d.ts +9 -0
  30. package/dist/keyboard/useKeepFocusedInputVisible.d.ts.map +1 -0
  31. package/dist/keyboard/useKeepFocusedInputVisible.js +36 -0
  32. package/dist/keyboard/useKeepFocusedInputVisible.js.map +1 -0
  33. package/dist/keyboard/useKeyboard.d.ts +8 -0
  34. package/dist/keyboard/useKeyboard.d.ts.map +1 -0
  35. package/dist/keyboard/useKeyboard.js +66 -0
  36. package/dist/keyboard/useKeyboard.js.map +1 -0
  37. package/dist/safe-area/SafeArea.d.ts +10 -0
  38. package/dist/safe-area/SafeArea.d.ts.map +1 -0
  39. package/dist/safe-area/SafeArea.js +11 -0
  40. package/dist/safe-area/SafeArea.js.map +1 -0
  41. package/dist/safe-area/useSafeAreaInsets.d.ts +7 -0
  42. package/dist/safe-area/useSafeAreaInsets.d.ts.map +1 -0
  43. package/dist/safe-area/useSafeAreaInsets.js +38 -0
  44. package/dist/safe-area/useSafeAreaInsets.js.map +1 -0
  45. package/dist/scroll/useScrollLock.d.ts +7 -0
  46. package/dist/scroll/useScrollLock.d.ts.map +1 -0
  47. package/dist/scroll/useScrollLock.js +35 -0
  48. package/dist/scroll/useScrollLock.js.map +1 -0
  49. package/dist/types.d.ts +12 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/viewport/useViewportHeightVars.d.ts +10 -0
  54. package/dist/viewport/useViewportHeightVars.d.ts.map +1 -0
  55. package/dist/viewport/useViewportHeightVars.js +30 -0
  56. package/dist/viewport/useViewportHeightVars.js.map +1 -0
  57. package/dist/viewport/useVisualViewport.d.ts +11 -0
  58. package/dist/viewport/useVisualViewport.d.ts.map +1 -0
  59. package/dist/viewport/useVisualViewport.js +36 -0
  60. package/dist/viewport/useVisualViewport.js.map +1 -0
  61. package/package.json +41 -18
  62. package/src/ios/useNoRubberBand.ts +0 -20
  63. package/src/keyboard/KeyboardAvoidingView.tsx +0 -24
  64. package/src/keyboard/focusOrder.ts +0 -20
  65. package/src/keyboard/useInputNavigation.ts +0 -44
  66. package/src/keyboard/useKeepFocusedInputVisible.ts +0 -41
  67. package/src/keyboard/useKeyboard.ts +0 -74
  68. package/src/safe-area/SafeArea.tsx +0 -23
  69. package/src/safe-area/useSafeAreaInsets.ts +0 -44
  70. package/src/scroll/useScrollLock.ts +0 -37
  71. package/src/types.ts +0 -8
  72. package/src/viewport/useViewportHeightVars.ts +0 -32
  73. package/src/viewport/useVisualViewport.ts +0 -42
@@ -0,0 +1,30 @@
1
+ import { useEffect } from "react";
2
+ import { isBrowser } from "../env";
3
+ /**
4
+ * Sets:
5
+ * --ttt-vh: 1% of *layout* viewport height (window.innerHeight)
6
+ * --ttt-dvh: 1% of *visual* viewport height (visualViewport.height when available)
7
+ *
8
+ * Use in CSS:
9
+ * height: calc(var(--ttt-dvh, var(--ttt-vh, 1vh)) * 100);
10
+ */
11
+ export function useViewportHeightVars() {
12
+ useEffect(() => {
13
+ if (!isBrowser)
14
+ return;
15
+ const apply = () => {
16
+ const vh = window.innerHeight * 0.01;
17
+ document.documentElement.style.setProperty("--ttt-vh", `${vh}px`);
18
+ const dvh = (window.visualViewport?.height ?? window.innerHeight) * 0.01;
19
+ document.documentElement.style.setProperty("--ttt-dvh", `${dvh}px`);
20
+ };
21
+ apply();
22
+ window.addEventListener("resize", apply, { passive: true });
23
+ window.visualViewport?.addEventListener("resize", apply, { passive: true });
24
+ return () => {
25
+ window.removeEventListener("resize", apply);
26
+ window.visualViewport?.removeEventListener("resize", apply);
27
+ };
28
+ }, []);
29
+ }
30
+ //# sourceMappingURL=useViewportHeightVars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useViewportHeightVars.js","sourceRoot":"","sources":["../../src/viewport/useViewportHeightVars.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB;IACnC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;YACrC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;YAElE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,IAAI,MAAM,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;YACzE,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC;QACtE,CAAC,CAAC;QAEF,KAAK,EAAE,CAAC;QACR,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,MAAM,CAAC,cAAc,EAAE,gBAAgB,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAC5C,MAAM,CAAC,cAAc,EAAE,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC9D,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare function useVisualViewport(): {
2
+ width: number;
3
+ height: number;
4
+ scale: number;
5
+ offsetTop: number;
6
+ offsetLeft: number;
7
+ pageTop: number;
8
+ pageLeft: number;
9
+ visualViewport: VisualViewport | null;
10
+ };
11
+ //# sourceMappingURL=useVisualViewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVisualViewport.d.ts","sourceRoot":"","sources":["../../src/viewport/useVisualViewport.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB;;;;;;;;;EAsChC"}
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from "react";
2
+ import { hasVisualViewport, isBrowser } from "../env";
3
+ export function useVisualViewport() {
4
+ const vv = isBrowser && hasVisualViewport ? window.visualViewport : null;
5
+ const [state, setState] = useState(() => ({
6
+ width: vv?.width ?? 0,
7
+ height: vv?.height ?? 0,
8
+ scale: vv?.scale ?? 1,
9
+ offsetTop: vv?.offsetTop ?? 0,
10
+ offsetLeft: vv?.offsetLeft ?? 0,
11
+ pageTop: vv?.pageTop ?? 0,
12
+ pageLeft: vv?.pageLeft ?? 0,
13
+ }));
14
+ useEffect(() => {
15
+ if (!vv)
16
+ return;
17
+ const onChange = () => setState({
18
+ width: vv.width,
19
+ height: vv.height,
20
+ scale: vv.scale,
21
+ offsetTop: vv.offsetTop,
22
+ offsetLeft: vv.offsetLeft,
23
+ pageTop: vv.pageTop,
24
+ pageLeft: vv.pageLeft,
25
+ });
26
+ onChange();
27
+ vv.addEventListener("resize", onChange, { passive: true });
28
+ vv.addEventListener("scroll", onChange, { passive: true });
29
+ return () => {
30
+ vv.removeEventListener("resize", onChange);
31
+ vv.removeEventListener("scroll", onChange);
32
+ };
33
+ }, [vv]);
34
+ return { visualViewport: vv, ...state };
35
+ }
36
+ //# sourceMappingURL=useVisualViewport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVisualViewport.js","sourceRoot":"","sources":["../../src/viewport/useVisualViewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEtD,MAAM,UAAU,iBAAiB;IAC/B,MAAM,EAAE,GAAG,SAAS,IAAI,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,cAAe,CAAC,CAAC,CAAC,IAAI,CAAC;IAE1E,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QACxC,KAAK,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC;QACrB,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,CAAC;QACvB,KAAK,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC;QACrB,SAAS,EAAE,EAAE,EAAE,SAAS,IAAI,CAAC;QAC7B,UAAU,EAAE,EAAE,EAAE,UAAU,IAAI,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,OAAO,IAAI,CAAC;QACzB,QAAQ,EAAE,EAAE,EAAE,QAAQ,IAAI,CAAC;KAC5B,CAAC,CAAC,CAAC;IAEJ,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,EAAE;YAAE,OAAO;QAEhB,MAAM,QAAQ,GAAG,GAAG,EAAE,CACpB,QAAQ,CAAC;YACP,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,SAAS,EAAE,EAAE,CAAC,SAAS;YACvB,UAAU,EAAE,EAAE,CAAC,UAAU;YACzB,OAAO,EAAE,EAAE,CAAC,OAAO;YACnB,QAAQ,EAAE,EAAE,CAAC,QAAQ;SACtB,CAAC,CAAC;QAEL,QAAQ,EAAE,CAAC;QACX,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3D,OAAO,GAAG,EAAE;YACV,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC3C,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAET,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC;AAC1C,CAAC"}
package/package.json CHANGED
@@ -1,20 +1,43 @@
1
1
  {
2
- "name": "@ttt-productions/mobile-core",
3
- "version": "0.0.1",
4
- "repository": {
5
- "type": "git",
6
- "url": "git+https://github.com/ttt-productions/ttt-packages.git",
7
- "directory": "packages/mobile-core"
8
- },
9
- "main": "dist/index.cjs",
10
- "module": "dist/index.mjs",
11
- "types": "dist/index.d.ts",
12
- "sideEffects": false,
13
- "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "import": "./dist/index.mjs",
17
- "require": "./dist/index.cjs"
18
- }
19
- }
2
+ "name": "@ttt-productions/mobile-core",
3
+ "version": "0.0.2",
4
+ "description": "Shared mobile utilities and responsive helpers for TTT Productions apps",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ttt-productions/ttt-packages.git",
8
+ "directory": "packages/mobile-core"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "module": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "clean": "rm -rf dist *.tsbuildinfo",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepublishOnly": "npm run clean && npm run build"
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=19.0.0",
32
+ "react-dom": ">=19.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^19.0.0",
36
+ "@types/react-dom": "^19.0.0",
37
+ "react": "^19.2.0",
38
+ "react-dom": "^19.2.0",
39
+ "typescript": "^5.8.3"
40
+ },
41
+ "author": "DJ (TTT Productions)",
42
+ "license": "MIT"
20
43
  }
@@ -1,20 +0,0 @@
1
- import { useEffect } from "react";
2
- import { isBrowser, isIOS } from "../env";
3
-
4
- /**
5
- * Prevents iOS rubber-band scroll on the document by limiting overscroll chaining.
6
- * Use sparingly (e.g., full-screen experiences).
7
- */
8
- export function useNoRubberBand(enabled: boolean) {
9
- useEffect(() => {
10
- if (!isBrowser || !isIOS || !enabled) return;
11
-
12
- const el = document.documentElement;
13
- const prev = el.style.overscrollBehaviorY as any;
14
- (el.style as any).overscrollBehaviorY = "none";
15
-
16
- return () => {
17
- (el.style as any).overscrollBehaviorY = prev;
18
- };
19
- }, [enabled]);
20
- }
@@ -1,24 +0,0 @@
1
- import React from "react";
2
- import { useKeyboard } from "./useKeyboard";
3
-
4
- type Props = React.HTMLAttributes<HTMLDivElement> & {
5
- /**
6
- * If true, adds padding-bottom equal to keyboard height while open.
7
- * Good default for forms.
8
- */
9
- padding?: boolean;
10
- /**
11
- * Adds extra px to bottom padding.
12
- */
13
- offset?: number;
14
- };
15
-
16
- /**
17
- * Desktop-safe: does nothing when keyboard is not detected.
18
- */
19
- export function KeyboardAvoidingView({ padding = true, offset = 0, style, ...rest }: Props) {
20
- const k = useKeyboard();
21
- const pb = padding && k.isOpen ? k.height + offset : 0;
22
-
23
- return <div {...rest} style={{ ...style, paddingBottom: (style as any)?.paddingBottom ?? pb }} />;
24
- }
@@ -1,20 +0,0 @@
1
- export function getFocusableInputs(root: HTMLElement | Document = document) {
2
- const el = root instanceof Document ? root : root;
3
- const list = Array.from(
4
- el.querySelectorAll<HTMLElement>(
5
- 'input, textarea, select, [contenteditable="true"], [data-ttt-input]'
6
- )
7
- ).filter((n) => !n.hasAttribute("disabled") && n.tabIndex !== -1);
8
-
9
- // DOM order is usually correct; allow explicit override
10
- list.sort((a, b) => {
11
- const ao = Number(a.getAttribute("data-input-order") ?? "0");
12
- const bo = Number(b.getAttribute("data-input-order") ?? "0");
13
- if (ao && bo) return ao - bo;
14
- if (ao) return -1;
15
- if (bo) return 1;
16
- return 0;
17
- });
18
-
19
- return list;
20
- }
@@ -1,44 +0,0 @@
1
- import { useCallback } from "react";
2
- import { getFocusableInputs } from "./focusOrder";
3
-
4
- type Options = {
5
- root?: HTMLElement | null; // scope inputs
6
- onDone?: () => void;
7
- };
8
-
9
- /**
10
- * Attach to inputs:
11
- * onKeyDown={nav.onKeyDown}
12
- * onSubmitEditing={nav.onSubmitEditing} (optional)
13
- *
14
- * Also supports explicit ordering via data-input-order.
15
- */
16
- export function useInputNavigation(opts?: Options) {
17
- const root = opts?.root ?? null;
18
-
19
- const focusNext = useCallback(() => {
20
- const scope = root ?? document;
21
- const inputs = getFocusableInputs(scope as any);
22
- const active = document.activeElement as HTMLElement | null;
23
- const idx = active ? inputs.indexOf(active) : -1;
24
- const next = inputs[idx + 1];
25
-
26
- if (next) next.focus();
27
- else opts?.onDone?.();
28
- }, [root, opts]);
29
-
30
- const onKeyDown = useCallback(
31
- (e: React.KeyboardEvent) => {
32
- if (e.key === "Enter") {
33
- // allow textarea newlines
34
- const el = e.currentTarget as HTMLElement;
35
- if (el.tagName.toLowerCase() === "textarea") return;
36
- e.preventDefault();
37
- focusNext();
38
- }
39
- },
40
- [focusNext]
41
- );
42
-
43
- return { focusNext, onKeyDown };
44
- }
@@ -1,41 +0,0 @@
1
- import { useEffect } from "react";
2
- import { isBrowser } from "../env";
3
- import { useKeyboard } from "./useKeyboard";
4
-
5
- /**
6
- * When keyboard opens, ensure focused element is visible within visual viewport.
7
- * - iOS Safari often hides the caret behind the keyboard.
8
- */
9
- export function useKeepFocusedInputVisible(opts?: {
10
- extraOffset?: number; // px
11
- scrollBehavior?: ScrollBehavior; // "smooth" | "auto"
12
- }) {
13
- const { isOpen } = useKeyboard();
14
- const extraOffset = opts?.extraOffset ?? 12;
15
- const behavior = opts?.scrollBehavior ?? "smooth";
16
-
17
- useEffect(() => {
18
- if (!isBrowser) return;
19
- if (!isOpen) return;
20
-
21
- const el = document.activeElement as HTMLElement | null;
22
- if (!el) return;
23
-
24
- // only inputs-ish
25
- const tag = el.tagName.toLowerCase();
26
- const isInput =
27
- tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable || el.hasAttribute("data-ttt-input");
28
- if (!isInput) return;
29
-
30
- const vv = window.visualViewport;
31
- const vvH = vv?.height ?? window.innerHeight;
32
- const rect = el.getBoundingClientRect();
33
- const bottom = rect.bottom;
34
- const limit = vvH - extraOffset;
35
-
36
- if (bottom > limit) {
37
- const delta = bottom - limit;
38
- window.scrollBy({ top: delta, behavior });
39
- }
40
- }, [isOpen, extraOffset, behavior]);
41
- }
@@ -1,74 +0,0 @@
1
- import { useEffect, useMemo, useState } from "react";
2
- import { hasVisualViewport, isBrowser } from "../env";
3
- import type { KeyboardState } from "../types";
4
-
5
- /**
6
- * Best effort keyboard detection:
7
- * - iOS Safari: visualViewport.height shrinks when keyboard shows
8
- * - fallback: focusin/focusout heuristics + innerHeight snapshots
9
- */
10
- export function useKeyboard(): KeyboardState {
11
- const [baseline, setBaseline] = useState<number>(() => (isBrowser ? window.innerHeight : 0));
12
- const [height, setHeight] = useState(0);
13
- const [open, setOpen] = useState(false);
14
- const [source, setSource] = useState<KeyboardState["source"]>("fallback");
15
-
16
- useEffect(() => {
17
- if (!isBrowser) return;
18
-
19
- const setBaseIfNeeded = () => {
20
- // update baseline when keyboard likely closed
21
- const vvH = window.visualViewport?.height ?? window.innerHeight;
22
- const inner = window.innerHeight;
23
- const candidate = Math.max(vvH, inner);
24
- setBaseline((b) => (Math.abs(candidate - b) > 40 ? candidate : b));
25
- };
26
-
27
- const onVV = () => {
28
- const vvH = window.visualViewport?.height ?? window.innerHeight;
29
- const delta = Math.max(0, Math.round(baseline - vvH));
30
- const isOpen = delta > 80; // threshold
31
- setSource("visualViewport");
32
- setHeight(isOpen ? delta : 0);
33
- setOpen(isOpen);
34
- if (!isOpen) setBaseIfNeeded();
35
- };
36
-
37
- const onFocusIn = () => {
38
- // baseline snapshot on focus to reduce false positives
39
- setBaseline((b) => Math.max(b, window.innerHeight));
40
- };
41
- const onFocusOut = () => {
42
- // allow baseline update shortly after blur
43
- setTimeout(setBaseIfNeeded, 50);
44
- setTimeout(() => {
45
- setOpen(false);
46
- setHeight(0);
47
- }, 250);
48
- };
49
-
50
- if (hasVisualViewport) {
51
- window.visualViewport!.addEventListener("resize", onVV, { passive: true });
52
- window.visualViewport!.addEventListener("scroll", onVV, { passive: true });
53
- onVV();
54
- }
55
-
56
- window.addEventListener("focusin", onFocusIn, { passive: true } as any);
57
- window.addEventListener("focusout", onFocusOut, { passive: true } as any);
58
- window.addEventListener("orientationchange", setBaseIfNeeded, { passive: true });
59
-
60
- setBaseIfNeeded();
61
-
62
- return () => {
63
- if (hasVisualViewport) {
64
- window.visualViewport!.removeEventListener("resize", onVV);
65
- window.visualViewport!.removeEventListener("scroll", onVV);
66
- }
67
- window.removeEventListener("focusin", onFocusIn as any);
68
- window.removeEventListener("focusout", onFocusOut as any);
69
- window.removeEventListener("orientationchange", setBaseIfNeeded);
70
- };
71
- }, [baseline]);
72
-
73
- return useMemo(() => ({ isOpen: open, height, source }), [open, height, source]);
74
- }
@@ -1,23 +0,0 @@
1
- import React from "react";
2
-
3
- type Props = React.HTMLAttributes<HTMLDivElement> & {
4
- top?: boolean;
5
- bottom?: boolean;
6
- left?: boolean;
7
- right?: boolean;
8
- };
9
-
10
- export function SafeArea({ top, bottom, left, right, style, ...rest }: Props) {
11
- return (
12
- <div
13
- {...rest}
14
- style={{
15
- ...style,
16
- paddingTop: top ? "env(safe-area-inset-top)" : (style as any)?.paddingTop,
17
- paddingBottom: bottom ? "env(safe-area-inset-bottom)" : (style as any)?.paddingBottom,
18
- paddingLeft: left ? "env(safe-area-inset-left)" : (style as any)?.paddingLeft,
19
- paddingRight: right ? "env(safe-area-inset-right)" : (style as any)?.paddingRight,
20
- }}
21
- />
22
- );
23
- }
@@ -1,44 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import type { Insets } from "../types";
3
- import { isBrowser } from "../env";
4
-
5
- /**
6
- * Uses CSS env(safe-area-inset-*) by writing them to CSS vars and reading computed values.
7
- * Works on iOS Safari; harmless elsewhere.
8
- */
9
- export function useSafeAreaInsets(): Insets {
10
- const [insets, setInsets] = useState<Insets>({ top: 0, right: 0, bottom: 0, left: 0 });
11
-
12
- useEffect(() => {
13
- if (!isBrowser) return;
14
-
15
- const el = document.documentElement;
16
-
17
- // set vars once
18
- el.style.setProperty("--ttt-sai-top", "env(safe-area-inset-top)");
19
- el.style.setProperty("--ttt-sai-right", "env(safe-area-inset-right)");
20
- el.style.setProperty("--ttt-sai-bottom", "env(safe-area-inset-bottom)");
21
- el.style.setProperty("--ttt-sai-left", "env(safe-area-inset-left)");
22
-
23
- const read = () => {
24
- const cs = getComputedStyle(el);
25
- const px = (v: string) => Math.max(0, Math.round(parseFloat(v || "0")));
26
- setInsets({
27
- top: px(cs.getPropertyValue("--ttt-sai-top")),
28
- right: px(cs.getPropertyValue("--ttt-sai-right")),
29
- bottom: px(cs.getPropertyValue("--ttt-sai-bottom")),
30
- left: px(cs.getPropertyValue("--ttt-sai-left")),
31
- });
32
- };
33
-
34
- read();
35
- window.addEventListener("resize", read, { passive: true });
36
- window.addEventListener("orientationchange", read, { passive: true });
37
- return () => {
38
- window.removeEventListener("resize", read);
39
- window.removeEventListener("orientationchange", read);
40
- };
41
- }, []);
42
-
43
- return insets;
44
- }
@@ -1,37 +0,0 @@
1
- import { useEffect } from "react";
2
- import { isBrowser } from "../env";
3
-
4
- /**
5
- * Locks body scroll (mobile sheet/modal fix).
6
- * - preserves current scroll position
7
- * - iOS safe enough for common cases
8
- */
9
- export function useScrollLock(locked: boolean) {
10
- useEffect(() => {
11
- if (!isBrowser) return;
12
- if (!locked) return;
13
-
14
- const { body } = document;
15
- const scrollY = window.scrollY;
16
-
17
- const prev = {
18
- position: body.style.position,
19
- top: body.style.top,
20
- width: body.style.width,
21
- overflowY: body.style.overflowY,
22
- };
23
-
24
- body.style.position = "fixed";
25
- body.style.top = `-${scrollY}px`;
26
- body.style.width = "100%";
27
- body.style.overflowY = "scroll";
28
-
29
- return () => {
30
- body.style.position = prev.position;
31
- body.style.top = prev.top;
32
- body.style.width = prev.width;
33
- body.style.overflowY = prev.overflowY;
34
- window.scrollTo(0, scrollY);
35
- };
36
- }, [locked]);
37
- }
package/src/types.ts DELETED
@@ -1,8 +0,0 @@
1
- export type Insets = { top: number; right: number; bottom: number; left: number };
2
-
3
- export type KeyboardState = {
4
- isOpen: boolean;
5
- height: number; // px
6
- // best-effort: iOS visualViewport + focus heuristics
7
- source: "visualViewport" | "fallback";
8
- };
@@ -1,32 +0,0 @@
1
- import { useEffect } from "react";
2
- import { isBrowser } from "../env";
3
-
4
- /**
5
- * Sets:
6
- * --ttt-vh: 1% of *layout* viewport height (window.innerHeight)
7
- * --ttt-dvh: 1% of *visual* viewport height (visualViewport.height when available)
8
- *
9
- * Use in CSS:
10
- * height: calc(var(--ttt-dvh, var(--ttt-vh, 1vh)) * 100);
11
- */
12
- export function useViewportHeightVars() {
13
- useEffect(() => {
14
- if (!isBrowser) return;
15
-
16
- const apply = () => {
17
- const vh = window.innerHeight * 0.01;
18
- document.documentElement.style.setProperty("--ttt-vh", `${vh}px`);
19
-
20
- const dvh = (window.visualViewport?.height ?? window.innerHeight) * 0.01;
21
- document.documentElement.style.setProperty("--ttt-dvh", `${dvh}px`);
22
- };
23
-
24
- apply();
25
- window.addEventListener("resize", apply, { passive: true });
26
- window.visualViewport?.addEventListener("resize", apply, { passive: true });
27
- return () => {
28
- window.removeEventListener("resize", apply);
29
- window.visualViewport?.removeEventListener("resize", apply);
30
- };
31
- }, []);
32
- }
@@ -1,42 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { hasVisualViewport, isBrowser } from "../env";
3
-
4
- export function useVisualViewport() {
5
- const vv = isBrowser && hasVisualViewport ? window.visualViewport! : null;
6
-
7
- const [state, setState] = useState(() => ({
8
- width: vv?.width ?? 0,
9
- height: vv?.height ?? 0,
10
- scale: vv?.scale ?? 1,
11
- offsetTop: vv?.offsetTop ?? 0,
12
- offsetLeft: vv?.offsetLeft ?? 0,
13
- pageTop: vv?.pageTop ?? 0,
14
- pageLeft: vv?.pageLeft ?? 0,
15
- }));
16
-
17
- useEffect(() => {
18
- if (!vv) return;
19
-
20
- const onChange = () =>
21
- setState({
22
- width: vv.width,
23
- height: vv.height,
24
- scale: vv.scale,
25
- offsetTop: vv.offsetTop,
26
- offsetLeft: vv.offsetLeft,
27
- pageTop: vv.pageTop,
28
- pageLeft: vv.pageLeft,
29
- });
30
-
31
- onChange();
32
- vv.addEventListener("resize", onChange, { passive: true });
33
- vv.addEventListener("scroll", onChange, { passive: true });
34
-
35
- return () => {
36
- vv.removeEventListener("resize", onChange);
37
- vv.removeEventListener("scroll", onChange);
38
- };
39
- }, [vv]);
40
-
41
- return { visualViewport: vv, ...state };
42
- }