@lolyjs/core 0.3.0-alpha.5 → 0.4.0-alpha.0
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 +162 -0
- package/dist/{bootstrap-BfGTMUkj.d.mts → bootstrap-B6W6XoI5.d.mts} +6 -0
- package/dist/{bootstrap-BfGTMUkj.d.ts → bootstrap-B6W6XoI5.d.ts} +6 -0
- package/dist/cli.cjs +3362 -867
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +3442 -952
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +3622 -897
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.mjs +3598 -879
- package/dist/index.mjs.map +1 -1
- package/dist/react/cache.cjs.map +1 -1
- package/dist/react/cache.mjs.map +1 -1
- package/dist/react/components.cjs +138 -5
- package/dist/react/components.cjs.map +1 -1
- package/dist/react/components.d.mts +58 -6
- package/dist/react/components.d.ts +58 -6
- package/dist/react/components.mjs +138 -6
- package/dist/react/components.mjs.map +1 -1
- package/dist/react/hooks.cjs +24 -0
- package/dist/react/hooks.cjs.map +1 -1
- package/dist/react/hooks.d.mts +49 -1
- package/dist/react/hooks.d.ts +49 -1
- package/dist/react/hooks.mjs +22 -0
- package/dist/react/hooks.mjs.map +1 -1
- package/dist/react/themes.cjs +0 -169
- package/dist/react/themes.cjs.map +1 -1
- package/dist/react/themes.d.mts +18 -11
- package/dist/react/themes.d.ts +18 -11
- package/dist/react/themes.mjs +0 -159
- package/dist/react/themes.mjs.map +1 -1
- package/dist/runtime.cjs +268 -24
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.mjs +255 -15
- package/dist/runtime.mjs.map +1 -1
- package/package.json +4 -2
package/dist/react/hooks.d.mts
CHANGED
|
@@ -86,4 +86,52 @@ interface Router {
|
|
|
86
86
|
*/
|
|
87
87
|
declare function useRouter(): Router;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Hook to detect if the component is mounted on the client.
|
|
91
|
+
* Returns `false` during SSR and initial render, `true` after hydration.
|
|
92
|
+
*
|
|
93
|
+
* Useful for avoiding hydration mismatches when rendering content that depends on client-side state.
|
|
94
|
+
*
|
|
95
|
+
* @returns `true` if mounted on client, `false` during SSR
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* function MyComponent() {
|
|
100
|
+
* const isMounted = useClientMounted();
|
|
101
|
+
*
|
|
102
|
+
* if (!isMounted) {
|
|
103
|
+
* return <div>Loading...</div>;
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* return <div>{window.innerWidth}</div>;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare function useClientMounted(): boolean;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Hook that uses `useLayoutEffect` on the client and `useEffect` (no-op) on the server.
|
|
114
|
+
*
|
|
115
|
+
* This prevents React warnings about using `useLayoutEffect` during SSR.
|
|
116
|
+
* `useLayoutEffect` runs synchronously after DOM mutations but before paint,
|
|
117
|
+
* making it ideal for DOM measurements and synchronous updates.
|
|
118
|
+
*
|
|
119
|
+
* @param effect - Effect function
|
|
120
|
+
* @param deps - Dependency array
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* function MyComponent() {
|
|
125
|
+
* useIsomorphicLayoutEffect(() => {
|
|
126
|
+
* // This runs synchronously on client, not on server
|
|
127
|
+
* const width = elementRef.current?.offsetWidth;
|
|
128
|
+
* setWidth(width);
|
|
129
|
+
* }, []);
|
|
130
|
+
*
|
|
131
|
+
* return <div ref={elementRef}>Content</div>;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function useIsomorphicLayoutEffect(effect: React.EffectCallback, deps?: React.DependencyList): void;
|
|
136
|
+
|
|
137
|
+
export { type Router, useBroadcastChannel, useClientMounted, useIsomorphicLayoutEffect, useRouter };
|
package/dist/react/hooks.d.ts
CHANGED
|
@@ -86,4 +86,52 @@ interface Router {
|
|
|
86
86
|
*/
|
|
87
87
|
declare function useRouter(): Router;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Hook to detect if the component is mounted on the client.
|
|
91
|
+
* Returns `false` during SSR and initial render, `true` after hydration.
|
|
92
|
+
*
|
|
93
|
+
* Useful for avoiding hydration mismatches when rendering content that depends on client-side state.
|
|
94
|
+
*
|
|
95
|
+
* @returns `true` if mounted on client, `false` during SSR
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* function MyComponent() {
|
|
100
|
+
* const isMounted = useClientMounted();
|
|
101
|
+
*
|
|
102
|
+
* if (!isMounted) {
|
|
103
|
+
* return <div>Loading...</div>;
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* return <div>{window.innerWidth}</div>;
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare function useClientMounted(): boolean;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Hook that uses `useLayoutEffect` on the client and `useEffect` (no-op) on the server.
|
|
114
|
+
*
|
|
115
|
+
* This prevents React warnings about using `useLayoutEffect` during SSR.
|
|
116
|
+
* `useLayoutEffect` runs synchronously after DOM mutations but before paint,
|
|
117
|
+
* making it ideal for DOM measurements and synchronous updates.
|
|
118
|
+
*
|
|
119
|
+
* @param effect - Effect function
|
|
120
|
+
* @param deps - Dependency array
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* function MyComponent() {
|
|
125
|
+
* useIsomorphicLayoutEffect(() => {
|
|
126
|
+
* // This runs synchronously on client, not on server
|
|
127
|
+
* const width = elementRef.current?.offsetWidth;
|
|
128
|
+
* setWidth(width);
|
|
129
|
+
* }, []);
|
|
130
|
+
*
|
|
131
|
+
* return <div ref={elementRef}>Content</div>;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function useIsomorphicLayoutEffect(effect: React.EffectCallback, deps?: React.DependencyList): void;
|
|
136
|
+
|
|
137
|
+
export { type Router, useBroadcastChannel, useClientMounted, useIsomorphicLayoutEffect, useRouter };
|
package/dist/react/hooks.mjs
CHANGED
|
@@ -233,8 +233,30 @@ function parseQueryString(search) {
|
|
|
233
233
|
}
|
|
234
234
|
return params;
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
// modules/react/hooks/useClientMounted/index.ts
|
|
238
|
+
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
239
|
+
function useClientMounted() {
|
|
240
|
+
const [mounted, setMounted] = useState3(false);
|
|
241
|
+
useEffect3(() => {
|
|
242
|
+
setMounted(true);
|
|
243
|
+
}, []);
|
|
244
|
+
return mounted;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// modules/react/hooks/useIsomorphicLayoutEffect/index.ts
|
|
248
|
+
import { useEffect as useEffect4, useLayoutEffect } from "react";
|
|
249
|
+
function useIsomorphicLayoutEffect(effect, deps) {
|
|
250
|
+
if (typeof window === "undefined") {
|
|
251
|
+
useEffect4(effect, deps);
|
|
252
|
+
} else {
|
|
253
|
+
useLayoutEffect(effect, deps);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
236
256
|
export {
|
|
237
257
|
useBroadcastChannel,
|
|
258
|
+
useClientMounted,
|
|
259
|
+
useIsomorphicLayoutEffect,
|
|
238
260
|
useRouter
|
|
239
261
|
};
|
|
240
262
|
//# sourceMappingURL=hooks.mjs.map
|
package/dist/react/hooks.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\nimport type { InitialData, RouterData } from \"./types\";\n\nconst LAYOUT_PROPS_KEY = \"__FW_LAYOUT_PROPS__\";\n\nexport function getWindowData(): InitialData | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n return (window[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\n}\n\n/**\n * Gets preserved layout props from window storage.\n * Layout props are preserved across SPA navigations when layout hooks are skipped.\n */\nexport function getPreservedLayoutProps(): Record<string, any> | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n return ((window as any)[LAYOUT_PROPS_KEY] as Record<string, any> | undefined) ?? null;\n}\n\n/**\n * Sets preserved layout props in window storage.\n * These props are used when layout hooks are skipped in SPA navigation.\n */\nexport function setPreservedLayoutProps(props: Record<string, any> | null): void {\n if (typeof window === \"undefined\") {\n return;\n }\n if (props === null) {\n delete (window as any)[LAYOUT_PROPS_KEY];\n } else {\n (window as any)[LAYOUT_PROPS_KEY] = props;\n }\n}\n\nexport function getRouterData(): RouterData | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n return (window[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\n}\n\nexport function setWindowData(data: InitialData): void {\n window[WINDOW_DATA_KEY] = data;\n \n // Dispatch event for components to listen to (e.g. ThemeProvider)\n // This ensures components update when navigating in SPA mode\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(\n new CustomEvent(\"fw-data-refresh\", {\n detail: { data },\n })\n );\n }\n}\n\nexport function setRouterData(data: RouterData): void {\n window[ROUTER_DATA_KEY] = data;\n \n // Dispatch event for router data updates\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(\n new CustomEvent(\"fw-router-data-refresh\", {\n detail: { data },\n })\n );\n }\n}\n\nexport function getCurrentTheme(): string | null {\n return getWindowData()?.theme ?? null;\n}\n\n"],"mappings":"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","names":["useState","useEffect","useCallback","useContext","useRef","useContext","useRef","useEffect","useState","useCallback"]}
|
|
1
|
+
{"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts","../../modules/react/hooks/useClientMounted/index.ts","../../modules/react/hooks/useIsomorphicLayoutEffect/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n","import { useState, useEffect, useCallback, useContext, useRef } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData, getRouterData } from \"../../../runtime/client/window-data\";\r\nimport { ROUTER_NAVIGATE_KEY } from \"../../../runtime/client/constants\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n * Alias for searchParams for backward compatibility\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Search parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n searchParams: Record<string, unknown>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n // Use a ref to store navigate so we can access it in callbacks even if context updates\r\n // Initialize with current navigate value\r\n const navigateRef = useRef(navigate);\r\n \r\n // Update ref when navigate changes (this ensures we always have the latest value)\r\n useEffect(() => {\r\n navigateRef.current = navigate;\r\n }, [navigate]);\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n searchParams: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n \r\n // Parse search params from URL if routerData is not available\r\n const searchParams = routerData?.searchParams || parseQueryString(window.location.search);\r\n \r\n return {\r\n pathname: routerData?.pathname || data?.pathname || window.location.pathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const routerData = getRouterData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n const searchParams = routerData?.searchParams || parseQueryString(currentSearch);\r\n \r\n setRouteData({\r\n pathname: routerData?.pathname || data?.pathname || currentPathname,\r\n query: searchParams as Record<string, string>, // For backward compatibility\r\n searchParams: searchParams,\r\n params: routerData?.params || data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.addEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"fw-router-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n /**\r\n * SOLUTION: Multi-source navigate function resolution\r\n * \r\n * During React hydration, RouterContext may not be available immediately.\r\n * We try three sources in order:\r\n * 1. navigateRef.current - Most up-to-date, updated via useEffect\r\n * 2. navigate from context - Direct context access\r\n * 3. window.__LOLY_ROUTER_NAVIGATE__ - Global fallback exposed by AppShell\r\n * \r\n * This ensures SPA navigation works even during hydration timing issues.\r\n */\r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return; // SSR\r\n }\r\n \r\n // Wait for context during hydration (up to 100ms)\r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n // Final fallback: full page reload if navigate is still unavailable\r\n if (!currentNavigate) {\r\n window.location.href = fullUrl;\r\n return;\r\n }\r\n \r\n // Check if we're already on this URL (same as link handler)\r\n const currentUrl = window.location.pathname + window.location.search;\r\n if (fullUrl === currentUrl) {\r\n return; // Already on this route, no need to navigate\r\n }\r\n \r\n // Update URL in browser history (same as link handler does)\r\n // This is done BEFORE navigation to match link behavior\r\n window.history.pushState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation (same as link handler)\r\n // If navigation fails, navigate() will handle the reload internally\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate] // Include navigate in dependencies so it updates when context becomes available\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.replace(fullUrl);\r\n return;\r\n }\r\n \r\n // Update URL in browser history using replace (doesn't add to history)\r\n window.history.replaceState({}, \"\", fullUrl);\r\n \r\n // Navigate using SPA navigation\r\n await currentNavigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n const getCurrentNavigate = () => {\r\n if (navigateRef.current) return navigateRef.current;\r\n if (navigate) return navigate;\r\n if (typeof window !== \"undefined\" && (window as any)[ROUTER_NAVIGATE_KEY]) {\r\n return (window as any)[ROUTER_NAVIGATE_KEY];\r\n }\r\n return null;\r\n };\r\n \r\n let currentNavigate = getCurrentNavigate();\r\n \r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n \r\n if (!currentNavigate) {\r\n for (let i = 0; i < 10; i++) {\r\n await new Promise(resolve => setTimeout(resolve, 10));\r\n currentNavigate = getCurrentNavigate();\r\n if (currentNavigate) break;\r\n }\r\n }\r\n \r\n if (!currentNavigate) {\r\n window.location.reload();\r\n return;\r\n }\r\n \r\n await currentNavigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n searchParams: routeData.searchParams,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const ROUTER_DATA_KEY = \"__LOLY_ROUTER_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n// Global key for navigate function fallback (exposed by AppShell for hydration timing issues)\r\nexport const ROUTER_NAVIGATE_KEY = \"__LOLY_ROUTER_NAVIGATE__\";\r\n\r\n","import { WINDOW_DATA_KEY, ROUTER_DATA_KEY } from \"./constants\";\r\nimport type { InitialData, RouterData } from \"./types\";\r\n\r\nconst LAYOUT_PROPS_KEY = \"__FW_LAYOUT_PROPS__\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\n/**\r\n * Gets preserved layout props from window storage.\r\n * Layout props are preserved across SPA navigations when layout hooks are skipped.\r\n */\r\nexport function getPreservedLayoutProps(): Record<string, any> | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[LAYOUT_PROPS_KEY] as Record<string, any> | undefined) ?? null;\r\n}\r\n\r\n/**\r\n * Sets preserved layout props in window storage.\r\n * These props are used when layout hooks are skipped in SPA navigation.\r\n */\r\nexport function setPreservedLayoutProps(props: Record<string, any> | null): void {\r\n if (typeof window === \"undefined\") {\r\n return;\r\n }\r\n if (props === null) {\r\n delete (window as any)[LAYOUT_PROPS_KEY];\r\n } else {\r\n (window as any)[LAYOUT_PROPS_KEY] = props;\r\n }\r\n}\r\n\r\nexport function getRouterData(): RouterData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return (window[ROUTER_DATA_KEY] as RouterData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n window[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for listeners (e.g. theme sync) to refresh client state\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function setRouterData(data: RouterData): void {\r\n window[ROUTER_DATA_KEY] = data;\r\n \r\n // Dispatch event for router data updates\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-router-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n","import { useState, useEffect } from \"react\";\r\n\r\n/**\r\n * Hook to detect if the component is mounted on the client.\r\n * Returns `false` during SSR and initial render, `true` after hydration.\r\n * \r\n * Useful for avoiding hydration mismatches when rendering content that depends on client-side state.\r\n * \r\n * @returns `true` if mounted on client, `false` during SSR\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const isMounted = useClientMounted();\r\n * \r\n * if (!isMounted) {\r\n * return <div>Loading...</div>;\r\n * }\r\n * \r\n * return <div>{window.innerWidth}</div>;\r\n * }\r\n * ```\r\n */\r\nexport function useClientMounted(): boolean {\r\n const [mounted, setMounted] = useState(false);\r\n\r\n useEffect(() => {\r\n setMounted(true);\r\n }, []);\r\n\r\n return mounted;\r\n}\r\n\r\n","import { useEffect, useLayoutEffect } from \"react\";\r\n\r\n/**\r\n * Hook that uses `useLayoutEffect` on the client and `useEffect` (no-op) on the server.\r\n * \r\n * This prevents React warnings about using `useLayoutEffect` during SSR.\r\n * `useLayoutEffect` runs synchronously after DOM mutations but before paint,\r\n * making it ideal for DOM measurements and synchronous updates.\r\n * \r\n * @param effect - Effect function\r\n * @param deps - Dependency array\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * useIsomorphicLayoutEffect(() => {\r\n * // This runs synchronously on client, not on server\r\n * const width = elementRef.current?.offsetWidth;\r\n * setWidth(width);\r\n * }, []);\r\n * \r\n * return <div ref={elementRef}>Content</div>;\r\n * }\r\n * ```\r\n */\r\nexport function useIsomorphicLayoutEffect(\r\n effect: React.EffectCallback,\r\n deps?: React.DependencyList\r\n): void {\r\n if (typeof window === \"undefined\") {\r\n // Server: use useEffect (no-op during SSR)\r\n useEffect(effect, deps);\r\n } else {\r\n // Client: use useLayoutEffect for synchronous execution\r\n useLayoutEffect(effect, deps);\r\n }\r\n}\r\n\r\n"],"mappings":"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","names":["useState","useEffect","useCallback","useContext","useRef","useContext","useRef","useEffect","useState","useCallback","useState","useEffect","useEffect"]}
|
package/dist/react/themes.cjs
CHANGED
|
@@ -3,10 +3,6 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
6
|
var __copyProps = (to, from, except, desc) => {
|
|
11
7
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
8
|
for (let key of __getOwnPropNames(from))
|
|
@@ -19,170 +15,5 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
15
|
|
|
20
16
|
// modules/react/themes/index.ts
|
|
21
17
|
var themes_exports = {};
|
|
22
|
-
__export(themes_exports, {
|
|
23
|
-
ThemeProvider: () => ThemeProvider,
|
|
24
|
-
useTheme: () => useTheme
|
|
25
|
-
});
|
|
26
18
|
module.exports = __toCommonJS(themes_exports);
|
|
27
|
-
|
|
28
|
-
// modules/react/themes/theme-provider/index.tsx
|
|
29
|
-
var import_react2 = require("react");
|
|
30
|
-
|
|
31
|
-
// modules/react/hooks/useBroadcastChannel/index.tsx
|
|
32
|
-
var import_react = require("react");
|
|
33
|
-
var useBroadcastChannel = (channelName) => {
|
|
34
|
-
const [message, setMessage] = (0, import_react.useState)(null);
|
|
35
|
-
const channelRef = (0, import_react.useRef)(null);
|
|
36
|
-
(0, import_react.useEffect)(() => {
|
|
37
|
-
if (!channelRef.current && typeof window !== "undefined") {
|
|
38
|
-
channelRef.current = new BroadcastChannel(channelName);
|
|
39
|
-
}
|
|
40
|
-
const channel = channelRef.current;
|
|
41
|
-
if (!channel) return;
|
|
42
|
-
const handleMessage = (event) => {
|
|
43
|
-
setMessage(event.data);
|
|
44
|
-
};
|
|
45
|
-
channel.onmessage = handleMessage;
|
|
46
|
-
return () => {
|
|
47
|
-
if (channelRef.current) {
|
|
48
|
-
channelRef.current.close();
|
|
49
|
-
channelRef.current = null;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}, [channelName]);
|
|
53
|
-
const sendMessage = (0, import_react.useCallback)((msg) => {
|
|
54
|
-
if (channelRef.current) {
|
|
55
|
-
channelRef.current.postMessage(msg);
|
|
56
|
-
}
|
|
57
|
-
}, []);
|
|
58
|
-
return { message, sendMessage };
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// modules/react/themes/theme-provider/index.tsx
|
|
62
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
63
|
-
var ThemeContext = (0, import_react2.createContext)({ theme: "light", handleThemeChange: () => {
|
|
64
|
-
} });
|
|
65
|
-
function getCookie(name) {
|
|
66
|
-
if (typeof document === "undefined") return null;
|
|
67
|
-
const value = `; ${document.cookie}`;
|
|
68
|
-
const parts = value.split(`; ${name}=`);
|
|
69
|
-
if (parts.length === 2) {
|
|
70
|
-
return parts.pop()?.split(";").shift() || null;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
var ThemeProvider = ({
|
|
75
|
-
children,
|
|
76
|
-
initialTheme
|
|
77
|
-
}) => {
|
|
78
|
-
const { message: themeMessage, sendMessage } = useBroadcastChannel("theme_channel");
|
|
79
|
-
const lastSentRef = (0, import_react2.useRef)(null);
|
|
80
|
-
const [theme, setTheme] = (0, import_react2.useState)(() => {
|
|
81
|
-
if (initialTheme) return initialTheme;
|
|
82
|
-
if (typeof window !== "undefined") {
|
|
83
|
-
const windowData = window.__FW_DATA__;
|
|
84
|
-
if (windowData?.theme) return windowData.theme;
|
|
85
|
-
}
|
|
86
|
-
if (typeof window !== "undefined") {
|
|
87
|
-
const cookieTheme = getCookie("theme");
|
|
88
|
-
if (cookieTheme) return cookieTheme;
|
|
89
|
-
}
|
|
90
|
-
return "light";
|
|
91
|
-
});
|
|
92
|
-
(0, import_react2.useEffect)(() => {
|
|
93
|
-
if (!themeMessage) return;
|
|
94
|
-
if (themeMessage === lastSentRef.current) {
|
|
95
|
-
lastSentRef.current = null;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
setTheme((currentTheme) => {
|
|
99
|
-
if (themeMessage !== currentTheme) {
|
|
100
|
-
if (typeof document !== "undefined") {
|
|
101
|
-
document.cookie = `theme=${themeMessage}; path=/; max-age=31536000`;
|
|
102
|
-
}
|
|
103
|
-
if (typeof window !== "undefined") {
|
|
104
|
-
if (!window.__FW_DATA__) {
|
|
105
|
-
window.__FW_DATA__ = {};
|
|
106
|
-
}
|
|
107
|
-
window.__FW_DATA__ = {
|
|
108
|
-
...window.__FW_DATA__,
|
|
109
|
-
theme: themeMessage
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return themeMessage;
|
|
113
|
-
}
|
|
114
|
-
return currentTheme;
|
|
115
|
-
});
|
|
116
|
-
}, [themeMessage]);
|
|
117
|
-
(0, import_react2.useEffect)(() => {
|
|
118
|
-
const handleDataRefresh = () => {
|
|
119
|
-
if (typeof window !== "undefined") {
|
|
120
|
-
const windowData = window.__FW_DATA__;
|
|
121
|
-
if (windowData?.theme) {
|
|
122
|
-
setTheme((currentTheme) => {
|
|
123
|
-
if (windowData.theme !== currentTheme) {
|
|
124
|
-
return windowData.theme;
|
|
125
|
-
}
|
|
126
|
-
return currentTheme;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
if (typeof window !== "undefined") {
|
|
132
|
-
window.addEventListener("fw-data-refresh", handleDataRefresh);
|
|
133
|
-
return () => {
|
|
134
|
-
window.removeEventListener("fw-data-refresh", handleDataRefresh);
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}, []);
|
|
138
|
-
(0, import_react2.useEffect)(() => {
|
|
139
|
-
if (initialTheme && initialTheme !== theme) {
|
|
140
|
-
setTheme(initialTheme);
|
|
141
|
-
}
|
|
142
|
-
}, [initialTheme]);
|
|
143
|
-
(0, import_react2.useEffect)(() => {
|
|
144
|
-
if (typeof document === "undefined") return;
|
|
145
|
-
const body = document.body;
|
|
146
|
-
const currentClasses = body.className.split(" ").filter(Boolean);
|
|
147
|
-
const themeClasses = ["light", "dark"];
|
|
148
|
-
const filteredClasses = currentClasses.filter(
|
|
149
|
-
(cls) => !themeClasses.includes(cls)
|
|
150
|
-
);
|
|
151
|
-
const newClassName = [...filteredClasses, theme].filter(Boolean).join(" ");
|
|
152
|
-
if (body.className !== newClassName) {
|
|
153
|
-
body.className = newClassName;
|
|
154
|
-
}
|
|
155
|
-
}, [theme]);
|
|
156
|
-
const handleThemeChange = (newTheme) => {
|
|
157
|
-
setTheme(newTheme);
|
|
158
|
-
if (typeof document !== "undefined") {
|
|
159
|
-
document.cookie = `theme=${newTheme}; path=/; max-age=31536000`;
|
|
160
|
-
}
|
|
161
|
-
if (typeof window !== "undefined") {
|
|
162
|
-
if (!window.__FW_DATA__) {
|
|
163
|
-
window.__FW_DATA__ = {};
|
|
164
|
-
}
|
|
165
|
-
window.__FW_DATA__ = {
|
|
166
|
-
...window.__FW_DATA__,
|
|
167
|
-
theme: newTheme
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
lastSentRef.current = newTheme;
|
|
171
|
-
sendMessage(newTheme);
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
if (lastSentRef.current === newTheme) {
|
|
174
|
-
lastSentRef.current = null;
|
|
175
|
-
}
|
|
176
|
-
}, 500);
|
|
177
|
-
};
|
|
178
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value: { theme, handleThemeChange }, children });
|
|
179
|
-
};
|
|
180
|
-
var useTheme = () => {
|
|
181
|
-
return (0, import_react2.useContext)(ThemeContext);
|
|
182
|
-
};
|
|
183
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
184
|
-
0 && (module.exports = {
|
|
185
|
-
ThemeProvider,
|
|
186
|
-
useTheme
|
|
187
|
-
});
|
|
188
19
|
//# sourceMappingURL=themes.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../modules/react/themes/index.ts","../../modules/react/themes/theme-provider/index.tsx","../../modules/react/hooks/useBroadcastChannel/index.tsx"],"sourcesContent":["export {\r\n ThemeProvider,\r\n useTheme\r\n} from './theme-provider';","import React, { createContext, useContext, useState, useEffect, useRef } from \"react\";\r\nimport { useBroadcastChannel } from \"../../hooks/useBroadcastChannel\";\r\n\r\nconst ThemeContext = createContext<{\r\n theme: string;\r\n handleThemeChange: (theme: string) => void;\r\n}>({ theme: \"light\", handleThemeChange: () => {} });\r\n\r\n// Helper function to get cookie value\r\nfunction getCookie(name: string): string | null {\r\n if (typeof document === \"undefined\") return null;\r\n const value = `; ${document.cookie}`;\r\n const parts = value.split(`; ${name}=`);\r\n if (parts.length === 2) {\r\n return parts.pop()?.split(\";\").shift() || null;\r\n }\r\n return null;\r\n}\r\n\r\nexport const ThemeProvider = ({ \r\n children,\r\n initialTheme \r\n}: { \r\n children: React.ReactNode;\r\n initialTheme?: string;\r\n}) => {\r\n const { message: themeMessage, sendMessage } = useBroadcastChannel('theme_channel');\r\n \r\n // Track what we last sent to avoid loops\r\n const lastSentRef = useRef<string | null>(null);\r\n\r\n // Initialize theme consistently between server and client\r\n const [theme, setTheme] = useState<string>(() => {\r\n if (initialTheme) return initialTheme;\r\n \r\n if (typeof window !== \"undefined\") {\r\n const windowData = (window as any).__FW_DATA__;\r\n if (windowData?.theme) return windowData.theme;\r\n }\r\n \r\n if (typeof window !== \"undefined\") {\r\n const cookieTheme = getCookie(\"theme\");\r\n if (cookieTheme) return cookieTheme;\r\n }\r\n \r\n return \"light\";\r\n });\r\n\r\n // Handle messages from broadcast channel (other tabs)\r\n // This effect ONLY responds to themeMessage changes, not theme changes\r\n useEffect(() => {\r\n if (!themeMessage) return;\r\n \r\n // Ignore if this is a message we just sent\r\n if (themeMessage === lastSentRef.current) {\r\n lastSentRef.current = null;\r\n return;\r\n }\r\n \r\n // Only update if different from current theme\r\n setTheme((currentTheme) => {\r\n if (themeMessage !== currentTheme) {\r\n // Update cookie\r\n if (typeof document !== \"undefined\") {\r\n document.cookie = `theme=${themeMessage}; path=/; max-age=31536000`;\r\n }\r\n \r\n // Update window data\r\n if (typeof window !== \"undefined\") {\r\n if (!(window as any).__FW_DATA__) {\r\n (window as any).__FW_DATA__ = {};\r\n }\r\n (window as any).__FW_DATA__ = {\r\n ...(window as any).__FW_DATA__,\r\n theme: themeMessage,\r\n };\r\n }\r\n \r\n return themeMessage;\r\n }\r\n return currentTheme;\r\n });\r\n }, [themeMessage]); // Only depend on themeMessage, NOT theme!\r\n\r\n // Handle window.__FW_DATA__ changes during SPA navigation\r\n useEffect(() => {\r\n const handleDataRefresh = () => {\r\n if (typeof window !== \"undefined\") {\r\n const windowData = (window as any).__FW_DATA__;\r\n if (windowData?.theme) {\r\n setTheme((currentTheme) => {\r\n if (windowData.theme !== currentTheme) {\r\n return windowData.theme;\r\n }\r\n return currentTheme;\r\n });\r\n }\r\n }\r\n };\r\n\r\n if (typeof window !== \"undefined\") {\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n };\r\n }\r\n }, []); // No dependencies - event listener doesn't need theme\r\n\r\n // Handle initialTheme prop changes\r\n useEffect(() => {\r\n if (initialTheme && initialTheme !== theme) {\r\n setTheme(initialTheme);\r\n }\r\n }, [initialTheme]); // Only depend on initialTheme, not theme\r\n\r\n // Update body class when theme changes\r\n useEffect(() => {\r\n if (typeof document === \"undefined\") return;\r\n\r\n const body = document.body;\r\n const currentClasses = body.className.split(\" \").filter(Boolean);\r\n const themeClasses = [\"light\", \"dark\"];\r\n const filteredClasses = currentClasses.filter(\r\n (cls) => !themeClasses.includes(cls)\r\n );\r\n const newClassName = [...filteredClasses, theme].filter(Boolean).join(\" \");\r\n \r\n if (body.className !== newClassName) {\r\n body.className = newClassName;\r\n }\r\n }, [theme]);\r\n\r\n const handleThemeChange = (newTheme: string) => {\r\n // Update state immediately\r\n setTheme(newTheme);\r\n\r\n // Update cookie\r\n if (typeof document !== \"undefined\") {\r\n document.cookie = `theme=${newTheme}; path=/; max-age=31536000`;\r\n }\r\n\r\n // Update window data\r\n if (typeof window !== \"undefined\") {\r\n if (!(window as any).__FW_DATA__) {\r\n (window as any).__FW_DATA__ = {};\r\n }\r\n (window as any).__FW_DATA__ = {\r\n ...(window as any).__FW_DATA__,\r\n theme: newTheme,\r\n };\r\n }\r\n \r\n // Mark this as the last value we sent\r\n lastSentRef.current = newTheme;\r\n \r\n // Broadcast to other tabs\r\n sendMessage(newTheme);\r\n \r\n // Clear the ref after a delay\r\n setTimeout(() => {\r\n if (lastSentRef.current === newTheme) {\r\n lastSentRef.current = null;\r\n }\r\n }, 500);\r\n };\r\n\r\n return (\r\n <ThemeContext.Provider value={{ theme, handleThemeChange }}>\r\n {children}\r\n </ThemeContext.Provider>\r\n );\r\n};\r\n\r\nexport const useTheme = () => {\r\n return useContext(ThemeContext);\r\n};\r\n","import React, { useEffect, useState, useRef, useCallback } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channelRef = useRef<BroadcastChannel | null>(null);\r\n\r\n useEffect(() => {\r\n // Create channel only once, inside useEffect\r\n if (!channelRef.current && typeof window !== \"undefined\") {\r\n channelRef.current = new BroadcastChannel(channelName);\r\n }\r\n\r\n const channel = channelRef.current;\r\n if (!channel) return;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n if (channelRef.current) {\r\n channelRef.current.close();\r\n channelRef.current = null;\r\n }\r\n };\r\n }, [channelName]);\r\n\r\n const sendMessage = useCallback((msg: unknown) => {\r\n if (channelRef.current) {\r\n channelRef.current.postMessage(msg);\r\n }\r\n }, []);\r\n\r\n return { message, sendMessage };\r\n};\r\n"],"mappings":"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","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../../modules/react/themes/index.ts"],"sourcesContent":["export type { LolyThemeAPI, ThemeScriptOptions } from \"./theme-global\";"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/dist/react/themes.d.mts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
initialTheme?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
handleThemeChange: (theme: string) => void;
|
|
1
|
+
type LolyThemeAPI = {
|
|
2
|
+
set: (theme: string) => void;
|
|
3
|
+
get: () => string;
|
|
4
|
+
};
|
|
5
|
+
type ThemeScriptOptions = {
|
|
6
|
+
initialTheme?: string | null;
|
|
7
|
+
channelName?: string;
|
|
8
|
+
cookieName?: string;
|
|
9
|
+
themeClasses?: string[];
|
|
11
10
|
};
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
loly?: {
|
|
14
|
+
theme?: LolyThemeAPI;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
export {
|
|
20
|
+
export type { LolyThemeAPI, ThemeScriptOptions };
|
package/dist/react/themes.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
initialTheme?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
handleThemeChange: (theme: string) => void;
|
|
1
|
+
type LolyThemeAPI = {
|
|
2
|
+
set: (theme: string) => void;
|
|
3
|
+
get: () => string;
|
|
4
|
+
};
|
|
5
|
+
type ThemeScriptOptions = {
|
|
6
|
+
initialTheme?: string | null;
|
|
7
|
+
channelName?: string;
|
|
8
|
+
cookieName?: string;
|
|
9
|
+
themeClasses?: string[];
|
|
11
10
|
};
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
loly?: {
|
|
14
|
+
theme?: LolyThemeAPI;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
export {
|
|
20
|
+
export type { LolyThemeAPI, ThemeScriptOptions };
|