vaderjs-native 1.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/LICENSE +21 -0
- package/README.MD +99 -0
- package/app-template/.idea/.name +1 -0
- package/app-template/.idea/AndroidProjectSystem.xml +6 -0
- package/app-template/.idea/codeStyles/Project.xml +123 -0
- package/app-template/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/app-template/.idea/compiler.xml +6 -0
- package/app-template/.idea/deploymentTargetSelector.xml +10 -0
- package/app-template/.idea/gradle.xml +19 -0
- package/app-template/.idea/inspectionProfiles/Project_Default.xml +61 -0
- package/app-template/.idea/migrations.xml +10 -0
- package/app-template/.idea/misc.xml +9 -0
- package/app-template/.idea/runConfigurations.xml +17 -0
- package/app-template/app/build.gradle.kts +54 -0
- package/app-template/app/proguard-rules.pro +21 -0
- package/app-template/app/src/main/AndroidManifest.xml +31 -0
- package/app-template/app/src/main/java/com/example/myapplication/MainActivity.kt +74 -0
- package/app-template/app/src/main/java/com/example/myapplication/ui/theme/Color.kt +11 -0
- package/app-template/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt +34 -0
- package/app-template/app/src/main/java/com/example/myapplication/ui/theme/Type.kt +36 -0
- package/app-template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/app-template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/app-template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/app-template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/app-template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/app-template/app/src/main/res/values/strings.xml +3 -0
- package/app-template/app/src/main/res/values/themes.xml +4 -0
- package/app-template/build.gradle.kts +5 -0
- package/app-template/gradle/libs.versions.toml +30 -0
- package/app-template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/app-template/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/app-template/gradle.properties +23 -0
- package/app-template/gradlew +251 -0
- package/app-template/gradlew.bat +94 -0
- package/app-template/settings.gradle.kts +23 -0
- package/cli.ts +232 -0
- package/config/index.ts +14 -0
- package/index.ts +1083 -0
- package/jsconfig.json +7 -0
- package/logo.png +0 -0
- package/main.js +643 -0
- package/package.json +20 -0
- package/plugins/index.ts +63 -0
- package/plugins/tailwind.ts +53 -0
package/index.ts
ADDED
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A lightweight React-like library with hooks implementation.
|
|
3
|
+
* @module vader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Global variables for the fiber tree and rendering process.
|
|
8
|
+
*/
|
|
9
|
+
let nextUnitOfWork: Fiber | null = null;
|
|
10
|
+
let wipRoot: Fiber | null = null;
|
|
11
|
+
let currentRoot: Fiber | null = null;
|
|
12
|
+
let deletions: Fiber[] | null = null;
|
|
13
|
+
let wipFiber: Fiber | null = null;
|
|
14
|
+
let hookIndex = 0;
|
|
15
|
+
let isRenderScheduled = false;
|
|
16
|
+
// Add to the top of your Vader.js file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
interface Fiber {
|
|
20
|
+
type?: string | Function;
|
|
21
|
+
dom?: Node;
|
|
22
|
+
props: {
|
|
23
|
+
children: VNode[];
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
};
|
|
26
|
+
parent?: Fiber;
|
|
27
|
+
child?: Fiber;
|
|
28
|
+
sibling?: Fiber;
|
|
29
|
+
alternate?: Fiber;
|
|
30
|
+
effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
|
|
31
|
+
hooks?: Hook[];
|
|
32
|
+
key?: string | number | null;
|
|
33
|
+
propsCache?: Record<string, any>;
|
|
34
|
+
__compareProps?: (prev: any, next: any) => boolean;
|
|
35
|
+
__skipMemo?: boolean;
|
|
36
|
+
_needsUpdate?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VNode {
|
|
40
|
+
type: string | Function;
|
|
41
|
+
props: {
|
|
42
|
+
children: VNode[];
|
|
43
|
+
[key: string]: any;
|
|
44
|
+
};
|
|
45
|
+
key?: string | number | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Hook {
|
|
49
|
+
state?: any;
|
|
50
|
+
queue?: any[];
|
|
51
|
+
deps?: any[];
|
|
52
|
+
_cleanupFn?: Function;
|
|
53
|
+
memoizedValue?: any;
|
|
54
|
+
current?: any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a property key is an event handler.
|
|
59
|
+
* @param {string} key - The property key to check.
|
|
60
|
+
* @returns {boolean} True if the key is an event handler.
|
|
61
|
+
*/
|
|
62
|
+
const isEvent = (key: string) => key.startsWith("on");
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if a property key is a regular property (not children or event).
|
|
66
|
+
* @param {string} key - The property key to check.
|
|
67
|
+
* @returns {boolean} True if the key is a regular property.
|
|
68
|
+
*/
|
|
69
|
+
const isProperty = (key: string) => key !== "children" && !isEvent(key);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates a function to check if a property has changed between objects.
|
|
73
|
+
* @param {object} prev - The previous object.
|
|
74
|
+
* @param {object} next - The next object.
|
|
75
|
+
* @returns {function} A function that takes a key and returns true if the property changed.
|
|
76
|
+
*/
|
|
77
|
+
const isNew = (prev: object, next: object) => (key: string) => prev[key] !== next[key];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a function to check if a property was removed from an object.
|
|
81
|
+
* @param {object} prev - The previous object.
|
|
82
|
+
* @param {object} next - The next object.
|
|
83
|
+
* @returns {function} A function that takes a key and returns true if the property was removed.
|
|
84
|
+
*/
|
|
85
|
+
const isGone = (prev: object, next: object) => (key: string) => !(key in next);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a DOM node for a fiber.
|
|
89
|
+
* @param {Fiber} fiber - The fiber to create a DOM node for.
|
|
90
|
+
* @returns {Node} The created DOM node.
|
|
91
|
+
*/
|
|
92
|
+
function createDom(fiber: Fiber): Node {
|
|
93
|
+
let dom: Node;
|
|
94
|
+
|
|
95
|
+
if (fiber.type === "TEXT_ELEMENT") {
|
|
96
|
+
dom = document.createTextNode("");
|
|
97
|
+
} else {
|
|
98
|
+
const isSvg = isSvgElement(fiber);
|
|
99
|
+
if (isSvg) {
|
|
100
|
+
dom = document.createElementNS("http://www.w3.org/2000/svg", fiber.type as string);
|
|
101
|
+
} else {
|
|
102
|
+
dom = document.createElement(fiber.type as string);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
updateDom(dom, {}, fiber.props);
|
|
107
|
+
return dom;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isSvgElement(fiber: Fiber): boolean {
|
|
111
|
+
// Check if the fiber is an <svg> itself or inside an <svg>
|
|
112
|
+
let parent = fiber.parent;
|
|
113
|
+
if (fiber.type === "svg") return true;
|
|
114
|
+
while (parent) {
|
|
115
|
+
if (parent.type === "svg") return true;
|
|
116
|
+
parent = parent.parent;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Applies updated props to a DOM node.
|
|
124
|
+
* @param {Node} dom - The DOM node to update.
|
|
125
|
+
* @param {object} prevProps - The previous properties.
|
|
126
|
+
* @param {object} nextProps - The new properties.
|
|
127
|
+
*/
|
|
128
|
+
function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
129
|
+
prevProps = prevProps || {};
|
|
130
|
+
nextProps = nextProps || {};
|
|
131
|
+
|
|
132
|
+
const isSvg = dom instanceof SVGElement;
|
|
133
|
+
|
|
134
|
+
// Remove old or changed event listeners
|
|
135
|
+
Object.keys(prevProps)
|
|
136
|
+
.filter(isEvent)
|
|
137
|
+
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
|
|
138
|
+
.forEach(name => {
|
|
139
|
+
const eventType = name.toLowerCase().substring(2);
|
|
140
|
+
if (typeof prevProps[name] === 'function') {
|
|
141
|
+
(dom as Element).removeEventListener(eventType, prevProps[name]);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Remove old properties
|
|
146
|
+
Object.keys(prevProps)
|
|
147
|
+
.filter(isProperty)
|
|
148
|
+
.filter(isGone(prevProps, nextProps))
|
|
149
|
+
.forEach(name => {
|
|
150
|
+
if (name === 'className' || name === 'class') {
|
|
151
|
+
(dom as Element).removeAttribute('class');
|
|
152
|
+
} else if (name === 'style') {
|
|
153
|
+
(dom as HTMLElement).style.cssText = '';
|
|
154
|
+
} else {
|
|
155
|
+
if (isSvg) {
|
|
156
|
+
(dom as Element).removeAttribute(name);
|
|
157
|
+
} else {
|
|
158
|
+
(dom as any)[name] = '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Set new or changed properties
|
|
164
|
+
Object.keys(nextProps)
|
|
165
|
+
.filter(isProperty)
|
|
166
|
+
.filter(isNew(prevProps, nextProps))
|
|
167
|
+
.forEach(name => {
|
|
168
|
+
if (name === 'style') {
|
|
169
|
+
const style = nextProps[name];
|
|
170
|
+
if (typeof style === 'string') {
|
|
171
|
+
(dom as HTMLElement).style.cssText = style;
|
|
172
|
+
} else if (typeof style === 'object' && style !== null) {
|
|
173
|
+
for (const [key, value] of Object.entries(style)) {
|
|
174
|
+
(dom as HTMLElement).style[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (name === 'className' || name === 'class') {
|
|
178
|
+
(dom as Element).setAttribute('class', nextProps[name]);
|
|
179
|
+
} else {
|
|
180
|
+
if (isSvg) {
|
|
181
|
+
(dom as Element).setAttribute(name, nextProps[name]);
|
|
182
|
+
} else {
|
|
183
|
+
(dom as any)[name] = nextProps[name];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Add new event listeners
|
|
189
|
+
Object.keys(nextProps)
|
|
190
|
+
.filter(isEvent)
|
|
191
|
+
.filter(isNew(prevProps, nextProps))
|
|
192
|
+
.forEach(name => {
|
|
193
|
+
const eventType = name.toLowerCase().substring(2);
|
|
194
|
+
const handler = nextProps[name];
|
|
195
|
+
if (typeof handler === 'function') {
|
|
196
|
+
(dom as Element).addEventListener(eventType, handler);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
Object.keys(nextProps)
|
|
201
|
+
.filter(isEvent)
|
|
202
|
+
.filter(isNew(prevProps, nextProps))
|
|
203
|
+
.forEach(name => {
|
|
204
|
+
const eventType = name.toLowerCase().substring(2);
|
|
205
|
+
const handler = nextProps[name];
|
|
206
|
+
if (typeof handler === 'function') {
|
|
207
|
+
// Remove old listener first if it exists
|
|
208
|
+
if (prevProps[name]) {
|
|
209
|
+
dom.removeEventListener(eventType, prevProps[name]);
|
|
210
|
+
}
|
|
211
|
+
// Add new listener with passive: true for better performance
|
|
212
|
+
dom.addEventListener(eventType, handler, { passive: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Commits the entire work-in-progress tree to the DOM.
|
|
220
|
+
*/
|
|
221
|
+
function commitRoot(): void {
|
|
222
|
+
deletions.forEach(commitWork);
|
|
223
|
+
commitWork(wipRoot.child);
|
|
224
|
+
currentRoot = wipRoot;
|
|
225
|
+
wipRoot = null;
|
|
226
|
+
isRenderScheduled = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Recursively commits a fiber and its children to the DOM.
|
|
231
|
+
* @param {Fiber} fiber - The fiber to commit.
|
|
232
|
+
*/
|
|
233
|
+
function commitWork(fiber: Fiber | null): void {
|
|
234
|
+
if (!fiber) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let domParentFiber = fiber.parent;
|
|
239
|
+
while (domParentFiber && !domParentFiber.dom) {
|
|
240
|
+
domParentFiber = domParentFiber.parent;
|
|
241
|
+
}
|
|
242
|
+
const domParent = domParentFiber ? domParentFiber.dom : null;
|
|
243
|
+
|
|
244
|
+
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
|
|
245
|
+
if (domParent) domParent.appendChild(fiber.dom);
|
|
246
|
+
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
|
|
247
|
+
updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
|
|
248
|
+
} else if (fiber.effectTag === "DELETION") {
|
|
249
|
+
commitDeletion(fiber);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
commitWork(fiber.child);
|
|
253
|
+
commitWork(fiber.sibling);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Recursively removes a fiber and its children from the DOM.
|
|
258
|
+
* @param {Fiber} fiber - The fiber to remove.
|
|
259
|
+
*/
|
|
260
|
+
function commitDeletion(fiber: Fiber | null): void {
|
|
261
|
+
if (!fiber) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (fiber.dom) {
|
|
265
|
+
if (fiber.dom.parentNode) {
|
|
266
|
+
fiber.dom.parentNode.removeChild(fiber.dom);
|
|
267
|
+
}
|
|
268
|
+
} else if (fiber.child) {
|
|
269
|
+
commitDeletion(fiber.child);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Renders a virtual DOM element into a container.
|
|
275
|
+
* @param {VNode} element - The root virtual DOM element to render.
|
|
276
|
+
* @param {Node} container - The DOM container to render into.
|
|
277
|
+
*/
|
|
278
|
+
export function render(element: VNode, container: Node): void {
|
|
279
|
+
container.innerHTML = "";
|
|
280
|
+
|
|
281
|
+
wipRoot = {
|
|
282
|
+
dom: container,
|
|
283
|
+
props: {
|
|
284
|
+
children: [element],
|
|
285
|
+
},
|
|
286
|
+
alternate: currentRoot,
|
|
287
|
+
};
|
|
288
|
+
deletions = [];
|
|
289
|
+
nextUnitOfWork = wipRoot;
|
|
290
|
+
requestAnimationFrame(workLoop);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The main work loop for rendering and reconciliation.
|
|
295
|
+
*/
|
|
296
|
+
function workLoop(): void {
|
|
297
|
+
if (!wipRoot && currentRoot) {
|
|
298
|
+
wipRoot = {
|
|
299
|
+
dom: currentRoot.dom,
|
|
300
|
+
props: currentRoot.props,
|
|
301
|
+
alternate: currentRoot,
|
|
302
|
+
};
|
|
303
|
+
deletions = [];
|
|
304
|
+
nextUnitOfWork = wipRoot;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
while (nextUnitOfWork) {
|
|
308
|
+
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!nextUnitOfWork && wipRoot) {
|
|
312
|
+
commitRoot();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Performs work on a single fiber unit.
|
|
319
|
+
* @param {Fiber} fiber - The fiber to perform work on.
|
|
320
|
+
* @returns {Fiber|null} The next fiber to work on.
|
|
321
|
+
*/
|
|
322
|
+
function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
323
|
+
const isFunctionComponent = fiber.type instanceof Function;
|
|
324
|
+
|
|
325
|
+
if (isFunctionComponent) {
|
|
326
|
+
updateFunctionComponent(fiber);
|
|
327
|
+
} else {
|
|
328
|
+
updateHostComponent(fiber);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (fiber.child) {
|
|
332
|
+
return fiber.child;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let nextFiber = fiber;
|
|
336
|
+
while (nextFiber) {
|
|
337
|
+
if (nextFiber.sibling) {
|
|
338
|
+
return nextFiber.sibling;
|
|
339
|
+
}
|
|
340
|
+
nextFiber = nextFiber.parent;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Updates a function component fiber.
|
|
347
|
+
* @param {Fiber} fiber - The function component fiber to update.
|
|
348
|
+
*/
|
|
349
|
+
function updateFunctionComponent(fiber: Fiber) {
|
|
350
|
+
wipFiber = fiber;
|
|
351
|
+
hookIndex = 0;
|
|
352
|
+
fiber.hooks = fiber.alternate?.hooks || [];
|
|
353
|
+
|
|
354
|
+
// Directly call the component function without memoization
|
|
355
|
+
// The 'createComponent' call is removed.
|
|
356
|
+
const children = [(fiber.type as Function)(fiber.props)]
|
|
357
|
+
.flat()
|
|
358
|
+
.filter(child => child != null && typeof child !== 'boolean')
|
|
359
|
+
.map(child => typeof child === 'object' ? child : createTextElement(child));
|
|
360
|
+
|
|
361
|
+
reconcileChildren(fiber, children);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Updates a host component fiber (DOM element).
|
|
365
|
+
* @param {Fiber} fiber - The host component fiber to update.
|
|
366
|
+
*/
|
|
367
|
+
function updateHostComponent(fiber: Fiber): void {
|
|
368
|
+
if (!fiber.dom) {
|
|
369
|
+
fiber.dom = createDom(fiber);
|
|
370
|
+
}
|
|
371
|
+
reconcileChildren(fiber, fiber.props.children);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Reconciles the children of a fiber with new elements.
|
|
376
|
+
* @param {Fiber} wipFiber - The work-in-progress fiber.
|
|
377
|
+
* @param {VNode[]} elements - The new child elements.
|
|
378
|
+
*/
|
|
379
|
+
function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
380
|
+
let index = 0;
|
|
381
|
+
let oldFiber = wipFiber.alternate?.child;
|
|
382
|
+
let prevSibling: Fiber | null = null;
|
|
383
|
+
|
|
384
|
+
// Create map of existing fibers by key
|
|
385
|
+
const existingFibers = new Map<string | number | null, Fiber>();
|
|
386
|
+
while (oldFiber) {
|
|
387
|
+
const key = oldFiber.key ?? index;
|
|
388
|
+
existingFibers.set(key, oldFiber);
|
|
389
|
+
oldFiber = oldFiber.sibling;
|
|
390
|
+
index++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
index = 0;
|
|
394
|
+
for (; index < elements.length; index++) {
|
|
395
|
+
const element = elements[index];
|
|
396
|
+
const key = element?.key ?? index;
|
|
397
|
+
const oldFiber = existingFibers.get(key);
|
|
398
|
+
|
|
399
|
+
const sameType = oldFiber && element && element.type === oldFiber.type;
|
|
400
|
+
let newFiber: Fiber | null = null;
|
|
401
|
+
|
|
402
|
+
if (sameType) {
|
|
403
|
+
// Reuse the fiber
|
|
404
|
+
newFiber = {
|
|
405
|
+
type: oldFiber.type,
|
|
406
|
+
props: element.props,
|
|
407
|
+
dom: oldFiber.dom,
|
|
408
|
+
parent: wipFiber,
|
|
409
|
+
alternate: oldFiber,
|
|
410
|
+
effectTag: "UPDATE",
|
|
411
|
+
hooks: oldFiber.hooks,
|
|
412
|
+
key
|
|
413
|
+
};
|
|
414
|
+
existingFibers.delete(key);
|
|
415
|
+
} else if (element) {
|
|
416
|
+
// Create new fiber
|
|
417
|
+
newFiber = {
|
|
418
|
+
type: element.type,
|
|
419
|
+
props: element.props,
|
|
420
|
+
dom: null,
|
|
421
|
+
parent: wipFiber,
|
|
422
|
+
alternate: null,
|
|
423
|
+
effectTag: "PLACEMENT",
|
|
424
|
+
key
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (oldFiber && !sameType) {
|
|
429
|
+
oldFiber.effectTag = "DELETION";
|
|
430
|
+
deletions.push(oldFiber);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (index === 0) {
|
|
434
|
+
wipFiber.child = newFiber;
|
|
435
|
+
} else if (prevSibling && newFiber) {
|
|
436
|
+
prevSibling.sibling = newFiber;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (newFiber) {
|
|
440
|
+
prevSibling = newFiber;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Mark remaining old fibers for deletion
|
|
445
|
+
existingFibers.forEach(fiber => {
|
|
446
|
+
fiber.effectTag = "DELETION";
|
|
447
|
+
deletions.push(fiber);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Creates a virtual DOM element.
|
|
453
|
+
* @param {string|Function} type - The type of the element.
|
|
454
|
+
* @param {object} props - The element's properties.
|
|
455
|
+
* @param {...any} children - The element's children.
|
|
456
|
+
* @returns {VNode} The created virtual DOM element.
|
|
457
|
+
*/
|
|
458
|
+
export function createElement(
|
|
459
|
+
type: string | Function,
|
|
460
|
+
props?: object,
|
|
461
|
+
...children: any[]
|
|
462
|
+
): VNode {
|
|
463
|
+
return {
|
|
464
|
+
type,
|
|
465
|
+
props: {
|
|
466
|
+
...props,
|
|
467
|
+
children: children
|
|
468
|
+
.flat()
|
|
469
|
+
.filter(child => child != null && typeof child !== "boolean")
|
|
470
|
+
.map(child =>
|
|
471
|
+
typeof child === "object" ? child : createTextElement(child)
|
|
472
|
+
),
|
|
473
|
+
},
|
|
474
|
+
key: props?.key ?? null,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Creates a text virtual DOM element.
|
|
480
|
+
* @param {string} text - The text content.
|
|
481
|
+
* @returns {VNode} The created text element.
|
|
482
|
+
*/
|
|
483
|
+
function createTextElement(text: string): VNode {
|
|
484
|
+
return {
|
|
485
|
+
type: "TEXT_ELEMENT",
|
|
486
|
+
props: {
|
|
487
|
+
nodeValue: text,
|
|
488
|
+
children: [],
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* A React-like useState hook for managing component state.
|
|
495
|
+
* @template T
|
|
496
|
+
* @param {T|(() => T)} initial - The initial state value or initializer function.
|
|
497
|
+
* @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
503
|
+
if (!wipFiber) {
|
|
504
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
508
|
+
if (!hook) {
|
|
509
|
+
hook = {
|
|
510
|
+
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
511
|
+
queue: [],
|
|
512
|
+
_needsUpdate: false
|
|
513
|
+
};
|
|
514
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const setState = (action: T | ((prevState: T) => T)) => {
|
|
518
|
+
// Calculate new state based on current state
|
|
519
|
+
const newState = typeof action === "function"
|
|
520
|
+
? (action as (prevState: T) => T)(hook.state)
|
|
521
|
+
: action;
|
|
522
|
+
|
|
523
|
+
hook.state = newState;
|
|
524
|
+
|
|
525
|
+
// Reset work-in-progress root to trigger re-r
|
|
526
|
+
|
|
527
|
+
deletions = [];
|
|
528
|
+
nextUnitOfWork = wipRoot;
|
|
529
|
+
|
|
530
|
+
// Start the render process
|
|
531
|
+
requestAnimationFrame(workLoop);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
hookIndex++;
|
|
535
|
+
return [hook.state, setState];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* A React-like useEffect hook for side effects.
|
|
540
|
+
* @param {Function} callback - The effect callback.
|
|
541
|
+
* @param {Array} deps - The dependency array.
|
|
542
|
+
*/
|
|
543
|
+
export function useEffect(callback: Function, deps?: any[]): void {
|
|
544
|
+
if (!wipFiber) {
|
|
545
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
549
|
+
if (!hook) {
|
|
550
|
+
hook = { deps: undefined, _cleanupFn: undefined };
|
|
551
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const hasChanged = hook.deps === undefined ||
|
|
555
|
+
!deps ||
|
|
556
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
557
|
+
|
|
558
|
+
if (hasChanged) {
|
|
559
|
+
if (hook._cleanupFn) {
|
|
560
|
+
hook._cleanupFn();
|
|
561
|
+
}
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
const newCleanup = callback();
|
|
564
|
+
if (typeof newCleanup === 'function') {
|
|
565
|
+
hook._cleanupFn = newCleanup;
|
|
566
|
+
} else {
|
|
567
|
+
hook._cleanupFn = undefined;
|
|
568
|
+
}
|
|
569
|
+
}, 0);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
hook.deps = deps;
|
|
573
|
+
hookIndex++;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* A switch component for conditional rendering.
|
|
578
|
+
* @param {object} props - The component props.
|
|
579
|
+
* @param {VNode[]} props.children - The child components.
|
|
580
|
+
* @returns {VNode|null} The matched child or null.
|
|
581
|
+
*/
|
|
582
|
+
export function Switch({ children }: { children: VNode[] }): VNode | null {
|
|
583
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
584
|
+
const match = childrenArray.find(child => child && child.props.when);
|
|
585
|
+
if (match) {
|
|
586
|
+
return match;
|
|
587
|
+
}
|
|
588
|
+
return childrenArray.find(child => child && child.props.default) || null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* A match component for use with Switch.
|
|
593
|
+
* @param {object} props - The component props.
|
|
594
|
+
* @param {boolean} props.when - The condition to match.
|
|
595
|
+
* @param {VNode[]} props.children - The child components.
|
|
596
|
+
* @returns {VNode|null} The children if when is true, otherwise null.
|
|
597
|
+
*/
|
|
598
|
+
export function Match({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
599
|
+
//@ts-ignore
|
|
600
|
+
return when ? children : null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
604
|
+
//@ts-ignore
|
|
605
|
+
return when ? children : null;
|
|
606
|
+
}
|
|
607
|
+
export function showToast (message: string, duration = 3000) {
|
|
608
|
+
//@ts-ignore
|
|
609
|
+
if (window.Android && typeof window.Android.showToast === "function") {
|
|
610
|
+
//@ts-ignore
|
|
611
|
+
window.Android.showToast(message, duration);
|
|
612
|
+
} else {
|
|
613
|
+
console.log(`[showToast] ${message}`);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* A React-like useRef hook for mutable references.
|
|
619
|
+
* @template T
|
|
620
|
+
* @param {T} initial - The initial reference value.
|
|
621
|
+
* @returns {{current: T}} A mutable ref object.
|
|
622
|
+
*/
|
|
623
|
+
export function useRef<T>(initial: T): { current: T } {
|
|
624
|
+
if (!wipFiber) {
|
|
625
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
629
|
+
if (!hook) {
|
|
630
|
+
hook = { current: initial };
|
|
631
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
hookIndex++;
|
|
635
|
+
//@ts-ignore
|
|
636
|
+
return hook;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
|
|
641
|
+
* @param {Function} callback - The effect callback.
|
|
642
|
+
* @param {Array} deps - The dependency array.
|
|
643
|
+
*/
|
|
644
|
+
export function useLayoutEffect(callback: Function, deps?: any[]): void {
|
|
645
|
+
if (!wipFiber) {
|
|
646
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
650
|
+
if (!hook) {
|
|
651
|
+
hook = { deps: undefined, _cleanupFn: undefined };
|
|
652
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const hasChanged = hook.deps === undefined ||
|
|
656
|
+
!deps ||
|
|
657
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
658
|
+
|
|
659
|
+
if (hasChanged) {
|
|
660
|
+
if (hook._cleanupFn) {
|
|
661
|
+
hook._cleanupFn();
|
|
662
|
+
}
|
|
663
|
+
const cleanup = callback();
|
|
664
|
+
if (typeof cleanup === 'function') {
|
|
665
|
+
hook._cleanupFn = cleanup;
|
|
666
|
+
} else {
|
|
667
|
+
hook._cleanupFn = undefined;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
hook.deps = deps;
|
|
672
|
+
hookIndex++;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* A React-like useReducer hook for state management with reducers.
|
|
677
|
+
* @template S
|
|
678
|
+
* @template A
|
|
679
|
+
* @param {(state: S, action: A) => S} reducer - The reducer function.
|
|
680
|
+
* @param {S} initialState - The initial state.
|
|
681
|
+
* @returns {[S, (action: A) => void]} The current state and dispatch function.
|
|
682
|
+
*/
|
|
683
|
+
export function useReducer<S, A>(
|
|
684
|
+
reducer: (state: S, action: A) => S,
|
|
685
|
+
initialState: S
|
|
686
|
+
): [S, (action: A) => void] {
|
|
687
|
+
if (!wipFiber) {
|
|
688
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
692
|
+
if (!hook) {
|
|
693
|
+
hook = {
|
|
694
|
+
state: initialState,
|
|
695
|
+
queue: [],
|
|
696
|
+
};
|
|
697
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
hook.queue.forEach((action) => {
|
|
701
|
+
hook.state = reducer(hook.state, action);
|
|
702
|
+
});
|
|
703
|
+
hook.queue = [];
|
|
704
|
+
|
|
705
|
+
const dispatch = (action: A) => {
|
|
706
|
+
hook.queue.push(action);
|
|
707
|
+
if (!isRenderScheduled) {
|
|
708
|
+
isRenderScheduled = true;
|
|
709
|
+
requestAnimationFrame(workLoop);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
hookIndex++;
|
|
714
|
+
return [hook.state, dispatch];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* A React-like useContext hook for accessing context values.
|
|
719
|
+
* @template T
|
|
720
|
+
* @param {Context<T>} Context - The context object to use.
|
|
721
|
+
* @returns {T} The current context value.
|
|
722
|
+
*/
|
|
723
|
+
export function useContext<T>(Context: Context<T>): T {
|
|
724
|
+
if (!wipFiber) {
|
|
725
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let fiber = wipFiber.parent;
|
|
729
|
+
while (fiber) {
|
|
730
|
+
if (fiber.type && fiber.type._context === Context) {
|
|
731
|
+
return fiber.props.value;
|
|
732
|
+
}
|
|
733
|
+
fiber = fiber.parent;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return Context._defaultValue;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
interface Context<T> {
|
|
740
|
+
_defaultValue: T;
|
|
741
|
+
Provider: Function & { _context: Context<T> };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Creates a context object for use with useContext.
|
|
746
|
+
* @template T
|
|
747
|
+
* @param {T} defaultValue - The default context value.
|
|
748
|
+
* @returns {Context<T>} The created context object.
|
|
749
|
+
*/
|
|
750
|
+
export function createContext<T>(defaultValue: T): Context<T> {
|
|
751
|
+
const context = {
|
|
752
|
+
_defaultValue: defaultValue,
|
|
753
|
+
Provider: function Provider({ children }: { children: VNode[] }) {
|
|
754
|
+
return children;
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
context.Provider._context = context;
|
|
758
|
+
return context;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* A React-like useMemo hook for memoizing expensive calculations.
|
|
763
|
+
* @template T
|
|
764
|
+
* @param {() => T} factory - The function to memoize.
|
|
765
|
+
* @param {Array} deps - The dependency array.
|
|
766
|
+
* @returns {T} The memoized value.
|
|
767
|
+
*/
|
|
768
|
+
export function useMemo<T>(factory: () => T, deps?: any[]): T {
|
|
769
|
+
if (!wipFiber) {
|
|
770
|
+
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
774
|
+
if (!hook) {
|
|
775
|
+
hook = { memoizedValue: factory(), deps };
|
|
776
|
+
wipFiber.hooks[hookIndex] = hook;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const hasChanged = hook.deps === undefined ||
|
|
780
|
+
!deps ||
|
|
781
|
+
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
|
|
782
|
+
if (hasChanged) {
|
|
783
|
+
hook.memoizedValue = factory();
|
|
784
|
+
hook.deps = deps;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
hookIndex++;
|
|
788
|
+
return hook.memoizedValue;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* A React-like useCallback hook for memoizing functions.
|
|
793
|
+
* @template T
|
|
794
|
+
* @param {T} callback - The function to memoize.
|
|
795
|
+
* @param {Array} deps - The dependency array.
|
|
796
|
+
* @returns {T} The memoized callback.
|
|
797
|
+
*/
|
|
798
|
+
export function useCallback<T extends Function>(callback: T, deps?: any[]): T {
|
|
799
|
+
return useMemo(() => callback, deps);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* A hook for managing arrays with common operations.
|
|
804
|
+
* @template T
|
|
805
|
+
* @param {T[]} initialValue - The initial array value.
|
|
806
|
+
* @returns {{
|
|
807
|
+
* array: T[],
|
|
808
|
+
* add: (item: T) => void,
|
|
809
|
+
* remove: (index: number) => void,
|
|
810
|
+
* update: (index: number, item: T) => void
|
|
811
|
+
* }} An object with the array and mutation functions.
|
|
812
|
+
*/
|
|
813
|
+
export function useArray<T>(initialValue: T[] = []): {
|
|
814
|
+
array: T[],
|
|
815
|
+
add: (item: T) => void,
|
|
816
|
+
remove: (index: number) => void,
|
|
817
|
+
update: (index: number, item: T) => void
|
|
818
|
+
} {
|
|
819
|
+
const [array, setArray] = useState(initialValue);
|
|
820
|
+
|
|
821
|
+
const add = (item: T) => {
|
|
822
|
+
setArray((prevArray) => [...prevArray, item]);
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const remove = (index: number) => {
|
|
826
|
+
setArray((prevArray) => prevArray.filter((_, i) => i !== index));
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const update = (index: number, item: T) => {
|
|
830
|
+
setArray((prevArray) => prevArray.map((prevItem, i) => (i === index ? item : prevItem)));
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
return { array, add, remove, update };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* A hook for running a function at a fixed interval.
|
|
838
|
+
* @param {Function} callback - The function to run.
|
|
839
|
+
* @param {number|null} delay - The delay in milliseconds, or null to stop.
|
|
840
|
+
*/
|
|
841
|
+
export function useInterval(callback: Function, delay: number | null): void {
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
if (delay === null) return;
|
|
844
|
+
const interval = setInterval(callback, delay);
|
|
845
|
+
return () => clearInterval(interval);
|
|
846
|
+
}, [callback, delay]);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Types for cache configuration
|
|
850
|
+
interface QueryCacheOptions {
|
|
851
|
+
expiryMs?: number; // Cache duration in milliseconds
|
|
852
|
+
enabled?: boolean; // Whether caching is enabled
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Default cache options
|
|
856
|
+
const DEFAULT_CACHE_OPTIONS: QueryCacheOptions = {
|
|
857
|
+
expiryMs: 5 * 60 * 1000, // 5 minutes default
|
|
858
|
+
enabled: true
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// In-memory cache store
|
|
862
|
+
const queryCache = new Map<string, {
|
|
863
|
+
data: any;
|
|
864
|
+
timestamp: number;
|
|
865
|
+
options: QueryCacheOptions;
|
|
866
|
+
}>();
|
|
867
|
+
|
|
868
|
+
export function useQuery<T>(
|
|
869
|
+
url: string,
|
|
870
|
+
cacheOptions: QueryCacheOptions = {} // Default to empty object
|
|
871
|
+
): {
|
|
872
|
+
data: T | null;
|
|
873
|
+
loading: boolean;
|
|
874
|
+
error: Error | null;
|
|
875
|
+
refetch: () => Promise<void>;
|
|
876
|
+
} {
|
|
877
|
+
const [data, setData] = useState<T | null>(null);
|
|
878
|
+
const [loading, setLoading] = useState(true);
|
|
879
|
+
const [error, setError] = useState<Error | null>(null);
|
|
880
|
+
|
|
881
|
+
// FIX: Destructure primitive values from cacheOptions for stable dependencies.
|
|
882
|
+
const {
|
|
883
|
+
enabled = DEFAULT_CACHE_OPTIONS.enabled,
|
|
884
|
+
expiryMs = DEFAULT_CACHE_OPTIONS.expiryMs
|
|
885
|
+
} = cacheOptions;
|
|
886
|
+
|
|
887
|
+
// FIX: Memoize the options object so its reference is stable across renders.
|
|
888
|
+
// It will only be recreated if `enabled` or `expiryMs` changes.
|
|
889
|
+
const mergedCacheOptions = useMemo(() => ({
|
|
890
|
+
enabled,
|
|
891
|
+
expiryMs,
|
|
892
|
+
}), [enabled, expiryMs]);
|
|
893
|
+
|
|
894
|
+
const fetchData = useCallback(async () => {
|
|
895
|
+
setLoading(true);
|
|
896
|
+
try {
|
|
897
|
+
// Check cache first if enabled
|
|
898
|
+
if (mergedCacheOptions.enabled) {
|
|
899
|
+
const cached = queryCache.get(url);
|
|
900
|
+
const now = Date.now();
|
|
901
|
+
|
|
902
|
+
if (cached && now - cached.timestamp < mergedCacheOptions.expiryMs) {
|
|
903
|
+
setData(cached.data);
|
|
904
|
+
setLoading(false);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Not in cache or expired - fetch fresh data
|
|
910
|
+
const response = await fetch(url);
|
|
911
|
+
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const result = await response.json();
|
|
917
|
+
|
|
918
|
+
// Update cache if enabled
|
|
919
|
+
if (mergedCacheOptions.enabled) {
|
|
920
|
+
queryCache.set(url, {
|
|
921
|
+
data: result,
|
|
922
|
+
timestamp: Date.now(),
|
|
923
|
+
options: mergedCacheOptions
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
setData(result);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
930
|
+
} finally {
|
|
931
|
+
setLoading(false);
|
|
932
|
+
}
|
|
933
|
+
}, [url, mergedCacheOptions]); // This dependency is now stable
|
|
934
|
+
|
|
935
|
+
useEffect(() => {
|
|
936
|
+
fetchData();
|
|
937
|
+
}, [fetchData]); // This dependency is now stable
|
|
938
|
+
|
|
939
|
+
return { data, loading, error, refetch: fetchData };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* A hook for tracking window focus state.
|
|
944
|
+
* @returns {boolean} True if the window is focused.
|
|
945
|
+
*/
|
|
946
|
+
export function useWindowFocus(): boolean {
|
|
947
|
+
const [isFocused, setIsFocused] = useState(true);
|
|
948
|
+
|
|
949
|
+
useEffect(() => {
|
|
950
|
+
const onFocus = () => setIsFocused(true);
|
|
951
|
+
const onBlur = () => setIsFocused(false);
|
|
952
|
+
|
|
953
|
+
window.addEventListener("focus", onFocus);
|
|
954
|
+
window.addEventListener("blur", onBlur);
|
|
955
|
+
|
|
956
|
+
return () => {
|
|
957
|
+
window.removeEventListener("focus", onFocus);
|
|
958
|
+
window.removeEventListener("blur", onBlur);
|
|
959
|
+
};
|
|
960
|
+
}, []);
|
|
961
|
+
|
|
962
|
+
return isFocused;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* A hook for syncing state with localStorage.
|
|
967
|
+
* @template T
|
|
968
|
+
* @param {string} key - The localStorage key.
|
|
969
|
+
* @param {T} initialValue - The initial value.
|
|
970
|
+
* @returns {[T, (value: T) => void]} The stored value and a function to update it.
|
|
971
|
+
*/
|
|
972
|
+
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
973
|
+
const [storedValue, setStoredValue] = useState(() => {
|
|
974
|
+
try {
|
|
975
|
+
const item = localStorage.getItem(key);
|
|
976
|
+
return item ? JSON.parse(item) : initialValue;
|
|
977
|
+
} catch (error) {
|
|
978
|
+
return initialValue;
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const setValue = (value: T) => {
|
|
983
|
+
try {
|
|
984
|
+
setStoredValue(value);
|
|
985
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error("Error saving to localStorage", error);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
return [storedValue, setValue];
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
export function Link({
|
|
996
|
+
to,
|
|
997
|
+
className,
|
|
998
|
+
children,
|
|
999
|
+
}: {
|
|
1000
|
+
to: string;
|
|
1001
|
+
className?: string;
|
|
1002
|
+
children: VNode[];
|
|
1003
|
+
}): VNode {
|
|
1004
|
+
const handleClick = (e: MouseEvent) => {
|
|
1005
|
+
e.preventDefault();
|
|
1006
|
+
|
|
1007
|
+
// Normalize path
|
|
1008
|
+
const normalized = to.startsWith("/") ? to : "/" + to;
|
|
1009
|
+
|
|
1010
|
+
// ✅ Android WebView bridge (optional)
|
|
1011
|
+
if (window.Android && typeof window.Android.navigate === "function") {
|
|
1012
|
+
window.Android.navigate(normalized);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ✅ Android asset WebView fallback
|
|
1017
|
+
if (location.protocol === "file:") {
|
|
1018
|
+
location.href = normalized + "/index.html";
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ✅ Normal browser navigation
|
|
1023
|
+
window.location.href = normalized;
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
return createElement(
|
|
1027
|
+
"a",
|
|
1028
|
+
{
|
|
1029
|
+
href: to,
|
|
1030
|
+
onClick: handleClick,
|
|
1031
|
+
className,
|
|
1032
|
+
},
|
|
1033
|
+
...children
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* A hook for detecting clicks outside an element.
|
|
1039
|
+
* @param {React.RefObject} ref - A ref to the element to watch.
|
|
1040
|
+
* @param {Function} handler - The handler to call when a click outside occurs.
|
|
1041
|
+
*/
|
|
1042
|
+
export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
|
|
1043
|
+
useEffect(() => {
|
|
1044
|
+
const listener = (event: MouseEvent) => {
|
|
1045
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
1046
|
+
handler(event);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
document.addEventListener("mousedown", listener);
|
|
1050
|
+
return () => {
|
|
1051
|
+
document.removeEventListener("mousedown", listener);
|
|
1052
|
+
};
|
|
1053
|
+
}, [ref, handler]);
|
|
1054
|
+
}
|
|
1055
|
+
const Vader = {
|
|
1056
|
+
render,
|
|
1057
|
+
createElement,
|
|
1058
|
+
useState,
|
|
1059
|
+
useEffect,
|
|
1060
|
+
useLayoutEffect,
|
|
1061
|
+
useReducer,
|
|
1062
|
+
useContext,
|
|
1063
|
+
createContext,
|
|
1064
|
+
useMemo,
|
|
1065
|
+
useCallback,
|
|
1066
|
+
useRef,
|
|
1067
|
+
useArray,
|
|
1068
|
+
useQuery,
|
|
1069
|
+
useWindowFocus,
|
|
1070
|
+
useLocalStorage,
|
|
1071
|
+
useInterval,
|
|
1072
|
+
Switch,
|
|
1073
|
+
Match,
|
|
1074
|
+
Show,
|
|
1075
|
+
Link,
|
|
1076
|
+
showToast
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
Object.defineProperty(window, "Vader", {
|
|
1080
|
+
value: Vader,
|
|
1081
|
+
writable: false,
|
|
1082
|
+
configurable: false,
|
|
1083
|
+
});
|