Initial commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user