@hypen-space/web 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/chunk-2s02mkzs.js +32 -0
  2. package/dist/chunk-2s02mkzs.js.map +9 -0
  3. package/dist/src/canvas/accessibility.js +152 -0
  4. package/dist/src/canvas/accessibility.js.map +10 -0
  5. package/dist/src/canvas/events.js +198 -0
  6. package/dist/src/canvas/events.js.map +10 -0
  7. package/dist/src/canvas/index.js +28 -0
  8. package/dist/src/canvas/index.js.map +9 -0
  9. package/dist/src/canvas/input.js +132 -0
  10. package/dist/src/canvas/input.js.map +10 -0
  11. package/dist/src/canvas/layout.js +309 -0
  12. package/dist/src/canvas/layout.js.map +10 -0
  13. package/dist/src/canvas/paint.js +878 -0
  14. package/dist/src/canvas/paint.js.map +10 -0
  15. package/dist/src/canvas/renderer.js +276 -0
  16. package/dist/src/canvas/renderer.js.map +10 -0
  17. package/dist/src/canvas/text.js +118 -0
  18. package/dist/src/canvas/text.js.map +10 -0
  19. package/dist/src/canvas/types.js +2 -0
  20. package/dist/src/canvas/types.js.map +9 -0
  21. package/dist/src/canvas/utils.js +139 -0
  22. package/dist/src/canvas/utils.js.map +10 -0
  23. package/dist/src/dom/applicators/advanced-layout.js +111 -0
  24. package/dist/src/dom/applicators/advanced-layout.js.map +10 -0
  25. package/dist/src/dom/applicators/background.js +54 -0
  26. package/dist/src/dom/applicators/background.js.map +10 -0
  27. package/dist/src/dom/applicators/border.js +33 -0
  28. package/dist/src/dom/applicators/border.js.map +10 -0
  29. package/dist/src/dom/applicators/color.js +36 -0
  30. package/dist/src/dom/applicators/color.js.map +10 -0
  31. package/dist/src/dom/applicators/display.js +57 -0
  32. package/dist/src/dom/applicators/display.js.map +10 -0
  33. package/dist/src/dom/applicators/effects.js +89 -0
  34. package/dist/src/dom/applicators/effects.js.map +10 -0
  35. package/dist/src/dom/applicators/events.js +518 -0
  36. package/dist/src/dom/applicators/events.js.map +10 -0
  37. package/dist/src/dom/applicators/font.js +39 -0
  38. package/dist/src/dom/applicators/font.js.map +10 -0
  39. package/dist/src/dom/applicators/index.js +296 -0
  40. package/dist/src/dom/applicators/index.js.map +10 -0
  41. package/dist/src/dom/applicators/layout.js +86 -0
  42. package/dist/src/dom/applicators/layout.js.map +10 -0
  43. package/dist/src/dom/applicators/margin.js +32 -0
  44. package/dist/src/dom/applicators/margin.js.map +10 -0
  45. package/dist/src/dom/applicators/padding.js +35 -0
  46. package/dist/src/dom/applicators/padding.js.map +10 -0
  47. package/dist/src/dom/applicators/size.js +42 -0
  48. package/dist/src/dom/applicators/size.js.map +10 -0
  49. package/dist/src/dom/applicators/transform.js +92 -0
  50. package/dist/src/dom/applicators/transform.js.map +10 -0
  51. package/dist/src/dom/applicators/transition.js +66 -0
  52. package/dist/src/dom/applicators/transition.js.map +10 -0
  53. package/dist/src/dom/applicators/typography.js +87 -0
  54. package/dist/src/dom/applicators/typography.js.map +10 -0
  55. package/dist/src/dom/canvas/index.js +50 -0
  56. package/dist/src/dom/canvas/index.js.map +10 -0
  57. package/dist/src/dom/components/audio.js +48 -0
  58. package/dist/src/dom/components/audio.js.map +10 -0
  59. package/dist/src/dom/components/avatar.js +58 -0
  60. package/dist/src/dom/components/avatar.js.map +10 -0
  61. package/dist/src/dom/components/badge.js +55 -0
  62. package/dist/src/dom/components/badge.js.map +10 -0
  63. package/dist/src/dom/components/button.js +29 -0
  64. package/dist/src/dom/components/button.js.map +10 -0
  65. package/dist/src/dom/components/card.js +33 -0
  66. package/dist/src/dom/components/card.js.map +10 -0
  67. package/dist/src/dom/components/center.js +32 -0
  68. package/dist/src/dom/components/center.js.map +10 -0
  69. package/dist/src/dom/components/checkbox.js +54 -0
  70. package/dist/src/dom/components/checkbox.js.map +10 -0
  71. package/dist/src/dom/components/column.js +31 -0
  72. package/dist/src/dom/components/column.js.map +10 -0
  73. package/dist/src/dom/components/container.js +29 -0
  74. package/dist/src/dom/components/container.js.map +10 -0
  75. package/dist/src/dom/components/divider.js +45 -0
  76. package/dist/src/dom/components/divider.js.map +10 -0
  77. package/dist/src/dom/components/grid.js +44 -0
  78. package/dist/src/dom/components/grid.js.map +10 -0
  79. package/dist/src/dom/components/heading.js +47 -0
  80. package/dist/src/dom/components/heading.js.map +10 -0
  81. package/dist/src/dom/components/image.js +39 -0
  82. package/dist/src/dom/components/image.js.map +10 -0
  83. package/dist/src/dom/components/index.js +217 -0
  84. package/dist/src/dom/components/index.js.map +10 -0
  85. package/dist/src/dom/components/input.js +41 -0
  86. package/dist/src/dom/components/input.js.map +10 -0
  87. package/dist/src/dom/components/link.js +42 -0
  88. package/dist/src/dom/components/link.js.map +10 -0
  89. package/dist/src/dom/components/list.js +42 -0
  90. package/dist/src/dom/components/list.js.map +10 -0
  91. package/dist/src/dom/components/paragraph.js +35 -0
  92. package/dist/src/dom/components/paragraph.js.map +10 -0
  93. package/dist/src/dom/components/progressbar.js +57 -0
  94. package/dist/src/dom/components/progressbar.js.map +10 -0
  95. package/dist/src/dom/components/route.js +44 -0
  96. package/dist/src/dom/components/route.js.map +10 -0
  97. package/dist/src/dom/components/router.js +33 -0
  98. package/dist/src/dom/components/router.js.map +10 -0
  99. package/dist/src/dom/components/row.js +31 -0
  100. package/dist/src/dom/components/row.js.map +10 -0
  101. package/dist/src/dom/components/select.js +57 -0
  102. package/dist/src/dom/components/select.js.map +10 -0
  103. package/dist/src/dom/components/slider.js +48 -0
  104. package/dist/src/dom/components/slider.js.map +10 -0
  105. package/dist/src/dom/components/spacer.js +30 -0
  106. package/dist/src/dom/components/spacer.js.map +10 -0
  107. package/dist/src/dom/components/spinner.js +65 -0
  108. package/dist/src/dom/components/spinner.js.map +10 -0
  109. package/dist/src/dom/components/stack.js +45 -0
  110. package/dist/src/dom/components/stack.js.map +10 -0
  111. package/dist/src/dom/components/switch.js +83 -0
  112. package/dist/src/dom/components/switch.js.map +10 -0
  113. package/dist/src/dom/components/text.js +37 -0
  114. package/dist/src/dom/components/text.js.map +10 -0
  115. package/dist/src/dom/components/textarea.js +51 -0
  116. package/dist/src/dom/components/textarea.js.map +10 -0
  117. package/dist/src/dom/components/video.js +51 -0
  118. package/dist/src/dom/components/video.js.map +10 -0
  119. package/dist/src/dom/debug.js +170 -0
  120. package/dist/src/dom/debug.js.map +10 -0
  121. package/dist/src/dom/events.js +112 -0
  122. package/dist/src/dom/events.js.map +10 -0
  123. package/dist/src/dom/index.js +73 -0
  124. package/dist/src/dom/index.js.map +9 -0
  125. package/dist/src/dom/renderer.js +277 -0
  126. package/dist/src/dom/renderer.js.map +10 -0
  127. package/dist/src/index.js +89 -0
  128. package/dist/src/index.js.map +9 -0
  129. package/package.json +84 -0
  130. package/src/canvas/QUICKSTART.md +421 -0
  131. package/src/canvas/README.md +376 -0
  132. package/src/canvas/accessibility.ts +218 -0
  133. package/src/canvas/events.ts +307 -0
  134. package/src/canvas/index.ts +35 -0
  135. package/src/canvas/input.ts +210 -0
  136. package/src/canvas/layout.ts +401 -0
  137. package/src/canvas/paint.ts +1321 -0
  138. package/src/canvas/renderer.ts +422 -0
  139. package/src/canvas/text.ts +182 -0
  140. package/src/canvas/types.ts +137 -0
  141. package/src/canvas/utils.ts +218 -0
  142. package/src/dom/README.md +265 -0
  143. package/src/dom/applicators/advanced-layout.ts +128 -0
  144. package/src/dom/applicators/background.ts +50 -0
  145. package/src/dom/applicators/border.ts +19 -0
  146. package/src/dom/applicators/color.ts +23 -0
  147. package/src/dom/applicators/display.ts +54 -0
  148. package/src/dom/applicators/effects.ts +97 -0
  149. package/src/dom/applicators/events.ts +689 -0
  150. package/src/dom/applicators/font.ts +27 -0
  151. package/src/dom/applicators/index.ts +354 -0
  152. package/src/dom/applicators/layout.ts +92 -0
  153. package/src/dom/applicators/margin.ts +18 -0
  154. package/src/dom/applicators/padding.ts +18 -0
  155. package/src/dom/applicators/size.ts +31 -0
  156. package/src/dom/applicators/transform.ts +93 -0
  157. package/src/dom/applicators/transition.ts +65 -0
  158. package/src/dom/applicators/typography.ts +91 -0
  159. package/src/dom/canvas/index.ts +60 -0
  160. package/src/dom/components/audio.ts +45 -0
  161. package/src/dom/components/avatar.ts +49 -0
  162. package/src/dom/components/badge.ts +45 -0
  163. package/src/dom/components/button.ts +13 -0
  164. package/src/dom/components/card.ts +19 -0
  165. package/src/dom/components/center.ts +16 -0
  166. package/src/dom/components/checkbox.ts +54 -0
  167. package/src/dom/components/column.ts +15 -0
  168. package/src/dom/components/container.ts +13 -0
  169. package/src/dom/components/divider.ts +37 -0
  170. package/src/dom/components/grid.ts +40 -0
  171. package/src/dom/components/heading.ts +41 -0
  172. package/src/dom/components/image.ts +27 -0
  173. package/src/dom/components/index.ts +115 -0
  174. package/src/dom/components/input.ts +29 -0
  175. package/src/dom/components/link.ts +35 -0
  176. package/src/dom/components/list.ts +30 -0
  177. package/src/dom/components/paragraph.ts +23 -0
  178. package/src/dom/components/progressbar.ts +51 -0
  179. package/src/dom/components/route.ts +37 -0
  180. package/src/dom/components/router.ts +22 -0
  181. package/src/dom/components/row.ts +15 -0
  182. package/src/dom/components/select.ts +56 -0
  183. package/src/dom/components/slider.ts +45 -0
  184. package/src/dom/components/spacer.ts +16 -0
  185. package/src/dom/components/spinner.ts +60 -0
  186. package/src/dom/components/stack.ts +34 -0
  187. package/src/dom/components/switch.ts +86 -0
  188. package/src/dom/components/text.ts +24 -0
  189. package/src/dom/components/textarea.ts +50 -0
  190. package/src/dom/components/video.ts +50 -0
  191. package/src/dom/debug.ts +247 -0
  192. package/src/dom/events.ts +168 -0
  193. package/src/dom/index.ts +11 -0
  194. package/src/dom/renderer.ts +327 -0
  195. package/src/index.ts +56 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Layout Engine
3
+ *
4
+ * Flexbox-like layout system for positioning virtual nodes
5
+ */
6
+
7
+ import type { VirtualNode, Layout, BoxSpacing } from "./types.js";
8
+ import { parseSpacing, parseSize } from "./utils.js";
9
+ import { measureText } from "./text.js";
10
+
11
+ /**
12
+ * Compute layout for a virtual node tree
13
+ */
14
+ export function computeLayout(
15
+ ctx: CanvasRenderingContext2D,
16
+ node: VirtualNode,
17
+ availableWidth: number,
18
+ availableHeight: number,
19
+ x: number = 0,
20
+ y: number = 0
21
+ ): void {
22
+ // Parse props
23
+ const props = node.props;
24
+
25
+ // Support individual margins (marginTop, marginRight, etc.)
26
+ let margin = parseSpacing(props.margin || 0);
27
+ if (props.marginTop !== undefined) margin.top = parseFloat(props.marginTop) || 0;
28
+ if (props.marginRight !== undefined) margin.right = parseFloat(props.marginRight) || 0;
29
+ if (props.marginBottom !== undefined) margin.bottom = parseFloat(props.marginBottom) || 0;
30
+ if (props.marginLeft !== undefined) margin.left = parseFloat(props.marginLeft) || 0;
31
+
32
+ // Support individual padding (paddingTop, paddingRight, etc.)
33
+ let padding = parseSpacing(props.padding || 0);
34
+ if (props.paddingTop !== undefined) padding.top = parseFloat(props.paddingTop) || 0;
35
+ if (props.paddingRight !== undefined) padding.right = parseFloat(props.paddingRight) || 0;
36
+ if (props.paddingBottom !== undefined) padding.bottom = parseFloat(props.paddingBottom) || 0;
37
+ if (props.paddingLeft !== undefined) padding.left = parseFloat(props.paddingLeft) || 0;
38
+
39
+ const borderWidth = parseFloat(props.borderWidth) || 0;
40
+ const borderColor = props.borderColor || "transparent";
41
+ const borderRadius = parseFloat(props.borderRadius) || 0;
42
+
43
+ // Calculate available space after margin
44
+ const availableAfterMargin = {
45
+ width: availableWidth - margin.left - margin.right,
46
+ height: availableHeight - margin.top - margin.bottom,
47
+ };
48
+
49
+ // Determine size
50
+ let width = parseSize(props.width);
51
+ let height = parseSize(props.height);
52
+
53
+ // Default sizing for specific component types
54
+ const type = node.type.toLowerCase();
55
+
56
+ if (type === "spacer") {
57
+ // Spacer fills available space by default
58
+ if (width === null) width = availableAfterMargin.width;
59
+ if (height === null) height = availableAfterMargin.height;
60
+ } else if (type === "divider" || type === "separator") {
61
+ const orientation = props.orientation || "horizontal";
62
+ const thickness = parseFloat(props.thickness) || 1;
63
+
64
+ if (orientation === "vertical") {
65
+ if (width === null) width = thickness;
66
+ if (height === null) height = availableAfterMargin.height;
67
+ } else {
68
+ if (width === null) width = availableAfterMargin.width;
69
+ if (height === null) height = thickness;
70
+ }
71
+ } else if (type === "checkbox" || type === "radio") {
72
+ const size = parseFloat(props.size) || 20;
73
+ if (width === null) width = size;
74
+ if (height === null) height = size;
75
+ } else if (type === "switch" || type === "toggle") {
76
+ if (width === null) width = 44;
77
+ if (height === null) height = 24;
78
+ } else if (type === "slider") {
79
+ if (width === null) width = 200;
80
+ if (height === null) height = 20;
81
+ } else if (type === "progress" || type === "progressbar") {
82
+ if (width === null) width = 200;
83
+ if (height === null) height = 8;
84
+ } else if (type === "spinner" || type === "loading") {
85
+ const size = parseFloat(props.size) || 24;
86
+ if (width === null) width = size;
87
+ if (height === null) height = size;
88
+ } else if (type === "badge") {
89
+ if (width === null) width = 20;
90
+ if (height === null) height = 20;
91
+ } else if (type === "avatar") {
92
+ const size = parseFloat(props.size) || 40;
93
+ if (width === null) width = size;
94
+ if (height === null) height = size;
95
+ } else if (type === "icon") {
96
+ const size = parseFloat(props.size) || 24;
97
+ if (width === null) width = size;
98
+ if (height === null) height = size;
99
+ }
100
+
101
+ // Intrinsic sizing for text nodes
102
+ if (node.type === "text" && node.props[0]) {
103
+ const text = String(node.props[0] || "");
104
+ const fontSize = parseFloat(props.fontSize) || 16;
105
+ const fontWeight = props.fontWeight || "normal";
106
+ const fontFamily = props.fontFamily || "system-ui, sans-serif";
107
+ const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
108
+
109
+ const maxWidth = width || availableAfterMargin.width - padding.left - padding.right;
110
+ const metrics = measureText(ctx, text, { fontSize, fontWeight, fontFamily, lineHeight }, maxWidth);
111
+
112
+ if (!width) width = metrics.width + padding.left + padding.right;
113
+ if (!height) height = metrics.height + padding.top + padding.bottom;
114
+ }
115
+
116
+ // Default to available size if not specified
117
+ if (width === null) width = availableAfterMargin.width;
118
+ if (height === null) height = availableAfterMargin.height;
119
+
120
+ // Apply constraints
121
+ const minWidth = parseSize(props.minWidth);
122
+ const maxWidth = parseSize(props.maxWidth);
123
+ const minHeight = parseSize(props.minHeight);
124
+ const maxHeight = parseSize(props.maxHeight);
125
+
126
+ if (minWidth !== null) width = Math.max(width, minWidth);
127
+ if (maxWidth !== null) width = Math.min(width, maxWidth);
128
+ if (minHeight !== null) height = Math.max(height, minHeight);
129
+ if (maxHeight !== null) height = Math.min(height, maxHeight);
130
+
131
+ // Create layout object
132
+ const layout: Layout = {
133
+ x: x + margin.left,
134
+ y: y + margin.top,
135
+ width,
136
+ height,
137
+ margin,
138
+ padding,
139
+ border: {
140
+ width: borderWidth,
141
+ color: borderColor,
142
+ radius: borderRadius,
143
+ },
144
+ contentX: padding.left + borderWidth,
145
+ contentY: padding.top + borderWidth,
146
+ contentWidth: width - padding.left - padding.right - borderWidth * 2,
147
+ contentHeight: height - padding.top - padding.bottom - borderWidth * 2,
148
+ };
149
+
150
+ node.layout = layout;
151
+
152
+ // Layout children if container
153
+ if (node.children.length > 0) {
154
+ layoutChildren(ctx, node);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Layout children using flexbox-like rules
160
+ */
161
+ function layoutChildren(ctx: CanvasRenderingContext2D, parent: VirtualNode): void {
162
+ const layout = parent.layout!;
163
+ const props = parent.props;
164
+
165
+ // Stack components overlay children on top of each other
166
+ if (parent.type.toLowerCase() === "stack") {
167
+ layoutStackChildren(ctx, parent);
168
+ return;
169
+ }
170
+
171
+ const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
172
+ const justifyContent = props.justifyContent || "flex-start";
173
+ const alignItems = props.alignItems || "flex-start";
174
+ const gap = parseFloat(props.gap) || 0;
175
+
176
+ const isColumn = flexDirection === "column";
177
+ const availableWidth = layout.contentWidth;
178
+ const availableHeight = layout.contentHeight;
179
+
180
+ // First pass: compute intrinsic sizes and collect flex info
181
+ const childInfo: Array<{
182
+ width: number;
183
+ height: number;
184
+ flexGrow: number;
185
+ flexShrink: number;
186
+ flexBasis: number | null;
187
+ }> = [];
188
+ let totalMainSize = 0;
189
+ let totalFlexGrow = 0;
190
+ let totalFlexShrink = 0;
191
+
192
+ for (const child of parent.children) {
193
+ const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
194
+ const flexShrink = parseFloat(child.props.flexShrink) || 1;
195
+ const flexBasis = parseSize(child.props.flexBasis);
196
+
197
+ // Compute child layout with available space
198
+ computeLayout(
199
+ ctx,
200
+ child,
201
+ availableWidth,
202
+ availableHeight,
203
+ 0,
204
+ 0
205
+ );
206
+
207
+ const childLayout = child.layout!;
208
+ let mainSize = isColumn ? childLayout.height : childLayout.width;
209
+
210
+ // Use flexBasis if specified
211
+ if (flexBasis !== null) {
212
+ mainSize = flexBasis;
213
+ if (isColumn) {
214
+ childLayout.height = flexBasis;
215
+ } else {
216
+ childLayout.width = flexBasis;
217
+ }
218
+ }
219
+
220
+ childInfo.push({
221
+ width: childLayout.width,
222
+ height: childLayout.height,
223
+ flexGrow,
224
+ flexShrink,
225
+ flexBasis,
226
+ });
227
+
228
+ totalMainSize += mainSize;
229
+ totalFlexGrow += flexGrow;
230
+ totalFlexShrink += flexShrink;
231
+ }
232
+
233
+ // Add gaps
234
+ const totalGap = gap * (parent.children.length - 1);
235
+ totalMainSize += totalGap;
236
+
237
+ // Apply flex grow/shrink
238
+ const availableMain = isColumn ? availableHeight : availableWidth;
239
+ let remainingSpace = availableMain - totalMainSize;
240
+
241
+ // Distribute remaining space to flex-grow children
242
+ if (remainingSpace > 0 && totalFlexGrow > 0) {
243
+ const spacePerFlex = remainingSpace / totalFlexGrow;
244
+
245
+ for (let i = 0; i < parent.children.length; i++) {
246
+ const info = childInfo[i];
247
+ if (info.flexGrow > 0) {
248
+ const extraSpace = spacePerFlex * info.flexGrow;
249
+ if (isColumn) {
250
+ info.height += extraSpace;
251
+ } else {
252
+ info.width += extraSpace;
253
+ }
254
+ totalMainSize += extraSpace;
255
+ }
256
+ }
257
+
258
+ remainingSpace = 0;
259
+ }
260
+
261
+ // Apply flex shrink if needed (content is too large)
262
+ if (remainingSpace < 0 && totalFlexShrink > 0) {
263
+ const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
264
+
265
+ for (let i = 0; i < parent.children.length; i++) {
266
+ const info = childInfo[i];
267
+ if (info.flexShrink > 0) {
268
+ const shrinkSpace = Math.min(
269
+ shrinkPerFlex * info.flexShrink,
270
+ isColumn ? info.height : info.width
271
+ );
272
+
273
+ if (isColumn) {
274
+ info.height = Math.max(0, info.height - shrinkSpace);
275
+ } else {
276
+ info.width = Math.max(0, info.width - shrinkSpace);
277
+ }
278
+
279
+ totalMainSize -= shrinkSpace;
280
+ }
281
+ }
282
+
283
+ remainingSpace = availableMain - totalMainSize;
284
+ }
285
+
286
+ // Calculate starting position based on justifyContent
287
+ let mainStart = 0;
288
+ let spacing = 0;
289
+
290
+ if (justifyContent === "center") {
291
+ mainStart = Math.max(0, remainingSpace / 2);
292
+ } else if (justifyContent === "flex-end") {
293
+ mainStart = Math.max(0, remainingSpace);
294
+ } else if (justifyContent === "space-between") {
295
+ spacing = remainingSpace / Math.max(1, parent.children.length - 1);
296
+ } else if (justifyContent === "space-around") {
297
+ spacing = remainingSpace / parent.children.length;
298
+ mainStart = spacing / 2;
299
+ }
300
+
301
+ // Position children
302
+ let currentMain = mainStart;
303
+
304
+ for (let i = 0; i < parent.children.length; i++) {
305
+ const child = parent.children[i];
306
+ const childLayout = child.layout!;
307
+ const info = childInfo[i];
308
+
309
+ // Update child layout with flex-adjusted size
310
+ childLayout.width = info.width;
311
+ childLayout.height = info.height;
312
+
313
+ // Calculate cross position based on alignItems
314
+ let crossStart = 0;
315
+ const availableCross = isColumn ? availableWidth : availableHeight;
316
+ const childCross = isColumn ? info.width : info.height;
317
+
318
+ if (alignItems === "center") {
319
+ crossStart = (availableCross - childCross) / 2;
320
+ } else if (alignItems === "flex-end") {
321
+ crossStart = availableCross - childCross;
322
+ }
323
+
324
+ // Update child position relative to parent content area
325
+ if (isColumn) {
326
+ childLayout.x = layout.x + layout.contentX + crossStart;
327
+ childLayout.y = layout.y + layout.contentY + currentMain;
328
+ currentMain += info.height + gap;
329
+ } else {
330
+ childLayout.x = layout.x + layout.contentX + currentMain;
331
+ childLayout.y = layout.y + layout.contentY + crossStart;
332
+ currentMain += info.width + gap;
333
+ }
334
+
335
+ if (justifyContent === "space-between" || justifyContent === "space-around") {
336
+ currentMain += spacing;
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Layout Stack children - overlays children on top of each other
343
+ */
344
+ function layoutStackChildren(ctx: CanvasRenderingContext2D, parent: VirtualNode): void {
345
+ const layout = parent.layout!;
346
+ const props = parent.props;
347
+
348
+ const alignItems = props.alignItems || "flex-start";
349
+ const justifyContent = props.justifyContent || "flex-start";
350
+
351
+ const availableWidth = layout.contentWidth;
352
+ const availableHeight = layout.contentHeight;
353
+
354
+ // Layout all children at the same position (stacked on top of each other)
355
+ for (const child of parent.children) {
356
+ // Compute child layout
357
+ computeLayout(
358
+ ctx,
359
+ child,
360
+ availableWidth,
361
+ availableHeight,
362
+ 0,
363
+ 0
364
+ );
365
+
366
+ const childLayout = child.layout!;
367
+
368
+ // Calculate position based on alignment
369
+ let x = 0;
370
+ let y = 0;
371
+
372
+ // Horizontal alignment
373
+ if (alignItems === "center") {
374
+ x = (availableWidth - childLayout.width) / 2;
375
+ } else if (alignItems === "flex-end") {
376
+ x = availableWidth - childLayout.width;
377
+ }
378
+
379
+ // Vertical alignment
380
+ if (justifyContent === "center") {
381
+ y = (availableHeight - childLayout.height) / 2;
382
+ } else if (justifyContent === "flex-end") {
383
+ y = availableHeight - childLayout.height;
384
+ }
385
+
386
+ // Update child position relative to parent content area
387
+ childLayout.x = layout.x + layout.contentX + x;
388
+ childLayout.y = layout.y + layout.contentY + y;
389
+ }
390
+ }
391
+
392
+
393
+
394
+
395
+
396
+
397
+
398
+
399
+
400
+
401
+