@hypen-space/web 0.2.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.
Files changed (195) hide show
  1. package/dist/chunk-2s02mkzs.js +32 -0
  2. package/dist/chunk-2s02mkzs.js.map +9 -0
  3. package/dist/src/canvas/accessibility.js +152 -0
  4. package/dist/src/canvas/accessibility.js.map +10 -0
  5. package/dist/src/canvas/events.js +198 -0
  6. package/dist/src/canvas/events.js.map +10 -0
  7. package/dist/src/canvas/index.js +28 -0
  8. package/dist/src/canvas/index.js.map +9 -0
  9. package/dist/src/canvas/input.js +132 -0
  10. package/dist/src/canvas/input.js.map +10 -0
  11. package/dist/src/canvas/layout.js +309 -0
  12. package/dist/src/canvas/layout.js.map +10 -0
  13. package/dist/src/canvas/paint.js +878 -0
  14. package/dist/src/canvas/paint.js.map +10 -0
  15. package/dist/src/canvas/renderer.js +276 -0
  16. package/dist/src/canvas/renderer.js.map +10 -0
  17. package/dist/src/canvas/text.js +118 -0
  18. package/dist/src/canvas/text.js.map +10 -0
  19. package/dist/src/canvas/types.js +2 -0
  20. package/dist/src/canvas/types.js.map +9 -0
  21. package/dist/src/canvas/utils.js +139 -0
  22. package/dist/src/canvas/utils.js.map +10 -0
  23. package/dist/src/dom/applicators/advanced-layout.js +111 -0
  24. package/dist/src/dom/applicators/advanced-layout.js.map +10 -0
  25. package/dist/src/dom/applicators/background.js +54 -0
  26. package/dist/src/dom/applicators/background.js.map +10 -0
  27. package/dist/src/dom/applicators/border.js +33 -0
  28. package/dist/src/dom/applicators/border.js.map +10 -0
  29. package/dist/src/dom/applicators/color.js +36 -0
  30. package/dist/src/dom/applicators/color.js.map +10 -0
  31. package/dist/src/dom/applicators/display.js +57 -0
  32. package/dist/src/dom/applicators/display.js.map +10 -0
  33. package/dist/src/dom/applicators/effects.js +89 -0
  34. package/dist/src/dom/applicators/effects.js.map +10 -0
  35. package/dist/src/dom/applicators/events.js +518 -0
  36. package/dist/src/dom/applicators/events.js.map +10 -0
  37. package/dist/src/dom/applicators/font.js +39 -0
  38. package/dist/src/dom/applicators/font.js.map +10 -0
  39. package/dist/src/dom/applicators/index.js +296 -0
  40. package/dist/src/dom/applicators/index.js.map +10 -0
  41. package/dist/src/dom/applicators/layout.js +86 -0
  42. package/dist/src/dom/applicators/layout.js.map +10 -0
  43. package/dist/src/dom/applicators/margin.js +32 -0
  44. package/dist/src/dom/applicators/margin.js.map +10 -0
  45. package/dist/src/dom/applicators/padding.js +35 -0
  46. package/dist/src/dom/applicators/padding.js.map +10 -0
  47. package/dist/src/dom/applicators/size.js +42 -0
  48. package/dist/src/dom/applicators/size.js.map +10 -0
  49. package/dist/src/dom/applicators/transform.js +92 -0
  50. package/dist/src/dom/applicators/transform.js.map +10 -0
  51. package/dist/src/dom/applicators/transition.js +66 -0
  52. package/dist/src/dom/applicators/transition.js.map +10 -0
  53. package/dist/src/dom/applicators/typography.js +87 -0
  54. package/dist/src/dom/applicators/typography.js.map +10 -0
  55. package/dist/src/dom/canvas/index.js +50 -0
  56. package/dist/src/dom/canvas/index.js.map +10 -0
  57. package/dist/src/dom/components/audio.js +48 -0
  58. package/dist/src/dom/components/audio.js.map +10 -0
  59. package/dist/src/dom/components/avatar.js +58 -0
  60. package/dist/src/dom/components/avatar.js.map +10 -0
  61. package/dist/src/dom/components/badge.js +55 -0
  62. package/dist/src/dom/components/badge.js.map +10 -0
  63. package/dist/src/dom/components/button.js +29 -0
  64. package/dist/src/dom/components/button.js.map +10 -0
  65. package/dist/src/dom/components/card.js +33 -0
  66. package/dist/src/dom/components/card.js.map +10 -0
  67. package/dist/src/dom/components/center.js +32 -0
  68. package/dist/src/dom/components/center.js.map +10 -0
  69. package/dist/src/dom/components/checkbox.js +54 -0
  70. package/dist/src/dom/components/checkbox.js.map +10 -0
  71. package/dist/src/dom/components/column.js +31 -0
  72. package/dist/src/dom/components/column.js.map +10 -0
  73. package/dist/src/dom/components/container.js +29 -0
  74. package/dist/src/dom/components/container.js.map +10 -0
  75. package/dist/src/dom/components/divider.js +45 -0
  76. package/dist/src/dom/components/divider.js.map +10 -0
  77. package/dist/src/dom/components/grid.js +44 -0
  78. package/dist/src/dom/components/grid.js.map +10 -0
  79. package/dist/src/dom/components/heading.js +47 -0
  80. package/dist/src/dom/components/heading.js.map +10 -0
  81. package/dist/src/dom/components/image.js +39 -0
  82. package/dist/src/dom/components/image.js.map +10 -0
  83. package/dist/src/dom/components/index.js +217 -0
  84. package/dist/src/dom/components/index.js.map +10 -0
  85. package/dist/src/dom/components/input.js +41 -0
  86. package/dist/src/dom/components/input.js.map +10 -0
  87. package/dist/src/dom/components/link.js +42 -0
  88. package/dist/src/dom/components/link.js.map +10 -0
  89. package/dist/src/dom/components/list.js +42 -0
  90. package/dist/src/dom/components/list.js.map +10 -0
  91. package/dist/src/dom/components/paragraph.js +35 -0
  92. package/dist/src/dom/components/paragraph.js.map +10 -0
  93. package/dist/src/dom/components/progressbar.js +57 -0
  94. package/dist/src/dom/components/progressbar.js.map +10 -0
  95. package/dist/src/dom/components/route.js +44 -0
  96. package/dist/src/dom/components/route.js.map +10 -0
  97. package/dist/src/dom/components/router.js +33 -0
  98. package/dist/src/dom/components/router.js.map +10 -0
  99. package/dist/src/dom/components/row.js +31 -0
  100. package/dist/src/dom/components/row.js.map +10 -0
  101. package/dist/src/dom/components/select.js +57 -0
  102. package/dist/src/dom/components/select.js.map +10 -0
  103. package/dist/src/dom/components/slider.js +48 -0
  104. package/dist/src/dom/components/slider.js.map +10 -0
  105. package/dist/src/dom/components/spacer.js +30 -0
  106. package/dist/src/dom/components/spacer.js.map +10 -0
  107. package/dist/src/dom/components/spinner.js +65 -0
  108. package/dist/src/dom/components/spinner.js.map +10 -0
  109. package/dist/src/dom/components/stack.js +45 -0
  110. package/dist/src/dom/components/stack.js.map +10 -0
  111. package/dist/src/dom/components/switch.js +83 -0
  112. package/dist/src/dom/components/switch.js.map +10 -0
  113. package/dist/src/dom/components/text.js +37 -0
  114. package/dist/src/dom/components/text.js.map +10 -0
  115. package/dist/src/dom/components/textarea.js +51 -0
  116. package/dist/src/dom/components/textarea.js.map +10 -0
  117. package/dist/src/dom/components/video.js +51 -0
  118. package/dist/src/dom/components/video.js.map +10 -0
  119. package/dist/src/dom/debug.js +170 -0
  120. package/dist/src/dom/debug.js.map +10 -0
  121. package/dist/src/dom/events.js +112 -0
  122. package/dist/src/dom/events.js.map +10 -0
  123. package/dist/src/dom/index.js +73 -0
  124. package/dist/src/dom/index.js.map +9 -0
  125. package/dist/src/dom/renderer.js +277 -0
  126. package/dist/src/dom/renderer.js.map +10 -0
  127. package/dist/src/index.js +89 -0
  128. package/dist/src/index.js.map +9 -0
  129. package/package.json +84 -0
  130. package/src/canvas/QUICKSTART.md +421 -0
  131. package/src/canvas/README.md +376 -0
  132. package/src/canvas/accessibility.ts +218 -0
  133. package/src/canvas/events.ts +307 -0
  134. package/src/canvas/index.ts +35 -0
  135. package/src/canvas/input.ts +210 -0
  136. package/src/canvas/layout.ts +401 -0
  137. package/src/canvas/paint.ts +1321 -0
  138. package/src/canvas/renderer.ts +422 -0
  139. package/src/canvas/text.ts +182 -0
  140. package/src/canvas/types.ts +137 -0
  141. package/src/canvas/utils.ts +218 -0
  142. package/src/dom/README.md +265 -0
  143. package/src/dom/applicators/advanced-layout.ts +128 -0
  144. package/src/dom/applicators/background.ts +50 -0
  145. package/src/dom/applicators/border.ts +19 -0
  146. package/src/dom/applicators/color.ts +23 -0
  147. package/src/dom/applicators/display.ts +54 -0
  148. package/src/dom/applicators/effects.ts +97 -0
  149. package/src/dom/applicators/events.ts +689 -0
  150. package/src/dom/applicators/font.ts +27 -0
  151. package/src/dom/applicators/index.ts +354 -0
  152. package/src/dom/applicators/layout.ts +92 -0
  153. package/src/dom/applicators/margin.ts +18 -0
  154. package/src/dom/applicators/padding.ts +18 -0
  155. package/src/dom/applicators/size.ts +31 -0
  156. package/src/dom/applicators/transform.ts +93 -0
  157. package/src/dom/applicators/transition.ts +65 -0
  158. package/src/dom/applicators/typography.ts +91 -0
  159. package/src/dom/canvas/index.ts +60 -0
  160. package/src/dom/components/audio.ts +45 -0
  161. package/src/dom/components/avatar.ts +49 -0
  162. package/src/dom/components/badge.ts +45 -0
  163. package/src/dom/components/button.ts +13 -0
  164. package/src/dom/components/card.ts +19 -0
  165. package/src/dom/components/center.ts +16 -0
  166. package/src/dom/components/checkbox.ts +54 -0
  167. package/src/dom/components/column.ts +15 -0
  168. package/src/dom/components/container.ts +13 -0
  169. package/src/dom/components/divider.ts +37 -0
  170. package/src/dom/components/grid.ts +40 -0
  171. package/src/dom/components/heading.ts +41 -0
  172. package/src/dom/components/image.ts +27 -0
  173. package/src/dom/components/index.ts +115 -0
  174. package/src/dom/components/input.ts +29 -0
  175. package/src/dom/components/link.ts +35 -0
  176. package/src/dom/components/list.ts +30 -0
  177. package/src/dom/components/paragraph.ts +23 -0
  178. package/src/dom/components/progressbar.ts +51 -0
  179. package/src/dom/components/route.ts +37 -0
  180. package/src/dom/components/router.ts +22 -0
  181. package/src/dom/components/row.ts +15 -0
  182. package/src/dom/components/select.ts +56 -0
  183. package/src/dom/components/slider.ts +45 -0
  184. package/src/dom/components/spacer.ts +16 -0
  185. package/src/dom/components/spinner.ts +60 -0
  186. package/src/dom/components/stack.ts +34 -0
  187. package/src/dom/components/switch.ts +86 -0
  188. package/src/dom/components/text.ts +24 -0
  189. package/src/dom/components/textarea.ts +50 -0
  190. package/src/dom/components/video.ts +50 -0
  191. package/src/dom/debug.ts +247 -0
  192. package/src/dom/events.ts +168 -0
  193. package/src/dom/index.ts +11 -0
  194. package/src/dom/renderer.ts +327 -0
  195. package/src/index.ts +56 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Event System
3
+ *
4
+ * Hit testing and event handling for canvas nodes
5
+ */
6
+
7
+ import type { VirtualNode, Point } from "./types.js";
8
+ import { isPointInRoundedRect, getAbsoluteBounds } from "./utils.js";
9
+
10
+ // Interface for the engine that CanvasEventManager needs
11
+ interface IEngine {
12
+ dispatchAction(name: string, payload?: any): void;
13
+ }
14
+
15
+ /**
16
+ * Canvas Event Manager
17
+ */
18
+ export class CanvasEventManager {
19
+ private canvas: HTMLCanvasElement;
20
+ private engine: IEngine;
21
+ private rootNode: VirtualNode | null = null;
22
+ private hoveredNode: VirtualNode | null = null;
23
+ private focusedNode: VirtualNode | null = null;
24
+ private mouseDownNode: VirtualNode | null = null;
25
+
26
+ constructor(canvas: HTMLCanvasElement, engine: IEngine) {
27
+ this.canvas = canvas;
28
+ this.engine = engine;
29
+ this.setupEventListeners();
30
+ }
31
+
32
+ /**
33
+ * Set the root node for hit testing
34
+ */
35
+ setRootNode(node: VirtualNode | null): void {
36
+ this.rootNode = node;
37
+ }
38
+
39
+ /**
40
+ * Setup canvas event listeners
41
+ */
42
+ 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));
50
+ }
51
+
52
+ /**
53
+ * Get canvas coordinates from mouse event
54
+ */
55
+ private getCanvasCoordinates(e: MouseEvent): Point {
56
+ const rect = this.canvas.getBoundingClientRect();
57
+ const scaleX = this.canvas.width / rect.width;
58
+ const scaleY = this.canvas.height / rect.height;
59
+
60
+ return {
61
+ x: (e.clientX - rect.left) * scaleX,
62
+ y: (e.clientY - rect.top) * scaleY,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Find node at canvas coordinates
68
+ */
69
+ private hitTest(point: Point): VirtualNode | null {
70
+ if (!this.rootNode) return null;
71
+ return this.hitTestNode(this.rootNode, point);
72
+ }
73
+
74
+ /**
75
+ * Recursively test node and children
76
+ */
77
+ private hitTestNode(node: VirtualNode, point: Point): VirtualNode | null {
78
+ if (!node.visible || !node.layout) return null;
79
+
80
+ const bounds = getAbsoluteBounds(node);
81
+ if (!bounds) return null;
82
+
83
+ // Test children first (front to back)
84
+ for (let i = node.children.length - 1; i >= 0; i--) {
85
+ const child = node.children[i];
86
+ const hit = this.hitTestNode(child, point);
87
+ if (hit) return hit;
88
+ }
89
+
90
+ // Test this node
91
+ const radius = node.layout.border.radius;
92
+ if (isPointInRoundedRect(point, bounds, radius)) {
93
+ return node;
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Handle mouse move
101
+ */
102
+ private onMouseMove(e: MouseEvent): void {
103
+ const point = this.getCanvasCoordinates(e);
104
+ const node = this.hitTest(point);
105
+
106
+ // Update hover state
107
+ if (node !== this.hoveredNode) {
108
+ // Leave old node
109
+ if (this.hoveredNode) {
110
+ this.hoveredNode.hovered = false;
111
+ this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
112
+ }
113
+
114
+ // Enter new node
115
+ this.hoveredNode = node;
116
+ if (node) {
117
+ node.hovered = true;
118
+ this.dispatchNodeEvent(node, "mouseenter", {});
119
+ }
120
+
121
+ // Update cursor
122
+ this.updateCursor(node);
123
+
124
+ // Request redraw for hover effects
125
+ this.requestRedraw();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Handle mouse down
131
+ */
132
+ private onMouseDown(e: MouseEvent): void {
133
+ const point = this.getCanvasCoordinates(e);
134
+ const node = this.hitTest(point);
135
+
136
+ this.mouseDownNode = node;
137
+
138
+ if (node && node.clickable) {
139
+ this.dispatchNodeEvent(node, "mousedown", {
140
+ button: e.button,
141
+ clientX: e.clientX,
142
+ clientY: e.clientY,
143
+ });
144
+ }
145
+
146
+ // Update focus
147
+ if (node && node.focusable) {
148
+ this.setFocus(node);
149
+ } else {
150
+ this.setFocus(null);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Handle mouse up
156
+ */
157
+ private onMouseUp(e: MouseEvent): void {
158
+ const point = this.getCanvasCoordinates(e);
159
+ const node = this.hitTest(point);
160
+
161
+ if (node && node.clickable) {
162
+ this.dispatchNodeEvent(node, "mouseup", {
163
+ button: e.button,
164
+ clientX: e.clientX,
165
+ clientY: e.clientY,
166
+ });
167
+ }
168
+
169
+ this.mouseDownNode = null;
170
+ }
171
+
172
+ /**
173
+ * Handle click
174
+ */
175
+ private onClick(e: MouseEvent): void {
176
+ const point = this.getCanvasCoordinates(e);
177
+ const node = this.hitTest(point);
178
+
179
+ if (node && node.clickable && node === this.mouseDownNode) {
180
+ this.dispatchNodeEvent(node, "click", {
181
+ button: e.button,
182
+ clientX: e.clientX,
183
+ clientY: e.clientY,
184
+ });
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Handle double click
190
+ */
191
+ private onDoubleClick(e: MouseEvent): void {
192
+ const point = this.getCanvasCoordinates(e);
193
+ const node = this.hitTest(point);
194
+
195
+ if (node && node.clickable) {
196
+ this.dispatchNodeEvent(node, "dblclick", {
197
+ button: e.button,
198
+ clientX: e.clientX,
199
+ clientY: e.clientY,
200
+ });
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Handle keyboard events
206
+ */
207
+ private onKeyDown(e: KeyboardEvent): void {
208
+ if (this.focusedNode) {
209
+ this.dispatchNodeEvent(this.focusedNode, "keydown", {
210
+ key: e.key,
211
+ code: e.code,
212
+ ctrlKey: e.ctrlKey,
213
+ shiftKey: e.shiftKey,
214
+ altKey: e.altKey,
215
+ });
216
+ }
217
+ }
218
+
219
+ private onKeyUp(e: KeyboardEvent): void {
220
+ if (this.focusedNode) {
221
+ this.dispatchNodeEvent(this.focusedNode, "keyup", {
222
+ key: e.key,
223
+ code: e.code,
224
+ ctrlKey: e.ctrlKey,
225
+ shiftKey: e.shiftKey,
226
+ altKey: e.altKey,
227
+ });
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Set focused node
233
+ */
234
+ private setFocus(node: VirtualNode | null): void {
235
+ if (node === this.focusedNode) return;
236
+
237
+ if (this.focusedNode) {
238
+ this.focusedNode.focused = false;
239
+ this.dispatchNodeEvent(this.focusedNode, "blur", {});
240
+ }
241
+
242
+ this.focusedNode = node;
243
+
244
+ if (node) {
245
+ node.focused = true;
246
+ this.dispatchNodeEvent(node, "focus", {});
247
+ }
248
+
249
+ this.requestRedraw();
250
+ }
251
+
252
+ /**
253
+ * Update cursor based on node
254
+ */
255
+ private updateCursor(node: VirtualNode | null): void {
256
+ if (!node) {
257
+ this.canvas.style.cursor = "default";
258
+ return;
259
+ }
260
+
261
+ const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
262
+ this.canvas.style.cursor = cursor;
263
+ }
264
+
265
+ /**
266
+ * Dispatch event to engine
267
+ */
268
+ private dispatchNodeEvent(node: VirtualNode, eventType: string, data: any): void {
269
+ // Check if node has action handler for this event
270
+ const actionName = node.props[`on${eventType}`] || node.props[eventType];
271
+
272
+ if (actionName && typeof actionName === "string") {
273
+ // Dispatch to engine
274
+ this.engine.dispatchAction(actionName, {
275
+ type: eventType,
276
+ nodeId: node.id,
277
+ timestamp: Date.now(),
278
+ ...data,
279
+ });
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Request redraw from renderer
285
+ */
286
+ private requestRedraw(): void {
287
+ // This will be called via a callback set by the renderer
288
+ // For now, dispatch a custom event
289
+ this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
290
+ }
291
+
292
+ /**
293
+ * Cleanup
294
+ */
295
+ destroy(): void {
296
+ // Remove event listeners if needed
297
+ }
298
+ }
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Canvas Renderer for Hypen
3
+ *
4
+ * Browser-only module for rendering Hypen UI to Canvas
5
+ */
6
+
7
+ export { CanvasRenderer } from "./renderer.js";
8
+ export { registerPainter } from "./paint.js";
9
+ export { CanvasEventManager } from "./events.js";
10
+ export { InputOverlay } from "./input.js";
11
+ export { AccessibilityLayer } from "./accessibility.js";
12
+
13
+ export type {
14
+ VirtualNode,
15
+ Layout,
16
+ Rectangle,
17
+ Point,
18
+ FontStyle,
19
+ TextStyle,
20
+ TextMetrics,
21
+ CanvasRendererOptions,
22
+ PainterFunction,
23
+ LayoutFunction,
24
+ } from "./types.js";
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Input Overlay System
3
+ *
4
+ * Handle text input using DOM overlay elements
5
+ */
6
+
7
+ import type { VirtualNode, Rectangle } from "./types.js";
8
+ import { getAbsoluteBounds } from "./utils.js";
9
+
10
+ /**
11
+ * Input Overlay Manager
12
+ */
13
+ export class InputOverlay {
14
+ private container: HTMLElement;
15
+ private overlay: HTMLInputElement | HTMLTextAreaElement | null = null;
16
+ private focusedNode: VirtualNode | null = null;
17
+ private onChangeCallback: ((value: string) => void) | null = null;
18
+
19
+ constructor(container: HTMLElement | null) {
20
+ this.container = container || ({} as HTMLElement);
21
+ }
22
+
23
+ /**
24
+ * Show input overlay for a node
25
+ */
26
+ showInput(
27
+ node: VirtualNode,
28
+ canvasBounds: DOMRect,
29
+ onChange: (value: string) => void
30
+ ): void {
31
+ // Skip if document is not available (non-browser environment)
32
+ if (typeof document === "undefined") return;
33
+
34
+ this.hideInput();
35
+
36
+ const bounds = getAbsoluteBounds(node);
37
+ if (!bounds) return;
38
+
39
+ const isMultiline = node.type === "textarea";
40
+ this.overlay = isMultiline
41
+ ? document.createElement("textarea")
42
+ : document.createElement("input");
43
+
44
+ // Style overlay
45
+ this.styleOverlay(node, bounds, canvasBounds);
46
+
47
+ // Set initial value
48
+ const value = node.props.value || "";
49
+ this.overlay.value = value;
50
+
51
+ // Setup event handlers
52
+ this.onChangeCallback = onChange;
53
+ this.overlay.addEventListener("input", this.onInput.bind(this));
54
+ this.overlay.addEventListener("blur", this.onBlur.bind(this));
55
+ this.overlay.addEventListener("keydown", this.onKeyDown.bind(this) as EventListener);
56
+
57
+ // Add to DOM and focus
58
+ this.container.appendChild(this.overlay);
59
+ this.overlay.focus();
60
+
61
+ this.focusedNode = node;
62
+ }
63
+
64
+ /**
65
+ * Hide input overlay
66
+ */
67
+ hideInput(): void {
68
+ if (this.overlay) {
69
+ this.overlay.remove();
70
+ this.overlay = null;
71
+ }
72
+ this.focusedNode = null;
73
+ this.onChangeCallback = null;
74
+ }
75
+
76
+ /**
77
+ * Update overlay position
78
+ */
79
+ updatePosition(node: VirtualNode, canvasBounds: DOMRect): void {
80
+ if (!this.overlay || node !== this.focusedNode) return;
81
+
82
+ const bounds = getAbsoluteBounds(node);
83
+ if (!bounds) return;
84
+
85
+ this.positionOverlay(bounds, canvasBounds);
86
+ }
87
+
88
+ /**
89
+ * Style overlay to match canvas node
90
+ */
91
+ private styleOverlay(
92
+ node: VirtualNode,
93
+ bounds: Rectangle,
94
+ canvasBounds: DOMRect
95
+ ): void {
96
+ if (!this.overlay) return;
97
+
98
+ const props = node.props;
99
+
100
+ // Position
101
+ this.positionOverlay(bounds, canvasBounds);
102
+
103
+ // Font and text styling
104
+ const fontSize = parseFloat(props.fontSize) || 16;
105
+ const fontWeight = props.fontWeight || "normal";
106
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
107
+ const color = props.color || "#000000";
108
+
109
+ Object.assign(this.overlay.style, {
110
+ fontSize: `${fontSize}px`,
111
+ fontWeight: fontWeight,
112
+ fontFamily: fontFamily,
113
+ color: color,
114
+ border: "none",
115
+ outline: "2px solid #007bff",
116
+ backgroundColor: props.backgroundColor || "#ffffff",
117
+ padding: `${props.padding || 8}px`,
118
+ borderRadius: `${props.borderRadius || 4}px`,
119
+ boxSizing: "border-box",
120
+ resize: "none",
121
+ });
122
+
123
+ // Placeholder
124
+ if (props.placeholder) {
125
+ this.overlay.placeholder = props.placeholder;
126
+ }
127
+
128
+ // Input type
129
+ if (this.overlay instanceof HTMLInputElement && props.type) {
130
+ this.overlay.type = props.type;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Position overlay over canvas node
136
+ */
137
+ private positionOverlay(bounds: Rectangle, canvasBounds: DOMRect): void {
138
+ if (!this.overlay) return;
139
+
140
+ // Calculate scale factor between canvas and display
141
+ const canvas = this.container.querySelector("canvas") as HTMLCanvasElement;
142
+ const scaleX = canvasBounds.width / canvas.width;
143
+ const scaleY = canvasBounds.height / canvas.height;
144
+
145
+ const left = bounds.x * scaleX;
146
+ const top = bounds.y * scaleY;
147
+ const width = bounds.width * scaleX;
148
+ const height = bounds.height * scaleY;
149
+
150
+ Object.assign(this.overlay.style, {
151
+ position: "absolute",
152
+ left: `${left}px`,
153
+ top: `${top}px`,
154
+ width: `${width}px`,
155
+ height: `${height}px`,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Handle input event
161
+ */
162
+ private onInput(e: Event): void {
163
+ if (!this.overlay || !this.onChangeCallback) return;
164
+
165
+ const value = this.overlay.value;
166
+ this.onChangeCallback(value);
167
+ }
168
+
169
+ /**
170
+ * Handle blur event
171
+ */
172
+ private onBlur(): void {
173
+ // Hide overlay when focus is lost
174
+ this.hideInput();
175
+ }
176
+
177
+ /**
178
+ * Handle keyboard events
179
+ */
180
+ private onKeyDown(e: KeyboardEvent): void {
181
+ if (!this.overlay) return;
182
+
183
+ // Enter key submits (unless multiline)
184
+ if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
185
+ e.preventDefault();
186
+ this.overlay.blur();
187
+ }
188
+
189
+ // Escape cancels
190
+ if (e.key === "Escape") {
191
+ e.preventDefault();
192
+ this.overlay.blur();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Check if input is currently shown
198
+ */
199
+ isShown(): boolean {
200
+ return this.overlay !== null;
201
+ }
202
+
203
+ /**
204
+ * Get current focused node
205
+ */
206
+ getFocusedNode(): VirtualNode | null {
207
+ return this.focusedNode;
208
+ }
209
+ }
210
+