wyreframe 0.1.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 (117) hide show
  1. package/README.md +123 -0
  2. package/dist/index.d.ts +267 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +195 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +63 -0
  7. package/src/parser/Core/Bounds.mjs +61 -0
  8. package/src/parser/Core/Bounds.res +65 -0
  9. package/src/parser/Core/Grid.mjs +268 -0
  10. package/src/parser/Core/Grid.res +265 -0
  11. package/src/parser/Core/Position.mjs +83 -0
  12. package/src/parser/Core/Position.res +54 -0
  13. package/src/parser/Core/Types.mjs +435 -0
  14. package/src/parser/Core/Types.res +331 -0
  15. package/src/parser/Core/__tests__/Bounds_test.mjs +326 -0
  16. package/src/parser/Core/__tests__/Bounds_test.res +412 -0
  17. package/src/parser/Core/__tests__/Grid_test.mjs +322 -0
  18. package/src/parser/Core/__tests__/Grid_test.res +319 -0
  19. package/src/parser/Core/__tests__/Types_test.mjs +614 -0
  20. package/src/parser/Core/__tests__/Types_test.res +650 -0
  21. package/src/parser/Detector/BoxTracer.mjs +302 -0
  22. package/src/parser/Detector/BoxTracer.res +374 -0
  23. package/src/parser/Detector/HierarchyBuilder.mjs +158 -0
  24. package/src/parser/Detector/HierarchyBuilder.res +315 -0
  25. package/src/parser/Detector/ShapeDetector.mjs +134 -0
  26. package/src/parser/Detector/ShapeDetector.res +236 -0
  27. package/src/parser/Detector/__tests__/BoxTracer_test.mjs +70 -0
  28. package/src/parser/Detector/__tests__/BoxTracer_test.res +92 -0
  29. package/src/parser/Detector/__tests__/HierarchyBuilder_test.mjs +489 -0
  30. package/src/parser/Detector/__tests__/HierarchyBuilder_test.res +849 -0
  31. package/src/parser/Detector/__tests__/ShapeDetector_test.mjs +377 -0
  32. package/src/parser/Detector/__tests__/ShapeDetector_test.res +563 -0
  33. package/src/parser/Errors/ErrorContext.mjs +106 -0
  34. package/src/parser/Errors/ErrorContext.res +191 -0
  35. package/src/parser/Errors/ErrorMessages.mjs +289 -0
  36. package/src/parser/Errors/ErrorMessages.res +303 -0
  37. package/src/parser/Errors/ErrorTypes.mjs +105 -0
  38. package/src/parser/Errors/ErrorTypes.res +169 -0
  39. package/src/parser/Interactions/InteractionMerger.mjs +266 -0
  40. package/src/parser/Interactions/InteractionMerger.res +450 -0
  41. package/src/parser/Interactions/InteractionParser.mjs +88 -0
  42. package/src/parser/Interactions/InteractionParser.res +127 -0
  43. package/src/parser/Interactions/SimpleInteractionParser.mjs +278 -0
  44. package/src/parser/Interactions/SimpleInteractionParser.res +262 -0
  45. package/src/parser/Interactions/__tests__/InteractionMerger_test.mjs +576 -0
  46. package/src/parser/Interactions/__tests__/InteractionMerger_test.res +646 -0
  47. package/src/parser/Parser.gen.tsx +96 -0
  48. package/src/parser/Parser.mjs +212 -0
  49. package/src/parser/Parser.res +481 -0
  50. package/src/parser/Scanner/__tests__/Grid_manual.mjs +214 -0
  51. package/src/parser/Scanner/__tests__/Grid_manual.res +141 -0
  52. package/src/parser/Semantic/ASTBuilder.mjs +197 -0
  53. package/src/parser/Semantic/ASTBuilder.res +288 -0
  54. package/src/parser/Semantic/AlignmentCalc.mjs +41 -0
  55. package/src/parser/Semantic/AlignmentCalc.res +104 -0
  56. package/src/parser/Semantic/Elements/ButtonParser.mjs +58 -0
  57. package/src/parser/Semantic/Elements/ButtonParser.res +131 -0
  58. package/src/parser/Semantic/Elements/CheckboxParser.mjs +58 -0
  59. package/src/parser/Semantic/Elements/CheckboxParser.res +79 -0
  60. package/src/parser/Semantic/Elements/CodeTextParser.mjs +50 -0
  61. package/src/parser/Semantic/Elements/CodeTextParser.res +111 -0
  62. package/src/parser/Semantic/Elements/ElementParser.mjs +15 -0
  63. package/src/parser/Semantic/Elements/ElementParser.res +83 -0
  64. package/src/parser/Semantic/Elements/EmphasisParser.mjs +46 -0
  65. package/src/parser/Semantic/Elements/EmphasisParser.res +67 -0
  66. package/src/parser/Semantic/Elements/InputParser.mjs +41 -0
  67. package/src/parser/Semantic/Elements/InputParser.res +97 -0
  68. package/src/parser/Semantic/Elements/LinkParser.mjs +60 -0
  69. package/src/parser/Semantic/Elements/LinkParser.res +156 -0
  70. package/src/parser/Semantic/Elements/TextParser.mjs +19 -0
  71. package/src/parser/Semantic/Elements/TextParser.res +42 -0
  72. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.mjs +189 -0
  73. package/src/parser/Semantic/Elements/__tests__/ButtonParser_test.res +257 -0
  74. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.mjs +202 -0
  75. package/src/parser/Semantic/Elements/__tests__/CheckboxParser_test.res +250 -0
  76. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.mjs +293 -0
  77. package/src/parser/Semantic/Elements/__tests__/CodeTextParser_manual.res +134 -0
  78. package/src/parser/Semantic/Elements/__tests__/InputParser_test.mjs +253 -0
  79. package/src/parser/Semantic/Elements/__tests__/InputParser_test.res +304 -0
  80. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.mjs +289 -0
  81. package/src/parser/Semantic/Elements/__tests__/LinkParser_test.res +402 -0
  82. package/src/parser/Semantic/Elements/__tests__/TextParser_test.mjs +149 -0
  83. package/src/parser/Semantic/Elements/__tests__/TextParser_test.res +167 -0
  84. package/src/parser/Semantic/ParserRegistry.mjs +82 -0
  85. package/src/parser/Semantic/ParserRegistry.res +145 -0
  86. package/src/parser/Semantic/SemanticParser.mjs +850 -0
  87. package/src/parser/Semantic/SemanticParser.res +1368 -0
  88. package/src/parser/Semantic/__tests__/ASTBuilder_test.mjs +187 -0
  89. package/src/parser/Semantic/__tests__/ASTBuilder_test.res +192 -0
  90. package/src/parser/Semantic/__tests__/ParserRegistry_test.mjs +154 -0
  91. package/src/parser/Semantic/__tests__/ParserRegistry_test.res +191 -0
  92. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.mjs +768 -0
  93. package/src/parser/Semantic/__tests__/SemanticParser_integration_test.res +1069 -0
  94. package/src/parser/Semantic/__tests__/SemanticParser_manual.mjs +1329 -0
  95. package/src/parser/Semantic/__tests__/SemanticParser_manual.res +544 -0
  96. package/src/parser/TestMain.mjs +21 -0
  97. package/src/parser/TestMain.res +14 -0
  98. package/src/parser/TextExtractor.mjs +179 -0
  99. package/src/parser/TextExtractor.res +264 -0
  100. package/src/parser/__tests__/GridScanner_integration.test.mjs +632 -0
  101. package/src/parser/__tests__/GridScanner_integration.test.res +816 -0
  102. package/src/parser/__tests__/Performance.test.mjs +244 -0
  103. package/src/parser/__tests__/Performance.test.res +371 -0
  104. package/src/parser/__tests__/PerformanceFixtures.mjs +200 -0
  105. package/src/parser/__tests__/PerformanceFixtures.res +284 -0
  106. package/src/parser/__tests__/WyreframeParser_integration.test.mjs +770 -0
  107. package/src/parser/__tests__/WyreframeParser_integration.test.res +1008 -0
  108. package/src/parser/__tests__/fixtures/alignment-test.txt +9 -0
  109. package/src/parser/__tests__/fixtures/all-elements.txt +16 -0
  110. package/src/parser/__tests__/fixtures/login-scene.txt +17 -0
  111. package/src/parser/__tests__/fixtures/multi-scene.txt +25 -0
  112. package/src/parser/__tests__/fixtures/nested-boxes.txt +15 -0
  113. package/src/parser/__tests__/fixtures/simple-box.txt +5 -0
  114. package/src/parser/__tests__/fixtures/with-dividers.txt +14 -0
  115. package/src/renderer/Renderer.gen.tsx +32 -0
  116. package/src/renderer/Renderer.mjs +391 -0
  117. package/src/renderer/Renderer.res +558 -0
@@ -0,0 +1,558 @@
1
+ // Renderer.res
2
+ // HTML/DOM Renderer for Wyreframe AST
3
+ // Converts parsed wireframe AST into interactive HTML elements
4
+
5
+ open Types
6
+
7
+ // ============================================================================
8
+ // DOM Bindings (must be defined first for type references)
9
+ // ============================================================================
10
+
11
+ module DomBindings = {
12
+ type element
13
+ type document
14
+ type style
15
+ type classList
16
+ type dataset
17
+
18
+ @val external document: document = "document"
19
+
20
+ @send external createElement: (document, string) => element = "createElement"
21
+ @send external getElementById: (document, string) => Nullable.t<element> = "getElementById"
22
+ @send external appendChild: (element, element) => unit = "appendChild"
23
+ @send external querySelector: (element, string) => Nullable.t<element> = "querySelector"
24
+
25
+ @set external setClassName: (element, string) => unit = "className"
26
+ @set external setId: (element, string) => unit = "id"
27
+ @set external setTextContent: (element, string) => unit = "textContent"
28
+ @set external setInnerHTML: (element, string) => unit = "innerHTML"
29
+ @set external setHref: (element, string) => unit = "href"
30
+ @set external setPlaceholder: (element, string) => unit = "placeholder"
31
+ @set external setType: (element, string) => unit = "type"
32
+ @set external setChecked: (element, bool) => unit = "checked"
33
+
34
+ @get external classList: element => classList = "classList"
35
+ @send external add: (classList, string) => unit = "add"
36
+ @send external remove: (classList, string) => unit = "remove"
37
+ @send external contains: (classList, string) => bool = "contains"
38
+
39
+ @get external dataset: element => dataset = "dataset"
40
+ @set_index external setDataAttr: (dataset, string, string) => unit = ""
41
+
42
+ @get external head: document => element = "head"
43
+ @send external addEventListener: (element, string, 'event => unit) => unit = "addEventListener"
44
+ @send external preventDefault: 'event => unit = "preventDefault"
45
+ }
46
+
47
+ // ============================================================================
48
+ // Render Options
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Configuration for the rendering process.
53
+ */
54
+ type renderOptions = {
55
+ theme: option<string>,
56
+ interactive: bool,
57
+ injectStyles: bool,
58
+ containerClass: option<string>,
59
+ }
60
+
61
+ /**
62
+ * Default render options.
63
+ */
64
+ let defaultOptions: renderOptions = {
65
+ theme: None,
66
+ interactive: true,
67
+ injectStyles: true,
68
+ containerClass: None,
69
+ }
70
+
71
+ // ============================================================================
72
+ // Scene Manager Type
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Scene management interface returned by render function.
77
+ */
78
+ type sceneManager = {
79
+ goto: string => unit,
80
+ getCurrentScene: unit => option<string>,
81
+ getSceneIds: unit => array<string>,
82
+ }
83
+
84
+ /**
85
+ * Render result containing the root element and scene manager.
86
+ */
87
+ type renderResult = {
88
+ root: DomBindings.element,
89
+ sceneManager: sceneManager,
90
+ }
91
+
92
+ // ============================================================================
93
+ // CSS Styles
94
+ // ============================================================================
95
+
96
+ let defaultStyles = `
97
+ .wf-app { font-family: monospace; position: relative; overflow: hidden; background: #fff; color: #333; font-size: 14px; margin: 0 auto; }
98
+ .wf-app.wf-device-desktop { width: 1440px; height: 900px; max-width: 100%; aspect-ratio: 16/10; }
99
+ .wf-app.wf-device-laptop { width: 1280px; height: 800px; max-width: 100%; aspect-ratio: 16/10; }
100
+ .wf-app.wf-device-tablet { width: 768px; height: 1024px; max-width: 100%; aspect-ratio: 3/4; }
101
+ .wf-app.wf-device-tablet-landscape { width: 1024px; height: 768px; max-width: 100%; aspect-ratio: 4/3; }
102
+ .wf-app.wf-device-mobile { width: 375px; height: 812px; max-width: 100%; aspect-ratio: 375/812; }
103
+ .wf-app.wf-device-mobile-landscape { width: 812px; height: 375px; max-width: 100%; aspect-ratio: 812/375; }
104
+ .wf-scene { position: absolute; top:0; left:0; width:100%; height:100%; padding:16px; box-sizing:border-box; opacity:0; pointer-events:none; transition: opacity 0.3s ease, transform 0.3s ease; overflow-y: auto; }
105
+ .wf-scene.active { opacity:1; pointer-events:auto; }
106
+ .wf-box { border:1px solid #333; padding:12px; margin:8px 0; background:#fff; }
107
+ .wf-box-named { position: relative; margin-top: 16px; }
108
+ .wf-box-named::before { content: attr(data-name); position: absolute; top: -10px; left: 8px; background: #fff; padding: 0 4px; font-size: 12px; color: #666; }
109
+ .wf-row { display:flex; gap:12px; align-items:center; margin:4px 0; }
110
+ .wf-column { flex:1; display:flex; flex-direction:column; gap:4px; }
111
+ .wf-button { display:block; width:fit-content; padding:8px 16px; background:#fff; color:#333; border:1px solid #333; font: inherit; cursor:pointer; margin:4px 0; }
112
+ .wf-button.secondary { background:#eee; }
113
+ .wf-button.ghost { background:transparent; border:1px dashed #999; color:#666; }
114
+ .wf-input { width:100%; padding:8px; border:1px solid #333; font: inherit; box-sizing:border-box; margin:4px 0; }
115
+ .wf-link { display:block; color:#333; text-decoration:underline; cursor:pointer; margin:4px 0; }
116
+ .wf-row .wf-button { display:inline-block; margin:0; }
117
+ .wf-row .wf-link { display:inline; margin:0 8px; }
118
+ .wf-text { margin:4px 0; line-height:1.4; }
119
+ .wf-text.emphasis { font-weight:bold; }
120
+ .wf-divider { border:none; border-top:1px solid #333; margin:12px 0; }
121
+ .wf-section { border:1px solid #333; margin:8px 0; }
122
+ .wf-section-header { background:#fff; padding:4px 8px; font-size:12px; color:#666; border-bottom:1px solid #333; }
123
+ .wf-section-content { padding:8px; }
124
+ .wf-checkbox { display:flex; align-items:center; gap:8px; margin:4px 0; cursor:pointer; }
125
+ .align-center { text-align:center; }
126
+ .align-right { text-align:right; }
127
+ .wf-row.align-center { justify-content:center; }
128
+ .wf-row.align-right { justify-content:flex-end; }
129
+ .wf-button.align-center, .wf-link.align-center { margin-left:auto; margin-right:auto; }
130
+ .wf-button.align-right, .wf-link.align-right { margin-left:auto; margin-right:0; }
131
+ `
132
+
133
+ // ============================================================================
134
+ // Helper Functions
135
+ // ============================================================================
136
+
137
+ // Noise text filter - filters out box border characters
138
+ let isNoiseText = (content: string): bool => {
139
+ let trimmed = content->String.trim
140
+ if trimmed == "" {
141
+ true
142
+ } else {
143
+ // Box border patterns: +---+, |, ===, etc.
144
+ let borderPattern = %re("/^[+|=\-\s]+$/")
145
+ let hasPipeOrPlus = %re("/[+|]/")
146
+
147
+ Js.Re.test_(borderPattern, trimmed) ||
148
+ Js.Re.test_(hasPipeOrPlus, trimmed)
149
+ }
150
+ }
151
+
152
+ // Check if a box contains only inputs (should unwrap and render as inputs directly)
153
+ let isInputOnlyBox = (elem: element): bool => {
154
+ switch elem {
155
+ | Box({name: None, children}) =>
156
+ // Check if all children are Inputs (allows for multiple inputs in a box)
157
+ children->Array.length > 0 &&
158
+ children->Array.every(child => {
159
+ switch child {
160
+ | Input(_) => true
161
+ | _ => false
162
+ }
163
+ })
164
+ | _ => false
165
+ }
166
+ }
167
+
168
+ // Get inputs from a box that contains only inputs
169
+ let getInputsFromBox = (elem: element): array<element> => {
170
+ switch elem {
171
+ | Box({name: None, children}) => children
172
+ | _ => []
173
+ }
174
+ }
175
+
176
+ let alignmentToClass = (align: alignment): option<string> => {
177
+ switch align {
178
+ | Left => None
179
+ | Center => Some("align-center")
180
+ | Right => Some("align-right")
181
+ }
182
+ }
183
+
184
+ let applyAlignment = (el: DomBindings.element, align: alignment): unit => {
185
+ switch alignmentToClass(align) {
186
+ | Some(cls) => el->DomBindings.classList->DomBindings.add(cls)
187
+ | None => ()
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Convert device type to CSS class name.
193
+ */
194
+ let deviceTypeToClass = (device: deviceType): string => {
195
+ switch device {
196
+ | Desktop => "wf-device-desktop"
197
+ | Laptop => "wf-device-laptop"
198
+ | Tablet => "wf-device-tablet"
199
+ | TabletLandscape => "wf-device-tablet-landscape"
200
+ | Mobile => "wf-device-mobile"
201
+ | MobileLandscape => "wf-device-mobile-landscape"
202
+ | Custom(w, h) => "wf-device-custom-" ++ Int.toString(w) ++ "x" ++ Int.toString(h)
203
+ }
204
+ }
205
+
206
+ // ============================================================================
207
+ // Action Handler Type
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Action handler function type - called when an element's action is triggered.
212
+ * The function receives the action and should execute it (e.g., goto scene).
213
+ */
214
+ type actionHandler = interactionAction => unit
215
+
216
+ // ============================================================================
217
+ // Element Rendering
218
+ // ============================================================================
219
+
220
+ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): option<DomBindings.element> => {
221
+ // Handle input-only boxes by rendering children directly in a wrapper
222
+ if isInputOnlyBox(elem) {
223
+ let inputs = getInputsFromBox(elem)
224
+ // If only one input, render it directly
225
+ switch inputs->Array.get(0) {
226
+ | Some(input) => renderElement(input, ~onAction?)
227
+ | None => None
228
+ }
229
+ } else {
230
+ switch elem {
231
+ | Box({name, children, _}) => {
232
+ let div = DomBindings.document->DomBindings.createElement("div")
233
+ div->DomBindings.setClassName("wf-box")
234
+
235
+ switch name {
236
+ | Some(n) => {
237
+ div->DomBindings.classList->DomBindings.add("wf-box-named")
238
+ div->DomBindings.dataset->DomBindings.setDataAttr("name", n)
239
+ }
240
+ | None => ()
241
+ }
242
+
243
+ children->Array.forEach(child => {
244
+ switch renderElement(child, ~onAction?) {
245
+ | Some(el) => div->DomBindings.appendChild(el)
246
+ | None => ()
247
+ }
248
+ })
249
+
250
+ Some(div)
251
+ }
252
+
253
+ | Button({id, text, align, actions, _}) => {
254
+ let btn = DomBindings.document->DomBindings.createElement("button")
255
+ btn->DomBindings.setClassName("wf-button")
256
+ btn->DomBindings.setId(id)
257
+ btn->DomBindings.setTextContent(text)
258
+ applyAlignment(btn, align)
259
+
260
+ // Attach click handler for actions
261
+ if actions->Array.length > 0 {
262
+ switch onAction {
263
+ | Some(handler) => {
264
+ btn->DomBindings.addEventListener("click", _event => {
265
+ // Execute first action (most common case)
266
+ switch actions->Array.get(0) {
267
+ | Some(action) => handler(action)
268
+ | None => ()
269
+ }
270
+ })
271
+ }
272
+ | None => ()
273
+ }
274
+ }
275
+
276
+ Some(btn)
277
+ }
278
+
279
+ | Input({id, placeholder, _}) => {
280
+ let input = DomBindings.document->DomBindings.createElement("input")
281
+ input->DomBindings.setClassName("wf-input")
282
+ input->DomBindings.setId(id)
283
+ switch placeholder {
284
+ | Some(p) => input->DomBindings.setPlaceholder(p)
285
+ | None => ()
286
+ }
287
+ Some(input)
288
+ }
289
+
290
+ | Link({id, text, align, actions, _}) => {
291
+ let link = DomBindings.document->DomBindings.createElement("a")
292
+ link->DomBindings.setClassName("wf-link")
293
+ link->DomBindings.setId(id)
294
+ link->DomBindings.setHref("#")
295
+ link->DomBindings.setTextContent(text)
296
+ applyAlignment(link, align)
297
+
298
+ // Attach click handler for actions
299
+ if actions->Array.length > 0 {
300
+ switch onAction {
301
+ | Some(handler) => {
302
+ link->DomBindings.addEventListener("click", event => {
303
+ DomBindings.preventDefault(event)
304
+ // Execute first action
305
+ switch actions->Array.get(0) {
306
+ | Some(action) => handler(action)
307
+ | None => ()
308
+ }
309
+ })
310
+ }
311
+ | None => ()
312
+ }
313
+ }
314
+
315
+ Some(link)
316
+ }
317
+
318
+ | Checkbox({checked, label, _}) => {
319
+ let labelEl = DomBindings.document->DomBindings.createElement("label")
320
+ labelEl->DomBindings.setClassName("wf-checkbox")
321
+
322
+ let input = DomBindings.document->DomBindings.createElement("input")
323
+ input->DomBindings.setType("checkbox")
324
+ input->DomBindings.setChecked(checked)
325
+
326
+ let span = DomBindings.document->DomBindings.createElement("span")
327
+ span->DomBindings.setTextContent(label)
328
+
329
+ labelEl->DomBindings.appendChild(input)
330
+ labelEl->DomBindings.appendChild(span)
331
+ Some(labelEl)
332
+ }
333
+
334
+ | Text({content, emphasis, align, _}) => {
335
+ // Filter noise text (box borders, etc.)
336
+ if isNoiseText(content) {
337
+ None
338
+ } else {
339
+ let p = DomBindings.document->DomBindings.createElement("p")
340
+ p->DomBindings.setClassName("wf-text")
341
+ if emphasis {
342
+ p->DomBindings.classList->DomBindings.add("emphasis")
343
+ }
344
+ applyAlignment(p, align)
345
+ p->DomBindings.setTextContent(content)
346
+ Some(p)
347
+ }
348
+ }
349
+
350
+ | Divider(_) => {
351
+ let hr = DomBindings.document->DomBindings.createElement("hr")
352
+ hr->DomBindings.setClassName("wf-divider")
353
+ Some(hr)
354
+ }
355
+
356
+ | Row({children, align}) => {
357
+ let row = DomBindings.document->DomBindings.createElement("div")
358
+ row->DomBindings.setClassName("wf-row")
359
+ applyAlignment(row, align)
360
+
361
+ children->Array.forEach(child => {
362
+ switch renderElement(child, ~onAction?) {
363
+ | Some(el) => row->DomBindings.appendChild(el)
364
+ | None => ()
365
+ }
366
+ })
367
+
368
+ Some(row)
369
+ }
370
+
371
+ | Section({name, children}) => {
372
+ let section = DomBindings.document->DomBindings.createElement("div")
373
+ section->DomBindings.setClassName("wf-section")
374
+
375
+ let header = DomBindings.document->DomBindings.createElement("div")
376
+ header->DomBindings.setClassName("wf-section-header")
377
+ header->DomBindings.setTextContent(name)
378
+
379
+ let contentEl = DomBindings.document->DomBindings.createElement("div")
380
+ contentEl->DomBindings.setClassName("wf-section-content")
381
+
382
+ children->Array.forEach(child => {
383
+ switch renderElement(child, ~onAction?) {
384
+ | Some(el) => contentEl->DomBindings.appendChild(el)
385
+ | None => ()
386
+ }
387
+ })
388
+
389
+ section->DomBindings.appendChild(header)
390
+ section->DomBindings.appendChild(contentEl)
391
+ Some(section)
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ let renderScene = (scene: scene, ~onAction: option<actionHandler>=?): DomBindings.element => {
398
+ let sceneEl = DomBindings.document->DomBindings.createElement("div")
399
+ sceneEl->DomBindings.setClassName("wf-scene")
400
+ sceneEl->DomBindings.dataset->DomBindings.setDataAttr("scene", scene.id)
401
+
402
+ scene.elements->Array.forEach(elem => {
403
+ switch renderElement(elem, ~onAction?) {
404
+ | Some(el) => sceneEl->DomBindings.appendChild(el)
405
+ | None => ()
406
+ }
407
+ })
408
+
409
+ sceneEl
410
+ }
411
+
412
+ // ============================================================================
413
+ // Scene Manager Implementation
414
+ // ============================================================================
415
+
416
+ let createSceneManager = (scenes: Map.t<string, DomBindings.element>): sceneManager => {
417
+ let currentScene = ref(None)
418
+
419
+ let goto = (id: string): unit => {
420
+ switch currentScene.contents {
421
+ | Some(currentId) => {
422
+ switch scenes->Map.get(currentId) {
423
+ | Some(el) => el->DomBindings.classList->DomBindings.remove("active")
424
+ | None => ()
425
+ }
426
+ }
427
+ | None => ()
428
+ }
429
+
430
+ switch scenes->Map.get(id) {
431
+ | Some(el) => {
432
+ el->DomBindings.classList->DomBindings.add("active")
433
+ currentScene := Some(id)
434
+ }
435
+ | None => ()
436
+ }
437
+ }
438
+
439
+ let getCurrentScene = (): option<string> => currentScene.contents
440
+
441
+ let getSceneIds = (): array<string> => {
442
+ scenes->Map.keys->Iterator.toArray
443
+ }
444
+
445
+ {
446
+ goto,
447
+ getCurrentScene,
448
+ getSceneIds,
449
+ }
450
+ }
451
+
452
+ // ============================================================================
453
+ // Style Injection
454
+ // ============================================================================
455
+
456
+ let injectStyles = (): unit => {
457
+ switch DomBindings.document->DomBindings.getElementById("wf-styles")->Nullable.toOption {
458
+ | Some(_) => ()
459
+ | None => {
460
+ let style = DomBindings.document->DomBindings.createElement("style")
461
+ style->DomBindings.setId("wf-styles")
462
+ style->DomBindings.setTextContent(defaultStyles)
463
+ DomBindings.document->DomBindings.head->DomBindings.appendChild(style)
464
+ }
465
+ }
466
+ }
467
+
468
+ // ============================================================================
469
+ // Main Render Function
470
+ // ============================================================================
471
+
472
+ @genType
473
+ let render = (ast: ast, options: option<renderOptions>): renderResult => {
474
+ let opts = options->Option.getOr(defaultOptions)
475
+
476
+ if opts.injectStyles {
477
+ injectStyles()
478
+ }
479
+
480
+ let app = DomBindings.document->DomBindings.createElement("div")
481
+ app->DomBindings.setClassName("wf-app")
482
+
483
+ switch opts.containerClass {
484
+ | Some(cls) => app->DomBindings.classList->DomBindings.add(cls)
485
+ | None => ()
486
+ }
487
+
488
+ // Apply device class based on first scene's device type
489
+ switch ast.scenes->Array.get(0) {
490
+ | Some(firstScene) => {
491
+ let deviceClass = deviceTypeToClass(firstScene.device)
492
+ app->DomBindings.classList->DomBindings.add(deviceClass)
493
+ }
494
+ | None => ()
495
+ }
496
+
497
+ // Create a ref to hold the goto function (set after sceneManager is created)
498
+ let gotoRef: ref<option<string => unit>> = ref(None)
499
+
500
+ // Create action handler that uses the gotoRef
501
+ let handleAction = (action: interactionAction): unit => {
502
+ switch action {
503
+ | Goto({target, _}) => {
504
+ switch gotoRef.contents {
505
+ | Some(goto) => goto(target)
506
+ | None => ()
507
+ }
508
+ }
509
+ | Back => {
510
+ // TODO: Implement history-based back navigation
511
+ ()
512
+ }
513
+ | Forward => {
514
+ // TODO: Implement history-based forward navigation
515
+ ()
516
+ }
517
+ | Validate(_) => {
518
+ // TODO: Implement field validation
519
+ ()
520
+ }
521
+ | Call(_) => {
522
+ // TODO: Implement custom function calls
523
+ ()
524
+ }
525
+ }
526
+ }
527
+
528
+ let sceneMap = Map.make()
529
+
530
+ ast.scenes->Array.forEach(scene => {
531
+ let sceneEl = renderScene(scene, ~onAction=handleAction)
532
+ app->DomBindings.appendChild(sceneEl)
533
+ sceneMap->Map.set(scene.id, sceneEl)
534
+ })
535
+
536
+ let manager = createSceneManager(sceneMap)
537
+
538
+ // Now that sceneManager is created, set the gotoRef
539
+ gotoRef := Some(manager.goto)
540
+
541
+ if ast.scenes->Array.length > 0 {
542
+ switch ast.scenes->Array.get(0) {
543
+ | Some(firstScene) => manager.goto(firstScene.id)
544
+ | None => ()
545
+ }
546
+ }
547
+
548
+ {
549
+ root: app,
550
+ sceneManager: manager,
551
+ }
552
+ }
553
+
554
+ @genType
555
+ let toHTMLString = (_ast: ast, _options: option<renderOptions>): string => {
556
+ "<!-- Static HTML generation not yet implemented -->"
557
+ }
558
+