@hypen-space/web 0.4.36 → 0.4.37

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 (139) hide show
  1. package/dist/canvas/accessibility.js +174 -174
  2. package/dist/canvas/accessibility.js.map +1 -10
  3. package/dist/canvas/events.js +264 -360
  4. package/dist/canvas/events.js.map +1 -11
  5. package/dist/canvas/index.js +11 -47
  6. package/dist/canvas/index.js.map +1 -9
  7. package/dist/canvas/input.js +178 -289
  8. package/dist/canvas/input.js.map +1 -11
  9. package/dist/canvas/layout.js +349 -547
  10. package/dist/canvas/layout.js.map +1 -12
  11. package/dist/canvas/paint.js +1122 -1146
  12. package/dist/canvas/paint.js.map +1 -12
  13. package/dist/canvas/renderer.js +352 -2259
  14. package/dist/canvas/renderer.js.map +1 -17
  15. package/dist/canvas/text.js +129 -264
  16. package/dist/canvas/text.js.map +1 -11
  17. package/dist/canvas/types.js +7 -2
  18. package/dist/canvas/types.js.map +1 -9
  19. package/dist/canvas/utils.js +158 -161
  20. package/dist/canvas/utils.js.map +1 -10
  21. package/dist/client.js +19 -45
  22. package/dist/client.js.map +1 -9
  23. package/dist/dom/applicators/advanced-layout.js +105 -138
  24. package/dist/dom/applicators/advanced-layout.js.map +1 -10
  25. package/dist/dom/applicators/background.js +36 -76
  26. package/dist/dom/applicators/background.js.map +1 -10
  27. package/dist/dom/applicators/border.js +82 -106
  28. package/dist/dom/applicators/border.js.map +1 -10
  29. package/dist/dom/applicators/color.js +17 -58
  30. package/dist/dom/applicators/color.js.map +1 -10
  31. package/dist/dom/applicators/display.js +39 -79
  32. package/dist/dom/applicators/display.js.map +1 -10
  33. package/dist/dom/applicators/effects.js +121 -147
  34. package/dist/dom/applicators/effects.js.map +1 -10
  35. package/dist/dom/applicators/events.js +413 -450
  36. package/dist/dom/applicators/events.js.map +1 -11
  37. package/dist/dom/applicators/font.js +119 -140
  38. package/dist/dom/applicators/font.js.map +1 -10
  39. package/dist/dom/applicators/index.js +386 -1770
  40. package/dist/dom/applicators/index.js.map +1 -26
  41. package/dist/dom/applicators/layout.js +143 -149
  42. package/dist/dom/applicators/layout.js.map +1 -10
  43. package/dist/dom/applicators/margin.js +64 -105
  44. package/dist/dom/applicators/margin.js.map +1 -10
  45. package/dist/dom/applicators/padding.js +64 -105
  46. package/dist/dom/applicators/padding.js.map +1 -10
  47. package/dist/dom/applicators/size.js +169 -172
  48. package/dist/dom/applicators/size.js.map +1 -10
  49. package/dist/dom/applicators/transform.js +73 -114
  50. package/dist/dom/applicators/transform.js.map +1 -10
  51. package/dist/dom/applicators/transition.js +47 -88
  52. package/dist/dom/applicators/transition.js.map +1 -10
  53. package/dist/dom/applicators/types.js +7 -2
  54. package/dist/dom/applicators/types.js.map +1 -9
  55. package/dist/dom/applicators/typography.js +68 -109
  56. package/dist/dom/applicators/typography.js.map +1 -10
  57. package/dist/dom/canvas/index.js +49 -83
  58. package/dist/dom/canvas/index.js.map +1 -10
  59. package/dist/dom/components/app.js +16 -63
  60. package/dist/dom/components/app.js.map +1 -10
  61. package/dist/dom/components/audio.js +31 -76
  62. package/dist/dom/components/audio.js.map +1 -10
  63. package/dist/dom/components/avatar.js +41 -87
  64. package/dist/dom/components/avatar.js.map +1 -10
  65. package/dist/dom/components/badge.js +35 -83
  66. package/dist/dom/components/badge.js.map +1 -10
  67. package/dist/dom/components/button.js +26 -69
  68. package/dist/dom/components/button.js.map +1 -10
  69. package/dist/dom/components/card.js +13 -63
  70. package/dist/dom/components/card.js.map +1 -10
  71. package/dist/dom/components/center.js +21 -65
  72. package/dist/dom/components/center.js.map +1 -10
  73. package/dist/dom/components/checkbox.js +37 -82
  74. package/dist/dom/components/checkbox.js.map +1 -10
  75. package/dist/dom/components/column.js +16 -62
  76. package/dist/dom/components/column.js.map +1 -10
  77. package/dist/dom/components/container.js +11 -59
  78. package/dist/dom/components/container.js.map +1 -10
  79. package/dist/dom/components/divider.js +27 -73
  80. package/dist/dom/components/divider.js.map +1 -10
  81. package/dist/dom/components/grid.js +41 -80
  82. package/dist/dom/components/grid.js.map +1 -10
  83. package/dist/dom/components/heading.js +28 -74
  84. package/dist/dom/components/heading.js.map +1 -10
  85. package/dist/dom/components/hypenapp.js +291 -288
  86. package/dist/dom/components/hypenapp.js.map +1 -10
  87. package/dist/dom/components/image.js +18 -67
  88. package/dist/dom/components/image.js.map +1 -10
  89. package/dist/dom/components/index.js +109 -1427
  90. package/dist/dom/components/index.js.map +1 -42
  91. package/dist/dom/components/input.js +19 -69
  92. package/dist/dom/components/input.js.map +1 -10
  93. package/dist/dom/components/link.js +23 -70
  94. package/dist/dom/components/link.js.map +1 -10
  95. package/dist/dom/components/list.js +26 -72
  96. package/dist/dom/components/list.js.map +1 -10
  97. package/dist/dom/components/paragraph.js +14 -63
  98. package/dist/dom/components/paragraph.js.map +1 -10
  99. package/dist/dom/components/progressbar.js +38 -85
  100. package/dist/dom/components/progressbar.js.map +1 -10
  101. package/dist/dom/components/route.js +27 -73
  102. package/dist/dom/components/route.js.map +1 -10
  103. package/dist/dom/components/router.js +15 -62
  104. package/dist/dom/components/router.js.map +1 -10
  105. package/dist/dom/components/row.js +25 -70
  106. package/dist/dom/components/row.js.map +1 -10
  107. package/dist/dom/components/select.js +42 -85
  108. package/dist/dom/components/select.js.map +1 -10
  109. package/dist/dom/components/slider.js +31 -76
  110. package/dist/dom/components/slider.js.map +1 -10
  111. package/dist/dom/components/spacer.js +10 -60
  112. package/dist/dom/components/spacer.js.map +1 -10
  113. package/dist/dom/components/spinner.js +41 -88
  114. package/dist/dom/components/spinner.js.map +1 -10
  115. package/dist/dom/components/stack.js +19 -67
  116. package/dist/dom/components/stack.js.map +1 -10
  117. package/dist/dom/components/switch.js +50 -93
  118. package/dist/dom/components/switch.js.map +1 -10
  119. package/dist/dom/components/text.js +24 -69
  120. package/dist/dom/components/text.js.map +1 -10
  121. package/dist/dom/components/textarea.js +35 -79
  122. package/dist/dom/components/textarea.js.map +1 -10
  123. package/dist/dom/components/video.js +35 -79
  124. package/dist/dom/components/video.js.map +1 -10
  125. package/dist/dom/debug.js +159 -160
  126. package/dist/dom/debug.js.map +1 -10
  127. package/dist/dom/element-data.js +181 -143
  128. package/dist/dom/element-data.js.map +1 -10
  129. package/dist/dom/events.js +133 -142
  130. package/dist/dom/events.js.map +1 -10
  131. package/dist/dom/index.js +11 -48
  132. package/dist/dom/index.js.map +1 -9
  133. package/dist/dom/renderer.js +311 -3596
  134. package/dist/dom/renderer.js.map +1 -62
  135. package/dist/hypen.js +385 -3880
  136. package/dist/hypen.js.map +1 -63
  137. package/dist/index.js +36 -53
  138. package/dist/index.js.map +1 -9
  139. package/package.json +2 -2
@@ -1,1218 +1,1194 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- function __accessProp(key) {
6
- return this[key];
1
+ /**
2
+ * Paint System
3
+ *
4
+ * Drawing virtual nodes to canvas
5
+ */
6
+ import { renderText } from "./text.js";
7
+ /**
8
+ * Custom painters registry
9
+ */
10
+ const customPainters = new Map();
11
+ /**
12
+ * Register a custom painter for a component type
13
+ */
14
+ export function registerPainter(type, painter) {
15
+ customPainters.set(type.toLowerCase(), painter);
7
16
  }
8
- var __toCommonJS = (from) => {
9
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
- if (entry)
11
- return entry;
12
- entry = __defProp({}, "__esModule", { value: true });
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (var key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(entry, key))
16
- __defProp(entry, key, {
17
- get: __accessProp.bind(from, key),
18
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
- });
20
- }
21
- __moduleCache.set(from, entry);
22
- return entry;
23
- };
24
- var __moduleCache;
25
- var __returnValue = (v) => v;
26
- function __exportSetter(name, newValue) {
27
- this[name] = __returnValue.bind(null, newValue);
28
- }
29
- var __export = (target, all) => {
30
- for (var name in all)
31
- __defProp(target, name, {
32
- get: all[name],
33
- enumerable: true,
34
- configurable: true,
35
- set: __exportSetter.bind(all, name)
36
- });
37
- };
38
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
39
-
40
- // src/canvas/utils.ts
41
- function parseSpacing(value) {
42
- if (typeof value === "number") {
43
- return { top: value, right: value, bottom: value, left: value };
44
- }
45
- if (typeof value === "string") {
46
- const parts = value.split(/\s+/).map((v) => parseFloat(v) || 0);
47
- if (parts.length === 1) {
48
- return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
49
- }
50
- if (parts.length === 2) {
51
- return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
52
- }
53
- if (parts.length === 4) {
54
- return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
55
- }
56
- }
57
- if (typeof value === "object" && value !== null) {
58
- return {
59
- top: parseFloat(value.top) || 0,
60
- right: parseFloat(value.right) || 0,
61
- bottom: parseFloat(value.bottom) || 0,
62
- left: parseFloat(value.left) || 0
63
- };
64
- }
65
- return { top: 0, right: 0, bottom: 0, left: 0 };
66
- }
67
- function parseSize(value) {
68
- if (typeof value === "number")
69
- return value;
70
- if (typeof value === "string") {
71
- if (value === "auto")
72
- return null;
73
- const num = parseFloat(value);
74
- return isNaN(num) ? null : num;
75
- }
76
- return null;
77
- }
78
- function isPointInRect(point, rect) {
79
- return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
80
- }
81
- function isPointInRoundedRect(point, rect, radius) {
82
- const { x, y, width, height } = rect;
83
- if (!isPointInRect(point, rect))
84
- return false;
85
- if (radius <= 0)
86
- return true;
87
- const px = point.x;
88
- const py = point.y;
89
- if (px < x + radius && py < y + radius) {
90
- return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
91
- }
92
- if (px > x + width - radius && py < y + radius) {
93
- return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
94
- }
95
- if (px < x + radius && py > y + height - radius) {
96
- return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
97
- }
98
- if (px > x + width - radius && py > y + height - radius) {
99
- return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
100
- }
101
- return true;
102
- }
103
- function mergeRects(rects) {
104
- if (rects.length === 0)
105
- return null;
106
- if (rects.length === 1)
107
- return rects[0];
108
- let minX = Infinity;
109
- let minY = Infinity;
110
- let maxX = -Infinity;
111
- let maxY = -Infinity;
112
- for (const rect of rects) {
113
- minX = Math.min(minX, rect.x);
114
- minY = Math.min(minY, rect.y);
115
- maxX = Math.max(maxX, rect.x + rect.width);
116
- maxY = Math.max(maxY, rect.y + rect.height);
117
- }
118
- return {
119
- x: minX,
120
- y: minY,
121
- width: maxX - minX,
122
- height: maxY - minY
123
- };
124
- }
125
- function createFontString(fontSize, fontWeight, fontFamily) {
126
- return `${fontWeight} ${fontSize}px ${fontFamily}`;
127
- }
128
- function walkTree(node, callback) {
129
- callback(node);
130
- for (const child of node.children) {
131
- walkTree(child, callback);
132
- }
133
- }
134
- function findNodeById(root, id) {
135
- if (root.id === id)
136
- return root;
137
- for (const child of root.children) {
138
- const found = findNodeById(child, id);
139
- if (found)
140
- return found;
141
- }
142
- return null;
143
- }
144
- function getAbsoluteBounds(node) {
145
- if (!node.layout)
146
- return null;
147
- let x = node.layout.x;
148
- let y = node.layout.y;
149
- let current = node.parent;
150
- while (current && current.layout) {
151
- x += current.layout.contentX;
152
- y += current.layout.contentY;
153
- current = current.parent;
154
- }
155
- return {
156
- x,
157
- y,
158
- width: node.layout.width,
159
- height: node.layout.height
160
- };
161
- }
162
-
163
- // src/canvas/text.ts
164
- import { frameworkLoggers } from "@hypen-space/core/logger";
165
- var log = frameworkLoggers.canvas;
166
- var textMetricsCache = new Map;
167
- function getCacheKey(text, fontStyle, maxWidth) {
168
- return `${text}|${fontStyle.fontSize}|${fontStyle.fontWeight}|${fontStyle.fontFamily}|${maxWidth || "auto"}`;
169
- }
170
- function measureText(ctx, text, fontStyle, maxWidth) {
171
- const cacheKey = getCacheKey(text, fontStyle, maxWidth);
172
- const cached = textMetricsCache.get(cacheKey);
173
- if (cached)
174
- return cached;
175
- const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
176
- ctx.save();
177
- ctx.font = font;
178
- const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
179
- if (!maxWidth) {
180
- const metrics = ctx.measureText(text);
181
- const result2 = {
182
- width: metrics.width,
183
- height: lineHeight,
184
- lines: [text],
185
- lineHeight
186
- };
187
- ctx.restore();
188
- textMetricsCache.set(cacheKey, result2);
189
- return result2;
190
- }
191
- const lines = wrapText(ctx, text, maxWidth);
192
- const width = Math.max(...lines.map((line) => ctx.measureText(line).width));
193
- const height = lines.length * lineHeight;
194
- const result = {
195
- width,
196
- height,
197
- lines,
198
- lineHeight
199
- };
200
- ctx.restore();
201
- textMetricsCache.set(cacheKey, result);
202
- return result;
203
- }
204
- function wrapText(ctx, text, maxWidth) {
205
- const lines = [];
206
- const paragraphs = text.split(`
207
- `);
208
- for (const paragraph of paragraphs) {
209
- const words = paragraph.split(" ");
210
- let currentLine = "";
211
- for (const word of words) {
212
- const testLine = currentLine ? `${currentLine} ${word}` : word;
213
- const metrics = ctx.measureText(testLine);
214
- if (metrics.width > maxWidth && currentLine) {
215
- lines.push(currentLine);
216
- currentLine = word;
217
- } else {
218
- currentLine = testLine;
219
- }
220
- }
221
- if (currentLine) {
222
- lines.push(currentLine);
223
- }
224
- }
225
- return lines.length > 0 ? lines : [""];
226
- }
227
- function renderText(ctx, text, x, y, width, height, style) {
228
- const font = createFontString(style.fontSize, style.fontWeight, style.fontFamily);
229
- ctx.save();
230
- ctx.font = font;
231
- ctx.fillStyle = style.color;
232
- ctx.textBaseline = "top";
233
- const metrics = measureText(ctx, text, style, width);
234
- let startY = y;
235
- if (style.verticalAlign === "middle") {
236
- startY = y + (height - metrics.height) / 2;
237
- } else if (style.verticalAlign === "bottom") {
238
- startY = y + height - metrics.height;
239
- }
240
- for (let i = 0;i < metrics.lines.length; i++) {
241
- const line = metrics.lines[i];
242
- const lineY = startY + i * metrics.lineHeight;
243
- let lineX = x;
244
- if (style.textAlign === "center") {
245
- const lineWidth = ctx.measureText(line).width;
246
- lineX = x + (width - lineWidth) / 2;
247
- } else if (style.textAlign === "right") {
248
- const lineWidth = ctx.measureText(line).width;
249
- lineX = x + width - lineWidth;
250
- }
251
- ctx.fillText(line, lineX, lineY);
252
- }
253
- ctx.restore();
254
- }
255
- function clearTextCache() {
256
- textMetricsCache.clear();
257
- }
258
- async function loadFont(fontFamily, fontWeight = "normal") {
259
- if (!("fonts" in document))
260
- return;
261
- const font = `${fontWeight} 16px ${fontFamily}`;
262
- try {
263
- await document.fonts.load(font);
264
- } catch (error) {
265
- log.warn(`Failed to load font: ${font}`, error);
266
- }
267
- }
268
-
269
- // src/canvas/paint.ts
270
- var customPainters = new Map;
271
- function registerPainter(type, painter) {
272
- customPainters.set(type.toLowerCase(), painter);
273
- }
274
- function paintNode(ctx, node) {
275
- if (!node.visible || !node.layout)
276
- return;
277
- ctx.save();
278
- applyTransforms(ctx, node);
279
- if (node.opacity < 1) {
280
- ctx.globalAlpha = node.opacity;
281
- }
282
- const customPainter = customPainters.get(node.type.toLowerCase());
283
- if (customPainter) {
284
- customPainter(ctx, node);
285
- ctx.restore();
286
- return;
287
- }
288
- switch (node.type.toLowerCase()) {
289
- case "column":
290
- case "row":
291
- case "stack":
292
- paintContainer(ctx, node);
293
- break;
294
- case "text":
295
- paintText(ctx, node);
296
- break;
297
- case "button":
298
- paintButton(ctx, node);
299
- break;
300
- case "input":
301
- paintInput(ctx, node);
302
- break;
303
- case "image":
304
- paintImage(ctx, node);
305
- break;
306
- case "spacer":
307
- break;
308
- case "divider":
309
- case "separator":
310
- paintDivider(ctx, node);
311
- break;
312
- case "checkbox":
313
- paintCheckbox(ctx, node);
314
- break;
315
- case "radio":
316
- paintRadio(ctx, node);
317
- break;
318
- case "switch":
319
- case "toggle":
320
- paintSwitch(ctx, node);
321
- break;
322
- case "slider":
323
- paintSlider(ctx, node);
324
- break;
325
- case "progress":
326
- case "progressbar":
327
- paintProgress(ctx, node);
328
- break;
329
- case "spinner":
330
- case "loading":
331
- paintSpinner(ctx, node);
332
- break;
333
- case "card":
334
- paintCard(ctx, node);
335
- break;
336
- case "badge":
337
- paintBadge(ctx, node);
338
- break;
339
- case "avatar":
340
- paintAvatar(ctx, node);
341
- break;
342
- case "icon":
343
- paintIcon(ctx, node);
344
- break;
345
- case "link":
346
- paintLink(ctx, node);
347
- break;
348
- case "app":
349
- case "container":
350
- case "box":
351
- paintContainer(ctx, node);
352
- break;
353
- default:
354
- paintContainer(ctx, node);
355
- }
356
- ctx.restore();
357
- for (const child of node.children) {
358
- paintNode(ctx, child);
359
- }
360
- if (node._needsRestore) {
17
+ /**
18
+ * Paint a virtual node and its children
19
+ */
20
+ export function paintNode(ctx, node) {
21
+ if (!node.visible || !node.layout)
22
+ return;
23
+ ctx.save();
24
+ // Apply transforms
25
+ applyTransforms(ctx, node);
26
+ // Apply opacity
27
+ if (node.opacity < 1) {
28
+ ctx.globalAlpha = node.opacity;
29
+ }
30
+ // Check for custom painter
31
+ const customPainter = customPainters.get(node.type.toLowerCase());
32
+ if (customPainter) {
33
+ customPainter(ctx, node);
34
+ ctx.restore();
35
+ return;
36
+ }
37
+ // Default painting based on type
38
+ switch (node.type.toLowerCase()) {
39
+ case "column":
40
+ case "row":
41
+ case "stack":
42
+ paintContainer(ctx, node);
43
+ break;
44
+ case "text":
45
+ paintText(ctx, node);
46
+ break;
47
+ case "button":
48
+ paintButton(ctx, node);
49
+ break;
50
+ case "input":
51
+ paintInput(ctx, node);
52
+ break;
53
+ case "image":
54
+ paintImage(ctx, node);
55
+ break;
56
+ case "spacer":
57
+ // Spacer is invisible, just takes up space
58
+ break;
59
+ case "divider":
60
+ case "separator":
61
+ paintDivider(ctx, node);
62
+ break;
63
+ case "checkbox":
64
+ paintCheckbox(ctx, node);
65
+ break;
66
+ case "radio":
67
+ paintRadio(ctx, node);
68
+ break;
69
+ case "switch":
70
+ case "toggle":
71
+ paintSwitch(ctx, node);
72
+ break;
73
+ case "slider":
74
+ paintSlider(ctx, node);
75
+ break;
76
+ case "progress":
77
+ case "progressbar":
78
+ paintProgress(ctx, node);
79
+ break;
80
+ case "spinner":
81
+ case "loading":
82
+ paintSpinner(ctx, node);
83
+ break;
84
+ case "card":
85
+ paintCard(ctx, node);
86
+ break;
87
+ case "badge":
88
+ paintBadge(ctx, node);
89
+ break;
90
+ case "avatar":
91
+ paintAvatar(ctx, node);
92
+ break;
93
+ case "icon":
94
+ paintIcon(ctx, node);
95
+ break;
96
+ case "link":
97
+ paintLink(ctx, node);
98
+ break;
99
+ case "app":
100
+ case "container":
101
+ case "box":
102
+ paintContainer(ctx, node);
103
+ break;
104
+ default:
105
+ paintContainer(ctx, node);
106
+ }
361
107
  ctx.restore();
362
- delete node._needsRestore;
363
- }
108
+ // Paint children
109
+ for (const child of node.children) {
110
+ paintNode(ctx, child);
111
+ }
112
+ // Restore context if overflow clipping was applied
113
+ if (node._needsRestore) {
114
+ ctx.restore();
115
+ delete node._needsRestore;
116
+ }
364
117
  }
118
+ /**
119
+ * Paint a container (Column, Row, Stack, Box)
120
+ */
365
121
  function paintContainer(ctx, node) {
366
- const layout = node.layout;
367
- const props = node.props;
368
- const x = layout.x;
369
- const y = layout.y;
370
- const width = layout.width;
371
- const height = layout.height;
372
- const radius = layout.border.radius;
373
- const shadow = props.shadow || props.boxShadow;
374
- if (shadow) {
375
- applyShadow(ctx, shadow);
376
- }
377
- const backgroundColor = props.backgroundColor || props.background;
378
- if (backgroundColor) {
379
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
380
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
381
- } else {
382
- ctx.fillStyle = backgroundColor;
383
- }
384
- if (radius > 0) {
385
- drawRoundedRect(ctx, x, y, width, height, radius);
386
- ctx.fill();
387
- } else {
388
- ctx.fillRect(x, y, width, height);
389
- }
390
- }
391
- if (shadow) {
392
- ctx.shadowColor = "transparent";
393
- ctx.shadowBlur = 0;
394
- ctx.shadowOffsetX = 0;
395
- ctx.shadowOffsetY = 0;
396
- }
397
- if (layout.border.width > 0 && layout.border.color !== "transparent") {
398
- ctx.strokeStyle = layout.border.color;
399
- ctx.lineWidth = layout.border.width;
400
- if (radius > 0) {
401
- drawRoundedRect(ctx, x, y, width, height, radius);
402
- ctx.stroke();
403
- } else {
404
- ctx.strokeRect(x, y, width, height);
405
- }
406
- }
407
- const overflow = props.overflow || "visible";
408
- if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
409
- ctx.save();
410
- ctx.beginPath();
411
- if (radius > 0) {
412
- drawRoundedRect(ctx, x, y, width, height, radius);
413
- } else {
414
- ctx.rect(x, y, width, height);
122
+ const layout = node.layout;
123
+ const props = node.props;
124
+ const x = layout.x;
125
+ const y = layout.y;
126
+ const width = layout.width;
127
+ const height = layout.height;
128
+ const radius = layout.border.radius;
129
+ // Apply shadow if specified
130
+ const shadow = props.shadow || props.boxShadow;
131
+ if (shadow) {
132
+ applyShadow(ctx, shadow);
133
+ }
134
+ // Draw background
135
+ const backgroundColor = props.backgroundColor || props.background;
136
+ if (backgroundColor) {
137
+ // Support for gradients
138
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
139
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
140
+ }
141
+ else {
142
+ ctx.fillStyle = backgroundColor;
143
+ }
144
+ if (radius > 0) {
145
+ drawRoundedRect(ctx, x, y, width, height, radius);
146
+ ctx.fill();
147
+ }
148
+ else {
149
+ ctx.fillRect(x, y, width, height);
150
+ }
151
+ }
152
+ // Reset shadow for border
153
+ if (shadow) {
154
+ ctx.shadowColor = "transparent";
155
+ ctx.shadowBlur = 0;
156
+ ctx.shadowOffsetX = 0;
157
+ ctx.shadowOffsetY = 0;
158
+ }
159
+ // Draw border
160
+ if (layout.border.width > 0 && layout.border.color !== "transparent") {
161
+ ctx.strokeStyle = layout.border.color;
162
+ ctx.lineWidth = layout.border.width;
163
+ if (radius > 0) {
164
+ drawRoundedRect(ctx, x, y, width, height, radius);
165
+ ctx.stroke();
166
+ }
167
+ else {
168
+ ctx.strokeRect(x, y, width, height);
169
+ }
170
+ }
171
+ // Apply overflow clipping for children
172
+ const overflow = props.overflow || "visible";
173
+ if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
174
+ ctx.save();
175
+ ctx.beginPath();
176
+ if (radius > 0) {
177
+ drawRoundedRect(ctx, x, y, width, height, radius);
178
+ }
179
+ else {
180
+ ctx.rect(x, y, width, height);
181
+ }
182
+ ctx.clip();
183
+ // Mark that we need to restore later
184
+ node._needsRestore = true;
415
185
  }
416
- ctx.clip();
417
- node._needsRestore = true;
418
- }
419
186
  }
187
+ /**
188
+ * Paint text node
189
+ */
420
190
  function paintText(ctx, node) {
421
- const layout = node.layout;
422
- const props = node.props;
423
- let text = String(props[0] || props.text || "");
424
- const color = props.color || "#000000";
425
- const fontSize = parseFloat(props.fontSize) || 16;
426
- const fontWeight = props.fontWeight || "normal";
427
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
428
- const textAlign = props.textAlign || "left";
429
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
430
- const textDecoration = props.textDecoration || "none";
431
- const textTransform = props.textTransform || "none";
432
- const letterSpacing = parseFloat(props.letterSpacing) || 0;
433
- if (textTransform === "uppercase") {
434
- text = text.toUpperCase();
435
- } else if (textTransform === "lowercase") {
436
- text = text.toLowerCase();
437
- } else if (textTransform === "capitalize") {
438
- text = text.replace(/\b\w/g, (char) => char.toUpperCase());
439
- }
440
- const textShadow = props.textShadow || props.shadow;
441
- if (textShadow) {
442
- applyShadow(ctx, textShadow);
443
- }
444
- const x = layout.x + layout.contentX;
445
- const y = layout.y + layout.contentY;
446
- if (letterSpacing !== 0) {
447
- ctx.fillStyle = color;
448
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
449
- ctx.textAlign = "left";
450
- ctx.textBaseline = "top";
451
- let currentX = x;
452
- for (let i = 0;i < text.length; i++) {
453
- ctx.fillText(text[i], currentX, y);
454
- currentX += ctx.measureText(text[i]).width;
455
- if (i < text.length - 1) {
456
- currentX += letterSpacing;
457
- }
458
- }
459
- if (textDecoration !== "none") {
460
- applyTextDecoration(ctx, textDecoration, color, x, y, currentX - x, fontSize);
461
- }
462
- } else {
463
- renderText(ctx, text, x, y, layout.contentWidth, layout.contentHeight, {
464
- color,
465
- fontSize,
466
- fontWeight,
467
- fontFamily,
468
- textAlign,
469
- verticalAlign: "top",
470
- lineHeight
471
- });
472
- if (textDecoration !== "none") {
473
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
474
- const textWidth = ctx.measureText(text).width;
475
- applyTextDecoration(ctx, textDecoration, color, x, y, textWidth, fontSize);
476
- }
477
- }
478
- if (textShadow) {
479
- ctx.shadowColor = "transparent";
480
- ctx.shadowBlur = 0;
481
- ctx.shadowOffsetX = 0;
482
- ctx.shadowOffsetY = 0;
483
- }
191
+ const layout = node.layout;
192
+ const props = node.props;
193
+ let text = String(props[0] || props.text || "");
194
+ const color = props.color || "#000000";
195
+ const fontSize = parseFloat(props.fontSize) || 16;
196
+ const fontWeight = props.fontWeight || "normal";
197
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
198
+ const textAlign = props.textAlign || "left";
199
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
200
+ const textDecoration = props.textDecoration || "none";
201
+ const textTransform = props.textTransform || "none";
202
+ const letterSpacing = parseFloat(props.letterSpacing) || 0;
203
+ // Apply text transform
204
+ if (textTransform === "uppercase") {
205
+ text = text.toUpperCase();
206
+ }
207
+ else if (textTransform === "lowercase") {
208
+ text = text.toLowerCase();
209
+ }
210
+ else if (textTransform === "capitalize") {
211
+ text = text.replace(/\b\w/g, (char) => char.toUpperCase());
212
+ }
213
+ // Apply text shadow if specified
214
+ const textShadow = props.textShadow || props.shadow;
215
+ if (textShadow) {
216
+ applyShadow(ctx, textShadow);
217
+ }
218
+ // Handle letter spacing
219
+ const x = layout.x + layout.contentX;
220
+ const y = layout.y + layout.contentY;
221
+ if (letterSpacing !== 0) {
222
+ // Manual letter spacing
223
+ ctx.fillStyle = color;
224
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
225
+ ctx.textAlign = "left";
226
+ ctx.textBaseline = "top";
227
+ let currentX = x;
228
+ for (let i = 0; i < text.length; i++) {
229
+ ctx.fillText(text[i], currentX, y);
230
+ currentX += ctx.measureText(text[i]).width;
231
+ // Add letter spacing after each character except the last
232
+ if (i < text.length - 1) {
233
+ currentX += letterSpacing;
234
+ }
235
+ }
236
+ // Text decoration with letter spacing (now correct width)
237
+ if (textDecoration !== "none") {
238
+ applyTextDecoration(ctx, textDecoration, color, x, y, currentX - x, fontSize);
239
+ }
240
+ }
241
+ else {
242
+ // Normal text rendering
243
+ renderText(ctx, text, x, y, layout.contentWidth, layout.contentHeight, {
244
+ color,
245
+ fontSize,
246
+ fontWeight,
247
+ fontFamily,
248
+ textAlign: textAlign,
249
+ verticalAlign: "top",
250
+ lineHeight,
251
+ });
252
+ // Text decoration (need to set font again for measurement)
253
+ if (textDecoration !== "none") {
254
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
255
+ const textWidth = ctx.measureText(text).width;
256
+ applyTextDecoration(ctx, textDecoration, color, x, y, textWidth, fontSize);
257
+ }
258
+ }
259
+ // Reset shadow
260
+ if (textShadow) {
261
+ ctx.shadowColor = "transparent";
262
+ ctx.shadowBlur = 0;
263
+ ctx.shadowOffsetX = 0;
264
+ ctx.shadowOffsetY = 0;
265
+ }
484
266
  }
267
+ /**
268
+ * Paint button node
269
+ */
485
270
  function paintButton(ctx, node) {
486
- const layout = node.layout;
487
- const props = node.props;
488
- const x = layout.x;
489
- const y = layout.y;
490
- const width = layout.width;
491
- const height = layout.height;
492
- const radius = layout.border.radius || 4;
493
- const shadow = props.shadow || props.boxShadow;
494
- if (shadow) {
495
- applyShadow(ctx, shadow);
496
- }
497
- let backgroundColor = props.backgroundColor || "#007bff";
498
- if (node.hovered) {
499
- backgroundColor = props.hoverColor || "#0056b3";
500
- }
501
- if (node.focused) {
502
- backgroundColor = props.focusColor || "#004085";
503
- }
504
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
505
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
506
- } else {
507
- ctx.fillStyle = backgroundColor;
508
- }
509
- drawRoundedRect(ctx, x, y, width, height, radius);
510
- ctx.fill();
511
- if (shadow) {
512
- ctx.shadowColor = "transparent";
513
- ctx.shadowBlur = 0;
514
- ctx.shadowOffsetX = 0;
515
- ctx.shadowOffsetY = 0;
516
- }
517
- if (layout.border.width > 0) {
518
- ctx.strokeStyle = layout.border.color;
519
- ctx.lineWidth = layout.border.width;
271
+ const layout = node.layout;
272
+ const props = node.props;
273
+ const x = layout.x;
274
+ const y = layout.y;
275
+ const width = layout.width;
276
+ const height = layout.height;
277
+ const radius = layout.border.radius || 4;
278
+ // Apply shadow if specified
279
+ const shadow = props.shadow || props.boxShadow;
280
+ if (shadow) {
281
+ applyShadow(ctx, shadow);
282
+ }
283
+ // Background color based on state
284
+ let backgroundColor = props.backgroundColor || "#007bff";
285
+ if (node.hovered) {
286
+ backgroundColor = props.hoverColor || "#0056b3";
287
+ }
288
+ if (node.focused) {
289
+ backgroundColor = props.focusColor || "#004085";
290
+ }
291
+ // Support gradients for button background
292
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
293
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
294
+ }
295
+ else {
296
+ ctx.fillStyle = backgroundColor;
297
+ }
298
+ // Draw button background
520
299
  drawRoundedRect(ctx, x, y, width, height, radius);
521
- ctx.stroke();
522
- }
523
- for (const child of node.children) {
524
- paintNode(ctx, child);
525
- }
300
+ ctx.fill();
301
+ // Reset shadow for border
302
+ if (shadow) {
303
+ ctx.shadowColor = "transparent";
304
+ ctx.shadowBlur = 0;
305
+ ctx.shadowOffsetX = 0;
306
+ ctx.shadowOffsetY = 0;
307
+ }
308
+ // Draw border
309
+ if (layout.border.width > 0) {
310
+ ctx.strokeStyle = layout.border.color;
311
+ ctx.lineWidth = layout.border.width;
312
+ drawRoundedRect(ctx, x, y, width, height, radius);
313
+ ctx.stroke();
314
+ }
315
+ // Paint children (typically Text)
316
+ for (const child of node.children) {
317
+ paintNode(ctx, child);
318
+ }
526
319
  }
320
+ /**
321
+ * Paint input node
322
+ */
527
323
  function paintInput(ctx, node) {
528
- const layout = node.layout;
529
- const props = node.props;
530
- const x = layout.x;
531
- const y = layout.y;
532
- const width = layout.width;
533
- const height = layout.height;
534
- const radius = layout.border.radius || 4;
535
- ctx.fillStyle = props.backgroundColor || "#ffffff";
536
- drawRoundedRect(ctx, x, y, width, height, radius);
537
- ctx.fill();
538
- const borderColor = node.focused ? "#007bff" : layout.border.color || "#cccccc";
539
- const borderWidth = node.focused ? 2 : layout.border.width || 1;
540
- ctx.strokeStyle = borderColor;
541
- ctx.lineWidth = borderWidth;
542
- drawRoundedRect(ctx, x, y, width, height, radius);
543
- ctx.stroke();
544
- const value = props.value || "";
545
- const placeholder = props.placeholder || "";
546
- const text = value || placeholder;
547
- const textColor = value ? props.color || "#000000" : "#999999";
548
- if (text) {
549
- const fontSize = parseFloat(props.fontSize) || 16;
550
- const fontWeight = props.fontWeight || "normal";
551
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
552
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
553
- renderText(ctx, text, layout.x + layout.contentX, layout.y + layout.contentY, layout.contentWidth, layout.contentHeight, {
554
- color: textColor,
555
- fontSize,
556
- fontWeight,
557
- fontFamily,
558
- textAlign: "left",
559
- verticalAlign: "middle",
560
- lineHeight
561
- });
562
- }
324
+ const layout = node.layout;
325
+ const props = node.props;
326
+ const x = layout.x;
327
+ const y = layout.y;
328
+ const width = layout.width;
329
+ const height = layout.height;
330
+ const radius = layout.border.radius || 4;
331
+ // Background
332
+ ctx.fillStyle = props.backgroundColor || "#ffffff";
333
+ drawRoundedRect(ctx, x, y, width, height, radius);
334
+ ctx.fill();
335
+ // Border (thicker if focused)
336
+ const borderColor = node.focused ? "#007bff" : (layout.border.color || "#cccccc");
337
+ const borderWidth = node.focused ? 2 : (layout.border.width || 1);
338
+ ctx.strokeStyle = borderColor;
339
+ ctx.lineWidth = borderWidth;
340
+ drawRoundedRect(ctx, x, y, width, height, radius);
341
+ ctx.stroke();
342
+ // Input value text
343
+ const value = props.value || "";
344
+ const placeholder = props.placeholder || "";
345
+ const text = value || placeholder;
346
+ const textColor = value ? (props.color || "#000000") : "#999999";
347
+ if (text) {
348
+ const fontSize = parseFloat(props.fontSize) || 16;
349
+ const fontWeight = props.fontWeight || "normal";
350
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
351
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
352
+ renderText(ctx, text, layout.x + layout.contentX, layout.y + layout.contentY, layout.contentWidth, layout.contentHeight, {
353
+ color: textColor,
354
+ fontSize,
355
+ fontWeight,
356
+ fontFamily,
357
+ textAlign: "left",
358
+ verticalAlign: "middle",
359
+ lineHeight,
360
+ });
361
+ }
563
362
  }
564
- var MAX_IMAGE_CACHE_SIZE = 100;
565
- var MAX_FAIL_CACHE_SIZE = 200;
566
- var imageCache = new Map;
567
- var imagePending = new Set;
568
- var imageInFlight = new Map;
569
- var imageFailCache = new Map;
570
- var IMAGE_FAIL_COOLDOWN_MS = 30000;
571
- var IMAGE_MAX_ATTEMPTS = 3;
363
+ /**
364
+ * Image cache for canvas renderer
365
+ */
366
+ const MAX_IMAGE_CACHE_SIZE = 100;
367
+ const MAX_FAIL_CACHE_SIZE = 200;
368
+ const imageCache = new Map();
369
+ const imagePending = new Set();
370
+ const imageInFlight = new Map();
371
+ const imageFailCache = new Map();
372
+ const IMAGE_FAIL_COOLDOWN_MS = 30_000;
373
+ const IMAGE_MAX_ATTEMPTS = 3;
374
+ /**
375
+ * Evict oldest entries from a Map when it exceeds the given maximum size.
376
+ */
572
377
  function evictMap(map, max) {
573
- while (map.size > max) {
574
- const oldest = map.keys().next().value;
575
- if (oldest === undefined)
576
- break;
577
- map.delete(oldest);
578
- }
378
+ while (map.size > max) {
379
+ const oldest = map.keys().next().value;
380
+ if (oldest === undefined)
381
+ break;
382
+ map.delete(oldest);
383
+ }
579
384
  }
385
+ /**
386
+ * Purge expired entries from imageFailCache.
387
+ */
580
388
  function purgeExpiredFailures() {
581
- const now = Date.now();
582
- for (const [key, entry] of imageFailCache) {
583
- if (entry.attempts >= IMAGE_MAX_ATTEMPTS)
584
- continue;
585
- if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
586
- imageFailCache.delete(key);
587
- }
588
- }
389
+ const now = Date.now();
390
+ for (const [key, entry] of imageFailCache) {
391
+ if (entry.attempts >= IMAGE_MAX_ATTEMPTS)
392
+ continue; // Keep max-attempt entries until evicted by size
393
+ if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
394
+ imageFailCache.delete(key);
395
+ }
396
+ }
589
397
  }
590
- function clearImageCache() {
591
- for (const img of imageInFlight.values()) {
592
- img.onload = null;
593
- img.onerror = null;
594
- img.src = "";
595
- }
596
- imageInFlight.clear();
597
- for (const img of imageCache.values()) {
598
- img.src = "";
599
- }
600
- imageCache.clear();
601
- imagePending.clear();
602
- imageFailCache.clear();
398
+ /**
399
+ * Clear the image cache and cancel all in-flight loads, freeing resources.
400
+ */
401
+ export function clearImageCache() {
402
+ // Cancel in-flight Image loads so their callbacks cannot repopulate caches
403
+ for (const img of imageInFlight.values()) {
404
+ img.onload = null;
405
+ img.onerror = null;
406
+ img.src = "";
407
+ }
408
+ imageInFlight.clear();
409
+ for (const img of imageCache.values()) {
410
+ img.src = "";
411
+ }
412
+ imageCache.clear();
413
+ imagePending.clear();
414
+ imageFailCache.clear();
603
415
  }
416
+ /**
417
+ * Paint image node
418
+ */
604
419
  function paintImage(ctx, node) {
605
- const layout = node.layout;
606
- const props = node.props;
607
- const src = props.src || props[0];
608
- if (!src)
609
- return;
610
- const x = layout.x;
611
- const y = layout.y;
612
- const width = layout.width;
613
- const height = layout.height;
614
- const cached = imageCache.get(src);
615
- if (cached && cached.complete && cached.naturalWidth > 0) {
616
- ctx.drawImage(cached, x, y, width, height);
617
- return;
618
- }
619
- ctx.fillStyle = "#e0e0e0";
620
- ctx.fillRect(x, y, width, height);
621
- ctx.strokeStyle = "#999999";
622
- ctx.lineWidth = 1;
623
- ctx.strokeRect(x, y, width, height);
624
- ctx.fillStyle = "#666666";
625
- ctx.font = "14px sans-serif";
626
- ctx.textAlign = "center";
627
- ctx.textBaseline = "middle";
628
- const failEntry = imageFailCache.get(src);
629
- if (failEntry) {
630
- if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
631
- ctx.fillText("Failed", x + width / 2, y + height / 2);
632
- return;
633
- }
634
- }
635
- ctx.fillText("Loading...", x + width / 2, y + height / 2);
636
- if (!imagePending.has(src)) {
637
- imagePending.add(src);
638
- const canvas = ctx.canvas;
639
- const img = new Image;
640
- img.crossOrigin = "anonymous";
641
- imageInFlight.set(src, img);
642
- img.onload = () => {
643
- imageInFlight.delete(src);
644
- imageCache.set(src, img);
645
- imagePending.delete(src);
646
- imageFailCache.delete(src);
647
- evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
648
- canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
649
- };
650
- img.onerror = () => {
651
- imageInFlight.delete(src);
652
- imagePending.delete(src);
653
- purgeExpiredFailures();
654
- imageFailCache.set(src, {
655
- failedAt: Date.now(),
656
- attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1
657
- });
658
- evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
659
- };
660
- img.src = src;
661
- }
420
+ const layout = node.layout;
421
+ const props = node.props;
422
+ const src = props.src || props[0];
423
+ if (!src)
424
+ return;
425
+ const x = layout.x;
426
+ const y = layout.y;
427
+ const width = layout.width;
428
+ const height = layout.height;
429
+ const cached = imageCache.get(src);
430
+ if (cached && cached.complete && cached.naturalWidth > 0) {
431
+ // Draw the loaded image
432
+ ctx.drawImage(cached, x, y, width, height);
433
+ return;
434
+ }
435
+ // Draw placeholder while loading
436
+ ctx.fillStyle = "#e0e0e0";
437
+ ctx.fillRect(x, y, width, height);
438
+ ctx.strokeStyle = "#999999";
439
+ ctx.lineWidth = 1;
440
+ ctx.strokeRect(x, y, width, height);
441
+ ctx.fillStyle = "#666666";
442
+ ctx.font = "14px sans-serif";
443
+ ctx.textAlign = "center";
444
+ ctx.textBaseline = "middle";
445
+ // Check if this URL has failed recently
446
+ const failEntry = imageFailCache.get(src);
447
+ if (failEntry) {
448
+ if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
449
+ ctx.fillText("Failed", x + width / 2, y + height / 2);
450
+ return;
451
+ }
452
+ }
453
+ ctx.fillText("Loading...", x + width / 2, y + height / 2);
454
+ // Start loading if not already pending
455
+ if (!imagePending.has(src)) {
456
+ imagePending.add(src);
457
+ const canvas = ctx.canvas;
458
+ const img = new Image();
459
+ img.crossOrigin = "anonymous";
460
+ imageInFlight.set(src, img);
461
+ img.onload = () => {
462
+ imageInFlight.delete(src);
463
+ imageCache.set(src, img);
464
+ imagePending.delete(src);
465
+ imageFailCache.delete(src);
466
+ evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
467
+ // Request a repaint so the loaded image is drawn
468
+ canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
469
+ };
470
+ img.onerror = () => {
471
+ imageInFlight.delete(src);
472
+ imagePending.delete(src);
473
+ purgeExpiredFailures();
474
+ imageFailCache.set(src, {
475
+ failedAt: Date.now(),
476
+ attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1,
477
+ });
478
+ evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
479
+ };
480
+ img.src = src;
481
+ }
662
482
  }
483
+ /**
484
+ * Draw rounded rectangle path
485
+ */
663
486
  function drawRoundedRect(ctx, x, y, width, height, radius) {
664
- if (radius <= 0) {
665
- ctx.rect(x, y, width, height);
666
- return;
667
- }
668
- ctx.beginPath();
669
- ctx.moveTo(x + radius, y);
670
- ctx.lineTo(x + width - radius, y);
671
- ctx.arcTo(x + width, y, x + width, y + radius, radius);
672
- ctx.lineTo(x + width, y + height - radius);
673
- ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
674
- ctx.lineTo(x + radius, y + height);
675
- ctx.arcTo(x, y + height, x, y + height - radius, radius);
676
- ctx.lineTo(x, y + radius);
677
- ctx.arcTo(x, y, x + radius, y, radius);
678
- ctx.closePath();
487
+ if (radius <= 0) {
488
+ ctx.rect(x, y, width, height);
489
+ return;
490
+ }
491
+ ctx.beginPath();
492
+ ctx.moveTo(x + radius, y);
493
+ ctx.lineTo(x + width - radius, y);
494
+ ctx.arcTo(x + width, y, x + width, y + radius, radius);
495
+ ctx.lineTo(x + width, y + height - radius);
496
+ ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
497
+ ctx.lineTo(x + radius, y + height);
498
+ ctx.arcTo(x, y + height, x, y + height - radius, radius);
499
+ ctx.lineTo(x, y + radius);
500
+ ctx.arcTo(x, y, x + radius, y, radius);
501
+ ctx.closePath();
679
502
  }
503
+ /**
504
+ * Apply shadow to canvas context
505
+ * Supports both simple and CSS-like shadow syntax:
506
+ * - Simple: "2 2 4 rgba(0,0,0,0.3)"
507
+ * - CSS-like: "2px 2px 4px rgba(0,0,0,0.3)"
508
+ * - Named: {offsetX: 2, offsetY: 2, blur: 4, color: "rgba(0,0,0,0.3)"}
509
+ */
680
510
  function applyShadow(ctx, shadow) {
681
- if (typeof shadow === "string") {
682
- const parts = shadow.trim().split(/\s+/);
683
- if (parts.length >= 3) {
684
- const offsetX = parseFloat(parts[0]);
685
- const offsetY = parseFloat(parts[1]);
686
- const blur = parseFloat(parts[2]);
687
- const color = parts.slice(3).join(" ") || "rgba(0,0,0,0.3)";
688
- ctx.shadowOffsetX = offsetX;
689
- ctx.shadowOffsetY = offsetY;
690
- ctx.shadowBlur = blur;
691
- ctx.shadowColor = color;
692
- }
693
- } else if (typeof shadow === "object") {
694
- ctx.shadowOffsetX = shadow.offsetX || 0;
695
- ctx.shadowOffsetY = shadow.offsetY || 0;
696
- ctx.shadowBlur = shadow.blur || 0;
697
- ctx.shadowColor = shadow.color || "rgba(0,0,0,0.3)";
698
- }
511
+ if (typeof shadow === "string") {
512
+ // Parse CSS-like shadow string: "offsetX offsetY blur color"
513
+ const parts = shadow.trim().split(/\s+/);
514
+ if (parts.length >= 3) {
515
+ const offsetX = parseFloat(parts[0]);
516
+ const offsetY = parseFloat(parts[1]);
517
+ const blur = parseFloat(parts[2]);
518
+ const color = parts.slice(3).join(" ") || "rgba(0,0,0,0.3)";
519
+ ctx.shadowOffsetX = offsetX;
520
+ ctx.shadowOffsetY = offsetY;
521
+ ctx.shadowBlur = blur;
522
+ ctx.shadowColor = color;
523
+ }
524
+ }
525
+ else if (typeof shadow === "object") {
526
+ // Object syntax
527
+ ctx.shadowOffsetX = shadow.offsetX || 0;
528
+ ctx.shadowOffsetY = shadow.offsetY || 0;
529
+ ctx.shadowBlur = shadow.blur || 0;
530
+ ctx.shadowColor = shadow.color || "rgba(0,0,0,0.3)";
531
+ }
699
532
  }
533
+ /**
534
+ * Parse gradient string and create canvas gradient
535
+ * Supports:
536
+ * - linear-gradient(direction, color1, color2, ...)
537
+ * - radial-gradient(color1, color2, ...)
538
+ */
700
539
  function parseGradient(ctx, gradientStr, x, y, width, height) {
701
- if (gradientStr.startsWith("linear-gradient")) {
702
- const match = gradientStr.match(/linear-gradient\((.*)\)/);
703
- if (!match)
704
- return gradientStr;
705
- const parts = match[1].split(",").map((s) => s.trim());
706
- let x0 = x, y0 = y, x1 = x, y1 = y + height;
707
- let colorStart = 0;
708
- if (parts[0].includes("deg") || parts[0].includes("to ")) {
709
- colorStart = 1;
710
- const direction = parts[0];
711
- if (direction.includes("to right") || direction === "90deg") {
712
- x1 = x + width;
713
- y1 = y;
714
- } else if (direction.includes("to left") || direction === "270deg") {
715
- x0 = x + width;
716
- x1 = x;
717
- y0 = y;
718
- y1 = y;
719
- } else if (direction.includes("to top") || direction === "0deg") {
720
- y0 = y + height;
721
- y1 = y;
722
- }
723
- }
724
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
725
- const colors = parts.slice(colorStart);
726
- colors.forEach((color, i) => {
727
- const stop = i / (colors.length - 1);
728
- gradient.addColorStop(stop, color.trim());
729
- });
730
- return gradient;
731
- } else if (gradientStr.startsWith("radial-gradient")) {
732
- const match = gradientStr.match(/radial-gradient\((.*)\)/);
733
- if (!match)
734
- return gradientStr;
735
- const parts = match[1].split(",").map((s) => s.trim());
736
- const centerX = x + width / 2;
737
- const centerY = y + height / 2;
738
- const radius = Math.max(width, height) / 2;
739
- const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
740
- parts.forEach((color, i) => {
741
- const stop = i / (parts.length - 1);
742
- gradient.addColorStop(stop, color.trim());
743
- });
744
- return gradient;
745
- }
746
- return gradientStr;
540
+ // Simple gradient parsing
541
+ if (gradientStr.startsWith("linear-gradient")) {
542
+ // Extract content between parentheses
543
+ const match = gradientStr.match(/linear-gradient\((.*)\)/);
544
+ if (!match)
545
+ return gradientStr;
546
+ const parts = match[1].split(",").map((s) => s.trim());
547
+ // Determine direction (default to bottom)
548
+ let x0 = x, y0 = y, x1 = x, y1 = y + height;
549
+ let colorStart = 0;
550
+ if (parts[0].includes("deg") || parts[0].includes("to ")) {
551
+ colorStart = 1;
552
+ const direction = parts[0];
553
+ if (direction.includes("to right") || direction === "90deg") {
554
+ x1 = x + width;
555
+ y1 = y;
556
+ }
557
+ else if (direction.includes("to left") || direction === "270deg") {
558
+ x0 = x + width;
559
+ x1 = x;
560
+ y0 = y;
561
+ y1 = y;
562
+ }
563
+ else if (direction.includes("to top") || direction === "0deg") {
564
+ y0 = y + height;
565
+ y1 = y;
566
+ }
567
+ // Default is "to bottom" which we already set
568
+ }
569
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
570
+ // Add color stops
571
+ const colors = parts.slice(colorStart);
572
+ colors.forEach((color, i) => {
573
+ const stop = i / (colors.length - 1);
574
+ gradient.addColorStop(stop, color.trim());
575
+ });
576
+ return gradient;
577
+ }
578
+ else if (gradientStr.startsWith("radial-gradient")) {
579
+ // Extract content between parentheses
580
+ const match = gradientStr.match(/radial-gradient\((.*)\)/);
581
+ if (!match)
582
+ return gradientStr;
583
+ const parts = match[1].split(",").map((s) => s.trim());
584
+ // Create radial gradient from center
585
+ const centerX = x + width / 2;
586
+ const centerY = y + height / 2;
587
+ const radius = Math.max(width, height) / 2;
588
+ const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
589
+ // Add color stops
590
+ parts.forEach((color, i) => {
591
+ const stop = i / (parts.length - 1);
592
+ gradient.addColorStop(stop, color.trim());
593
+ });
594
+ return gradient;
595
+ }
596
+ return gradientStr;
747
597
  }
598
+ /**
599
+ * Apply CSS-like transforms to canvas context
600
+ * Supports:
601
+ * - translate(x, y)
602
+ * - rotate(angle)
603
+ * - scale(x, y)
604
+ * - skew(x, y)
605
+ * - transform: "translate(10, 20) rotate(45) scale(1.5)"
606
+ */
748
607
  function applyTransforms(ctx, node) {
749
- const props = node.props;
750
- const layout = node.layout;
751
- const originX = parseFloat(props.transformOriginX) || 0.5;
752
- const originY = parseFloat(props.transformOriginY) || 0.5;
753
- const centerX = layout.x + layout.width * originX;
754
- const centerY = layout.y + layout.height * originY;
755
- const translateX = parseFloat(props.translateX) || 0;
756
- const translateY = parseFloat(props.translateY) || 0;
757
- const rotate = parseFloat(props.rotate) || 0;
758
- const scaleX = parseFloat(props.scaleX) || parseFloat(props.scale) || 1;
759
- const scaleY = parseFloat(props.scaleY) || parseFloat(props.scale) || 1;
760
- const skewX = parseFloat(props.skewX) || parseFloat(props.skew) || 0;
761
- const skewY = parseFloat(props.skewY) || 0;
762
- if (translateX !== 0 || translateY !== 0 || rotate !== 0 || scaleX !== 1 || scaleY !== 1 || skewX !== 0 || skewY !== 0) {
763
- ctx.translate(centerX, centerY);
764
- if (scaleX !== 1 || scaleY !== 1) {
765
- ctx.scale(scaleX, scaleY);
766
- }
767
- if (rotate !== 0) {
768
- ctx.rotate(rotate * Math.PI / 180);
769
- }
770
- if (skewX !== 0 || skewY !== 0) {
771
- const skewXRad = skewX * Math.PI / 180;
772
- const skewYRad = skewY * Math.PI / 180;
773
- ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
774
- }
775
- ctx.translate(-centerX + translateX, -centerY + translateY);
776
- }
777
- if (props.transform && typeof props.transform === "string") {
778
- parseTransformString(ctx, props.transform, centerX, centerY);
779
- }
608
+ const props = node.props;
609
+ const layout = node.layout;
610
+ // Get transform origin (default to center)
611
+ const originX = parseFloat(props.transformOriginX) || 0.5;
612
+ const originY = parseFloat(props.transformOriginY) || 0.5;
613
+ const centerX = layout.x + layout.width * originX;
614
+ const centerY = layout.y + layout.height * originY;
615
+ // Check for individual transform properties
616
+ const translateX = parseFloat(props.translateX) || 0;
617
+ const translateY = parseFloat(props.translateY) || 0;
618
+ const rotate = parseFloat(props.rotate) || 0; // in degrees
619
+ const scaleX = parseFloat(props.scaleX) || parseFloat(props.scale) || 1;
620
+ const scaleY = parseFloat(props.scaleY) || parseFloat(props.scale) || 1;
621
+ const skewX = parseFloat(props.skewX) || parseFloat(props.skew) || 0; // in degrees
622
+ const skewY = parseFloat(props.skewY) || 0; // in degrees
623
+ // Apply transforms in order: translate to origin, scale, rotate, skew, translate back, translate by offset
624
+ if (translateX !== 0 || translateY !== 0 || rotate !== 0 || scaleX !== 1 || scaleY !== 1 || skewX !== 0 || skewY !== 0) {
625
+ // Move to transform origin
626
+ ctx.translate(centerX, centerY);
627
+ // Apply scale
628
+ if (scaleX !== 1 || scaleY !== 1) {
629
+ ctx.scale(scaleX, scaleY);
630
+ }
631
+ // Apply rotation (convert degrees to radians)
632
+ if (rotate !== 0) {
633
+ ctx.rotate((rotate * Math.PI) / 180);
634
+ }
635
+ // Apply skew using transform matrix
636
+ if (skewX !== 0 || skewY !== 0) {
637
+ const skewXRad = (skewX * Math.PI) / 180;
638
+ const skewYRad = (skewY * Math.PI) / 180;
639
+ ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
640
+ }
641
+ // Move back from origin and apply translation
642
+ ctx.translate(-centerX + translateX, -centerY + translateY);
643
+ }
644
+ // Also support compound transform string (optional, for future extensibility)
645
+ if (props.transform && typeof props.transform === "string") {
646
+ parseTransformString(ctx, props.transform, centerX, centerY);
647
+ }
780
648
  }
649
+ /**
650
+ * Parse and apply a CSS-like transform string
651
+ */
781
652
  function parseTransformString(ctx, transformStr, originX, originY) {
782
- const transforms = transformStr.match(/(\w+)\(([^)]+)\)/g);
783
- if (!transforms)
784
- return;
785
- ctx.translate(originX, originY);
786
- for (const transform of transforms) {
787
- const match = transform.match(/(\w+)\(([^)]+)\)/);
788
- if (!match)
789
- continue;
790
- const [, func, args] = match;
791
- const values = args.split(",").map((v) => parseFloat(v.trim()));
792
- switch (func.toLowerCase()) {
793
- case "translate":
794
- ctx.translate(values[0] || 0, values[1] || 0);
795
- break;
796
- case "rotate":
797
- ctx.rotate(values[0] * Math.PI / 180);
798
- break;
799
- case "scale":
800
- ctx.scale(values[0] || 1, values[1] || values[0] || 1);
801
- break;
802
- }
803
- }
804
- ctx.translate(-originX, -originY);
653
+ // Simple transform parsing - matches translate(), rotate(), scale()
654
+ const transforms = transformStr.match(/(\w+)\(([^)]+)\)/g);
655
+ if (!transforms)
656
+ return;
657
+ ctx.translate(originX, originY);
658
+ for (const transform of transforms) {
659
+ const match = transform.match(/(\w+)\(([^)]+)\)/);
660
+ if (!match)
661
+ continue;
662
+ const [, func, args] = match;
663
+ const values = args.split(",").map((v) => parseFloat(v.trim()));
664
+ switch (func.toLowerCase()) {
665
+ case "translate":
666
+ ctx.translate(values[0] || 0, values[1] || 0);
667
+ break;
668
+ case "rotate":
669
+ ctx.rotate((values[0] * Math.PI) / 180);
670
+ break;
671
+ case "scale":
672
+ ctx.scale(values[0] || 1, values[1] || values[0] || 1);
673
+ break;
674
+ }
675
+ }
676
+ ctx.translate(-originX, -originY);
805
677
  }
678
+ /**
679
+ * Paint divider/separator node
680
+ */
806
681
  function paintDivider(ctx, node) {
807
- const layout = node.layout;
808
- const props = node.props;
809
- const orientation = props.orientation || "horizontal";
810
- const color = props.color || props.backgroundColor || "#e0e0e0";
811
- const thickness = parseFloat(props.thickness) || 1;
812
- ctx.strokeStyle = color;
813
- ctx.lineWidth = thickness;
814
- ctx.beginPath();
815
- if (orientation === "vertical") {
816
- const x = layout.x + layout.width / 2;
817
- ctx.moveTo(x, layout.y);
818
- ctx.lineTo(x, layout.y + layout.height);
819
- } else {
820
- const y = layout.y + layout.height / 2;
821
- ctx.moveTo(layout.x, y);
822
- ctx.lineTo(layout.x + layout.width, y);
823
- }
824
- ctx.stroke();
682
+ const layout = node.layout;
683
+ const props = node.props;
684
+ const orientation = props.orientation || "horizontal";
685
+ const color = props.color || props.backgroundColor || "#e0e0e0";
686
+ const thickness = parseFloat(props.thickness) || 1;
687
+ ctx.strokeStyle = color;
688
+ ctx.lineWidth = thickness;
689
+ ctx.beginPath();
690
+ if (orientation === "vertical") {
691
+ const x = layout.x + layout.width / 2;
692
+ ctx.moveTo(x, layout.y);
693
+ ctx.lineTo(x, layout.y + layout.height);
694
+ }
695
+ else {
696
+ const y = layout.y + layout.height / 2;
697
+ ctx.moveTo(layout.x, y);
698
+ ctx.lineTo(layout.x + layout.width, y);
699
+ }
700
+ ctx.stroke();
825
701
  }
702
+ /**
703
+ * Paint checkbox node
704
+ */
826
705
  function paintCheckbox(ctx, node) {
827
- const layout = node.layout;
828
- const props = node.props;
829
- const size = Math.min(layout.width, layout.height);
830
- const x = layout.x;
831
- const y = layout.y;
832
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
833
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
834
- const radius = parseFloat(props.borderRadius) || 2;
835
- const bgColor = checked ? props.checkedColor || "#007bff" : props.backgroundColor || "#ffffff";
836
- ctx.fillStyle = bgColor;
837
- drawRoundedRect(ctx, x, y, size, size, radius);
838
- ctx.fill();
839
- const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
840
- ctx.strokeStyle = borderColor;
841
- ctx.lineWidth = node.focused ? 2 : 1;
842
- drawRoundedRect(ctx, x, y, size, size, radius);
843
- ctx.stroke();
844
- if (checked) {
845
- ctx.strokeStyle = props.checkColor || "#ffffff";
846
- ctx.lineWidth = 2;
847
- ctx.lineCap = "round";
848
- ctx.lineJoin = "round";
849
- const padding = size * 0.25;
850
- ctx.beginPath();
851
- ctx.moveTo(x + padding, y + size / 2);
852
- ctx.lineTo(x + size * 0.4, y + size - padding);
853
- ctx.lineTo(x + size - padding, y + padding);
706
+ const layout = node.layout;
707
+ const props = node.props;
708
+ const size = Math.min(layout.width, layout.height);
709
+ const x = layout.x;
710
+ const y = layout.y;
711
+ // Properly parse boolean values (handle string "false")
712
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
713
+ const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
714
+ const radius = parseFloat(props.borderRadius) || 2;
715
+ // Background
716
+ const bgColor = checked ? (props.checkedColor || "#007bff") : (props.backgroundColor || "#ffffff");
717
+ ctx.fillStyle = bgColor;
718
+ drawRoundedRect(ctx, x, y, size, size, radius);
719
+ ctx.fill();
720
+ // Border
721
+ const borderColor = checked ? (props.checkedColor || "#007bff") : (props.borderColor || "#cccccc");
722
+ ctx.strokeStyle = borderColor;
723
+ ctx.lineWidth = node.focused ? 2 : 1;
724
+ drawRoundedRect(ctx, x, y, size, size, radius);
854
725
  ctx.stroke();
855
- }
726
+ // Checkmark
727
+ if (checked) {
728
+ ctx.strokeStyle = props.checkColor || "#ffffff";
729
+ ctx.lineWidth = 2;
730
+ ctx.lineCap = "round";
731
+ ctx.lineJoin = "round";
732
+ const padding = size * 0.25;
733
+ ctx.beginPath();
734
+ ctx.moveTo(x + padding, y + size / 2);
735
+ ctx.lineTo(x + size * 0.4, y + size - padding);
736
+ ctx.lineTo(x + size - padding, y + padding);
737
+ ctx.stroke();
738
+ }
856
739
  }
740
+ /**
741
+ * Paint radio button node
742
+ */
857
743
  function paintRadio(ctx, node) {
858
- const layout = node.layout;
859
- const props = node.props;
860
- const size = Math.min(layout.width, layout.height);
861
- const centerX = layout.x + size / 2;
862
- const centerY = layout.y + size / 2;
863
- const radius = size / 2;
864
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
865
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
866
- const bgColor = props.backgroundColor || "#ffffff";
867
- ctx.fillStyle = bgColor;
868
- ctx.beginPath();
869
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
870
- ctx.fill();
871
- const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
872
- ctx.strokeStyle = borderColor;
873
- ctx.lineWidth = node.focused ? 2 : 1;
874
- ctx.beginPath();
875
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
876
- ctx.stroke();
877
- if (checked) {
878
- ctx.fillStyle = props.checkedColor || "#007bff";
744
+ const layout = node.layout;
745
+ const props = node.props;
746
+ const size = Math.min(layout.width, layout.height);
747
+ const centerX = layout.x + size / 2;
748
+ const centerY = layout.y + size / 2;
749
+ const radius = size / 2;
750
+ // Properly parse boolean values (handle string "false")
751
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
752
+ const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
753
+ // Outer circle background
754
+ const bgColor = props.backgroundColor || "#ffffff";
755
+ ctx.fillStyle = bgColor;
879
756
  ctx.beginPath();
880
- ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
757
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
881
758
  ctx.fill();
882
- }
759
+ // Outer circle border
760
+ const borderColor = checked ? (props.checkedColor || "#007bff") : (props.borderColor || "#cccccc");
761
+ ctx.strokeStyle = borderColor;
762
+ ctx.lineWidth = node.focused ? 2 : 1;
763
+ ctx.beginPath();
764
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
765
+ ctx.stroke();
766
+ // Inner filled circle when checked
767
+ if (checked) {
768
+ ctx.fillStyle = props.checkedColor || "#007bff";
769
+ ctx.beginPath();
770
+ ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
771
+ ctx.fill();
772
+ }
883
773
  }
774
+ /**
775
+ * Paint switch/toggle node
776
+ */
884
777
  function paintSwitch(ctx, node) {
885
- const layout = node.layout;
886
- const props = node.props;
887
- const width = layout.width;
888
- const height = layout.height;
889
- const x = layout.x;
890
- const y = layout.y;
891
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
892
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
893
- const radius = height / 2;
894
- const trackColor = checked ? props.checkedColor || "#4caf50" : props.backgroundColor || "#cccccc";
895
- ctx.fillStyle = trackColor;
896
- drawRoundedRect(ctx, x, y, width, height, radius);
897
- ctx.fill();
898
- const thumbRadius = radius * 0.8;
899
- const thumbX = checked ? x + width - radius : x + radius;
900
- const thumbY = y + radius;
901
- ctx.fillStyle = props.thumbColor || "#ffffff";
902
- ctx.beginPath();
903
- ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
904
- ctx.fill();
905
- if (props.shadow !== false) {
906
- ctx.shadowColor = "rgba(0,0,0,0.2)";
907
- ctx.shadowBlur = 2;
908
- ctx.shadowOffsetY = 1;
778
+ const layout = node.layout;
779
+ const props = node.props;
780
+ const width = layout.width;
781
+ const height = layout.height;
782
+ const x = layout.x;
783
+ const y = layout.y;
784
+ // Properly parse boolean values (handle string "false")
785
+ const checkedValue = props.checked !== undefined ? props.checked : props.value;
786
+ const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
787
+ const radius = height / 2;
788
+ // Track background
789
+ const trackColor = checked ? (props.checkedColor || "#4caf50") : (props.backgroundColor || "#cccccc");
790
+ ctx.fillStyle = trackColor;
791
+ drawRoundedRect(ctx, x, y, width, height, radius);
792
+ ctx.fill();
793
+ // Thumb (circle)
794
+ const thumbRadius = radius * 0.8;
795
+ const thumbX = checked ? (x + width - radius) : (x + radius);
796
+ const thumbY = y + radius;
797
+ ctx.fillStyle = props.thumbColor || "#ffffff";
909
798
  ctx.beginPath();
910
799
  ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
911
800
  ctx.fill();
912
- ctx.shadowColor = "transparent";
913
- ctx.shadowBlur = 0;
914
- ctx.shadowOffsetY = 0;
915
- }
801
+ // Thumb shadow
802
+ if (props.shadow !== false) {
803
+ ctx.shadowColor = "rgba(0,0,0,0.2)";
804
+ ctx.shadowBlur = 2;
805
+ ctx.shadowOffsetY = 1;
806
+ ctx.beginPath();
807
+ ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
808
+ ctx.fill();
809
+ ctx.shadowColor = "transparent";
810
+ ctx.shadowBlur = 0;
811
+ ctx.shadowOffsetY = 0;
812
+ }
916
813
  }
814
+ /**
815
+ * Paint slider node
816
+ */
917
817
  function paintSlider(ctx, node) {
918
- const layout = node.layout;
919
- const props = node.props;
920
- const width = layout.width;
921
- const height = layout.height;
922
- const x = layout.x;
923
- const y = layout.y;
924
- const min = parseFloat(props.min) || 0;
925
- const max = parseFloat(props.max) || 100;
926
- const value = parseFloat(props.value) || min;
927
- const percentage = (value - min) / (max - min);
928
- const trackHeight = parseFloat(props.trackHeight) || 4;
929
- const thumbSize = parseFloat(props.thumbSize) || 16;
930
- const trackY = y + (height - trackHeight) / 2;
931
- ctx.fillStyle = props.trackColor || "#e0e0e0";
932
- drawRoundedRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
933
- ctx.fill();
934
- const fillWidth = width * percentage;
935
- const fillColor = props.fillColor || props.color || "#007bff";
936
- if (typeof fillColor === "string" && fillColor.includes("gradient")) {
937
- ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
938
- } else {
939
- ctx.fillStyle = fillColor;
940
- }
941
- drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
942
- ctx.fill();
943
- const thumbX = x + fillWidth;
944
- const thumbY = y + height / 2;
945
- ctx.fillStyle = props.thumbColor || "#007bff";
946
- ctx.beginPath();
947
- ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
948
- ctx.fill();
949
- ctx.strokeStyle = "#ffffff";
950
- ctx.lineWidth = 2;
951
- ctx.beginPath();
952
- ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
953
- ctx.stroke();
954
- }
955
- function paintProgress(ctx, node) {
956
- const layout = node.layout;
957
- const props = node.props;
958
- const width = layout.width;
959
- const height = layout.height;
960
- const x = layout.x;
961
- const y = layout.y;
962
- const min = parseFloat(props.min) || 0;
963
- const max = parseFloat(props.max) || 100;
964
- const value = parseFloat(props.value) || 0;
965
- const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
966
- const radius = layout.border.radius || height / 2;
967
- ctx.fillStyle = props.backgroundColor || "#e0e0e0";
968
- drawRoundedRect(ctx, x, y, width, height, radius);
969
- ctx.fill();
970
- if (percentage > 0) {
818
+ const layout = node.layout;
819
+ const props = node.props;
820
+ const width = layout.width;
821
+ const height = layout.height;
822
+ const x = layout.x;
823
+ const y = layout.y;
824
+ const min = parseFloat(props.min) || 0;
825
+ const max = parseFloat(props.max) || 100;
826
+ const value = parseFloat(props.value) || min;
827
+ const percentage = (value - min) / (max - min);
828
+ const trackHeight = parseFloat(props.trackHeight) || 4;
829
+ const thumbSize = parseFloat(props.thumbSize) || 16;
830
+ const trackY = y + (height - trackHeight) / 2;
831
+ // Track background
832
+ ctx.fillStyle = props.trackColor || "#e0e0e0";
833
+ drawRoundedRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
834
+ ctx.fill();
835
+ // Track fill
971
836
  const fillWidth = width * percentage;
972
837
  const fillColor = props.fillColor || props.color || "#007bff";
838
+ // Support gradient fills
973
839
  if (typeof fillColor === "string" && fillColor.includes("gradient")) {
974
- ctx.fillStyle = parseGradient(ctx, fillColor, x, y, fillWidth, height);
975
- } else {
976
- ctx.fillStyle = fillColor;
840
+ ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
977
841
  }
978
- drawRoundedRect(ctx, x, y, fillWidth, height, radius);
842
+ else {
843
+ ctx.fillStyle = fillColor;
844
+ }
845
+ drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
979
846
  ctx.fill();
980
- }
981
- if (props.showLabel) {
982
- const label = props.label || `${Math.round(percentage * 100)}%`;
983
- ctx.fillStyle = props.labelColor || "#ffffff";
984
- ctx.font = `${props.fontSize || 12}px ${props.fontFamily || "sans-serif"}`;
985
- ctx.textAlign = "center";
986
- ctx.textBaseline = "middle";
987
- ctx.fillText(label, x + width / 2, y + height / 2);
988
- }
847
+ // Thumb
848
+ const thumbX = x + fillWidth;
849
+ const thumbY = y + height / 2;
850
+ ctx.fillStyle = props.thumbColor || "#007bff";
851
+ ctx.beginPath();
852
+ ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
853
+ ctx.fill();
854
+ // Thumb border
855
+ ctx.strokeStyle = "#ffffff";
856
+ ctx.lineWidth = 2;
857
+ ctx.beginPath();
858
+ ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
859
+ ctx.stroke();
989
860
  }
861
+ /**
862
+ * Paint progress bar node
863
+ */
864
+ function paintProgress(ctx, node) {
865
+ const layout = node.layout;
866
+ const props = node.props;
867
+ const width = layout.width;
868
+ const height = layout.height;
869
+ const x = layout.x;
870
+ const y = layout.y;
871
+ const min = parseFloat(props.min) || 0;
872
+ const max = parseFloat(props.max) || 100;
873
+ const value = parseFloat(props.value) || 0;
874
+ const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
875
+ const radius = layout.border.radius || height / 2;
876
+ // Background
877
+ ctx.fillStyle = props.backgroundColor || "#e0e0e0";
878
+ drawRoundedRect(ctx, x, y, width, height, radius);
879
+ ctx.fill();
880
+ // Progress fill
881
+ if (percentage > 0) {
882
+ const fillWidth = width * percentage;
883
+ // Support gradient fills
884
+ const fillColor = props.fillColor || props.color || "#007bff";
885
+ if (typeof fillColor === "string" && fillColor.includes("gradient")) {
886
+ ctx.fillStyle = parseGradient(ctx, fillColor, x, y, fillWidth, height);
887
+ }
888
+ else {
889
+ ctx.fillStyle = fillColor;
890
+ }
891
+ drawRoundedRect(ctx, x, y, fillWidth, height, radius);
892
+ ctx.fill();
893
+ }
894
+ // Optional text label
895
+ if (props.showLabel) {
896
+ const label = props.label || `${Math.round(percentage * 100)}%`;
897
+ ctx.fillStyle = props.labelColor || "#ffffff";
898
+ ctx.font = `${props.fontSize || 12}px ${props.fontFamily || "sans-serif"}`;
899
+ ctx.textAlign = "center";
900
+ ctx.textBaseline = "middle";
901
+ ctx.fillText(label, x + width / 2, y + height / 2);
902
+ }
903
+ }
904
+ /**
905
+ * Paint spinner/loading node
906
+ */
990
907
  function paintSpinner(ctx, node) {
991
- const layout = node.layout;
992
- const props = node.props;
993
- const size = Math.min(layout.width, layout.height);
994
- const centerX = layout.x + size / 2;
995
- const centerY = layout.y + size / 2;
996
- const radius = size / 2 - 4;
997
- const thickness = parseFloat(props.thickness) || 4;
998
- const color = props.color || "#007bff";
999
- const rotation = Date.now() / 1000 * Math.PI;
1000
- ctx.strokeStyle = color;
1001
- ctx.lineWidth = thickness;
1002
- ctx.lineCap = "round";
1003
- ctx.beginPath();
1004
- ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
1005
- ctx.stroke();
1006
- ctx.globalAlpha = 0.3;
1007
- ctx.beginPath();
1008
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1009
- ctx.stroke();
1010
- ctx.globalAlpha = 1;
908
+ const layout = node.layout;
909
+ const props = node.props;
910
+ const size = Math.min(layout.width, layout.height);
911
+ const centerX = layout.x + size / 2;
912
+ const centerY = layout.y + size / 2;
913
+ const radius = size / 2 - 4;
914
+ const thickness = parseFloat(props.thickness) || 4;
915
+ const color = props.color || "#007bff";
916
+ // Use timestamp for animation if available
917
+ const rotation = (Date.now() / 1000) * Math.PI; // Rotate based on time
918
+ ctx.strokeStyle = color;
919
+ ctx.lineWidth = thickness;
920
+ ctx.lineCap = "round";
921
+ // Draw circular arc
922
+ ctx.beginPath();
923
+ ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
924
+ ctx.stroke();
925
+ // Fade out effect
926
+ ctx.globalAlpha = 0.3;
927
+ ctx.beginPath();
928
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
929
+ ctx.stroke();
930
+ ctx.globalAlpha = 1;
1011
931
  }
932
+ /**
933
+ * Paint card node
934
+ */
1012
935
  function paintCard(ctx, node) {
1013
- const layout = node.layout;
1014
- const props = node.props;
1015
- const x = layout.x;
1016
- const y = layout.y;
1017
- const width = layout.width;
1018
- const height = layout.height;
1019
- const radius = layout.border.radius || 8;
1020
- const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
1021
- applyShadow(ctx, shadow);
1022
- const backgroundColor = props.backgroundColor || "#ffffff";
1023
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
1024
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
1025
- } else {
1026
- ctx.fillStyle = backgroundColor;
1027
- }
1028
- drawRoundedRect(ctx, x, y, width, height, radius);
1029
- ctx.fill();
1030
- ctx.shadowColor = "transparent";
1031
- ctx.shadowBlur = 0;
1032
- ctx.shadowOffsetX = 0;
1033
- ctx.shadowOffsetY = 0;
1034
- if (layout.border.width > 0) {
1035
- ctx.strokeStyle = layout.border.color;
1036
- ctx.lineWidth = layout.border.width;
936
+ const layout = node.layout;
937
+ const props = node.props;
938
+ const x = layout.x;
939
+ const y = layout.y;
940
+ const width = layout.width;
941
+ const height = layout.height;
942
+ const radius = layout.border.radius || 8;
943
+ // Default card shadow
944
+ const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
945
+ applyShadow(ctx, shadow);
946
+ // Background
947
+ const backgroundColor = props.backgroundColor || "#ffffff";
948
+ if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
949
+ ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
950
+ }
951
+ else {
952
+ ctx.fillStyle = backgroundColor;
953
+ }
1037
954
  drawRoundedRect(ctx, x, y, width, height, radius);
1038
- ctx.stroke();
1039
- }
1040
- for (const child of node.children) {
1041
- paintNode(ctx, child);
1042
- }
955
+ ctx.fill();
956
+ // Reset shadow
957
+ ctx.shadowColor = "transparent";
958
+ ctx.shadowBlur = 0;
959
+ ctx.shadowOffsetX = 0;
960
+ ctx.shadowOffsetY = 0;
961
+ // Border
962
+ if (layout.border.width > 0) {
963
+ ctx.strokeStyle = layout.border.color;
964
+ ctx.lineWidth = layout.border.width;
965
+ drawRoundedRect(ctx, x, y, width, height, radius);
966
+ ctx.stroke();
967
+ }
968
+ // Paint children
969
+ for (const child of node.children) {
970
+ paintNode(ctx, child);
971
+ }
1043
972
  }
973
+ /**
974
+ * Paint badge node
975
+ */
1044
976
  function paintBadge(ctx, node) {
1045
- const layout = node.layout;
1046
- const props = node.props;
1047
- const x = layout.x;
1048
- const y = layout.y;
1049
- const width = layout.width;
1050
- const height = layout.height;
1051
- const radius = layout.border.radius || height / 2;
1052
- const backgroundColor = props.backgroundColor || "#dc3545";
1053
- ctx.fillStyle = backgroundColor;
1054
- drawRoundedRect(ctx, x, y, width, height, radius);
1055
- ctx.fill();
1056
- const text = String(props[0] || props.text || "");
1057
- if (text) {
1058
- ctx.fillStyle = props.color || "#ffffff";
1059
- ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
1060
- ctx.textAlign = "center";
1061
- ctx.textBaseline = "middle";
1062
- ctx.fillText(text, x + width / 2, y + height / 2);
1063
- }
977
+ const layout = node.layout;
978
+ const props = node.props;
979
+ const x = layout.x;
980
+ const y = layout.y;
981
+ const width = layout.width;
982
+ const height = layout.height;
983
+ const radius = layout.border.radius || height / 2;
984
+ // Background
985
+ const backgroundColor = props.backgroundColor || "#dc3545";
986
+ ctx.fillStyle = backgroundColor;
987
+ drawRoundedRect(ctx, x, y, width, height, radius);
988
+ ctx.fill();
989
+ // Text content
990
+ const text = String(props[0] || props.text || "");
991
+ if (text) {
992
+ ctx.fillStyle = props.color || "#ffffff";
993
+ ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
994
+ ctx.textAlign = "center";
995
+ ctx.textBaseline = "middle";
996
+ ctx.fillText(text, x + width / 2, y + height / 2);
997
+ }
1064
998
  }
999
+ /**
1000
+ * Paint avatar node
1001
+ */
1065
1002
  function paintAvatar(ctx, node) {
1066
- const layout = node.layout;
1067
- const props = node.props;
1068
- const size = Math.min(layout.width, layout.height);
1069
- const centerX = layout.x + size / 2;
1070
- const centerY = layout.y + size / 2;
1071
- const radius = size / 2;
1072
- ctx.save();
1073
- ctx.beginPath();
1074
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1075
- ctx.clip();
1076
- const backgroundColor = props.backgroundColor || "#cccccc";
1077
- ctx.fillStyle = backgroundColor;
1078
- ctx.beginPath();
1079
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1080
- ctx.fill();
1081
- const text = String(props[0] || props.text || props.initials || "");
1082
- if (text) {
1083
- ctx.fillStyle = props.color || "#ffffff";
1084
- ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
1085
- ctx.textAlign = "center";
1086
- ctx.textBaseline = "middle";
1087
- ctx.fillText(text, centerX, centerY);
1088
- }
1089
- ctx.restore();
1090
- if (layout.border.width > 0) {
1091
- ctx.strokeStyle = layout.border.color;
1092
- ctx.lineWidth = layout.border.width;
1003
+ const layout = node.layout;
1004
+ const props = node.props;
1005
+ const size = Math.min(layout.width, layout.height);
1006
+ const centerX = layout.x + size / 2;
1007
+ const centerY = layout.y + size / 2;
1008
+ const radius = size / 2;
1009
+ // Clip to circle
1010
+ ctx.save();
1093
1011
  ctx.beginPath();
1094
1012
  ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1095
- ctx.stroke();
1096
- }
1013
+ ctx.clip();
1014
+ // Background
1015
+ const backgroundColor = props.backgroundColor || "#cccccc";
1016
+ ctx.fillStyle = backgroundColor;
1017
+ ctx.beginPath();
1018
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1019
+ ctx.fill();
1020
+ // Text initials if provided
1021
+ const text = String(props[0] || props.text || props.initials || "");
1022
+ if (text) {
1023
+ ctx.fillStyle = props.color || "#ffffff";
1024
+ ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
1025
+ ctx.textAlign = "center";
1026
+ ctx.textBaseline = "middle";
1027
+ ctx.fillText(text, centerX, centerY);
1028
+ }
1029
+ ctx.restore();
1030
+ // Border
1031
+ if (layout.border.width > 0) {
1032
+ ctx.strokeStyle = layout.border.color;
1033
+ ctx.lineWidth = layout.border.width;
1034
+ ctx.beginPath();
1035
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1036
+ ctx.stroke();
1037
+ }
1097
1038
  }
1039
+ /**
1040
+ * Paint icon node (simple placeholder - real icons would need SVG path support)
1041
+ */
1098
1042
  function paintIcon(ctx, node) {
1099
- const layout = node.layout;
1100
- const props = node.props;
1101
- const size = Math.min(layout.width, layout.height);
1102
- const x = layout.x;
1103
- const y = layout.y;
1104
- const color = props.color || "#000000";
1105
- const iconName = props.icon || props.name || "circle";
1106
- ctx.fillStyle = color;
1107
- ctx.strokeStyle = color;
1108
- ctx.lineWidth = 2;
1109
- switch (iconName.toLowerCase()) {
1110
- case "circle":
1111
- ctx.beginPath();
1112
- ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1113
- ctx.fill();
1114
- break;
1115
- case "square":
1116
- const padding = size * 0.2;
1117
- ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
1118
- break;
1119
- case "star":
1120
- drawStar(ctx, x + size / 2, y + size / 2, 5, size / 3, size / 6);
1121
- ctx.fill();
1122
- break;
1123
- case "check":
1124
- case "checkmark":
1125
- const p = size * 0.2;
1126
- ctx.beginPath();
1127
- ctx.moveTo(x + p, y + size / 2);
1128
- ctx.lineTo(x + size * 0.4, y + size - p);
1129
- ctx.lineTo(x + size - p, y + p);
1130
- ctx.stroke();
1131
- break;
1132
- case "x":
1133
- case "close":
1134
- const pd = size * 0.2;
1135
- ctx.beginPath();
1136
- ctx.moveTo(x + pd, y + pd);
1137
- ctx.lineTo(x + size - pd, y + size - pd);
1138
- ctx.moveTo(x + size - pd, y + pd);
1139
- ctx.lineTo(x + pd, y + size - pd);
1140
- ctx.stroke();
1141
- break;
1142
- default:
1143
- ctx.beginPath();
1144
- ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1145
- ctx.fill();
1146
- }
1043
+ const layout = node.layout;
1044
+ const props = node.props;
1045
+ const size = Math.min(layout.width, layout.height);
1046
+ const x = layout.x;
1047
+ const y = layout.y;
1048
+ const color = props.color || "#000000";
1049
+ const iconName = props.icon || props.name || "circle";
1050
+ ctx.fillStyle = color;
1051
+ ctx.strokeStyle = color;
1052
+ ctx.lineWidth = 2;
1053
+ // Simple icon shapes
1054
+ switch (iconName.toLowerCase()) {
1055
+ case "circle":
1056
+ ctx.beginPath();
1057
+ ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1058
+ ctx.fill();
1059
+ break;
1060
+ case "square":
1061
+ const padding = size * 0.2;
1062
+ ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
1063
+ break;
1064
+ case "star":
1065
+ drawStar(ctx, x + size / 2, y + size / 2, 5, size / 3, size / 6);
1066
+ ctx.fill();
1067
+ break;
1068
+ case "check":
1069
+ case "checkmark":
1070
+ const p = size * 0.2;
1071
+ ctx.beginPath();
1072
+ ctx.moveTo(x + p, y + size / 2);
1073
+ ctx.lineTo(x + size * 0.4, y + size - p);
1074
+ ctx.lineTo(x + size - p, y + p);
1075
+ ctx.stroke();
1076
+ break;
1077
+ case "x":
1078
+ case "close":
1079
+ const pd = size * 0.2;
1080
+ ctx.beginPath();
1081
+ ctx.moveTo(x + pd, y + pd);
1082
+ ctx.lineTo(x + size - pd, y + size - pd);
1083
+ ctx.moveTo(x + size - pd, y + pd);
1084
+ ctx.lineTo(x + pd, y + size - pd);
1085
+ ctx.stroke();
1086
+ break;
1087
+ default:
1088
+ // Default: filled circle
1089
+ ctx.beginPath();
1090
+ ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1091
+ ctx.fill();
1092
+ }
1147
1093
  }
1094
+ /**
1095
+ * Paint link node
1096
+ */
1148
1097
  function paintLink(ctx, node) {
1149
- const layout = node.layout;
1150
- const props = node.props;
1151
- const text = String(props[0] || props.text || "");
1152
- const color = node.hovered ? props.hoverColor || "#0056b3" : props.color || "#007bff";
1153
- const fontSize = parseFloat(props.fontSize) || 16;
1154
- const fontWeight = props.fontWeight || "normal";
1155
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
1156
- const textDecoration = props.textDecoration !== undefined ? props.textDecoration : "underline";
1157
- ctx.fillStyle = color;
1158
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
1159
- ctx.textAlign = "left";
1160
- ctx.textBaseline = "top";
1161
- const x = layout.x + layout.contentX;
1162
- const y = layout.y + layout.contentY;
1163
- ctx.fillText(text, x, y);
1164
- if (textDecoration === "underline") {
1165
- const textWidth = ctx.measureText(text).width;
1098
+ const layout = node.layout;
1099
+ const props = node.props;
1100
+ const text = String(props[0] || props.text || "");
1101
+ const color = node.hovered ? (props.hoverColor || "#0056b3") : (props.color || "#007bff");
1102
+ const fontSize = parseFloat(props.fontSize) || 16;
1103
+ const fontWeight = props.fontWeight || "normal";
1104
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
1105
+ const textDecoration = props.textDecoration !== undefined ? props.textDecoration : "underline";
1106
+ ctx.fillStyle = color;
1107
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
1108
+ ctx.textAlign = "left";
1109
+ ctx.textBaseline = "top";
1110
+ const x = layout.x + layout.contentX;
1111
+ const y = layout.y + layout.contentY;
1112
+ ctx.fillText(text, x, y);
1113
+ // Underline
1114
+ if (textDecoration === "underline") {
1115
+ const textWidth = ctx.measureText(text).width;
1116
+ ctx.strokeStyle = color;
1117
+ ctx.lineWidth = 1;
1118
+ ctx.beginPath();
1119
+ ctx.moveTo(x, y + fontSize + 2);
1120
+ ctx.lineTo(x + textWidth, y + fontSize + 2);
1121
+ ctx.stroke();
1122
+ }
1123
+ }
1124
+ /**
1125
+ * Apply text decoration (underline, line-through, etc.)
1126
+ */
1127
+ function applyTextDecoration(ctx, decoration, color, x, y, width, fontSize) {
1166
1128
  ctx.strokeStyle = color;
1167
- ctx.lineWidth = 1;
1129
+ ctx.lineWidth = Math.max(1, fontSize / 16);
1168
1130
  ctx.beginPath();
1169
- ctx.moveTo(x, y + fontSize + 2);
1170
- ctx.lineTo(x + textWidth, y + fontSize + 2);
1131
+ if (decoration === "underline") {
1132
+ const underlineY = y + fontSize + 2;
1133
+ ctx.moveTo(x, underlineY);
1134
+ ctx.lineTo(x + width, underlineY);
1135
+ }
1136
+ else if (decoration === "line-through" || decoration === "strikethrough") {
1137
+ const lineThroughY = y + fontSize / 2;
1138
+ ctx.moveTo(x, lineThroughY);
1139
+ ctx.lineTo(x + width, lineThroughY);
1140
+ }
1141
+ else if (decoration === "overline") {
1142
+ ctx.moveTo(x, y);
1143
+ ctx.lineTo(x + width, y);
1144
+ }
1171
1145
  ctx.stroke();
1172
- }
1173
1146
  }
1174
- function applyTextDecoration(ctx, decoration, color, x, y, width, fontSize) {
1175
- ctx.strokeStyle = color;
1176
- ctx.lineWidth = Math.max(1, fontSize / 16);
1177
- ctx.beginPath();
1178
- if (decoration === "underline") {
1179
- const underlineY = y + fontSize + 2;
1180
- ctx.moveTo(x, underlineY);
1181
- ctx.lineTo(x + width, underlineY);
1182
- } else if (decoration === "line-through" || decoration === "strikethrough") {
1183
- const lineThroughY = y + fontSize / 2;
1184
- ctx.moveTo(x, lineThroughY);
1185
- ctx.lineTo(x + width, lineThroughY);
1186
- } else if (decoration === "overline") {
1187
- ctx.moveTo(x, y);
1188
- ctx.lineTo(x + width, y);
1189
- }
1190
- ctx.stroke();
1147
+ /**
1148
+ * Apply overflow clipping
1149
+ */
1150
+ function applyOverflowClip(ctx, node) {
1151
+ const props = node.props;
1152
+ const overflow = props.overflow || "visible";
1153
+ if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
1154
+ const layout = node.layout;
1155
+ const x = layout.x;
1156
+ const y = layout.y;
1157
+ const width = layout.width;
1158
+ const height = layout.height;
1159
+ const radius = layout.border.radius;
1160
+ ctx.save();
1161
+ ctx.beginPath();
1162
+ if (radius > 0) {
1163
+ drawRoundedRect(ctx, x, y, width, height, radius);
1164
+ }
1165
+ else {
1166
+ ctx.rect(x, y, width, height);
1167
+ }
1168
+ ctx.clip();
1169
+ }
1191
1170
  }
1171
+ /**
1172
+ * Helper: Draw a star shape
1173
+ */
1192
1174
  function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
1193
- let rot = Math.PI / 2 * 3;
1194
- let x = cx;
1195
- let y = cy;
1196
- const step = Math.PI / spikes;
1197
- ctx.beginPath();
1198
- ctx.moveTo(cx, cy - outerRadius);
1199
- for (let i = 0;i < spikes; i++) {
1200
- x = cx + Math.cos(rot) * outerRadius;
1201
- y = cy + Math.sin(rot) * outerRadius;
1202
- ctx.lineTo(x, y);
1203
- rot += step;
1204
- x = cx + Math.cos(rot) * innerRadius;
1205
- y = cy + Math.sin(rot) * innerRadius;
1206
- ctx.lineTo(x, y);
1207
- rot += step;
1208
- }
1209
- ctx.lineTo(cx, cy - outerRadius);
1210
- ctx.closePath();
1175
+ let rot = (Math.PI / 2) * 3;
1176
+ let x = cx;
1177
+ let y = cy;
1178
+ const step = Math.PI / spikes;
1179
+ ctx.beginPath();
1180
+ ctx.moveTo(cx, cy - outerRadius);
1181
+ for (let i = 0; i < spikes; i++) {
1182
+ x = cx + Math.cos(rot) * outerRadius;
1183
+ y = cy + Math.sin(rot) * outerRadius;
1184
+ ctx.lineTo(x, y);
1185
+ rot += step;
1186
+ x = cx + Math.cos(rot) * innerRadius;
1187
+ y = cy + Math.sin(rot) * innerRadius;
1188
+ ctx.lineTo(x, y);
1189
+ rot += step;
1190
+ }
1191
+ ctx.lineTo(cx, cy - outerRadius);
1192
+ ctx.closePath();
1211
1193
  }
1212
- export {
1213
- registerPainter,
1214
- paintNode,
1215
- clearImageCache
1216
- };
1217
-
1218
- //# debugId=DB7D7BF5FB8A39B564756E2164756E21
1194
+ //# sourceMappingURL=paint.js.map