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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyreframe",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "ASCII wireframe + interaction DSL to HTML converter with scene transitions",
5
5
  "author": "wickedev",
6
6
  "repository": {
@@ -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) => void;
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 !== undefined) {
186
- return onAction(action);
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 !== undefined) {
220
- return onAction(action);
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
- applyAlignment(row, elem.align);
263
- elem.children.forEach(child => {
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
- return back();
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
- return forward();
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 => unit,
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 => unit
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) => handler(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) => handler(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
- applyAlignment(row, align)
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): unit => {
546
- // Add current scene to history before navigating
547
- switch currentScene.contents {
548
- | Some(currentId) if currentId != id => {
549
- historyStack := historyStack.contents->Array.concat([currentId])
550
- // Clear forward stack when navigating to new scene
551
- forwardStack := []
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 => unit>> = ref(None)
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
- let handleAction = (action: interactionAction): unit => {
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) => back()
716
- | None => ()
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) => forward()
722
- | None => ()
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) => manager.goto(firstScene.id)
857
+ | Some(firstScene) => {
858
+ let _ = manager.goto(firstScene.id)
859
+ }
768
860
  | None => ()
769
861
  }
770
862
  }