@hypen-space/web 0.4.32 → 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
@@ -2,31 +2,185 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __moduleCache = /* @__PURE__ */ new WeakMap;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
6
8
  var __toCommonJS = (from) => {
7
- var entry = __moduleCache.get(from), desc;
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
8
10
  if (entry)
9
11
  return entry;
10
12
  entry = __defProp({}, "__esModule", { value: true });
11
- if (from && typeof from === "object" || typeof from === "function")
12
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
- get: () => from[key],
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- }));
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
16
21
  __moduleCache.set(from, entry);
17
22
  return entry;
18
23
  };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
19
29
  var __export = (target, all) => {
20
30
  for (var name in all)
21
31
  __defProp(target, name, {
22
32
  get: all[name],
23
33
  enumerable: true,
24
34
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
35
+ set: __exportSetter.bind(all, name)
26
36
  });
27
37
  };
28
38
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
39
 
40
+ // src/canvas/accessibility.ts
41
+ class AccessibilityLayer {
42
+ shadowRoot;
43
+ nodeMap = new Map;
44
+ enabled;
45
+ constructor(container, enabled = true) {
46
+ this.enabled = enabled && typeof document !== "undefined";
47
+ if (this.enabled && typeof document !== "undefined") {
48
+ this.shadowRoot = document.createElement("div");
49
+ this.shadowRoot.setAttribute("role", "application");
50
+ this.shadowRoot.setAttribute("aria-label", "Hypen Canvas Application");
51
+ this.shadowRoot.style.position = "absolute";
52
+ this.shadowRoot.style.left = "-9999px";
53
+ this.shadowRoot.style.width = "1px";
54
+ this.shadowRoot.style.height = "1px";
55
+ this.shadowRoot.style.overflow = "hidden";
56
+ if (container) {
57
+ container.appendChild(this.shadowRoot);
58
+ }
59
+ } else {
60
+ this.shadowRoot = {};
61
+ }
62
+ }
63
+ syncTree(root) {
64
+ if (!this.enabled)
65
+ return;
66
+ this.shadowRoot.innerHTML = "";
67
+ this.nodeMap.clear();
68
+ const shadowNode = this.createShadowNode(root);
69
+ if (shadowNode) {
70
+ this.shadowRoot.appendChild(shadowNode);
71
+ }
72
+ }
73
+ createShadowNode(node) {
74
+ if (!node.visible)
75
+ return null;
76
+ let element;
77
+ switch (node.type.toLowerCase()) {
78
+ case "button":
79
+ element = document.createElement("button");
80
+ element.textContent = this.getNodeText(node);
81
+ if (node.props.onclick) {
82
+ element.setAttribute("aria-label", "Clickable button");
83
+ }
84
+ break;
85
+ case "input":
86
+ element = document.createElement("input");
87
+ element.type = node.props.type || "text";
88
+ element.value = node.props.value || "";
89
+ if (node.props.placeholder) {
90
+ element.placeholder = node.props.placeholder;
91
+ }
92
+ break;
93
+ case "textarea":
94
+ element = document.createElement("textarea");
95
+ element.value = node.props.value || "";
96
+ if (node.props.placeholder) {
97
+ element.placeholder = node.props.placeholder;
98
+ }
99
+ break;
100
+ case "image":
101
+ element = document.createElement("img");
102
+ element.src = node.props.src || "";
103
+ element.alt = node.props.alt || "Image";
104
+ break;
105
+ case "text":
106
+ element = document.createElement("span");
107
+ element.textContent = String(node.props[0] || node.props.text || "");
108
+ break;
109
+ case "column":
110
+ case "row":
111
+ case "container":
112
+ case "box":
113
+ element = document.createElement("div");
114
+ element.setAttribute("role", node.type === "column" ? "group" : "group");
115
+ break;
116
+ default:
117
+ element = document.createElement("div");
118
+ }
119
+ element.setAttribute("data-hypen-id", node.id);
120
+ if (node.props["aria-label"]) {
121
+ element.setAttribute("aria-label", node.props["aria-label"]);
122
+ }
123
+ if (node.focusable) {
124
+ element.tabIndex = 0;
125
+ }
126
+ this.nodeMap.set(node.id, element);
127
+ for (const child of node.children) {
128
+ const childElement = this.createShadowNode(child);
129
+ if (childElement) {
130
+ element.appendChild(childElement);
131
+ }
132
+ }
133
+ return element;
134
+ }
135
+ getNodeText(node) {
136
+ if (node.type === "text") {
137
+ return String(node.props[0] || node.props.text || "");
138
+ }
139
+ let text = "";
140
+ for (const child of node.children) {
141
+ text += this.getNodeText(child);
142
+ }
143
+ return text;
144
+ }
145
+ focusNode(nodeId) {
146
+ if (!this.enabled)
147
+ return;
148
+ const element = this.nodeMap.get(nodeId);
149
+ if (element) {
150
+ element.focus();
151
+ }
152
+ }
153
+ updateNode(node) {
154
+ if (!this.enabled)
155
+ return;
156
+ const element = this.nodeMap.get(node.id);
157
+ if (!element)
158
+ return;
159
+ if (node.type === "text") {
160
+ element.textContent = String(node.props[0] || node.props.text || "");
161
+ }
162
+ if (node.type === "input" || node.type === "textarea") {
163
+ element.value = node.props.value || "";
164
+ }
165
+ element.style.display = node.visible ? "" : "none";
166
+ }
167
+ getElement(nodeId) {
168
+ return this.nodeMap.get(nodeId);
169
+ }
170
+ setEnabled(enabled) {
171
+ this.enabled = enabled;
172
+ if (!enabled && this.shadowRoot.parentElement) {
173
+ this.shadowRoot.remove();
174
+ }
175
+ }
176
+ destroy() {
177
+ if (this.shadowRoot.parentElement) {
178
+ this.shadowRoot.remove();
179
+ }
180
+ this.nodeMap.clear();
181
+ }
182
+ }
183
+
30
184
  // src/canvas/utils.ts
31
185
  function parseSpacing(value) {
32
186
  if (typeof value === "number") {
@@ -256,302 +410,136 @@ async function loadFont(fontFamily, fontWeight = "normal") {
256
410
  }
257
411
  }
258
412
 
259
- // src/canvas/layout.ts
260
- function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
261
- const props = node.props;
262
- let margin = parseSpacing(props.margin || 0);
263
- if (props.marginTop !== undefined)
264
- margin.top = parseFloat(props.marginTop) || 0;
265
- if (props.marginRight !== undefined)
266
- margin.right = parseFloat(props.marginRight) || 0;
267
- if (props.marginBottom !== undefined)
268
- margin.bottom = parseFloat(props.marginBottom) || 0;
269
- if (props.marginLeft !== undefined)
270
- margin.left = parseFloat(props.marginLeft) || 0;
271
- let padding = parseSpacing(props.padding || 0);
272
- if (props.paddingTop !== undefined)
273
- padding.top = parseFloat(props.paddingTop) || 0;
274
- if (props.paddingRight !== undefined)
275
- padding.right = parseFloat(props.paddingRight) || 0;
276
- if (props.paddingBottom !== undefined)
277
- padding.bottom = parseFloat(props.paddingBottom) || 0;
278
- if (props.paddingLeft !== undefined)
279
- padding.left = parseFloat(props.paddingLeft) || 0;
280
- const borderWidth = parseFloat(props.borderWidth) || 0;
281
- const borderColor = props.borderColor || "transparent";
282
- const borderRadius = parseFloat(props.borderRadius) || 0;
283
- const availableAfterMargin = {
284
- width: availableWidth - margin.left - margin.right,
285
- height: availableHeight - margin.top - margin.bottom
286
- };
287
- let width = parseSize(props.width);
288
- let height = parseSize(props.height);
289
- const type = node.type.toLowerCase();
290
- if (type === "app") {
291
- if (width === null)
292
- width = availableAfterMargin.width;
293
- if (height === null)
294
- height = availableAfterMargin.height;
295
- } else if (type === "spacer") {
296
- if (width === null)
297
- width = availableAfterMargin.width;
298
- if (height === null)
299
- height = availableAfterMargin.height;
300
- } else if (type === "divider" || type === "separator") {
301
- const orientation = props.orientation || "horizontal";
302
- const thickness = parseFloat(props.thickness) || 1;
303
- if (orientation === "vertical") {
304
- if (width === null)
305
- width = thickness;
306
- if (height === null)
307
- height = availableAfterMargin.height;
308
- } else {
309
- if (width === null)
310
- width = availableAfterMargin.width;
311
- if (height === null)
312
- height = thickness;
313
- }
314
- } else if (type === "checkbox" || type === "radio") {
315
- const size = parseFloat(props.size) || 20;
316
- if (width === null)
317
- width = size;
318
- if (height === null)
319
- height = size;
320
- } else if (type === "switch" || type === "toggle") {
321
- if (width === null)
322
- width = 44;
323
- if (height === null)
324
- height = 24;
325
- } else if (type === "slider") {
326
- if (width === null)
327
- width = 200;
328
- if (height === null)
329
- height = 20;
330
- } else if (type === "progress" || type === "progressbar") {
331
- if (width === null)
332
- width = 200;
333
- if (height === null)
334
- height = 8;
335
- } else if (type === "spinner" || type === "loading") {
336
- const size = parseFloat(props.size) || 24;
337
- if (width === null)
338
- width = size;
339
- if (height === null)
340
- height = size;
341
- } else if (type === "badge") {
342
- if (width === null)
343
- width = 20;
344
- if (height === null)
345
- height = 20;
346
- } else if (type === "avatar") {
347
- const size = parseFloat(props.size) || 40;
348
- if (width === null)
349
- width = size;
350
- if (height === null)
351
- height = size;
352
- } else if (type === "icon") {
353
- const size = parseFloat(props.size) || 24;
354
- if (width === null)
355
- width = size;
356
- if (height === null)
357
- height = size;
413
+ // src/canvas/input.ts
414
+ class InputOverlay {
415
+ container;
416
+ overlay = null;
417
+ focusedNode = null;
418
+ onChangeCallback = null;
419
+ bindPath = null;
420
+ engine = null;
421
+ constructor(container) {
422
+ this.container = container || {};
358
423
  }
359
- if (node.type === "text" && node.props[0]) {
360
- const text = String(node.props[0] || "");
361
- const fontSize = parseFloat(props.fontSize) || 16;
362
- const fontWeight = props.fontWeight || "normal";
363
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
364
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
365
- const maxWidth2 = width || availableAfterMargin.width - padding.left - padding.right;
366
- const metrics = measureText(ctx, text, { fontSize, fontWeight, fontFamily, lineHeight }, maxWidth2);
367
- if (!width)
368
- width = metrics.width + padding.left + padding.right;
369
- if (!height)
370
- height = metrics.height + padding.top + padding.bottom;
424
+ showInput(node, canvasBounds, onChange, engine) {
425
+ if (typeof document === "undefined")
426
+ return;
427
+ this.hideInput();
428
+ this.bindPath = node.props.bind || null;
429
+ this.engine = engine || null;
430
+ const bounds = getAbsoluteBounds(node);
431
+ if (!bounds)
432
+ return;
433
+ const isMultiline = node.type === "textarea";
434
+ this.overlay = isMultiline ? document.createElement("textarea") : document.createElement("input");
435
+ this.styleOverlay(node, bounds, canvasBounds);
436
+ const value = node.props.value || "";
437
+ this.overlay.value = value;
438
+ this.onChangeCallback = onChange;
439
+ this.overlay.addEventListener("input", this.onInput.bind(this));
440
+ this.overlay.addEventListener("blur", this.onBlur.bind(this));
441
+ this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
442
+ this.container.appendChild(this.overlay);
443
+ this.overlay.focus();
444
+ this.focusedNode = node;
371
445
  }
372
- if (width === null)
373
- width = availableAfterMargin.width;
374
- if (height === null)
375
- height = availableAfterMargin.height;
376
- const minWidth = parseSize(props.minWidth);
377
- const maxWidth = parseSize(props.maxWidth);
378
- const minHeight = parseSize(props.minHeight);
379
- const maxHeight = parseSize(props.maxHeight);
380
- if (minWidth !== null)
381
- width = Math.max(width, minWidth);
382
- if (maxWidth !== null)
383
- width = Math.min(width, maxWidth);
384
- if (minHeight !== null)
385
- height = Math.max(height, minHeight);
386
- if (maxHeight !== null)
387
- height = Math.min(height, maxHeight);
388
- const layout = {
389
- x: x + margin.left,
390
- y: y + margin.top,
391
- width,
392
- height,
393
- margin,
394
- padding,
395
- border: {
396
- width: borderWidth,
397
- color: borderColor,
398
- radius: borderRadius
399
- },
400
- contentX: padding.left + borderWidth,
401
- contentY: padding.top + borderWidth,
402
- contentWidth: width - padding.left - padding.right - borderWidth * 2,
403
- contentHeight: height - padding.top - padding.bottom - borderWidth * 2
404
- };
405
- node.layout = layout;
406
- if (node.children.length > 0) {
407
- layoutChildren(ctx, node);
446
+ hideInput() {
447
+ if (this.overlay) {
448
+ this.overlay.remove();
449
+ this.overlay = null;
450
+ }
451
+ this.focusedNode = null;
452
+ this.onChangeCallback = null;
453
+ this.bindPath = null;
454
+ this.engine = null;
408
455
  }
409
- }
410
- function layoutChildren(ctx, parent) {
411
- const layout = parent.layout;
412
- const props = parent.props;
413
- if (parent.type.toLowerCase() === "stack") {
414
- layoutStackChildren(ctx, parent);
415
- return;
456
+ updatePosition(node, canvasBounds) {
457
+ if (!this.overlay || node !== this.focusedNode)
458
+ return;
459
+ const bounds = getAbsoluteBounds(node);
460
+ if (!bounds)
461
+ return;
462
+ this.positionOverlay(bounds, canvasBounds);
416
463
  }
417
- const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
418
- const isColumn = flexDirection === "column";
419
- const justifyContent = isColumn ? props.verticalAlignment || "flex-start" : props.horizontalAlignment || "flex-start";
420
- const alignItems = isColumn ? props.horizontalAlignment || "flex-start" : props.verticalAlignment || "flex-start";
421
- const gap = parseFloat(props.gap) || 0;
422
- const availableWidth = layout.contentWidth;
423
- const availableHeight = layout.contentHeight;
424
- const childInfo = [];
425
- let totalMainSize = 0;
426
- let totalFlexGrow = 0;
427
- let totalFlexShrink = 0;
428
- for (const child of parent.children) {
429
- const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
430
- const flexShrink = parseFloat(child.props.flexShrink) || 1;
431
- const flexBasis = parseSize(child.props.flexBasis);
432
- computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
433
- const childLayout = child.layout;
434
- let mainSize = isColumn ? childLayout.height : childLayout.width;
435
- if (flexBasis !== null) {
436
- mainSize = flexBasis;
437
- if (isColumn) {
438
- childLayout.height = flexBasis;
439
- } else {
440
- childLayout.width = flexBasis;
441
- }
464
+ styleOverlay(node, bounds, canvasBounds) {
465
+ if (!this.overlay)
466
+ return;
467
+ const props = node.props;
468
+ this.positionOverlay(bounds, canvasBounds);
469
+ const fontSize = parseFloat(props.fontSize) || 16;
470
+ const fontWeight = props.fontWeight || "normal";
471
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
472
+ const color = props.color || "#000000";
473
+ Object.assign(this.overlay.style, {
474
+ fontSize: `${fontSize}px`,
475
+ fontWeight,
476
+ fontFamily,
477
+ color,
478
+ border: "none",
479
+ outline: "2px solid #007bff",
480
+ backgroundColor: props.backgroundColor || "#ffffff",
481
+ padding: `${props.padding || 8}px`,
482
+ borderRadius: `${props.borderRadius || 4}px`,
483
+ boxSizing: "border-box",
484
+ resize: "none"
485
+ });
486
+ if (props.placeholder) {
487
+ this.overlay.placeholder = props.placeholder;
442
488
  }
443
- childInfo.push({
444
- width: childLayout.width,
445
- height: childLayout.height,
446
- flexGrow,
447
- flexShrink,
448
- flexBasis
489
+ if (this.overlay instanceof HTMLInputElement && props.type) {
490
+ this.overlay.type = props.type;
491
+ }
492
+ }
493
+ positionOverlay(bounds, canvasBounds) {
494
+ if (!this.overlay)
495
+ return;
496
+ const canvas = this.container.querySelector("canvas");
497
+ const scaleX = canvasBounds.width / canvas.width;
498
+ const scaleY = canvasBounds.height / canvas.height;
499
+ const left = bounds.x * scaleX;
500
+ const top = bounds.y * scaleY;
501
+ const width = bounds.width * scaleX;
502
+ const height = bounds.height * scaleY;
503
+ Object.assign(this.overlay.style, {
504
+ position: "absolute",
505
+ left: `${left}px`,
506
+ top: `${top}px`,
507
+ width: `${width}px`,
508
+ height: `${height}px`
449
509
  });
450
- totalMainSize += mainSize;
451
- totalFlexGrow += flexGrow;
452
- totalFlexShrink += flexShrink;
453
510
  }
454
- const totalGap = gap * (parent.children.length - 1);
455
- totalMainSize += totalGap;
456
- const availableMain = isColumn ? availableHeight : availableWidth;
457
- let remainingSpace = availableMain - totalMainSize;
458
- if (remainingSpace > 0 && totalFlexGrow > 0) {
459
- const spacePerFlex = remainingSpace / totalFlexGrow;
460
- for (let i = 0;i < parent.children.length; i++) {
461
- const info = childInfo[i];
462
- if (info.flexGrow > 0) {
463
- const extraSpace = spacePerFlex * info.flexGrow;
464
- if (isColumn) {
465
- info.height += extraSpace;
466
- } else {
467
- info.width += extraSpace;
468
- }
469
- totalMainSize += extraSpace;
470
- }
511
+ onInput(e) {
512
+ if (!this.overlay || !this.onChangeCallback)
513
+ return;
514
+ const value = this.overlay.value;
515
+ this.onChangeCallback(value);
516
+ if (this.bindPath && this.engine) {
517
+ this.engine.dispatchAction("__hypen_bind", {
518
+ path: this.bindPath,
519
+ value
520
+ });
471
521
  }
472
- remainingSpace = 0;
473
522
  }
474
- if (remainingSpace < 0 && totalFlexShrink > 0) {
475
- const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
476
- for (let i = 0;i < parent.children.length; i++) {
477
- const info = childInfo[i];
478
- if (info.flexShrink > 0) {
479
- const shrinkSpace = Math.min(shrinkPerFlex * info.flexShrink, isColumn ? info.height : info.width);
480
- if (isColumn) {
481
- info.height = Math.max(0, info.height - shrinkSpace);
482
- } else {
483
- info.width = Math.max(0, info.width - shrinkSpace);
484
- }
485
- totalMainSize -= shrinkSpace;
486
- }
523
+ onBlur() {
524
+ this.hideInput();
525
+ }
526
+ onKeyDown(e) {
527
+ if (!this.overlay)
528
+ return;
529
+ if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
530
+ e.preventDefault();
531
+ this.overlay.blur();
532
+ }
533
+ if (e.key === "Escape") {
534
+ e.preventDefault();
535
+ this.overlay.blur();
487
536
  }
488
- remainingSpace = availableMain - totalMainSize;
489
537
  }
490
- let mainStart = 0;
491
- let spacing = 0;
492
- if (justifyContent === "center") {
493
- mainStart = Math.max(0, remainingSpace / 2);
494
- } else if (justifyContent === "flex-end") {
495
- mainStart = Math.max(0, remainingSpace);
496
- } else if (justifyContent === "space-between") {
497
- spacing = remainingSpace / Math.max(1, parent.children.length - 1);
498
- } else if (justifyContent === "space-around") {
499
- spacing = remainingSpace / parent.children.length;
500
- mainStart = spacing / 2;
538
+ isShown() {
539
+ return this.overlay !== null;
501
540
  }
502
- let currentMain = mainStart;
503
- for (let i = 0;i < parent.children.length; i++) {
504
- const child = parent.children[i];
505
- const childLayout = child.layout;
506
- const info = childInfo[i];
507
- childLayout.width = info.width;
508
- childLayout.height = info.height;
509
- let crossStart = 0;
510
- const availableCross = isColumn ? availableWidth : availableHeight;
511
- const childCross = isColumn ? info.width : info.height;
512
- if (alignItems === "center") {
513
- crossStart = (availableCross - childCross) / 2;
514
- } else if (alignItems === "flex-end") {
515
- crossStart = availableCross - childCross;
516
- }
517
- if (isColumn) {
518
- childLayout.x = layout.x + layout.contentX + crossStart;
519
- childLayout.y = layout.y + layout.contentY + currentMain;
520
- currentMain += info.height + gap;
521
- } else {
522
- childLayout.x = layout.x + layout.contentX + currentMain;
523
- childLayout.y = layout.y + layout.contentY + crossStart;
524
- currentMain += info.width + gap;
525
- }
526
- if (justifyContent === "space-between" || justifyContent === "space-around") {
527
- currentMain += spacing;
528
- }
529
- }
530
- }
531
- function layoutStackChildren(ctx, parent) {
532
- const layout = parent.layout;
533
- const props = parent.props;
534
- const horizontalAlignment = props.horizontalAlignment || "flex-start";
535
- const verticalAlignment = props.verticalAlignment || "flex-start";
536
- const availableWidth = layout.contentWidth;
537
- const availableHeight = layout.contentHeight;
538
- for (const child of parent.children) {
539
- computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
540
- const childLayout = child.layout;
541
- let x = 0;
542
- let y = 0;
543
- if (horizontalAlignment === "center") {
544
- x = (availableWidth - childLayout.width) / 2;
545
- } else if (horizontalAlignment === "flex-end") {
546
- x = availableWidth - childLayout.width;
547
- }
548
- if (verticalAlignment === "center") {
549
- y = (availableHeight - childLayout.height) / 2;
550
- } else if (verticalAlignment === "flex-end") {
551
- y = availableHeight - childLayout.height;
552
- }
553
- childLayout.x = layout.x + layout.contentX + x;
554
- childLayout.y = layout.y + layout.contentY + y;
541
+ getFocusedNode() {
542
+ return this.focusedNode;
555
543
  }
556
544
  }
557
545
 
@@ -850,6 +838,46 @@ function paintInput(ctx, node) {
850
838
  });
851
839
  }
852
840
  }
841
+ var MAX_IMAGE_CACHE_SIZE = 100;
842
+ var MAX_FAIL_CACHE_SIZE = 200;
843
+ var imageCache = new Map;
844
+ var imagePending = new Set;
845
+ var imageInFlight = new Map;
846
+ var imageFailCache = new Map;
847
+ var IMAGE_FAIL_COOLDOWN_MS = 30000;
848
+ var IMAGE_MAX_ATTEMPTS = 3;
849
+ function evictMap(map, max) {
850
+ while (map.size > max) {
851
+ const oldest = map.keys().next().value;
852
+ if (oldest === undefined)
853
+ break;
854
+ map.delete(oldest);
855
+ }
856
+ }
857
+ function purgeExpiredFailures() {
858
+ const now = Date.now();
859
+ for (const [key, entry] of imageFailCache) {
860
+ if (entry.attempts >= IMAGE_MAX_ATTEMPTS)
861
+ continue;
862
+ if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
863
+ imageFailCache.delete(key);
864
+ }
865
+ }
866
+ }
867
+ function clearImageCache() {
868
+ for (const img of imageInFlight.values()) {
869
+ img.onload = null;
870
+ img.onerror = null;
871
+ img.src = "";
872
+ }
873
+ imageInFlight.clear();
874
+ for (const img of imageCache.values()) {
875
+ img.src = "";
876
+ }
877
+ imageCache.clear();
878
+ imagePending.clear();
879
+ imageFailCache.clear();
880
+ }
853
881
  function paintImage(ctx, node) {
854
882
  const layout = node.layout;
855
883
  const props = node.props;
@@ -860,6 +888,11 @@ function paintImage(ctx, node) {
860
888
  const y = layout.y;
861
889
  const width = layout.width;
862
890
  const height = layout.height;
891
+ const cached = imageCache.get(src);
892
+ if (cached && cached.complete && cached.naturalWidth > 0) {
893
+ ctx.drawImage(cached, x, y, width, height);
894
+ return;
895
+ }
863
896
  ctx.fillStyle = "#e0e0e0";
864
897
  ctx.fillRect(x, y, width, height);
865
898
  ctx.strokeStyle = "#999999";
@@ -869,7 +902,40 @@ function paintImage(ctx, node) {
869
902
  ctx.font = "14px sans-serif";
870
903
  ctx.textAlign = "center";
871
904
  ctx.textBaseline = "middle";
872
- ctx.fillText("IMG", x + width / 2, y + height / 2);
905
+ const failEntry = imageFailCache.get(src);
906
+ if (failEntry) {
907
+ if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
908
+ ctx.fillText("Failed", x + width / 2, y + height / 2);
909
+ return;
910
+ }
911
+ }
912
+ ctx.fillText("Loading...", x + width / 2, y + height / 2);
913
+ if (!imagePending.has(src)) {
914
+ imagePending.add(src);
915
+ const canvas = ctx.canvas;
916
+ const img = new Image;
917
+ img.crossOrigin = "anonymous";
918
+ imageInFlight.set(src, img);
919
+ img.onload = () => {
920
+ imageInFlight.delete(src);
921
+ imageCache.set(src, img);
922
+ imagePending.delete(src);
923
+ imageFailCache.delete(src);
924
+ evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
925
+ canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
926
+ };
927
+ img.onerror = () => {
928
+ imageInFlight.delete(src);
929
+ imagePending.delete(src);
930
+ purgeExpiredFailures();
931
+ imageFailCache.set(src, {
932
+ failedAt: Date.now(),
933
+ attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1
934
+ });
935
+ evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
936
+ };
937
+ img.src = src;
938
+ }
873
939
  }
874
940
  function drawRoundedRect(ctx, x, y, width, height, radius) {
875
941
  if (radius <= 0) {
@@ -1421,466 +1487,514 @@ function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
1421
1487
  ctx.closePath();
1422
1488
  }
1423
1489
 
1424
- // src/canvas/events.ts
1425
- class CanvasEventManager {
1426
- canvas;
1427
- engine;
1428
- rootNode = null;
1429
- hoveredNode = null;
1430
- focusedNode = null;
1431
- mouseDownNode = null;
1432
- constructor(canvas, engine) {
1433
- this.canvas = canvas;
1434
- this.engine = engine;
1435
- this.setupEventListeners();
1436
- }
1437
- setRootNode(node) {
1438
- this.rootNode = node;
1439
- }
1440
- setupEventListeners() {
1441
- this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
1442
- this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
1443
- this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
1444
- this.canvas.addEventListener("click", this.onClick.bind(this));
1445
- this.canvas.addEventListener("dblclick", this.onDoubleClick.bind(this));
1446
- this.canvas.addEventListener("keydown", this.onKeyDown.bind(this));
1447
- this.canvas.addEventListener("keyup", this.onKeyUp.bind(this));
1448
- }
1449
- getCanvasCoordinates(e) {
1450
- const rect = this.canvas.getBoundingClientRect();
1451
- const scaleX = this.canvas.width / rect.width;
1452
- const scaleY = this.canvas.height / rect.height;
1453
- return {
1454
- x: (e.clientX - rect.left) * scaleX,
1455
- y: (e.clientY - rect.top) * scaleY
1456
- };
1457
- }
1458
- hitTest(point) {
1459
- if (!this.rootNode)
1460
- return null;
1461
- return this.hitTestNode(this.rootNode, point);
1462
- }
1463
- hitTestNode(node, point) {
1464
- if (!node.visible || !node.layout)
1465
- return null;
1466
- const bounds = getAbsoluteBounds(node);
1467
- if (!bounds)
1468
- return null;
1469
- for (let i = node.children.length - 1;i >= 0; i--) {
1470
- const child = node.children[i];
1471
- const hit = this.hitTestNode(child, point);
1472
- if (hit)
1473
- return hit;
1474
- }
1475
- const radius = node.layout.border.radius;
1476
- if (isPointInRoundedRect(point, bounds, radius)) {
1477
- return node;
1478
- }
1479
- return null;
1480
- }
1481
- onMouseMove(e) {
1482
- const point = this.getCanvasCoordinates(e);
1483
- const node = this.hitTest(point);
1484
- if (node !== this.hoveredNode) {
1485
- if (this.hoveredNode) {
1486
- this.hoveredNode.hovered = false;
1487
- this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
1488
- }
1489
- this.hoveredNode = node;
1490
- if (node) {
1491
- node.hovered = true;
1492
- this.dispatchNodeEvent(node, "mouseenter", {});
1493
- }
1494
- this.updateCursor(node);
1495
- this.requestRedraw();
1496
- }
1497
- }
1498
- onMouseDown(e) {
1499
- const point = this.getCanvasCoordinates(e);
1500
- const node = this.hitTest(point);
1501
- this.mouseDownNode = node;
1502
- if (node && node.clickable) {
1503
- this.dispatchNodeEvent(node, "mousedown", {
1504
- button: e.button,
1505
- clientX: e.clientX,
1506
- clientY: e.clientY
1507
- });
1508
- }
1509
- if (node && node.focusable) {
1510
- this.setFocus(node);
1490
+ // src/canvas/layout.ts
1491
+ function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
1492
+ const props = node.props;
1493
+ let margin = parseSpacing(props.margin || 0);
1494
+ if (props.marginTop !== undefined)
1495
+ margin.top = parseFloat(props.marginTop) || 0;
1496
+ if (props.marginRight !== undefined)
1497
+ margin.right = parseFloat(props.marginRight) || 0;
1498
+ if (props.marginBottom !== undefined)
1499
+ margin.bottom = parseFloat(props.marginBottom) || 0;
1500
+ if (props.marginLeft !== undefined)
1501
+ margin.left = parseFloat(props.marginLeft) || 0;
1502
+ let padding = parseSpacing(props.padding || 0);
1503
+ if (props.paddingTop !== undefined)
1504
+ padding.top = parseFloat(props.paddingTop) || 0;
1505
+ if (props.paddingRight !== undefined)
1506
+ padding.right = parseFloat(props.paddingRight) || 0;
1507
+ if (props.paddingBottom !== undefined)
1508
+ padding.bottom = parseFloat(props.paddingBottom) || 0;
1509
+ if (props.paddingLeft !== undefined)
1510
+ padding.left = parseFloat(props.paddingLeft) || 0;
1511
+ const borderWidth = parseFloat(props.borderWidth) || 0;
1512
+ const borderColor = props.borderColor || "transparent";
1513
+ const borderRadius = parseFloat(props.borderRadius) || 0;
1514
+ const availableAfterMargin = {
1515
+ width: availableWidth - margin.left - margin.right,
1516
+ height: availableHeight - margin.top - margin.bottom
1517
+ };
1518
+ let width = parseSize(props.width);
1519
+ let height = parseSize(props.height);
1520
+ const type = node.type.toLowerCase();
1521
+ if (type === "app") {
1522
+ if (width === null)
1523
+ width = availableAfterMargin.width;
1524
+ if (height === null)
1525
+ height = availableAfterMargin.height;
1526
+ } else if (type === "spacer") {
1527
+ if (width === null)
1528
+ width = availableAfterMargin.width;
1529
+ if (height === null)
1530
+ height = availableAfterMargin.height;
1531
+ } else if (type === "divider" || type === "separator") {
1532
+ const orientation = props.orientation || "horizontal";
1533
+ const thickness = parseFloat(props.thickness) || 1;
1534
+ if (orientation === "vertical") {
1535
+ if (width === null)
1536
+ width = thickness;
1537
+ if (height === null)
1538
+ height = availableAfterMargin.height;
1511
1539
  } else {
1512
- this.setFocus(null);
1540
+ if (width === null)
1541
+ width = availableAfterMargin.width;
1542
+ if (height === null)
1543
+ height = thickness;
1513
1544
  }
1545
+ } else if (type === "checkbox" || type === "radio") {
1546
+ const size = parseFloat(props.size) || 20;
1547
+ if (width === null)
1548
+ width = size;
1549
+ if (height === null)
1550
+ height = size;
1551
+ } else if (type === "switch" || type === "toggle") {
1552
+ if (width === null)
1553
+ width = 44;
1554
+ if (height === null)
1555
+ height = 24;
1556
+ } else if (type === "slider") {
1557
+ if (width === null)
1558
+ width = 200;
1559
+ if (height === null)
1560
+ height = 20;
1561
+ } else if (type === "progress" || type === "progressbar") {
1562
+ if (width === null)
1563
+ width = 200;
1564
+ if (height === null)
1565
+ height = 8;
1566
+ } else if (type === "spinner" || type === "loading") {
1567
+ const size = parseFloat(props.size) || 24;
1568
+ if (width === null)
1569
+ width = size;
1570
+ if (height === null)
1571
+ height = size;
1572
+ } else if (type === "badge") {
1573
+ if (width === null)
1574
+ width = 20;
1575
+ if (height === null)
1576
+ height = 20;
1577
+ } else if (type === "avatar") {
1578
+ const size = parseFloat(props.size) || 40;
1579
+ if (width === null)
1580
+ width = size;
1581
+ if (height === null)
1582
+ height = size;
1583
+ } else if (type === "icon") {
1584
+ const size = parseFloat(props.size) || 24;
1585
+ if (width === null)
1586
+ width = size;
1587
+ if (height === null)
1588
+ height = size;
1514
1589
  }
1515
- onMouseUp(e) {
1516
- const point = this.getCanvasCoordinates(e);
1517
- const node = this.hitTest(point);
1518
- if (node && node.clickable) {
1519
- this.dispatchNodeEvent(node, "mouseup", {
1520
- button: e.button,
1521
- clientX: e.clientX,
1522
- clientY: e.clientY
1523
- });
1524
- }
1525
- this.mouseDownNode = null;
1590
+ if (node.type === "text" && node.props[0]) {
1591
+ const text = String(node.props[0] || "");
1592
+ const fontSize = parseFloat(props.fontSize) || 16;
1593
+ const fontWeight = props.fontWeight || "normal";
1594
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
1595
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
1596
+ const maxWidth2 = width || availableAfterMargin.width - padding.left - padding.right;
1597
+ const metrics = measureText(ctx, text, { fontSize, fontWeight, fontFamily, lineHeight }, maxWidth2);
1598
+ if (!width)
1599
+ width = metrics.width + padding.left + padding.right;
1600
+ if (!height)
1601
+ height = metrics.height + padding.top + padding.bottom;
1526
1602
  }
1527
- onClick(e) {
1528
- const point = this.getCanvasCoordinates(e);
1529
- const node = this.hitTest(point);
1530
- if (node && node.clickable && node === this.mouseDownNode) {
1531
- this.dispatchNodeEvent(node, "click", {
1532
- button: e.button,
1533
- clientX: e.clientX,
1534
- clientY: e.clientY
1535
- });
1536
- }
1603
+ if (width === null)
1604
+ width = availableAfterMargin.width;
1605
+ if (height === null)
1606
+ height = availableAfterMargin.height;
1607
+ const minWidth = parseSize(props.minWidth);
1608
+ const maxWidth = parseSize(props.maxWidth);
1609
+ const minHeight = parseSize(props.minHeight);
1610
+ const maxHeight = parseSize(props.maxHeight);
1611
+ if (minWidth !== null)
1612
+ width = Math.max(width, minWidth);
1613
+ if (maxWidth !== null)
1614
+ width = Math.min(width, maxWidth);
1615
+ if (minHeight !== null)
1616
+ height = Math.max(height, minHeight);
1617
+ if (maxHeight !== null)
1618
+ height = Math.min(height, maxHeight);
1619
+ const layout = {
1620
+ x: x + margin.left,
1621
+ y: y + margin.top,
1622
+ width,
1623
+ height,
1624
+ margin,
1625
+ padding,
1626
+ border: {
1627
+ width: borderWidth,
1628
+ color: borderColor,
1629
+ radius: borderRadius
1630
+ },
1631
+ contentX: padding.left + borderWidth,
1632
+ contentY: padding.top + borderWidth,
1633
+ contentWidth: width - padding.left - padding.right - borderWidth * 2,
1634
+ contentHeight: height - padding.top - padding.bottom - borderWidth * 2
1635
+ };
1636
+ node.layout = layout;
1637
+ if (node.children.length > 0) {
1638
+ layoutChildren(ctx, node);
1537
1639
  }
1538
- onDoubleClick(e) {
1539
- const point = this.getCanvasCoordinates(e);
1540
- const node = this.hitTest(point);
1541
- if (node && node.clickable) {
1542
- this.dispatchNodeEvent(node, "dblclick", {
1543
- button: e.button,
1544
- clientX: e.clientX,
1545
- clientY: e.clientY
1546
- });
1640
+ }
1641
+ function layoutChildren(ctx, parent) {
1642
+ const layout = parent.layout;
1643
+ const props = parent.props;
1644
+ if (parent.type.toLowerCase() === "stack") {
1645
+ layoutStackChildren(ctx, parent);
1646
+ return;
1647
+ }
1648
+ const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
1649
+ const isColumn = flexDirection === "column";
1650
+ const justifyContent = isColumn ? props.verticalAlignment || "flex-start" : props.horizontalAlignment || "flex-start";
1651
+ const alignItems = isColumn ? props.horizontalAlignment || "flex-start" : props.verticalAlignment || "flex-start";
1652
+ const gap = parseFloat(props.gap) || 0;
1653
+ const availableWidth = layout.contentWidth;
1654
+ const availableHeight = layout.contentHeight;
1655
+ const childInfo = [];
1656
+ let totalMainSize = 0;
1657
+ let totalFlexGrow = 0;
1658
+ let totalFlexShrink = 0;
1659
+ for (const child of parent.children) {
1660
+ const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
1661
+ const flexShrink = parseFloat(child.props.flexShrink) || 1;
1662
+ const flexBasis = parseSize(child.props.flexBasis);
1663
+ computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
1664
+ const childLayout = child.layout;
1665
+ let mainSize = isColumn ? childLayout.height : childLayout.width;
1666
+ if (flexBasis !== null) {
1667
+ mainSize = flexBasis;
1668
+ if (isColumn) {
1669
+ childLayout.height = flexBasis;
1670
+ } else {
1671
+ childLayout.width = flexBasis;
1672
+ }
1547
1673
  }
1674
+ childInfo.push({
1675
+ width: childLayout.width,
1676
+ height: childLayout.height,
1677
+ flexGrow,
1678
+ flexShrink,
1679
+ flexBasis
1680
+ });
1681
+ totalMainSize += mainSize;
1682
+ totalFlexGrow += flexGrow;
1683
+ totalFlexShrink += flexShrink;
1548
1684
  }
1549
- onKeyDown(e) {
1550
- if (this.focusedNode) {
1551
- this.dispatchNodeEvent(this.focusedNode, "keydown", {
1552
- key: e.key,
1553
- code: e.code,
1554
- ctrlKey: e.ctrlKey,
1555
- shiftKey: e.shiftKey,
1556
- altKey: e.altKey
1557
- });
1685
+ const totalGap = gap * (parent.children.length - 1);
1686
+ totalMainSize += totalGap;
1687
+ const availableMain = isColumn ? availableHeight : availableWidth;
1688
+ let remainingSpace = availableMain - totalMainSize;
1689
+ if (remainingSpace > 0 && totalFlexGrow > 0) {
1690
+ const spacePerFlex = remainingSpace / totalFlexGrow;
1691
+ for (let i = 0;i < parent.children.length; i++) {
1692
+ const info = childInfo[i];
1693
+ if (info.flexGrow > 0) {
1694
+ const extraSpace = spacePerFlex * info.flexGrow;
1695
+ if (isColumn) {
1696
+ info.height += extraSpace;
1697
+ } else {
1698
+ info.width += extraSpace;
1699
+ }
1700
+ totalMainSize += extraSpace;
1701
+ }
1558
1702
  }
1703
+ remainingSpace = 0;
1559
1704
  }
1560
- onKeyUp(e) {
1561
- if (this.focusedNode) {
1562
- this.dispatchNodeEvent(this.focusedNode, "keyup", {
1563
- key: e.key,
1564
- code: e.code,
1565
- ctrlKey: e.ctrlKey,
1566
- shiftKey: e.shiftKey,
1567
- altKey: e.altKey
1568
- });
1705
+ if (remainingSpace < 0 && totalFlexShrink > 0) {
1706
+ const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
1707
+ for (let i = 0;i < parent.children.length; i++) {
1708
+ const info = childInfo[i];
1709
+ if (info.flexShrink > 0) {
1710
+ const shrinkSpace = Math.min(shrinkPerFlex * info.flexShrink, isColumn ? info.height : info.width);
1711
+ if (isColumn) {
1712
+ info.height = Math.max(0, info.height - shrinkSpace);
1713
+ } else {
1714
+ info.width = Math.max(0, info.width - shrinkSpace);
1715
+ }
1716
+ totalMainSize -= shrinkSpace;
1717
+ }
1569
1718
  }
1719
+ remainingSpace = availableMain - totalMainSize;
1570
1720
  }
1571
- setFocus(node) {
1572
- if (node === this.focusedNode)
1573
- return;
1574
- if (this.focusedNode) {
1575
- this.focusedNode.focused = false;
1576
- this.dispatchNodeEvent(this.focusedNode, "blur", {});
1721
+ let mainStart = 0;
1722
+ let spacing = 0;
1723
+ if (justifyContent === "center") {
1724
+ mainStart = Math.max(0, remainingSpace / 2);
1725
+ } else if (justifyContent === "flex-end") {
1726
+ mainStart = Math.max(0, remainingSpace);
1727
+ } else if (justifyContent === "space-between") {
1728
+ spacing = remainingSpace / Math.max(1, parent.children.length - 1);
1729
+ } else if (justifyContent === "space-around") {
1730
+ spacing = remainingSpace / parent.children.length;
1731
+ mainStart = spacing / 2;
1732
+ }
1733
+ let currentMain = mainStart;
1734
+ for (let i = 0;i < parent.children.length; i++) {
1735
+ const child = parent.children[i];
1736
+ const childLayout = child.layout;
1737
+ const info = childInfo[i];
1738
+ childLayout.width = info.width;
1739
+ childLayout.height = info.height;
1740
+ let crossStart = 0;
1741
+ const availableCross = isColumn ? availableWidth : availableHeight;
1742
+ const childCross = isColumn ? info.width : info.height;
1743
+ if (alignItems === "center") {
1744
+ crossStart = (availableCross - childCross) / 2;
1745
+ } else if (alignItems === "flex-end") {
1746
+ crossStart = availableCross - childCross;
1577
1747
  }
1578
- this.focusedNode = node;
1579
- if (node) {
1580
- node.focused = true;
1581
- this.dispatchNodeEvent(node, "focus", {});
1748
+ if (isColumn) {
1749
+ childLayout.x = layout.x + layout.contentX + crossStart;
1750
+ childLayout.y = layout.y + layout.contentY + currentMain;
1751
+ currentMain += info.height + gap;
1752
+ } else {
1753
+ childLayout.x = layout.x + layout.contentX + currentMain;
1754
+ childLayout.y = layout.y + layout.contentY + crossStart;
1755
+ currentMain += info.width + gap;
1582
1756
  }
1583
- this.requestRedraw();
1584
- }
1585
- updateCursor(node) {
1586
- if (!node) {
1587
- this.canvas.style.cursor = "default";
1588
- return;
1757
+ if (justifyContent === "space-between" || justifyContent === "space-around") {
1758
+ currentMain += spacing;
1589
1759
  }
1590
- const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
1591
- this.canvas.style.cursor = cursor;
1592
1760
  }
1593
- dispatchNodeEvent(node, eventType, data) {
1594
- const actionName = node.props[`on${eventType}`] || node.props[eventType];
1595
- if (actionName && typeof actionName === "string") {
1596
- this.engine.dispatchAction(actionName, {
1597
- type: eventType,
1598
- nodeId: node.id,
1599
- timestamp: Date.now(),
1600
- ...data
1601
- });
1761
+ }
1762
+ function layoutStackChildren(ctx, parent) {
1763
+ const layout = parent.layout;
1764
+ const props = parent.props;
1765
+ const horizontalAlignment = props.horizontalAlignment || "flex-start";
1766
+ const verticalAlignment = props.verticalAlignment || "flex-start";
1767
+ const availableWidth = layout.contentWidth;
1768
+ const availableHeight = layout.contentHeight;
1769
+ for (const child of parent.children) {
1770
+ computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
1771
+ const childLayout = child.layout;
1772
+ let x = 0;
1773
+ let y = 0;
1774
+ if (horizontalAlignment === "center") {
1775
+ x = (availableWidth - childLayout.width) / 2;
1776
+ } else if (horizontalAlignment === "flex-end") {
1777
+ x = availableWidth - childLayout.width;
1602
1778
  }
1779
+ if (verticalAlignment === "center") {
1780
+ y = (availableHeight - childLayout.height) / 2;
1781
+ } else if (verticalAlignment === "flex-end") {
1782
+ y = availableHeight - childLayout.height;
1783
+ }
1784
+ childLayout.x = layout.x + layout.contentX + x;
1785
+ childLayout.y = layout.y + layout.contentY + y;
1603
1786
  }
1604
- requestRedraw() {
1605
- this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
1606
- }
1607
- destroy() {}
1608
1787
  }
1609
1788
 
1610
- // src/canvas/input.ts
1611
- class InputOverlay {
1612
- container;
1613
- overlay = null;
1789
+ // src/canvas/events.ts
1790
+ class CanvasEventManager {
1791
+ canvas;
1792
+ engine;
1793
+ rootNode = null;
1794
+ hoveredNode = null;
1614
1795
  focusedNode = null;
1615
- onChangeCallback = null;
1616
- bindPath = null;
1617
- engine = null;
1618
- constructor(container) {
1619
- this.container = container || {};
1620
- }
1621
- showInput(node, canvasBounds, onChange, engine) {
1622
- if (typeof document === "undefined")
1623
- return;
1624
- this.hideInput();
1625
- this.bindPath = node.props.bind || null;
1626
- this.engine = engine || null;
1627
- const bounds = getAbsoluteBounds(node);
1628
- if (!bounds)
1629
- return;
1630
- const isMultiline = node.type === "textarea";
1631
- this.overlay = isMultiline ? document.createElement("textarea") : document.createElement("input");
1632
- this.styleOverlay(node, bounds, canvasBounds);
1633
- const value = node.props.value || "";
1634
- this.overlay.value = value;
1635
- this.onChangeCallback = onChange;
1636
- this.overlay.addEventListener("input", this.onInput.bind(this));
1637
- this.overlay.addEventListener("blur", this.onBlur.bind(this));
1638
- this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
1639
- this.container.appendChild(this.overlay);
1640
- this.overlay.focus();
1641
- this.focusedNode = node;
1642
- }
1643
- hideInput() {
1644
- if (this.overlay) {
1645
- this.overlay.remove();
1646
- this.overlay = null;
1647
- }
1648
- this.focusedNode = null;
1649
- this.onChangeCallback = null;
1650
- this.bindPath = null;
1651
- this.engine = null;
1652
- }
1653
- updatePosition(node, canvasBounds) {
1654
- if (!this.overlay || node !== this.focusedNode)
1655
- return;
1656
- const bounds = getAbsoluteBounds(node);
1657
- if (!bounds)
1658
- return;
1659
- this.positionOverlay(bounds, canvasBounds);
1660
- }
1661
- styleOverlay(node, bounds, canvasBounds) {
1662
- if (!this.overlay)
1663
- return;
1664
- const props = node.props;
1665
- this.positionOverlay(bounds, canvasBounds);
1666
- const fontSize = parseFloat(props.fontSize) || 16;
1667
- const fontWeight = props.fontWeight || "normal";
1668
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
1669
- const color = props.color || "#000000";
1670
- Object.assign(this.overlay.style, {
1671
- fontSize: `${fontSize}px`,
1672
- fontWeight,
1673
- fontFamily,
1674
- color,
1675
- border: "none",
1676
- outline: "2px solid #007bff",
1677
- backgroundColor: props.backgroundColor || "#ffffff",
1678
- padding: `${props.padding || 8}px`,
1679
- borderRadius: `${props.borderRadius || 4}px`,
1680
- boxSizing: "border-box",
1681
- resize: "none"
1682
- });
1683
- if (props.placeholder) {
1684
- this.overlay.placeholder = props.placeholder;
1685
- }
1686
- if (this.overlay instanceof HTMLInputElement && props.type) {
1687
- this.overlay.type = props.type;
1688
- }
1689
- }
1690
- positionOverlay(bounds, canvasBounds) {
1691
- if (!this.overlay)
1692
- return;
1693
- const canvas = this.container.querySelector("canvas");
1694
- const scaleX = canvasBounds.width / canvas.width;
1695
- const scaleY = canvasBounds.height / canvas.height;
1696
- const left = bounds.x * scaleX;
1697
- const top = bounds.y * scaleY;
1698
- const width = bounds.width * scaleX;
1699
- const height = bounds.height * scaleY;
1700
- Object.assign(this.overlay.style, {
1701
- position: "absolute",
1702
- left: `${left}px`,
1703
- top: `${top}px`,
1704
- width: `${width}px`,
1705
- height: `${height}px`
1706
- });
1707
- }
1708
- onInput(e) {
1709
- if (!this.overlay || !this.onChangeCallback)
1710
- return;
1711
- const value = this.overlay.value;
1712
- this.onChangeCallback(value);
1713
- if (this.bindPath && this.engine) {
1714
- this.engine.dispatchAction("__hypen_bind", {
1715
- path: this.bindPath,
1716
- value
1717
- });
1718
- }
1796
+ mouseDownNode = null;
1797
+ boundOnMouseMove;
1798
+ boundOnMouseDown;
1799
+ boundOnMouseUp;
1800
+ boundOnClick;
1801
+ boundOnDoubleClick;
1802
+ boundOnKeyDown;
1803
+ boundOnKeyUp;
1804
+ constructor(canvas, engine) {
1805
+ this.canvas = canvas;
1806
+ this.engine = engine;
1807
+ this.setupEventListeners();
1719
1808
  }
1720
- onBlur() {
1721
- this.hideInput();
1809
+ setRootNode(node) {
1810
+ this.rootNode = node;
1722
1811
  }
1723
- onKeyDown(e) {
1724
- if (!this.overlay)
1725
- return;
1726
- if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
1727
- e.preventDefault();
1728
- this.overlay.blur();
1729
- }
1730
- if (e.key === "Escape") {
1731
- e.preventDefault();
1732
- this.overlay.blur();
1733
- }
1812
+ setupEventListeners() {
1813
+ this.boundOnMouseMove = this.onMouseMove.bind(this);
1814
+ this.boundOnMouseDown = this.onMouseDown.bind(this);
1815
+ this.boundOnMouseUp = this.onMouseUp.bind(this);
1816
+ this.boundOnClick = this.onClick.bind(this);
1817
+ this.boundOnDoubleClick = this.onDoubleClick.bind(this);
1818
+ this.boundOnKeyDown = this.onKeyDown.bind(this);
1819
+ this.boundOnKeyUp = this.onKeyUp.bind(this);
1820
+ this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
1821
+ this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
1822
+ this.canvas.addEventListener("mouseup", this.boundOnMouseUp);
1823
+ this.canvas.addEventListener("click", this.boundOnClick);
1824
+ this.canvas.addEventListener("dblclick", this.boundOnDoubleClick);
1825
+ this.canvas.addEventListener("keydown", this.boundOnKeyDown);
1826
+ this.canvas.addEventListener("keyup", this.boundOnKeyUp);
1734
1827
  }
1735
- isShown() {
1736
- return this.overlay !== null;
1828
+ getCanvasCoordinates(e) {
1829
+ const rect = this.canvas.getBoundingClientRect();
1830
+ const scaleX = this.canvas.width / rect.width;
1831
+ const scaleY = this.canvas.height / rect.height;
1832
+ return {
1833
+ x: (e.clientX - rect.left) * scaleX,
1834
+ y: (e.clientY - rect.top) * scaleY
1835
+ };
1737
1836
  }
1738
- getFocusedNode() {
1739
- return this.focusedNode;
1837
+ hitTest(point) {
1838
+ if (!this.rootNode)
1839
+ return null;
1840
+ return this.hitTestNode(this.rootNode, point);
1740
1841
  }
1741
- }
1742
-
1743
- // src/canvas/accessibility.ts
1744
- class AccessibilityLayer {
1745
- shadowRoot;
1746
- nodeMap = new Map;
1747
- enabled;
1748
- constructor(container, enabled = true) {
1749
- this.enabled = enabled && typeof document !== "undefined";
1750
- if (this.enabled && typeof document !== "undefined") {
1751
- this.shadowRoot = document.createElement("div");
1752
- this.shadowRoot.setAttribute("role", "application");
1753
- this.shadowRoot.setAttribute("aria-label", "Hypen Canvas Application");
1754
- this.shadowRoot.style.position = "absolute";
1755
- this.shadowRoot.style.left = "-9999px";
1756
- this.shadowRoot.style.width = "1px";
1757
- this.shadowRoot.style.height = "1px";
1758
- this.shadowRoot.style.overflow = "hidden";
1759
- if (container) {
1760
- container.appendChild(this.shadowRoot);
1761
- }
1762
- } else {
1763
- this.shadowRoot = {};
1842
+ hitTestNode(node, point) {
1843
+ if (!node.visible || !node.layout)
1844
+ return null;
1845
+ const bounds = getAbsoluteBounds(node);
1846
+ if (!bounds)
1847
+ return null;
1848
+ for (let i = node.children.length - 1;i >= 0; i--) {
1849
+ const child = node.children[i];
1850
+ const hit = this.hitTestNode(child, point);
1851
+ if (hit)
1852
+ return hit;
1853
+ }
1854
+ const radius = node.layout.border.radius;
1855
+ if (isPointInRoundedRect(point, bounds, radius)) {
1856
+ return node;
1764
1857
  }
1858
+ return null;
1765
1859
  }
1766
- syncTree(root) {
1767
- if (!this.enabled)
1768
- return;
1769
- this.shadowRoot.innerHTML = "";
1770
- this.nodeMap.clear();
1771
- const shadowNode = this.createShadowNode(root);
1772
- if (shadowNode) {
1773
- this.shadowRoot.appendChild(shadowNode);
1860
+ onMouseMove(e) {
1861
+ const point = this.getCanvasCoordinates(e);
1862
+ const node = this.hitTest(point);
1863
+ if (node !== this.hoveredNode) {
1864
+ if (this.hoveredNode) {
1865
+ this.hoveredNode.hovered = false;
1866
+ this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
1867
+ }
1868
+ this.hoveredNode = node;
1869
+ if (node) {
1870
+ node.hovered = true;
1871
+ this.dispatchNodeEvent(node, "mouseenter", {});
1872
+ }
1873
+ this.updateCursor(node);
1874
+ this.requestRedraw();
1774
1875
  }
1775
1876
  }
1776
- createShadowNode(node) {
1777
- if (!node.visible)
1778
- return null;
1779
- let element;
1780
- switch (node.type.toLowerCase()) {
1781
- case "button":
1782
- element = document.createElement("button");
1783
- element.textContent = this.getNodeText(node);
1784
- if (node.props.onclick) {
1785
- element.setAttribute("aria-label", "Clickable button");
1786
- }
1787
- break;
1788
- case "input":
1789
- element = document.createElement("input");
1790
- element.type = node.props.type || "text";
1791
- element.value = node.props.value || "";
1792
- if (node.props.placeholder) {
1793
- element.placeholder = node.props.placeholder;
1794
- }
1795
- break;
1796
- case "textarea":
1797
- element = document.createElement("textarea");
1798
- element.value = node.props.value || "";
1799
- if (node.props.placeholder) {
1800
- element.placeholder = node.props.placeholder;
1801
- }
1802
- break;
1803
- case "image":
1804
- element = document.createElement("img");
1805
- element.src = node.props.src || "";
1806
- element.alt = node.props.alt || "Image";
1807
- break;
1808
- case "text":
1809
- element = document.createElement("span");
1810
- element.textContent = String(node.props[0] || node.props.text || "");
1811
- break;
1812
- case "column":
1813
- case "row":
1814
- case "container":
1815
- case "box":
1816
- element = document.createElement("div");
1817
- element.setAttribute("role", node.type === "column" ? "group" : "group");
1818
- break;
1819
- default:
1820
- element = document.createElement("div");
1877
+ onMouseDown(e) {
1878
+ const point = this.getCanvasCoordinates(e);
1879
+ const node = this.hitTest(point);
1880
+ this.mouseDownNode = node;
1881
+ if (node && node.clickable) {
1882
+ this.dispatchNodeEvent(node, "mousedown", {
1883
+ button: e.button,
1884
+ clientX: e.clientX,
1885
+ clientY: e.clientY
1886
+ });
1821
1887
  }
1822
- element.setAttribute("data-hypen-id", node.id);
1823
- if (node.props["aria-label"]) {
1824
- element.setAttribute("aria-label", node.props["aria-label"]);
1888
+ if (node && node.focusable) {
1889
+ this.setFocus(node);
1890
+ } else {
1891
+ this.setFocus(null);
1825
1892
  }
1826
- if (node.focusable) {
1827
- element.tabIndex = 0;
1893
+ }
1894
+ onMouseUp(e) {
1895
+ const point = this.getCanvasCoordinates(e);
1896
+ const node = this.hitTest(point);
1897
+ if (node && node.clickable) {
1898
+ this.dispatchNodeEvent(node, "mouseup", {
1899
+ button: e.button,
1900
+ clientX: e.clientX,
1901
+ clientY: e.clientY
1902
+ });
1828
1903
  }
1829
- this.nodeMap.set(node.id, element);
1830
- for (const child of node.children) {
1831
- const childElement = this.createShadowNode(child);
1832
- if (childElement) {
1833
- element.appendChild(childElement);
1834
- }
1904
+ this.mouseDownNode = null;
1905
+ }
1906
+ onClick(e) {
1907
+ const point = this.getCanvasCoordinates(e);
1908
+ const node = this.hitTest(point);
1909
+ if (node && node.clickable && node === this.mouseDownNode) {
1910
+ this.dispatchNodeEvent(node, "click", {
1911
+ button: e.button,
1912
+ clientX: e.clientX,
1913
+ clientY: e.clientY
1914
+ });
1835
1915
  }
1836
- return element;
1837
1916
  }
1838
- getNodeText(node) {
1839
- if (node.type === "text") {
1840
- return String(node.props[0] || node.props.text || "");
1917
+ onDoubleClick(e) {
1918
+ const point = this.getCanvasCoordinates(e);
1919
+ const node = this.hitTest(point);
1920
+ if (node && node.clickable) {
1921
+ this.dispatchNodeEvent(node, "dblclick", {
1922
+ button: e.button,
1923
+ clientX: e.clientX,
1924
+ clientY: e.clientY
1925
+ });
1841
1926
  }
1842
- let text = "";
1843
- for (const child of node.children) {
1844
- text += this.getNodeText(child);
1927
+ }
1928
+ onKeyDown(e) {
1929
+ if (this.focusedNode) {
1930
+ this.dispatchNodeEvent(this.focusedNode, "keydown", {
1931
+ key: e.key,
1932
+ code: e.code,
1933
+ ctrlKey: e.ctrlKey,
1934
+ shiftKey: e.shiftKey,
1935
+ altKey: e.altKey
1936
+ });
1845
1937
  }
1846
- return text;
1847
1938
  }
1848
- focusNode(nodeId) {
1849
- if (!this.enabled)
1850
- return;
1851
- const element = this.nodeMap.get(nodeId);
1852
- if (element) {
1853
- element.focus();
1939
+ onKeyUp(e) {
1940
+ if (this.focusedNode) {
1941
+ this.dispatchNodeEvent(this.focusedNode, "keyup", {
1942
+ key: e.key,
1943
+ code: e.code,
1944
+ ctrlKey: e.ctrlKey,
1945
+ shiftKey: e.shiftKey,
1946
+ altKey: e.altKey
1947
+ });
1854
1948
  }
1855
1949
  }
1856
- updateNode(node) {
1857
- if (!this.enabled)
1858
- return;
1859
- const element = this.nodeMap.get(node.id);
1860
- if (!element)
1950
+ setFocus(node) {
1951
+ if (node === this.focusedNode)
1861
1952
  return;
1862
- if (node.type === "text") {
1863
- element.textContent = String(node.props[0] || node.props.text || "");
1953
+ if (this.focusedNode) {
1954
+ this.focusedNode.focused = false;
1955
+ this.dispatchNodeEvent(this.focusedNode, "blur", {});
1864
1956
  }
1865
- if (node.type === "input" || node.type === "textarea") {
1866
- element.value = node.props.value || "";
1957
+ this.focusedNode = node;
1958
+ if (node) {
1959
+ node.focused = true;
1960
+ this.dispatchNodeEvent(node, "focus", {});
1867
1961
  }
1868
- element.style.display = node.visible ? "" : "none";
1962
+ this.requestRedraw();
1869
1963
  }
1870
- getElement(nodeId) {
1871
- return this.nodeMap.get(nodeId);
1964
+ updateCursor(node) {
1965
+ if (!node) {
1966
+ this.canvas.style.cursor = "default";
1967
+ return;
1968
+ }
1969
+ const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
1970
+ this.canvas.style.cursor = cursor;
1872
1971
  }
1873
- setEnabled(enabled) {
1874
- this.enabled = enabled;
1875
- if (!enabled && this.shadowRoot.parentElement) {
1876
- this.shadowRoot.remove();
1972
+ dispatchNodeEvent(node, eventType, data) {
1973
+ const actionName = node.props[`on${eventType}`] || node.props[eventType];
1974
+ if (actionName && typeof actionName === "string") {
1975
+ this.engine.dispatchAction(actionName, {
1976
+ type: eventType,
1977
+ nodeId: node.id,
1978
+ timestamp: Date.now(),
1979
+ ...data
1980
+ });
1877
1981
  }
1878
1982
  }
1983
+ requestRedraw() {
1984
+ this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
1985
+ }
1879
1986
  destroy() {
1880
- if (this.shadowRoot.parentElement) {
1881
- this.shadowRoot.remove();
1882
- }
1883
- this.nodeMap.clear();
1987
+ this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
1988
+ this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
1989
+ this.canvas.removeEventListener("mouseup", this.boundOnMouseUp);
1990
+ this.canvas.removeEventListener("click", this.boundOnClick);
1991
+ this.canvas.removeEventListener("dblclick", this.boundOnDoubleClick);
1992
+ this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
1993
+ this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
1994
+ this.rootNode = null;
1995
+ this.hoveredNode = null;
1996
+ this.focusedNode = null;
1997
+ this.mouseDownNode = null;
1884
1998
  }
1885
1999
  }
1886
2000
 
@@ -2155,4 +2269,4 @@ export {
2155
2269
  CanvasRenderer
2156
2270
  };
2157
2271
 
2158
- //# debugId=64A1C0C9DB735D3764756E2164756E21
2272
+ //# debugId=F583F289DA278D9964756E2164756E21