wyreframe 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Wyreframe
2
2
 
3
- > A library that converts ASCII wireframes into working HTML/UI
3
+ > A library that converts ASCII wireframes into working HTML/UI with scene management
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/wyreframe.svg)](https://www.npmjs.com/package/wyreframe)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/wyreframe.svg)](https://www.npmjs.com/package/wyreframe)
@@ -19,6 +19,15 @@
19
19
  +---------------------------+
20
20
  ```
21
21
 
22
+ ## Features
23
+
24
+ - **ASCII to HTML**: Convert simple ASCII art into interactive UI elements
25
+ - **Scene Management**: Multi-screen prototypes with transitions (fade, slide, zoom)
26
+ - **Interaction DSL**: Define button clicks, navigation, and form validation
27
+ - **Device Preview**: Responsive previews for mobile, tablet, and desktop
28
+ - **Auto-Fix**: Automatically correct common wireframe formatting issues
29
+ - **TypeScript/ReScript**: Full type safety with both language support
30
+
22
31
  ## Installation
23
32
 
24
33
  ```bash
@@ -56,121 +65,284 @@ if (result.success) {
56
65
  }
57
66
  ```
58
67
 
59
- ### ReScript
68
+ ## Syntax Reference
60
69
 
61
- ```rescript
62
- let ui = `
63
- @scene: login
70
+ ### UI Elements
64
71
 
65
- +---------------------------+
66
- | 'WYREFRAME' |
67
- | +---------------------+ |
68
- | | #email | |
69
- | +---------------------+ |
70
- | [ Login ] |
71
- +---------------------------+
72
+ | Syntax | Description | HTML Output |
73
+ |--------|-------------|-------------|
74
+ | `+---+` | Box/Container | `<div>` |
75
+ | `[ Text ]` | Button | `<button>` |
76
+ | `#id` | Input field | `<input>` |
77
+ | `"text"` | Link | `<a>` |
78
+ | `'text'` | Emphasis text | Title, Heading |
79
+ | `[x]` / `[ ]` | Checkbox | `<input type="checkbox">` |
80
+ | `---` | Divider | `<hr>` |
81
+
82
+ ### Scene Directives
83
+
84
+ ```yaml
85
+ @scene: sceneId # Scene identifier (required)
86
+ @title: Page Title # Optional page title
87
+ @device: mobile # Device type for sizing
88
+ @transition: fade # Default transition effect
89
+ ```
72
90
 
91
+ ### Device Types
92
+
93
+ | Device | Dimensions | Description |
94
+ |--------|------------|-------------|
95
+ | `desktop` | 1440x900 | Desktop monitor |
96
+ | `laptop` | 1280x800 | Laptop screen |
97
+ | `tablet` | 768x1024 | Tablet portrait |
98
+ | `tablet-landscape` | 1024x768 | Tablet landscape |
99
+ | `mobile` | 375x812 | iPhone X ratio |
100
+ | `mobile-landscape` | 812x375 | Mobile landscape |
101
+
102
+ ### Interactions
103
+
104
+ ```yaml
73
105
  #email:
74
- placeholder: "Enter your email"
106
+ placeholder: "Email"
75
107
 
76
108
  [Login]:
109
+ variant: primary
77
110
  @click -> goto(dashboard, slide-left)
78
- `
79
111
 
80
- switch Renderer.createUI(ui, None) {
81
- | Ok({root, sceneManager, _}) => {
82
- // Append root to DOM
83
- sceneManager.goto("login")
84
- }
85
- | Error(errors) => Console.error(errors)
86
- }
112
+ "Forgot Password":
113
+ @click -> goto(reset)
87
114
  ```
88
115
 
89
- ## Syntax Summary
116
+ **Actions:**
90
117
 
91
- | Syntax | Description | Example |
92
- |------|------|------|
93
- | `+---+` | Box/Container | `<div>` |
94
- | `[ Text ]` | Button | `<button>` |
95
- | `#id` | Input field | `<input>` |
96
- | `"text"` | Link | `<a>` |
97
- | `'text'` | Emphasis text | Title, Heading |
98
- | `[x]` / `[ ]` | Checkbox | `<input type="checkbox">` |
99
- | `---` | Scene separator | Multi-scene |
118
+ | Action | Description | Example |
119
+ |--------|-------------|---------|
120
+ | `goto(scene, transition?)` | Navigate to scene | `@click -> goto(home, fade)` |
121
+ | `back()` | Navigate back | `@click -> back()` |
122
+ | `forward()` | Navigate forward | `@click -> forward()` |
123
+ | `validate(fields)` | Validate inputs | `@submit -> validate(email, password)` |
124
+ | `call(fn, args)` | Custom function | `@click -> call(submit, form)` |
125
+
126
+ **Transitions:** `fade`, `slide-left`, `slide-right`, `zoom`
127
+
128
+ **Variants:** `primary`, `secondary`, `ghost`
100
129
 
101
130
  ## API
102
131
 
103
132
  ### JavaScript/TypeScript
104
133
 
105
134
  ```javascript
106
- import { parse, render, createUI, createUIOrThrow } from 'wyreframe';
135
+ import {
136
+ parse,
137
+ parseOrThrow,
138
+ render,
139
+ createUI,
140
+ createUIOrThrow,
141
+ fix,
142
+ fixOnly
143
+ } from 'wyreframe';
144
+
145
+ // Parse only - returns { success, ast, warnings } or { success: false, errors }
146
+ const parseResult = parse(text);
147
+
148
+ // Parse and throw on error
149
+ const ast = parseOrThrow(text);
150
+
151
+ // Render AST to DOM (pass ast, not parseResult!)
152
+ if (parseResult.success) {
153
+ const { root, sceneManager } = render(parseResult.ast, options);
154
+ }
107
155
 
108
- // Parse only - returns { success, ast } or { success, errors }
109
- const result = parse(text);
156
+ // Parse + Render combined (recommended)
157
+ const result = createUI(text, options);
110
158
 
111
- // Render only - IMPORTANT: pass result.ast, not result directly!
112
- if (result.success) {
113
- const { root, sceneManager } = render(result.ast);
114
- }
159
+ // Parse + Render, throw on error
160
+ const { root, sceneManager } = createUIOrThrow(text, options);
115
161
 
116
- // Parse + Render (recommended) - handles the wrapper automatically
117
- const result = createUI(text);
162
+ // Auto-fix wireframe formatting issues
163
+ const fixResult = fix(text);
164
+ if (fixResult.success) {
165
+ console.log('Fixed:', fixResult.fixed.length, 'issues');
166
+ const cleanText = fixResult.text;
167
+ }
118
168
 
119
- // Throw on error
120
- const { root, sceneManager } = createUIOrThrow(text);
169
+ // Fix and return text only
170
+ const fixedText = fixOnly(text);
121
171
  ```
122
172
 
123
- > **Note:** `parse()` returns a wrapper object `{ success: true, ast: AST }` or `{ success: false, errors: [] }`.
124
- > When using `render()` directly, make sure to pass `result.ast`, not the result object itself.
173
+ ### Render Options
125
174
 
126
- ### ReScript
175
+ ```typescript
176
+ const options = {
177
+ // Additional CSS class for container
178
+ containerClass: 'my-app',
127
179
 
128
- ```rescript
129
- // Parse only
130
- let result = Parser.parse(text)
180
+ // Inject default styles (default: true)
181
+ injectStyles: true,
182
+
183
+ // Override device type for all scenes
184
+ device: 'mobile',
131
185
 
132
- // Render only
133
- let {root, sceneManager} = Renderer.render(ast, None)
186
+ // Scene change callback
187
+ onSceneChange: (fromScene, toScene) => {
188
+ console.log(`Navigated from ${fromScene} to ${toScene}`);
189
+ },
134
190
 
135
- // Parse + Render (recommended)
136
- let result = Renderer.createUI(text, None)
191
+ // Dead-end click callback (buttons/links without navigation)
192
+ onDeadEndClick: (info) => {
193
+ console.log(`Clicked: ${info.elementText} in scene ${info.sceneId}`);
194
+ // Show modal, custom logic, etc.
195
+ }
196
+ };
137
197
 
138
- // Throw on error
139
- let {root, sceneManager, ast} = Renderer.createUIOrThrow(text, None)
198
+ const result = createUI(text, options);
140
199
  ```
141
200
 
142
201
  ### SceneManager
143
202
 
144
203
  ```javascript
145
- sceneManager.goto('dashboard'); // Navigate to scene
146
- sceneManager.getCurrentScene(); // Get current scene
147
- sceneManager.getSceneIds(); // Get all scene IDs
204
+ const { sceneManager } = result;
205
+
206
+ sceneManager.goto('dashboard'); // Navigate to scene
207
+ sceneManager.goto('home', 'fade'); // Navigate with transition
208
+ sceneManager.back(); // Go back in history
209
+ sceneManager.forward(); // Go forward in history
210
+ sceneManager.getCurrentScene(); // Get current scene ID
211
+ sceneManager.getSceneIds(); // Get all scene IDs
148
212
  ```
149
213
 
214
+ ### ReScript
215
+
150
216
  ```rescript
151
- sceneManager.goto("dashboard") // Navigate to scene
152
- sceneManager.getCurrentScene() // Get current scene (option<string>)
153
- sceneManager.getSceneIds() // Get all scene IDs (array<string>)
217
+ // Parse + Render
218
+ switch Renderer.createUI(ui, None) {
219
+ | Ok({root, sceneManager, _}) =>
220
+ sceneManager.goto("login")
221
+ | Error(errors) => Console.error(errors)
222
+ }
223
+
224
+ // With options
225
+ let options = {
226
+ device: Some(#mobile),
227
+ onSceneChange: Some((from, to) => Console.log2(from, to)),
228
+ onDeadEndClick: None,
229
+ containerClass: None,
230
+ injectStyles: None,
231
+ }
232
+ switch Renderer.createUI(ui, Some(options)) {
233
+ | Ok({root, sceneManager, _}) => ...
234
+ | Error(errors) => ...
235
+ }
154
236
  ```
155
237
 
156
- ## Interactions
238
+ ## Auto-Fix
239
+
240
+ Wyreframe can automatically fix common formatting issues:
241
+
242
+ ```javascript
243
+ import { fix, fixOnly } from 'wyreframe';
244
+
245
+ const messyWireframe = `
246
+ +----------+
247
+ | Button | <- Misaligned pipe
248
+ +---------+ <- Width mismatch
249
+ `;
250
+
251
+ const result = fix(messyWireframe);
252
+ if (result.success) {
253
+ console.log('Fixed issues:', result.fixed);
254
+ console.log('Remaining issues:', result.remaining);
255
+ console.log('Clean wireframe:', result.text);
256
+ }
257
+
258
+ // Or just get the fixed text
259
+ const cleanText = fixOnly(messyWireframe);
260
+ ```
261
+
262
+ **Fixable Issues:**
263
+ - Misaligned pipes (|)
264
+ - Mismatched border widths
265
+ - Tabs instead of spaces
266
+ - Unclosed brackets
267
+
268
+ ## Multi-Scene Example
269
+
270
+ ```javascript
271
+ const app = `
272
+ @scene: login
273
+ @device: mobile
274
+
275
+ +---------------------------+
276
+ | 'Login' |
277
+ | +---------------------+ |
278
+ | | #email | |
279
+ | +---------------------+ |
280
+ | +---------------------+ |
281
+ | | #password | |
282
+ | +---------------------+ |
283
+ | [ Sign In ] |
284
+ | |
285
+ | "Create Account" |
286
+ +---------------------------+
287
+
288
+ ---
289
+
290
+ @scene: signup
291
+ @device: mobile
292
+
293
+ +---------------------------+
294
+ | 'Sign Up' |
295
+ | +---------------------+ |
296
+ | | #name | |
297
+ | +---------------------+ |
298
+ | +---------------------+ |
299
+ | | #email | |
300
+ | +---------------------+ |
301
+ | [ Register ] |
302
+ | |
303
+ | "Back to Login" |
304
+ +---------------------------+
157
305
 
158
- ```yaml
159
306
  #email:
160
307
  placeholder: "Email"
308
+ #password:
309
+ placeholder: "Password"
310
+ #name:
311
+ placeholder: "Full Name"
161
312
 
162
- [Login]:
313
+ [Sign In]:
163
314
  variant: primary
164
- @click -> goto(dashboard, slide-left)
165
- ```
315
+ @click -> goto(signup, slide-left)
316
+
317
+ "Create Account":
318
+ @click -> goto(signup, slide-left)
319
+
320
+ [Register]:
321
+ variant: primary
322
+ @click -> goto(login, slide-right)
323
+
324
+ "Back to Login":
325
+ @click -> goto(login, slide-right)
326
+ `;
327
+
328
+ const result = createUI(app, {
329
+ onSceneChange: (from, to) => {
330
+ console.log(`Scene: ${from} -> ${to}`);
331
+ }
332
+ });
166
333
 
167
- **Transition effects:** `fade`, `slide-left`, `slide-right`, `zoom`
334
+ if (result.success) {
335
+ document.getElementById('app').appendChild(result.root);
336
+ result.sceneManager.goto('login');
337
+ }
338
+ ```
168
339
 
169
340
  ## Documentation
170
341
 
171
342
  - [API Reference](docs/api.md)
172
- - [Developer Guide](docs/developer-guide.md)
343
+ - [Type Definitions](docs/types.md)
173
344
  - [Examples](docs/examples.md)
345
+ - [Developer Guide](docs/developer-guide.md)
174
346
  - [Testing Guide](docs/testing.md)
175
347
  - [Live Demo](examples/index.html)
176
348
 
@@ -179,10 +351,24 @@ sceneManager.getSceneIds() // Get all scene IDs (array<string>)
179
351
  ```bash
180
352
  npm install
181
353
  npm run res:build # ReScript build
354
+ npm run ts:build # TypeScript build
355
+ npm run build # Full build
182
356
  npm run dev # Dev server (http://localhost:3000/examples)
183
357
  npm test # Run tests
358
+ npm run test:watch # Test watch mode
359
+ npm run test:coverage # Generate coverage report
184
360
  ```
185
361
 
362
+ ## Architecture
363
+
364
+ Wyreframe uses a 3-stage parsing pipeline:
365
+
366
+ 1. **Grid Scanner**: Converts ASCII text to 2D character grid
367
+ 2. **Shape Detector**: Identifies boxes, nesting, and hierarchy
368
+ 3. **Semantic Parser**: Recognizes UI elements via pluggable parsers
369
+
370
+ The renderer generates pure DOM elements with CSS-based scene visibility and transitions.
371
+
186
372
  ## License
187
373
 
188
374
  GPL-3.0 License - see [LICENSE](LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyreframe",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "ASCII wireframe + interaction DSL to HTML converter with scene transitions",
5
5
  "author": "wickedev",
6
6
  "repository": {
@@ -595,7 +595,7 @@ function segmentToElement(segment, basePosition, baseCol, bounds) {
595
595
  let text$1 = segment._0;
596
596
  let actualCol$1 = baseCol + segment._1 | 0;
597
597
  let position$1 = Types.Position.make(basePosition.row, actualCol$1);
598
- let id = text$1.trim().toLowerCase().replace(/\\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
598
+ let id = text$1.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
599
599
  let buttonContent = "[ " + text$1 + " ]";
600
600
  let align$1 = AlignmentCalc.calculateWithStrategy(buttonContent, position$1, bounds, "RespectPosition");
601
601
  return {
@@ -610,7 +610,7 @@ function segmentToElement(segment, basePosition, baseCol, bounds) {
610
610
  let text$2 = segment._0;
611
611
  let actualCol$2 = baseCol + segment._1 | 0;
612
612
  let position$2 = Types.Position.make(basePosition.row, actualCol$2);
613
- let id$1 = text$2.trim().toLowerCase().replace(/\\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
613
+ let id$1 = text$2.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
614
614
  let align$2 = AlignmentCalc.calculateWithStrategy(text$2, position$2, bounds, "RespectPosition");
615
615
  return {
616
616
  TAG: "Link",
@@ -992,13 +992,14 @@ let segmentToElement = (
992
992
  let position = Position.make(basePosition.row, actualCol)
993
993
 
994
994
  // Create button ID from text (slugified)
995
+ // Use String.replaceRegExp (modern API) to avoid escaping issues with %re
995
996
  let id = text
996
997
  ->String.trim
997
998
  ->String.toLowerCase
998
- ->Js.String2.replaceByRe(%re("/\\s+/g"), "-")
999
- ->Js.String2.replaceByRe(%re("/[^a-z0-9-]/g"), "")
1000
- ->Js.String2.replaceByRe(%re("/-+/g"), "-")
1001
- ->Js.String2.replaceByRe(%re("/^-+|-+$/g"), "")
999
+ ->String.replaceRegExp(%re("/\s+/g"), "-")
1000
+ ->String.replaceRegExp(%re("/[^a-z0-9-]/g"), "")
1001
+ ->String.replaceRegExp(%re("/-+/g"), "-")
1002
+ ->String.replaceRegExp(%re("/^-+|-+$/g"), "")
1002
1003
 
1003
1004
  // Use "[ text ]" format (with spaces) to match visual button width for alignment
1004
1005
  let buttonContent = "[ " ++ text ++ " ]"
@@ -1022,13 +1023,14 @@ let segmentToElement = (
1022
1023
  let position = Position.make(basePosition.row, actualCol)
1023
1024
 
1024
1025
  // Use the same slugify logic as LinkParser for consistent ID generation
1026
+ // Use String.replaceRegExp (modern API) to avoid escaping issues with %re
1025
1027
  let id = text
1026
1028
  ->String.trim
1027
1029
  ->String.toLowerCase
1028
- ->Js.String2.replaceByRe(%re("/\\s+/g"), "-")
1029
- ->Js.String2.replaceByRe(%re("/[^a-z0-9-]/g"), "")
1030
- ->Js.String2.replaceByRe(%re("/-+/g"), "-")
1031
- ->Js.String2.replaceByRe(%re("/^-+|-+$/g"), "")
1030
+ ->String.replaceRegExp(%re("/\s+/g"), "-")
1031
+ ->String.replaceRegExp(%re("/[^a-z0-9-]/g"), "")
1032
+ ->String.replaceRegExp(%re("/-+/g"), "-")
1033
+ ->String.replaceRegExp(%re("/^-+|-+$/g"), "")
1032
1034
 
1033
1035
  let align = AlignmentCalc.calculateWithStrategy(
1034
1036
  text,