Initial commit
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Screenshot Import Data Merging
|
||||
|
||||
This document provides helper functions for merging vision agent outputs into a unified data structure.
|
||||
|
||||
## Overview
|
||||
|
||||
After 3 vision agents complete (layout, components, visual properties), their outputs must be merged into a single enriched data structure suitable for component generation.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### 1. findSectionForComponent()
|
||||
|
||||
Determines which layout section contains a component:
|
||||
|
||||
```typescript
|
||||
function findSectionForComponent(compLocation, layoutStructure) {
|
||||
// Match component location to layout sections
|
||||
for (const section of layoutStructure.sections) {
|
||||
if (compLocation.section === section.id) {
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to main section if no match
|
||||
return layoutStructure.sections.find(s => s.type === 'main');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. categorizeComponents()
|
||||
|
||||
Splits components into atomic, composite, and screen categories:
|
||||
|
||||
```typescript
|
||||
function categorizeComponents(components, screenType) {
|
||||
const atomicComponents = components
|
||||
.filter(c => c.category === "atomic")
|
||||
.map(c => c.id);
|
||||
|
||||
const compositeComponents = components
|
||||
.filter(c => c.category === "composite")
|
||||
.map(c => c.id);
|
||||
|
||||
return {
|
||||
atomicComponents,
|
||||
compositeComponents,
|
||||
screenComponents: [`${screenType}-screen`]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. generateScreenName()
|
||||
|
||||
Converts screen type to human-readable name:
|
||||
|
||||
```typescript
|
||||
function generateScreenName(screenType) {
|
||||
const names = {
|
||||
"dashboard": "Dashboard",
|
||||
"login": "Login Screen",
|
||||
"form": "Form Screen",
|
||||
"list": "List View",
|
||||
"detail": "Detail View",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile Page"
|
||||
};
|
||||
|
||||
return names[screenType] || `${screenType.charAt(0).toUpperCase()}${screenType.slice(1)} Screen`;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. generateScreenDescription()
|
||||
|
||||
Creates screen description from type and layout:
|
||||
|
||||
```typescript
|
||||
function generateScreenDescription(screenType, layoutStructure) {
|
||||
const sectionNames = layoutStructure.sections.map(s => s.type).join(", ");
|
||||
return `${generateScreenName(screenType)} with ${sectionNames} sections`;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. enrichComponentWithVisualProps()
|
||||
|
||||
Merges component data with visual properties (with defaults):
|
||||
|
||||
```typescript
|
||||
function enrichComponentWithVisualProps(comp, visualResult) {
|
||||
const visualProps = visualResult.visualProperties[comp.id] || {
|
||||
dimensions: {
|
||||
width: comp.location.position.width,
|
||||
height: comp.location.position.height,
|
||||
unit: "characters"
|
||||
},
|
||||
borderStyle: "light",
|
||||
fillPattern: "transparent",
|
||||
textAlignment: "left",
|
||||
spacing: { padding: "normal", margin: "normal" }
|
||||
};
|
||||
|
||||
return {
|
||||
width: visualProps.dimensions?.width || comp.location.position.width,
|
||||
height: visualProps.dimensions?.height || comp.location.position.height,
|
||||
borderStyle: visualProps.borderStyle || "light",
|
||||
fillPattern: visualProps.fillPattern || "transparent",
|
||||
textAlignment: visualProps.textAlignment || "left",
|
||||
textContent: comp.textContent,
|
||||
placeholder: comp.placeholder || ""
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Merging Workflow
|
||||
|
||||
```typescript
|
||||
function mergeAgentResults(layoutResult, componentResult, visualResult) {
|
||||
const mergedData = {
|
||||
screen: {
|
||||
type: layoutResult.screenType,
|
||||
name: generateScreenName(layoutResult.screenType),
|
||||
description: generateScreenDescription(layoutResult.screenType, layoutResult.layoutStructure),
|
||||
layout: layoutResult.layoutStructure.type
|
||||
},
|
||||
components: componentResult.components.map(comp => {
|
||||
// Find which section contains this component
|
||||
const section = findSectionForComponent(comp.location, layoutResult.layoutStructure);
|
||||
|
||||
// Enrich with visual properties
|
||||
const visualProperties = enrichComponentWithVisualProps(comp, visualResult);
|
||||
|
||||
return {
|
||||
id: comp.id,
|
||||
type: comp.type,
|
||||
category: comp.category,
|
||||
section: section?.id || "main",
|
||||
visualProperties,
|
||||
states: comp.states || ["default"],
|
||||
accessibility: comp.accessibility,
|
||||
location: comp.location // preserve original location data
|
||||
};
|
||||
}),
|
||||
composition: categorizeComponents(componentResult.components, layoutResult.screenType),
|
||||
layoutHierarchy: layoutResult.hierarchy
|
||||
};
|
||||
|
||||
return mergedData;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
// After all 3 vision agents complete
|
||||
const layoutResult = await layoutAgent();
|
||||
const componentResult = await componentAgent();
|
||||
const visualResult = await visualAgent();
|
||||
|
||||
// Merge their outputs
|
||||
const mergedData = mergeAgentResults(layoutResult, componentResult, visualResult);
|
||||
|
||||
// mergedData is now ready for component generation
|
||||
console.log(`Merged ${mergedData.components.length} components for ${mergedData.screen.type} screen`);
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
The merged data structure:
|
||||
|
||||
```typescript
|
||||
{
|
||||
screen: {
|
||||
type: "dashboard",
|
||||
name: "Dashboard",
|
||||
description: "Dashboard with header, main sections",
|
||||
layout: "fixed-header-sidebar"
|
||||
},
|
||||
components: [
|
||||
{
|
||||
id: "email-input",
|
||||
type: "input",
|
||||
category: "atomic",
|
||||
section: "main",
|
||||
visualProperties: {
|
||||
width: 40,
|
||||
height: 3,
|
||||
borderStyle: "light",
|
||||
fillPattern: "transparent",
|
||||
textAlignment: "left",
|
||||
textContent: "Email",
|
||||
placeholder: "Enter your email"
|
||||
},
|
||||
states: ["default", "focus", "error"],
|
||||
accessibility: {
|
||||
role: "textbox",
|
||||
label: "Email address"
|
||||
},
|
||||
location: { /* original location data */ }
|
||||
}
|
||||
],
|
||||
composition: {
|
||||
atomicComponents: ["email-input", "password-input"],
|
||||
compositeComponents: ["login-form"],
|
||||
screenComponents: ["dashboard-screen"]
|
||||
},
|
||||
layoutHierarchy: {
|
||||
root: "screen",
|
||||
children: {
|
||||
header: ["logo", "navigation"],
|
||||
main: ["login-form"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,481 @@
|
||||
# Screenshot Import ASCII Generation
|
||||
|
||||
This document contains all ASCII art generation functions for screenshot-to-uxscii conversion. These functions transform visual component properties into uxscii-compliant ASCII representations.
|
||||
|
||||
## Core ASCII Functions
|
||||
|
||||
### selectBorderChars()
|
||||
|
||||
Selects box-drawing characters based on state and border style:
|
||||
|
||||
```typescript
|
||||
function selectBorderChars(state: string, baseStyle: string): string {
|
||||
// Border character map: state → style → character sequence
|
||||
// Format: "topLeft|top|topRight|side|bottomLeft|bottom|bottomRight"
|
||||
const styleMap: Record<string, Record<string, string>> = {
|
||||
'default': {
|
||||
'light': '┌|─|┐|│|└|┘',
|
||||
'rounded': '╭|─|╮|│|╰|╯',
|
||||
'double': '╔|═|╗|║|╚|╝',
|
||||
'heavy': '┏|━|┓|┃|┗|┛',
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'hover': {
|
||||
'light': '┏|━|┓|┃|┗|┛', // Upgrade to heavy
|
||||
'rounded': '┏|━|┓|┃|┗|┛', // Upgrade to heavy
|
||||
'double': '╔|═|╗|║|╚|╝', // Keep double
|
||||
'heavy': '┏|━|┓|┃|┗|┛', // Keep heavy
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'focus': {
|
||||
'light': '┏|━|┓|┃|┗|┛', // Upgrade to heavy
|
||||
'rounded': '┏|━|┓|┃|┗|┛', // Upgrade to heavy
|
||||
'double': '╔|═|╗|║|╚|╝', // Keep double
|
||||
'heavy': '┏|━|┓|┃|┗|┛', // Keep heavy
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'active': {
|
||||
'light': '┏|━|┓|┃|┗|┛',
|
||||
'rounded': '┏|━|┓|┃|┗|┛',
|
||||
'double': '╔|═|╗|║|╚|╝',
|
||||
'heavy': '┏|━|┓|┃|┗|┛',
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'disabled': {
|
||||
'light': '┌| ─ |┐|│|└| ─ |┘', // Dashed pattern
|
||||
'rounded': '╭| ─ |╮|│|╰| ─ |╯', // Dashed pattern
|
||||
'double': '╔| ═ |╗|║|╚| ═ |╝', // Dashed pattern
|
||||
'heavy': '┏| ━ |┓|┃|┗| ━ |┛', // Dashed pattern
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'error': {
|
||||
'light': '┏|━|┓|┃|┗|┛',
|
||||
'rounded': '┏|━|┓|┃|┗|┛',
|
||||
'double': '╔|═|╗|║|╚|╝',
|
||||
'heavy': '┏|━|┓|┃|┗|┛',
|
||||
'none': ' | | | | | '
|
||||
},
|
||||
'success': {
|
||||
'light': '┏|━|┓|┃|┗|┛',
|
||||
'rounded': '┏|━|┓|┃|┗|┛',
|
||||
'double': '╔|═|╗|║|╚|╝',
|
||||
'heavy': '┏|━|┓|┃|┗|┛',
|
||||
'none': ' | | | | | '
|
||||
}
|
||||
};
|
||||
|
||||
const stateStyles = styleMap[state] || styleMap['default'];
|
||||
return stateStyles[baseStyle] || styleMap['default']['light'];
|
||||
}
|
||||
```
|
||||
|
||||
**State Transformations:**
|
||||
- **hover/focus/active**: Upgrade light/rounded to heavy
|
||||
- **disabled**: Add spaces for dashed appearance
|
||||
- **error/success**: Use heavy borders for attention
|
||||
|
||||
### selectFillPattern()
|
||||
|
||||
Determines interior fill character based on component type and state:
|
||||
|
||||
```typescript
|
||||
function selectFillPattern(state: string, componentType: string): string {
|
||||
const typePatterns: Record<string, string> = {
|
||||
'button': '▓', // Solid button fill
|
||||
'input': ' ', // Empty space for text
|
||||
'checkbox': ' ', // Filled by special generator
|
||||
'radio': ' ', // Filled by special generator
|
||||
'select': ' ', // Text area
|
||||
'card': ' ', // Content area
|
||||
'modal': ' ', // Content area
|
||||
'panel': ' ', // Content area
|
||||
'alert': ' ', // Message area
|
||||
'badge': '▓', // Solid badge fill
|
||||
'progress': ' ', // Filled by special generator
|
||||
'spinner': ' ', // Filled by special generator
|
||||
'toast': ' ', // Message area
|
||||
'table': ' ', // Data cells
|
||||
'list': ' ' // List items
|
||||
};
|
||||
|
||||
const baseFill = typePatterns[componentType] || ' ';
|
||||
|
||||
// State-specific modifications
|
||||
if (state === 'hover' && componentType === 'button') {
|
||||
return '█'; // Darker fill on hover
|
||||
}
|
||||
if (state === 'disabled') {
|
||||
return ' '; // Empty on disabled
|
||||
}
|
||||
if (state === 'active' && componentType === 'button') {
|
||||
return '█'; // Full solid on active
|
||||
}
|
||||
if (state === 'focus' && componentType === 'input') {
|
||||
return '│'; // Cursor indicator
|
||||
}
|
||||
|
||||
return baseFill;
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern Types:**
|
||||
- Solid `▓`: Buttons, badges
|
||||
- Full `█`: Hover/active states
|
||||
- Cursor `│`: Input focus
|
||||
- Empty ` `: Inputs, containers
|
||||
|
||||
### buildASCIIBox()
|
||||
|
||||
Constructs ASCII box with text centering:
|
||||
|
||||
```typescript
|
||||
function buildASCIIBox(
|
||||
width: number,
|
||||
height: number,
|
||||
text: string,
|
||||
borderChars: string,
|
||||
fillPattern: string
|
||||
): string {
|
||||
const [tl, t, tr, s, bl, b, br] = borderChars.split('|');
|
||||
const lines: string[] = [];
|
||||
const innerWidth = width - 2;
|
||||
const innerHeight = height - 2;
|
||||
|
||||
// Top border
|
||||
lines.push(tl + t.repeat(innerWidth) + tr);
|
||||
|
||||
// Calculate text position (vertical center)
|
||||
const textLine = Math.floor(innerHeight / 2);
|
||||
|
||||
// Middle lines
|
||||
for (let i = 0; i < innerHeight; i++) {
|
||||
if (i === textLine && text) {
|
||||
// Center text horizontally
|
||||
const textLength = text.length;
|
||||
const paddingLeft = Math.floor((innerWidth - textLength) / 2);
|
||||
const paddingRight = innerWidth - textLength - paddingLeft;
|
||||
|
||||
const line = s +
|
||||
' '.repeat(paddingLeft) +
|
||||
text +
|
||||
' '.repeat(paddingRight) +
|
||||
s;
|
||||
lines.push(line);
|
||||
} else {
|
||||
lines.push(s + fillPattern.repeat(innerWidth) + s);
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
lines.push(bl + b.repeat(innerWidth) + br);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
```
|
||||
|
||||
### generateASCII()
|
||||
|
||||
Main ASCII generation function with optional minimal mode:
|
||||
|
||||
```typescript
|
||||
function generateASCII(
|
||||
componentId: string,
|
||||
state: string,
|
||||
visualProperties: any,
|
||||
componentType: string,
|
||||
minimalMode: boolean = false // NEW: Enable single-state generation
|
||||
): string {
|
||||
// Special component handlers
|
||||
if (componentType === 'checkbox') {
|
||||
return generateCheckbox(state, visualProperties.textContent);
|
||||
}
|
||||
if (componentType === 'radio') {
|
||||
return generateRadio(state, visualProperties.textContent);
|
||||
}
|
||||
if (componentType === 'progress') {
|
||||
return generateProgressBar(50, visualProperties.width);
|
||||
}
|
||||
if (componentType === 'spinner') {
|
||||
return generateSpinner(0);
|
||||
}
|
||||
|
||||
// Standard box components
|
||||
const borderChars = selectBorderChars(state, visualProperties.borderStyle);
|
||||
const fillPattern = selectFillPattern(state, componentType);
|
||||
|
||||
let text = visualProperties.textContent || '';
|
||||
|
||||
// Add state indicators
|
||||
if (state === 'focus' && componentType === 'button') {
|
||||
text += ' ✨';
|
||||
}
|
||||
if (state === 'error' && componentType === 'input') {
|
||||
text = '⚠️ ' + text;
|
||||
}
|
||||
if (state === 'success' && componentType === 'input') {
|
||||
text = '✅ ' + text;
|
||||
}
|
||||
|
||||
return buildASCIIBox(
|
||||
visualProperties.width,
|
||||
visualProperties.height,
|
||||
text,
|
||||
borderChars,
|
||||
fillPattern
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Minimal Mode Usage**:
|
||||
|
||||
When `minimalMode` is `true`, this function is typically only called for the 'default' state during initial component creation. This enables fast MVP component generation.
|
||||
|
||||
```typescript
|
||||
// Minimal mode - only generate default state
|
||||
const defaultOnlyASCII = generateASCII(
|
||||
'submit-button',
|
||||
'default',
|
||||
visualProperties,
|
||||
'button',
|
||||
true // minimalMode = true
|
||||
);
|
||||
|
||||
// Full mode - generate any state
|
||||
const hoverASCII = generateASCII(
|
||||
'submit-button',
|
||||
'hover',
|
||||
visualProperties,
|
||||
'button',
|
||||
false // minimalMode = false (default)
|
||||
);
|
||||
```
|
||||
|
||||
The `minimalMode` parameter doesn't change the function behavior directly, but signals intent for documentation. When creating components in minimal mode, only call this function once for 'default' state instead of looping through all states.
|
||||
|
||||
## Special Component Generators
|
||||
|
||||
### generateCheckbox()
|
||||
|
||||
```typescript
|
||||
function generateCheckbox(state: string, label: string): string {
|
||||
let box = '[ ]'; // Unchecked
|
||||
|
||||
if (state === 'checked') {
|
||||
box = '[✓]';
|
||||
} else if (state === 'indeterminate') {
|
||||
box = '[▬]';
|
||||
} else if (state === 'disabled') {
|
||||
box = '[─]';
|
||||
}
|
||||
|
||||
return `${box} ${label}`;
|
||||
}
|
||||
```
|
||||
|
||||
**States:**
|
||||
- Unchecked: `[ ]`
|
||||
- Checked: `[✓]`
|
||||
- Indeterminate: `[▬]`
|
||||
- Disabled: `[─]`
|
||||
|
||||
### generateRadio()
|
||||
|
||||
```typescript
|
||||
function generateRadio(state: string, label: string): string {
|
||||
let circle = '○'; // Unselected
|
||||
|
||||
if (state === 'selected') {
|
||||
circle = '◉';
|
||||
} else if (state === 'disabled') {
|
||||
circle = '◌';
|
||||
}
|
||||
|
||||
return `${circle} ${label}`;
|
||||
}
|
||||
```
|
||||
|
||||
**States:**
|
||||
- Unselected: `○`
|
||||
- Selected: `◉`
|
||||
- Disabled: `◌`
|
||||
|
||||
### generateProgressBar()
|
||||
|
||||
```typescript
|
||||
function generateProgressBar(percent: number, width: number): string {
|
||||
const filled = Math.floor((width * percent) / 100);
|
||||
const remaining = width - filled;
|
||||
|
||||
const bar = '█'.repeat(filled) + '░'.repeat(remaining);
|
||||
return `${bar} ${percent}%`;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:** `████░░░░░░ 40%`
|
||||
|
||||
### generateSpinner()
|
||||
|
||||
```typescript
|
||||
function generateSpinner(frame: number): string {
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
return frames[frame % frames.length];
|
||||
}
|
||||
```
|
||||
|
||||
**Animation:** 10 Braille pattern characters
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### maskPassword()
|
||||
|
||||
```typescript
|
||||
function maskPassword(value: string): string {
|
||||
return '•'.repeat(value.length);
|
||||
}
|
||||
```
|
||||
|
||||
### renderInputPlaceholder()
|
||||
|
||||
```typescript
|
||||
function renderInputPlaceholder(
|
||||
placeholder: string,
|
||||
value: string,
|
||||
width: number
|
||||
): string {
|
||||
if (value) {
|
||||
return value.padEnd(width - 2, ' ');
|
||||
} else {
|
||||
return placeholder.padEnd(width - 2, ' ');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### addGlowEffect()
|
||||
|
||||
For hover states:
|
||||
|
||||
```typescript
|
||||
function addGlowEffect(ascii: string): string {
|
||||
const lines = ascii.split('\n');
|
||||
const glowLines = lines.map(line => `░${line}░`);
|
||||
|
||||
const glowTop = '░'.repeat(glowLines[0].length);
|
||||
const glowBottom = '░'.repeat(glowLines[0].length);
|
||||
|
||||
return [glowTop, ...glowLines, glowBottom].join('\n');
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Without glow: With glow:
|
||||
╭─────────╮ ░░░░░░░░░░░░░
|
||||
│ Click │ ░╭─────────╮░
|
||||
╰─────────╯ ░│ Click │░
|
||||
░╰─────────╯░
|
||||
░░░░░░░░░░░░░
|
||||
```
|
||||
|
||||
### addValidationIndicator()
|
||||
|
||||
```typescript
|
||||
function addValidationIndicator(
|
||||
ascii: string,
|
||||
state: string,
|
||||
message?: string
|
||||
): string {
|
||||
let indicator = '';
|
||||
|
||||
if (state === 'error') {
|
||||
indicator = '⚠️';
|
||||
if (message) {
|
||||
indicator += '\n❌ ' + message;
|
||||
}
|
||||
} else if (state === 'success') {
|
||||
indicator = '✅';
|
||||
}
|
||||
|
||||
return ascii + indicator;
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Generation Example
|
||||
|
||||
Putting it all together for a button:
|
||||
|
||||
```typescript
|
||||
// Input from vision analysis
|
||||
const componentData = {
|
||||
id: 'submit-button',
|
||||
type: 'button',
|
||||
visualProperties: {
|
||||
width: 20,
|
||||
height: 3,
|
||||
borderStyle: 'rounded',
|
||||
textContent: 'Submit'
|
||||
}
|
||||
};
|
||||
|
||||
// Generate default state
|
||||
const defaultASCII = generateASCII(
|
||||
'submit-button',
|
||||
'default',
|
||||
componentData.visualProperties,
|
||||
'button'
|
||||
);
|
||||
// Result:
|
||||
// ╭──────────────────╮
|
||||
// │▓▓▓▓▓▓Submit▓▓▓▓▓▓│
|
||||
// ╰──────────────────╯
|
||||
|
||||
// Generate hover state
|
||||
const hoverASCII = generateASCII(
|
||||
'submit-button',
|
||||
'hover',
|
||||
componentData.visualProperties,
|
||||
'button'
|
||||
);
|
||||
// Result:
|
||||
// ┏━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃████████Submit████┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
// Generate disabled state
|
||||
const disabledASCII = generateASCII(
|
||||
'submit-button',
|
||||
'disabled',
|
||||
componentData.visualProperties,
|
||||
'button'
|
||||
);
|
||||
// Result:
|
||||
// ╭ ─ ─ ─ ─ ─ ─ ─ ─ ╮
|
||||
// │ Submit │
|
||||
// ╰ ─ ─ ─ ─ ─ ─ ─ ─ ╯
|
||||
```
|
||||
|
||||
## ASCII Generation Guidelines
|
||||
|
||||
**Consistency Rules:**
|
||||
1. Same dimensions across all states
|
||||
2. Border progression: default→light, hover→heavy, disabled→dashed
|
||||
3. Text always centered (horizontal and vertical)
|
||||
4. State indicators used sparingly (✨, ⚠️, ✅)
|
||||
5. Test in monospace font
|
||||
|
||||
**Performance Tips:**
|
||||
- Pre-compile border character sets
|
||||
- Cache generated ASCII for repeated components
|
||||
- Reuse fill patterns
|
||||
|
||||
**Accessibility:**
|
||||
- Keep text readable (adequate padding)
|
||||
- Use Unicode carefully (terminal support varies)
|
||||
- Provide text alternatives in metadata
|
||||
|
||||
## Reference
|
||||
|
||||
These functions implement ASCII patterns documented in:
|
||||
- `fluxwing/data/docs/06-ascii-patterns.md` - Standard box-drawing patterns
|
||||
- Examples in `fluxwing/data/examples/*.md` - Real component templates
|
||||
|
||||
Use these functions to generate consistent, high-quality ASCII representations for all uxscii components.
|
||||
@@ -0,0 +1,715 @@
|
||||
# Screenshot Import Complete Examples
|
||||
|
||||
This document contains complete end-to-end examples of screenshot-to-uxscii conversion, showing input data, generated files, and expected outputs.
|
||||
|
||||
## Example 1: Simple Button Component
|
||||
|
||||
### Input: Vision Analysis Data
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "submit-button",
|
||||
"type": "button",
|
||||
"category": "atomic",
|
||||
"visualProperties": {
|
||||
"width": 20,
|
||||
"height": 3,
|
||||
"borderStyle": "rounded",
|
||||
"textContent": "Submit"
|
||||
},
|
||||
"states": ["default", "hover", "focus", "disabled"],
|
||||
"accessibility": {
|
||||
"role": "button",
|
||||
"label": "Submit form"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: submit-button.uxm
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "submit-button",
|
||||
"type": "button",
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": "Submit Button",
|
||||
"description": "An interactive button component for user actions. Submit form",
|
||||
"author": "Fluxwing Screenshot Import",
|
||||
"created": "2025-10-13T12:00:00Z",
|
||||
"modified": "2025-10-13T12:00:00Z",
|
||||
"tags": ["button", "imported", "screenshot-generated", "utility", "interactive"],
|
||||
"category": "utility"
|
||||
},
|
||||
"props": {
|
||||
"text": "Submit",
|
||||
"variant": "secondary",
|
||||
"size": "small"
|
||||
},
|
||||
"behavior": {
|
||||
"states": [
|
||||
{
|
||||
"name": "default",
|
||||
"properties": {
|
||||
"border": "rounded",
|
||||
"background": "filled",
|
||||
"textColor": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hover",
|
||||
"properties": {
|
||||
"border": "heavy",
|
||||
"background": "filled",
|
||||
"textColor": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "focus",
|
||||
"properties": {
|
||||
"border": "heavy",
|
||||
"background": "filled",
|
||||
"textColor": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disabled",
|
||||
"properties": {
|
||||
"border": "dashed",
|
||||
"background": "filled",
|
||||
"textColor": "default",
|
||||
"opacity": 0.5,
|
||||
"cursor": "not-allowed"
|
||||
}
|
||||
}
|
||||
],
|
||||
"interactions": ["click", "keyboard"],
|
||||
"accessibility": {
|
||||
"role": "button",
|
||||
"focusable": true,
|
||||
"keyboardSupport": ["Enter", "Space"],
|
||||
"ariaLabel": "Submit form"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"display": "inline-block",
|
||||
"positioning": "static",
|
||||
"spacing": {
|
||||
"padding": { "x": 2, "y": 1 },
|
||||
"margin": { "x": 0, "y": 0 }
|
||||
},
|
||||
"sizing": {
|
||||
"minWidth": 20,
|
||||
"height": 3
|
||||
}
|
||||
},
|
||||
"ascii": {
|
||||
"templateFile": "submit-button.md",
|
||||
"width": 20,
|
||||
"height": 3,
|
||||
"variables": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "Submit",
|
||||
"description": "button label text"
|
||||
},
|
||||
{
|
||||
"name": "variant",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "primary",
|
||||
"description": "Button style variant (primary, secondary, danger)"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "medium",
|
||||
"description": "Component size (small, medium, large)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: submit-button.md
|
||||
|
||||
```markdown
|
||||
# Submit Button
|
||||
|
||||
An interactive button component for user actions. Submit form
|
||||
|
||||
## Default State
|
||||
|
||||
```
|
||||
╭──────────────────╮
|
||||
│▓▓▓▓▓▓Submit▓▓▓▓▓▓│
|
||||
╰──────────────────╯
|
||||
```
|
||||
|
||||
## Hover State
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━┓
|
||||
┃████████Submit████┃
|
||||
┗━━━━━━━━━━━━━━━━━━┛
|
||||
```
|
||||
|
||||
## Focus State
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━┓
|
||||
┃▓▓▓▓▓Submit ✨▓▓▓▓┃
|
||||
┗━━━━━━━━━━━━━━━━━━┛
|
||||
```
|
||||
|
||||
## Disabled State
|
||||
|
||||
```
|
||||
╭ ─ ─ ─ ─ ─ ─ ─ ─ ╮
|
||||
│ Submit │
|
||||
╰ ─ ─ ─ ─ ─ ─ ─ ─ ╯
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
- `text` (string, required): button label text
|
||||
- Default: "Submit"
|
||||
- Example: "Submit Form"
|
||||
|
||||
- `variant` (string, optional): Button style variant (primary, secondary, danger)
|
||||
- Default: "primary"
|
||||
|
||||
- `size` (string, optional): Component size (small, medium, large)
|
||||
- Default: "medium"
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Role**: button
|
||||
- **Focusable**: Yes
|
||||
- **Keyboard Support**: Enter, Space
|
||||
- **ARIA Label**: Submit form
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```
|
||||
╭──────────────────╮
|
||||
│▓▓▓Submit Form▓▓▓▓│
|
||||
╰──────────────────╯
|
||||
```
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Email Input Component
|
||||
|
||||
### Input: Vision Analysis Data
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "email-input",
|
||||
"type": "input",
|
||||
"category": "atomic",
|
||||
"visualProperties": {
|
||||
"width": 40,
|
||||
"height": 3,
|
||||
"borderStyle": "light",
|
||||
"textContent": "Email",
|
||||
"placeholder": "Enter your email"
|
||||
},
|
||||
"states": ["default", "focus", "error", "disabled"],
|
||||
"accessibility": {
|
||||
"role": "textbox",
|
||||
"label": "Email address"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: email-input.uxm (abbreviated)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "email-input",
|
||||
"type": "input",
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": "Email Input",
|
||||
"description": "A text input field for user data entry. Email address",
|
||||
"category": "input"
|
||||
},
|
||||
"props": {
|
||||
"text": "Email",
|
||||
"placeholder": "Enter your email",
|
||||
"type": "email",
|
||||
"size": "large",
|
||||
"maxLength": 32
|
||||
},
|
||||
"ascii": {
|
||||
"templateFile": "email-input.md",
|
||||
"width": 40,
|
||||
"height": 3,
|
||||
"variables": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "Email",
|
||||
"description": "input label text"
|
||||
},
|
||||
{
|
||||
"name": "placeholder",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "Enter your email",
|
||||
"description": "Placeholder text when empty"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "",
|
||||
"description": "Current value"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: email-input.md
|
||||
|
||||
```markdown
|
||||
# Email Input
|
||||
|
||||
A text input field for user data entry. Email address
|
||||
|
||||
## Default State
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Email │
|
||||
│ Enter your email │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Focus State
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Email │┃
|
||||
┃ john.doe@example.com ┃
|
||||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
```
|
||||
|
||||
## Error State
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ ⚠️ Email ┃
|
||||
┃ invalid@email ┃
|
||||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
❌ Please enter valid email address
|
||||
```
|
||||
|
||||
## Disabled State
|
||||
|
||||
```
|
||||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
|
||||
│ Email │
|
||||
│ ────────────────────── │
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
- `text` (string, required): input label text
|
||||
- Default: "Email"
|
||||
|
||||
- `placeholder` (string, optional): Placeholder text when empty
|
||||
- Default: "Enter your email"
|
||||
|
||||
- `value` (string, optional): Current value
|
||||
- Default: ""
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Role**: textbox
|
||||
- **Focusable**: Yes
|
||||
- **Keyboard Support**: Tab, Escape
|
||||
- **ARIA Label**: Email address
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Multi-Component Login Form
|
||||
|
||||
### Input: Vision Analysis Data
|
||||
|
||||
```json
|
||||
{
|
||||
"screen": {
|
||||
"type": "login-form",
|
||||
"name": "Login Screen",
|
||||
"description": "User authentication screen with email/password inputs",
|
||||
"layout": "vertical-center"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"id": "email-input",
|
||||
"type": "input",
|
||||
"category": "atomic",
|
||||
"visualProperties": { "width": 40, "height": 3, "borderStyle": "light", "textContent": "Email", "placeholder": "Enter your email" },
|
||||
"states": ["default", "focus", "error"],
|
||||
"accessibility": { "role": "textbox", "label": "Email address" }
|
||||
},
|
||||
{
|
||||
"id": "password-input",
|
||||
"type": "input",
|
||||
"category": "atomic",
|
||||
"visualProperties": { "width": 40, "height": 3, "borderStyle": "light", "textContent": "Password", "placeholder": "Enter your password" },
|
||||
"states": ["default", "focus", "error"],
|
||||
"accessibility": { "role": "textbox", "label": "Password" }
|
||||
},
|
||||
{
|
||||
"id": "submit-button",
|
||||
"type": "button",
|
||||
"category": "atomic",
|
||||
"visualProperties": { "width": 20, "height": 3, "borderStyle": "rounded", "textContent": "Sign In" },
|
||||
"states": ["default", "hover", "disabled"],
|
||||
"accessibility": { "role": "button", "label": "Sign in to account" }
|
||||
},
|
||||
{
|
||||
"id": "login-form",
|
||||
"type": "form",
|
||||
"category": "composite",
|
||||
"visualProperties": { "width": 50, "height": 20, "borderStyle": "rounded", "textContent": "Sign In" },
|
||||
"states": ["default"],
|
||||
"accessibility": { "role": "form", "label": "Login form" }
|
||||
}
|
||||
],
|
||||
"composition": {
|
||||
"atomicComponents": ["email-input", "password-input", "submit-button"],
|
||||
"compositeComponents": ["login-form"],
|
||||
"screenComponents": ["login-screen"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Files Generated
|
||||
|
||||
1. **Atomic Components:**
|
||||
- `./fluxwing/components/email-input.uxm`
|
||||
- `./fluxwing/components/email-input.md`
|
||||
- `./fluxwing/components/password-input.uxm`
|
||||
- `./fluxwing/components/password-input.md`
|
||||
- `./fluxwing/components/submit-button.uxm`
|
||||
- `./fluxwing/components/submit-button.md`
|
||||
|
||||
2. **Composite Component:**
|
||||
- `./fluxwing/components/login-form.uxm`
|
||||
- `./fluxwing/components/login-form.md`
|
||||
|
||||
3. **Screen:**
|
||||
- `./fluxwing/screens/login-screen.uxm`
|
||||
- `./fluxwing/screens/login-screen.md`
|
||||
- `./fluxwing/screens/login-screen.rendered.md`
|
||||
|
||||
### Output: login-form.uxm (composite)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "login-form",
|
||||
"type": "form",
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": "Login Form",
|
||||
"description": "A form container for collecting user input. Login form",
|
||||
"category": "utility"
|
||||
},
|
||||
"props": {
|
||||
"title": "Sign In",
|
||||
"components": [
|
||||
{ "id": "email-input", "slot": "field-1" },
|
||||
{ "id": "password-input", "slot": "field-2" },
|
||||
{ "id": "submit-button", "slot": "action" }
|
||||
]
|
||||
},
|
||||
"ascii": {
|
||||
"templateFile": "login-form.md",
|
||||
"width": 50,
|
||||
"height": 20,
|
||||
"variables": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"default": "Sign In",
|
||||
"description": "Form title"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: login-form.md (composite)
|
||||
|
||||
```markdown
|
||||
# Login Form
|
||||
|
||||
A form container for collecting user input. Login form
|
||||
|
||||
## Default State
|
||||
|
||||
```
|
||||
╭────────────────────────────────────────────────╮
|
||||
│ Sign In │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ {{component:email-input}} │
|
||||
│ │
|
||||
│ {{component:password-input}} │
|
||||
│ │
|
||||
│ {{component:submit-button}} │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
- `title` (string, required): Form title
|
||||
- Default: "Sign In"
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Role**: form
|
||||
- **Focusable**: No
|
||||
- **ARIA Label**: Login form
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
### Output: login-screen.rendered.md
|
||||
|
||||
```markdown
|
||||
# Login Screen
|
||||
|
||||
User authentication screen with email/password inputs
|
||||
|
||||
## Rendered Example
|
||||
|
||||
```
|
||||
╭────────────────────────────────────────────────╮
|
||||
│ Sign In │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ john.doe@example.com │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Password │ │
|
||||
│ │ •••••••• │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ╭──────────────────╮ │
|
||||
│ │▓▓▓▓Sign In▓▓▓▓▓▓▓│ │
|
||||
│ ╰──────────────────╯ │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
**Example Data:**
|
||||
- Email: john.doe@example.com
|
||||
- Password: ••••••••
|
||||
- Button: Sign In
|
||||
|
||||
**Actions:**
|
||||
- Submit form → authenticate user
|
||||
- Focus management → email → password → button
|
||||
|
||||
## Components Used
|
||||
|
||||
- `email-input` - Email Input (input)
|
||||
- `password-input` - Password Input (input)
|
||||
- `submit-button` - Submit Button (button)
|
||||
- `login-form` - Login Form (form)
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Checkbox Component
|
||||
|
||||
### Input: Vision Analysis Data
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "remember-me-checkbox",
|
||||
"type": "checkbox",
|
||||
"category": "atomic",
|
||||
"visualProperties": {
|
||||
"width": 20,
|
||||
"height": 1,
|
||||
"borderStyle": "none",
|
||||
"textContent": "Remember me"
|
||||
},
|
||||
"states": ["default", "checked", "disabled"],
|
||||
"accessibility": {
|
||||
"role": "checkbox",
|
||||
"label": "Remember login credentials"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: remember-me-checkbox.md
|
||||
|
||||
```markdown
|
||||
# Remember Me Checkbox
|
||||
|
||||
A checkbox input for boolean selection. Remember login credentials
|
||||
|
||||
## Default State
|
||||
|
||||
```
|
||||
[ ] Remember me
|
||||
```
|
||||
|
||||
## Checked State
|
||||
|
||||
```
|
||||
[✓] Remember me
|
||||
```
|
||||
|
||||
## Disabled State
|
||||
|
||||
```
|
||||
[─] Remember me
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
- `text` (string, required): checkbox label text
|
||||
- Default: "Remember me"
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Role**: checkbox
|
||||
- **Focusable**: Yes
|
||||
- **Keyboard Support**: Space
|
||||
- **ARIA Label**: Remember login credentials
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Badge Component
|
||||
|
||||
### Input: Vision Analysis Data
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "new-badge",
|
||||
"type": "badge",
|
||||
"category": "atomic",
|
||||
"visualProperties": {
|
||||
"width": 8,
|
||||
"height": 1,
|
||||
"borderStyle": "none",
|
||||
"textContent": "New"
|
||||
},
|
||||
"states": ["default"],
|
||||
"accessibility": {
|
||||
"role": "status",
|
||||
"label": "New item indicator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output: new-badge.md
|
||||
|
||||
```markdown
|
||||
# New Badge
|
||||
|
||||
A small badge component for labels or counts. New item indicator
|
||||
|
||||
## Default State
|
||||
|
||||
```
|
||||
▓ New ▓
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
- `text` (string, required): badge label text
|
||||
- Default: "New"
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Role**: status
|
||||
- **Focusable**: No
|
||||
- **ARIA Label**: New item indicator
|
||||
|
||||
---
|
||||
*Generated by Fluxwing Screenshot Import*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Pattern 1: Generate Single Atomic Component
|
||||
|
||||
```typescript
|
||||
const timestamp = new Date().toISOString();
|
||||
const uxmData = generateAtomicUXM(visionComponentData, timestamp);
|
||||
const mdContent = generateAtomicMD(visionComponentData, uxmData);
|
||||
await saveAtomicComponent(componentId, uxmData, mdContent);
|
||||
```
|
||||
|
||||
### Pattern 2: Generate All Components from Screen
|
||||
|
||||
```typescript
|
||||
// 1. Generate atomics first
|
||||
for (const atomicId of composition.atomicComponents) {
|
||||
const componentData = components.find(c => c.id === atomicId);
|
||||
await generateAtomicComponent(componentData);
|
||||
}
|
||||
|
||||
// 2. Generate composites (can reference atomics)
|
||||
for (const compositeId of composition.compositeComponents) {
|
||||
const componentData = components.find(c => c.id === compositeId);
|
||||
await generateCompositeComponent(componentData);
|
||||
}
|
||||
|
||||
// 3. Generate screen (references everything)
|
||||
await generateScreen(screenData, composition);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
These examples demonstrate the complete screenshot-to-uxscii workflow:
|
||||
|
||||
1. **Vision analysis** produces structured JSON
|
||||
2. **Helper functions** (see screenshot-import-helpers.md) transform data
|
||||
3. **ASCII generation** (see screenshot-import-ascii.md) creates visual representations
|
||||
4. **File generation** produces valid .uxm + .md pairs
|
||||
5. **Validation** ensures quality and schema compliance
|
||||
|
||||
All generated files conform to:
|
||||
- `fluxwing/data/schema/uxm-component.schema.json` - JSON Schema
|
||||
- Quality standards documented in the schema
|
||||
@@ -0,0 +1,610 @@
|
||||
# Screenshot Import Helper Functions
|
||||
|
||||
This document contains all helper functions used during screenshot-to-uxscii conversion. These functions transform vision analysis data into valid uxscii component structures.
|
||||
|
||||
## Component Metadata Helpers
|
||||
|
||||
### mapTypeToCategory()
|
||||
|
||||
Maps component type to UXM category for proper organization:
|
||||
|
||||
```typescript
|
||||
function mapTypeToCategory(componentType: string): string {
|
||||
const categoryMap = {
|
||||
// Input category - interactive data entry
|
||||
'input': 'input', 'checkbox': 'input', 'radio': 'input',
|
||||
'select': 'input', 'slider': 'input', 'toggle': 'input',
|
||||
|
||||
// Layout category - structural containers
|
||||
'container': 'layout', 'card': 'layout', 'panel': 'layout',
|
||||
'tabs': 'layout', 'fieldset': 'layout',
|
||||
|
||||
// Display category - content presentation
|
||||
'text': 'display', 'heading': 'display', 'label': 'display',
|
||||
'badge': 'display', 'icon': 'display', 'image': 'display',
|
||||
'divider': 'display',
|
||||
|
||||
// Navigation category - movement and wayfinding
|
||||
'navigation': 'navigation', 'breadcrumb': 'navigation',
|
||||
'pagination': 'navigation', 'link': 'navigation',
|
||||
|
||||
// Feedback category - system responses
|
||||
'alert': 'feedback', 'toast': 'feedback', 'progress': 'feedback',
|
||||
'spinner': 'feedback',
|
||||
|
||||
// Utility category - action triggers
|
||||
'button': 'utility', 'form': 'utility',
|
||||
|
||||
// Overlay category - modal displays
|
||||
'modal': 'overlay',
|
||||
|
||||
// Data category - structured information
|
||||
'list': 'data', 'table': 'data', 'tree': 'data', 'chart': 'data'
|
||||
};
|
||||
|
||||
return categoryMap[componentType] || 'custom';
|
||||
}
|
||||
```
|
||||
|
||||
### generateComponentName()
|
||||
|
||||
Creates human-readable component name from kebab-case ID:
|
||||
|
||||
```typescript
|
||||
function generateComponentName(componentId: string): string {
|
||||
// Convert kebab-case to Title Case
|
||||
return componentId
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Examples:
|
||||
// "email-input" → "Email Input"
|
||||
// "submit-button" → "Submit Button"
|
||||
// "user-profile-card" → "User Profile Card"
|
||||
```
|
||||
|
||||
### generateComponentDescription()
|
||||
|
||||
Creates description based on type, properties, and accessibility:
|
||||
|
||||
```typescript
|
||||
function generateComponentDescription(
|
||||
componentType: string,
|
||||
visualProperties: any,
|
||||
accessibility: any
|
||||
): string {
|
||||
const typeDescriptions = {
|
||||
'button': 'An interactive button component for user actions',
|
||||
'input': 'A text input field for user data entry',
|
||||
'checkbox': 'A checkbox input for boolean selection',
|
||||
'radio': 'A radio button for single selection from a group',
|
||||
'select': 'A dropdown select component for choosing from options',
|
||||
'card': 'A container component for grouping related content',
|
||||
'modal': 'An overlay component for focused interactions',
|
||||
'alert': 'A feedback component for displaying messages',
|
||||
'navigation': 'A navigation component for site/app navigation',
|
||||
'form': 'A form container for collecting user input',
|
||||
'badge': 'A small badge component for labels or counts',
|
||||
'icon': 'An icon component for visual symbols',
|
||||
'text': 'A text display component',
|
||||
'heading': 'A heading component for section titles',
|
||||
'divider': 'A visual divider for separating content'
|
||||
};
|
||||
|
||||
let description = typeDescriptions[componentType] || `A ${componentType} component`;
|
||||
|
||||
// Add context from accessibility label if available
|
||||
if (accessibility?.label && accessibility.label !== visualProperties.textContent) {
|
||||
description += `. ${accessibility.label}`;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior Helpers
|
||||
|
||||
### inferBackground()
|
||||
|
||||
Determines background fill pattern based on component type:
|
||||
|
||||
```typescript
|
||||
function inferBackground(componentType: string): string {
|
||||
const backgroundMap = {
|
||||
'button': 'filled', // Buttons have solid background
|
||||
'input': 'transparent', // Inputs are hollow
|
||||
'card': 'filled', // Cards have background
|
||||
'modal': 'filled', // Modals have background
|
||||
'alert': 'filled', // Alerts have background
|
||||
'badge': 'filled', // Badges have background
|
||||
'panel': 'filled', // Panels have background
|
||||
'toast': 'filled' // Toasts have background
|
||||
};
|
||||
|
||||
return backgroundMap[componentType] || 'transparent';
|
||||
}
|
||||
```
|
||||
|
||||
### generateInteractions()
|
||||
|
||||
Generates interaction array based on component type:
|
||||
|
||||
```typescript
|
||||
function generateInteractions(componentType: string): string[] {
|
||||
const interactionMap = {
|
||||
'button': ['click', 'keyboard'],
|
||||
'input': ['click', 'keyboard', 'type'],
|
||||
'checkbox': ['click', 'keyboard'],
|
||||
'radio': ['click', 'keyboard'],
|
||||
'select': ['click', 'keyboard'],
|
||||
'slider': ['click', 'drag', 'keyboard'],
|
||||
'toggle': ['click', 'keyboard'],
|
||||
'link': ['click', 'keyboard'],
|
||||
'tabs': ['click', 'keyboard'],
|
||||
'navigation': ['click', 'keyboard'],
|
||||
'modal': ['click', 'keyboard'], // Close on ESC
|
||||
'toast': [], // No interaction (auto-dismiss)
|
||||
'progress': [], // No interaction (passive display)
|
||||
'spinner': [] // No interaction (passive display)
|
||||
};
|
||||
|
||||
return interactionMap[componentType] || [];
|
||||
}
|
||||
```
|
||||
|
||||
### isFocusable()
|
||||
|
||||
Determines if component should be focusable for keyboard navigation:
|
||||
|
||||
```typescript
|
||||
function isFocusable(componentType: string): boolean {
|
||||
const focusableTypes = [
|
||||
'button', 'input', 'checkbox', 'radio', 'select',
|
||||
'slider', 'toggle', 'link', 'tabs', 'navigation'
|
||||
];
|
||||
|
||||
return focusableTypes.includes(componentType);
|
||||
}
|
||||
```
|
||||
|
||||
### generateKeyboardSupport()
|
||||
|
||||
Generates keyboard shortcuts based on component type:
|
||||
|
||||
```typescript
|
||||
function generateKeyboardSupport(componentType: string): string[] {
|
||||
const keyboardMap = {
|
||||
'button': ['Enter', 'Space'],
|
||||
'input': ['Tab', 'Escape'],
|
||||
'checkbox': ['Space'],
|
||||
'radio': ['Arrow keys'],
|
||||
'select': ['Arrow keys', 'Enter', 'Escape'],
|
||||
'slider': ['Arrow keys', 'Home', 'End'],
|
||||
'toggle': ['Space'],
|
||||
'link': ['Enter'],
|
||||
'tabs': ['Arrow keys', 'Home', 'End'],
|
||||
'navigation': ['Arrow keys', 'Enter'],
|
||||
'modal': ['Escape']
|
||||
};
|
||||
|
||||
return keyboardMap[componentType] || [];
|
||||
}
|
||||
```
|
||||
|
||||
### generateStatesFromList()
|
||||
|
||||
Creates state objects for each state in the states array:
|
||||
|
||||
```typescript
|
||||
function generateStatesFromList(
|
||||
states: string[],
|
||||
baseProperties: any,
|
||||
componentType: string
|
||||
): any[] {
|
||||
const stateObjects = [];
|
||||
|
||||
for (const stateName of states) {
|
||||
if (stateName === 'default') continue; // Skip default, handled separately
|
||||
|
||||
const stateProperties: any = {};
|
||||
|
||||
// State-specific border styles
|
||||
if (stateName === 'hover' || stateName === 'focus') {
|
||||
stateProperties.border = 'heavy';
|
||||
} else if (stateName === 'disabled') {
|
||||
stateProperties.border = 'dashed';
|
||||
stateProperties.opacity = 0.5;
|
||||
stateProperties.cursor = 'not-allowed';
|
||||
} else if (stateName === 'error') {
|
||||
stateProperties.border = 'heavy';
|
||||
stateProperties.borderColor = 'red';
|
||||
} else if (stateName === 'success') {
|
||||
stateProperties.border = 'heavy';
|
||||
stateProperties.borderColor = 'green';
|
||||
} else if (stateName === 'loading') {
|
||||
stateProperties.opacity = 0.7;
|
||||
stateProperties.cursor = 'wait';
|
||||
} else if (stateName === 'active') {
|
||||
stateProperties.border = 'heavy';
|
||||
stateProperties.background = 'filled';
|
||||
}
|
||||
|
||||
// Copy base properties and merge with state-specific ones
|
||||
stateObjects.push({
|
||||
name: stateName,
|
||||
properties: { ...baseProperties, ...stateProperties }
|
||||
});
|
||||
}
|
||||
|
||||
return stateObjects;
|
||||
}
|
||||
```
|
||||
|
||||
### generateMinimalDefaultState()
|
||||
|
||||
Creates a single default state object for MVP component creation (fast mode):
|
||||
|
||||
```typescript
|
||||
function generateMinimalDefaultState(
|
||||
visualProperties: any,
|
||||
componentType: string
|
||||
): any {
|
||||
return {
|
||||
name: 'default',
|
||||
properties: {
|
||||
border: visualProperties.borderStyle || 'light',
|
||||
background: inferBackground(componentType),
|
||||
textColor: 'default'
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**: This function creates ONLY the default state, enabling fast MVP component creation. Use `generateStatesFromList()` later when expanding components with `/fluxwing-expand-component`.
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Minimal mode - single state
|
||||
const minimalStates = [generateMinimalDefaultState(visualProps, 'button')];
|
||||
|
||||
// Full mode - multiple states
|
||||
const fullStates = [
|
||||
generateMinimalDefaultState(visualProps, 'button'),
|
||||
...generateStatesFromList(['hover', 'active', 'disabled'], baseProps, 'button')
|
||||
];
|
||||
```
|
||||
|
||||
## Layout Helpers
|
||||
|
||||
### inferDisplay()
|
||||
|
||||
Determines CSS display property based on component type:
|
||||
|
||||
```typescript
|
||||
function inferDisplay(componentType: string): string {
|
||||
const displayMap = {
|
||||
'button': 'inline-block',
|
||||
'input': 'inline-block',
|
||||
'checkbox': 'inline-block',
|
||||
'radio': 'inline-block',
|
||||
'badge': 'inline',
|
||||
'link': 'inline',
|
||||
'text': 'inline',
|
||||
'heading': 'block',
|
||||
'divider': 'block',
|
||||
'card': 'block',
|
||||
'panel': 'block',
|
||||
'modal': 'block',
|
||||
'container': 'block',
|
||||
'form': 'block',
|
||||
'list': 'block',
|
||||
'table': 'block'
|
||||
};
|
||||
|
||||
return displayMap[componentType] || 'block';
|
||||
}
|
||||
```
|
||||
|
||||
### generateSpacing()
|
||||
|
||||
Calculates padding based on component dimensions (~10% of size):
|
||||
|
||||
```typescript
|
||||
function generateSpacing(width: number, height: number): any {
|
||||
const paddingX = Math.max(1, Math.floor(width * 0.1));
|
||||
const paddingY = Math.max(1, Math.floor(height * 0.1));
|
||||
|
||||
return {
|
||||
padding: {
|
||||
x: paddingX,
|
||||
y: paddingY
|
||||
},
|
||||
margin: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Props & Variables Helpers
|
||||
|
||||
### extractVariables()
|
||||
|
||||
Extracts variable definitions from visual properties:
|
||||
|
||||
```typescript
|
||||
function extractVariables(
|
||||
visualProperties: any,
|
||||
componentType: string
|
||||
): any[] {
|
||||
const variables: any[] = [];
|
||||
|
||||
// Text content variable (common to most components)
|
||||
if (visualProperties.textContent) {
|
||||
variables.push({
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: visualProperties.textContent,
|
||||
description: `${componentType} label text`
|
||||
});
|
||||
}
|
||||
|
||||
// Placeholder variable (for inputs)
|
||||
if (visualProperties.placeholder) {
|
||||
variables.push({
|
||||
name: 'placeholder',
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: visualProperties.placeholder,
|
||||
description: 'Placeholder text when empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Value variable (for inputs/displays)
|
||||
if (['input', 'select', 'text'].includes(componentType)) {
|
||||
variables.push({
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: '',
|
||||
description: 'Current value'
|
||||
});
|
||||
}
|
||||
|
||||
// Variant variable (for buttons)
|
||||
if (componentType === 'button') {
|
||||
variables.push({
|
||||
name: 'variant',
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: 'primary',
|
||||
description: 'Button style variant (primary, secondary, danger)'
|
||||
});
|
||||
}
|
||||
|
||||
// Size variable (for scalable components)
|
||||
if (['button', 'input', 'badge'].includes(componentType)) {
|
||||
variables.push({
|
||||
name: 'size',
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: 'medium',
|
||||
description: 'Component size (small, medium, large)'
|
||||
});
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
```
|
||||
|
||||
### extractPropsFromVisualProperties()
|
||||
|
||||
Extract component props based on type and visual properties:
|
||||
|
||||
```typescript
|
||||
function extractPropsFromVisualProperties(
|
||||
visualProperties: any,
|
||||
componentType: string
|
||||
): any {
|
||||
const props: any = {};
|
||||
|
||||
// Text content (most components)
|
||||
if (visualProperties.textContent) {
|
||||
props.text = visualProperties.textContent;
|
||||
}
|
||||
|
||||
// Placeholder (inputs)
|
||||
if (visualProperties.placeholder) {
|
||||
props.placeholder = visualProperties.placeholder;
|
||||
}
|
||||
|
||||
// Type-specific props
|
||||
if (componentType === 'button') {
|
||||
props.variant = inferButtonVariant(visualProperties);
|
||||
props.size = inferSize(visualProperties.width, visualProperties.height);
|
||||
}
|
||||
|
||||
if (componentType === 'input') {
|
||||
props.type = inferInputType(visualProperties);
|
||||
props.size = inferSize(visualProperties.width, visualProperties.height);
|
||||
props.maxLength = Math.floor(visualProperties.width * 0.8);
|
||||
}
|
||||
|
||||
if (componentType === 'checkbox' || componentType === 'radio') {
|
||||
props.label = visualProperties.textContent;
|
||||
props.checked = false; // Default unchecked
|
||||
}
|
||||
|
||||
if (componentType === 'badge') {
|
||||
props.variant = inferBadgeVariant(visualProperties);
|
||||
}
|
||||
|
||||
if (componentType === 'icon') {
|
||||
props.name = visualProperties.textContent || 'icon';
|
||||
props.size = inferSize(visualProperties.width, visualProperties.height);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
```
|
||||
|
||||
## Inference Helpers
|
||||
|
||||
### inferButtonVariant()
|
||||
|
||||
```typescript
|
||||
function inferButtonVariant(vp: any): string {
|
||||
// Primary buttons typically have filled backgrounds
|
||||
// Secondary buttons have borders
|
||||
if (vp.borderStyle === 'heavy' || vp.borderStyle === 'double') {
|
||||
return 'primary';
|
||||
}
|
||||
return 'secondary';
|
||||
}
|
||||
```
|
||||
|
||||
### inferInputType()
|
||||
|
||||
```typescript
|
||||
function inferInputType(vp: any): string {
|
||||
const text = vp.textContent?.toLowerCase() || '';
|
||||
const placeholder = vp.placeholder?.toLowerCase() || '';
|
||||
|
||||
if (text.includes('password') || placeholder.includes('password')) {
|
||||
return 'password';
|
||||
}
|
||||
if (text.includes('email') || placeholder.includes('email')) {
|
||||
return 'email';
|
||||
}
|
||||
if (text.includes('number') || placeholder.includes('number')) {
|
||||
return 'number';
|
||||
}
|
||||
if (text.includes('search')) {
|
||||
return 'search';
|
||||
}
|
||||
return 'text';
|
||||
}
|
||||
```
|
||||
|
||||
### inferSize()
|
||||
|
||||
```typescript
|
||||
function inferSize(width: number, height: number): string {
|
||||
// Small: < 20 width or < 3 height
|
||||
// Medium: 20-40 width, 3-5 height
|
||||
// Large: > 40 width or > 5 height
|
||||
if (width < 20 || height < 3) return 'small';
|
||||
if (width > 40 || height > 5) return 'large';
|
||||
return 'medium';
|
||||
}
|
||||
```
|
||||
|
||||
### inferBadgeVariant()
|
||||
|
||||
```typescript
|
||||
function inferBadgeVariant(vp: any): string {
|
||||
// Could be inferred from color analysis in future
|
||||
// For now, default to neutral
|
||||
return 'neutral';
|
||||
}
|
||||
```
|
||||
|
||||
### inferAdditionalTags()
|
||||
|
||||
```typescript
|
||||
function inferAdditionalTags(type: string, vp: any): string[] {
|
||||
const tags: string[] = [];
|
||||
|
||||
// Add category tags
|
||||
const category = mapTypeToCategory(type);
|
||||
if (category !== 'custom') {
|
||||
tags.push(category);
|
||||
}
|
||||
|
||||
// Add interaction tags
|
||||
if (isFocusable(type)) {
|
||||
tags.push('interactive');
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
These helper functions work together to transform vision analysis into complete .uxm files:
|
||||
|
||||
```typescript
|
||||
// Example: Generate atomic component .uxm
|
||||
function generateAtomicUXM(componentData: any, timestamp: string): any {
|
||||
const { id, type, visualProperties, states, accessibility } = componentData;
|
||||
|
||||
return {
|
||||
"id": id,
|
||||
"type": type,
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": generateComponentName(id),
|
||||
"description": generateComponentDescription(type, visualProperties, accessibility),
|
||||
"author": "Fluxwing Screenshot Import",
|
||||
"created": timestamp,
|
||||
"modified": timestamp,
|
||||
"tags": [type, "imported", "screenshot-generated", ...inferAdditionalTags(type, visualProperties)],
|
||||
"category": mapTypeToCategory(type)
|
||||
},
|
||||
"props": extractPropsFromVisualProperties(visualProperties, type),
|
||||
"behavior": {
|
||||
"states": [
|
||||
{
|
||||
"name": "default",
|
||||
"properties": {
|
||||
"border": visualProperties.borderStyle,
|
||||
"background": inferBackground(type),
|
||||
"textColor": "default"
|
||||
}
|
||||
},
|
||||
...generateStatesFromList(
|
||||
states.filter(s => s !== 'default'),
|
||||
{ border: visualProperties.borderStyle, background: inferBackground(type), textColor: "default" },
|
||||
type
|
||||
)
|
||||
],
|
||||
"interactions": generateInteractions(type),
|
||||
"accessibility": {
|
||||
"role": accessibility.role,
|
||||
"focusable": isFocusable(type),
|
||||
"keyboardSupport": generateKeyboardSupport(type),
|
||||
"ariaLabel": accessibility.label || visualProperties.textContent
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"display": inferDisplay(type),
|
||||
"positioning": "static",
|
||||
"spacing": generateSpacing(visualProperties.width, visualProperties.height),
|
||||
"sizing": {
|
||||
"minWidth": visualProperties.width,
|
||||
"height": visualProperties.height
|
||||
}
|
||||
},
|
||||
"ascii": {
|
||||
"templateFile": `${id}.md`,
|
||||
"width": visualProperties.width,
|
||||
"height": visualProperties.height,
|
||||
"variables": extractVariables(visualProperties, type)
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
These functions implement the uxscii component specification documented in:
|
||||
- `fluxwing/data/docs/01-uxscii-specification.md` - Format specification
|
||||
- `fluxwing/data/docs/03-component-creation.md` - Component structure
|
||||
- `fluxwing/data/schema/uxm-component.schema.json` - JSON Schema validation
|
||||
|
||||
Use these helpers to ensure consistent, valid component generation from screenshot analysis data.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Screenshot Import Screen Generation
|
||||
|
||||
This document provides helper functions for generating screen files (.uxm, .md, .rendered.md) during screenshot import.
|
||||
|
||||
## Overview
|
||||
|
||||
Screen generation creates 3 files:
|
||||
1. **`.uxm`** - Screen metadata and component references
|
||||
2. **`.md`** - Template with `{{component:id}}` references
|
||||
3. **`.rendered.md`** - Actual ASCII preview with real data
|
||||
|
||||
## Step 1: Generate Screen Metadata (.uxm)
|
||||
|
||||
```typescript
|
||||
function generateScreenUxm(mergedData) {
|
||||
const screenId = `${mergedData.screen.type}-screen`;
|
||||
const screenName = mergedData.screen.name;
|
||||
const screenDescription = mergedData.screen.description;
|
||||
const allComponentIds = [
|
||||
...mergedData.composition.atomicComponents,
|
||||
...mergedData.composition.compositeComponents
|
||||
];
|
||||
|
||||
return {
|
||||
"id": screenId,
|
||||
"type": "container",
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": screenName,
|
||||
"description": screenDescription,
|
||||
"author": "Fluxwing Screenshot Import",
|
||||
"created": new Date().toISOString(),
|
||||
"modified": new Date().toISOString(),
|
||||
"tags": ["screen", mergedData.screen.type, "imported"],
|
||||
"category": "layout"
|
||||
},
|
||||
"props": {
|
||||
"title": screenName,
|
||||
"layout": mergedData.screen.layout,
|
||||
"components": allComponentIds
|
||||
},
|
||||
"ascii": {
|
||||
"templateFile": `${screenId}.md`,
|
||||
"width": 80,
|
||||
"height": 40
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Generate Screen Template (.md)
|
||||
|
||||
```typescript
|
||||
function generateScreenTemplate(screenId, screenName, description, components, mergedData) {
|
||||
let markdown = `# ${screenName}\n\n${description}\n\n`;
|
||||
|
||||
markdown += `## Layout\n\n\`\`\`\n`;
|
||||
|
||||
// Reference all components
|
||||
for (const compId of components) {
|
||||
markdown += `{{component:${compId}}}\n\n`;
|
||||
}
|
||||
|
||||
markdown += '\`\`\`\n\n';
|
||||
|
||||
markdown += `## Components Used\n\n`;
|
||||
for (const compId of components) {
|
||||
const comp = mergedData.components.find(c => c.id === compId);
|
||||
const compName = comp ? generateComponentName(compId) : compId;
|
||||
markdown += `- \`${compId}\` - ${compName} (${comp?.type || 'unknown'})\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
// Helper: Convert kebab-case ID to Title Case name
|
||||
function generateComponentName(id) {
|
||||
return id.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Generate Rendered Screen (.rendered.md)
|
||||
|
||||
**CRITICAL:** Embed actual ASCII from component files, NOT `{{variables}}`
|
||||
|
||||
```typescript
|
||||
async function generateScreenRendered(screenId, screenName, description, mergedData) {
|
||||
let markdown = `# ${screenName}\n\n`;
|
||||
|
||||
markdown += `## Rendered Example\n\n\`\`\`\n`;
|
||||
|
||||
// Build complete rendered layout
|
||||
// Stack all components vertically (user can refine layout)
|
||||
const allComponentIds = [
|
||||
...mergedData.composition.atomicComponents,
|
||||
...mergedData.composition.compositeComponents
|
||||
];
|
||||
|
||||
for (const compId of allComponentIds) {
|
||||
// Read component .md file
|
||||
const compMdPath = `./fluxwing/components/${compId}.md`;
|
||||
try {
|
||||
const compMdContent = await read(compMdPath);
|
||||
|
||||
// Extract default state ASCII (between first ``` pair after "## Default State")
|
||||
const asciiMatch = compMdContent.match(/## Default State\n\n```\n([\s\S]*?)\n```/);
|
||||
if (asciiMatch) {
|
||||
markdown += asciiMatch[1] + '\n\n';
|
||||
} else {
|
||||
markdown += `[Component ${compId} - ASCII not found]\n\n`;
|
||||
}
|
||||
} catch (error) {
|
||||
markdown += `[Component ${compId} - File not found]\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
markdown += '\`\`\`\n\n';
|
||||
|
||||
// Add example data section
|
||||
markdown += `**Example Data:**\n`;
|
||||
const exampleData = generateExampleData(mergedData.screen.type);
|
||||
for (const [key, value] of Object.entries(exampleData)) {
|
||||
markdown += `- ${key}: ${value}\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Generate Example Data by Screen Type
|
||||
|
||||
```typescript
|
||||
function generateExampleData(screenType) {
|
||||
const examples = {
|
||||
"login": {
|
||||
"Email": "john.doe@example.com",
|
||||
"Password": "••••••••",
|
||||
"Button": "Sign In"
|
||||
},
|
||||
"dashboard": {
|
||||
"Revenue": "$24,567",
|
||||
"Users": "1,234",
|
||||
"Growth": "+12.5%"
|
||||
},
|
||||
"profile": {
|
||||
"Name": "Jane Smith",
|
||||
"Role": "Product Manager",
|
||||
"Email": "jane.smith@company.com"
|
||||
},
|
||||
"settings": {
|
||||
"Theme": "Dark Mode",
|
||||
"Language": "English",
|
||||
"Notifications": "Enabled"
|
||||
},
|
||||
"form": {
|
||||
"Field 1": "Example value",
|
||||
"Field 2": "Another value"
|
||||
}
|
||||
};
|
||||
|
||||
return examples[screenType] || examples["form"];
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Write All Files Concurrently
|
||||
|
||||
```typescript
|
||||
async function writeScreenFiles(screenId, screenUxm, screenMd, screenRendered) {
|
||||
// Create screens directory
|
||||
await bash('mkdir -p ./fluxwing/screens');
|
||||
|
||||
const screenDir = './fluxwing/screens';
|
||||
const uxmPath = `${screenDir}/${screenId}.uxm`;
|
||||
const mdPath = `${screenDir}/${screenId}.md`;
|
||||
const renderedPath = `${screenDir}/${screenId}.rendered.md`;
|
||||
|
||||
// Write all 3 files concurrently
|
||||
await Promise.all([
|
||||
write(uxmPath, JSON.stringify(screenUxm, null, 2)),
|
||||
write(mdPath, screenMd),
|
||||
write(renderedPath, screenRendered)
|
||||
]);
|
||||
|
||||
return { uxmPath, mdPath, renderedPath };
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Workflow
|
||||
|
||||
```typescript
|
||||
async function generateScreen(mergedData) {
|
||||
const screenId = `${mergedData.screen.type}-screen`;
|
||||
const allComponentIds = [
|
||||
...mergedData.composition.atomicComponents,
|
||||
...mergedData.composition.compositeComponents
|
||||
];
|
||||
|
||||
// Generate all content
|
||||
const screenUxm = generateScreenUxm(mergedData);
|
||||
const screenMd = generateScreenTemplate(
|
||||
screenId,
|
||||
mergedData.screen.name,
|
||||
mergedData.screen.description,
|
||||
allComponentIds,
|
||||
mergedData
|
||||
);
|
||||
const screenRendered = await generateScreenRendered(
|
||||
screenId,
|
||||
mergedData.screen.name,
|
||||
mergedData.screen.description,
|
||||
mergedData
|
||||
);
|
||||
|
||||
// Write all files
|
||||
const paths = await writeScreenFiles(screenId, screenUxm, screenMd, screenRendered);
|
||||
|
||||
console.log(`✓ Created: ${paths.uxmPath}`);
|
||||
console.log(`✓ Created: ${paths.mdPath}`);
|
||||
console.log(`✓ Created: ${paths.renderedPath}`);
|
||||
|
||||
return { screenId, ...paths };
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const screenResult = await generateScreen(mergedData);
|
||||
console.log(`✅ Screen generated with ${allComponentIds.length} components`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to generate screen: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,221 @@
|
||||
# Screenshot Import Validation Functions
|
||||
|
||||
This document provides validation function implementations for verifying generated uxscii components during screenshot import.
|
||||
|
||||
## Overview
|
||||
|
||||
Five validation checks run concurrently for each component:
|
||||
|
||||
1. **validateSchema()** - Check against JSON Schema
|
||||
2. **validateFileIntegrity()** - Check template file exists and matches
|
||||
3. **validateVariableConsistency()** - Check variables are defined and used
|
||||
4. **validateComponentReferences()** - Check referenced components exist
|
||||
5. **validateBestPractices()** - Check quality standards (warnings only)
|
||||
|
||||
## Validation Function Implementations
|
||||
|
||||
### 1. validateSchema()
|
||||
|
||||
Check against JSON Schema:
|
||||
|
||||
```typescript
|
||||
async function validateSchema(uxmPath) {
|
||||
const schemaPath = '{PLUGIN_ROOT}/data/schema/uxm-component.schema.json';
|
||||
const schema = JSON.parse(await read(schemaPath));
|
||||
const uxmData = JSON.parse(await read(uxmPath));
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Required fields
|
||||
if (!uxmData.id || !uxmData.type || !uxmData.version || !uxmData.metadata) {
|
||||
errors.push("Missing required top-level fields");
|
||||
}
|
||||
|
||||
// ID format
|
||||
if (uxmData.id && !uxmData.id.match(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)) {
|
||||
errors.push(`Invalid ID format: ${uxmData.id}`);
|
||||
}
|
||||
|
||||
// Version format
|
||||
if (uxmData.version && !uxmData.version.match(/^\d+\.\d+\.\d+$/)) {
|
||||
errors.push(`Invalid version format: ${uxmData.version}`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Schema validation errors: ${errors.join(', ')}`);
|
||||
}
|
||||
|
||||
return { passed: true, message: "Schema compliance verified" };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. validateFileIntegrity()
|
||||
|
||||
Check template file exists and matches:
|
||||
|
||||
```typescript
|
||||
async function validateFileIntegrity(uxmPath, mdPath) {
|
||||
const uxmData = JSON.parse(await read(uxmPath));
|
||||
const expectedTemplateName = uxmData.ascii.templateFile;
|
||||
|
||||
// Check .md file exists
|
||||
try {
|
||||
await read(mdPath);
|
||||
} catch (error) {
|
||||
throw new Error(`Template file missing: ${mdPath}`);
|
||||
}
|
||||
|
||||
// Check filename matches reference
|
||||
const actualFileName = mdPath.split('/').pop();
|
||||
if (expectedTemplateName !== actualFileName) {
|
||||
throw new Error(`Template filename mismatch: expected ${expectedTemplateName}, got ${actualFileName}`);
|
||||
}
|
||||
|
||||
return { passed: true, message: "File integrity verified" };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. validateVariableConsistency()
|
||||
|
||||
Check variables are defined and used:
|
||||
|
||||
```typescript
|
||||
async function validateVariableConsistency(uxmPath, mdPath) {
|
||||
const uxmData = JSON.parse(await read(uxmPath));
|
||||
const mdContent = await read(mdPath);
|
||||
|
||||
// Extract defined variables from .uxm
|
||||
const definedVariables = uxmData.ascii?.variables ? uxmData.ascii.variables.map(v => v.name) : [];
|
||||
|
||||
// Extract used variables from .md (matches {{variableName}} but NOT {{component:id}})
|
||||
const usedVariables = [...mdContent.matchAll(/\{\{(?!component:)(\w+)\}\}/g)].map(m => m[1]);
|
||||
|
||||
// Check all used variables are defined
|
||||
const undefinedVars = usedVariables.filter(v => !definedVariables.includes(v));
|
||||
if (undefinedVars.length > 0) {
|
||||
throw new Error(`Undefined variables in template: ${undefinedVars.join(', ')}`);
|
||||
}
|
||||
|
||||
// Warn about unused variables (non-blocking)
|
||||
const unusedVars = definedVariables.filter(v => !usedVariables.includes(v));
|
||||
if (unusedVars.length > 0) {
|
||||
console.warn(`⚠️ Unused variables defined in ${uxmPath}: ${unusedVars.join(', ')}`);
|
||||
}
|
||||
|
||||
return { passed: true, message: "Variable consistency verified", warnings: unusedVars.length };
|
||||
}
|
||||
```
|
||||
|
||||
### 4. validateComponentReferences()
|
||||
|
||||
Check referenced components exist:
|
||||
|
||||
```typescript
|
||||
async function validateComponentReferences(uxmPath) {
|
||||
const uxmData = JSON.parse(await read(uxmPath));
|
||||
|
||||
// Check if component has child references
|
||||
if (!uxmData.props.components || uxmData.props.components.length === 0) {
|
||||
return { passed: true, message: "No component references to validate" };
|
||||
}
|
||||
|
||||
// Check each referenced component exists
|
||||
for (const ref of uxmData.props.components) {
|
||||
const refPath = `./fluxwing/components/${ref.id}.uxm`;
|
||||
try {
|
||||
await read(refPath);
|
||||
} catch (error) {
|
||||
throw new Error(`Referenced component not found: ${ref.id} (expected at ${refPath})`);
|
||||
}
|
||||
}
|
||||
|
||||
return { passed: true, message: `${uxmData.props.components.length} component references verified` };
|
||||
}
|
||||
```
|
||||
|
||||
### 5. validateBestPractices()
|
||||
|
||||
Check quality standards (warnings only):
|
||||
|
||||
```typescript
|
||||
async function validateBestPractices(uxmPath) {
|
||||
const uxmData = JSON.parse(await read(uxmPath));
|
||||
const warnings = [];
|
||||
|
||||
// Check multiple states
|
||||
if (uxmData.behavior?.states && uxmData.behavior.states.length < 3) {
|
||||
warnings.push("Consider adding more states (recommended: 3+)");
|
||||
}
|
||||
|
||||
// Check accessibility
|
||||
if (!uxmData.behavior?.accessibility?.ariaLabel) {
|
||||
warnings.push("Missing ARIA label for accessibility");
|
||||
}
|
||||
|
||||
// Check description
|
||||
if (!uxmData.metadata.description || uxmData.metadata.description.length < 10) {
|
||||
warnings.push("Description is too brief");
|
||||
}
|
||||
|
||||
// Check tags
|
||||
if (!uxmData.metadata.tags || uxmData.metadata.tags.length < 2) {
|
||||
warnings.push("Add more tags for discoverability");
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.warn(`⚠️ Best practices warnings for ${uxmData.id}:\n - ${warnings.join('\n - ')}`);
|
||||
}
|
||||
|
||||
return { passed: true, warnings: warnings.length, messages: warnings };
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Pattern
|
||||
|
||||
Run all 5 validations concurrently for each component:
|
||||
|
||||
```typescript
|
||||
const validationResults = await Promise.all(
|
||||
allFiles.map(async (fileSet) => {
|
||||
const { uxmPath, mdPath, id } = fileSet;
|
||||
|
||||
try {
|
||||
const [
|
||||
schemaResult,
|
||||
integrityResult,
|
||||
variableResult,
|
||||
referenceResult,
|
||||
practicesResult
|
||||
] = await Promise.all([
|
||||
validateSchema(uxmPath),
|
||||
validateFileIntegrity(uxmPath, mdPath),
|
||||
validateVariableConsistency(uxmPath, mdPath),
|
||||
validateComponentReferences(uxmPath),
|
||||
validateBestPractices(uxmPath)
|
||||
]);
|
||||
|
||||
return {
|
||||
componentId: id,
|
||||
success: true,
|
||||
checks: {
|
||||
schema: schemaResult,
|
||||
integrity: integrityResult,
|
||||
variables: variableResult,
|
||||
references: referenceResult,
|
||||
practices: practicesResult
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Validation failed for ${id}: ${error.message}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Schema, Integrity, Variables, References**: Fail-fast (throw errors)
|
||||
- **Best Practices**: Non-blocking (warnings only)
|
||||
- Always provide component ID in error messages
|
||||
- Collect all warnings for summary report
|
||||
Reference in New Issue
Block a user