@simpleapps-com/augur-hooks 0.1.0 → 0.1.1

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/web.cjs CHANGED
@@ -10,6 +10,7 @@ var THROTTLE_MS = 150;
10
10
  var isMobileState = false;
11
11
  var listeners = /* @__PURE__ */ new Set();
12
12
  var throttleTimeout = null;
13
+ var cleanupResize = null;
13
14
  function getSnapshot() {
14
15
  return isMobileState;
15
16
  }
@@ -31,7 +32,7 @@ function subscribe(callback) {
31
32
  }, THROTTLE_MS);
32
33
  };
33
34
  window.addEventListener("resize", handleResize, { passive: true });
34
- subscribe._cleanup = () => {
35
+ cleanupResize = () => {
35
36
  window.removeEventListener("resize", handleResize);
36
37
  if (throttleTimeout) {
37
38
  clearTimeout(throttleTimeout);
@@ -42,8 +43,9 @@ function subscribe(callback) {
42
43
  listeners.add(callback);
43
44
  return () => {
44
45
  listeners.delete(callback);
45
- if (listeners.size === 0 && subscribe._cleanup) {
46
- subscribe._cleanup();
46
+ if (listeners.size === 0 && cleanupResize) {
47
+ cleanupResize();
48
+ cleanupResize = null;
47
49
  }
48
50
  };
49
51
  }
package/dist/web.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/augur-packages/augur-packages/packages/augur-hooks/dist/web.cjs","../src/web/use-mobile-detection.tsx","../src/web/use-mobile-optimized-intersection.ts"],"names":[],"mappings":"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","file":"/home/runner/work/augur-packages/augur-packages/packages/augur-hooks/dist/web.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\n\nconst MOBILE_BREAKPOINT = 992;\nconst THROTTLE_MS = 150;\n\n// Singleton state for mobile detection\nlet isMobileState = false;\nconst listeners: Set<() => void> = new Set();\nlet throttleTimeout: ReturnType<typeof setTimeout> | null = null;\n\nfunction getSnapshot(): boolean {\n return isMobileState;\n}\n\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nfunction subscribe(callback: () => void): () => void {\n if (listeners.size === 0 && typeof window !== \"undefined\") {\n isMobileState = window.innerWidth <= MOBILE_BREAKPOINT;\n\n const handleResize = () => {\n if (throttleTimeout) return;\n\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null;\n const newValue = window.innerWidth <= MOBILE_BREAKPOINT;\n if (newValue !== isMobileState) {\n isMobileState = newValue;\n listeners.forEach((listener) => listener());\n }\n }, THROTTLE_MS);\n };\n\n window.addEventListener(\"resize\", handleResize, { passive: true });\n\n (subscribe as any)._cleanup = () => {\n window.removeEventListener(\"resize\", handleResize);\n if (throttleTimeout) {\n clearTimeout(throttleTimeout);\n throttleTimeout = null;\n }\n };\n }\n\n listeners.add(callback);\n\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0 && (subscribe as any)._cleanup) {\n (subscribe as any)._cleanup();\n }\n };\n}\n\n/**\n * Returns whether the screen is mobile-sized.\n * Uses `useSyncExternalStore` for proper SSR support.\n *\n * - SSR-safe: returns false on server, updates on client after hydration\n * - Single listener: one resize listener regardless of component count\n * - Throttled: resize events throttled to prevent excessive re-renders\n */\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nconst HydrationContext = createContext(false);\n\ninterface MobileProviderProps {\n children: ReactNode;\n}\n\n/**\n * Optional provider that tracks hydration state.\n * Wrap your app with this if you need to conditionally render\n * mobile-only content after hydration.\n */\nexport function MobileProvider({ children }: MobileProviderProps) {\n const isHydrated = useSyncExternalStore(\n () => () => {},\n () => true,\n () => false,\n );\n\n return (\n <HydrationContext.Provider value={isHydrated}>\n {children}\n </HydrationContext.Provider>\n );\n}\n\n/**\n * Returns true only after the client has hydrated.\n * Prevents hydration mismatches with conditional rendering.\n */\nexport function useIsHydrated(): boolean {\n return useContext(HydrationContext);\n}\n","\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\n\ninterface UseMobileOptimizedIntersectionOptions {\n rootMarginMobile?: string;\n rootMarginDesktop?: string;\n threshold?: number;\n enabled?: boolean;\n mobileBreakpoint?: number;\n}\n\n/**\n * IntersectionObserver hook with mobile-optimized root margins.\n * Uses larger margins on mobile for more aggressive prefetching/lazy loading.\n */\nexport const useMobileOptimizedIntersection = ({\n rootMarginMobile = \"300px\",\n rootMarginDesktop = \"150px\",\n threshold = 0,\n enabled = true,\n mobileBreakpoint = 992,\n}: UseMobileOptimizedIntersectionOptions = {}) => {\n const [isIntersecting, setIsIntersecting] = useState(false);\n const elementRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!enabled || !elementRef.current) return;\n\n const isMobile = window.innerWidth <= mobileBreakpoint;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting && !isIntersecting) {\n setIsIntersecting(true);\n }\n });\n },\n {\n rootMargin: isMobile ? rootMarginMobile : rootMarginDesktop,\n threshold,\n },\n );\n\n observer.observe(elementRef.current);\n\n return () => {\n observer.disconnect();\n };\n }, [\n enabled,\n rootMarginMobile,\n rootMarginDesktop,\n threshold,\n isIntersecting,\n mobileBreakpoint,\n ]);\n\n return {\n ref: elementRef,\n isIntersecting,\n };\n};\n"]}
1
+ {"version":3,"sources":["/home/runner/work/augur-packages/augur-packages/packages/augur-hooks/dist/web.cjs","../src/web/use-mobile-detection.tsx","../src/web/use-mobile-optimized-intersection.ts"],"names":[],"mappings":"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","file":"/home/runner/work/augur-packages/augur-packages/packages/augur-hooks/dist/web.cjs","sourcesContent":[null,"import {\n createContext,\n useContext,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\n\nconst MOBILE_BREAKPOINT = 992;\nconst THROTTLE_MS = 150;\n\n// Singleton state for mobile detection\nlet isMobileState = false;\nconst listeners: Set<() => void> = new Set();\nlet throttleTimeout: ReturnType<typeof setTimeout> | null = null;\nlet cleanupResize: (() => void) | null = null;\n\nfunction getSnapshot(): boolean {\n return isMobileState;\n}\n\n/* v8 ignore next 3 */\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nfunction subscribe(callback: () => void): () => void {\n if (listeners.size === 0 && typeof window !== \"undefined\") {\n isMobileState = window.innerWidth <= MOBILE_BREAKPOINT;\n\n const handleResize = () => {\n if (throttleTimeout) return;\n\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null;\n const newValue = window.innerWidth <= MOBILE_BREAKPOINT;\n if (newValue !== isMobileState) {\n isMobileState = newValue;\n listeners.forEach((listener) => listener());\n }\n }, THROTTLE_MS);\n };\n\n window.addEventListener(\"resize\", handleResize, { passive: true });\n\n cleanupResize = () => {\n window.removeEventListener(\"resize\", handleResize);\n if (throttleTimeout) {\n clearTimeout(throttleTimeout);\n throttleTimeout = null;\n }\n };\n }\n\n listeners.add(callback);\n\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0 && cleanupResize) {\n cleanupResize();\n cleanupResize = null;\n }\n };\n}\n\n/**\n * Returns whether the screen is mobile-sized.\n * Uses `useSyncExternalStore` for proper SSR support.\n *\n * - SSR-safe: returns false on server, updates on client after hydration\n * - Single listener: one resize listener regardless of component count\n * - Throttled: resize events throttled to prevent excessive re-renders\n */\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nconst HydrationContext = createContext(false);\n\ninterface MobileProviderProps {\n children: ReactNode;\n}\n\n/**\n * Optional provider that tracks hydration state.\n * Wrap your app with this if you need to conditionally render\n * mobile-only content after hydration.\n */\nexport function MobileProvider({ children }: MobileProviderProps) {\n const isHydrated = useSyncExternalStore(\n () => () => {},\n () => true,\n () => false,\n );\n\n return (\n <HydrationContext.Provider value={isHydrated}>\n {children}\n </HydrationContext.Provider>\n );\n}\n\n/**\n * Returns true only after the client has hydrated.\n * Prevents hydration mismatches with conditional rendering.\n */\nexport function useIsHydrated(): boolean {\n return useContext(HydrationContext);\n}\n","\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\n\ninterface UseMobileOptimizedIntersectionOptions {\n rootMarginMobile?: string;\n rootMarginDesktop?: string;\n threshold?: number;\n enabled?: boolean;\n mobileBreakpoint?: number;\n}\n\n/**\n * IntersectionObserver hook with mobile-optimized root margins.\n * Uses larger margins on mobile for more aggressive prefetching/lazy loading.\n */\nexport const useMobileOptimizedIntersection = ({\n rootMarginMobile = \"300px\",\n rootMarginDesktop = \"150px\",\n threshold = 0,\n enabled = true,\n mobileBreakpoint = 992,\n}: UseMobileOptimizedIntersectionOptions = {}) => {\n const [isIntersecting, setIsIntersecting] = useState(false);\n const elementRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!enabled || !elementRef.current) return;\n\n const isMobile = window.innerWidth <= mobileBreakpoint;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting && !isIntersecting) {\n setIsIntersecting(true);\n }\n });\n },\n {\n rootMargin: isMobile ? rootMarginMobile : rootMarginDesktop,\n threshold,\n },\n );\n\n observer.observe(elementRef.current);\n\n return () => {\n observer.disconnect();\n };\n }, [\n enabled,\n rootMarginMobile,\n rootMarginDesktop,\n threshold,\n isIntersecting,\n mobileBreakpoint,\n ]);\n\n return {\n ref: elementRef,\n isIntersecting,\n };\n};\n"]}
package/dist/web.js CHANGED
@@ -10,6 +10,7 @@ var THROTTLE_MS = 150;
10
10
  var isMobileState = false;
11
11
  var listeners = /* @__PURE__ */ new Set();
12
12
  var throttleTimeout = null;
13
+ var cleanupResize = null;
13
14
  function getSnapshot() {
14
15
  return isMobileState;
15
16
  }
@@ -31,7 +32,7 @@ function subscribe(callback) {
31
32
  }, THROTTLE_MS);
32
33
  };
33
34
  window.addEventListener("resize", handleResize, { passive: true });
34
- subscribe._cleanup = () => {
35
+ cleanupResize = () => {
35
36
  window.removeEventListener("resize", handleResize);
36
37
  if (throttleTimeout) {
37
38
  clearTimeout(throttleTimeout);
@@ -42,8 +43,9 @@ function subscribe(callback) {
42
43
  listeners.add(callback);
43
44
  return () => {
44
45
  listeners.delete(callback);
45
- if (listeners.size === 0 && subscribe._cleanup) {
46
- subscribe._cleanup();
46
+ if (listeners.size === 0 && cleanupResize) {
47
+ cleanupResize();
48
+ cleanupResize = null;
47
49
  }
48
50
  };
49
51
  }
package/dist/web.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/web/use-mobile-detection.tsx","../src/web/use-mobile-optimized-intersection.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\n\nconst MOBILE_BREAKPOINT = 992;\nconst THROTTLE_MS = 150;\n\n// Singleton state for mobile detection\nlet isMobileState = false;\nconst listeners: Set<() => void> = new Set();\nlet throttleTimeout: ReturnType<typeof setTimeout> | null = null;\n\nfunction getSnapshot(): boolean {\n return isMobileState;\n}\n\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nfunction subscribe(callback: () => void): () => void {\n if (listeners.size === 0 && typeof window !== \"undefined\") {\n isMobileState = window.innerWidth <= MOBILE_BREAKPOINT;\n\n const handleResize = () => {\n if (throttleTimeout) return;\n\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null;\n const newValue = window.innerWidth <= MOBILE_BREAKPOINT;\n if (newValue !== isMobileState) {\n isMobileState = newValue;\n listeners.forEach((listener) => listener());\n }\n }, THROTTLE_MS);\n };\n\n window.addEventListener(\"resize\", handleResize, { passive: true });\n\n (subscribe as any)._cleanup = () => {\n window.removeEventListener(\"resize\", handleResize);\n if (throttleTimeout) {\n clearTimeout(throttleTimeout);\n throttleTimeout = null;\n }\n };\n }\n\n listeners.add(callback);\n\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0 && (subscribe as any)._cleanup) {\n (subscribe as any)._cleanup();\n }\n };\n}\n\n/**\n * Returns whether the screen is mobile-sized.\n * Uses `useSyncExternalStore` for proper SSR support.\n *\n * - SSR-safe: returns false on server, updates on client after hydration\n * - Single listener: one resize listener regardless of component count\n * - Throttled: resize events throttled to prevent excessive re-renders\n */\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nconst HydrationContext = createContext(false);\n\ninterface MobileProviderProps {\n children: ReactNode;\n}\n\n/**\n * Optional provider that tracks hydration state.\n * Wrap your app with this if you need to conditionally render\n * mobile-only content after hydration.\n */\nexport function MobileProvider({ children }: MobileProviderProps) {\n const isHydrated = useSyncExternalStore(\n () => () => {},\n () => true,\n () => false,\n );\n\n return (\n <HydrationContext.Provider value={isHydrated}>\n {children}\n </HydrationContext.Provider>\n );\n}\n\n/**\n * Returns true only after the client has hydrated.\n * Prevents hydration mismatches with conditional rendering.\n */\nexport function useIsHydrated(): boolean {\n return useContext(HydrationContext);\n}\n","\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\n\ninterface UseMobileOptimizedIntersectionOptions {\n rootMarginMobile?: string;\n rootMarginDesktop?: string;\n threshold?: number;\n enabled?: boolean;\n mobileBreakpoint?: number;\n}\n\n/**\n * IntersectionObserver hook with mobile-optimized root margins.\n * Uses larger margins on mobile for more aggressive prefetching/lazy loading.\n */\nexport const useMobileOptimizedIntersection = ({\n rootMarginMobile = \"300px\",\n rootMarginDesktop = \"150px\",\n threshold = 0,\n enabled = true,\n mobileBreakpoint = 992,\n}: UseMobileOptimizedIntersectionOptions = {}) => {\n const [isIntersecting, setIsIntersecting] = useState(false);\n const elementRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!enabled || !elementRef.current) return;\n\n const isMobile = window.innerWidth <= mobileBreakpoint;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting && !isIntersecting) {\n setIsIntersecting(true);\n }\n });\n },\n {\n rootMargin: isMobile ? rootMarginMobile : rootMarginDesktop,\n threshold,\n },\n );\n\n observer.observe(elementRef.current);\n\n return () => {\n observer.disconnect();\n };\n }, [\n enabled,\n rootMarginMobile,\n rootMarginDesktop,\n threshold,\n isIntersecting,\n mobileBreakpoint,\n ]);\n\n return {\n ref: elementRef,\n isIntersecting,\n };\n};\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAuFH;AArFJ,IAAM,oBAAoB;AAC1B,IAAM,cAAc;AAGpB,IAAI,gBAAgB;AACpB,IAAM,YAA6B,oBAAI,IAAI;AAC3C,IAAI,kBAAwD;AAE5D,SAAS,cAAuB;AAC9B,SAAO;AACT;AAEA,SAAS,oBAA6B;AACpC,SAAO;AACT;AAEA,SAAS,UAAU,UAAkC;AACnD,MAAI,UAAU,SAAS,KAAK,OAAO,WAAW,aAAa;AACzD,oBAAgB,OAAO,cAAc;AAErC,UAAM,eAAe,MAAM;AACzB,UAAI,gBAAiB;AAErB,wBAAkB,WAAW,MAAM;AACjC,0BAAkB;AAClB,cAAM,WAAW,OAAO,cAAc;AACtC,YAAI,aAAa,eAAe;AAC9B,0BAAgB;AAChB,oBAAU,QAAQ,CAAC,aAAa,SAAS,CAAC;AAAA,QAC5C;AAAA,MACF,GAAG,WAAW;AAAA,IAChB;AAEA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AAEjE,IAAC,UAAkB,WAAW,MAAM;AAClC,aAAO,oBAAoB,UAAU,YAAY;AACjD,UAAI,iBAAiB;AACnB,qBAAa,eAAe;AAC5B,0BAAkB;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAEA,YAAU,IAAI,QAAQ;AAEtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AACzB,QAAI,UAAU,SAAS,KAAM,UAAkB,UAAU;AACvD,MAAC,UAAkB,SAAS;AAAA,IAC9B;AAAA,EACF;AACF;AAUO,SAAS,cAAuB;AACrC,SAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;AAEA,IAAM,mBAAmB,cAAc,KAAK;AAWrC,SAAS,eAAe,EAAE,SAAS,GAAwB;AAChE,QAAM,aAAa;AAAA,IACjB,MAAM,MAAM;AAAA,IAAC;AAAA,IACb,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAEA,SACE,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,YAC/B,UACH;AAEJ;AAMO,SAAS,gBAAyB;AACvC,SAAO,WAAW,gBAAgB;AACpC;;;ACtGA,SAAS,WAAW,UAAU,cAAc;AAcrC,IAAM,iCAAiC,CAAC;AAAA,EAC7C,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,mBAAmB;AACrB,IAA2C,CAAC,MAAM;AAChD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,WAAW,QAAS;AAErC,UAAM,WAAW,OAAO,cAAc;AAEtC,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,gBAAQ,QAAQ,CAAC,UAAU;AACzB,cAAI,MAAM,kBAAkB,CAAC,gBAAgB;AAC3C,8BAAkB,IAAI;AAAA,UACxB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA;AAAA,QACE,YAAY,WAAW,mBAAmB;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAEA,aAAS,QAAQ,WAAW,OAAO;AAEnC,WAAO,MAAM;AACX,eAAS,WAAW;AAAA,IACtB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,KAAK;AAAA,IACL;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/web/use-mobile-detection.tsx","../src/web/use-mobile-optimized-intersection.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\n\nconst MOBILE_BREAKPOINT = 992;\nconst THROTTLE_MS = 150;\n\n// Singleton state for mobile detection\nlet isMobileState = false;\nconst listeners: Set<() => void> = new Set();\nlet throttleTimeout: ReturnType<typeof setTimeout> | null = null;\nlet cleanupResize: (() => void) | null = null;\n\nfunction getSnapshot(): boolean {\n return isMobileState;\n}\n\n/* v8 ignore next 3 */\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nfunction subscribe(callback: () => void): () => void {\n if (listeners.size === 0 && typeof window !== \"undefined\") {\n isMobileState = window.innerWidth <= MOBILE_BREAKPOINT;\n\n const handleResize = () => {\n if (throttleTimeout) return;\n\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null;\n const newValue = window.innerWidth <= MOBILE_BREAKPOINT;\n if (newValue !== isMobileState) {\n isMobileState = newValue;\n listeners.forEach((listener) => listener());\n }\n }, THROTTLE_MS);\n };\n\n window.addEventListener(\"resize\", handleResize, { passive: true });\n\n cleanupResize = () => {\n window.removeEventListener(\"resize\", handleResize);\n if (throttleTimeout) {\n clearTimeout(throttleTimeout);\n throttleTimeout = null;\n }\n };\n }\n\n listeners.add(callback);\n\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0 && cleanupResize) {\n cleanupResize();\n cleanupResize = null;\n }\n };\n}\n\n/**\n * Returns whether the screen is mobile-sized.\n * Uses `useSyncExternalStore` for proper SSR support.\n *\n * - SSR-safe: returns false on server, updates on client after hydration\n * - Single listener: one resize listener regardless of component count\n * - Throttled: resize events throttled to prevent excessive re-renders\n */\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nconst HydrationContext = createContext(false);\n\ninterface MobileProviderProps {\n children: ReactNode;\n}\n\n/**\n * Optional provider that tracks hydration state.\n * Wrap your app with this if you need to conditionally render\n * mobile-only content after hydration.\n */\nexport function MobileProvider({ children }: MobileProviderProps) {\n const isHydrated = useSyncExternalStore(\n () => () => {},\n () => true,\n () => false,\n );\n\n return (\n <HydrationContext.Provider value={isHydrated}>\n {children}\n </HydrationContext.Provider>\n );\n}\n\n/**\n * Returns true only after the client has hydrated.\n * Prevents hydration mismatches with conditional rendering.\n */\nexport function useIsHydrated(): boolean {\n return useContext(HydrationContext);\n}\n","\"use client\";\n\nimport { useEffect, useState, useRef } from \"react\";\n\ninterface UseMobileOptimizedIntersectionOptions {\n rootMarginMobile?: string;\n rootMarginDesktop?: string;\n threshold?: number;\n enabled?: boolean;\n mobileBreakpoint?: number;\n}\n\n/**\n * IntersectionObserver hook with mobile-optimized root margins.\n * Uses larger margins on mobile for more aggressive prefetching/lazy loading.\n */\nexport const useMobileOptimizedIntersection = ({\n rootMarginMobile = \"300px\",\n rootMarginDesktop = \"150px\",\n threshold = 0,\n enabled = true,\n mobileBreakpoint = 992,\n}: UseMobileOptimizedIntersectionOptions = {}) => {\n const [isIntersecting, setIsIntersecting] = useState(false);\n const elementRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!enabled || !elementRef.current) return;\n\n const isMobile = window.innerWidth <= mobileBreakpoint;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting && !isIntersecting) {\n setIsIntersecting(true);\n }\n });\n },\n {\n rootMargin: isMobile ? rootMarginMobile : rootMarginDesktop,\n threshold,\n },\n );\n\n observer.observe(elementRef.current);\n\n return () => {\n observer.disconnect();\n };\n }, [\n enabled,\n rootMarginMobile,\n rootMarginDesktop,\n threshold,\n isIntersecting,\n mobileBreakpoint,\n ]);\n\n return {\n ref: elementRef,\n isIntersecting,\n };\n};\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AA0FH;AAxFJ,IAAM,oBAAoB;AAC1B,IAAM,cAAc;AAGpB,IAAI,gBAAgB;AACpB,IAAM,YAA6B,oBAAI,IAAI;AAC3C,IAAI,kBAAwD;AAC5D,IAAI,gBAAqC;AAEzC,SAAS,cAAuB;AAC9B,SAAO;AACT;AAGA,SAAS,oBAA6B;AACpC,SAAO;AACT;AAEA,SAAS,UAAU,UAAkC;AACnD,MAAI,UAAU,SAAS,KAAK,OAAO,WAAW,aAAa;AACzD,oBAAgB,OAAO,cAAc;AAErC,UAAM,eAAe,MAAM;AACzB,UAAI,gBAAiB;AAErB,wBAAkB,WAAW,MAAM;AACjC,0BAAkB;AAClB,cAAM,WAAW,OAAO,cAAc;AACtC,YAAI,aAAa,eAAe;AAC9B,0BAAgB;AAChB,oBAAU,QAAQ,CAAC,aAAa,SAAS,CAAC;AAAA,QAC5C;AAAA,MACF,GAAG,WAAW;AAAA,IAChB;AAEA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AAEjE,oBAAgB,MAAM;AACpB,aAAO,oBAAoB,UAAU,YAAY;AACjD,UAAI,iBAAiB;AACnB,qBAAa,eAAe;AAC5B,0BAAkB;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAEA,YAAU,IAAI,QAAQ;AAEtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AACzB,QAAI,UAAU,SAAS,KAAK,eAAe;AACzC,oBAAc;AACd,sBAAgB;AAAA,IAClB;AAAA,EACF;AACF;AAUO,SAAS,cAAuB;AACrC,SAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;AAEA,IAAM,mBAAmB,cAAc,KAAK;AAWrC,SAAS,eAAe,EAAE,SAAS,GAAwB;AAChE,QAAM,aAAa;AAAA,IACjB,MAAM,MAAM;AAAA,IAAC;AAAA,IACb,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAEA,SACE,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,YAC/B,UACH;AAEJ;AAMO,SAAS,gBAAyB;AACvC,SAAO,WAAW,gBAAgB;AACpC;;;ACzGA,SAAS,WAAW,UAAU,cAAc;AAcrC,IAAM,iCAAiC,CAAC;AAAA,EAC7C,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,mBAAmB;AACrB,IAA2C,CAAC,MAAM;AAChD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,aAAa,OAA8B,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,WAAW,QAAS;AAErC,UAAM,WAAW,OAAO,cAAc;AAEtC,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,gBAAQ,QAAQ,CAAC,UAAU;AACzB,cAAI,MAAM,kBAAkB,CAAC,gBAAgB;AAC3C,8BAAkB,IAAI;AAAA,UACxB;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA;AAAA,QACE,YAAY,WAAW,mBAAmB;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAEA,aAAS,QAAQ,WAAW,OAAO;AAEnC,WAAO,MAAM;AACX,eAAS,WAAW;AAAA,IACtB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,KAAK;AAAA,IACL;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simpleapps-com/augur-hooks",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cross-platform React Query hooks and Zustand stores for Augur ecommerce sites",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,21 +26,26 @@
26
26
  "dist"
27
27
  ],
28
28
  "dependencies": {
29
- "@simpleapps-com/augur-utils": "0.1.0"
29
+ "@simpleapps-com/augur-utils": "0.1.1"
30
30
  },
31
31
  "peerDependencies": {
32
- "react": "^19.0.0",
32
+ "@simpleapps-com/augur-api": "^0.9.0",
33
33
  "@tanstack/react-query": "^5.80.0",
34
34
  "@tanstack/react-query-persist-client": "^5.80.0",
35
- "zustand": "^5.0.0",
36
- "@simpleapps-com/augur-api": "^0.9.0"
35
+ "react": "^19.0.0",
36
+ "zustand": "^5.0.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@tanstack/react-query": "^5.80.0",
40
+ "@testing-library/react": "^16.3.2",
41
+ "@testing-library/react-hooks": "^8.0.1",
40
42
  "@types/node": "^22.0.0",
41
43
  "@types/react": "^19.0.0",
42
44
  "@types/react-dom": "^19.0.0",
45
+ "@vitest/coverage-v8": "^3.2.4",
46
+ "jsdom": "^28.1.0",
43
47
  "react": "^19.0.0",
48
+ "react-dom": "^19.2.4",
44
49
  "tsup": "^8.5.0",
45
50
  "vitest": "^3.2.0",
46
51
  "zustand": "^5.0.0",
@@ -50,7 +55,7 @@
50
55
  "build": "tsup",
51
56
  "dev": "tsup --watch",
52
57
  "lint": "eslint src/",
53
- "test": "vitest run --passWithNoTests",
58
+ "test": "vitest run --coverage",
54
59
  "typecheck": "tsc --noEmit"
55
60
  }
56
61
  }