@hypen-space/web 0.2.5 → 0.2.7

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 (129) hide show
  1. package/dist/src/canvas/accessibility.js +29 -4
  2. package/dist/src/canvas/accessibility.js.map +2 -2
  3. package/dist/src/canvas/events.js +152 -8
  4. package/dist/src/canvas/events.js.map +4 -3
  5. package/dist/src/canvas/index.js +2116 -20
  6. package/dist/src/canvas/index.js.map +11 -3
  7. package/dist/src/canvas/input.js +152 -7
  8. package/dist/src/canvas/input.js.map +4 -3
  9. package/dist/src/canvas/layout.js +256 -11
  10. package/dist/src/canvas/layout.js.map +5 -3
  11. package/dist/src/canvas/paint.js +256 -8
  12. package/dist/src/canvas/paint.js.map +5 -3
  13. package/dist/src/canvas/renderer.js +1866 -22
  14. package/dist/src/canvas/renderer.js.map +10 -3
  15. package/dist/src/canvas/text.js +152 -7
  16. package/dist/src/canvas/text.js.map +4 -3
  17. package/dist/src/canvas/utils.js +29 -4
  18. package/dist/src/canvas/utils.js.map +2 -2
  19. package/dist/src/dom/applicators/advanced-layout.js +29 -7
  20. package/dist/src/dom/applicators/advanced-layout.js.map +2 -2
  21. package/dist/src/dom/applicators/background.js +29 -7
  22. package/dist/src/dom/applicators/background.js.map +2 -2
  23. package/dist/src/dom/applicators/border.js +29 -7
  24. package/dist/src/dom/applicators/border.js.map +2 -2
  25. package/dist/src/dom/applicators/color.js +29 -7
  26. package/dist/src/dom/applicators/color.js.map +2 -2
  27. package/dist/src/dom/applicators/display.js +29 -7
  28. package/dist/src/dom/applicators/display.js.map +2 -2
  29. package/dist/src/dom/applicators/effects.js +29 -7
  30. package/dist/src/dom/applicators/effects.js.map +2 -2
  31. package/dist/src/dom/applicators/events.js +29 -7
  32. package/dist/src/dom/applicators/events.js.map +2 -2
  33. package/dist/src/dom/applicators/font.js +29 -7
  34. package/dist/src/dom/applicators/font.js.map +2 -2
  35. package/dist/src/dom/applicators/index.js +1244 -94
  36. package/dist/src/dom/applicators/index.js.map +18 -3
  37. package/dist/src/dom/applicators/layout.js +29 -7
  38. package/dist/src/dom/applicators/layout.js.map +2 -2
  39. package/dist/src/dom/applicators/margin.js +29 -6
  40. package/dist/src/dom/applicators/margin.js.map +2 -2
  41. package/dist/src/dom/applicators/padding.js +29 -9
  42. package/dist/src/dom/applicators/padding.js.map +2 -2
  43. package/dist/src/dom/applicators/size.js +29 -7
  44. package/dist/src/dom/applicators/size.js.map +2 -2
  45. package/dist/src/dom/applicators/transform.js +29 -7
  46. package/dist/src/dom/applicators/transform.js.map +2 -2
  47. package/dist/src/dom/applicators/transition.js +29 -7
  48. package/dist/src/dom/applicators/transition.js.map +2 -2
  49. package/dist/src/dom/applicators/typography.js +29 -7
  50. package/dist/src/dom/applicators/typography.js.map +2 -2
  51. package/dist/src/dom/canvas/index.js +29 -4
  52. package/dist/src/dom/canvas/index.js.map +2 -2
  53. package/dist/src/dom/components/audio.js +29 -7
  54. package/dist/src/dom/components/audio.js.map +2 -2
  55. package/dist/src/dom/components/avatar.js +29 -7
  56. package/dist/src/dom/components/avatar.js.map +2 -2
  57. package/dist/src/dom/components/badge.js +29 -7
  58. package/dist/src/dom/components/badge.js.map +2 -2
  59. package/dist/src/dom/components/button.js +29 -7
  60. package/dist/src/dom/components/button.js.map +2 -2
  61. package/dist/src/dom/components/card.js +29 -7
  62. package/dist/src/dom/components/card.js.map +2 -2
  63. package/dist/src/dom/components/center.js +29 -7
  64. package/dist/src/dom/components/center.js.map +2 -2
  65. package/dist/src/dom/components/checkbox.js +29 -7
  66. package/dist/src/dom/components/checkbox.js.map +2 -2
  67. package/dist/src/dom/components/column.js +29 -7
  68. package/dist/src/dom/components/column.js.map +2 -2
  69. package/dist/src/dom/components/container.js +29 -7
  70. package/dist/src/dom/components/container.js.map +2 -2
  71. package/dist/src/dom/components/divider.js +29 -7
  72. package/dist/src/dom/components/divider.js.map +2 -2
  73. package/dist/src/dom/components/grid.js +29 -7
  74. package/dist/src/dom/components/grid.js.map +2 -2
  75. package/dist/src/dom/components/heading.js +29 -7
  76. package/dist/src/dom/components/heading.js.map +2 -2
  77. package/dist/src/dom/components/image.js +29 -7
  78. package/dist/src/dom/components/image.js.map +2 -2
  79. package/dist/src/dom/components/index.js +1036 -187
  80. package/dist/src/dom/components/index.js.map +33 -3
  81. package/dist/src/dom/components/input.js +29 -7
  82. package/dist/src/dom/components/input.js.map +2 -2
  83. package/dist/src/dom/components/link.js +29 -7
  84. package/dist/src/dom/components/link.js.map +2 -2
  85. package/dist/src/dom/components/list.js +29 -7
  86. package/dist/src/dom/components/list.js.map +2 -2
  87. package/dist/src/dom/components/paragraph.js +29 -7
  88. package/dist/src/dom/components/paragraph.js.map +2 -2
  89. package/dist/src/dom/components/progressbar.js +29 -7
  90. package/dist/src/dom/components/progressbar.js.map +2 -2
  91. package/dist/src/dom/components/route.js +29 -7
  92. package/dist/src/dom/components/route.js.map +2 -2
  93. package/dist/src/dom/components/router.js +29 -7
  94. package/dist/src/dom/components/router.js.map +2 -2
  95. package/dist/src/dom/components/row.js +29 -7
  96. package/dist/src/dom/components/row.js.map +2 -2
  97. package/dist/src/dom/components/select.js +29 -7
  98. package/dist/src/dom/components/select.js.map +2 -2
  99. package/dist/src/dom/components/slider.js +29 -7
  100. package/dist/src/dom/components/slider.js.map +2 -2
  101. package/dist/src/dom/components/spacer.js +29 -7
  102. package/dist/src/dom/components/spacer.js.map +2 -2
  103. package/dist/src/dom/components/spinner.js +29 -7
  104. package/dist/src/dom/components/spinner.js.map +2 -2
  105. package/dist/src/dom/components/stack.js +29 -7
  106. package/dist/src/dom/components/stack.js.map +2 -2
  107. package/dist/src/dom/components/switch.js +29 -7
  108. package/dist/src/dom/components/switch.js.map +2 -2
  109. package/dist/src/dom/components/text.js +29 -7
  110. package/dist/src/dom/components/text.js.map +2 -2
  111. package/dist/src/dom/components/textarea.js +29 -7
  112. package/dist/src/dom/components/textarea.js.map +2 -2
  113. package/dist/src/dom/components/video.js +29 -7
  114. package/dist/src/dom/components/video.js.map +2 -2
  115. package/dist/src/dom/debug.js +29 -4
  116. package/dist/src/dom/debug.js.map +2 -2
  117. package/dist/src/dom/events.js +29 -4
  118. package/dist/src/dom/events.js.map +2 -2
  119. package/dist/src/dom/index.js +2999 -64
  120. package/dist/src/dom/index.js.map +54 -3
  121. package/dist/src/dom/renderer.js +2677 -63
  122. package/dist/src/dom/renderer.js.map +52 -3
  123. package/dist/src/hypen.js +2895 -56
  124. package/dist/src/hypen.js.map +53 -3
  125. package/dist/src/index.js +5419 -81
  126. package/dist/src/index.js.map +63 -3
  127. package/package.json +22 -22
  128. package/dist/chunk-2s02mkzs.js +0 -32
  129. package/dist/chunk-2s02mkzs.js.map +0 -9
@@ -1,22 +1,1868 @@
1
- import {
2
- InputOverlay
3
- } from "./input.js";
4
- import {
5
- paintNode,
6
- registerPainter
7
- } from "./paint.js";
8
- import {
9
- computeLayout
10
- } from "./layout.js";
11
- import {
12
- CanvasEventManager
13
- } from "./events.js";
14
- import {
15
- AccessibilityLayer
16
- } from "./accessibility.js";
17
- import"./text.js";
18
- import"./utils.js";
19
- import"../../chunk-2s02mkzs.js";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ 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
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+
30
+ // src/canvas/utils.ts
31
+ function parseSpacing(value) {
32
+ if (typeof value === "number") {
33
+ return { top: value, right: value, bottom: value, left: value };
34
+ }
35
+ if (typeof value === "string") {
36
+ const parts = value.split(/\s+/).map((v) => parseFloat(v) || 0);
37
+ if (parts.length === 1) {
38
+ return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
39
+ }
40
+ if (parts.length === 2) {
41
+ return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
42
+ }
43
+ if (parts.length === 4) {
44
+ return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
45
+ }
46
+ }
47
+ if (typeof value === "object" && value !== null) {
48
+ return {
49
+ top: parseFloat(value.top) || 0,
50
+ right: parseFloat(value.right) || 0,
51
+ bottom: parseFloat(value.bottom) || 0,
52
+ left: parseFloat(value.left) || 0
53
+ };
54
+ }
55
+ return { top: 0, right: 0, bottom: 0, left: 0 };
56
+ }
57
+ function parseSize(value) {
58
+ if (typeof value === "number")
59
+ return value;
60
+ if (typeof value === "string") {
61
+ if (value === "auto")
62
+ return null;
63
+ const num = parseFloat(value);
64
+ return isNaN(num) ? null : num;
65
+ }
66
+ return null;
67
+ }
68
+ function isPointInRect(point, rect) {
69
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
70
+ }
71
+ function isPointInRoundedRect(point, rect, radius) {
72
+ const { x, y, width, height } = rect;
73
+ if (!isPointInRect(point, rect))
74
+ return false;
75
+ if (radius <= 0)
76
+ return true;
77
+ const px = point.x;
78
+ const py = point.y;
79
+ if (px < x + radius && py < y + radius) {
80
+ return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
81
+ }
82
+ if (px > x + width - radius && py < y + radius) {
83
+ return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
84
+ }
85
+ if (px < x + radius && py > y + height - radius) {
86
+ return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
87
+ }
88
+ if (px > x + width - radius && py > y + height - radius) {
89
+ return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
90
+ }
91
+ return true;
92
+ }
93
+ function mergeRects(rects) {
94
+ if (rects.length === 0)
95
+ return null;
96
+ if (rects.length === 1)
97
+ return rects[0];
98
+ let minX = Infinity;
99
+ let minY = Infinity;
100
+ let maxX = -Infinity;
101
+ let maxY = -Infinity;
102
+ for (const rect of rects) {
103
+ minX = Math.min(minX, rect.x);
104
+ minY = Math.min(minY, rect.y);
105
+ maxX = Math.max(maxX, rect.x + rect.width);
106
+ maxY = Math.max(maxY, rect.y + rect.height);
107
+ }
108
+ return {
109
+ x: minX,
110
+ y: minY,
111
+ width: maxX - minX,
112
+ height: maxY - minY
113
+ };
114
+ }
115
+ function createFontString(fontSize, fontWeight, fontFamily) {
116
+ return `${fontWeight} ${fontSize}px ${fontFamily}`;
117
+ }
118
+ function walkTree(node, callback) {
119
+ callback(node);
120
+ for (const child of node.children) {
121
+ walkTree(child, callback);
122
+ }
123
+ }
124
+ function findNodeById(root, id) {
125
+ if (root.id === id)
126
+ return root;
127
+ for (const child of root.children) {
128
+ const found = findNodeById(child, id);
129
+ if (found)
130
+ return found;
131
+ }
132
+ return null;
133
+ }
134
+ function getAbsoluteBounds(node) {
135
+ if (!node.layout)
136
+ return null;
137
+ let x = node.layout.x;
138
+ let y = node.layout.y;
139
+ let current = node.parent;
140
+ while (current && current.layout) {
141
+ x += current.layout.contentX;
142
+ y += current.layout.contentY;
143
+ current = current.parent;
144
+ }
145
+ return {
146
+ x,
147
+ y,
148
+ width: node.layout.width,
149
+ height: node.layout.height
150
+ };
151
+ }
152
+
153
+ // src/canvas/text.ts
154
+ var textMetricsCache = new Map;
155
+ function getCacheKey(text, fontStyle, maxWidth) {
156
+ return `${text}|${fontStyle.fontSize}|${fontStyle.fontWeight}|${fontStyle.fontFamily}|${maxWidth || "auto"}`;
157
+ }
158
+ function measureText(ctx, text, fontStyle, maxWidth) {
159
+ const cacheKey = getCacheKey(text, fontStyle, maxWidth);
160
+ const cached = textMetricsCache.get(cacheKey);
161
+ if (cached)
162
+ return cached;
163
+ const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
164
+ ctx.save();
165
+ ctx.font = font;
166
+ const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
167
+ if (!maxWidth) {
168
+ const metrics = ctx.measureText(text);
169
+ const result2 = {
170
+ width: metrics.width,
171
+ height: lineHeight,
172
+ lines: [text],
173
+ lineHeight
174
+ };
175
+ ctx.restore();
176
+ textMetricsCache.set(cacheKey, result2);
177
+ return result2;
178
+ }
179
+ const lines = wrapText(ctx, text, maxWidth);
180
+ const width = Math.max(...lines.map((line) => ctx.measureText(line).width));
181
+ const height = lines.length * lineHeight;
182
+ const result = {
183
+ width,
184
+ height,
185
+ lines,
186
+ lineHeight
187
+ };
188
+ ctx.restore();
189
+ textMetricsCache.set(cacheKey, result);
190
+ return result;
191
+ }
192
+ function wrapText(ctx, text, maxWidth) {
193
+ const lines = [];
194
+ const paragraphs = text.split(`
195
+ `);
196
+ for (const paragraph of paragraphs) {
197
+ const words = paragraph.split(" ");
198
+ let currentLine = "";
199
+ for (const word of words) {
200
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
201
+ const metrics = ctx.measureText(testLine);
202
+ if (metrics.width > maxWidth && currentLine) {
203
+ lines.push(currentLine);
204
+ currentLine = word;
205
+ } else {
206
+ currentLine = testLine;
207
+ }
208
+ }
209
+ if (currentLine) {
210
+ lines.push(currentLine);
211
+ }
212
+ }
213
+ return lines.length > 0 ? lines : [""];
214
+ }
215
+ function renderText(ctx, text, x, y, width, height, style) {
216
+ const font = createFontString(style.fontSize, style.fontWeight, style.fontFamily);
217
+ ctx.save();
218
+ ctx.font = font;
219
+ ctx.fillStyle = style.color;
220
+ ctx.textBaseline = "top";
221
+ const metrics = measureText(ctx, text, style, width);
222
+ let startY = y;
223
+ if (style.verticalAlign === "middle") {
224
+ startY = y + (height - metrics.height) / 2;
225
+ } else if (style.verticalAlign === "bottom") {
226
+ startY = y + height - metrics.height;
227
+ }
228
+ for (let i = 0;i < metrics.lines.length; i++) {
229
+ const line = metrics.lines[i];
230
+ const lineY = startY + i * metrics.lineHeight;
231
+ let lineX = x;
232
+ if (style.textAlign === "center") {
233
+ const lineWidth = ctx.measureText(line).width;
234
+ lineX = x + (width - lineWidth) / 2;
235
+ } else if (style.textAlign === "right") {
236
+ const lineWidth = ctx.measureText(line).width;
237
+ lineX = x + width - lineWidth;
238
+ }
239
+ ctx.fillText(line, lineX, lineY);
240
+ }
241
+ ctx.restore();
242
+ }
243
+ function clearTextCache() {
244
+ textMetricsCache.clear();
245
+ }
246
+ async function loadFont(fontFamily, fontWeight = "normal") {
247
+ if (!("fonts" in document))
248
+ return;
249
+ const font = `${fontWeight} 16px ${fontFamily}`;
250
+ try {
251
+ await document.fonts.load(font);
252
+ } catch (error) {
253
+ console.warn(`Failed to load font: ${font}`, error);
254
+ }
255
+ }
256
+
257
+ // src/canvas/layout.ts
258
+ function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
259
+ const props = node.props;
260
+ let margin = parseSpacing(props.margin || 0);
261
+ if (props.marginTop !== undefined)
262
+ margin.top = parseFloat(props.marginTop) || 0;
263
+ if (props.marginRight !== undefined)
264
+ margin.right = parseFloat(props.marginRight) || 0;
265
+ if (props.marginBottom !== undefined)
266
+ margin.bottom = parseFloat(props.marginBottom) || 0;
267
+ if (props.marginLeft !== undefined)
268
+ margin.left = parseFloat(props.marginLeft) || 0;
269
+ let padding = parseSpacing(props.padding || 0);
270
+ if (props.paddingTop !== undefined)
271
+ padding.top = parseFloat(props.paddingTop) || 0;
272
+ if (props.paddingRight !== undefined)
273
+ padding.right = parseFloat(props.paddingRight) || 0;
274
+ if (props.paddingBottom !== undefined)
275
+ padding.bottom = parseFloat(props.paddingBottom) || 0;
276
+ if (props.paddingLeft !== undefined)
277
+ padding.left = parseFloat(props.paddingLeft) || 0;
278
+ const borderWidth = parseFloat(props.borderWidth) || 0;
279
+ const borderColor = props.borderColor || "transparent";
280
+ const borderRadius = parseFloat(props.borderRadius) || 0;
281
+ const availableAfterMargin = {
282
+ width: availableWidth - margin.left - margin.right,
283
+ height: availableHeight - margin.top - margin.bottom
284
+ };
285
+ let width = parseSize(props.width);
286
+ let height = parseSize(props.height);
287
+ const type = node.type.toLowerCase();
288
+ if (type === "spacer") {
289
+ if (width === null)
290
+ width = availableAfterMargin.width;
291
+ if (height === null)
292
+ height = availableAfterMargin.height;
293
+ } else if (type === "divider" || type === "separator") {
294
+ const orientation = props.orientation || "horizontal";
295
+ const thickness = parseFloat(props.thickness) || 1;
296
+ if (orientation === "vertical") {
297
+ if (width === null)
298
+ width = thickness;
299
+ if (height === null)
300
+ height = availableAfterMargin.height;
301
+ } else {
302
+ if (width === null)
303
+ width = availableAfterMargin.width;
304
+ if (height === null)
305
+ height = thickness;
306
+ }
307
+ } else if (type === "checkbox" || type === "radio") {
308
+ const size = parseFloat(props.size) || 20;
309
+ if (width === null)
310
+ width = size;
311
+ if (height === null)
312
+ height = size;
313
+ } else if (type === "switch" || type === "toggle") {
314
+ if (width === null)
315
+ width = 44;
316
+ if (height === null)
317
+ height = 24;
318
+ } else if (type === "slider") {
319
+ if (width === null)
320
+ width = 200;
321
+ if (height === null)
322
+ height = 20;
323
+ } else if (type === "progress" || type === "progressbar") {
324
+ if (width === null)
325
+ width = 200;
326
+ if (height === null)
327
+ height = 8;
328
+ } else if (type === "spinner" || type === "loading") {
329
+ const size = parseFloat(props.size) || 24;
330
+ if (width === null)
331
+ width = size;
332
+ if (height === null)
333
+ height = size;
334
+ } else if (type === "badge") {
335
+ if (width === null)
336
+ width = 20;
337
+ if (height === null)
338
+ height = 20;
339
+ } else if (type === "avatar") {
340
+ const size = parseFloat(props.size) || 40;
341
+ if (width === null)
342
+ width = size;
343
+ if (height === null)
344
+ height = size;
345
+ } else if (type === "icon") {
346
+ const size = parseFloat(props.size) || 24;
347
+ if (width === null)
348
+ width = size;
349
+ if (height === null)
350
+ height = size;
351
+ }
352
+ if (node.type === "text" && node.props[0]) {
353
+ const text = String(node.props[0] || "");
354
+ const fontSize = parseFloat(props.fontSize) || 16;
355
+ const fontWeight = props.fontWeight || "normal";
356
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
357
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
358
+ const maxWidth2 = width || availableAfterMargin.width - padding.left - padding.right;
359
+ const metrics = measureText(ctx, text, { fontSize, fontWeight, fontFamily, lineHeight }, maxWidth2);
360
+ if (!width)
361
+ width = metrics.width + padding.left + padding.right;
362
+ if (!height)
363
+ height = metrics.height + padding.top + padding.bottom;
364
+ }
365
+ if (width === null)
366
+ width = availableAfterMargin.width;
367
+ if (height === null)
368
+ height = availableAfterMargin.height;
369
+ const minWidth = parseSize(props.minWidth);
370
+ const maxWidth = parseSize(props.maxWidth);
371
+ const minHeight = parseSize(props.minHeight);
372
+ const maxHeight = parseSize(props.maxHeight);
373
+ if (minWidth !== null)
374
+ width = Math.max(width, minWidth);
375
+ if (maxWidth !== null)
376
+ width = Math.min(width, maxWidth);
377
+ if (minHeight !== null)
378
+ height = Math.max(height, minHeight);
379
+ if (maxHeight !== null)
380
+ height = Math.min(height, maxHeight);
381
+ const layout = {
382
+ x: x + margin.left,
383
+ y: y + margin.top,
384
+ width,
385
+ height,
386
+ margin,
387
+ padding,
388
+ border: {
389
+ width: borderWidth,
390
+ color: borderColor,
391
+ radius: borderRadius
392
+ },
393
+ contentX: padding.left + borderWidth,
394
+ contentY: padding.top + borderWidth,
395
+ contentWidth: width - padding.left - padding.right - borderWidth * 2,
396
+ contentHeight: height - padding.top - padding.bottom - borderWidth * 2
397
+ };
398
+ node.layout = layout;
399
+ if (node.children.length > 0) {
400
+ layoutChildren(ctx, node);
401
+ }
402
+ }
403
+ function layoutChildren(ctx, parent) {
404
+ const layout = parent.layout;
405
+ const props = parent.props;
406
+ if (parent.type.toLowerCase() === "stack") {
407
+ layoutStackChildren(ctx, parent);
408
+ return;
409
+ }
410
+ const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
411
+ const justifyContent = props.justifyContent || "flex-start";
412
+ const alignItems = props.alignItems || "flex-start";
413
+ const gap = parseFloat(props.gap) || 0;
414
+ const isColumn = flexDirection === "column";
415
+ const availableWidth = layout.contentWidth;
416
+ const availableHeight = layout.contentHeight;
417
+ const childInfo = [];
418
+ let totalMainSize = 0;
419
+ let totalFlexGrow = 0;
420
+ let totalFlexShrink = 0;
421
+ for (const child of parent.children) {
422
+ const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
423
+ const flexShrink = parseFloat(child.props.flexShrink) || 1;
424
+ const flexBasis = parseSize(child.props.flexBasis);
425
+ computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
426
+ const childLayout = child.layout;
427
+ let mainSize = isColumn ? childLayout.height : childLayout.width;
428
+ if (flexBasis !== null) {
429
+ mainSize = flexBasis;
430
+ if (isColumn) {
431
+ childLayout.height = flexBasis;
432
+ } else {
433
+ childLayout.width = flexBasis;
434
+ }
435
+ }
436
+ childInfo.push({
437
+ width: childLayout.width,
438
+ height: childLayout.height,
439
+ flexGrow,
440
+ flexShrink,
441
+ flexBasis
442
+ });
443
+ totalMainSize += mainSize;
444
+ totalFlexGrow += flexGrow;
445
+ totalFlexShrink += flexShrink;
446
+ }
447
+ const totalGap = gap * (parent.children.length - 1);
448
+ totalMainSize += totalGap;
449
+ const availableMain = isColumn ? availableHeight : availableWidth;
450
+ let remainingSpace = availableMain - totalMainSize;
451
+ if (remainingSpace > 0 && totalFlexGrow > 0) {
452
+ const spacePerFlex = remainingSpace / totalFlexGrow;
453
+ for (let i = 0;i < parent.children.length; i++) {
454
+ const info = childInfo[i];
455
+ if (info.flexGrow > 0) {
456
+ const extraSpace = spacePerFlex * info.flexGrow;
457
+ if (isColumn) {
458
+ info.height += extraSpace;
459
+ } else {
460
+ info.width += extraSpace;
461
+ }
462
+ totalMainSize += extraSpace;
463
+ }
464
+ }
465
+ remainingSpace = 0;
466
+ }
467
+ if (remainingSpace < 0 && totalFlexShrink > 0) {
468
+ const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
469
+ for (let i = 0;i < parent.children.length; i++) {
470
+ const info = childInfo[i];
471
+ if (info.flexShrink > 0) {
472
+ const shrinkSpace = Math.min(shrinkPerFlex * info.flexShrink, isColumn ? info.height : info.width);
473
+ if (isColumn) {
474
+ info.height = Math.max(0, info.height - shrinkSpace);
475
+ } else {
476
+ info.width = Math.max(0, info.width - shrinkSpace);
477
+ }
478
+ totalMainSize -= shrinkSpace;
479
+ }
480
+ }
481
+ remainingSpace = availableMain - totalMainSize;
482
+ }
483
+ let mainStart = 0;
484
+ let spacing = 0;
485
+ if (justifyContent === "center") {
486
+ mainStart = Math.max(0, remainingSpace / 2);
487
+ } else if (justifyContent === "flex-end") {
488
+ mainStart = Math.max(0, remainingSpace);
489
+ } else if (justifyContent === "space-between") {
490
+ spacing = remainingSpace / Math.max(1, parent.children.length - 1);
491
+ } else if (justifyContent === "space-around") {
492
+ spacing = remainingSpace / parent.children.length;
493
+ mainStart = spacing / 2;
494
+ }
495
+ let currentMain = mainStart;
496
+ for (let i = 0;i < parent.children.length; i++) {
497
+ const child = parent.children[i];
498
+ const childLayout = child.layout;
499
+ const info = childInfo[i];
500
+ childLayout.width = info.width;
501
+ childLayout.height = info.height;
502
+ let crossStart = 0;
503
+ const availableCross = isColumn ? availableWidth : availableHeight;
504
+ const childCross = isColumn ? info.width : info.height;
505
+ if (alignItems === "center") {
506
+ crossStart = (availableCross - childCross) / 2;
507
+ } else if (alignItems === "flex-end") {
508
+ crossStart = availableCross - childCross;
509
+ }
510
+ if (isColumn) {
511
+ childLayout.x = layout.x + layout.contentX + crossStart;
512
+ childLayout.y = layout.y + layout.contentY + currentMain;
513
+ currentMain += info.height + gap;
514
+ } else {
515
+ childLayout.x = layout.x + layout.contentX + currentMain;
516
+ childLayout.y = layout.y + layout.contentY + crossStart;
517
+ currentMain += info.width + gap;
518
+ }
519
+ if (justifyContent === "space-between" || justifyContent === "space-around") {
520
+ currentMain += spacing;
521
+ }
522
+ }
523
+ }
524
+ function layoutStackChildren(ctx, parent) {
525
+ const layout = parent.layout;
526
+ const props = parent.props;
527
+ const alignItems = props.alignItems || "flex-start";
528
+ const justifyContent = props.justifyContent || "flex-start";
529
+ const availableWidth = layout.contentWidth;
530
+ const availableHeight = layout.contentHeight;
531
+ for (const child of parent.children) {
532
+ computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
533
+ const childLayout = child.layout;
534
+ let x = 0;
535
+ let y = 0;
536
+ if (alignItems === "center") {
537
+ x = (availableWidth - childLayout.width) / 2;
538
+ } else if (alignItems === "flex-end") {
539
+ x = availableWidth - childLayout.width;
540
+ }
541
+ if (justifyContent === "center") {
542
+ y = (availableHeight - childLayout.height) / 2;
543
+ } else if (justifyContent === "flex-end") {
544
+ y = availableHeight - childLayout.height;
545
+ }
546
+ childLayout.x = layout.x + layout.contentX + x;
547
+ childLayout.y = layout.y + layout.contentY + y;
548
+ }
549
+ }
550
+
551
+ // src/canvas/paint.ts
552
+ var customPainters = new Map;
553
+ function registerPainter(type, painter) {
554
+ customPainters.set(type.toLowerCase(), painter);
555
+ }
556
+ function paintNode(ctx, node) {
557
+ if (!node.visible || !node.layout)
558
+ return;
559
+ ctx.save();
560
+ applyTransforms(ctx, node);
561
+ if (node.opacity < 1) {
562
+ ctx.globalAlpha = node.opacity;
563
+ }
564
+ const customPainter = customPainters.get(node.type.toLowerCase());
565
+ if (customPainter) {
566
+ customPainter(ctx, node);
567
+ ctx.restore();
568
+ return;
569
+ }
570
+ switch (node.type.toLowerCase()) {
571
+ case "column":
572
+ case "row":
573
+ case "stack":
574
+ paintContainer(ctx, node);
575
+ break;
576
+ case "text":
577
+ paintText(ctx, node);
578
+ break;
579
+ case "button":
580
+ paintButton(ctx, node);
581
+ break;
582
+ case "input":
583
+ paintInput(ctx, node);
584
+ break;
585
+ case "image":
586
+ paintImage(ctx, node);
587
+ break;
588
+ case "spacer":
589
+ break;
590
+ case "divider":
591
+ case "separator":
592
+ paintDivider(ctx, node);
593
+ break;
594
+ case "checkbox":
595
+ paintCheckbox(ctx, node);
596
+ break;
597
+ case "radio":
598
+ paintRadio(ctx, node);
599
+ break;
600
+ case "switch":
601
+ case "toggle":
602
+ paintSwitch(ctx, node);
603
+ break;
604
+ case "slider":
605
+ paintSlider(ctx, node);
606
+ break;
607
+ case "progress":
608
+ case "progressbar":
609
+ paintProgress(ctx, node);
610
+ break;
611
+ case "spinner":
612
+ case "loading":
613
+ paintSpinner(ctx, node);
614
+ break;
615
+ case "card":
616
+ paintCard(ctx, node);
617
+ break;
618
+ case "badge":
619
+ paintBadge(ctx, node);
620
+ break;
621
+ case "avatar":
622
+ paintAvatar(ctx, node);
623
+ break;
624
+ case "icon":
625
+ paintIcon(ctx, node);
626
+ break;
627
+ case "link":
628
+ paintLink(ctx, node);
629
+ break;
630
+ case "container":
631
+ case "box":
632
+ paintContainer(ctx, node);
633
+ break;
634
+ default:
635
+ paintContainer(ctx, node);
636
+ }
637
+ ctx.restore();
638
+ for (const child of node.children) {
639
+ paintNode(ctx, child);
640
+ }
641
+ if (node._needsRestore) {
642
+ ctx.restore();
643
+ delete node._needsRestore;
644
+ }
645
+ }
646
+ function paintContainer(ctx, node) {
647
+ const layout = node.layout;
648
+ const props = node.props;
649
+ const x = layout.x;
650
+ const y = layout.y;
651
+ const width = layout.width;
652
+ const height = layout.height;
653
+ const radius = layout.border.radius;
654
+ const shadow = props.shadow || props.boxShadow;
655
+ if (shadow) {
656
+ applyShadow(ctx, shadow);
657
+ }
658
+ const backgroundColor = props.backgroundColor || props.background;
659
+ if (backgroundColor) {
660
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
661
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
662
+ } else {
663
+ ctx.fillStyle = backgroundColor;
664
+ }
665
+ if (radius > 0) {
666
+ drawRoundedRect(ctx, x, y, width, height, radius);
667
+ ctx.fill();
668
+ } else {
669
+ ctx.fillRect(x, y, width, height);
670
+ }
671
+ }
672
+ if (shadow) {
673
+ ctx.shadowColor = "transparent";
674
+ ctx.shadowBlur = 0;
675
+ ctx.shadowOffsetX = 0;
676
+ ctx.shadowOffsetY = 0;
677
+ }
678
+ if (layout.border.width > 0 && layout.border.color !== "transparent") {
679
+ ctx.strokeStyle = layout.border.color;
680
+ ctx.lineWidth = layout.border.width;
681
+ if (radius > 0) {
682
+ drawRoundedRect(ctx, x, y, width, height, radius);
683
+ ctx.stroke();
684
+ } else {
685
+ ctx.strokeRect(x, y, width, height);
686
+ }
687
+ }
688
+ const overflow = props.overflow || "visible";
689
+ if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
690
+ ctx.save();
691
+ ctx.beginPath();
692
+ if (radius > 0) {
693
+ drawRoundedRect(ctx, x, y, width, height, radius);
694
+ } else {
695
+ ctx.rect(x, y, width, height);
696
+ }
697
+ ctx.clip();
698
+ node._needsRestore = true;
699
+ }
700
+ }
701
+ function paintText(ctx, node) {
702
+ const layout = node.layout;
703
+ const props = node.props;
704
+ let text = String(props[0] || props.text || "");
705
+ const color = props.color || "#000000";
706
+ const fontSize = parseFloat(props.fontSize) || 16;
707
+ const fontWeight = props.fontWeight || "normal";
708
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
709
+ const textAlign = props.textAlign || "left";
710
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
711
+ const textDecoration = props.textDecoration || "none";
712
+ const textTransform = props.textTransform || "none";
713
+ const letterSpacing = parseFloat(props.letterSpacing) || 0;
714
+ if (textTransform === "uppercase") {
715
+ text = text.toUpperCase();
716
+ } else if (textTransform === "lowercase") {
717
+ text = text.toLowerCase();
718
+ } else if (textTransform === "capitalize") {
719
+ text = text.replace(/\b\w/g, (char) => char.toUpperCase());
720
+ }
721
+ const textShadow = props.textShadow || props.shadow;
722
+ if (textShadow) {
723
+ applyShadow(ctx, textShadow);
724
+ }
725
+ const x = layout.x + layout.contentX;
726
+ const y = layout.y + layout.contentY;
727
+ if (letterSpacing !== 0) {
728
+ ctx.fillStyle = color;
729
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
730
+ ctx.textAlign = "left";
731
+ ctx.textBaseline = "top";
732
+ let currentX = x;
733
+ for (let i = 0;i < text.length; i++) {
734
+ ctx.fillText(text[i], currentX, y);
735
+ currentX += ctx.measureText(text[i]).width;
736
+ if (i < text.length - 1) {
737
+ currentX += letterSpacing;
738
+ }
739
+ }
740
+ if (textDecoration !== "none") {
741
+ applyTextDecoration(ctx, textDecoration, color, x, y, currentX - x, fontSize);
742
+ }
743
+ } else {
744
+ renderText(ctx, text, x, y, layout.contentWidth, layout.contentHeight, {
745
+ color,
746
+ fontSize,
747
+ fontWeight,
748
+ fontFamily,
749
+ textAlign,
750
+ verticalAlign: "top",
751
+ lineHeight
752
+ });
753
+ if (textDecoration !== "none") {
754
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
755
+ const textWidth = ctx.measureText(text).width;
756
+ applyTextDecoration(ctx, textDecoration, color, x, y, textWidth, fontSize);
757
+ }
758
+ }
759
+ if (textShadow) {
760
+ ctx.shadowColor = "transparent";
761
+ ctx.shadowBlur = 0;
762
+ ctx.shadowOffsetX = 0;
763
+ ctx.shadowOffsetY = 0;
764
+ }
765
+ }
766
+ function paintButton(ctx, node) {
767
+ const layout = node.layout;
768
+ const props = node.props;
769
+ const x = layout.x;
770
+ const y = layout.y;
771
+ const width = layout.width;
772
+ const height = layout.height;
773
+ const radius = layout.border.radius || 4;
774
+ const shadow = props.shadow || props.boxShadow;
775
+ if (shadow) {
776
+ applyShadow(ctx, shadow);
777
+ }
778
+ let backgroundColor = props.backgroundColor || "#007bff";
779
+ if (node.hovered) {
780
+ backgroundColor = props.hoverColor || "#0056b3";
781
+ }
782
+ if (node.focused) {
783
+ backgroundColor = props.focusColor || "#004085";
784
+ }
785
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
786
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
787
+ } else {
788
+ ctx.fillStyle = backgroundColor;
789
+ }
790
+ drawRoundedRect(ctx, x, y, width, height, radius);
791
+ ctx.fill();
792
+ if (shadow) {
793
+ ctx.shadowColor = "transparent";
794
+ ctx.shadowBlur = 0;
795
+ ctx.shadowOffsetX = 0;
796
+ ctx.shadowOffsetY = 0;
797
+ }
798
+ if (layout.border.width > 0) {
799
+ ctx.strokeStyle = layout.border.color;
800
+ ctx.lineWidth = layout.border.width;
801
+ drawRoundedRect(ctx, x, y, width, height, radius);
802
+ ctx.stroke();
803
+ }
804
+ for (const child of node.children) {
805
+ paintNode(ctx, child);
806
+ }
807
+ }
808
+ function paintInput(ctx, node) {
809
+ const layout = node.layout;
810
+ const props = node.props;
811
+ const x = layout.x;
812
+ const y = layout.y;
813
+ const width = layout.width;
814
+ const height = layout.height;
815
+ const radius = layout.border.radius || 4;
816
+ ctx.fillStyle = props.backgroundColor || "#ffffff";
817
+ drawRoundedRect(ctx, x, y, width, height, radius);
818
+ ctx.fill();
819
+ const borderColor = node.focused ? "#007bff" : layout.border.color || "#cccccc";
820
+ const borderWidth = node.focused ? 2 : layout.border.width || 1;
821
+ ctx.strokeStyle = borderColor;
822
+ ctx.lineWidth = borderWidth;
823
+ drawRoundedRect(ctx, x, y, width, height, radius);
824
+ ctx.stroke();
825
+ const value = props.value || "";
826
+ const placeholder = props.placeholder || "";
827
+ const text = value || placeholder;
828
+ const textColor = value ? props.color || "#000000" : "#999999";
829
+ if (text) {
830
+ const fontSize = parseFloat(props.fontSize) || 16;
831
+ const fontWeight = props.fontWeight || "normal";
832
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
833
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
834
+ renderText(ctx, text, layout.x + layout.contentX, layout.y + layout.contentY, layout.contentWidth, layout.contentHeight, {
835
+ color: textColor,
836
+ fontSize,
837
+ fontWeight,
838
+ fontFamily,
839
+ textAlign: "left",
840
+ verticalAlign: "middle",
841
+ lineHeight
842
+ });
843
+ }
844
+ }
845
+ function paintImage(ctx, node) {
846
+ const layout = node.layout;
847
+ const props = node.props;
848
+ const src = props.src || props[0];
849
+ if (!src)
850
+ return;
851
+ const x = layout.x;
852
+ const y = layout.y;
853
+ const width = layout.width;
854
+ const height = layout.height;
855
+ ctx.fillStyle = "#e0e0e0";
856
+ ctx.fillRect(x, y, width, height);
857
+ ctx.strokeStyle = "#999999";
858
+ ctx.lineWidth = 1;
859
+ ctx.strokeRect(x, y, width, height);
860
+ ctx.fillStyle = "#666666";
861
+ ctx.font = "14px sans-serif";
862
+ ctx.textAlign = "center";
863
+ ctx.textBaseline = "middle";
864
+ ctx.fillText("IMG", x + width / 2, y + height / 2);
865
+ }
866
+ function drawRoundedRect(ctx, x, y, width, height, radius) {
867
+ if (radius <= 0) {
868
+ ctx.rect(x, y, width, height);
869
+ return;
870
+ }
871
+ ctx.beginPath();
872
+ ctx.moveTo(x + radius, y);
873
+ ctx.lineTo(x + width - radius, y);
874
+ ctx.arcTo(x + width, y, x + width, y + radius, radius);
875
+ ctx.lineTo(x + width, y + height - radius);
876
+ ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
877
+ ctx.lineTo(x + radius, y + height);
878
+ ctx.arcTo(x, y + height, x, y + height - radius, radius);
879
+ ctx.lineTo(x, y + radius);
880
+ ctx.arcTo(x, y, x + radius, y, radius);
881
+ ctx.closePath();
882
+ }
883
+ function applyShadow(ctx, shadow) {
884
+ if (typeof shadow === "string") {
885
+ const parts = shadow.trim().split(/\s+/);
886
+ if (parts.length >= 3) {
887
+ const offsetX = parseFloat(parts[0]);
888
+ const offsetY = parseFloat(parts[1]);
889
+ const blur = parseFloat(parts[2]);
890
+ const color = parts.slice(3).join(" ") || "rgba(0,0,0,0.3)";
891
+ ctx.shadowOffsetX = offsetX;
892
+ ctx.shadowOffsetY = offsetY;
893
+ ctx.shadowBlur = blur;
894
+ ctx.shadowColor = color;
895
+ }
896
+ } else if (typeof shadow === "object") {
897
+ ctx.shadowOffsetX = shadow.offsetX || 0;
898
+ ctx.shadowOffsetY = shadow.offsetY || 0;
899
+ ctx.shadowBlur = shadow.blur || 0;
900
+ ctx.shadowColor = shadow.color || "rgba(0,0,0,0.3)";
901
+ }
902
+ }
903
+ function parseGradient(ctx, gradientStr, x, y, width, height) {
904
+ if (gradientStr.startsWith("linear-gradient")) {
905
+ const match = gradientStr.match(/linear-gradient\((.*)\)/);
906
+ if (!match)
907
+ return gradientStr;
908
+ const parts = match[1].split(",").map((s) => s.trim());
909
+ let x0 = x, y0 = y, x1 = x, y1 = y + height;
910
+ let colorStart = 0;
911
+ if (parts[0].includes("deg") || parts[0].includes("to ")) {
912
+ colorStart = 1;
913
+ const direction = parts[0];
914
+ if (direction.includes("to right") || direction === "90deg") {
915
+ x1 = x + width;
916
+ y1 = y;
917
+ } else if (direction.includes("to left") || direction === "270deg") {
918
+ x0 = x + width;
919
+ x1 = x;
920
+ y0 = y;
921
+ y1 = y;
922
+ } else if (direction.includes("to top") || direction === "0deg") {
923
+ y0 = y + height;
924
+ y1 = y;
925
+ }
926
+ }
927
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
928
+ const colors = parts.slice(colorStart);
929
+ colors.forEach((color, i) => {
930
+ const stop = i / (colors.length - 1);
931
+ gradient.addColorStop(stop, color.trim());
932
+ });
933
+ return gradient;
934
+ } else if (gradientStr.startsWith("radial-gradient")) {
935
+ const match = gradientStr.match(/radial-gradient\((.*)\)/);
936
+ if (!match)
937
+ return gradientStr;
938
+ const parts = match[1].split(",").map((s) => s.trim());
939
+ const centerX = x + width / 2;
940
+ const centerY = y + height / 2;
941
+ const radius = Math.max(width, height) / 2;
942
+ const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
943
+ parts.forEach((color, i) => {
944
+ const stop = i / (parts.length - 1);
945
+ gradient.addColorStop(stop, color.trim());
946
+ });
947
+ return gradient;
948
+ }
949
+ return gradientStr;
950
+ }
951
+ function applyTransforms(ctx, node) {
952
+ const props = node.props;
953
+ const layout = node.layout;
954
+ const originX = parseFloat(props.transformOriginX) || 0.5;
955
+ const originY = parseFloat(props.transformOriginY) || 0.5;
956
+ const centerX = layout.x + layout.width * originX;
957
+ const centerY = layout.y + layout.height * originY;
958
+ const translateX = parseFloat(props.translateX) || 0;
959
+ const translateY = parseFloat(props.translateY) || 0;
960
+ const rotate = parseFloat(props.rotate) || 0;
961
+ const scaleX = parseFloat(props.scaleX) || parseFloat(props.scale) || 1;
962
+ const scaleY = parseFloat(props.scaleY) || parseFloat(props.scale) || 1;
963
+ const skewX = parseFloat(props.skewX) || parseFloat(props.skew) || 0;
964
+ const skewY = parseFloat(props.skewY) || 0;
965
+ if (translateX !== 0 || translateY !== 0 || rotate !== 0 || scaleX !== 1 || scaleY !== 1 || skewX !== 0 || skewY !== 0) {
966
+ ctx.translate(centerX, centerY);
967
+ if (scaleX !== 1 || scaleY !== 1) {
968
+ ctx.scale(scaleX, scaleY);
969
+ }
970
+ if (rotate !== 0) {
971
+ ctx.rotate(rotate * Math.PI / 180);
972
+ }
973
+ if (skewX !== 0 || skewY !== 0) {
974
+ const skewXRad = skewX * Math.PI / 180;
975
+ const skewYRad = skewY * Math.PI / 180;
976
+ ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
977
+ }
978
+ ctx.translate(-centerX + translateX, -centerY + translateY);
979
+ }
980
+ if (props.transform && typeof props.transform === "string") {
981
+ parseTransformString(ctx, props.transform, centerX, centerY);
982
+ }
983
+ }
984
+ function parseTransformString(ctx, transformStr, originX, originY) {
985
+ const transforms = transformStr.match(/(\w+)\(([^)]+)\)/g);
986
+ if (!transforms)
987
+ return;
988
+ ctx.translate(originX, originY);
989
+ for (const transform of transforms) {
990
+ const match = transform.match(/(\w+)\(([^)]+)\)/);
991
+ if (!match)
992
+ continue;
993
+ const [, func, args] = match;
994
+ const values = args.split(",").map((v) => parseFloat(v.trim()));
995
+ switch (func.toLowerCase()) {
996
+ case "translate":
997
+ ctx.translate(values[0] || 0, values[1] || 0);
998
+ break;
999
+ case "rotate":
1000
+ ctx.rotate(values[0] * Math.PI / 180);
1001
+ break;
1002
+ case "scale":
1003
+ ctx.scale(values[0] || 1, values[1] || values[0] || 1);
1004
+ break;
1005
+ }
1006
+ }
1007
+ ctx.translate(-originX, -originY);
1008
+ }
1009
+ function paintDivider(ctx, node) {
1010
+ const layout = node.layout;
1011
+ const props = node.props;
1012
+ const orientation = props.orientation || "horizontal";
1013
+ const color = props.color || props.backgroundColor || "#e0e0e0";
1014
+ const thickness = parseFloat(props.thickness) || 1;
1015
+ ctx.strokeStyle = color;
1016
+ ctx.lineWidth = thickness;
1017
+ ctx.beginPath();
1018
+ if (orientation === "vertical") {
1019
+ const x = layout.x + layout.width / 2;
1020
+ ctx.moveTo(x, layout.y);
1021
+ ctx.lineTo(x, layout.y + layout.height);
1022
+ } else {
1023
+ const y = layout.y + layout.height / 2;
1024
+ ctx.moveTo(layout.x, y);
1025
+ ctx.lineTo(layout.x + layout.width, y);
1026
+ }
1027
+ ctx.stroke();
1028
+ }
1029
+ function paintCheckbox(ctx, node) {
1030
+ const layout = node.layout;
1031
+ const props = node.props;
1032
+ const size = Math.min(layout.width, layout.height);
1033
+ const x = layout.x;
1034
+ const y = layout.y;
1035
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
1036
+ const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1037
+ const radius = parseFloat(props.borderRadius) || 2;
1038
+ const bgColor = checked ? props.checkedColor || "#007bff" : props.backgroundColor || "#ffffff";
1039
+ ctx.fillStyle = bgColor;
1040
+ drawRoundedRect(ctx, x, y, size, size, radius);
1041
+ ctx.fill();
1042
+ const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
1043
+ ctx.strokeStyle = borderColor;
1044
+ ctx.lineWidth = node.focused ? 2 : 1;
1045
+ drawRoundedRect(ctx, x, y, size, size, radius);
1046
+ ctx.stroke();
1047
+ if (checked) {
1048
+ ctx.strokeStyle = props.checkColor || "#ffffff";
1049
+ ctx.lineWidth = 2;
1050
+ ctx.lineCap = "round";
1051
+ ctx.lineJoin = "round";
1052
+ const padding = size * 0.25;
1053
+ ctx.beginPath();
1054
+ ctx.moveTo(x + padding, y + size / 2);
1055
+ ctx.lineTo(x + size * 0.4, y + size - padding);
1056
+ ctx.lineTo(x + size - padding, y + padding);
1057
+ ctx.stroke();
1058
+ }
1059
+ }
1060
+ function paintRadio(ctx, node) {
1061
+ const layout = node.layout;
1062
+ const props = node.props;
1063
+ const size = Math.min(layout.width, layout.height);
1064
+ const centerX = layout.x + size / 2;
1065
+ const centerY = layout.y + size / 2;
1066
+ const radius = size / 2;
1067
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
1068
+ const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1069
+ const bgColor = props.backgroundColor || "#ffffff";
1070
+ ctx.fillStyle = bgColor;
1071
+ ctx.beginPath();
1072
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1073
+ ctx.fill();
1074
+ const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
1075
+ ctx.strokeStyle = borderColor;
1076
+ ctx.lineWidth = node.focused ? 2 : 1;
1077
+ ctx.beginPath();
1078
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1079
+ ctx.stroke();
1080
+ if (checked) {
1081
+ ctx.fillStyle = props.checkedColor || "#007bff";
1082
+ ctx.beginPath();
1083
+ ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
1084
+ ctx.fill();
1085
+ }
1086
+ }
1087
+ function paintSwitch(ctx, node) {
1088
+ const layout = node.layout;
1089
+ const props = node.props;
1090
+ const width = layout.width;
1091
+ const height = layout.height;
1092
+ const x = layout.x;
1093
+ const y = layout.y;
1094
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
1095
+ const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1096
+ const radius = height / 2;
1097
+ const trackColor = checked ? props.checkedColor || "#4caf50" : props.backgroundColor || "#cccccc";
1098
+ ctx.fillStyle = trackColor;
1099
+ drawRoundedRect(ctx, x, y, width, height, radius);
1100
+ ctx.fill();
1101
+ const thumbRadius = radius * 0.8;
1102
+ const thumbX = checked ? x + width - radius : x + radius;
1103
+ const thumbY = y + radius;
1104
+ ctx.fillStyle = props.thumbColor || "#ffffff";
1105
+ ctx.beginPath();
1106
+ ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
1107
+ ctx.fill();
1108
+ if (props.shadow !== false) {
1109
+ ctx.shadowColor = "rgba(0,0,0,0.2)";
1110
+ ctx.shadowBlur = 2;
1111
+ ctx.shadowOffsetY = 1;
1112
+ ctx.beginPath();
1113
+ ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
1114
+ ctx.fill();
1115
+ ctx.shadowColor = "transparent";
1116
+ ctx.shadowBlur = 0;
1117
+ ctx.shadowOffsetY = 0;
1118
+ }
1119
+ }
1120
+ function paintSlider(ctx, node) {
1121
+ const layout = node.layout;
1122
+ const props = node.props;
1123
+ const width = layout.width;
1124
+ const height = layout.height;
1125
+ const x = layout.x;
1126
+ const y = layout.y;
1127
+ const min = parseFloat(props.min) || 0;
1128
+ const max = parseFloat(props.max) || 100;
1129
+ const value = parseFloat(props.value) || min;
1130
+ const percentage = (value - min) / (max - min);
1131
+ const trackHeight = parseFloat(props.trackHeight) || 4;
1132
+ const thumbSize = parseFloat(props.thumbSize) || 16;
1133
+ const trackY = y + (height - trackHeight) / 2;
1134
+ ctx.fillStyle = props.trackColor || "#e0e0e0";
1135
+ drawRoundedRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
1136
+ ctx.fill();
1137
+ const fillWidth = width * percentage;
1138
+ const fillColor = props.fillColor || props.color || "#007bff";
1139
+ if (typeof fillColor === "string" && fillColor.includes("gradient")) {
1140
+ ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
1141
+ } else {
1142
+ ctx.fillStyle = fillColor;
1143
+ }
1144
+ drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
1145
+ ctx.fill();
1146
+ const thumbX = x + fillWidth;
1147
+ const thumbY = y + height / 2;
1148
+ ctx.fillStyle = props.thumbColor || "#007bff";
1149
+ ctx.beginPath();
1150
+ ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
1151
+ ctx.fill();
1152
+ ctx.strokeStyle = "#ffffff";
1153
+ ctx.lineWidth = 2;
1154
+ ctx.beginPath();
1155
+ ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
1156
+ ctx.stroke();
1157
+ }
1158
+ function paintProgress(ctx, node) {
1159
+ const layout = node.layout;
1160
+ const props = node.props;
1161
+ const width = layout.width;
1162
+ const height = layout.height;
1163
+ const x = layout.x;
1164
+ const y = layout.y;
1165
+ const min = parseFloat(props.min) || 0;
1166
+ const max = parseFloat(props.max) || 100;
1167
+ const value = parseFloat(props.value) || 0;
1168
+ const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
1169
+ const radius = layout.border.radius || height / 2;
1170
+ ctx.fillStyle = props.backgroundColor || "#e0e0e0";
1171
+ drawRoundedRect(ctx, x, y, width, height, radius);
1172
+ ctx.fill();
1173
+ if (percentage > 0) {
1174
+ const fillWidth = width * percentage;
1175
+ const fillColor = props.fillColor || props.color || "#007bff";
1176
+ if (typeof fillColor === "string" && fillColor.includes("gradient")) {
1177
+ ctx.fillStyle = parseGradient(ctx, fillColor, x, y, fillWidth, height);
1178
+ } else {
1179
+ ctx.fillStyle = fillColor;
1180
+ }
1181
+ drawRoundedRect(ctx, x, y, fillWidth, height, radius);
1182
+ ctx.fill();
1183
+ }
1184
+ if (props.showLabel) {
1185
+ const label = props.label || `${Math.round(percentage * 100)}%`;
1186
+ ctx.fillStyle = props.labelColor || "#ffffff";
1187
+ ctx.font = `${props.fontSize || 12}px ${props.fontFamily || "sans-serif"}`;
1188
+ ctx.textAlign = "center";
1189
+ ctx.textBaseline = "middle";
1190
+ ctx.fillText(label, x + width / 2, y + height / 2);
1191
+ }
1192
+ }
1193
+ function paintSpinner(ctx, node) {
1194
+ const layout = node.layout;
1195
+ const props = node.props;
1196
+ const size = Math.min(layout.width, layout.height);
1197
+ const centerX = layout.x + size / 2;
1198
+ const centerY = layout.y + size / 2;
1199
+ const radius = size / 2 - 4;
1200
+ const thickness = parseFloat(props.thickness) || 4;
1201
+ const color = props.color || "#007bff";
1202
+ const rotation = Date.now() / 1000 * Math.PI;
1203
+ ctx.strokeStyle = color;
1204
+ ctx.lineWidth = thickness;
1205
+ ctx.lineCap = "round";
1206
+ ctx.beginPath();
1207
+ ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
1208
+ ctx.stroke();
1209
+ ctx.globalAlpha = 0.3;
1210
+ ctx.beginPath();
1211
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1212
+ ctx.stroke();
1213
+ ctx.globalAlpha = 1;
1214
+ }
1215
+ function paintCard(ctx, node) {
1216
+ const layout = node.layout;
1217
+ const props = node.props;
1218
+ const x = layout.x;
1219
+ const y = layout.y;
1220
+ const width = layout.width;
1221
+ const height = layout.height;
1222
+ const radius = layout.border.radius || 8;
1223
+ const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
1224
+ applyShadow(ctx, shadow);
1225
+ const backgroundColor = props.backgroundColor || "#ffffff";
1226
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
1227
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
1228
+ } else {
1229
+ ctx.fillStyle = backgroundColor;
1230
+ }
1231
+ drawRoundedRect(ctx, x, y, width, height, radius);
1232
+ ctx.fill();
1233
+ ctx.shadowColor = "transparent";
1234
+ ctx.shadowBlur = 0;
1235
+ ctx.shadowOffsetX = 0;
1236
+ ctx.shadowOffsetY = 0;
1237
+ if (layout.border.width > 0) {
1238
+ ctx.strokeStyle = layout.border.color;
1239
+ ctx.lineWidth = layout.border.width;
1240
+ drawRoundedRect(ctx, x, y, width, height, radius);
1241
+ ctx.stroke();
1242
+ }
1243
+ for (const child of node.children) {
1244
+ paintNode(ctx, child);
1245
+ }
1246
+ }
1247
+ function paintBadge(ctx, node) {
1248
+ const layout = node.layout;
1249
+ const props = node.props;
1250
+ const x = layout.x;
1251
+ const y = layout.y;
1252
+ const width = layout.width;
1253
+ const height = layout.height;
1254
+ const radius = layout.border.radius || height / 2;
1255
+ const backgroundColor = props.backgroundColor || "#dc3545";
1256
+ ctx.fillStyle = backgroundColor;
1257
+ drawRoundedRect(ctx, x, y, width, height, radius);
1258
+ ctx.fill();
1259
+ const text = String(props[0] || props.text || "");
1260
+ if (text) {
1261
+ ctx.fillStyle = props.color || "#ffffff";
1262
+ ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
1263
+ ctx.textAlign = "center";
1264
+ ctx.textBaseline = "middle";
1265
+ ctx.fillText(text, x + width / 2, y + height / 2);
1266
+ }
1267
+ }
1268
+ function paintAvatar(ctx, node) {
1269
+ const layout = node.layout;
1270
+ const props = node.props;
1271
+ const size = Math.min(layout.width, layout.height);
1272
+ const centerX = layout.x + size / 2;
1273
+ const centerY = layout.y + size / 2;
1274
+ const radius = size / 2;
1275
+ ctx.save();
1276
+ ctx.beginPath();
1277
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1278
+ ctx.clip();
1279
+ const backgroundColor = props.backgroundColor || "#cccccc";
1280
+ ctx.fillStyle = backgroundColor;
1281
+ ctx.beginPath();
1282
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1283
+ ctx.fill();
1284
+ const text = String(props[0] || props.text || props.initials || "");
1285
+ if (text) {
1286
+ ctx.fillStyle = props.color || "#ffffff";
1287
+ ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
1288
+ ctx.textAlign = "center";
1289
+ ctx.textBaseline = "middle";
1290
+ ctx.fillText(text, centerX, centerY);
1291
+ }
1292
+ ctx.restore();
1293
+ if (layout.border.width > 0) {
1294
+ ctx.strokeStyle = layout.border.color;
1295
+ ctx.lineWidth = layout.border.width;
1296
+ ctx.beginPath();
1297
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1298
+ ctx.stroke();
1299
+ }
1300
+ }
1301
+ function paintIcon(ctx, node) {
1302
+ const layout = node.layout;
1303
+ const props = node.props;
1304
+ const size = Math.min(layout.width, layout.height);
1305
+ const x = layout.x;
1306
+ const y = layout.y;
1307
+ const color = props.color || "#000000";
1308
+ const iconName = props.icon || props.name || "circle";
1309
+ ctx.fillStyle = color;
1310
+ ctx.strokeStyle = color;
1311
+ ctx.lineWidth = 2;
1312
+ switch (iconName.toLowerCase()) {
1313
+ case "circle":
1314
+ ctx.beginPath();
1315
+ ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1316
+ ctx.fill();
1317
+ break;
1318
+ case "square":
1319
+ const padding = size * 0.2;
1320
+ ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
1321
+ break;
1322
+ case "star":
1323
+ drawStar(ctx, x + size / 2, y + size / 2, 5, size / 3, size / 6);
1324
+ ctx.fill();
1325
+ break;
1326
+ case "check":
1327
+ case "checkmark":
1328
+ const p = size * 0.2;
1329
+ ctx.beginPath();
1330
+ ctx.moveTo(x + p, y + size / 2);
1331
+ ctx.lineTo(x + size * 0.4, y + size - p);
1332
+ ctx.lineTo(x + size - p, y + p);
1333
+ ctx.stroke();
1334
+ break;
1335
+ case "x":
1336
+ case "close":
1337
+ const pd = size * 0.2;
1338
+ ctx.beginPath();
1339
+ ctx.moveTo(x + pd, y + pd);
1340
+ ctx.lineTo(x + size - pd, y + size - pd);
1341
+ ctx.moveTo(x + size - pd, y + pd);
1342
+ ctx.lineTo(x + pd, y + size - pd);
1343
+ ctx.stroke();
1344
+ break;
1345
+ default:
1346
+ ctx.beginPath();
1347
+ ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1348
+ ctx.fill();
1349
+ }
1350
+ }
1351
+ function paintLink(ctx, node) {
1352
+ const layout = node.layout;
1353
+ const props = node.props;
1354
+ const text = String(props[0] || props.text || "");
1355
+ const color = node.hovered ? props.hoverColor || "#0056b3" : props.color || "#007bff";
1356
+ const fontSize = parseFloat(props.fontSize) || 16;
1357
+ const fontWeight = props.fontWeight || "normal";
1358
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
1359
+ const textDecoration = props.textDecoration !== undefined ? props.textDecoration : "underline";
1360
+ ctx.fillStyle = color;
1361
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
1362
+ ctx.textAlign = "left";
1363
+ ctx.textBaseline = "top";
1364
+ const x = layout.x + layout.contentX;
1365
+ const y = layout.y + layout.contentY;
1366
+ ctx.fillText(text, x, y);
1367
+ if (textDecoration === "underline") {
1368
+ const textWidth = ctx.measureText(text).width;
1369
+ ctx.strokeStyle = color;
1370
+ ctx.lineWidth = 1;
1371
+ ctx.beginPath();
1372
+ ctx.moveTo(x, y + fontSize + 2);
1373
+ ctx.lineTo(x + textWidth, y + fontSize + 2);
1374
+ ctx.stroke();
1375
+ }
1376
+ }
1377
+ function applyTextDecoration(ctx, decoration, color, x, y, width, fontSize) {
1378
+ ctx.strokeStyle = color;
1379
+ ctx.lineWidth = Math.max(1, fontSize / 16);
1380
+ ctx.beginPath();
1381
+ if (decoration === "underline") {
1382
+ const underlineY = y + fontSize + 2;
1383
+ ctx.moveTo(x, underlineY);
1384
+ ctx.lineTo(x + width, underlineY);
1385
+ } else if (decoration === "line-through" || decoration === "strikethrough") {
1386
+ const lineThroughY = y + fontSize / 2;
1387
+ ctx.moveTo(x, lineThroughY);
1388
+ ctx.lineTo(x + width, lineThroughY);
1389
+ } else if (decoration === "overline") {
1390
+ ctx.moveTo(x, y);
1391
+ ctx.lineTo(x + width, y);
1392
+ }
1393
+ ctx.stroke();
1394
+ }
1395
+ function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
1396
+ let rot = Math.PI / 2 * 3;
1397
+ let x = cx;
1398
+ let y = cy;
1399
+ const step = Math.PI / spikes;
1400
+ ctx.beginPath();
1401
+ ctx.moveTo(cx, cy - outerRadius);
1402
+ for (let i = 0;i < spikes; i++) {
1403
+ x = cx + Math.cos(rot) * outerRadius;
1404
+ y = cy + Math.sin(rot) * outerRadius;
1405
+ ctx.lineTo(x, y);
1406
+ rot += step;
1407
+ x = cx + Math.cos(rot) * innerRadius;
1408
+ y = cy + Math.sin(rot) * innerRadius;
1409
+ ctx.lineTo(x, y);
1410
+ rot += step;
1411
+ }
1412
+ ctx.lineTo(cx, cy - outerRadius);
1413
+ ctx.closePath();
1414
+ }
1415
+
1416
+ // src/canvas/events.ts
1417
+ class CanvasEventManager {
1418
+ canvas;
1419
+ engine;
1420
+ rootNode = null;
1421
+ hoveredNode = null;
1422
+ focusedNode = null;
1423
+ mouseDownNode = null;
1424
+ constructor(canvas, engine) {
1425
+ this.canvas = canvas;
1426
+ this.engine = engine;
1427
+ this.setupEventListeners();
1428
+ }
1429
+ setRootNode(node) {
1430
+ this.rootNode = node;
1431
+ }
1432
+ setupEventListeners() {
1433
+ this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
1434
+ this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
1435
+ this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
1436
+ this.canvas.addEventListener("click", this.onClick.bind(this));
1437
+ this.canvas.addEventListener("dblclick", this.onDoubleClick.bind(this));
1438
+ this.canvas.addEventListener("keydown", this.onKeyDown.bind(this));
1439
+ this.canvas.addEventListener("keyup", this.onKeyUp.bind(this));
1440
+ }
1441
+ getCanvasCoordinates(e) {
1442
+ const rect = this.canvas.getBoundingClientRect();
1443
+ const scaleX = this.canvas.width / rect.width;
1444
+ const scaleY = this.canvas.height / rect.height;
1445
+ return {
1446
+ x: (e.clientX - rect.left) * scaleX,
1447
+ y: (e.clientY - rect.top) * scaleY
1448
+ };
1449
+ }
1450
+ hitTest(point) {
1451
+ if (!this.rootNode)
1452
+ return null;
1453
+ return this.hitTestNode(this.rootNode, point);
1454
+ }
1455
+ hitTestNode(node, point) {
1456
+ if (!node.visible || !node.layout)
1457
+ return null;
1458
+ const bounds = getAbsoluteBounds(node);
1459
+ if (!bounds)
1460
+ return null;
1461
+ for (let i = node.children.length - 1;i >= 0; i--) {
1462
+ const child = node.children[i];
1463
+ const hit = this.hitTestNode(child, point);
1464
+ if (hit)
1465
+ return hit;
1466
+ }
1467
+ const radius = node.layout.border.radius;
1468
+ if (isPointInRoundedRect(point, bounds, radius)) {
1469
+ return node;
1470
+ }
1471
+ return null;
1472
+ }
1473
+ onMouseMove(e) {
1474
+ const point = this.getCanvasCoordinates(e);
1475
+ const node = this.hitTest(point);
1476
+ if (node !== this.hoveredNode) {
1477
+ if (this.hoveredNode) {
1478
+ this.hoveredNode.hovered = false;
1479
+ this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
1480
+ }
1481
+ this.hoveredNode = node;
1482
+ if (node) {
1483
+ node.hovered = true;
1484
+ this.dispatchNodeEvent(node, "mouseenter", {});
1485
+ }
1486
+ this.updateCursor(node);
1487
+ this.requestRedraw();
1488
+ }
1489
+ }
1490
+ onMouseDown(e) {
1491
+ const point = this.getCanvasCoordinates(e);
1492
+ const node = this.hitTest(point);
1493
+ this.mouseDownNode = node;
1494
+ if (node && node.clickable) {
1495
+ this.dispatchNodeEvent(node, "mousedown", {
1496
+ button: e.button,
1497
+ clientX: e.clientX,
1498
+ clientY: e.clientY
1499
+ });
1500
+ }
1501
+ if (node && node.focusable) {
1502
+ this.setFocus(node);
1503
+ } else {
1504
+ this.setFocus(null);
1505
+ }
1506
+ }
1507
+ onMouseUp(e) {
1508
+ const point = this.getCanvasCoordinates(e);
1509
+ const node = this.hitTest(point);
1510
+ if (node && node.clickable) {
1511
+ this.dispatchNodeEvent(node, "mouseup", {
1512
+ button: e.button,
1513
+ clientX: e.clientX,
1514
+ clientY: e.clientY
1515
+ });
1516
+ }
1517
+ this.mouseDownNode = null;
1518
+ }
1519
+ onClick(e) {
1520
+ const point = this.getCanvasCoordinates(e);
1521
+ const node = this.hitTest(point);
1522
+ if (node && node.clickable && node === this.mouseDownNode) {
1523
+ this.dispatchNodeEvent(node, "click", {
1524
+ button: e.button,
1525
+ clientX: e.clientX,
1526
+ clientY: e.clientY
1527
+ });
1528
+ }
1529
+ }
1530
+ onDoubleClick(e) {
1531
+ const point = this.getCanvasCoordinates(e);
1532
+ const node = this.hitTest(point);
1533
+ if (node && node.clickable) {
1534
+ this.dispatchNodeEvent(node, "dblclick", {
1535
+ button: e.button,
1536
+ clientX: e.clientX,
1537
+ clientY: e.clientY
1538
+ });
1539
+ }
1540
+ }
1541
+ onKeyDown(e) {
1542
+ if (this.focusedNode) {
1543
+ this.dispatchNodeEvent(this.focusedNode, "keydown", {
1544
+ key: e.key,
1545
+ code: e.code,
1546
+ ctrlKey: e.ctrlKey,
1547
+ shiftKey: e.shiftKey,
1548
+ altKey: e.altKey
1549
+ });
1550
+ }
1551
+ }
1552
+ onKeyUp(e) {
1553
+ if (this.focusedNode) {
1554
+ this.dispatchNodeEvent(this.focusedNode, "keyup", {
1555
+ key: e.key,
1556
+ code: e.code,
1557
+ ctrlKey: e.ctrlKey,
1558
+ shiftKey: e.shiftKey,
1559
+ altKey: e.altKey
1560
+ });
1561
+ }
1562
+ }
1563
+ setFocus(node) {
1564
+ if (node === this.focusedNode)
1565
+ return;
1566
+ if (this.focusedNode) {
1567
+ this.focusedNode.focused = false;
1568
+ this.dispatchNodeEvent(this.focusedNode, "blur", {});
1569
+ }
1570
+ this.focusedNode = node;
1571
+ if (node) {
1572
+ node.focused = true;
1573
+ this.dispatchNodeEvent(node, "focus", {});
1574
+ }
1575
+ this.requestRedraw();
1576
+ }
1577
+ updateCursor(node) {
1578
+ if (!node) {
1579
+ this.canvas.style.cursor = "default";
1580
+ return;
1581
+ }
1582
+ const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
1583
+ this.canvas.style.cursor = cursor;
1584
+ }
1585
+ dispatchNodeEvent(node, eventType, data) {
1586
+ const actionName = node.props[`on${eventType}`] || node.props[eventType];
1587
+ if (actionName && typeof actionName === "string") {
1588
+ this.engine.dispatchAction(actionName, {
1589
+ type: eventType,
1590
+ nodeId: node.id,
1591
+ timestamp: Date.now(),
1592
+ ...data
1593
+ });
1594
+ }
1595
+ }
1596
+ requestRedraw() {
1597
+ this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
1598
+ }
1599
+ destroy() {}
1600
+ }
1601
+
1602
+ // src/canvas/input.ts
1603
+ class InputOverlay {
1604
+ container;
1605
+ overlay = null;
1606
+ focusedNode = null;
1607
+ onChangeCallback = null;
1608
+ constructor(container) {
1609
+ this.container = container || {};
1610
+ }
1611
+ showInput(node, canvasBounds, onChange) {
1612
+ if (typeof document === "undefined")
1613
+ return;
1614
+ this.hideInput();
1615
+ const bounds = getAbsoluteBounds(node);
1616
+ if (!bounds)
1617
+ return;
1618
+ const isMultiline = node.type === "textarea";
1619
+ this.overlay = isMultiline ? document.createElement("textarea") : document.createElement("input");
1620
+ this.styleOverlay(node, bounds, canvasBounds);
1621
+ const value = node.props.value || "";
1622
+ this.overlay.value = value;
1623
+ this.onChangeCallback = onChange;
1624
+ this.overlay.addEventListener("input", this.onInput.bind(this));
1625
+ this.overlay.addEventListener("blur", this.onBlur.bind(this));
1626
+ this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
1627
+ this.container.appendChild(this.overlay);
1628
+ this.overlay.focus();
1629
+ this.focusedNode = node;
1630
+ }
1631
+ hideInput() {
1632
+ if (this.overlay) {
1633
+ this.overlay.remove();
1634
+ this.overlay = null;
1635
+ }
1636
+ this.focusedNode = null;
1637
+ this.onChangeCallback = null;
1638
+ }
1639
+ updatePosition(node, canvasBounds) {
1640
+ if (!this.overlay || node !== this.focusedNode)
1641
+ return;
1642
+ const bounds = getAbsoluteBounds(node);
1643
+ if (!bounds)
1644
+ return;
1645
+ this.positionOverlay(bounds, canvasBounds);
1646
+ }
1647
+ styleOverlay(node, bounds, canvasBounds) {
1648
+ if (!this.overlay)
1649
+ return;
1650
+ const props = node.props;
1651
+ this.positionOverlay(bounds, canvasBounds);
1652
+ const fontSize = parseFloat(props.fontSize) || 16;
1653
+ const fontWeight = props.fontWeight || "normal";
1654
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
1655
+ const color = props.color || "#000000";
1656
+ Object.assign(this.overlay.style, {
1657
+ fontSize: `${fontSize}px`,
1658
+ fontWeight,
1659
+ fontFamily,
1660
+ color,
1661
+ border: "none",
1662
+ outline: "2px solid #007bff",
1663
+ backgroundColor: props.backgroundColor || "#ffffff",
1664
+ padding: `${props.padding || 8}px`,
1665
+ borderRadius: `${props.borderRadius || 4}px`,
1666
+ boxSizing: "border-box",
1667
+ resize: "none"
1668
+ });
1669
+ if (props.placeholder) {
1670
+ this.overlay.placeholder = props.placeholder;
1671
+ }
1672
+ if (this.overlay instanceof HTMLInputElement && props.type) {
1673
+ this.overlay.type = props.type;
1674
+ }
1675
+ }
1676
+ positionOverlay(bounds, canvasBounds) {
1677
+ if (!this.overlay)
1678
+ return;
1679
+ const canvas = this.container.querySelector("canvas");
1680
+ const scaleX = canvasBounds.width / canvas.width;
1681
+ const scaleY = canvasBounds.height / canvas.height;
1682
+ const left = bounds.x * scaleX;
1683
+ const top = bounds.y * scaleY;
1684
+ const width = bounds.width * scaleX;
1685
+ const height = bounds.height * scaleY;
1686
+ Object.assign(this.overlay.style, {
1687
+ position: "absolute",
1688
+ left: `${left}px`,
1689
+ top: `${top}px`,
1690
+ width: `${width}px`,
1691
+ height: `${height}px`
1692
+ });
1693
+ }
1694
+ onInput(e) {
1695
+ if (!this.overlay || !this.onChangeCallback)
1696
+ return;
1697
+ const value = this.overlay.value;
1698
+ this.onChangeCallback(value);
1699
+ }
1700
+ onBlur() {
1701
+ this.hideInput();
1702
+ }
1703
+ onKeyDown(e) {
1704
+ if (!this.overlay)
1705
+ return;
1706
+ if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
1707
+ e.preventDefault();
1708
+ this.overlay.blur();
1709
+ }
1710
+ if (e.key === "Escape") {
1711
+ e.preventDefault();
1712
+ this.overlay.blur();
1713
+ }
1714
+ }
1715
+ isShown() {
1716
+ return this.overlay !== null;
1717
+ }
1718
+ getFocusedNode() {
1719
+ return this.focusedNode;
1720
+ }
1721
+ }
1722
+
1723
+ // src/canvas/accessibility.ts
1724
+ class AccessibilityLayer {
1725
+ shadowRoot;
1726
+ nodeMap = new Map;
1727
+ enabled;
1728
+ constructor(container, enabled = true) {
1729
+ this.enabled = enabled && typeof document !== "undefined";
1730
+ if (this.enabled && typeof document !== "undefined") {
1731
+ this.shadowRoot = document.createElement("div");
1732
+ this.shadowRoot.setAttribute("role", "application");
1733
+ this.shadowRoot.setAttribute("aria-label", "Hypen Canvas Application");
1734
+ this.shadowRoot.style.position = "absolute";
1735
+ this.shadowRoot.style.left = "-9999px";
1736
+ this.shadowRoot.style.width = "1px";
1737
+ this.shadowRoot.style.height = "1px";
1738
+ this.shadowRoot.style.overflow = "hidden";
1739
+ if (container) {
1740
+ container.appendChild(this.shadowRoot);
1741
+ }
1742
+ } else {
1743
+ this.shadowRoot = {};
1744
+ }
1745
+ }
1746
+ syncTree(root) {
1747
+ if (!this.enabled)
1748
+ return;
1749
+ this.shadowRoot.innerHTML = "";
1750
+ this.nodeMap.clear();
1751
+ const shadowNode = this.createShadowNode(root);
1752
+ if (shadowNode) {
1753
+ this.shadowRoot.appendChild(shadowNode);
1754
+ }
1755
+ }
1756
+ createShadowNode(node) {
1757
+ if (!node.visible)
1758
+ return null;
1759
+ let element;
1760
+ switch (node.type.toLowerCase()) {
1761
+ case "button":
1762
+ element = document.createElement("button");
1763
+ element.textContent = this.getNodeText(node);
1764
+ if (node.props.onclick) {
1765
+ element.setAttribute("aria-label", "Clickable button");
1766
+ }
1767
+ break;
1768
+ case "input":
1769
+ element = document.createElement("input");
1770
+ element.type = node.props.type || "text";
1771
+ element.value = node.props.value || "";
1772
+ if (node.props.placeholder) {
1773
+ element.placeholder = node.props.placeholder;
1774
+ }
1775
+ break;
1776
+ case "textarea":
1777
+ element = document.createElement("textarea");
1778
+ element.value = node.props.value || "";
1779
+ if (node.props.placeholder) {
1780
+ element.placeholder = node.props.placeholder;
1781
+ }
1782
+ break;
1783
+ case "image":
1784
+ element = document.createElement("img");
1785
+ element.src = node.props.src || "";
1786
+ element.alt = node.props.alt || "Image";
1787
+ break;
1788
+ case "text":
1789
+ element = document.createElement("span");
1790
+ element.textContent = String(node.props[0] || node.props.text || "");
1791
+ break;
1792
+ case "column":
1793
+ case "row":
1794
+ case "container":
1795
+ case "box":
1796
+ element = document.createElement("div");
1797
+ element.setAttribute("role", node.type === "column" ? "group" : "group");
1798
+ break;
1799
+ default:
1800
+ element = document.createElement("div");
1801
+ }
1802
+ element.setAttribute("data-hypen-id", node.id);
1803
+ if (node.props["aria-label"]) {
1804
+ element.setAttribute("aria-label", node.props["aria-label"]);
1805
+ }
1806
+ if (node.focusable) {
1807
+ element.tabIndex = 0;
1808
+ }
1809
+ this.nodeMap.set(node.id, element);
1810
+ for (const child of node.children) {
1811
+ const childElement = this.createShadowNode(child);
1812
+ if (childElement) {
1813
+ element.appendChild(childElement);
1814
+ }
1815
+ }
1816
+ return element;
1817
+ }
1818
+ getNodeText(node) {
1819
+ if (node.type === "text") {
1820
+ return String(node.props[0] || node.props.text || "");
1821
+ }
1822
+ let text = "";
1823
+ for (const child of node.children) {
1824
+ text += this.getNodeText(child);
1825
+ }
1826
+ return text;
1827
+ }
1828
+ focusNode(nodeId) {
1829
+ if (!this.enabled)
1830
+ return;
1831
+ const element = this.nodeMap.get(nodeId);
1832
+ if (element) {
1833
+ element.focus();
1834
+ }
1835
+ }
1836
+ updateNode(node) {
1837
+ if (!this.enabled)
1838
+ return;
1839
+ const element = this.nodeMap.get(node.id);
1840
+ if (!element)
1841
+ return;
1842
+ if (node.type === "text") {
1843
+ element.textContent = String(node.props[0] || node.props.text || "");
1844
+ }
1845
+ if (node.type === "input" || node.type === "textarea") {
1846
+ element.value = node.props.value || "";
1847
+ }
1848
+ element.style.display = node.visible ? "" : "none";
1849
+ }
1850
+ getElement(nodeId) {
1851
+ return this.nodeMap.get(nodeId);
1852
+ }
1853
+ setEnabled(enabled) {
1854
+ this.enabled = enabled;
1855
+ if (!enabled && this.shadowRoot.parentElement) {
1856
+ this.shadowRoot.remove();
1857
+ }
1858
+ }
1859
+ destroy() {
1860
+ if (this.shadowRoot.parentElement) {
1861
+ this.shadowRoot.remove();
1862
+ }
1863
+ this.nodeMap.clear();
1864
+ }
1865
+ }
20
1866
 
21
1867
  // src/canvas/renderer.ts
22
1868
  var DEFAULT_OPTIONS = {
@@ -271,6 +2117,4 @@ export {
271
2117
  CanvasRenderer
272
2118
  };
273
2119
 
274
- export { CanvasRenderer };
275
-
276
- //# debugId=C74D0582CBD605AC64756E2164756E21
2120
+ //# debugId=000BEE03B23C15A564756E2164756E21