wyreframe 0.7.8 → 0.7.10
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.
- package/package.json +1 -1
- package/src/renderer/Renderer.gen.tsx +7 -1
- package/src/renderer/Renderer.mjs +64 -12
- package/src/renderer/Renderer.res +116 -24
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@ import type {deviceType as Types_deviceType} from '../../src/parser/Core/Types.g
|
|
|
11
11
|
|
|
12
12
|
import type {t as ErrorTypes_t} from '../../src/parser/Errors/ErrorTypes.gen';
|
|
13
13
|
|
|
14
|
+
import type {t as Map_t} from './Map.gen';
|
|
15
|
+
|
|
14
16
|
export abstract class DomBindings_element { protected opaque!: any }; /* simulate opaque types */
|
|
15
17
|
|
|
16
18
|
/** * Scene change callback type.
|
|
@@ -48,7 +50,7 @@ export type renderOptions = {
|
|
|
48
50
|
|
|
49
51
|
/** * Scene management interface returned by render function. */
|
|
50
52
|
export type sceneManager = {
|
|
51
|
-
readonly goto: (_1:string) =>
|
|
53
|
+
readonly goto: (_1:string) => boolean;
|
|
52
54
|
readonly back: () => void;
|
|
53
55
|
readonly forward: () => void;
|
|
54
56
|
readonly refresh: () => void;
|
|
@@ -74,6 +76,10 @@ export type createUIResult =
|
|
|
74
76
|
{ TAG: "Ok"; _0: createUISuccessResult }
|
|
75
77
|
| { TAG: "Error"; _0: ErrorTypes_t[] };
|
|
76
78
|
|
|
79
|
+
/** * Creates a scene manager for navigation between scenes.
|
|
80
|
+
* Exported for testing purposes. */
|
|
81
|
+
export const createSceneManager: (scenes:Map_t<string,DomBindings_element>, onSceneChange:(undefined | onSceneChangeCallback)) => sceneManager = RendererJS.createSceneManager as any;
|
|
82
|
+
|
|
77
83
|
export const render: (ast:Types_ast, options:(undefined | renderOptions)) => renderResult = RendererJS.render as any;
|
|
78
84
|
|
|
79
85
|
export const toHTMLString: (_ast:Types_ast, _options:(undefined | renderOptions)) => string = RendererJS.toHTMLString as any;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Parser from "../parser/Parser.mjs";
|
|
4
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.mjs";
|
|
4
5
|
import * as Core__Option from "@rescript/core/src/Core__Option.mjs";
|
|
5
6
|
import * as ErrorMessages from "../parser/Errors/ErrorMessages.mjs";
|
|
6
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
@@ -52,6 +53,7 @@ let defaultStyles = `
|
|
|
52
53
|
.align-right { text-align:right; }
|
|
53
54
|
.wf-row.align-center { justify-content:center; }
|
|
54
55
|
.wf-row.align-right { justify-content:flex-end; }
|
|
56
|
+
.wf-row.distribute { justify-content:space-between; }
|
|
55
57
|
.wf-button.align-center, .wf-link.align-center { margin-left:auto; margin-right:auto; }
|
|
56
58
|
.wf-button.align-right, .wf-link.align-right { margin-left:auto; margin-right:0; }
|
|
57
59
|
`;
|
|
@@ -102,6 +104,35 @@ function applyAlignment(el, align) {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
function getElementAlignment(elem) {
|
|
108
|
+
switch (elem.TAG) {
|
|
109
|
+
case "Button" :
|
|
110
|
+
case "Link" :
|
|
111
|
+
case "Text" :
|
|
112
|
+
return elem.align;
|
|
113
|
+
default:
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasDistributedChildren(children) {
|
|
119
|
+
let alignments = Core__Array.filterMap(children, getElementAlignment);
|
|
120
|
+
if (alignments.length < 2) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let hasLeft = alignments.some(a => a === "Left");
|
|
124
|
+
let hasCenter = alignments.some(a => a === "Center");
|
|
125
|
+
let hasRight = alignments.some(a => a === "Right");
|
|
126
|
+
let differentAlignmentCount = ((
|
|
127
|
+
hasLeft ? 1 : 0
|
|
128
|
+
) + (
|
|
129
|
+
hasCenter ? 1 : 0
|
|
130
|
+
) | 0) + (
|
|
131
|
+
hasRight ? 1 : 0
|
|
132
|
+
) | 0;
|
|
133
|
+
return differentAlignmentCount >= 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
function deviceTypeToClass(device) {
|
|
106
137
|
if (typeof device === "object") {
|
|
107
138
|
return "wf-device-custom-" + device._0.toString() + "x" + device._1.toString();
|
|
@@ -182,8 +213,12 @@ function renderElement(_elem, onAction, onDeadEnd) {
|
|
|
182
213
|
if (onAction !== undefined) {
|
|
183
214
|
btn.addEventListener("click", _event => {
|
|
184
215
|
let action = actions[0];
|
|
185
|
-
if (action
|
|
186
|
-
return
|
|
216
|
+
if (action === undefined) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let success = onAction(action);
|
|
220
|
+
if (!success && onDeadEnd !== undefined) {
|
|
221
|
+
return onDeadEnd(id, text, "button");
|
|
187
222
|
}
|
|
188
223
|
});
|
|
189
224
|
}
|
|
@@ -216,8 +251,12 @@ function renderElement(_elem, onAction, onDeadEnd) {
|
|
|
216
251
|
link.addEventListener("click", event => {
|
|
217
252
|
event.preventDefault();
|
|
218
253
|
let action = actions$1[0];
|
|
219
|
-
if (action
|
|
220
|
-
return
|
|
254
|
+
if (action === undefined) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
let success = onAction(action);
|
|
258
|
+
if (!success && onDeadEnd !== undefined) {
|
|
259
|
+
return onDeadEnd(id$1, text$1, "link");
|
|
221
260
|
}
|
|
222
261
|
});
|
|
223
262
|
}
|
|
@@ -257,10 +296,15 @@ function renderElement(_elem, onAction, onDeadEnd) {
|
|
|
257
296
|
spacer.className = "wf-spacer";
|
|
258
297
|
return Primitive_option.some(spacer);
|
|
259
298
|
case "Row" :
|
|
299
|
+
let children = elem.children;
|
|
260
300
|
let row = document.createElement("div");
|
|
261
301
|
row.className = "wf-row";
|
|
262
|
-
|
|
263
|
-
|
|
302
|
+
if (hasDistributedChildren(children)) {
|
|
303
|
+
row.classList.add("distribute");
|
|
304
|
+
} else {
|
|
305
|
+
applyAlignment(row, elem.align);
|
|
306
|
+
}
|
|
307
|
+
children.forEach(child => {
|
|
264
308
|
let el = renderElement(child, onAction, onDeadEnd);
|
|
265
309
|
if (el !== undefined) {
|
|
266
310
|
row.appendChild(Primitive_option.valFromOption(el));
|
|
@@ -339,12 +383,16 @@ function createSceneManager(scenes, onSceneChange) {
|
|
|
339
383
|
}
|
|
340
384
|
};
|
|
341
385
|
let goto = id => {
|
|
386
|
+
if (!scenes.has(id)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
342
389
|
let currentId = currentScene.contents;
|
|
343
390
|
if (currentId !== undefined && currentId !== id) {
|
|
344
391
|
historyStack.contents = historyStack.contents.concat([currentId]);
|
|
345
392
|
forwardStack.contents = [];
|
|
346
393
|
}
|
|
347
394
|
switchToScene(id, undefined);
|
|
395
|
+
return true;
|
|
348
396
|
};
|
|
349
397
|
let back = () => {
|
|
350
398
|
let history = historyStack.contents;
|
|
@@ -459,16 +507,18 @@ function render(ast, options) {
|
|
|
459
507
|
if (action === "Back") {
|
|
460
508
|
let back = backRef.contents;
|
|
461
509
|
if (back !== undefined) {
|
|
462
|
-
|
|
510
|
+
back();
|
|
511
|
+
return true;
|
|
463
512
|
} else {
|
|
464
|
-
return;
|
|
513
|
+
return false;
|
|
465
514
|
}
|
|
466
515
|
}
|
|
467
516
|
let forward = forwardRef.contents;
|
|
468
517
|
if (forward !== undefined) {
|
|
469
|
-
|
|
518
|
+
forward();
|
|
519
|
+
return true;
|
|
470
520
|
} else {
|
|
471
|
-
return;
|
|
521
|
+
return false;
|
|
472
522
|
}
|
|
473
523
|
} else {
|
|
474
524
|
switch (action.TAG) {
|
|
@@ -477,11 +527,11 @@ function render(ast, options) {
|
|
|
477
527
|
if (goto !== undefined) {
|
|
478
528
|
return goto(action.target);
|
|
479
529
|
} else {
|
|
480
|
-
return;
|
|
530
|
+
return false;
|
|
481
531
|
}
|
|
482
532
|
case "Validate" :
|
|
483
533
|
case "Call" :
|
|
484
|
-
return;
|
|
534
|
+
return true;
|
|
485
535
|
}
|
|
486
536
|
}
|
|
487
537
|
};
|
|
@@ -565,6 +615,8 @@ export {
|
|
|
565
615
|
getInputsFromBox,
|
|
566
616
|
alignmentToClass,
|
|
567
617
|
applyAlignment,
|
|
618
|
+
getElementAlignment,
|
|
619
|
+
hasDistributedChildren,
|
|
568
620
|
deviceTypeToClass,
|
|
569
621
|
isNavigationAction,
|
|
570
622
|
hasNavigationAction,
|
|
@@ -108,7 +108,7 @@ let defaultOptions: renderOptions = {
|
|
|
108
108
|
* Scene management interface returned by render function.
|
|
109
109
|
*/
|
|
110
110
|
type sceneManager = {
|
|
111
|
-
goto: string =>
|
|
111
|
+
goto: string => bool,
|
|
112
112
|
back: unit => unit,
|
|
113
113
|
forward: unit => unit,
|
|
114
114
|
refresh: unit => unit,
|
|
@@ -164,6 +164,7 @@ let defaultStyles = `
|
|
|
164
164
|
.align-right { text-align:right; }
|
|
165
165
|
.wf-row.align-center { justify-content:center; }
|
|
166
166
|
.wf-row.align-right { justify-content:flex-end; }
|
|
167
|
+
.wf-row.distribute { justify-content:space-between; }
|
|
167
168
|
.wf-button.align-center, .wf-link.align-center { margin-left:auto; margin-right:auto; }
|
|
168
169
|
.wf-button.align-right, .wf-link.align-right { margin-left:auto; margin-right:0; }
|
|
169
170
|
`
|
|
@@ -211,6 +212,48 @@ let applyAlignment = (el: DomBindings.element, align: alignment): unit => {
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Get the alignment of an element (for buttons, links, text).
|
|
217
|
+
*/
|
|
218
|
+
let getElementAlignment = (elem: element): option<alignment> => {
|
|
219
|
+
switch elem {
|
|
220
|
+
| Button({align, _}) => Some(align)
|
|
221
|
+
| Link({align, _}) => Some(align)
|
|
222
|
+
| Text({align, _}) => Some(align)
|
|
223
|
+
| _ => None
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if Row children have distributed alignments (Left/Center/Right pattern).
|
|
229
|
+
* This indicates the buttons should be distributed using justify-content: space-between.
|
|
230
|
+
*
|
|
231
|
+
* Issue #23: When buttons are positioned across a row with different alignments
|
|
232
|
+
* (e.g., [ Google ] [ Apple ] [ GitHub ] with Left/Center/Right),
|
|
233
|
+
* use flexbox space-between distribution instead of per-element alignment.
|
|
234
|
+
*/
|
|
235
|
+
let hasDistributedChildren = (children: array<element>): bool => {
|
|
236
|
+
// Get all child alignments
|
|
237
|
+
let alignments = children
|
|
238
|
+
->Array.filterMap(getElementAlignment)
|
|
239
|
+
|
|
240
|
+
// Need at least 2 elements to be considered distributed
|
|
241
|
+
if alignments->Array.length < 2 {
|
|
242
|
+
false
|
|
243
|
+
} else {
|
|
244
|
+
// Check if we have different alignments
|
|
245
|
+
let hasLeft = alignments->Array.some(a => a == Left)
|
|
246
|
+
let hasCenter = alignments->Array.some(a => a == Center)
|
|
247
|
+
let hasRight = alignments->Array.some(a => a == Right)
|
|
248
|
+
|
|
249
|
+
// Distributed means we have at least 2 different alignments
|
|
250
|
+
let differentAlignmentCount =
|
|
251
|
+
(hasLeft ? 1 : 0) + (hasCenter ? 1 : 0) + (hasRight ? 1 : 0)
|
|
252
|
+
|
|
253
|
+
differentAlignmentCount >= 2
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
214
257
|
/**
|
|
215
258
|
* Convert device type to CSS class name.
|
|
216
259
|
*/
|
|
@@ -233,8 +276,9 @@ let deviceTypeToClass = (device: deviceType): string => {
|
|
|
233
276
|
/**
|
|
234
277
|
* Action handler function type - called when an element's action is triggered.
|
|
235
278
|
* The function receives the action and should execute it (e.g., goto scene).
|
|
279
|
+
* Returns true if the action was successful, false if it failed (e.g., scene doesn't exist).
|
|
236
280
|
*/
|
|
237
|
-
type actionHandler = interactionAction =>
|
|
281
|
+
type actionHandler = interactionAction => bool
|
|
238
282
|
|
|
239
283
|
/**
|
|
240
284
|
* Dead end handler function type - called when an element without navigation is clicked.
|
|
@@ -317,7 +361,17 @@ let rec renderElement = (
|
|
|
317
361
|
btn->DomBindings.addEventListener("click", _event => {
|
|
318
362
|
// Execute first action (most common case)
|
|
319
363
|
switch actions->Array.get(0) {
|
|
320
|
-
| Some(action) =>
|
|
364
|
+
| Some(action) => {
|
|
365
|
+
let success = handler(action)
|
|
366
|
+
// If navigation failed (e.g., target scene doesn't exist),
|
|
367
|
+
// treat it as a dead end (Issue #24)
|
|
368
|
+
if !success {
|
|
369
|
+
switch onDeadEnd {
|
|
370
|
+
| Some(deadEndHandler) => deadEndHandler(id, text, #button)
|
|
371
|
+
| None => ()
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
321
375
|
| None => ()
|
|
322
376
|
}
|
|
323
377
|
})
|
|
@@ -369,7 +423,17 @@ let rec renderElement = (
|
|
|
369
423
|
DomBindings.preventDefault(event)
|
|
370
424
|
// Execute first action
|
|
371
425
|
switch actions->Array.get(0) {
|
|
372
|
-
| Some(action) =>
|
|
426
|
+
| Some(action) => {
|
|
427
|
+
let success = handler(action)
|
|
428
|
+
// If navigation failed (e.g., target scene doesn't exist),
|
|
429
|
+
// treat it as a dead end (Issue #24)
|
|
430
|
+
if !success {
|
|
431
|
+
switch onDeadEnd {
|
|
432
|
+
| Some(deadEndHandler) => deadEndHandler(id, text, #link)
|
|
433
|
+
| None => ()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
373
437
|
| None => ()
|
|
374
438
|
}
|
|
375
439
|
})
|
|
@@ -435,7 +499,13 @@ let rec renderElement = (
|
|
|
435
499
|
| Row({children, align}) => {
|
|
436
500
|
let row = DomBindings.document->DomBindings.createElement("div")
|
|
437
501
|
row->DomBindings.setClassName("wf-row")
|
|
438
|
-
|
|
502
|
+
|
|
503
|
+
// Issue #23: Use space-between distribution for rows with distributed alignments
|
|
504
|
+
if hasDistributedChildren(children) {
|
|
505
|
+
row->DomBindings.classList->DomBindings.add("distribute")
|
|
506
|
+
} else {
|
|
507
|
+
applyAlignment(row, align)
|
|
508
|
+
}
|
|
439
509
|
|
|
440
510
|
children->Array.forEach(child => {
|
|
441
511
|
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
|
|
@@ -496,6 +566,11 @@ let renderScene = (
|
|
|
496
566
|
// Scene Manager Implementation
|
|
497
567
|
// ============================================================================
|
|
498
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Creates a scene manager for navigation between scenes.
|
|
571
|
+
* Exported for testing purposes.
|
|
572
|
+
*/
|
|
573
|
+
@genType
|
|
499
574
|
let createSceneManager = (
|
|
500
575
|
scenes: Map.t<string, DomBindings.element>,
|
|
501
576
|
~onSceneChange: option<onSceneChangeCallback>=?,
|
|
@@ -542,17 +617,25 @@ let createSceneManager = (
|
|
|
542
617
|
}
|
|
543
618
|
}
|
|
544
619
|
|
|
545
|
-
let goto = (id: string):
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
620
|
+
let goto = (id: string): bool => {
|
|
621
|
+
// Check if target scene exists first
|
|
622
|
+
if !(scenes->Map.has(id)) {
|
|
623
|
+
// Scene doesn't exist - return false to indicate failure
|
|
624
|
+
// Do NOT modify history or switch scenes
|
|
625
|
+
false
|
|
626
|
+
} else {
|
|
627
|
+
// Add current scene to history before navigating
|
|
628
|
+
switch currentScene.contents {
|
|
629
|
+
| Some(currentId) if currentId != id => {
|
|
630
|
+
historyStack := historyStack.contents->Array.concat([currentId])
|
|
631
|
+
// Clear forward stack when navigating to new scene
|
|
632
|
+
forwardStack := []
|
|
633
|
+
}
|
|
634
|
+
| _ => ()
|
|
552
635
|
}
|
|
553
|
-
|
|
636
|
+
switchToScene(id)
|
|
637
|
+
true
|
|
554
638
|
}
|
|
555
|
-
switchToScene(id)
|
|
556
639
|
}
|
|
557
640
|
|
|
558
641
|
let back = (): unit => {
|
|
@@ -697,38 +780,45 @@ let render = (ast: ast, options: option<renderOptions>): renderResult => {
|
|
|
697
780
|
}
|
|
698
781
|
|
|
699
782
|
// Create refs to hold navigation functions (set after sceneManager is created)
|
|
700
|
-
let gotoRef: ref<option<string =>
|
|
783
|
+
let gotoRef: ref<option<string => bool>> = ref(None)
|
|
701
784
|
let backRef: ref<option<unit => unit>> = ref(None)
|
|
702
785
|
let forwardRef: ref<option<unit => unit>> = ref(None)
|
|
703
786
|
|
|
704
787
|
// Create action handler that uses the refs
|
|
705
|
-
|
|
788
|
+
// Returns true if action succeeded, false if it failed (e.g., target scene doesn't exist)
|
|
789
|
+
let handleAction = (action: interactionAction): bool => {
|
|
706
790
|
switch action {
|
|
707
791
|
| Goto({target, _}) => {
|
|
708
792
|
switch gotoRef.contents {
|
|
709
793
|
| Some(goto) => goto(target)
|
|
710
|
-
| None =>
|
|
794
|
+
| None => false
|
|
711
795
|
}
|
|
712
796
|
}
|
|
713
797
|
| Back => {
|
|
714
798
|
switch backRef.contents {
|
|
715
|
-
| Some(back) =>
|
|
716
|
-
|
|
799
|
+
| Some(back) => {
|
|
800
|
+
back()
|
|
801
|
+
true
|
|
802
|
+
}
|
|
803
|
+
| None => false
|
|
717
804
|
}
|
|
718
805
|
}
|
|
719
806
|
| Forward => {
|
|
720
807
|
switch forwardRef.contents {
|
|
721
|
-
| Some(forward) =>
|
|
722
|
-
|
|
808
|
+
| Some(forward) => {
|
|
809
|
+
forward()
|
|
810
|
+
true
|
|
811
|
+
}
|
|
812
|
+
| None => false
|
|
723
813
|
}
|
|
724
814
|
}
|
|
725
815
|
| Validate(_) => {
|
|
726
816
|
// TODO: Implement field validation
|
|
727
|
-
|
|
817
|
+
true
|
|
728
818
|
}
|
|
729
819
|
| Call(_) => {
|
|
730
820
|
// TODO: Implement custom function calls
|
|
731
|
-
|
|
821
|
+
true
|
|
732
822
|
}
|
|
733
823
|
}
|
|
734
824
|
}
|
|
@@ -764,7 +854,9 @@ let render = (ast: ast, options: option<renderOptions>): renderResult => {
|
|
|
764
854
|
|
|
765
855
|
if ast.scenes->Array.length > 0 {
|
|
766
856
|
switch ast.scenes->Array.get(0) {
|
|
767
|
-
| Some(firstScene) =>
|
|
857
|
+
| Some(firstScene) => {
|
|
858
|
+
let _ = manager.goto(firstScene.id)
|
|
859
|
+
}
|
|
768
860
|
| None => ()
|
|
769
861
|
}
|
|
770
862
|
}
|