611 lines
16 KiB
Markdown
611 lines
16 KiB
Markdown
# 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.
|