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
|
[](https://www.npmjs.com/package/wyreframe)
|
|
6
6
|
[](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
|
-
|
|
68
|
+
## Syntax Reference
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
let ui = `
|
|
63
|
-
@scene: login
|
|
70
|
+
### UI Elements
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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: "
|
|
106
|
+
placeholder: "Email"
|
|
75
107
|
|
|
76
108
|
[Login]:
|
|
109
|
+
variant: primary
|
|
77
110
|
@click -> goto(dashboard, slide-left)
|
|
78
|
-
`
|
|
79
111
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
116
|
+
**Actions:**
|
|
90
117
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
|
94
|
-
| `
|
|
95
|
-
|
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
|
|
99
|
-
|
|
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 {
|
|
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
|
|
109
|
-
const result =
|
|
156
|
+
// Parse + Render combined (recommended)
|
|
157
|
+
const result = createUI(text, options);
|
|
110
158
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
const { root, sceneManager } = render(result.ast);
|
|
114
|
-
}
|
|
159
|
+
// Parse + Render, throw on error
|
|
160
|
+
const { root, sceneManager } = createUIOrThrow(text, options);
|
|
115
161
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
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
|
-
//
|
|
120
|
-
const
|
|
169
|
+
// Fix and return text only
|
|
170
|
+
const fixedText = fixOnly(text);
|
|
121
171
|
```
|
|
122
172
|
|
|
123
|
-
|
|
124
|
-
> When using `render()` directly, make sure to pass `result.ast`, not the result object itself.
|
|
173
|
+
### Render Options
|
|
125
174
|
|
|
126
|
-
|
|
175
|
+
```typescript
|
|
176
|
+
const options = {
|
|
177
|
+
// Additional CSS class for container
|
|
178
|
+
containerClass: 'my-app',
|
|
127
179
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
// Inject default styles (default: true)
|
|
181
|
+
injectStyles: true,
|
|
182
|
+
|
|
183
|
+
// Override device type for all scenes
|
|
184
|
+
device: 'mobile',
|
|
131
185
|
|
|
132
|
-
//
|
|
133
|
-
|
|
186
|
+
// Scene change callback
|
|
187
|
+
onSceneChange: (fromScene, toScene) => {
|
|
188
|
+
console.log(`Navigated from ${fromScene} to ${toScene}`);
|
|
189
|
+
},
|
|
134
190
|
|
|
135
|
-
//
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
sceneManager.
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
##
|
|
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
|
-
[
|
|
313
|
+
[Sign In]:
|
|
163
314
|
variant: primary
|
|
164
|
-
@click -> goto(
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
@@ -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(
|
|
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(
|
|
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
|
-
->
|
|
999
|
-
->
|
|
1000
|
-
->
|
|
1001
|
-
->
|
|
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
|
-
->
|
|
1029
|
-
->
|
|
1030
|
-
->
|
|
1031
|
-
->
|
|
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,
|