lilact 0.0.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/Icon/r +0 -0
- package/LICENSE.TXT +26 -0
- package/README.md +116 -0
- package/bin/copy-demo.js +26 -0
- package/bin/transpile-dir.js +112 -0
- package/bin/transpile.js +137 -0
- package/dist/lilact.development.js +9344 -0
- package/dist/lilact.development.js.map +1 -0
- package/dist/lilact.production.min.js +63 -0
- package/dist/lilact.production.min.js.map +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +71 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/classes/accessories.ErrorBoundary.html +37 -0
- package/docs/classes/accessories.Suspense.html +41 -0
- package/docs/classes/components.Component.html +45 -0
- package/docs/classes/components.HTMLComponent.html +32 -0
- package/docs/classes/components.RootComponent.html +32 -0
- package/docs/classes/transition.Transition.html +31 -0
- package/docs/functions/accessories.Spinner.html +9 -0
- package/docs/functions/components.createComponent.html +8 -0
- package/docs/functions/components.createRoot.html +5 -0
- package/docs/functions/components.render.html +5 -0
- package/docs/functions/hooks.createContext.html +3 -0
- package/docs/functions/hooks.useActionState.html +3 -0
- package/docs/functions/hooks.useCallback.html +5 -0
- package/docs/functions/hooks.useContext.html +3 -0
- package/docs/functions/hooks.useDeferredValue.html +4 -0
- package/docs/functions/hooks.useEffect.html +3 -0
- package/docs/functions/hooks.useHook.html +5 -0
- package/docs/functions/hooks.useId.html +4 -0
- package/docs/functions/hooks.useLayoutEffect.html +3 -0
- package/docs/functions/hooks.useLocalStorage.html +4 -0
- package/docs/functions/hooks.useMemo.html +4 -0
- package/docs/functions/hooks.useReducer.html +6 -0
- package/docs/functions/hooks.useRef.html +4 -0
- package/docs/functions/hooks.useState.html +3 -0
- package/docs/functions/hooks.useTransition.html +3 -0
- package/docs/functions/jsx.transpileJSX.html +6 -0
- package/docs/functions/misc.Fragment.html +3 -0
- package/docs/functions/misc.classNames.html +2 -0
- package/docs/functions/misc.deepEqual.html +3 -0
- package/docs/functions/misc.findDOMNode.html +3 -0
- package/docs/functions/misc.getComponentByPointer.html +3 -0
- package/docs/functions/misc.isAsync.html +3 -0
- package/docs/functions/misc.isClass.html +3 -0
- package/docs/functions/misc.isEmpty.html +3 -0
- package/docs/functions/misc.isError.html +3 -0
- package/docs/functions/misc.isThenable.html +3 -0
- package/docs/functions/misc.isValidElement.html +3 -0
- package/docs/functions/misc.shallowEqual.html +3 -0
- package/docs/functions/redux.Provider.html +5 -0
- package/docs/functions/redux.connect.html +5 -0
- package/docs/functions/redux.useDispatch.html +3 -0
- package/docs/functions/redux.useSelector.html +5 -0
- package/docs/functions/redux.useStore.html +3 -0
- package/docs/functions/router.HashRouter.html +5 -0
- package/docs/functions/router.Link.html +12 -0
- package/docs/functions/router.NavLink.html +10 -0
- package/docs/functions/router.Route.html +6 -0
- package/docs/functions/router.Routes.html +4 -0
- package/docs/functions/router.useLocation.html +3 -0
- package/docs/functions/router.useNavigate.html +3 -0
- package/docs/functions/run.require.html +14 -0
- package/docs/functions/run.run.html +13 -0
- package/docs/functions/run.runScripts.html +7 -0
- package/docs/functions/run.traceError.html +6 -0
- package/docs/functions/timers.animationFramePromise.html +3 -0
- package/docs/functions/timers.clearInterval.html +2 -0
- package/docs/functions/timers.clearTimeout.html +2 -0
- package/docs/functions/timers.grabTimers.html +3 -0
- package/docs/functions/timers.pauseTimers.html +2 -0
- package/docs/functions/timers.releaseTimers.html +2 -0
- package/docs/functions/timers.resetTimers.html +2 -0
- package/docs/functions/timers.resumeTimers.html +2 -0
- package/docs/functions/timers.setInterval.html +5 -0
- package/docs/functions/timers.setTimeout.html +4 -0
- package/docs/functions/timers.timeoutPromise.html +4 -0
- package/docs/functions/transition.CSSTransition.html +18 -0
- package/docs/functions/transition.TransitionGroup.html +5 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +88 -0
- package/docs/media/icon.png +0 -0
- package/docs/modules/accessories.html +1 -0
- package/docs/modules/components.html +1 -0
- package/docs/modules/hooks.html +1 -0
- package/docs/modules/jsx.html +1 -0
- package/docs/modules/misc.html +1 -0
- package/docs/modules/redux.html +1 -0
- package/docs/modules/router.html +1 -0
- package/docs/modules/run.html +1 -0
- package/docs/modules/timers.html +1 -0
- package/docs/modules/transition.html +1 -0
- package/docs/modules.html +1 -0
- package/docs/static/demos/actionstate.jsx +41 -0
- package/docs/static/demos/boundary.jsx +52 -0
- package/docs/static/demos/context.jsx +68 -0
- package/docs/static/demos/css-transition.jsx +65 -0
- package/docs/static/demos/modal.jsx +41 -0
- package/docs/static/demos/proptypes.jsx +37 -0
- package/docs/static/demos/reducer.jsx +71 -0
- package/docs/static/demos/redux.jsx +58 -0
- package/docs/static/demos/router.jsx +153 -0
- package/docs/static/demos/stopwatch.jsx +42 -0
- package/docs/static/demos/suspense.jsx +48 -0
- package/docs/static/demos/transition.jsx +40 -0
- package/docs/static/demos/use-differed.jsx +30 -0
- package/docs/static/demos/usetransition.jsx +37 -0
- package/docs/static/index.html +212 -0
- package/docs/static/lilact.development.js +9344 -0
- package/docs/static/lilact.production.min.js +63 -0
- package/docs/static/prismjs/LICENSE.md +21 -0
- package/docs/static/prismjs/prism-jsx.min.js +1 -0
- package/docs/static/prismjs/prism.min.css +1 -0
- package/docs/static/prismjs/prism.min.js +16 -0
- package/docs/variables/jsx.transpilerConfig.html +1 -0
- package/docs/variables/misc.Children.html +10 -0
- package/icon.png +0 -0
- package/package.json +39 -0
- package/root/demos/actionstate.jsx +41 -0
- package/root/demos/boundary.jsx +52 -0
- package/root/demos/context.jsx +68 -0
- package/root/demos/css-transition.jsx +65 -0
- package/root/demos/modal.jsx +41 -0
- package/root/demos/proptypes.jsx +37 -0
- package/root/demos/reducer.jsx +71 -0
- package/root/demos/redux.jsx +58 -0
- package/root/demos/router.jsx +153 -0
- package/root/demos/stopwatch.jsx +42 -0
- package/root/demos/suspense.jsx +48 -0
- package/root/demos/transition.jsx +40 -0
- package/root/demos/use-differed.jsx +30 -0
- package/root/demos/usetransition.jsx +37 -0
- package/root/index.html +212 -0
- package/root/lilact.development.js +9344 -0
- package/root/lilact.production.min.js +63 -0
- package/root/prismjs/LICENSE.md +21 -0
- package/root/prismjs/prism-jsx.min.js +1 -0
- package/root/prismjs/prism.min.css +1 -0
- package/root/prismjs/prism.min.js +16 -0
- package/src/accessories.jsx +287 -0
- package/src/components.jsx +1063 -0
- package/src/events.jsx +175 -0
- package/src/hooks.jsx +461 -0
- package/src/jsx.addons.js +70 -0
- package/src/jsx.js +1168 -0
- package/src/lilact.jsx +115 -0
- package/src/loader.cjs +59 -0
- package/src/misc.jsx +419 -0
- package/src/redux.jsx +198 -0
- package/src/router.jsx +282 -0
- package/src/run.jsx +275 -0
- package/src/timers.jsx +313 -0
- package/src/transition.jsx +244 -0
- package/src/vlq.js +94 -0
- package/tsconfig.json +15 -0
- package/typedoc.json +23 -0
- package/webpack.config.js +196 -0
package/src/redux.jsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Lilact
|
|
4
|
+
Copyright (C) 2024-2026 Arash Kazemi <contact.arash.kazemi@gmail.com>
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
BSD-2-Clause
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without
|
|
10
|
+
modification, are permitted provided that the following conditions are met:
|
|
11
|
+
|
|
12
|
+
* Redistributions of source code must retain the above copyright
|
|
13
|
+
notice, this list of conditions and the following disclaimer.
|
|
14
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
15
|
+
notice, this list of conditions and the following disclaimer in the
|
|
16
|
+
documentation and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
+
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
22
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
25
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
27
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ʔ defineSymbols ( "LILACT", [ "CORE" ] ) ʔ
|
|
33
|
+
|
|
34
|
+
let ReduxContext;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Provider component that supplies the store instance to the component tree.
|
|
38
|
+
*
|
|
39
|
+
* @param props - Component props.
|
|
40
|
+
* @param props.store - Store instance to make available to descendants.
|
|
41
|
+
* @param props.children - Components that can access the store via hooks.
|
|
42
|
+
*/
|
|
43
|
+
export function Provider({ store, children })
|
|
44
|
+
{
|
|
45
|
+
ReduxContext ??= Lilact.createContext(null);
|
|
46
|
+
return (
|
|
47
|
+
<ReduxContext.Provider value={store}>{children}</ReduxContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook that returns the current store instance from the nearest Provider.
|
|
53
|
+
*
|
|
54
|
+
* @returns The store object from the Provider context.
|
|
55
|
+
*/
|
|
56
|
+
export function useStore()
|
|
57
|
+
{
|
|
58
|
+
const store = Lilact.useContext(ReduxContext);
|
|
59
|
+
if (!store) {
|
|
60
|
+
throw new Error("Could not find Redux store in context. <Provider> is missing.");
|
|
61
|
+
}
|
|
62
|
+
return store;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hook that returns the store's dispatch function from the nearest Provider.
|
|
68
|
+
*
|
|
69
|
+
* @returns The dispatch function used to send actions to the store.
|
|
70
|
+
*/
|
|
71
|
+
export function useDispatch()
|
|
72
|
+
{
|
|
73
|
+
const store = Lilact.useStore();
|
|
74
|
+
return store.dispatch;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hook that returns selected data from the store using a selector.
|
|
79
|
+
*
|
|
80
|
+
* @param selector - Function that derives a value from the store state.
|
|
81
|
+
* @param [equalityFn=(a, b) => a === b] - Determines when the selected value should update.
|
|
82
|
+
* @returns The selected slice of state.
|
|
83
|
+
*/
|
|
84
|
+
export function useSelector(selector, equalityFn = (a, b) => a === b)
|
|
85
|
+
{
|
|
86
|
+
const store = Lilact.useStore();
|
|
87
|
+
const latestSelectedRef = Lilact.useRef();
|
|
88
|
+
const latestSelectorRef = Lilact.useRef();
|
|
89
|
+
const [, forceRender] = Lilact.useState(0);
|
|
90
|
+
|
|
91
|
+
if (!latestSelectorRef.current) {
|
|
92
|
+
latestSelectorRef.current = selector;
|
|
93
|
+
}
|
|
94
|
+
latestSelectorRef.current = selector;
|
|
95
|
+
|
|
96
|
+
let selected = selector(store.getState()); // todo: should exception be covered?
|
|
97
|
+
|
|
98
|
+
if (latestSelectedRef.current === undefined) {
|
|
99
|
+
latestSelectedRef.current = selected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Lilact.useLayoutEffect(() => {
|
|
103
|
+
// keep latestSelectedRef in sync after render (in case selector used props or similar)
|
|
104
|
+
latestSelectedRef.current = selected;
|
|
105
|
+
}, [selected]);
|
|
106
|
+
|
|
107
|
+
Lilact.useEffect(() => {
|
|
108
|
+
|
|
109
|
+
let didUnsubscribe = false;
|
|
110
|
+
|
|
111
|
+
function checkForUpdates() {
|
|
112
|
+
if (didUnsubscribe) return;
|
|
113
|
+
try {
|
|
114
|
+
const newSelected = latestSelectorRef.current(store.getState());
|
|
115
|
+
const oldSelected = latestSelectedRef.current;
|
|
116
|
+
if (!equalityFn(oldSelected, newSelected)) {
|
|
117
|
+
latestSelectedRef.current = newSelected;
|
|
118
|
+
|
|
119
|
+
forceRender((n) => n + 1);
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
|
|
123
|
+
ʔ if(DEBUG) {
|
|
124
|
+
// todo: which is better? cover the error or throw?
|
|
125
|
+
|
|
126
|
+
// If selector throws during subscription notifications, re-render to surface error
|
|
127
|
+
// (could instead log or ignore)
|
|
128
|
+
// throw err;
|
|
129
|
+
console.warn(err);
|
|
130
|
+
ʔ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const unsubscribe = store.subscribe(checkForUpdates);
|
|
135
|
+
|
|
136
|
+
// run check once in case state changed between render and effect
|
|
137
|
+
checkForUpdates();
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
didUnsubscribe = true;
|
|
141
|
+
unsubscribe();
|
|
142
|
+
};
|
|
143
|
+
}, [store, equalityFn]);
|
|
144
|
+
|
|
145
|
+
return latestSelectedRef.current;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Higher-order component that connects a component to store state and dispatch.
|
|
151
|
+
*
|
|
152
|
+
* @param mapStateToProps - Maps store state to the wrapped component's props.
|
|
153
|
+
* @param mapDispatchToProps - Maps dispatch (or action creators) to the wrapped component's props.
|
|
154
|
+
* @returns A connected component that receives store-derived props.
|
|
155
|
+
*/
|
|
156
|
+
export function connect(mapStateToProps, mapDispatchToProps)
|
|
157
|
+
{
|
|
158
|
+
const shouldSubscribe = Boolean(mapStateToProps);
|
|
159
|
+
|
|
160
|
+
return function wrapWithConnect(WrappedComponent) {
|
|
161
|
+
|
|
162
|
+
function ConnectedComponent(props) {
|
|
163
|
+
const store = Lilact.useStore();
|
|
164
|
+
|
|
165
|
+
let dispatchProps = { dispatch: store.dispatch };
|
|
166
|
+
|
|
167
|
+
if (typeof mapDispatchToProps === "function") {
|
|
168
|
+
dispatchProps = mapDispatchToProps(store.dispatch, props);
|
|
169
|
+
}
|
|
170
|
+
else if (typeof mapDispatchToProps === "object" && mapDispatchToProps !== null) {
|
|
171
|
+
dispatchProps = {};
|
|
172
|
+
const dispatch = store.dispatch;
|
|
173
|
+
for (const key in mapDispatchToProps) {
|
|
174
|
+
const actionCreator = mapDispatchToProps[key];
|
|
175
|
+
dispatchProps[key] = (...args) => dispatch(actionCreator(...args));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let stateProps = {};
|
|
180
|
+
if (mapStateToProps) {
|
|
181
|
+
const selector = (state) => mapStateToProps(state, props);
|
|
182
|
+
|
|
183
|
+
// todo: is shallowEqual enough?
|
|
184
|
+
stateProps = Lilact.useSelector(selector, Lilact.shallowEqual) || {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const mergedProps = { ...props, ...stateProps, ...dispatchProps };
|
|
188
|
+
|
|
189
|
+
return <WrappedComponent {...mergedProps} />;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// todo: memoize to avoid unnecessary re-renders if parent props are same?
|
|
193
|
+
//return memo(ConnectedComponent);
|
|
194
|
+
|
|
195
|
+
return ConnectedComponent;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
package/src/router.jsx
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Lilact
|
|
4
|
+
Copyright (C) 2024-2026 Arash Kazemi <contact.arash.kazemi@gmail.com>
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
BSD-2-Clause
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without
|
|
10
|
+
modification, are permitted provided that the following conditions are met:
|
|
11
|
+
|
|
12
|
+
* Redistributions of source code must retain the above copyright
|
|
13
|
+
notice, this list of conditions and the following disclaimer.
|
|
14
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
15
|
+
notice, this list of conditions and the following disclaimer in the
|
|
16
|
+
documentation and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
+
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
22
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
25
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
27
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
ʔ defineSymbols ( "LILACT", [ "CORE", "COMPONENT" ] ) ʔ
|
|
32
|
+
|
|
33
|
+
import {createContext, useContext, useState, useCallback, useEffect} from "./hooks.jsx"
|
|
34
|
+
import {Children} from "./misc.jsx"
|
|
35
|
+
|
|
36
|
+
const RouterContext = createContext(null);
|
|
37
|
+
const RouteContext = createContext({ params: {} });
|
|
38
|
+
|
|
39
|
+
// --- HashRouter (as before) ---
|
|
40
|
+
const createURL = (to) => (typeof to === "string" ? to : (to.pathname || "") + (to.search || "") + (to.hash || ""));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hash-based router component that syncs navigation state with the URL hash (#...).
|
|
44
|
+
*
|
|
45
|
+
* @param props - Component props.
|
|
46
|
+
* @param props.children - Route definitions and/or components to render under this router.
|
|
47
|
+
* @param [props.basename=""] - Optional base path prefix applied to all route paths.
|
|
48
|
+
*/
|
|
49
|
+
export function HashRouter({ children, basename = "" }) {
|
|
50
|
+
const readLocation = () => {
|
|
51
|
+
const raw = window.location.hash || "#/";
|
|
52
|
+
const full = raw.slice(1);
|
|
53
|
+
const withoutBase = full.replace(new RegExp(`^${basename}`), "") || "/";
|
|
54
|
+
const [pathAndSearch, hashPart] = withoutBase.split("#");
|
|
55
|
+
const [path, search = ""] = pathAndSearch.split("?");
|
|
56
|
+
return {
|
|
57
|
+
pathname: path || "/",
|
|
58
|
+
search: search ? "?" + search : "",
|
|
59
|
+
hash: hashPart ? "#" + hashPart : "",
|
|
60
|
+
state: history.state?.__state,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const [location, setLocation] = useState(readLocation);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const onHash = () => setLocation(readLocation());
|
|
67
|
+
window.addEventListener("hashchange", onHash);
|
|
68
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
69
|
+
}, [basename]);
|
|
70
|
+
|
|
71
|
+
const navigate = useCallback((to, { replace = false, state } = {}) => {
|
|
72
|
+
const url = createURL(to);
|
|
73
|
+
const href = "#" + (basename + url);
|
|
74
|
+
if (replace) history.replaceState({ __state: state }, "", href);
|
|
75
|
+
else history.pushState({ __state: state }, "", href);
|
|
76
|
+
setLocation(readLocation());
|
|
77
|
+
}, [basename]);
|
|
78
|
+
|
|
79
|
+
return <RouterContext.Provider value={{ location, navigate, basename }}>{children}</RouterContext.Provider>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hook that returns the current location object (path, search, hash, and navigation state).
|
|
84
|
+
*
|
|
85
|
+
* @returns The current location data for the active router context.
|
|
86
|
+
*/
|
|
87
|
+
export function useLocation() {
|
|
88
|
+
const ctx = useContext(RouterContext);
|
|
89
|
+
if (!ctx) throw new Error("useLocation must be used inside a Router");
|
|
90
|
+
return ctx.location;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Hook that returns a function used to imperatively navigate to a new location.
|
|
95
|
+
*
|
|
96
|
+
* @returns A navigate function for programmatic route changes (e.g., navigate(to, options)).
|
|
97
|
+
*/
|
|
98
|
+
export function useNavigate() {
|
|
99
|
+
const ctx = useContext(RouterContext);
|
|
100
|
+
if (!ctx) throw new Error("useNavigate must be used inside a Router");
|
|
101
|
+
return ctx.navigate;
|
|
102
|
+
}
|
|
103
|
+
function useParams() {
|
|
104
|
+
const ctx = useContext(RouteContext);
|
|
105
|
+
return ctx.params || {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generic Link component that renders an anchor (<a>) and forwards common navigation/link attributes.
|
|
110
|
+
*
|
|
111
|
+
* @param props - Component props.
|
|
112
|
+
* @param props.to - Target location (route or URL) the link points to.
|
|
113
|
+
* @param [props.replace=false] - If true, uses history replacement semantics instead of pushing a new entry.
|
|
114
|
+
* @param [props.state] - Optional state to associate with the navigation action.
|
|
115
|
+
* @param [props.onClick] - Click handler invoked when the link is activated.
|
|
116
|
+
* @param [props.target] - Specifies where to open the linked document (e.g., "_blank").
|
|
117
|
+
* @param [props.download] - Sets the download behavior for the link (download attribute).
|
|
118
|
+
* @param [props.className] - CSS class name(s) for styling the underlying element.
|
|
119
|
+
* @param [props.style] - Inline style object applied to the underlying element.
|
|
120
|
+
* @param [props.children] - Content rendered inside the link (link label, icons, etc.).
|
|
121
|
+
* @param [props.rest] - Additional props forwarded to the underlying <a>.
|
|
122
|
+
*/
|
|
123
|
+
export function Link({ to, replace = false, state, onClick, target, download, className, style, children, ...rest }) {
|
|
124
|
+
const navigate = useNavigate();
|
|
125
|
+
const href = "#" + createURL(to);
|
|
126
|
+
function handleClick(e) {
|
|
127
|
+
if (onClick) onClick(e);
|
|
128
|
+
if (
|
|
129
|
+
e.defaultPrevented ||
|
|
130
|
+
e.button !== 0 ||
|
|
131
|
+
(target && target !== "_self") ||
|
|
132
|
+
e.metaKey || e.altKey || e.ctrlKey || e.shiftKey
|
|
133
|
+
) return;
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
navigate(to, { replace, state });
|
|
136
|
+
}
|
|
137
|
+
return (
|
|
138
|
+
<a href={href} onClick={handleClick} target={target} download={download} className={className} style={style} {...rest}>
|
|
139
|
+
{children}
|
|
140
|
+
</a>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizePath(p) {
|
|
145
|
+
if (!p) return "/";
|
|
146
|
+
return p.replace(/\/+$/, "") || "/";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* React navigation link component (like a typed NavLink) that forwards props and supports common link attributes.
|
|
152
|
+
*
|
|
153
|
+
* @param props - Component props.
|
|
154
|
+
* @param props.to - Target location (route) to navigate to.
|
|
155
|
+
* @param [props.replace=false] - If true, replaces the current history entry instead of pushing a new one.
|
|
156
|
+
* @param [props.state] - Optional navigation state passed along with the route transition.
|
|
157
|
+
* @param [props.onClick] - Click handler invoked when the link is activated.
|
|
158
|
+
* @param [props.target] - Sets the link target attribute (e.g., "_blank").
|
|
159
|
+
* @param [props.download] - Enables download behavior for the link (sets the download attribute).
|
|
160
|
+
* @param [props.className] - CSS class name(s) applied to the underlying element.
|
|
161
|
+
* @param [props.style] - Inline styles applied to the underlying element.
|
|
162
|
+
* @param [props.children] - Content rendered inside the link.
|
|
163
|
+
* @param [props.rest] - Additional props forwarded to the underlying element.
|
|
164
|
+
*/
|
|
165
|
+
export function NavLink({
|
|
166
|
+
to,
|
|
167
|
+
end = false,
|
|
168
|
+
activeClassName = "active",
|
|
169
|
+
className,
|
|
170
|
+
activeStyle,
|
|
171
|
+
style,
|
|
172
|
+
replace = false,
|
|
173
|
+
state,
|
|
174
|
+
children,
|
|
175
|
+
onClick,
|
|
176
|
+
...rest
|
|
177
|
+
}) {
|
|
178
|
+
const navigate = useNavigate();
|
|
179
|
+
const location = useLocation();
|
|
180
|
+
const targetPath = typeof to === "string" ? to.split("?")[0].split("#")[0] : (to.pathname || "/");
|
|
181
|
+
const currentPath = location.pathname || "/";
|
|
182
|
+
const isActive = end ? normalizePath(currentPath) === normalizePath(targetPath) : normalizePath(currentPath).startsWith(normalizePath(targetPath));
|
|
183
|
+
const resolvedClassName = typeof className === "function" ? className({ isActive }) : className;
|
|
184
|
+
const finalClassName = [resolvedClassName, isActive ? activeClassName : null].filter(Boolean).join(" ") || undefined;
|
|
185
|
+
const resolvedStyle = typeof style === "function" ? style({ isActive }) : style;
|
|
186
|
+
const mergedStyle = isActive ? { ...(resolvedStyle || {}), ...(activeStyle || {}) } : resolvedStyle;
|
|
187
|
+
const href = "#" + createURL(to);
|
|
188
|
+
|
|
189
|
+
function handleClick(e) {
|
|
190
|
+
if (onClick) onClick(e);
|
|
191
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
navigate(to, { replace, state });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<a href={href} onClick={handleClick} className={finalClassName} style={mergedStyle} aria-current={isActive ? "page" : undefined} {...rest}>
|
|
198
|
+
{typeof children === "function" ? children({ isActive }) : children}
|
|
199
|
+
</a>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Simple route matching utility ---
|
|
204
|
+
// supports params like /users/:id and wildcard * at end (e.g., /files/*)
|
|
205
|
+
function compilePath(pattern) {
|
|
206
|
+
const paramNames = [];
|
|
207
|
+
// escape regex and replace :param and *
|
|
208
|
+
let regexSource = "^" + pattern.replace(/\/+$/,"").replace(/([.+?^=!:${}()|[\]\/\\])/g, "\\$1")
|
|
209
|
+
.replace(/\\\:([A-Za-z0-9_]+)/g, (_, name) => {
|
|
210
|
+
paramNames.push(name);
|
|
211
|
+
return "([^/]+)";
|
|
212
|
+
})
|
|
213
|
+
.replace(/\\\*$/g, "(.+?)?"); // trailing * -> capture rest optional
|
|
214
|
+
regexSource += "/?$";
|
|
215
|
+
const regex = new RegExp(regexSource);
|
|
216
|
+
return { regex, paramNames };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function matchPath(pattern, pathname) {
|
|
220
|
+
if (pattern == null) return { matched: true, params: {} }; // <Route index> like
|
|
221
|
+
const { regex, paramNames } = compilePath(pattern);
|
|
222
|
+
const m = regex.exec(pathname);
|
|
223
|
+
if (!m) return { matched: false };
|
|
224
|
+
const params = {};
|
|
225
|
+
paramNames.forEach((n, i) => (params[n] = decodeURIComponent(m[i + 1] || "")));
|
|
226
|
+
// if wildcard captured as last group without a name, include as splat
|
|
227
|
+
if (m.length > paramNames.length + 1) {
|
|
228
|
+
params["*"] = m[paramNames.length + 1] ? decodeURIComponent(m[paramNames.length + 1]) : undefined;
|
|
229
|
+
}
|
|
230
|
+
return { matched: true, params };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Route and Routes components ---
|
|
234
|
+
/**
|
|
235
|
+
* Route configuration component that maps a URL path to a rendered element.
|
|
236
|
+
*
|
|
237
|
+
* @param props - Component props.
|
|
238
|
+
* @param props.path - The path pattern this route should match (e.g., "/users/:id").
|
|
239
|
+
* @param [props.element=null] - The React element to render when the route matches.
|
|
240
|
+
* @param [props.children] - Optional nested route definitions or additional content under this route.
|
|
241
|
+
*/
|
|
242
|
+
export function Route({ path, element = null, children }) {
|
|
243
|
+
// Route is used as a descriptor only inside <Routes>
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Route container component that groups multiple <Route> definitions.
|
|
248
|
+
*
|
|
249
|
+
* @param props - Component props.
|
|
250
|
+
* @param props.children - One or more route elements to register under the router.
|
|
251
|
+
*/
|
|
252
|
+
export function Routes({ children }) {
|
|
253
|
+
const location = useLocation();
|
|
254
|
+
const pathname = location.pathname || "/";
|
|
255
|
+
// flatten children (React elements)
|
|
256
|
+
const routes = Children.toArray(children);
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < routes.length; i++) {
|
|
259
|
+
const route = routes[i];
|
|
260
|
+
// accept <Route path="..." element={<.../>} />
|
|
261
|
+
const path = route.props.path === undefined ? null : route.props.path;
|
|
262
|
+
const element = route.props.element ?? null;
|
|
263
|
+
const childRoutes = route.props.children;
|
|
264
|
+
|
|
265
|
+
const { matched, params } = matchPath(path, pathname);
|
|
266
|
+
if (matched) {
|
|
267
|
+
// if element is null but has children (nested routes), attempt to render nested Routes
|
|
268
|
+
if (element) {
|
|
269
|
+
return <RouteContext.Provider value={{ params }}>{element}</RouteContext.Provider>;
|
|
270
|
+
} else if (childRoutes) {
|
|
271
|
+
// render nested Routes within same params
|
|
272
|
+
return <RouteContext.Provider value={{ params }}><Routes>{childRoutes}</Routes></RouteContext.Provider>;
|
|
273
|
+
} else {
|
|
274
|
+
return <RouteContext.Provider value={{ params }}><div /></RouteContext.Provider>;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// no match -> null
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|