@ikdao/hyp 0.1.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/src/hyp.js ADDED
@@ -0,0 +1,671 @@
1
+ // HYP UI Framework
2
+
3
+ // Zero One One License - 011sl
4
+ // https://legal/ikdao.org/license/011sl
5
+ // Hyp UI Framework - [Hemang Tewari]
6
+
7
+ // HYP Organ Factory h()
8
+ // --- 1. Hyperscript h() ---
9
+
10
+ export const h = (ty, prp, ...chd) => {
11
+ // Normalize props & children
12
+ if (prp == null || typeof prp !== "object" || Array.isArray(prp)) {
13
+ chd.unshift(prp);
14
+ prp = {};
15
+ }
16
+
17
+ const stack = [...chd];
18
+ const flatChildren = [];
19
+
20
+ while (stack.length > 0) {
21
+ const item = stack.pop();
22
+ if (item == null || item === false) continue;
23
+
24
+ if (Array.isArray(item)) {
25
+ // Push in reverse to preserve order
26
+ for (let i = item.length - 1; i >= 0; i--) {
27
+ stack.push(item[i]);
28
+ }
29
+ } else if (item instanceof Actor) {
30
+ flatChildren.push(item);
31
+ } else if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
32
+ flatChildren.push(String(item));
33
+ } else if (typeof item === "object" && item.ty) {
34
+ flatChildren.push(item);
35
+ } else if (typeof item === "function") {
36
+ flatChildren.push(item());
37
+ } else {
38
+ flatChildren.push(String(item));
39
+ }
40
+ }
41
+
42
+ flatChildren.reverse(); // because we popped in reverse
43
+
44
+ // Handle component (function as type)
45
+ if (typeof ty === "function") {
46
+ return ty({ ...prp, children: flatChildren });
47
+ }
48
+
49
+ // Handle element
50
+ return {
51
+ ty,
52
+ prp,
53
+ chd: flatChildren,
54
+ key: prp.key ?? null,
55
+ ref: prp.ref ?? null,
56
+ };
57
+ };
58
+
59
+ // HYP Triad Architectural Pattern
60
+ // spatial/temporal/execution
61
+
62
+ // SCHEDULER (s)
63
+ // Temporal Layer — queues & runs tasks efficiently
64
+
65
+ export const s = (function () {
66
+ const left = new Set();
67
+ let flushing = false;
68
+
69
+ function flush() {
70
+ flushing = false;
71
+ const tasks = Array.from(left);
72
+ left.clear();
73
+ for (const task of tasks) {
74
+ try { task.fn(); }
75
+ catch (err) { console.error("Scheduler task error:", err); }
76
+ }
77
+ }
78
+
79
+ return {
80
+ add(fn, ei) {
81
+ if (ei && !o.isAlive(ei)) return;
82
+ left.add({ fn, ei });
83
+ if (!flushing) {
84
+ queueMicrotask(flush);
85
+ flushing = true;
86
+ }
87
+ },
88
+
89
+ flush() { flush(); },
90
+
91
+ clear(ei) {
92
+ for (const task of [...left]) {
93
+ if (task.ei === ei) left.delete(task);
94
+ }
95
+ }
96
+ };
97
+ })();
98
+
99
+ // ORGANISER (o)
100
+ // Structural Layer — Organise Organs, keep identities and map
101
+
102
+ export const o = (function () {
103
+ const organs = new Map();
104
+ let nextEi = 1;
105
+
106
+ function newEi() { return "ei_" + nextEi++; }
107
+
108
+ return {
109
+ create(hi, body) {
110
+ const ei = newEi();
111
+ organs.set(ei, {
112
+ hi,
113
+ body,
114
+ ctx: new Map(),
115
+ mounted: true,
116
+ lifecycles: {
117
+ willMount: [], didMount: [],
118
+ willUpdate: [], didUpdate: [],
119
+ willUnmount: [], didUnmount: []
120
+ },
121
+ effects: new Set()
122
+ });
123
+ return ei;
124
+ },
125
+ addLifecycle(ei, phase, fn) {
126
+ const inst = organs.get(ei);
127
+ if (inst) inst.lifecycles[phase].push(fn);
128
+ },
129
+ runLifecycle(ei, phase, bodyRef) {
130
+ const inst = organs.get(ei);
131
+ if (!inst) return;
132
+ const list = inst.lifecycles[phase];
133
+ if (!list) return;
134
+ for (const fn of list)
135
+ s.add(() => fn(bodyRef), ei);
136
+ },
137
+ addEffect(ei, clear) {
138
+ const inst = organs.get(ei);
139
+ if (inst) inst.effects.add({ clear });
140
+ },
141
+ destroy(ei, { runLifecycle = true } = {}) {
142
+ const inst = organs.get(ei);
143
+ if (!inst) return;
144
+
145
+ inst.mounted = false;
146
+
147
+ if (runLifecycle) {
148
+ this.runLifecycle(ei, "willUnmount");
149
+ s.add(() => this.runLifecycle(ei, "didUnmount"), ei);
150
+ }
151
+
152
+ if (inst.effects) {
153
+ for (const ef of inst.effects)
154
+ if (typeof ef.clear === "function") {
155
+ try { ef.clear(); }
156
+ catch (err) { console.error("Effect clear error:", err); }
157
+ }
158
+ }
159
+ organs.delete(ei);
160
+ s.clear(ei);
161
+ },
162
+ get(ei) { return organs.get(ei); },
163
+ has(ei) { return organs.has(ei); },
164
+ isAlive(ei) {
165
+ const inst = organs.get(ei);
166
+ return inst ? inst.mounted : false;
167
+ },
168
+ all() { return organs; }
169
+ };
170
+ })();
171
+
172
+ // executor e()
173
+ // EI execution identity/instance
174
+ // render/update/unmount
175
+
176
+ export const e = (function () {
177
+ const execStack = [];
178
+
179
+ function pushEI(ei) { execStack.push(ei); }
180
+ function popEI() { execStack.pop(); }
181
+ function currentEI() { return execStack[execStack.length - 1] || null; }
182
+
183
+ function render(vnode, body) {
184
+ const hi = vnode?.ty?.name || vnode?.ty || "anonymous";
185
+ const ei = o.create(hi, body);
186
+ pushEI(ei);
187
+ o.runLifecycle(ei, "willMount");
188
+ const dom = createDom(vnode, ei);
189
+ if (body) body.appendChild(dom);
190
+ s.add(() => o.runLifecycle(ei, "didMount"), ei);
191
+ popEI();
192
+ return ei;
193
+ }
194
+
195
+ function patch(dom, oldVNode, newVNode, ei) {
196
+ // Guard: invalid DOM or dead instance
197
+ if (!dom || !o.isAlive(ei)) return dom;
198
+
199
+ if (oldVNode == null) {
200
+ const newDom = createDom(newVNode, ei);
201
+ dom.replaceWith(newDom);
202
+ s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
203
+ return newDom;
204
+ }
205
+
206
+ if (newVNode == null) {
207
+ dom.remove();
208
+ return null;
209
+ }
210
+
211
+ pushEI(ei);
212
+ o.runLifecycle(ei, "willUpdate");
213
+
214
+ // Type or key changed → full replace
215
+ if (oldVNode.ty !== newVNode.ty || oldVNode.key !== newVNode.key) {
216
+ const newDom = createDom(newVNode, ei);
217
+ dom.replaceWith(newDom);
218
+ s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
219
+ popEI();
220
+ return newDom;
221
+ }
222
+
223
+ // Handle reactive text nodes (Actor)
224
+ if (oldVNode instanceof Actor && newVNode instanceof Actor) {
225
+ dom.data = newVNode.get();
226
+ s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
227
+ popEI();
228
+ return dom;
229
+ }
230
+
231
+ // Handle primitive text nodes
232
+ if ((typeof oldVNode === "string" || typeof oldVNode === "number") &&
233
+ (typeof newVNode === "string" || typeof newVNode === "number")) {
234
+ const newVal = String(newVNode);
235
+ if (dom.data !== newVal) dom.data = newVal;
236
+ s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
237
+ popEI();
238
+ return dom;
239
+ }
240
+
241
+ // Update props and children
242
+ updateprps(dom, oldVNode.prp || {}, newVNode.prp || {});
243
+ patchChildren(dom, oldVNode.chd || [], newVNode.chd || [], ei);
244
+
245
+ // Handle ref
246
+ if (newVNode.ref) newVNode.ref(dom);
247
+
248
+ s.add(() => o.runLifecycle(ei, "didUpdate"), ei);
249
+ popEI();
250
+
251
+ return dom;
252
+ }
253
+
254
+ function unmount(vnode = null, ei) {
255
+ const inst = o.get(ei);
256
+ if (!inst) return;
257
+ pushEI(ei);
258
+ const bodyRef = inst.body;
259
+
260
+ o.runLifecycle(ei, "willUnmount");
261
+ if (bodyRef?.parentNode)
262
+ bodyRef.parentNode.removeChild(bodyRef);
263
+
264
+ s.add(() => o.runLifecycle(ei, "didUnmount", bodyRef), ei);
265
+
266
+ o.destroy(ei, { runLifecycle: false });
267
+ popEI();
268
+ }
269
+
270
+
271
+ function createDom(v, ei) {
272
+ // null or primitive → text node
273
+ if (v == null) return document.createTextNode("");
274
+ if (typeof v === "string" || typeof v === "number")
275
+ return document.createTextNode(String(v));
276
+
277
+ // Reactive text node (Actor or dA)
278
+ if (v instanceof Actor) {
279
+ const textNode = document.createTextNode(v.get());
280
+ const update = () => { textNode.data = v.get(); };
281
+ const unsub = v.subscribe(update);
282
+ // tie cleanup to organiser (o)
283
+ if (ei) o.addEffect(ei, unsub);
284
+ return textNode;
285
+ }
286
+
287
+ const el = document.createElement(v.ty);
288
+
289
+ for (const [k, val] of Object.entries(v.prp || {})) {
290
+ if (k.startsWith("on") && typeof val === "function") {
291
+ el.addEventListener(k.slice(2).toLowerCase(), val);
292
+ continue;
293
+ }
294
+ if (k === "style" && typeof val === "object") {
295
+ for (const [sk, sv] of Object.entries(val)) {
296
+ if (sv instanceof Actor) {
297
+ const updateStyle = () => { el.style[sk] = sv.get(); };
298
+ updateStyle();
299
+ const unsub = sv.subscribe(updateStyle);
300
+ if (ei) o.addEffect(ei, unsub);
301
+ } else {
302
+ el.style[sk] = sv;
303
+ }
304
+ }
305
+ continue;
306
+ }
307
+ if (val instanceof Actor) {
308
+ const updateAttr = () => {
309
+ const next = val.get();
310
+ if (k in el) el[k] = next;
311
+ else el.setAttribute(k, next);
312
+ };
313
+ updateAttr();
314
+ const unsub = val.subscribe(updateAttr);
315
+ if (ei) o.addEffect(ei, unsub);
316
+ continue;
317
+ }
318
+
319
+ if (k in el) el[k] = val;
320
+ else el.setAttribute(k, val);
321
+ }
322
+
323
+ (v.chd || []).forEach(ch => {
324
+ el.appendChild(createDom(ch, ei));
325
+ });
326
+
327
+ if (v.ref) v.ref(el);
328
+
329
+ return el;
330
+ }
331
+
332
+ function updateprps(dom, oldprps, newprps) {
333
+ for (const k in oldprps) {
334
+ if (!(k in newprps)) {
335
+ if (k.startsWith("on") && typeof oldprps[k] === "function")
336
+ dom.removeEventListener(k.slice(2).toLowerCase(), oldprps[k]);
337
+ else
338
+ dom.removeAttribute(k);
339
+ }
340
+ }
341
+
342
+ for (const [k, v] of Object.entries(newprps)) {
343
+ if (oldprps[k] !== v) {
344
+ if (k.startsWith("on") && typeof v === "function") {
345
+ if (oldprps[k]) dom.removeEventListener(k.slice(2).toLowerCase(), oldprps[k]);
346
+ dom.addEventListener(k.slice(2).toLowerCase(), v);
347
+ } else {
348
+ dom.setAttribute(k, v);
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ function patchChildren(dom, oldCh, newCh, ei) {
355
+ const oldKeyed = new Map();
356
+ const usedKeys = new Set();
357
+
358
+ // Index old children by key (skip non-keyed)
359
+ oldCh.forEach((c, i) => {
360
+ if (c && c.key != null) {
361
+ oldKeyed.set(c.key, { vnode: c, dom: dom.childNodes[i], index: i });
362
+ }
363
+ });
364
+
365
+ const newDoms = [];
366
+
367
+ // Process each new child
368
+ for (let i = 0; i < newCh.length; i++) {
369
+ const newV = newCh[i];
370
+ let newDom;
371
+
372
+ if (newV && newV.key != null) {
373
+ // Keyed node: try to reuse
374
+ const oldEntry = oldKeyed.get(newV.key);
375
+ if (oldEntry && oldEntry.dom) {
376
+ newDom = patch(oldEntry.dom, oldEntry.vnode, newV, ei);
377
+ usedKeys.add(newV.key);
378
+ } else {
379
+ // Create new
380
+ newDom = createDom(newV, ei);
381
+ }
382
+ } else {
383
+ // Non-keyed: patch by index if possible
384
+ const oldV = oldCh[i];
385
+ const oldDom = dom.childNodes[i];
386
+ if (oldDom && oldV != null) {
387
+ newDom = patch(oldDom, oldV, newV, ei);
388
+ } else if (oldDom) {
389
+ // Replace with new content
390
+ newDom = createDom(newV, ei);
391
+ oldDom.replaceWith(newDom);
392
+ } else {
393
+ // Append new
394
+ newDom = createDom(newV, ei);
395
+ }
396
+ }
397
+
398
+ newDoms.push(newDom);
399
+ }
400
+
401
+ // Update DOM order to match newDoms
402
+ for (let i = 0; i < newDoms.length; i++) {
403
+ const nextDom = dom.childNodes[i];
404
+ if (nextDom !== newDoms[i]) {
405
+ dom.insertBefore(newDoms[i], nextDom || null);
406
+ }
407
+ }
408
+
409
+ // Remove unused keyed nodes
410
+ for (const [key, entry] of oldKeyed) {
411
+ if (!usedKeys.has(key) && entry.dom) {
412
+ entry.dom.remove();
413
+ }
414
+ }
415
+
416
+ // Remove extra non-keyed nodes at the end
417
+ while (dom.childNodes.length > newCh.length) {
418
+ dom.lastChild.remove();
419
+ }
420
+ }
421
+
422
+ return { render, patch, unmount, pushEI, popEI, currentEI };
423
+ })();
424
+
425
+ // Active/Reactive/Interactive Parts
426
+
427
+ // Actor a()
428
+ let tr = null;
429
+ export class Actor {
430
+ constructor(initial) {
431
+ this.value = initial;
432
+ this.subs = new Set();
433
+ }
434
+ get() {
435
+ if (tr) this.subs.add(tr);
436
+ return this.value;
437
+ }
438
+ set(next) {
439
+ if (next === this.value) return;
440
+ this.value = next;
441
+ this.subs.forEach(fn => s.add(fn));
442
+ }
443
+ subscribe(fn) {
444
+ this.subs.add(fn);
445
+ return () => this.subs.delete(fn);
446
+ }
447
+ }
448
+ export const a = (initial) => new Actor(initial);
449
+
450
+ // Reactor r()/Derived Act dA()
451
+ export const r = (compute) => {
452
+ const sig = a();
453
+ const recompute = () => {
454
+ tr = recompute;
455
+ const val = compute();
456
+ tr = null;
457
+ sig.set(val);
458
+ };
459
+ recompute();
460
+ return sig;
461
+ };
462
+
463
+ // Interactor i()/Side Act sA()
464
+ export const i = (effect, explicitEI = null) => {
465
+ const ei = explicitEI ?? e.currentEI();
466
+ if (!ei) return;
467
+ let cleanup; // Track previous effect cleanup
468
+ const run = () => {
469
+ if (cleanup) {
470
+ try { cleanup(); } catch (err) { console.error("sA cleanup error:", err); }
471
+ }
472
+ tr = run;
473
+ cleanup = effect();
474
+ tr = null;
475
+ if (cleanup) o.addEffect(ei, cleanup);
476
+ };
477
+ run();
478
+ };
479
+
480
+ /* ---- Utilities -------------------------------- */
481
+
482
+ function parseQuery(search) {
483
+ return Object.fromEntries(new URLSearchParams(search || ""));
484
+ }
485
+
486
+ function compileMatcher(pattern) {
487
+ const keys = [];
488
+ const regex = new RegExp(
489
+ "^" +
490
+ pattern
491
+ .replace(/\/+$/, "")
492
+ .replace(/:([^/]+)/g, (_, k) => {
493
+ keys.push(k);
494
+ return "([^/]+)";
495
+ }) +
496
+ "$"
497
+ );
498
+
499
+ return (path) => {
500
+ const m = path.replace(/\/+$/, "").match(regex);
501
+ if (!m) return null;
502
+ const params = {};
503
+ keys.forEach((k, i) => (params[k] = decodeURIComponent(m[i + 1])));
504
+ return params;
505
+ };
506
+ }
507
+
508
+ /* ---- Navigator factory -------------- */
509
+
510
+ export function createNavigator({
511
+ url = null,
512
+ historyEnabled = true
513
+ } = {}) {
514
+ const initialURL = url
515
+ ? new URL(url, "http://localhost")
516
+ : typeof window !== "undefined"
517
+ ? window.location
518
+ : { pathname: "/", search: "" };
519
+
520
+ /* path is ALWAYS a string */
521
+ const path = a(String(initialURL.pathname));
522
+ const query = a(parseQuery(initialURL.search));
523
+ const params = a({});
524
+
525
+ /* async state */
526
+ const loading = a(false);
527
+ const error = a(null);
528
+
529
+ const routes = [];
530
+ let beforeEach = null;
531
+
532
+ /* canonical path setter */
533
+ function setPath(next) {
534
+ if (typeof next !== "string") {
535
+ console.warn("[navigator] path must be string, got:", next);
536
+ next = String(next);
537
+ }
538
+ path.set(next);
539
+ }
540
+
541
+ function syncFromLocation() {
542
+ if (typeof window === "undefined") return;
543
+ setPath(window.location.pathname);
544
+ query.set(parseQuery(window.location.search));
545
+ matchRoutes();
546
+ }
547
+
548
+ function matchRoutes() {
549
+ const p = path.get();
550
+ for (const r of routes) {
551
+ const res = r.match(p);
552
+ if (res) {
553
+ params.set(res);
554
+ return r;
555
+ }
556
+ }
557
+ params.set({});
558
+ return null;
559
+ }
560
+
561
+ async function navigate(to, replace = false) {
562
+ const from = path.get();
563
+ const target = String(to);
564
+
565
+ /* Guards */
566
+ if (beforeEach) {
567
+ const result = await beforeEach(target, from);
568
+ if (result === false) return;
569
+ if (typeof result === "string") {
570
+ return navigate(result, true);
571
+ }
572
+ }
573
+
574
+ if (typeof window !== "undefined" && historyEnabled) {
575
+ if (replace) history.replaceState(null, "", target);
576
+ else history.pushState(null, "", target);
577
+ }
578
+
579
+ setPath(target);
580
+ matchRoutes();
581
+ }
582
+
583
+ if (typeof window !== "undefined" && historyEnabled) {
584
+ window.addEventListener("popstate", syncFromLocation);
585
+ }
586
+
587
+ /* -------- 4: async resolve ---------------- */
588
+
589
+ async function resolveAsync() {
590
+ loading.set(true);
591
+ error.set(null);
592
+
593
+ try {
594
+ const r = matchRoutes();
595
+ if (!r) {
596
+ loading.set(false);
597
+ return null;
598
+ }
599
+
600
+ let view = r.view;
601
+
602
+ // Allow lazy / async route views
603
+ if (typeof view === "function") {
604
+ const res = view();
605
+ view = res instanceof Promise ? await res : res;
606
+ }
607
+
608
+ loading.set(false);
609
+ return view;
610
+ } catch (err) {
611
+ error.set(err);
612
+ loading.set(false);
613
+ throw err;
614
+ }
615
+ }
616
+
617
+ return {
618
+ /* Reactive state */
619
+ path,
620
+ query,
621
+ params,
622
+ loading,
623
+ error,
624
+
625
+ /* Routing table */
626
+ route(pattern, view) {
627
+ routes.push({
628
+ pattern,
629
+ view,
630
+ match: compileMatcher(pattern)
631
+ });
632
+ },
633
+
634
+ /* Navigation */
635
+ go(to) {
636
+ return navigate(to, false);
637
+ },
638
+
639
+ replace(to) {
640
+ return navigate(to, true);
641
+ },
642
+
643
+ back() {
644
+ if (typeof window !== "undefined") history.back();
645
+ },
646
+
647
+ forward() {
648
+ if (typeof window !== "undefined") history.forward();
649
+ },
650
+
651
+ /* Guards */
652
+ beforeEach(fn) {
653
+ beforeEach = fn;
654
+ },
655
+
656
+ /* Sync resolve (unchanged) */
657
+ resolve() {
658
+ const r = matchRoutes();
659
+ return r ? r.view : null;
660
+ },
661
+
662
+ /* Async resolve (NEW) */
663
+ resolveAsync
664
+ };
665
+ }
666
+
667
+ export const n = createNavigator();
668
+
669
+ const HYP = { h, e, o, s, a, r, i, n };
670
+ window.HYP = HYP;
671
+ export default HYP;
package/src/i.js ADDED
@@ -0,0 +1,20 @@
1
+ // Interactor i()/Side Act sA()
2
+ // Self License - 01SL
3
+ // HYP UI Framework
4
+ // Author: Hemang Tewari
5
+
6
+ export const i = (effect, explicitEI = null) => {
7
+ const ei = explicitEI ?? e.currentEI();
8
+ if (!ei) return;
9
+ let cleanup; // Track previous effect cleanup
10
+ const run = () => {
11
+ if (cleanup) {
12
+ try { cleanup(); } catch (err) { console.error("i cleanup error:", err); }
13
+ }
14
+ tr = run;
15
+ cleanup = effect();
16
+ tr = null;
17
+ if (cleanup) o.addEffect(ei, cleanup);
18
+ };
19
+ run();
20
+ };