@hypen-space/web 0.4.31 → 0.4.35

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.
Files changed (143) hide show
  1. package/dist/canvas/accessibility.js +19 -9
  2. package/dist/canvas/accessibility.js.map +2 -2
  3. package/dist/canvas/events.d.ts +7 -0
  4. package/dist/canvas/events.js +53 -17
  5. package/dist/canvas/events.js.map +3 -3
  6. package/dist/canvas/index.js +19 -2134
  7. package/dist/canvas/index.js.map +3 -11
  8. package/dist/canvas/input.js +19 -9
  9. package/dist/canvas/input.js.map +2 -2
  10. package/dist/canvas/layout.js +19 -9
  11. package/dist/canvas/layout.js.map +2 -2
  12. package/dist/canvas/paint.d.ts +4 -0
  13. package/dist/canvas/paint.js +100 -11
  14. package/dist/canvas/paint.js.map +3 -3
  15. package/dist/canvas/renderer.js +821 -707
  16. package/dist/canvas/renderer.js.map +7 -7
  17. package/dist/canvas/text.js +19 -9
  18. package/dist/canvas/text.js.map +2 -2
  19. package/dist/canvas/utils.js +19 -9
  20. package/dist/canvas/utils.js.map +2 -2
  21. package/dist/client.js +19 -3863
  22. package/dist/client.js.map +3 -57
  23. package/dist/dom/applicators/advanced-layout.js +19 -9
  24. package/dist/dom/applicators/advanced-layout.js.map +2 -2
  25. package/dist/dom/applicators/background.js +19 -9
  26. package/dist/dom/applicators/background.js.map +2 -2
  27. package/dist/dom/applicators/border.js +19 -9
  28. package/dist/dom/applicators/border.js.map +2 -2
  29. package/dist/dom/applicators/color.js +19 -9
  30. package/dist/dom/applicators/color.js.map +2 -2
  31. package/dist/dom/applicators/display.js +19 -9
  32. package/dist/dom/applicators/display.js.map +2 -2
  33. package/dist/dom/applicators/effects.js +19 -9
  34. package/dist/dom/applicators/effects.js.map +2 -2
  35. package/dist/dom/applicators/events.js +26 -10
  36. package/dist/dom/applicators/events.js.map +3 -3
  37. package/dist/dom/applicators/font.js +19 -9
  38. package/dist/dom/applicators/font.js.map +2 -2
  39. package/dist/dom/applicators/index.js +26 -10
  40. package/dist/dom/applicators/index.js.map +3 -3
  41. package/dist/dom/applicators/layout.js +19 -9
  42. package/dist/dom/applicators/layout.js.map +2 -2
  43. package/dist/dom/applicators/margin.js +19 -9
  44. package/dist/dom/applicators/margin.js.map +2 -2
  45. package/dist/dom/applicators/padding.js +19 -9
  46. package/dist/dom/applicators/padding.js.map +2 -2
  47. package/dist/dom/applicators/size.js +19 -9
  48. package/dist/dom/applicators/size.js.map +2 -2
  49. package/dist/dom/applicators/transform.js +19 -9
  50. package/dist/dom/applicators/transform.js.map +2 -2
  51. package/dist/dom/applicators/transition.js +19 -9
  52. package/dist/dom/applicators/transition.js.map +2 -2
  53. package/dist/dom/applicators/typography.js +19 -9
  54. package/dist/dom/applicators/typography.js.map +2 -2
  55. package/dist/dom/canvas/index.js +19 -9
  56. package/dist/dom/canvas/index.js.map +2 -2
  57. package/dist/dom/components/app.js +19 -9
  58. package/dist/dom/components/app.js.map +2 -2
  59. package/dist/dom/components/audio.js +19 -9
  60. package/dist/dom/components/audio.js.map +2 -2
  61. package/dist/dom/components/avatar.js +19 -9
  62. package/dist/dom/components/avatar.js.map +2 -2
  63. package/dist/dom/components/badge.js +19 -9
  64. package/dist/dom/components/badge.js.map +2 -2
  65. package/dist/dom/components/button.js +19 -9
  66. package/dist/dom/components/button.js.map +2 -2
  67. package/dist/dom/components/card.js +19 -9
  68. package/dist/dom/components/card.js.map +2 -2
  69. package/dist/dom/components/center.js +19 -9
  70. package/dist/dom/components/center.js.map +2 -2
  71. package/dist/dom/components/checkbox.js +19 -9
  72. package/dist/dom/components/checkbox.js.map +2 -2
  73. package/dist/dom/components/column.js +19 -9
  74. package/dist/dom/components/column.js.map +2 -2
  75. package/dist/dom/components/container.js +19 -9
  76. package/dist/dom/components/container.js.map +2 -2
  77. package/dist/dom/components/divider.js +19 -9
  78. package/dist/dom/components/divider.js.map +2 -2
  79. package/dist/dom/components/grid.js +19 -9
  80. package/dist/dom/components/grid.js.map +2 -2
  81. package/dist/dom/components/heading.js +19 -9
  82. package/dist/dom/components/heading.js.map +2 -2
  83. package/dist/dom/components/hypenapp.js +19 -9
  84. package/dist/dom/components/hypenapp.js.map +2 -2
  85. package/dist/dom/components/image.js +19 -9
  86. package/dist/dom/components/image.js.map +2 -2
  87. package/dist/dom/components/index.js +20 -10
  88. package/dist/dom/components/index.js.map +3 -3
  89. package/dist/dom/components/input.js +19 -9
  90. package/dist/dom/components/input.js.map +2 -2
  91. package/dist/dom/components/link.js +19 -9
  92. package/dist/dom/components/link.js.map +2 -2
  93. package/dist/dom/components/list.js +19 -9
  94. package/dist/dom/components/list.js.map +2 -2
  95. package/dist/dom/components/paragraph.js +19 -9
  96. package/dist/dom/components/paragraph.js.map +2 -2
  97. package/dist/dom/components/progressbar.js +19 -9
  98. package/dist/dom/components/progressbar.js.map +2 -2
  99. package/dist/dom/components/route.js +19 -9
  100. package/dist/dom/components/route.js.map +2 -2
  101. package/dist/dom/components/router.js +19 -9
  102. package/dist/dom/components/router.js.map +2 -2
  103. package/dist/dom/components/row.js +19 -9
  104. package/dist/dom/components/row.js.map +2 -2
  105. package/dist/dom/components/select.js +19 -9
  106. package/dist/dom/components/select.js.map +2 -2
  107. package/dist/dom/components/slider.js +19 -9
  108. package/dist/dom/components/slider.js.map +2 -2
  109. package/dist/dom/components/spacer.js +19 -9
  110. package/dist/dom/components/spacer.js.map +2 -2
  111. package/dist/dom/components/spinner.js +19 -9
  112. package/dist/dom/components/spinner.js.map +2 -2
  113. package/dist/dom/components/stack.js +19 -9
  114. package/dist/dom/components/stack.js.map +2 -2
  115. package/dist/dom/components/switch.js +19 -9
  116. package/dist/dom/components/switch.js.map +2 -2
  117. package/dist/dom/components/text.js +19 -9
  118. package/dist/dom/components/text.js.map +2 -2
  119. package/dist/dom/components/textarea.js +20 -10
  120. package/dist/dom/components/textarea.js.map +3 -3
  121. package/dist/dom/components/video.js +19 -9
  122. package/dist/dom/components/video.js.map +2 -2
  123. package/dist/dom/debug.js +19 -9
  124. package/dist/dom/debug.js.map +2 -2
  125. package/dist/dom/element-data.js +19 -9
  126. package/dist/dom/element-data.js.map +2 -2
  127. package/dist/dom/events.js +28 -18
  128. package/dist/dom/events.js.map +3 -3
  129. package/dist/dom/index.js +19 -3682
  130. package/dist/dom/index.js.map +3 -57
  131. package/dist/dom/renderer.js +31 -11
  132. package/dist/dom/renderer.js.map +5 -5
  133. package/dist/hypen.js +31 -11
  134. package/dist/hypen.js.map +5 -5
  135. package/dist/index.js +19 -5810
  136. package/dist/index.js.map +3 -66
  137. package/package.json +2 -2
  138. package/src/canvas/events.ts +35 -8
  139. package/src/canvas/paint.ts +104 -4
  140. package/src/dom/applicators/events.ts +11 -2
  141. package/src/dom/components/textarea.ts +3 -3
  142. package/src/dom/events.ts +14 -12
  143. package/src/dom/renderer.ts +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/web",
3
- "version": "0.4.31",
3
+ "version": "0.4.35",
4
4
  "description": "Hypen web renderers - DOM and Canvas rendering for browsers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "clean": "rm -rf dist"
57
57
  },
58
58
  "dependencies": {
59
- "@hypen-space/core": "^0.4.31"
59
+ "@hypen-space/core": "^0.4.35"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/bun": "latest",
@@ -23,6 +23,15 @@ export class CanvasEventManager {
23
23
  private focusedNode: VirtualNode | null = null;
24
24
  private mouseDownNode: VirtualNode | null = null;
25
25
 
26
+ // Bound handler references for cleanup
27
+ private boundOnMouseMove!: (e: MouseEvent) => void;
28
+ private boundOnMouseDown!: (e: MouseEvent) => void;
29
+ private boundOnMouseUp!: (e: MouseEvent) => void;
30
+ private boundOnClick!: (e: MouseEvent) => void;
31
+ private boundOnDoubleClick!: (e: MouseEvent) => void;
32
+ private boundOnKeyDown!: (e: KeyboardEvent) => void;
33
+ private boundOnKeyUp!: (e: KeyboardEvent) => void;
34
+
26
35
  constructor(canvas: HTMLCanvasElement, engine: IEngine) {
27
36
  this.canvas = canvas;
28
37
  this.engine = engine;
@@ -40,13 +49,21 @@ export class CanvasEventManager {
40
49
  * Setup canvas event listeners
41
50
  */
42
51
  private setupEventListeners(): void {
43
- this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
44
- this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
45
- this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
46
- this.canvas.addEventListener("click", this.onClick.bind(this));
47
- this.canvas.addEventListener("dblclick", this.onDoubleClick.bind(this));
48
- this.canvas.addEventListener("keydown", this.onKeyDown.bind(this));
49
- this.canvas.addEventListener("keyup", this.onKeyUp.bind(this));
52
+ this.boundOnMouseMove = this.onMouseMove.bind(this);
53
+ this.boundOnMouseDown = this.onMouseDown.bind(this);
54
+ this.boundOnMouseUp = this.onMouseUp.bind(this);
55
+ this.boundOnClick = this.onClick.bind(this);
56
+ this.boundOnDoubleClick = this.onDoubleClick.bind(this);
57
+ this.boundOnKeyDown = this.onKeyDown.bind(this);
58
+ this.boundOnKeyUp = this.onKeyUp.bind(this);
59
+
60
+ this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
61
+ this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
62
+ this.canvas.addEventListener("mouseup", this.boundOnMouseUp);
63
+ this.canvas.addEventListener("click", this.boundOnClick);
64
+ this.canvas.addEventListener("dblclick", this.boundOnDoubleClick);
65
+ this.canvas.addEventListener("keydown", this.boundOnKeyDown);
66
+ this.canvas.addEventListener("keyup", this.boundOnKeyUp);
50
67
  }
51
68
 
52
69
  /**
@@ -293,7 +310,17 @@ export class CanvasEventManager {
293
310
  * Cleanup
294
311
  */
295
312
  destroy(): void {
296
- // Remove event listeners if needed
313
+ this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
314
+ this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
315
+ this.canvas.removeEventListener("mouseup", this.boundOnMouseUp);
316
+ this.canvas.removeEventListener("click", this.boundOnClick);
317
+ this.canvas.removeEventListener("dblclick", this.boundOnDoubleClick);
318
+ this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
319
+ this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
320
+ this.rootNode = null;
321
+ this.hoveredNode = null;
322
+ this.focusedNode = null;
323
+ this.mouseDownNode = null;
297
324
  }
298
325
  }
299
326
 
@@ -416,6 +416,62 @@ function paintInput(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
416
416
  }
417
417
  }
418
418
 
419
+ /**
420
+ * Image cache for canvas renderer
421
+ */
422
+ const MAX_IMAGE_CACHE_SIZE = 100;
423
+ const MAX_FAIL_CACHE_SIZE = 200;
424
+ const imageCache = new Map<string, HTMLImageElement>();
425
+ const imagePending = new Set<string>();
426
+ const imageInFlight = new Map<string, HTMLImageElement>();
427
+ const imageFailCache = new Map<string, { failedAt: number; attempts: number }>();
428
+ const IMAGE_FAIL_COOLDOWN_MS = 30_000;
429
+ const IMAGE_MAX_ATTEMPTS = 3;
430
+
431
+ /**
432
+ * Evict oldest entries from a Map when it exceeds the given maximum size.
433
+ */
434
+ function evictMap<V>(map: Map<string, V>, max: number): void {
435
+ while (map.size > max) {
436
+ const oldest = map.keys().next().value;
437
+ if (oldest === undefined) break;
438
+ map.delete(oldest);
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Purge expired entries from imageFailCache.
444
+ */
445
+ function purgeExpiredFailures(): void {
446
+ const now = Date.now();
447
+ for (const [key, entry] of imageFailCache) {
448
+ if (entry.attempts >= IMAGE_MAX_ATTEMPTS) continue; // Keep max-attempt entries until evicted by size
449
+ if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
450
+ imageFailCache.delete(key);
451
+ }
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Clear the image cache and cancel all in-flight loads, freeing resources.
457
+ */
458
+ export function clearImageCache(): void {
459
+ // Cancel in-flight Image loads so their callbacks cannot repopulate caches
460
+ for (const img of imageInFlight.values()) {
461
+ img.onload = null;
462
+ img.onerror = null;
463
+ img.src = "";
464
+ }
465
+ imageInFlight.clear();
466
+
467
+ for (const img of imageCache.values()) {
468
+ img.src = "";
469
+ }
470
+ imageCache.clear();
471
+ imagePending.clear();
472
+ imageFailCache.clear();
473
+ }
474
+
419
475
  /**
420
476
  * Paint image node
421
477
  */
@@ -426,13 +482,19 @@ function paintImage(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
426
482
  const src = props.src || props[0];
427
483
  if (!src) return;
428
484
 
429
- // TODO: Load and cache images
430
- // For now, just draw a placeholder
431
485
  const x = layout.x;
432
486
  const y = layout.y;
433
487
  const width = layout.width;
434
488
  const height = layout.height;
435
489
 
490
+ const cached = imageCache.get(src);
491
+ if (cached && cached.complete && cached.naturalWidth > 0) {
492
+ // Draw the loaded image
493
+ ctx.drawImage(cached, x, y, width, height);
494
+ return;
495
+ }
496
+
497
+ // Draw placeholder while loading
436
498
  ctx.fillStyle = "#e0e0e0";
437
499
  ctx.fillRect(x, y, width, height);
438
500
 
@@ -440,12 +502,50 @@ function paintImage(ctx: CanvasRenderingContext2D, node: VirtualNode): void {
440
502
  ctx.lineWidth = 1;
441
503
  ctx.strokeRect(x, y, width, height);
442
504
 
443
- // Draw "IMG" text
444
505
  ctx.fillStyle = "#666666";
445
506
  ctx.font = "14px sans-serif";
446
507
  ctx.textAlign = "center";
447
508
  ctx.textBaseline = "middle";
448
- ctx.fillText("IMG", x + width / 2, y + height / 2);
509
+
510
+ // Check if this URL has failed recently
511
+ const failEntry = imageFailCache.get(src);
512
+ if (failEntry) {
513
+ if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
514
+ ctx.fillText("Failed", x + width / 2, y + height / 2);
515
+ return;
516
+ }
517
+ }
518
+
519
+ ctx.fillText("Loading...", x + width / 2, y + height / 2);
520
+
521
+ // Start loading if not already pending
522
+ if (!imagePending.has(src)) {
523
+ imagePending.add(src);
524
+ const canvas = ctx.canvas;
525
+ const img = new Image();
526
+ img.crossOrigin = "anonymous";
527
+ imageInFlight.set(src, img);
528
+ img.onload = () => {
529
+ imageInFlight.delete(src);
530
+ imageCache.set(src, img);
531
+ imagePending.delete(src);
532
+ imageFailCache.delete(src);
533
+ evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
534
+ // Request a repaint so the loaded image is drawn
535
+ canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
536
+ };
537
+ img.onerror = () => {
538
+ imageInFlight.delete(src);
539
+ imagePending.delete(src);
540
+ purgeExpiredFailures();
541
+ imageFailCache.set(src, {
542
+ failedAt: Date.now(),
543
+ attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1,
544
+ });
545
+ evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
546
+ };
547
+ img.src = src;
548
+ }
449
549
  }
450
550
 
451
551
  /**
@@ -246,10 +246,14 @@ function createEventHandler(
246
246
  ? options.extractPayload(event, element)
247
247
  : extractEventData(event, element);
248
248
 
249
- // Dispatch to engine
249
+ // Dispatch to engine, catching any async rejections
250
250
  const engine = getEngine(element);
251
251
  if (engine) {
252
- engine.dispatchAction(actionName, payload);
252
+ try {
253
+ engine.dispatchAction(actionName, payload);
254
+ } catch (err) {
255
+ log.error(`Error dispatching action "${actionName}":`, err);
256
+ }
253
257
  }
254
258
  };
255
259
 
@@ -558,6 +562,11 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
558
562
  disposableListener(element, "input", listener, { passive: true })
559
563
  );
560
564
  } else {
565
+ log.warn(
566
+ `.bind() is not supported on element type "${element.dataset?.hypenType || element.tagName}". ` +
567
+ `Supported types: input, textarea, checkbox, switch, select.`
568
+ );
569
+ unregisterEvent(element, eventKey);
561
570
  return;
562
571
  }
563
572
 
@@ -8,14 +8,14 @@ export const textareaHandler: ComponentHandler = {
8
8
  create(): HTMLElement {
9
9
  const el = document.createElement("textarea");
10
10
  el.dataset.hypenType = "textarea";
11
- return el as any as HTMLElement;
11
+ return el as HTMLElement;
12
12
  },
13
13
 
14
14
  applyProps(el: HTMLElement, props: Record<string, any>): void {
15
15
  const textarea = el as HTMLTextAreaElement;
16
16
 
17
- // Value property
18
- const value = props["0"] || props.value;
17
+ // Value property - check named prop first, then positional (consistent with Input)
18
+ const value = props.value ?? props["0"];
19
19
  if (value !== undefined) {
20
20
  textarea.value = String(value);
21
21
  }
package/src/dom/events.ts CHANGED
@@ -60,13 +60,20 @@ export class EventManager {
60
60
  }
61
61
  };
62
62
 
63
- // Store listener reference
63
+ // Store listener reference using `domEventName` so `detach()` uses the correct name
64
64
  let elementBindings = this.bindings.get(elementId);
65
65
  if (!elementBindings) {
66
66
  elementBindings = new Map();
67
67
  this.bindings.set(elementId, elementBindings);
68
68
  }
69
- elementBindings.set(eventName, listener);
69
+
70
+ // Remove any previously attached listener for this event before overwriting,
71
+ // otherwise the old DOM listener leaks and fires alongside the new one.
72
+ const existingListener = elementBindings.get(domEventName);
73
+ if (existingListener) {
74
+ element.removeEventListener(domEventName, existingListener);
75
+ }
76
+ elementBindings.set(domEventName, listener);
70
77
 
71
78
  // Attach to DOM
72
79
  element.addEventListener(domEventName, listener);
@@ -77,26 +84,21 @@ export class EventManager {
77
84
  dataset: element.dataset,
78
85
  textContent: element.textContent?.substring(0, 50)
79
86
  });
80
-
81
- // Test: Add a simple click listener to verify DOM events work
82
- if (domEventName === "click") {
83
- element.addEventListener("click", (e) => {
84
- log.debug(`Raw DOM click detected on ${element.tagName}`, e);
85
- });
86
- }
87
87
  }
88
88
 
89
89
  /**
90
90
  * Detach an event listener from an element
91
91
  */
92
92
  detach(elementId: string, element: HTMLElement, eventName: string): void {
93
+ // Convert framework event name to DOM event name, matching `attach()`
94
+ const domEventName = eventName === "onClick" ? "click" : eventName;
93
95
  const elementBindings = this.bindings.get(elementId);
94
96
  if (!elementBindings) return;
95
97
 
96
- const listener = elementBindings.get(eventName);
98
+ const listener = elementBindings.get(domEventName);
97
99
  if (listener) {
98
- element.removeEventListener(eventName, listener);
99
- elementBindings.delete(eventName);
100
+ element.removeEventListener(domEventName, listener);
101
+ elementBindings.delete(domEventName);
100
102
  }
101
103
 
102
104
  if (elementBindings.size === 0) {
@@ -15,7 +15,7 @@ import { ComponentRegistry } from "./components/index.js";
15
15
  import { ApplicatorRegistry } from "./applicators/index.js";
16
16
  import { canvasHandler, canvasApplicators } from "./canvas/index.js";
17
17
  import { RerenderTracker, type DebugConfig, defaultDebugConfig } from "./debug.js";
18
- import { setEngine } from "./element-data.js";
18
+ import { setEngine, disposeHypenElement } from "./element-data.js";
19
19
 
20
20
  // Interface for the engine that renderer needs
21
21
  interface IEngine {
@@ -311,6 +311,9 @@ export class DOMRenderer {
311
311
  const element = this.nodes.get(id);
312
312
  if (!element) return;
313
313
 
314
+ // Dispose event listeners and other resources before removing from DOM
315
+ disposeHypenElement(element);
316
+
314
317
  if (element.parentNode) {
315
318
  element.parentNode.removeChild(element);
316
319
  }
@@ -333,6 +336,10 @@ export class DOMRenderer {
333
336
  * Clear all nodes
334
337
  */
335
338
  clear(): void {
339
+ // Dispose all element resources before clearing
340
+ for (const element of this.nodes.values()) {
341
+ disposeHypenElement(element);
342
+ }
336
343
  this.container.innerHTML = "";
337
344
  this.nodes.clear();
338
345
  this.rootId = null;