@myxo-victor/chexjs 6.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/ChexJs/.htaccess +9 -0
- package/ChexJs/Chex.js +712 -0
- package/ChexJs/Chex.php +354 -0
- package/ChexJs/Chex_notify.php +68 -0
- package/ChexJs/LICENSE +22 -0
- package/ChexJs/README.md +121 -0
- package/ChexJs/api/read.txt +4 -0
- package/ChexJs/app/api/api.txt +4 -0
- package/ChexJs/app/components/main.js +4 -0
- package/ChexJs/app/index.css +12 -0
- package/ChexJs/app/index.html +28 -0
- package/ChexJs/app/logic/app.js +4 -0
- package/ChexJs/components/demo.js +45 -0
- package/ChexJs/components/main.js +609 -0
- package/ChexJs/images/logo.png +0 -0
- package/ChexJs/index.css +118 -0
- package/ChexJs/index.html +44 -0
- package/ChexJs/libs/modal.js +617 -0
- package/ChexJs/libs/orbit.js +217 -0
- package/ChexJs/libs/racket.js +374 -0
- package/ChexJs/libs/rinx.js +88 -0
- package/ChexJs/libs/scrollEcho.js +212 -0
- package/ChexJs/libs/skeleton.js +341 -0
- package/ChexJs/libs/smooth.js +647 -0
- package/ChexJs/logic/app.js +36 -0
- package/ChexJs/notification_handler.txt +1 -0
- package/ChexJs/sw.js +67 -0
- package/package.json +12 -0
package/ChexJs/Chex.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @author Myxo victor
|
|
3
|
+
* @type Enterprise-Level UI Framework + Engine
|
|
4
|
+
* @name Chex
|
|
5
|
+
* @Version 6.0.0 (Unified: VDom + Signals + Store + Animation + API + Notifications + Components)
|
|
6
|
+
* @copy Copyright Aximon 2026 | MIT License
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const Chex = (() => {
|
|
10
|
+
const isWeb = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
11
|
+
|
|
12
|
+
// --- UTILS ---
|
|
13
|
+
const isEvent = (key) => key.startsWith("on");
|
|
14
|
+
const isProperty = (key) => key !== "children" && key !== "key" && key !== "ref" && key !== "onMount" && key !== "onUnmount" && !isEvent(key);
|
|
15
|
+
const isLifecycle = (key) => key === "onMount" || key === "onUnmount";
|
|
16
|
+
const shallowEqual = (a, b) => {
|
|
17
|
+
if (a === b) return true;
|
|
18
|
+
if (!a || !b) return false;
|
|
19
|
+
const keysA = Object.keys(a);
|
|
20
|
+
const keysB = Object.keys(b);
|
|
21
|
+
if (keysA.length !== keysB.length) return false;
|
|
22
|
+
for (const key of keysA) {
|
|
23
|
+
if (a[key] !== b[key]) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// --- VIRTUAL DOM CORE ---
|
|
29
|
+
const isPropsObject = (value) => value !== null
|
|
30
|
+
&& typeof value === "object"
|
|
31
|
+
&& !Array.isArray(value)
|
|
32
|
+
&& !Object.prototype.hasOwnProperty.call(value, "tag");
|
|
33
|
+
|
|
34
|
+
// Tags accept either (props, children) or just (children). This keeps
|
|
35
|
+
// everyday markup short: div([h1('Hello')]) and button('Save').
|
|
36
|
+
function normalizeElementArgs(first, rest) {
|
|
37
|
+
if (isPropsObject(first)) return { props: first, children: rest };
|
|
38
|
+
return {
|
|
39
|
+
props: {},
|
|
40
|
+
children: first === undefined ? rest : [first, ...rest]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createVNode(tag, props, ...children) {
|
|
45
|
+
const flatChildren = children.flat(Infinity).filter(c => c !== null && c !== undefined);
|
|
46
|
+
const vnodeProps = props || {};
|
|
47
|
+
return {
|
|
48
|
+
tag,
|
|
49
|
+
key: vnodeProps.key,
|
|
50
|
+
props: vnodeProps,
|
|
51
|
+
children: flatChildren.map(child =>
|
|
52
|
+
(typeof child === "object" && child.tag)
|
|
53
|
+
? child
|
|
54
|
+
: createTextVNode(String(child))
|
|
55
|
+
)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createTextVNode(text) {
|
|
60
|
+
return {
|
|
61
|
+
tag: "TEXT_ELEMENT",
|
|
62
|
+
props: { nodeValue: text },
|
|
63
|
+
children: []
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createElement(tag, first, ...rest) {
|
|
68
|
+
const { props, children } = normalizeElementArgs(first, rest);
|
|
69
|
+
return createVNode(tag, props, ...children);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tag = (name) => (first, ...rest) => createElement(name, first, ...rest);
|
|
73
|
+
|
|
74
|
+
// --- REACTIVE SIGNALS ---
|
|
75
|
+
let activeEffect = null;
|
|
76
|
+
|
|
77
|
+
function stopEffect(effectRunner) {
|
|
78
|
+
if (!effectRunner) return;
|
|
79
|
+
if (typeof effectRunner._cleanup === "function") {
|
|
80
|
+
effectRunner._cleanup();
|
|
81
|
+
}
|
|
82
|
+
effectRunner._cleanup = null;
|
|
83
|
+
effectRunner._deps.forEach(depSet => depSet.delete(effectRunner));
|
|
84
|
+
effectRunner._deps.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function signal(value) {
|
|
88
|
+
const subscriptions = new Set();
|
|
89
|
+
return {
|
|
90
|
+
get value() {
|
|
91
|
+
if (activeEffect) {
|
|
92
|
+
subscriptions.add(activeEffect);
|
|
93
|
+
activeEffect._deps.add(subscriptions);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
},
|
|
97
|
+
set value(newValue) {
|
|
98
|
+
if (value === newValue) return;
|
|
99
|
+
value = newValue;
|
|
100
|
+
Array.from(subscriptions).forEach(fn => fn());
|
|
101
|
+
},
|
|
102
|
+
peek: () => value
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function effect(fn) {
|
|
107
|
+
const effectRunner = () => {
|
|
108
|
+
stopEffect(effectRunner);
|
|
109
|
+
activeEffect = effectRunner;
|
|
110
|
+
const nextCleanup = fn();
|
|
111
|
+
activeEffect = null;
|
|
112
|
+
if (typeof nextCleanup === "function") {
|
|
113
|
+
effectRunner._cleanup = nextCleanup;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
effectRunner._cleanup = null;
|
|
117
|
+
effectRunner._deps = new Set();
|
|
118
|
+
effectRunner();
|
|
119
|
+
return () => stopEffect(effectRunner);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- DOM RENDERING ENGINE ---
|
|
123
|
+
function createDom(vnode) {
|
|
124
|
+
const dom = vnode.tag === "TEXT_ELEMENT"
|
|
125
|
+
? document.createTextNode("")
|
|
126
|
+
: document.createElement(vnode.tag);
|
|
127
|
+
|
|
128
|
+
updateDom(dom, {}, vnode.props);
|
|
129
|
+
|
|
130
|
+
vnode.children.forEach(child => {
|
|
131
|
+
dom.appendChild(createDom(child));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
vnode.dom = dom;
|
|
135
|
+
if (typeof vnode.props?.ref === "function") vnode.props.ref(dom);
|
|
136
|
+
return dom;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function updateDom(dom, prevProps, nextProps) {
|
|
140
|
+
const prev = prevProps || {};
|
|
141
|
+
const next = nextProps || {};
|
|
142
|
+
|
|
143
|
+
// Remove old or changed event listeners
|
|
144
|
+
Object.keys(prev).forEach(name => {
|
|
145
|
+
if (isEvent(name)) {
|
|
146
|
+
const eventType = name.toLowerCase().substring(2);
|
|
147
|
+
if (!next[name] || prev[name] !== next[name]) {
|
|
148
|
+
dom.removeEventListener(eventType, prev[name]);
|
|
149
|
+
}
|
|
150
|
+
} else if (isProperty(name) && !(name in next)) {
|
|
151
|
+
if (name === "className" || name === "class") {
|
|
152
|
+
dom.className = "";
|
|
153
|
+
} else if (name === "style" && typeof prev[name] === "object") {
|
|
154
|
+
Object.keys(prev[name]).forEach(styleKey => {
|
|
155
|
+
dom.style[styleKey] = "";
|
|
156
|
+
});
|
|
157
|
+
} else if (name in dom) {
|
|
158
|
+
dom[name] = "";
|
|
159
|
+
} else {
|
|
160
|
+
dom.removeAttribute(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Add/update new props
|
|
166
|
+
Object.keys(next).forEach(name => {
|
|
167
|
+
if (isLifecycle(name)) return;
|
|
168
|
+
const val = next[name];
|
|
169
|
+
if (isEvent(name)) {
|
|
170
|
+
const eventType = name.toLowerCase().substring(2);
|
|
171
|
+
if (!prev[name] || prev[name] !== val) {
|
|
172
|
+
dom.addEventListener(eventType, val);
|
|
173
|
+
}
|
|
174
|
+
} else if (isProperty(name)) {
|
|
175
|
+
if (name === "style" && typeof val === "object") {
|
|
176
|
+
const prevStyle = prev.style && typeof prev.style === "object" ? prev.style : {};
|
|
177
|
+
Object.keys(prevStyle).forEach(styleKey => {
|
|
178
|
+
if (!(styleKey in val)) dom.style[styleKey] = "";
|
|
179
|
+
});
|
|
180
|
+
Object.assign(dom.style, val);
|
|
181
|
+
} else if (name === "className" || name === "class") {
|
|
182
|
+
dom.className = val;
|
|
183
|
+
} else if (name === "innerHTML") {
|
|
184
|
+
dom.innerHTML = val;
|
|
185
|
+
} else if (name === "value" || name === "checked" || name === "selected" || name === "disabled") {
|
|
186
|
+
dom[name] = val;
|
|
187
|
+
} else {
|
|
188
|
+
if (name in dom) dom[name] = val;
|
|
189
|
+
else dom.setAttribute(name, val);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (typeof next.ref === "function") next.ref(dom);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sameVNode(a, b) {
|
|
198
|
+
if (!a || !b) return false;
|
|
199
|
+
if (a.tag !== b.tag) return false;
|
|
200
|
+
if (a.tag === "TEXT_ELEMENT") {
|
|
201
|
+
return a.props.nodeValue === b.props.nodeValue;
|
|
202
|
+
}
|
|
203
|
+
if (a.key !== undefined || b.key !== undefined) {
|
|
204
|
+
return a.key === b.key && a.tag === b.tag;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function callMount(vnode) {
|
|
210
|
+
if (!vnode) return;
|
|
211
|
+
if (typeof vnode.props?.onMount === "function" && vnode.dom) {
|
|
212
|
+
vnode.props.onMount(vnode.dom, vnode);
|
|
213
|
+
}
|
|
214
|
+
vnode.children.forEach(callMount);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function callUnmount(vnode) {
|
|
218
|
+
if (!vnode) return;
|
|
219
|
+
vnode.children.forEach(callUnmount);
|
|
220
|
+
if (typeof vnode.props?.onUnmount === "function" && vnode.dom) {
|
|
221
|
+
vnode.props.onUnmount(vnode.dom, vnode);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function patch(parentDom, oldVNode, newVNode, index = 0) {
|
|
226
|
+
const existingDom = parentDom.childNodes[index];
|
|
227
|
+
|
|
228
|
+
if (!oldVNode && newVNode) {
|
|
229
|
+
const newDom = createDom(newVNode);
|
|
230
|
+
parentDom.appendChild(newDom);
|
|
231
|
+
callMount(newVNode);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (oldVNode && !newVNode) {
|
|
236
|
+
callUnmount(oldVNode);
|
|
237
|
+
if (existingDom) parentDom.removeChild(existingDom);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!oldVNode || !newVNode) return;
|
|
242
|
+
|
|
243
|
+
if (!sameVNode(oldVNode, newVNode)) {
|
|
244
|
+
const newDom = createDom(newVNode);
|
|
245
|
+
callUnmount(oldVNode);
|
|
246
|
+
if (existingDom) parentDom.replaceChild(newDom, existingDom);
|
|
247
|
+
else parentDom.appendChild(newDom);
|
|
248
|
+
callMount(newVNode);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (newVNode.tag === "TEXT_ELEMENT") {
|
|
253
|
+
const oldText = oldVNode.props.nodeValue;
|
|
254
|
+
const newText = newVNode.props.nodeValue;
|
|
255
|
+
if (oldText !== newText && existingDom) existingDom.nodeValue = newText;
|
|
256
|
+
newVNode.dom = existingDom;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
newVNode.dom = existingDom;
|
|
261
|
+
updateDom(existingDom, oldVNode.props, newVNode.props);
|
|
262
|
+
|
|
263
|
+
const oldChildren = oldVNode.children || [];
|
|
264
|
+
const newChildren = newVNode.children || [];
|
|
265
|
+
const commonLength = Math.min(oldChildren.length, newChildren.length);
|
|
266
|
+
|
|
267
|
+
// 1. Update existing common children
|
|
268
|
+
for (let i = 0; i < commonLength; i++) {
|
|
269
|
+
patch(existingDom, oldChildren[i], newChildren[i], i);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 2. Add new children or remove excess old children
|
|
273
|
+
if (newChildren.length > oldChildren.length) {
|
|
274
|
+
for (let i = commonLength; i < newChildren.length; i++) {
|
|
275
|
+
patch(existingDom, null, newChildren[i], i);
|
|
276
|
+
}
|
|
277
|
+
} else if (oldChildren.length > newChildren.length) {
|
|
278
|
+
// Iterate backwards to keep DOM indices stable during removal
|
|
279
|
+
for (let i = oldChildren.length - 1; i >= commonLength; i--) {
|
|
280
|
+
patch(existingDom, oldChildren[i], null, i);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- ENTERPRISE UI COMPONENTS ---
|
|
286
|
+
const UI = {
|
|
287
|
+
button: (first, ...rest) => {
|
|
288
|
+
const { props, children } = normalizeElementArgs(first, rest);
|
|
289
|
+
const baseClass = "px-6 py-2.5 rounded-xl font-bold transition-all active:scale-95 flex items-center justify-center gap-2 ";
|
|
290
|
+
const variant = props.variant === 'outline'
|
|
291
|
+
? "border-2 border-blue-600 text-blue-600 hover:bg-blue-50"
|
|
292
|
+
: "bg-blue-600 text-white shadow-lg hover:shadow-blue-200";
|
|
293
|
+
return createVNode("button", { ...props, class: baseClass + variant + " " + (props.class || "") }, ...children);
|
|
294
|
+
},
|
|
295
|
+
input: (props = {}) => createVNode("div", { class: "flex flex-col gap-1 w-full" }, [
|
|
296
|
+
props.label ? createVNode("label", { class: "text-sm font-semibold text-gray-600 ml-1" }, props.label) : null,
|
|
297
|
+
createVNode("input", {
|
|
298
|
+
...props,
|
|
299
|
+
class: "px-4 py-3 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition-all " + (props.class || "")
|
|
300
|
+
})
|
|
301
|
+
]),
|
|
302
|
+
appBar: (props) => createVNode("nav", {
|
|
303
|
+
class: "sticky top-0 z-50 w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/80 border-b border-gray-100"
|
|
304
|
+
}, [
|
|
305
|
+
createVNode("div", { class: "text-xl font-black tracking-tighter text-blue-600" }, props.title || "CHEX"),
|
|
306
|
+
createVNode("div", { class: "hidden md:flex gap-6 font-medium text-gray-600" }, props.links || []),
|
|
307
|
+
props.trailing || createVNode("div", { class: "w-8 h-8 rounded-full bg-gray-200" })
|
|
308
|
+
]),
|
|
309
|
+
sideBar: (props) => {
|
|
310
|
+
const isOpen = props.isOpen !== false;
|
|
311
|
+
return createVNode("aside", {
|
|
312
|
+
class: `fixed left-0 top-0 h-full bg-white border-r border-gray-100 transition-all duration-300 z-[60] ${isOpen ? 'w-64' : 'w-0 -translate-x-full'} md:translate-x-0 md:w-64 p-6`
|
|
313
|
+
}, props.children || []);
|
|
314
|
+
},
|
|
315
|
+
bottomNav: (props) => createVNode("div", {
|
|
316
|
+
class: "fixed bottom-0 left-0 w-full h-16 bg-white border-t border-gray-100 flex md:hidden items-center justify-around px-4 pb-safe z-50"
|
|
317
|
+
}, props.items || []),
|
|
318
|
+
card: (first, ...rest) => {
|
|
319
|
+
const { props, children } = normalizeElementArgs(first, rest);
|
|
320
|
+
return createVNode("div", {
|
|
321
|
+
...props,
|
|
322
|
+
class: "bg-white rounded-3xl p-6 border border-gray-100 shadow-sm hover:shadow-md transition-shadow " + (props.class || "")
|
|
323
|
+
}, ...children);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// --- THE CORE BASE OBJECT ---
|
|
328
|
+
const base = {
|
|
329
|
+
isWeb,
|
|
330
|
+
signal,
|
|
331
|
+
effect,
|
|
332
|
+
createElement,
|
|
333
|
+
...UI,
|
|
334
|
+
|
|
335
|
+
render: function(container, componentFactory) {
|
|
336
|
+
if (!isWeb || !container) return;
|
|
337
|
+
let prevVNode = null;
|
|
338
|
+
const stop = effect(() => {
|
|
339
|
+
const nextVNode = typeof componentFactory === 'function' ? componentFactory() : componentFactory;
|
|
340
|
+
if (!prevVNode) {
|
|
341
|
+
container.innerHTML = '';
|
|
342
|
+
const dom = createDom(nextVNode);
|
|
343
|
+
container.appendChild(dom);
|
|
344
|
+
callMount(nextVNode);
|
|
345
|
+
} else {
|
|
346
|
+
patch(container, prevVNode, nextVNode, 0);
|
|
347
|
+
}
|
|
348
|
+
prevVNode = nextVNode;
|
|
349
|
+
});
|
|
350
|
+
return () => {
|
|
351
|
+
stop();
|
|
352
|
+
if (prevVNode) callUnmount(prevVNode);
|
|
353
|
+
container.innerHTML = '';
|
|
354
|
+
prevVNode = null;
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
createRouter: function(routes = {}, options = {}) {
|
|
359
|
+
const mode = options.mode === "history" ? "history" : "hash";
|
|
360
|
+
const basePath = (() => {
|
|
361
|
+
const rawBase = options.base ? String(options.base).trim() : "";
|
|
362
|
+
if (!rawBase || rawBase === "/") return "";
|
|
363
|
+
const withPrefix = rawBase.startsWith("/") ? rawBase : `/${rawBase}`;
|
|
364
|
+
return withPrefix.endsWith("/") ? withPrefix.slice(0, -1) : withPrefix;
|
|
365
|
+
})();
|
|
366
|
+
const normalizePath = (path) => {
|
|
367
|
+
if (!path) return "/";
|
|
368
|
+
const trimmed = String(path).trim();
|
|
369
|
+
if (trimmed === "#/" || trimmed === "#") return "/";
|
|
370
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed.replace(/^#/, "")}`;
|
|
371
|
+
};
|
|
372
|
+
const withBase = (path) => {
|
|
373
|
+
if (!basePath) return path;
|
|
374
|
+
if (path === "/") return `${basePath}/`;
|
|
375
|
+
return `${basePath}${path}`;
|
|
376
|
+
};
|
|
377
|
+
const stripBase = (path) => {
|
|
378
|
+
const normalized = normalizePath(path);
|
|
379
|
+
if (!basePath) return normalized;
|
|
380
|
+
if (normalized === basePath) return "/";
|
|
381
|
+
if (normalized.startsWith(`${basePath}/`)) {
|
|
382
|
+
return normalized.slice(basePath.length) || "/";
|
|
383
|
+
}
|
|
384
|
+
return normalized;
|
|
385
|
+
};
|
|
386
|
+
const readPath = () => {
|
|
387
|
+
if (!isWeb) return "/";
|
|
388
|
+
if (mode === "hash") {
|
|
389
|
+
return normalizePath(window.location.hash.replace(/^#/, ""));
|
|
390
|
+
}
|
|
391
|
+
return stripBase(window.location.pathname);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const current = signal(readPath());
|
|
395
|
+
const onChange = () => { current.value = readPath(); };
|
|
396
|
+
|
|
397
|
+
if (isWeb) {
|
|
398
|
+
if (mode === "hash") window.addEventListener("hashchange", onChange);
|
|
399
|
+
else window.addEventListener("popstate", onChange);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const navigate = (to) => {
|
|
403
|
+
const nextPath = normalizePath(to);
|
|
404
|
+
if (!isWeb) {
|
|
405
|
+
current.value = nextPath;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (mode === "hash") {
|
|
409
|
+
if (window.location.hash !== `#${nextPath}`) window.location.hash = nextPath;
|
|
410
|
+
else current.value = nextPath;
|
|
411
|
+
} else {
|
|
412
|
+
const target = withBase(nextPath);
|
|
413
|
+
if (window.location.pathname !== target) window.history.pushState({}, "", target);
|
|
414
|
+
current.value = nextPath;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const resolve = () => {
|
|
419
|
+
const page = routes[current.value] || routes["*"] || options.notFound;
|
|
420
|
+
if (!page) return createVNode("p", { style: "padding:12px" }, `Route "${current.value}" not found`);
|
|
421
|
+
return typeof page === "function" ? page({ path: current.value, navigate }) : page;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
path: current,
|
|
426
|
+
navigate,
|
|
427
|
+
resolve,
|
|
428
|
+
view: () => resolve(),
|
|
429
|
+
destroy: () => {
|
|
430
|
+
if (!isWeb) return;
|
|
431
|
+
if (mode === "hash") window.removeEventListener("hashchange", onChange);
|
|
432
|
+
else window.removeEventListener("popstate", onChange);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
notification: {
|
|
438
|
+
registerServiceWorker: async (swPath = '/sw.js') => {
|
|
439
|
+
if (isWeb && 'serviceWorker' in navigator) {
|
|
440
|
+
try {
|
|
441
|
+
return await navigator.serviceWorker.register(swPath);
|
|
442
|
+
} catch (e) { console.error('SW Registration Failed:', e); }
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
ask: async (phpUrl = null) => {
|
|
446
|
+
if (!isWeb || !("Notification" in window)) return false;
|
|
447
|
+
const permission = await Notification.requestPermission();
|
|
448
|
+
if (permission === "granted") {
|
|
449
|
+
const deviceId = btoa(navigator.userAgent + Math.random()).substring(0, 24);
|
|
450
|
+
if (phpUrl) {
|
|
451
|
+
fetch(phpUrl, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: { 'Content-Type': 'application/json' },
|
|
454
|
+
body: JSON.stringify({ deviceId, type: 'browser_push', timestamp: Date.now() })
|
|
455
|
+
}).catch(e => console.error("Chex Sync Error:", e));
|
|
456
|
+
}
|
|
457
|
+
return deviceId;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
},
|
|
461
|
+
push: (options) => {
|
|
462
|
+
if (isWeb && Notification.permission === "granted") {
|
|
463
|
+
const n = new Notification("Chex App", {
|
|
464
|
+
body: options.Content,
|
|
465
|
+
icon: options.icon || '',
|
|
466
|
+
});
|
|
467
|
+
n.onclick = () => {
|
|
468
|
+
window.focus();
|
|
469
|
+
if (options.url) window.open(options.url, '_blank');
|
|
470
|
+
n.close();
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
createStore: (initialState) => {
|
|
477
|
+
let state = { ...initialState };
|
|
478
|
+
const listeners = [];
|
|
479
|
+
return {
|
|
480
|
+
getState: () => state,
|
|
481
|
+
setState: (ns) => { state = { ...state, ...ns }; listeners.forEach(l => l(state)); },
|
|
482
|
+
subscribe: (l) => { listeners.push(l); return () => listeners.splice(listeners.indexOf(l), 1); }
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
api: {
|
|
487
|
+
connect: async (url, options = {}) => {
|
|
488
|
+
const config = {
|
|
489
|
+
method: options.method || 'GET',
|
|
490
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
491
|
+
...options
|
|
492
|
+
};
|
|
493
|
+
if (options.body && typeof options.body === 'object') config.body = JSON.stringify(options.body);
|
|
494
|
+
const res = await fetch(url, config);
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
const text = await res.text().catch(() => '');
|
|
497
|
+
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
|
|
498
|
+
}
|
|
499
|
+
return await res.json();
|
|
500
|
+
},
|
|
501
|
+
_cache: new Map(),
|
|
502
|
+
query: async function(key, fetcher, options = {}) {
|
|
503
|
+
const cacheKey = typeof key === "string" ? key : JSON.stringify(key);
|
|
504
|
+
const ttl = typeof options.ttl === "number" ? options.ttl : 30000;
|
|
505
|
+
const force = options.force === true;
|
|
506
|
+
const now = Date.now();
|
|
507
|
+
const cached = this._cache.get(cacheKey);
|
|
508
|
+
if (!force && cached && (now - cached.timestamp) < ttl) {
|
|
509
|
+
return cached.data;
|
|
510
|
+
}
|
|
511
|
+
const data = await fetcher();
|
|
512
|
+
this._cache.set(cacheKey, { data, timestamp: now });
|
|
513
|
+
return data;
|
|
514
|
+
},
|
|
515
|
+
invalidate: function(key) {
|
|
516
|
+
const cacheKey = typeof key === "string" ? key : JSON.stringify(key);
|
|
517
|
+
this._cache.delete(cacheKey);
|
|
518
|
+
},
|
|
519
|
+
clearCache: function() {
|
|
520
|
+
this._cache.clear();
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
db: {
|
|
525
|
+
request: async function(endpoint, payload = {}, options = {}) {
|
|
526
|
+
const headers = {
|
|
527
|
+
'Content-Type': 'application/json',
|
|
528
|
+
...(options.headers || {})
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
if (options.apiKey) {
|
|
532
|
+
headers['X-Chex-Key'] = String(options.apiKey);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const res = await fetch(endpoint, {
|
|
536
|
+
method: 'POST',
|
|
537
|
+
headers,
|
|
538
|
+
body: JSON.stringify(payload),
|
|
539
|
+
credentials: options.credentials || 'same-origin',
|
|
540
|
+
mode: options.mode || 'cors',
|
|
541
|
+
cache: 'no-store'
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const raw = await res.text();
|
|
545
|
+
let json = null;
|
|
546
|
+
try { json = raw ? JSON.parse(raw) : null; } catch (_) {}
|
|
547
|
+
|
|
548
|
+
if (!res.ok || (json && json.ok === false)) {
|
|
549
|
+
const message = (json && (json.error || json.message)) || `HTTP ${res.status}`;
|
|
550
|
+
throw new Error(message);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return json;
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
connect: function(config = {}) {
|
|
557
|
+
const endpoint = config.endpoint || '/Chex.php';
|
|
558
|
+
const defaultTable = config.table || null;
|
|
559
|
+
const options = {
|
|
560
|
+
apiKey: config.apiKey || '',
|
|
561
|
+
headers: config.headers || {},
|
|
562
|
+
credentials: config.credentials || 'same-origin',
|
|
563
|
+
mode: config.mode || 'cors'
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const send = (payload) => this.request(endpoint, payload, options);
|
|
567
|
+
const resolveTable = (tableOverride) => tableOverride || defaultTable;
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
request: (payload) => send(payload),
|
|
571
|
+
read: (args = {}) => send({
|
|
572
|
+
op: 'read',
|
|
573
|
+
table: resolveTable(args.table),
|
|
574
|
+
select: args.select,
|
|
575
|
+
where: args.where,
|
|
576
|
+
orderBy: args.orderBy,
|
|
577
|
+
limit: args.limit,
|
|
578
|
+
offset: args.offset
|
|
579
|
+
}),
|
|
580
|
+
create: (data, args = {}) => send({
|
|
581
|
+
op: 'create',
|
|
582
|
+
table: resolveTable(args.table),
|
|
583
|
+
data
|
|
584
|
+
}),
|
|
585
|
+
register: (data, args = {}) => send({
|
|
586
|
+
op: 'register',
|
|
587
|
+
table: resolveTable(args.table),
|
|
588
|
+
data
|
|
589
|
+
}),
|
|
590
|
+
update: (where, data, args = {}) => send({
|
|
591
|
+
op: 'update',
|
|
592
|
+
table: resolveTable(args.table),
|
|
593
|
+
where,
|
|
594
|
+
data
|
|
595
|
+
}),
|
|
596
|
+
remove: (where, args = {}) => send({
|
|
597
|
+
op: 'delete',
|
|
598
|
+
table: resolveTable(args.table),
|
|
599
|
+
where
|
|
600
|
+
}),
|
|
601
|
+
delete: (where, args = {}) => send({
|
|
602
|
+
op: 'delete',
|
|
603
|
+
table: resolveTable(args.table),
|
|
604
|
+
where
|
|
605
|
+
}),
|
|
606
|
+
exists: (where, args = {}) => send({
|
|
607
|
+
op: 'exists',
|
|
608
|
+
table: resolveTable(args.table),
|
|
609
|
+
where
|
|
610
|
+
}),
|
|
611
|
+
login: (credentials, args = {}) => send({
|
|
612
|
+
op: 'login',
|
|
613
|
+
table: resolveTable(args.table),
|
|
614
|
+
credentials,
|
|
615
|
+
login: {
|
|
616
|
+
userField: args.userField || 'email',
|
|
617
|
+
passField: args.passField || 'password_hash'
|
|
618
|
+
},
|
|
619
|
+
select: args.select || ['id', 'email']
|
|
620
|
+
})
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
// --- FIXED ANIMATION ENGINE ---
|
|
626
|
+
animate: function(selector, options = {}) {
|
|
627
|
+
if (!isWeb) return;
|
|
628
|
+
const elements = typeof selector === 'string' ? document.querySelectorAll(selector) : (selector instanceof HTMLElement ? [selector] : selector);
|
|
629
|
+
if (!elements || elements.length === 0) return;
|
|
630
|
+
|
|
631
|
+
const config = {
|
|
632
|
+
duration: options.duration || 800,
|
|
633
|
+
easing: options.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
634
|
+
once: options.once !== false
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const observer = new IntersectionObserver((entries) => {
|
|
638
|
+
entries.forEach(entry => {
|
|
639
|
+
if (entry.isIntersecting) {
|
|
640
|
+
this._play(entry.target, options, config);
|
|
641
|
+
if (config.once) observer.unobserve(entry.target);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}, { threshold: options.threshold || 0.1 });
|
|
645
|
+
|
|
646
|
+
elements.forEach(el => {
|
|
647
|
+
// Set initial state to avoid flash
|
|
648
|
+
el.style.opacity = options.opacity ? options.opacity[0] : 0;
|
|
649
|
+
observer.observe(el);
|
|
650
|
+
});
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
_play: (el, options, config) => {
|
|
654
|
+
// Directional Map for slideFrom logic
|
|
655
|
+
const directionMap = {
|
|
656
|
+
'top': 'translateY(-100px)',
|
|
657
|
+
'bottom': 'translateY(100px)',
|
|
658
|
+
'left': 'translateX(-100px)',
|
|
659
|
+
'right': 'translateX(100px)'
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const startTransform = options.transform
|
|
663
|
+
? options.transform[0]
|
|
664
|
+
: (directionMap[options.slideFrom] || 'translateY(40px)');
|
|
665
|
+
|
|
666
|
+
const keyframes = [
|
|
667
|
+
{
|
|
668
|
+
opacity: options.opacity ? options.opacity[0] : 0,
|
|
669
|
+
transform: startTransform
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
opacity: options.opacity ? options.opacity[1] : 1,
|
|
673
|
+
transform: options.transform ? options.transform[1] : 'translate(0, 0)'
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
el.animate(keyframes, {
|
|
678
|
+
duration: config.duration,
|
|
679
|
+
easing: config.easing,
|
|
680
|
+
fill: 'forwards'
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Dynamic tag helpers make `const { div, h1 } = Chex` possible.
|
|
686
|
+
return new Proxy(base, {
|
|
687
|
+
get(target, prop) {
|
|
688
|
+
if (prop in target) return target[prop];
|
|
689
|
+
if (typeof prop !== "string") return undefined;
|
|
690
|
+
return tag(prop);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
})();
|
|
694
|
+
|
|
695
|
+
Chex.mount = Chex.render;
|
|
696
|
+
Chex.chex = Chex.render;
|
|
697
|
+
|
|
698
|
+
// --- GLOBAL ATTACHMENT ---
|
|
699
|
+
if (typeof window !== 'undefined') {
|
|
700
|
+
window.Chex = Chex;
|
|
701
|
+
window.chex = Chex;
|
|
702
|
+
window.h = Chex.createElement;
|
|
703
|
+
// Convenient globals for plain-script projects. Module users can instead
|
|
704
|
+
// destructure these helpers from Chex.
|
|
705
|
+
["div", "main", "section", "header", "footer", "article", "span", "p", "h1", "h2", "h3", "button", "a", "img", "ul", "li", "input", "label"].forEach((name) => {
|
|
706
|
+
if (!(name in window)) window[name] = Chex[name];
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (typeof exports !== 'undefined') {
|
|
711
|
+
module.exports = Chex;
|
|
712
|
+
}
|