First push

This commit is contained in:
Sam Redfern committed 2025-04-28 10:06:04 +10:00
1 parent db25d6dbda
commit 7299578599
48 files changed
+17513 -1

No files matched your search

+33
View File
@@ -130,3 +130,36 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# dependencies
/node_modules
/client/node_modules
/server/node_modules
/.pnp
.pnp.js
# testing
/coverage
/client/coverage
/server/coverage
# production
/build
/client/build
/client/dist
/server/dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
/server/.env
# IDE
.idea
.vscode
*.swp
*.swo
+93 -1
View File
@@ -1,2 +1,94 @@
# mock_tool_image_gen # UI Mockup Generator
A web application for creating UI mockups with standardized JSON configuration that can be generated by LLMs.
## Overview
The UI Mockup Generator allows users to create, customize, and export mockup UI elements for various interfaces. The tool provides a simple yet powerful interface for designing professional-looking UI mockups, with a specific focus on making the configuration format LLM-friendly.
![UI Mockup Generator](https://placeholder-image-url.com/ui-mockup-generator.png)
## Features
- **Component Library**: Funnel diagrams, charts, cards, form elements, and more
- **Style Customization**: Color pickers, typography settings, spacing controls
- **Layout Management**: Drag-and-drop interface, grid alignment, layers
- **Export Options**: PNG, SVG, PDF, and JSON configuration
- **LLM Integration**: Schema designed to be easily generated by language models
## Getting Started
### Prerequisites
- Node.js 16.x or higher
- npm 8.x or higher
### Installation
1. Clone the repository:
```
git clone https://github.com/yourusername/ui-mockup-generator.git
cd ui-mockup-generator
```
2. Install dependencies:
```
npm install
```
3. Start the development server:
```
npm run dev
```
4. Open your browser and navigate to `http://localhost:3000`
## Usage
### Creating a Mockup
1. Select components from the sidebar and drag them onto the canvas
2. Customize component properties using the property panel
3. Style components using the style panel
4. Arrange components using drag-and-drop
### Exporting a Mockup
1. Click the "Export" button in the toolbar
2. Choose your preferred export format (PNG, SVG, PDF, JSON)
3. Save the file to your computer
### Using with LLMs
The UI Mockup Generator uses a standardized JSON schema that is designed to be both human-readable and machine-friendly. This makes it ideal for integration with Large Language Models.
See the [LLM Integration Guide](./specs/llm_integration_guide.md) for details on how to prompt LLMs to generate compatible mockups.
## Documentation
Detailed documentation is available in the `specs` directory:
- [Software Specification](./specs/software_specification.md): Detailed overview of the application
- [JSON Schema](./specs/json_schema.md): Specification of the JSON configuration format
- [Implementation Plan](./specs/implementation_plan.md): Technical implementation details
- [LLM Integration Guide](./specs/llm_integration_guide.md): Guidelines for using with LLMs
## Project Structure
```
ui-mockup-generator/
├── client/ # Frontend React application
├── server/ # Backend Node.js server (optional)
├── specs/ # Specifications and documentation
├── package.json # Root package.json
└── README.md # This file
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
+63
View File
@@ -0,0 +1,63 @@
{
"name": "ui-mockup-generator-client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@reduxjs/toolkit": "^2.0.1",
"file-saver": "^2.0.5",
"framer-motion": "^10.16.5",
"html-to-image": "^1.11.11",
"konva": "^9.2.3",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-konva": "^18.2.10",
"react-redux": "^9.0.0",
"recharts": "^2.10.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.0",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.54.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^26.1.0",
"typescript": "^5.3.2",
"vite": "^5.0.2",
"vitest": "^0.34.6"
},
"scripts": {
"start": "vite",
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="UI Mockup Generator - Create UI mockups with JSON configuration for LLMs"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>UI Mockup Generator</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { ChakraProvider, Box, Flex } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { store } from './store';
import theme from './theme';
import Header from './components/common/Header';
import Sidebar from './components/panels/Sidebar';
import Canvas from './components/canvas/Canvas';
import PropertyPanel from './components/panels/PropertyPanel';
const App: React.FC = () => {
return (
<Provider store={store}>
<ChakraProvider theme={theme}>
<Box h="100vh" overflow="hidden">
<Header />
<Flex h="calc(100vh - 60px)">
<Sidebar />
<Canvas />
<PropertyPanel />
</Flex>
</Box>
</ChakraProvider>
</Provider>
);
};
export default App;
+93
View File
@@ -0,0 +1,93 @@
import React, { useRef, useCallback } from 'react';
import { Box, Center } from '@chakra-ui/react';
import { Stage, Layer, Rect } from 'react-konva';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store';
import { selectComponent, clearSelection } from '@/store/slices/selectionSlice';
import ComponentRenderer from './ComponentRenderer';
import useCanvasSize from '@/hooks/useCanvasSize';
import useCanvasZoom from '@/hooks/useCanvasZoom';
const Canvas: React.FC = () => {
const dispatch = useDispatch();
const stageRef = useRef<any>(null);
const { mockup } = useSelector((state: RootState) => state);
const { components, canvas } = mockup;
// Custom hooks for canvas operations
const { containerSize, canvasContainerRef } = useCanvasSize();
const { scale, position, handleWheel, handleDragMove } = useCanvasZoom();
// Handle background click (deselect all)
const handleStageClick = useCallback((e: any) => {
// If clicked on the stage background
if (e.target === e.currentTarget) {
dispatch(clearSelection());
}
}, [dispatch]);
// Handle component selection
const handleComponentClick = useCallback((id: string, e: any) => {
e.cancelBubble = true; // Stop propagation to parent
dispatch(selectComponent(id));
}, [dispatch]);
return (
<Center
flex="1"
bg="gray.100"
overflow="hidden"
ref={canvasContainerRef}
position="relative"
>
<Box
position="absolute"
top="0"
left="0"
bottom="0"
right="0"
className="canvas-grid"
/>
<Stage
ref={stageRef}
width={containerSize.width}
height={containerSize.height}
onClick={handleStageClick}
onWheel={handleWheel}
draggable
onDragMove={handleDragMove}
x={position.x}
y={position.y}
scaleX={scale}
scaleY={scale}
>
<Layer>
{/* Canvas background */}
<Rect
x={0}
y={0}
width={canvas.width}
height={canvas.height}
fill={canvas.background}
shadowColor="rgba(0,0,0,0.2)"
shadowBlur={10}
shadowOffsetX={0}
shadowOffsetY={0}
cornerRadius={4}
/>
{/* Render components */}
{components.map((component) => (
<ComponentRenderer
key={component.id}
component={component}
onClick={(e) => handleComponentClick(component.id, e)}
/>
))}
</Layer>
</Stage>
</Center>
);
};
export default Canvas;
@@ -0,0 +1,58 @@
import React from 'react';
import { Group } from 'react-konva';
import { useSelector } from 'react-redux';
import { RootState } from '@/store';
import { Component } from '@/types/mockup';
import ChartComponent from './components/ChartComponent';
import FunnelComponent from './components/FunnelComponent';
interface ComponentRendererProps {
component: Component;
onClick?: (e: any) => void;
}
const ComponentRenderer: React.FC<ComponentRendererProps> = ({ component, onClick }) => {
const selection = useSelector((state: RootState) => state.selection);
const isSelected = selection?.selectedComponentId === component.id;
// Common props for all component types
const commonProps = {
id: component.id,
position: component.position,
size: component.size,
style: component.style,
isSelected,
onClick,
};
// Only render components that we know exist
switch (component.type) {
case 'chart':
return (
<ChartComponent
{...commonProps}
data={component.data}
/>
);
case 'funnel':
return (
<FunnelComponent
{...commonProps}
data={component.data}
/>
);
default:
// Draw a placeholder for other component types
return (
<Group
x={component.position.x}
y={component.position.y}
onClick={onClick}
/>
);
}
};
export default ComponentRenderer;
@@ -0,0 +1,116 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import ComponentRenderer from '../ComponentRenderer';
import { ChartData } from '@/types/mockup';
// Mock the components used by ComponentRenderer
vi.mock('../components/ChartComponent', () => ({
default: ({ id, data }: { id: string; data: ChartData }) => (
<div data-testid="chart-component" data-id={id}>
{data.title}
</div>
),
}));
vi.mock('../components/FunnelComponent', () => ({
default: ({ id, data }: { id: string; data: any }) => (
<div data-testid="funnel-component" data-id={id}>
{data.steps ? data.steps.length : 0} steps
</div>
),
}));
// Mock Konva components
vi.mock('react-konva', () => ({
Group: ({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }) => (
<div data-testid="konva-group" onClick={onClick}>
{children}
</div>
),
}));
describe('ComponentRenderer', () => {
let store: any;
beforeEach(() => {
store = configureStore({
reducer: {
selection: (state = { selectedComponentId: 'chart-1' }) => state
}
});
});
test('renders chart component correctly', () => {
const chartComponent = {
id: 'chart-1',
type: 'chart',
position: { x: 10, y: 20 },
size: { width: 400, height: 300 },
data: {
title: 'Test Chart',
labels: ['Jan', 'Feb', 'Mar'],
datasets: [
{ label: 'Dataset 1', data: [10, 20, 30], backgroundColor: '#4caf50' }
]
}
};
render(
<Provider store={store}>
<ComponentRenderer component={chartComponent} onClick={() => {}} />
</Provider>
);
const chartElement = screen.getByTestId('chart-component');
expect(chartElement).toBeTruthy();
expect(chartElement.textContent).toBe('Test Chart');
expect(chartElement.getAttribute('data-id')).toBe('chart-1');
});
test('renders funnel component correctly', () => {
const funnelComponent = {
id: 'funnel-1',
type: 'funnel',
position: { x: 10, y: 20 },
size: { width: 400, height: 300 },
data: {
steps: [
{ label: 'Step 1', value: 1000, dropoff: 0 },
{ label: 'Step 2', value: 600, dropoff: 40 }
]
}
};
render(
<Provider store={store}>
<ComponentRenderer component={funnelComponent} onClick={() => {}} />
</Provider>
);
const funnelElement = screen.getByTestId('funnel-component');
expect(funnelElement).toBeTruthy();
expect(funnelElement.textContent).toBe('2 steps');
expect(funnelElement.getAttribute('data-id')).toBe('funnel-1');
});
test('renders placeholder for unknown component types', () => {
const unknownComponent = {
id: 'unknown-1',
type: 'unknown-type',
position: { x: 10, y: 20 },
size: { width: 400, height: 300 },
data: {}
};
render(
<Provider store={store}>
<ComponentRenderer component={unknownComponent} onClick={() => {}} />
</Provider>
);
expect(screen.getByTestId('konva-group')).toBeTruthy();
});
});
@@ -0,0 +1,169 @@
import React from 'react';
import { Group, Rect, Line, Text } from 'react-konva';
import { ChartComponent as ChartComponentType, ChartData, ChartOptions } from '@/types/mockup';
interface ChartComponentProps {
id: string;
position: ChartComponentType['position'];
size?: ChartComponentType['size'];
style?: ChartComponentType['style'];
data: ChartData;
isSelected?: boolean;
onClick?: (e: any) => void;
}
const ChartComponent: React.FC<ChartComponentProps> = ({
size,
data,
style,
isSelected,
onClick
}) => {
if (!size) return null;
const { title, labels = [], datasets = [] } = data;
const options: ChartOptions = (data as any).options || {};
// Extract colors from style or use defaults
const backgroundColor = style?.backgroundColor || 'white';
const colorList = datasets.map((dataset, index) => {
if (Array.isArray(dataset.backgroundColor)) return dataset.backgroundColor[0];
return dataset.backgroundColor || ['#4caf50', '#2196f3', '#ff9800', '#e91e63'][index % 4];
});
// Currently these options aren't being used but could be in future enhancements
// const {
// showLegend = true,
// showGrid = true,
// showTooltip = true
// } = options;
const padding = 40;
const chartWidth = size.width - padding * 2;
const chartHeight = size.height - padding * 2;
const renderBarChart = () => {
const barWidth = chartWidth / (labels.length * 2);
const allValues = datasets.flatMap(dataset => dataset.data);
const maxValue = Math.max(...allValues, 1); // Prevent division by zero
return (
<Group>
{/* X and Y axis */}
<Line
points={[padding, padding, padding, size.height - padding]}
stroke="black"
strokeWidth={1}
/>
<Line
points={[padding, size.height - padding, size.width - padding, size.height - padding]}
stroke="black"
strokeWidth={1}
/>
{/* Render bars for each data point */}
{datasets.map((dataset, datasetIndex) => (
<Group key={datasetIndex}>
{dataset.data.map((value, index) => {
const barHeight = (value / maxValue) * chartHeight;
const xPos = padding + index * (barWidth * 2) + datasetIndex * barWidth;
const yPos = size.height - padding - barHeight;
const barColor = Array.isArray(dataset.backgroundColor)
? dataset.backgroundColor[index % dataset.backgroundColor.length]
: dataset.backgroundColor || colorList[datasetIndex];
return (
<Group key={`bar-${datasetIndex}-${index}`}>
<Rect
x={xPos}
y={yPos}
width={barWidth}
height={barHeight}
fill={barColor as string}
/>
{/* Display values on bars */}
<Text
text={value.toString()}
x={xPos}
y={yPos - 15}
fontSize={style?.fontSize || 10}
fontFamily={style?.fontFamily || 'Arial'}
fill="black"
width={barWidth}
align="center"
/>
</Group>
);
})}
</Group>
))}
{/* X axis labels */}
{labels.map((label, index) => {
const xPos = padding + index * (barWidth * 2);
return (
<Text
key={`label-${index}`}
text={label}
x={xPos}
y={size.height - padding + 5}
fontSize={style?.fontSize || 10}
fontFamily={style?.fontFamily || 'Arial'}
fill="black"
width={barWidth * 2}
align="center"
/>
);
})}
</Group>
);
};
return (
<Group onClick={onClick}>
{/* Chart background */}
<Rect
width={size.width}
height={size.height}
fill={backgroundColor}
cornerRadius={style?.borderRadius || 0}
shadowColor="rgba(0,0,0,0.1)"
shadowBlur={5}
shadowOffsetY={2}
/>
{/* Chart title */}
{title && (
<Text
text={title}
x={padding}
y={10}
fontSize={style?.fontSize ? style.fontSize + 2 : 14}
fontFamily={style?.fontFamily || 'Arial'}
fontStyle="bold"
fill="black"
width={size.width - padding * 2}
align="center"
/>
)}
{/* Render bar chart (we can add other chart types later) */}
{renderBarChart()}
{/* Add selection indicator if selected */}
{isSelected && (
<Rect
width={size.width}
height={size.height}
stroke="#2196f3"
strokeWidth={2}
/>
)}
</Group>
);
};
export default ChartComponent;
@@ -0,0 +1,105 @@
import React from 'react';
import { Group, Rect, Text } from 'react-konva';
import { FunnelComponent as FunnelComponentType } from '@/types/mockup';
interface FunnelComponentProps {
id: string;
position: FunnelComponentType['position'];
size?: FunnelComponentType['size'];
style?: FunnelComponentType['style'];
data: FunnelComponentType['data'];
isSelected?: boolean;
onClick?: (e: any) => void;
}
const FunnelComponent: React.FC<FunnelComponentProps> = ({
size,
data,
style,
isSelected,
onClick
}) => {
if (!size) return null;
const { steps, colors = {}, options = {} } = data;
const {
primary = '#1a2b47',
secondary = '#e0756b',
tertiary = '#d3d3d3',
success = '#4caf50'
} = colors;
const {
showPercentages = true,
showLabels = true,
orientation = 'vertical'
} = options;
const stepHeight = size.height / (steps.length + 1);
return (
<Group onClick={onClick}>
{/* Funnel background/container */}
<Rect
width={size.width}
height={size.height}
fill={style?.backgroundColor || 'white'}
cornerRadius={style?.borderRadius || 0}
shadowColor="rgba(0,0,0,0.1)"
shadowBlur={5}
shadowOffsetY={2}
/>
{/* Funnel steps */}
{steps.map((step, index) => {
const yPos = stepHeight * (index + 0.5);
const stepWidth = size.width * (1 - (index * 0.15));
const xPos = (size.width - stepWidth) / 2;
const fillColor = step.conversion ? success : primary;
return (
<Group key={index} y={yPos}>
{/* Step bar */}
<Rect
x={xPos}
width={stepWidth}
height={stepHeight * 0.6}
fill={fillColor}
cornerRadius={4}
/>
{/* Step label */}
{showLabels && (
<Text
text={step.label}
x={xPos + 10}
y={10}
fontSize={style?.fontSize || 12}
fontFamily={style?.fontFamily || 'Arial'}
fill="white"
width={stepWidth - 20}
align="left"
/>
)}
{/* Dropoff percentage */}
{showPercentages && step.dropoff && (
<Text
text={`${step.dropoff}%`}
x={xPos + stepWidth - 50}
y={10}
fontSize={style?.fontSize || 12}
fontFamily={style?.fontFamily || 'Arial'}
fill="white"
width={40}
align="right"
/>
)}
</Group>
);
})}
</Group>
);
};
export default FunnelComponent;
@@ -0,0 +1,100 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, test, expect, vi } from 'vitest';
import { Stage, Layer } from 'react-konva';
import ChartComponent from '../ChartComponent';
import { ChartData } from '@/types/mockup';
// Mock Konva as it doesn't work well in testing environments
vi.mock('react-konva', () => {
return {
Stage: ({ children }: { children: React.ReactNode }) => <div data-testid="stage">{children}</div>,
Layer: ({ children }: { children: React.ReactNode }) => <div data-testid="layer">{children}</div>,
Group: ({ children }: { children: React.ReactNode }) => <div data-testid="group">{children}</div>,
Rect: () => <div data-testid="rect"></div>,
Line: () => <div data-testid="line"></div>,
Text: ({ text }: { text: string }) => <div data-testid="text">{text}</div>,
}
});
describe('ChartComponent', () => {
const mockChartData: ChartData = {
title: 'Test Chart',
labels: ['Jan', 'Feb', 'Mar'],
datasets: [
{
label: 'Dataset 1',
data: [10, 20, 30],
backgroundColor: '#4caf50'
}
]
};
const defaultProps = {
id: 'chart-1',
position: { x: 10, y: 10 },
size: { width: 400, height: 300 },
style: { backgroundColor: 'white', fontSize: 12 },
data: mockChartData,
isSelected: false,
onClick: vi.fn()
};
test('renders chart with correct data', () => {
render(
<Stage>
<Layer>
<ChartComponent {...defaultProps} />
</Layer>
</Stage>
);
// Check if chart title is rendered
expect(screen.queryByText('Test Chart')).toBeTruthy();
// Check if all labels are rendered
expect(screen.queryByText('Jan')).toBeTruthy();
expect(screen.queryByText('Feb')).toBeTruthy();
expect(screen.queryByText('Mar')).toBeTruthy();
// Check if data values are rendered
expect(screen.queryByText('10')).toBeTruthy();
expect(screen.queryByText('20')).toBeTruthy();
expect(screen.queryByText('30')).toBeTruthy();
// Check chart structure - we can't use getAllByTestId with the mocked components
const rects = document.querySelectorAll('[data-testid="rect"]');
const lines = document.querySelectorAll('[data-testid="line"]');
expect(rects.length).toBeGreaterThan(0);
expect(lines.length).toBe(2); // X and Y axis
});
test('handles undefined size gracefully', () => {
const propsWithoutSize = { ...defaultProps, size: undefined };
const { container } = render(
<Stage>
<Layer>
<ChartComponent {...propsWithoutSize} />
</Layer>
</Stage>
);
// Component should return null if size is undefined
expect(container.querySelector('[data-testid="group"]')).toBeNull();
});
test('handles selection state correctly', () => {
const selectedProps = { ...defaultProps, isSelected: true };
render(
<Stage>
<Layer>
<ChartComponent {...selectedProps} />
</Layer>
</Stage>
);
// When selected, we should have an extra rect for the selection indicator
const rects = document.querySelectorAll('[data-testid="rect"]');
expect(rects.length).toBeGreaterThan(1);
});
});
+120
View File
@@ -0,0 +1,120 @@
import React from 'react';
import {
Box,
Flex,
Heading,
Button,
HStack,
useDisclosure,
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
Tooltip
} from '@chakra-ui/react';
import {
FiSave,
FiUpload,
FiDownload,
FiCode,
FiSettings,
FiChevronDown,
FiUndo,
FiRedo
} from 'react-icons/fi';
import { useDispatch } from 'react-redux';
import { undo, redo } from '@/store/slices/historySlice';
import ExportDialog from '../dialogs/ExportDialog';
import ImportDialog from '../dialogs/ImportDialog';
const Header: React.FC = () => {
const dispatch = useDispatch();
const {
isOpen: isExportOpen,
onOpen: onExportOpen,
onClose: onExportClose
} = useDisclosure();
const {
isOpen: isImportOpen,
onOpen: onImportOpen,
onClose: onImportClose
} = useDisclosure();
return (
<Box
as="header"
height="60px"
borderBottom="1px solid"
borderColor="gray.200"
bg="white"
px={4}
>
<Flex h="100%" alignItems="center" justifyContent="space-between">
{/* Logo/Title */}
<Heading size="md" color="brand.600">UI Mockup Generator</Heading>
{/* Main toolbar */}
<HStack spacing={2}>
{/* Undo/Redo */}
<Tooltip label="Undo" placement="bottom">
<IconButton
aria-label="Undo"
icon={<FiUndo />}
variant="ghost"
onClick={() => dispatch(undo())}
/>
</Tooltip>
<Tooltip label="Redo" placement="bottom">
<IconButton
aria-label="Redo"
icon={<FiRedo />}
variant="ghost"
onClick={() => dispatch(redo())}
/>
</Tooltip>
{/* File operations */}
<Menu>
<MenuButton as={Button} rightIcon={<FiChevronDown />} variant="ghost">
File
</MenuButton>
<MenuList>
<MenuItem icon={<FiSave />} command="⌘S">
Save Project
</MenuItem>
<MenuItem icon={<FiUpload />} onClick={onImportOpen}>
Import JSON
</MenuItem>
<MenuItem icon={<FiDownload />} onClick={onExportOpen}>
Export
</MenuItem>
<MenuItem icon={<FiCode />}>
View JSON
</MenuItem>
</MenuList>
</Menu>
{/* Settings */}
<Tooltip label="Settings" placement="bottom">
<IconButton
aria-label="Settings"
icon={<FiSettings />}
variant="ghost"
/>
</Tooltip>
</HStack>
</Flex>
{/* Export Dialog */}
<ExportDialog isOpen={isExportOpen} onClose={onExportClose} />
{/* Import Dialog */}
<ImportDialog isOpen={isImportOpen} onClose={onImportClose} />
</Box>
);
};
export default Header;
@@ -0,0 +1,411 @@
import React from 'react';
import {
Box,
VStack,
Heading,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
FormControl,
FormLabel,
Input,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Text,
useColorModeValue,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Button,
HStack,
} from '@chakra-ui/react';
import { HexColorPicker } from 'react-colorful';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@/store';
import { updateComponent, removeComponent } from '@/store/slices/mockupSlice';
import { Component } from '@/types/mockup';
const PropertyPanel: React.FC = () => {
const dispatch = useDispatch();
const { activeId } = useSelector((state: RootState) => state.selection);
const { components } = useSelector((state: RootState) => state.mockup);
// Find the active component
const activeComponent = activeId
? components.find(component => component.id === activeId)
: null;
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
// Handle position change
const handlePositionChange = (axis: 'x' | 'y', value: number) => {
if (!activeComponent) return;
dispatch(updateComponent({
id: activeComponent.id,
changes: {
position: {
...activeComponent.position,
[axis]: value,
},
},
}));
};
// Handle size change
const handleSizeChange = (dimension: 'width' | 'height', value: number) => {
if (!activeComponent || !activeComponent.size) return;
dispatch(updateComponent({
id: activeComponent.id,
changes: {
size: {
...activeComponent.size,
[dimension]: value,
},
},
}));
};
// Handle style change
const handleStyleChange = (property: string, value: any) => {
if (!activeComponent) return;
dispatch(updateComponent({
id: activeComponent.id,
changes: {
style: {
...(activeComponent.style || {}),
[property]: value,
},
},
}));
};
// Handle text content change
const handleContentChange = (content: string) => {
if (!activeComponent || activeComponent.type !== 'text') return;
dispatch(updateComponent({
id: activeComponent.id,
changes: {
data: {
...activeComponent.data,
content,
},
},
}));
};
// Handle delete component
const handleDeleteComponent = () => {
if (!activeComponent) return;
dispatch(removeComponent(activeComponent.id));
};
// Render component-specific properties
const renderComponentProperties = (component: Component) => {
switch (component.type) {
case 'text':
return (
<FormControl mt={4}>
<FormLabel>Text Content</FormLabel>
<Input
value={(component.data as any)?.content || ''}
onChange={(e) => handleContentChange(e.target.value)}
/>
</FormControl>
);
case 'funnel':
return (
<Box>
<Heading size="sm" mt={4} mb={2}>Funnel Settings</Heading>
<Text fontSize="sm">Funnel properties editor would go here</Text>
</Box>
);
case 'chart':
return (
<Box>
<Heading size="sm" mt={4} mb={2}>Chart Settings</Heading>
<Text fontSize="sm">Chart properties editor would go here</Text>
</Box>
);
case 'card':
return (
<Box>
<Heading size="sm" mt={4} mb={2}>Card Settings</Heading>
<Text fontSize="sm">Card properties editor would go here</Text>
</Box>
);
default:
return null;
}
};
// If no component is selected
if (!activeComponent) {
return (
<Box
w="300px"
h="100%"
borderLeft="1px solid"
borderColor={borderColor}
bg={bgColor}
p={4}
>
<Text color="gray.500">Select a component to edit its properties</Text>
</Box>
);
}
return (
<Box
w="300px"
h="100%"
borderLeft="1px solid"
borderColor={borderColor}
bg={bgColor}
overflowY="auto"
>
<Tabs>
<TabList>
<Tab>Properties</Tab>
<Tab>Style</Tab>
<Tab>Data</Tab>
</TabList>
<TabPanels>
{/* Properties Panel */}
<TabPanel>
<VStack align="stretch" spacing={4}>
<Heading size="sm">
{activeComponent.type.charAt(0).toUpperCase() + activeComponent.type.slice(1)} Properties
</Heading>
{/* Position */}
<Accordion defaultIndex={[0]} allowMultiple>
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
<Text fontWeight="bold">Position</Text>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<FormControl>
<FormLabel>X Position</FormLabel>
<NumberInput
value={activeComponent.position.x}
onChange={(_, val) => handlePositionChange('x', val)}
min={0}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
<FormControl mt={2}>
<FormLabel>Y Position</FormLabel>
<NumberInput
value={activeComponent.position.y}
onChange={(_, val) => handlePositionChange('y', val)}
min={0}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
</AccordionPanel>
</AccordionItem>
{/* Size */}
{activeComponent.size && (
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
<Text fontWeight="bold">Size</Text>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<FormControl>
<FormLabel>Width</FormLabel>
<NumberInput
value={activeComponent.size.width}
onChange={(_, val) => handleSizeChange('width', val)}
min={10}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
<FormControl mt={2}>
<FormLabel>Height</FormLabel>
<NumberInput
value={activeComponent.size.height}
onChange={(_, val) => handleSizeChange('height', val)}
min={10}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
</AccordionPanel>
</AccordionItem>
)}
{/* Component-specific properties */}
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
<Text fontWeight="bold">Content</Text>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{renderComponentProperties(activeComponent)}
</AccordionPanel>
</AccordionItem>
</Accordion>
{/* Delete button */}
<Box mt={4}>
<Button
colorScheme="red"
size="sm"
onClick={handleDeleteComponent}
width="100%"
>
Delete Component
</Button>
</Box>
</VStack>
</TabPanel>
{/* Style Panel */}
<TabPanel>
<VStack align="stretch" spacing={4}>
<Heading size="sm">Style Properties</Heading>
{activeComponent.style && (
<Accordion defaultIndex={[0]} allowMultiple>
{/* Color */}
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
<Text fontWeight="bold">Colors</Text>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{/* Background Color */}
<FormControl mt={2}>
<FormLabel>Background Color</FormLabel>
<Box mb={2}>
<HexColorPicker
color={activeComponent.style.backgroundColor || '#ffffff'}
onChange={(color) => handleStyleChange('backgroundColor', color)}
style={{ width: '100%' }}
/>
</Box>
<Input
value={activeComponent.style.backgroundColor || '#ffffff'}
onChange={(e) => handleStyleChange('backgroundColor', e.target.value)}
/>
</FormControl>
{/* Text Color */}
<FormControl mt={4}>
<FormLabel>Text Color</FormLabel>
<Box mb={2}>
<HexColorPicker
color={activeComponent.style.color || '#000000'}
onChange={(color) => handleStyleChange('color', color)}
style={{ width: '100%' }}
/>
</Box>
<Input
value={activeComponent.style.color || '#000000'}
onChange={(e) => handleStyleChange('color', e.target.value)}
/>
</FormControl>
</AccordionPanel>
</AccordionItem>
{/* Typography */}
<AccordionItem>
<AccordionButton>
<Box flex="1" textAlign="left">
<Text fontWeight="bold">Typography</Text>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{/* Font Size */}
<FormControl>
<FormLabel>Font Size</FormLabel>
<NumberInput
value={activeComponent.style.fontSize || 16}
onChange={(_, val) => handleStyleChange('fontSize', val)}
min={8}
max={72}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
{/* Font Family */}
<FormControl mt={2}>
<FormLabel>Font Family</FormLabel>
<Input
value={activeComponent.style.fontFamily || 'Inter'}
onChange={(e) => handleStyleChange('fontFamily', e.target.value)}
/>
</FormControl>
</AccordionPanel>
</AccordionItem>
</Accordion>
)}
</VStack>
</TabPanel>
{/* Data Panel */}
<TabPanel>
<Text>Data properties would go here</Text>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};
export default PropertyPanel;
+228
View File
@@ -0,0 +1,228 @@
import React from 'react';
import {
Box,
VStack,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Heading,
Text,
SimpleGrid,
Button,
Icon,
useColorModeValue,
} from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store';
import { addComponent } from '@/store/slices/mockupSlice';
import { setActiveTool } from '@/store/slices/uiSlice';
import { FiLayout, FiType, FiBarChart2, FiBox, FiImage, FiSquare } from 'react-icons/fi';
const Sidebar: React.FC = () => {
const dispatch = useDispatch();
const activeTool = useSelector((state: RootState) => state.ui.activeTool);
const { sidebarWidth } = useSelector((state: RootState) => state.ui);
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
// Handle component creation
const handleAddComponent = (type: string) => {
let newComponent: any = {
type,
position: { x: 100, y: 100 },
};
// Add type-specific properties
switch (type) {
case 'funnel':
newComponent = {
...newComponent,
size: { width: 300, height: 400 },
data: {
steps: [
{ label: 'Step 1', value: 100, dropoff: 20 },
{ label: 'Step 2', value: 80, dropoff: 25 },
{ label: 'Step 3', value: 60, conversion: true },
],
options: {
showPercentages: true,
showLabels: true,
orientation: 'vertical',
},
colors: {
primary: '#1a2b47',
secondary: '#e0756b',
tertiary: '#d3d3d3',
success: '#4caf50',
},
},
};
break;
case 'chart':
newComponent = {
...newComponent,
size: { width: 300, height: 300 },
chartType: 'bar',
data: {
labels: ['A', 'B', 'C', 'D', 'E'],
datasets: [
{
label: 'Dataset 1',
data: [12, 19, 3, 5, 2],
backgroundColor: '#2563eb',
},
],
},
};
break;
case 'text':
newComponent = {
...newComponent,
size: { width: 200, height: 50 },
data: {
content: 'Text Content',
variant: 'body1',
},
style: {
fontSize: 16,
fontFamily: 'Inter',
color: '#333333',
},
};
break;
case 'card':
newComponent = {
...newComponent,
size: { width: 300, height: 200 },
data: {
header: {
title: 'Card Title',
},
body: {
content: 'Card content goes here',
},
},
style: {
backgroundColor: '#ffffff',
borderRadius: 8,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
},
};
break;
default:
break;
}
dispatch(addComponent(newComponent));
};
// Tool buttons for the sidebar
const tools = [
{ id: 'select', label: 'Select', icon: FiSquare },
{ id: 'text', label: 'Text', icon: FiType },
{ id: 'funnel', label: 'Funnel', icon: FiLayout },
{ id: 'chart', label: 'Chart', icon: FiBarChart2 },
{ id: 'card', label: 'Card', icon: FiBox },
{ id: 'image', label: 'Image', icon: FiImage },
];
return (
<Box
w={`${sidebarWidth}px`}
h="100%"
borderRight="1px solid"
borderColor={borderColor}
bg={bgColor}
overflowY="auto"
>
<Tabs>
<TabList>
<Tab>Components</Tab>
<Tab>Styles</Tab>
<Tab>Data</Tab>
</TabList>
<TabPanels>
{/* Components Panel */}
<TabPanel p={3}>
<VStack spacing={4} align="stretch">
<Box>
<Heading size="sm" mb={2}>Tools</Heading>
<SimpleGrid columns={3} spacing={2}>
{tools.map((tool) => (
<Button
key={tool.id}
variant={activeTool === tool.id ? 'solid' : 'outline'}
size="sm"
onClick={() => dispatch(setActiveTool(tool.id))}
colorScheme={activeTool === tool.id ? 'brand' : 'gray'}
h="60px"
display="flex"
flexDir="column"
justifyContent="center"
>
<Icon as={tool.icon} mb={1} />
<Text fontSize="xs">{tool.label}</Text>
</Button>
))}
</SimpleGrid>
</Box>
<Box>
<Heading size="sm" mb={2}>Insert</Heading>
<VStack spacing={2} align="stretch">
<Button
size="sm"
justifyContent="flex-start"
leftIcon={<Icon as={FiLayout} />}
onClick={() => handleAddComponent('funnel')}
>
Funnel
</Button>
<Button
size="sm"
justifyContent="flex-start"
leftIcon={<Icon as={FiBarChart2} />}
onClick={() => handleAddComponent('chart')}
>
Chart
</Button>
<Button
size="sm"
justifyContent="flex-start"
leftIcon={<Icon as={FiType} />}
onClick={() => handleAddComponent('text')}
>
Text
</Button>
<Button
size="sm"
justifyContent="flex-start"
leftIcon={<Icon as={FiBox} />}
onClick={() => handleAddComponent('card')}
>
Card
</Button>
</VStack>
</Box>
</VStack>
</TabPanel>
{/* Styles Panel */}
<TabPanel>
<Text>Style options go here</Text>
</TabPanel>
{/* Data Panel */}
<TabPanel>
<Text>Data options go here</Text>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};
export default Sidebar;
+43
View File
@@ -0,0 +1,43 @@
import { useState, useEffect, useCallback, useRef } from 'react';
interface ContainerSize {
width: number;
height: number;
}
const DEFAULT_SIZE = { width: 100, height: 100 };
/**
* Custom hook to handle canvas container resizing
*/
const useCanvasSize = () => {
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
const [containerSize, setContainerSize] = useState<ContainerSize>(DEFAULT_SIZE);
const updateSize = useCallback(() => {
if (canvasContainerRef.current) {
const { width, height } = canvasContainerRef.current.getBoundingClientRect();
setContainerSize({ width, height });
}
}, []);
// Update size on resize
useEffect(() => {
window.addEventListener('resize', updateSize);
// Initial size calculation
updateSize();
// Additional timeout to handle initial render edge cases
const timeout = setTimeout(updateSize, 100);
return () => {
window.removeEventListener('resize', updateSize);
clearTimeout(timeout);
};
}, [updateSize]);
return { containerSize, canvasContainerRef, updateSize };
};
export default useCanvasSize;
+69
View File
@@ -0,0 +1,69 @@
import { useState, useCallback } from 'react';
interface Position {
x: number;
y: number;
}
const MIN_SCALE = 0.1;
const MAX_SCALE = 5;
const SCALE_FACTOR = 1.1;
/**
* Custom hook to handle canvas zooming and panning
*/
const useCanvasZoom = () => {
const [scale, setScale] = useState<number>(1);
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
// Handle wheel event for zooming
const handleWheel = useCallback((e: any) => {
e.evt.preventDefault();
const stage = e.target.getStage();
const oldScale = scale;
const pointer = stage.getPointerPosition();
const mousePointTo = {
x: (pointer.x - position.x) / oldScale,
y: (pointer.y - position.y) / oldScale,
};
// Calculate new scale
const newScale = e.evt.deltaY < 0 ?
Math.min(oldScale * SCALE_FACTOR, MAX_SCALE) :
Math.max(oldScale / SCALE_FACTOR, MIN_SCALE);
// Calculate new position that keeps the point under the mouse in the same position
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
setScale(newScale);
setPosition(newPos);
}, [scale, position]);
// Handle drag move for panning
const handleDragMove = useCallback((e: any) => {
setPosition({
x: e.target.x(),
y: e.target.y(),
});
}, []);
// Reset zoom and position
const resetView = useCallback(() => {
setScale(1);
setPosition({ x: 0, y: 0 });
}, []);
return {
scale,
position,
handleWheel,
handleDragMove,
resetView,
};
};
export default useCanvasZoom;
+67
View File
@@ -0,0 +1,67 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Canvas grid background */
.canvas-grid {
background-size: 40px 40px;
background-image:
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Utility classes */
.no-select {
user-select: none;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.pointer {
cursor: pointer;
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+24
View File
@@ -0,0 +1,24 @@
import { configureStore } from '@reduxjs/toolkit';
import mockupReducer from './slices/mockupSlice';
import selectionReducer from './slices/selectionSlice';
import historyReducer from './slices/historySlice';
import uiReducer from './slices/uiSlice';
export const store = configureStore({
reducer: {
mockup: mockupReducer,
selection: selectionReducer,
history: historyReducer,
ui: uiReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types for serializability checks
ignoredActions: ['history/push', 'history/undo', 'history/redo'],
},
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
+65
View File
@@ -0,0 +1,65 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { HistoryState, Mockup } from '@/types/mockup';
const initialState: HistoryState = {
past: [],
current: {
version: '1.0',
canvas: {
width: 1200,
height: 800,
background: '#f5f5f5',
gridSize: 8,
showGrid: true,
},
components: [],
},
future: [],
};
const MAX_HISTORY_LENGTH = 30;
export const historySlice = createSlice({
name: 'history',
initialState,
reducers: {
push: (state, action: PayloadAction<Mockup>) => {
return {
past: [...state.past, state.current].slice(-MAX_HISTORY_LENGTH),
current: action.payload,
future: [],
};
},
undo: (state) => {
if (state.past.length === 0) return state;
const previous = state.past[state.past.length - 1];
const newPast = state.past.slice(0, state.past.length - 1);
return {
past: newPast,
current: previous,
future: [state.current, ...state.future],
};
},
redo: (state) => {
if (state.future.length === 0) return state;
const next = state.future[0];
const newFuture = state.future.slice(1);
return {
past: [...state.past, state.current],
current: next,
future: newFuture,
};
},
clear: () => {
return initialState;
},
},
});
export const { push, undo, redo, clear } = historySlice.actions;
export default historySlice.reducer;
+89
View File
@@ -0,0 +1,89 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid';
import { Mockup, Component } from '@/types/mockup';
const initialState: Mockup = {
version: '1.0',
canvas: {
width: 1200,
height: 800,
background: '#f5f5f5',
gridSize: 8,
showGrid: true,
},
components: [],
};
export const mockupSlice = createSlice({
name: 'mockup',
initialState,
reducers: {
setMockup: (state, action: PayloadAction<Mockup>) => {
return action.payload;
},
updateCanvas: (state, action: PayloadAction<Partial<Mockup['canvas']>>) => {
state.canvas = { ...state.canvas, ...action.payload };
},
addComponent: (state, action: PayloadAction<Omit<Component, 'id'>>) => {
const component = { ...action.payload, id: uuidv4() };
state.components.push(component as Component);
},
updateComponent: (state, action: PayloadAction<{ id: string; changes: Partial<Component> }>) => {
const { id, changes } = action.payload;
const index = state.components.findIndex(component => component.id === id);
if (index !== -1) {
state.components[index] = { ...state.components[index], ...changes };
}
},
removeComponent: (state, action: PayloadAction<string>) => {
state.components = state.components.filter(component => component.id !== action.payload);
},
moveComponent: (state, action: PayloadAction<{ id: string; position: { x: number; y: number } }>) => {
const { id, position } = action.payload;
const index = state.components.findIndex(component => component.id === id);
if (index !== -1) {
state.components[index].position = {
...state.components[index].position,
x: position.x,
y: position.y,
};
}
},
resizeComponent: (state, action: PayloadAction<{ id: string; size: { width: number; height: number } }>) => {
const { id, size } = action.payload;
const index = state.components.findIndex(component => component.id === id);
if (index !== -1 && state.components[index].size) {
state.components[index].size = {
...state.components[index].size,
width: size.width,
height: size.height,
};
}
},
reorderComponents: (state, action: PayloadAction<{ id: string; newIndex: number }>) => {
const { id, newIndex } = action.payload;
const currentIndex = state.components.findIndex(component => component.id === id);
if (currentIndex !== -1 && newIndex >= 0 && newIndex < state.components.length) {
const [removed] = state.components.splice(currentIndex, 1);
state.components.splice(newIndex, 0, removed);
}
},
clearMockup: (state) => {
state.components = [];
},
},
});
export const {
setMockup,
updateCanvas,
addComponent,
updateComponent,
removeComponent,
moveComponent,
resizeComponent,
reorderComponents,
clearMockup,
} = mockupSlice.actions;
export default mockupSlice.reducer;
+52
View File
@@ -0,0 +1,52 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SelectionState } from '@/types/mockup';
const initialState: SelectionState = {
selectedIds: [],
activeId: null,
};
export const selectionSlice = createSlice({
name: 'selection',
initialState,
reducers: {
selectComponent: (state, action: PayloadAction<string>) => {
state.selectedIds = [action.payload];
state.activeId = action.payload;
},
selectAdditionalComponent: (state, action: PayloadAction<string>) => {
if (!state.selectedIds.includes(action.payload)) {
state.selectedIds.push(action.payload);
}
state.activeId = action.payload;
},
deselectComponent: (state, action: PayloadAction<string>) => {
state.selectedIds = state.selectedIds.filter(id => id !== action.payload);
if (state.activeId === action.payload) {
state.activeId = state.selectedIds.length > 0 ? state.selectedIds[0] : null;
}
},
setActiveComponent: (state, action: PayloadAction<string | null>) => {
state.activeId = action.payload;
},
clearSelection: (state) => {
state.selectedIds = [];
state.activeId = null;
},
selectMultipleComponents: (state, action: PayloadAction<string[]>) => {
state.selectedIds = action.payload;
state.activeId = action.payload.length > 0 ? action.payload[0] : null;
},
},
});
export const {
selectComponent,
selectAdditionalComponent,
deselectComponent,
setActiveComponent,
clearSelection,
selectMultipleComponents,
} = selectionSlice.actions;
export default selectionSlice.reducer;
+65
View File
@@ -0,0 +1,65 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { UIState } from '@/types/mockup';
const initialState: UIState = {
activeTool: 'select',
activePanel: 'components',
sidebarWidth: 260,
colorPalette: [
'#1a2b47', // Dark blue
'#2563eb', // Blue
'#4ade80', // Green
'#f59e0b', // Yellow
'#ef4444', // Red
'#8b5cf6', // Purple
'#ec4899', // Pink
'#0d9488', // Teal
'#64748b', // Gray blue
'#0f172a', // Dark gray
'#ffffff', // White
'#000000', // Black
],
theme: 'light',
};
export const uiSlice = createSlice({
name: 'ui',
initialState,
reducers: {
setActiveTool: (state, action: PayloadAction<string>) => {
state.activeTool = action.payload;
},
setActivePanel: (state, action: PayloadAction<string>) => {
state.activePanel = action.payload;
},
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload;
},
addColorToPalette: (state, action: PayloadAction<string>) => {
if (!state.colorPalette.includes(action.payload)) {
state.colorPalette.push(action.payload);
}
},
removeColorFromPalette: (state, action: PayloadAction<string>) => {
state.colorPalette = state.colorPalette.filter(color => color !== action.payload);
},
setTheme: (state, action: PayloadAction<'light' | 'dark'>) => {
state.theme = action.payload;
},
resetUI: () => {
return initialState;
},
},
});
export const {
setActiveTool,
setActivePanel,
setSidebarWidth,
addColorToPalette,
removeColorFromPalette,
setTheme,
resetUI,
} = uiSlice.actions;
export default uiSlice.reducer;
+11
View File
@@ -0,0 +1,11 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import matchers from '@testing-library/jest-dom/matchers';
// Extend Vitest's expect with testing-library matchers
expect.extend(matchers);
// Clean up after each test
afterEach(() => {
cleanup();
});
+99
View File
@@ -0,0 +1,99 @@
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
// Define the color mode config
const config: ThemeConfig = {
initialColorMode: 'light',
useSystemColorMode: false,
};
// Define custom colors
const colors = {
brand: {
50: '#e6f6ff',
100: '#bae3ff',
200: '#7cc4fa',
300: '#47a3f3',
400: '#2186eb',
500: '#0967d2',
600: '#0552b5',
700: '#03449e',
800: '#01337d',
900: '#002159',
},
};
// Define custom component styles
const components = {
Button: {
baseStyle: {
fontWeight: 'semibold',
borderRadius: 'md',
},
variants: {
solid: {
bg: 'brand.500',
color: 'white',
_hover: {
bg: 'brand.600',
},
},
outline: {
border: '1px solid',
borderColor: 'brand.500',
color: 'brand.500',
},
ghost: {
color: 'brand.500',
_hover: {
bg: 'brand.50',
},
},
},
},
Tooltip: {
baseStyle: {
borderRadius: 'md',
bg: 'gray.700',
color: 'white',
fontSize: 'sm',
px: 2,
py: 1,
},
},
};
// Define custom text styles
const textStyles = {
h1: {
fontSize: ['2xl', '3xl'],
fontWeight: 'bold',
lineHeight: '110%',
},
h2: {
fontSize: ['xl', '2xl'],
fontWeight: 'semibold',
lineHeight: '110%',
},
};
// Extend the theme
const theme = extendTheme({
config,
colors,
components,
textStyles,
fonts: {
heading: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif`,
body: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif`,
},
styles: {
global: {
body: {
bg: 'gray.50',
color: 'gray.800',
},
},
},
});
export default theme;
+222
View File
@@ -0,0 +1,222 @@
// Base types for the mockup generator
export interface Position {
x: number;
y: number;
z?: number;
}
export interface Size {
width: number;
height: number;
}
export interface Canvas {
width: number;
height: number;
background: string;
gridSize: number;
showGrid: boolean;
}
export interface Style {
backgroundColor?: string;
borderRadius?: number;
borderWidth?: number;
borderColor?: string;
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none';
fontFamily?: string;
fontSize?: number;
fontWeight?: string;
fontStyle?: 'normal' | 'italic' | 'oblique';
textAlign?: 'left' | 'center' | 'right' | 'justify';
color?: string;
opacity?: number;
boxShadow?: string;
padding?: string;
margin?: string;
[key: string]: any;
}
// Base component interface
export interface BaseComponent {
id: string;
type: string;
position: Position;
size?: Size;
style?: Style;
data?: Record<string, any>;
}
// Funnel component
export interface FunnelStep {
label: string;
value: number;
dropoff?: number;
color?: string;
conversion?: boolean;
icon?: string;
}
export interface FunnelOptions {
showPercentages?: boolean;
showLabels?: boolean;
showValues?: boolean;
orientation?: 'vertical' | 'horizontal';
stepNumbering?: boolean;
}
export interface FunnelColors {
primary?: string;
secondary?: string;
tertiary?: string;
success?: string;
}
export interface FunnelData {
title?: string;
steps: FunnelStep[];
options?: FunnelOptions;
colors?: FunnelColors;
}
export interface FunnelComponent extends BaseComponent {
type: 'funnel';
data: FunnelData;
}
// Chart component
export interface ChartDataset {
label: string;
data: number[];
backgroundColor?: string | string[];
borderColor?: string | string[];
borderWidth?: number;
fill?: boolean;
tension?: number;
pointRadius?: number;
}
export interface ChartOptions {
showLegend?: boolean;
legendPosition?: 'top' | 'bottom' | 'left' | 'right';
showGrid?: boolean;
showTooltip?: boolean;
stacked?: boolean;
aspectRatio?: number;
axisLabels?: {
x?: string;
y?: string;
};
}
export interface ChartData {
title?: string;
labels: string[];
datasets: ChartDataset[];
}
export interface ChartComponent extends BaseComponent {
type: 'chart';
chartType: 'bar' | 'line' | 'pie' | 'area' | 'scatter' | 'radar' | 'bubble' | 'heatmap';
data: ChartData;
options?: ChartOptions;
}
// Text component
export interface TextData {
content: string;
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body1' | 'body2' | 'caption' | 'overline';
truncate?: boolean;
maxLines?: number;
}
export interface TextComponent extends BaseComponent {
type: 'text';
data: TextData;
}
// Card component
export interface CardHeader {
title?: string;
subtitle?: string;
icon?: string;
avatar?: string;
}
export interface CardBody {
content?: string;
image?: string;
imagePosition?: 'top' | 'bottom' | 'left' | 'right' | 'background';
}
export interface CardAction {
label: string;
icon?: string;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
}
export interface CardFooter {
content?: string;
actions?: CardAction[];
}
export interface CardOptions {
elevation?: number;
hoverable?: boolean;
outlined?: boolean;
}
export interface CardData {
header?: CardHeader;
body?: CardBody;
footer?: CardFooter;
}
export interface CardComponent extends BaseComponent {
type: 'card';
data: CardData;
options?: CardOptions;
}
// Union type for all components
export type Component =
| FunnelComponent
| ChartComponent
| TextComponent
| CardComponent
| BaseComponent;
// Mockup type (root JSON structure)
export interface Mockup {
version: string;
canvas: Canvas;
components: Component[];
}
// Application state types
export interface SelectionState {
selectedIds: string[];
activeId: string | null;
}
export interface HistoryState {
past: Mockup[];
current: Mockup;
future: Mockup[];
}
export interface UIState {
activeTool: string;
activePanel: string;
sidebarWidth: number;
colorPalette: string[];
theme: 'light' | 'dark';
}
export interface AppState {
mockup: Mockup;
selection: SelectionState;
history: HistoryState;
ui: UIState;
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+24
View File
@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
open: true,
// Proxy API requests to the backend during development
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
});
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.js'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
+12832
View File
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "ui-mockup-generator",
"version": "0.1.0",
"description": "A web application for creating UI mockups with JSON configuration that can be used with LLMs",
"private": true,
"workspaces": [
"client",
"server"
],
"scripts": {
"start": "npm run start:client",
"start:client": "npm run start --workspace=client",
"start:server": "npm run start --workspace=server",
"dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
"dev:client": "npm run dev --workspace=client",
"dev:server": "npm run dev --workspace=server",
"build": "npm run build --workspace=client",
"test": "npm run test --workspace=client && npm run test --workspace=server",
"lint": "npm run lint --workspace=client && npm run lint --workspace=server",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
},
"devDependencies": {
"concurrently": "^8.2.2",
"prettier": "^3.1.0"
},
"engines": {
"node": ">=16.0.0"
},
"author": "",
"license": "MIT"
}
+12
View File
@@ -0,0 +1,12 @@
# Server Configuration
PORT=3001
NODE_ENV=development
# JWT Configuration
JWT_SECRET=your-secret-key-change-this-in-production
# MongoDB Configuration
MONGODB_URI=mongodb://localhost:27017/ui-mockup-generator
# Cors Configuration
CORS_ORIGIN=http://localhost:3000
+40
View File
@@ -0,0 +1,40 @@
{
"name": "ui-mockup-generator-server",
"version": "0.1.0",
"description": "Backend server for UI Mockup Generator",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon --watch src --exec ts-node src/index.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint src --ext ts --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"@types/cors": "^2.8.16",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.10",
"@types/jsonwebtoken": "^9.0.5",
"@types/multer": "^1.4.10",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"nodemon": "^3.0.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
},
"engines": {
"node": ">=16.0.0"
}
}
+92
View File
@@ -0,0 +1,92 @@
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import User from '../models/User';
// JWT secret key
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Register a new user
export const register = async (req: Request, res: Response) => {
try {
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user
const newUser = new User({
username,
email,
password, // Note: Password should be hashed in the User model
});
await newUser.save();
// Generate JWT token
const token = jwt.sign(
{ userId: newUser._id },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
token,
user: {
id: newUser._id,
username: newUser.username,
email: newUser.email,
},
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Login
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Check password (should use compare method in a real app)
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user._id },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(200).json({
token,
user: {
id: user._id,
username: user.username,
email: user.email,
},
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Logout (in a JWT-based auth, this is mostly client-side)
export const logout = (req: Request, res: Response) => {
// In a real app, you might want to blacklist the token
// or handle refresh tokens
res.status(200).json({ message: 'Logged out successfully' });
};
+100
View File
@@ -0,0 +1,100 @@
import { Request, Response } from 'express';
import Project from '../models/Project';
// Get all projects
export const getProjects = async (req: Request, res: Response) => {
try {
const projects = await Project.find({ user: req.userId });
res.status(200).json(projects);
} catch (error) {
console.error('Error fetching projects:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Get project by ID
export const getProjectById = async (req: Request, res: Response) => {
try {
const project = await Project.findOne({
_id: req.params.id,
user: req.userId
});
if (!project) {
return res.status(404).json({ message: 'Project not found' });
}
res.status(200).json(project);
} catch (error) {
console.error('Error fetching project:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Create a new project
export const createProject = async (req: Request, res: Response) => {
try {
const { name, description, content } = req.body;
const newProject = new Project({
name,
description,
content,
user: req.userId,
createdAt: new Date(),
updatedAt: new Date()
});
const savedProject = await newProject.save();
res.status(201).json(savedProject);
} catch (error) {
console.error('Error creating project:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Update a project
export const updateProject = async (req: Request, res: Response) => {
try {
const { name, description, content } = req.body;
const project = await Project.findOneAndUpdate(
{ _id: req.params.id, user: req.userId },
{
name,
description,
content,
updatedAt: new Date()
},
{ new: true }
);
if (!project) {
return res.status(404).json({ message: 'Project not found' });
}
res.status(200).json(project);
} catch (error) {
console.error('Error updating project:', error);
res.status(500).json({ message: 'Server error' });
}
};
// Delete a project
export const deleteProject = async (req: Request, res: Response) => {
try {
const project = await Project.findOneAndDelete({
_id: req.params.id,
user: req.userId
});
if (!project) {
return res.status(404).json({ message: 'Project not found' });
}
res.status(200).json({ message: 'Project deleted successfully' });
} catch (error) {
console.error('Error deleting project:', error);
res.status(500).json({ message: 'Server error' });
}
};
+63
View File
@@ -0,0 +1,63 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import mongoose from 'mongoose';
import projectRoutes from './routes/projectRoutes';
import authRoutes from './routes/authRoutes';
// Load environment variables
dotenv.config();
// Create Express app
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// API routes
app.use('/api/projects', projectRoutes);
app.use('/api/auth', authRoutes);
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// Connect to MongoDB (if available)
const connectDB = async () => {
try {
if (process.env.MONGODB_URI) {
await mongoose.connect(process.env.MONGODB_URI);
console.log('MongoDB connected successfully');
} else {
console.log('MongoDB URI not provided, running without database');
}
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
// Start server
const startServer = async () => {
try {
// Connect to database (optional)
if (process.env.MONGODB_URI) {
await connectDB();
}
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
// Initialize server
startServer();
+35
View File
@@ -0,0 +1,35 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
// JWT secret key
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Define custom Request interface with userId
interface AuthRequest extends Request {
userId?: string;
}
// Middleware to authenticate JWT token
export const auth = (req: AuthRequest, res: Response, next: NextFunction) => {
try {
// Get token from header
const token = req.header('x-auth-token');
// Check if no token
if (!token) {
return res.status(401).json({ message: 'No token, authorization denied' });
}
// Verify token
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
// Add user ID to request
req.userId = decoded.userId;
// Continue to the next middleware/controller
next();
} catch (error) {
console.error('Token verification failed:', error);
res.status(401).json({ message: 'Token is not valid' });
}
};
+41
View File
@@ -0,0 +1,41 @@
import mongoose, { Schema, Document } from 'mongoose';
export interface IProject extends Document {
name: string;
description?: string;
content: any;
user: mongoose.Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const ProjectSchema: Schema = new Schema({
name: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
trim: true,
},
content: {
type: Schema.Types.Mixed,
required: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
export default mongoose.model<IProject>('Project', ProjectSchema);
+63
View File
@@ -0,0 +1,63 @@
import mongoose, { Schema, Document } from 'mongoose';
import { hash, compare } from 'bcrypt';
export interface IUser extends Document {
username: string;
email: string;
password: string;
createdAt: Date;
comparePassword(candidatePassword: string): Promise<boolean>;
}
const UserSchema: Schema = new Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please provide a valid email address'],
},
password: {
type: String,
required: true,
minlength: 6,
},
createdAt: {
type: Date,
default: Date.now,
},
});
// Hash password before saving
UserSchema.pre<IUser>('save', async function(next) {
// Only hash the password if it's modified (or new)
if (!this.isModified('password')) return next();
try {
// Generate a salt
const salt = 10;
// Hash the password with the salt
this.password = await hash(this.password, salt);
next();
} catch (error) {
next(error as Error);
}
});
// Method to compare password
UserSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
try {
return await compare(candidatePassword, this.password);
} catch (error) {
throw error;
}
};
export default mongoose.model<IUser>('User', UserSchema);
+15
View File
@@ -0,0 +1,15 @@
import express from 'express';
import { register, login, logout } from '../controllers/authController';
const router = express.Router();
// Register a new user
router.post('/register', register);
// Login
router.post('/login', login);
// Logout
router.post('/logout', logout);
export default router;
+27
View File
@@ -0,0 +1,27 @@
import express from 'express';
import {
getProjects,
getProjectById,
createProject,
updateProject,
deleteProject
} from '../controllers/projectController';
const router = express.Router();
// Get all projects
router.get('/', getProjects);
// Get project by ID
router.get('/:id', getProjectById);
// Create a new project
router.post('/', createProject);
// Update a project
router.put('/:id', updateProject);
// Delete a project
router.delete('/:id', deleteProject);
export default router;
+9
View File
@@ -0,0 +1,9 @@
import { Express, Request } from 'express';
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}
+406
View File
@@ -0,0 +1,406 @@
# UI Mockup Generator - Implementation Plan
This document outlines the technical implementation plan for building the UI Mockup Generator application.
## Technology Stack
### Frontend
- **Framework**: React.js (v18+) with TypeScript
- **State Management**: Redux Toolkit
- **UI Component Library**: Chakra UI
- **Styling**: Emotion (comes with Chakra UI)
- **Canvas Management**: react-konva or fabric.js
- **Charts**: recharts
- **Color Picker**: react-colorful
- **Drag and Drop**: react-dnd or react-beautiful-dnd
- **Export Utilities**: html-to-image, file-saver
### Backend (Optional - for saving projects)
- **Runtime**: Node.js
- **Framework**: Express.js
- **Database**: MongoDB with Mongoose
- **Authentication**: JWT
- **File Storage**: Local or AWS S3
## Project Structure
```
ui-mockup-generator/
├── client/ # Frontend
│ ├── public/
│ ├── src/
│ │ ├── assets/ # Static assets
│ │ ├── components/ # Reusable UI components
│ │ │ ├── canvas/ # Canvas and drawing components
│ │ │ ├── common/ # Common UI components
│ │ │ ├── editor/ # Editor-specific components
│ │ │ ├── mockups/ # Mockup component implementations
│ │ │ └── panels/ # Control panels and toolbars
│ │ ├── hooks/ # Custom React hooks
│ │ ├── store/ # Redux store setup
│ │ │ ├── slices/ # Redux slices
│ │ │ └── index.ts # Store configuration
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ ├── App.tsx # Main app component
│ │ └── index.tsx # Entry point
│ ├── package.json
│ └── tsconfig.json
├── server/ # Backend (optional)
│ ├── src/
│ │ ├── controllers/ # Route controllers
│ │ ├── middleware/ # Express middleware
│ │ ├── models/ # Database models
│ │ ├── routes/ # API routes
│ │ ├── utils/ # Utility functions
│ │ └── index.ts # Entry point
│ ├── package.json
│ └── tsconfig.json
├── package.json # Root package.json
└── README.md
```
## Implementation Phases
### Phase 1: Project Setup and Core Infrastructure (Week 1)
1. **Project Initialization**
- Set up React project with TypeScript
- Configure Chakra UI
- Set up Redux store with Redux Toolkit
- Create basic folder structure
2. **Canvas Implementation**
- Implement basic canvas with react-konva or fabric.js
- Set up zoom and pan functionality
- Implement grid and snap-to-grid
- Create base component class structure
3. **Basic Component System**
- Implement component registration system
- Create base component rendering framework
- Develop component selection and highlighting
- Basic property panel implementation
### Phase 2: Core Components and Editor (Week 2)
1. **Basic Shape Components**
- Rectangle/box component
- Text component
- Image component
- Simple container component
2. **Editor Features**
- Component drag-and-drop
- Resize and rotation handles
- Layer management
- Copy, paste, delete operations
3. **Property Panel**
- Component property editing
- Style editing (colors, borders, etc.)
- Position and size controls
- Common property editors
### Phase 3: Advanced Components (Week 3)
1. **Funnel Component**
- Implement funnel visualization
- Configure step management
- Style customization
- Dropoff percentage visualization
2. **Chart Components**
- Bar chart implementation
- Line chart implementation
- Pie chart implementation
- Data configuration
3. **UI Components**
- Card component
- Button component
- Form elements
- Navigation components
### Phase 4: JSON Import/Export & Styling (Week 4)
1. **JSON Schema**
- Finalize JSON schema
- Implement validation
- Create export functionality
- Import from JSON
2. **Style System**
- Color palette management
- Theme support
- Style presets
- Responsive design tools
3. **Export System**
- Export as PNG/SVG
- Export JSON configuration
- Clipboard support
### Phase 5: LLM Integration & Documentation (Week 5)
1. **LLM Integration**
- Create LLM-friendly schema documentation
- Build example prompts
- Implement schema validation for LLM outputs
- Test with various prompts
2. **Documentation**
- User documentation
- API documentation
- JSON schema documentation
- Example gallery
3. **Final Polish**
- Performance optimization
- Bug fixes
- UI/UX improvements
- Accessibility audit
## Component Implementation Details
### Canvas System
The canvas will be the core of the application, responsible for:
- Rendering components
- Handling selection and focus
- Managing component hierarchy
- Implementing snap-to-grid
- Providing zoom and pan functionality
```typescript
// Example Canvas component outline
interface CanvasProps {
width: number;
height: number;
background: string;
components: Component[];
gridSize: number;
showGrid: boolean;
}
const Canvas: React.FC<CanvasProps> = ({
width,
height,
background,
components,
gridSize,
showGrid
}) => {
// Canvas implementation using react-konva or similar
};
```
### Component System
Each component will be implemented as a React component with:
- Standard props interface
- Rendering logic
- Property controls
- Selection handling
- Drag behavior
```typescript
// Example base component interface
interface BaseComponentProps {
id: string;
type: string;
position: { x: number; y: number; z?: number };
size?: { width: number; height: number };
style?: Record<string, any>;
data?: Record<string, any>;
selected?: boolean;
onSelect?: (id: string) => void;
}
// Example component registration system
const componentRegistry = {
funnel: FunnelComponent,
chart: ChartComponent,
text: TextComponent,
// ...other components
};
const renderComponent = (component: BaseComponentProps) => {
const Component = componentRegistry[component.type];
if (!Component) return null;
return <Component {...component} />;
};
```
### Funnel Component Implementation
The funnel component will visualize conversion funnels:
```typescript
// Example funnel component implementation outline
interface FunnelStep {
label: string;
value: number;
dropoff?: number;
color?: string;
conversion?: boolean;
}
interface FunnelProps extends BaseComponentProps {
data: {
steps: FunnelStep[];
options?: {
showPercentages?: boolean;
showLabels?: boolean;
orientation?: 'vertical' | 'horizontal';
};
colors?: {
primary: string;
secondary: string;
tertiary: string;
success: string;
};
};
}
const FunnelComponent: React.FC<FunnelProps> = ({ id, position, size, data, style, selected, onSelect }) => {
// Funnel rendering logic using SVG or Canvas
};
```
### JSON Import/Export System
The JSON import/export system will handle:
- Converting the internal state to JSON
- Importing JSON into the application state
- Validating JSON against the schema
- Generating code for external use
```typescript
// Example JSON export function outline
const exportToJson = (state: AppState): string => {
const { canvas, components } = state;
return JSON.stringify({
version: '1.0',
canvas: {
width: canvas.width,
height: canvas.height,
background: canvas.background,
gridSize: canvas.gridSize,
showGrid: canvas.showGrid
},
components: components.map(component => ({
id: component.id,
type: component.type,
position: component.position,
size: component.size,
style: component.style,
data: component.data
}))
}, null, 2);
};
```
## Redux Store Structure
The Redux store will manage the application state:
```typescript
// Example Redux slice structure
interface AppState {
canvas: {
width: number;
height: number;
background: string;
gridSize: number;
showGrid: boolean;
zoom: number;
pan: { x: number; y: number };
};
components: Component[];
selection: {
selectedIds: string[];
activeId: string | null;
};
history: {
past: AppState[];
future: AppState[];
};
ui: {
activeTool: string;
activePanel: string;
sidebarWidth: number;
colorPalette: string[];
theme: 'light' | 'dark';
};
}
```
## API Endpoints (Optional Backend)
If implementing a backend for project saving:
```
POST /api/projects - Create a new project
GET /api/projects - List user projects
GET /api/projects/:id - Get a specific project
PUT /api/projects/:id - Update a project
DELETE /api/projects/:id - Delete a project
POST /api/auth/register - Register a new user
POST /api/auth/login - Login
POST /api/auth/logout - Logout
```
## Development and Build Process
1. **Development Setup**
- `npm install` - Install dependencies
- `npm run dev` - Start development server
- `npm run test` - Run tests
2. **Build Process**
- `npm run build` - Build production version
- `npm run lint` - Run linting
- `npm run format` - Run code formatting
3. **Deployment**
- Deploy frontend to Vercel or Netlify
- Deploy backend to Heroku or similar (if implemented)
## Testing Strategy
1. **Unit Tests**
- Component rendering tests
- Utility function tests
- Redux reducer tests
2. **Integration Tests**
- Component interaction tests
- JSON import/export tests
- Redux action flow tests
3. **E2E Tests**
- User flow tests
- Export functionality tests
## Next Steps and Future Enhancements
1. **Advanced Functionality**
- Collaboration features
- Version history
- Template marketplace
- Custom component creation
2. **Integration Options**
- API for programmatic access
- Embed code generation
- Plugin system
3. **AI Enhancements**
- AI-assisted design suggestions
- Automated layout optimization
- Smart data generation
This implementation plan provides a roadmap for building the UI Mockup Generator application from initial setup through to completion, with a focus on creating a flexible, extensible system for generating UI mockups that can be easily integrated with LLMs.
+638
View File
@@ -0,0 +1,638 @@
# UI Mockup Generator JSON Schema
This document provides a detailed specification of the JSON schema used for configuring mockup UI elements in the UI Mockup Generator application.
## Base Schema
All mockups follow this base schema:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UI Mockup",
"type": "object",
"required": ["version", "canvas", "components"],
"properties": {
"version": {
"type": "string",
"description": "Schema version"
},
"canvas": {
"type": "object",
"description": "Canvas properties",
"required": ["width", "height"],
"properties": {
"width": {
"type": "number",
"description": "Canvas width in pixels"
},
"height": {
"type": "number",
"description": "Canvas height in pixels"
},
"background": {
"type": "string",
"description": "Canvas background color (hex, rgb, or named)"
},
"gridSize": {
"type": "number",
"description": "Size of grid for snapping (if enabled)"
},
"showGrid": {
"type": "boolean",
"description": "Whether to display the grid"
}
}
},
"components": {
"type": "array",
"description": "Array of UI components",
"items": {
"type": "object",
"required": ["id", "type", "position"],
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the component"
},
"type": {
"type": "string",
"description": "Component type",
"enum": ["funnel", "chart", "table", "card", "text", "image", "button", "input", "container", "icon", "avatar", "navigation", "modal", "alert"]
},
"position": {
"type": "object",
"description": "Position on canvas",
"required": ["x", "y"],
"properties": {
"x": {
"type": "number",
"description": "X coordinate"
},
"y": {
"type": "number",
"description": "Y coordinate"
},
"z": {
"type": "number",
"description": "Z-index/layer order"
}
}
},
"size": {
"type": "object",
"description": "Component dimensions",
"properties": {
"width": {
"type": "number",
"description": "Width in pixels"
},
"height": {
"type": "number",
"description": "Height in pixels"
}
}
},
"style": {
"type": "object",
"description": "Visual styling properties",
"properties": {
"backgroundColor": {
"type": "string",
"description": "Background color"
},
"borderRadius": {
"type": "number",
"description": "Border radius in pixels"
},
"borderWidth": {
"type": "number",
"description": "Border width in pixels"
},
"borderColor": {
"type": "string",
"description": "Border color"
},
"borderStyle": {
"type": "string",
"enum": ["solid", "dashed", "dotted", "none"],
"description": "Border style"
},
"fontFamily": {
"type": "string",
"description": "Font family"
},
"fontSize": {
"type": "number",
"description": "Font size in pixels"
},
"fontWeight": {
"type": "string",
"description": "Font weight",
"enum": ["normal", "bold", "lighter", "bolder", "100", "200", "300", "400", "500", "600", "700", "800", "900"]
},
"fontStyle": {
"type": "string",
"description": "Font style",
"enum": ["normal", "italic", "oblique"]
},
"textAlign": {
"type": "string",
"description": "Text alignment",
"enum": ["left", "center", "right", "justify"]
},
"color": {
"type": "string",
"description": "Text color"
},
"opacity": {
"type": "number",
"description": "Opacity (0-1)"
},
"boxShadow": {
"type": "string",
"description": "Box shadow CSS value"
},
"padding": {
"type": "string",
"description": "Padding (CSS shorthand or object)"
},
"margin": {
"type": "string",
"description": "Margin (CSS shorthand or object)"
}
}
}
}
}
}
}
}
```
## Component-Specific Schemas
### Funnel Component
```json
{
"type": "funnel",
"data": {
"title": {
"type": "string",
"description": "Funnel title"
},
"steps": {
"type": "array",
"description": "Funnel steps",
"items": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": {
"type": "string",
"description": "Step label"
},
"value": {
"type": "number",
"description": "Step value"
},
"dropoff": {
"type": "number",
"description": "Dropoff percentage"
},
"color": {
"type": "string",
"description": "Custom color for this step"
},
"icon": {
"type": "string",
"description": "Icon identifier for this step"
},
"conversion": {
"type": "boolean",
"description": "Whether this step represents conversion"
}
}
}
},
"options": {
"type": "object",
"properties": {
"showPercentages": {
"type": "boolean",
"description": "Whether to show percentages"
},
"showLabels": {
"type": "boolean",
"description": "Whether to show step labels"
},
"showValues": {
"type": "boolean",
"description": "Whether to show absolute values"
},
"orientation": {
"type": "string",
"enum": ["vertical", "horizontal"],
"description": "Funnel orientation"
},
"stepNumbering": {
"type": "boolean",
"description": "Whether to show step numbers"
}
}
},
"colors": {
"type": "object",
"properties": {
"primary": {
"type": "string",
"description": "Primary color"
},
"secondary": {
"type": "string",
"description": "Secondary color (dropoff)"
},
"tertiary": {
"type": "string",
"description": "Tertiary color (background)"
},
"success": {
"type": "string",
"description": "Success color (conversion)"
}
}
}
}
}
```
### Chart Component
```json
{
"type": "chart",
"chartType": {
"type": "string",
"enum": ["bar", "line", "pie", "area", "scatter", "radar", "bubble", "heatmap"],
"description": "Type of chart to display"
},
"data": {
"title": {
"type": "string",
"description": "Chart title"
},
"labels": {
"type": "array",
"description": "Labels for data points",
"items": {
"type": "string"
}
},
"datasets": {
"type": "array",
"description": "Data series",
"items": {
"type": "object",
"required": ["label", "data"],
"properties": {
"label": {
"type": "string",
"description": "Dataset label"
},
"data": {
"type": "array",
"description": "Data points",
"items": {
"type": "number"
}
},
"backgroundColor": {
"type": ["string", "array"],
"description": "Background color(s)"
},
"borderColor": {
"type": ["string", "array"],
"description": "Border color(s)"
},
"borderWidth": {
"type": "number",
"description": "Border width"
},
"fill": {
"type": "boolean",
"description": "Whether to fill area (for area charts)"
},
"tension": {
"type": "number",
"description": "Line tension (for line and area charts)"
},
"pointRadius": {
"type": "number",
"description": "Point radius (for line and scatter charts)"
}
}
}
}
},
"options": {
"type": "object",
"properties": {
"showLegend": {
"type": "boolean",
"description": "Whether to show legend"
},
"legendPosition": {
"type": "string",
"enum": ["top", "bottom", "left", "right"],
"description": "Legend position"
},
"showGrid": {
"type": "boolean",
"description": "Whether to show grid lines"
},
"showTooltip": {
"type": "boolean",
"description": "Whether to show tooltips"
},
"stacked": {
"type": "boolean",
"description": "Whether to stack data (bar, area)"
},
"aspectRatio": {
"type": "number",
"description": "Aspect ratio of the chart"
},
"axisLabels": {
"type": "object",
"properties": {
"x": {
"type": "string",
"description": "X-axis label"
},
"y": {
"type": "string",
"description": "Y-axis label"
}
}
}
}
}
}
```
### Card Component
```json
{
"type": "card",
"data": {
"header": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Card title"
},
"subtitle": {
"type": "string",
"description": "Card subtitle"
},
"icon": {
"type": "string",
"description": "Icon identifier"
},
"avatar": {
"type": "string",
"description": "Avatar image URL"
}
}
},
"body": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Text content"
},
"image": {
"type": "string",
"description": "Image URL"
},
"imagePosition": {
"type": "string",
"enum": ["top", "bottom", "left", "right", "background"],
"description": "Image position"
}
}
},
"footer": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Footer content"
},
"actions": {
"type": "array",
"description": "Action buttons",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Button label"
},
"icon": {
"type": "string",
"description": "Button icon"
},
"variant": {
"type": "string",
"enum": ["primary", "secondary", "outline", "ghost", "link"],
"description": "Button variant"
}
}
}
}
}
}
},
"options": {
"type": "object",
"properties": {
"elevation": {
"type": "number",
"description": "Card elevation level (0-5)"
},
"hoverable": {
"type": "boolean",
"description": "Whether card has hover effect"
},
"outlined": {
"type": "boolean",
"description": "Whether to use outline style"
}
}
}
}
```
### Text Component
```json
{
"type": "text",
"data": {
"content": {
"type": "string",
"description": "Text content"
},
"variant": {
"type": "string",
"enum": ["h1", "h2", "h3", "h4", "h5", "h6", "body1", "body2", "caption", "overline"],
"description": "Text variant"
},
"truncate": {
"type": "boolean",
"description": "Whether to truncate with ellipsis"
},
"maxLines": {
"type": "number",
"description": "Maximum number of lines"
}
}
}
```
### Additional Components
Additional component schemas will follow similar patterns for:
- Table/Data Grid
- Button
- Input (text, checkbox, select, etc.)
- Container/Box
- Icon
- Avatar
- Navigation (tabs, breadcrumbs, etc.)
- Modal/Dialog
- Alert/Notification
- Image
- Progress indicators
## Example Complete Mockup
```json
{
"version": "1.0",
"canvas": {
"width": 1200,
"height": 800,
"background": "#f5f5f5",
"gridSize": 8,
"showGrid": true
},
"components": [
{
"id": "header-1",
"type": "text",
"position": { "x": 40, "y": 40, "z": 1 },
"data": {
"content": "FUNNEL VIEWER",
"variant": "h2"
},
"style": {
"fontFamily": "Arial",
"fontWeight": "bold",
"color": "#1a2b47"
}
},
{
"id": "steps-label",
"type": "text",
"position": { "x": 40, "y": 100, "z": 1 },
"data": {
"content": "STEPS",
"variant": "body2"
},
"style": {
"fontFamily": "Arial",
"fontWeight": "bold",
"color": "#666666"
}
},
{
"id": "funnel-1",
"type": "funnel",
"position": { "x": 100, "y": 140, "z": 1 },
"size": { "width": 500, "height": 400 },
"data": {
"steps": [
{ "label": "Product Details - Viewed", "value": 100, "dropoff": 36 },
{ "label": "Add to Cart - Clicked", "value": 64, "dropoff": 27 },
{ "label": "Details - Entered", "value": 37, "dropoff": 33 },
{ "label": "Place Order - Clicked", "value": 16, "conversion": true }
],
"options": {
"showPercentages": true,
"showLabels": true,
"orientation": "vertical",
"stepNumbering": true
},
"colors": {
"primary": "#1a2b47",
"secondary": "#e0756b",
"tertiary": "#d3d3d3",
"success": "#4caf50"
}
},
"style": {
"borderRadius": 5,
"fontFamily": "Arial",
"fontSize": 14
}
},
{
"id": "segments-card",
"type": "card",
"position": { "x": 650, "y": 140, "z": 1 },
"size": { "width": 400, "height": 250 },
"data": {
"header": {
"title": "SEGMENTS & FILTERS"
},
"body": {
"content": ""
}
},
"style": {
"backgroundColor": "#f0f4ff",
"borderRadius": 8,
"padding": "16px"
}
},
{
"id": "replay-card",
"type": "card",
"position": { "x": 650, "y": 410, "z": 1 },
"size": { "width": 400, "height": 250 },
"data": {
"header": {
"title": "ANALYSIS REPLAY"
},
"body": {
"content": ""
}
},
"style": {
"backgroundColor": "#e6fff0",
"borderRadius": 8,
"padding": "16px"
}
}
]
}
```
This schema provides a foundation for defining UI mockups in a structured, consistent format that can be easily generated by LLMs or created through the UI Mockup Generator application.
+354
View File
@@ -0,0 +1,354 @@
# LLM Integration Guide for UI Mockup Generator
This document provides guidelines for integrating Large Language Models (LLMs) with the UI Mockup Generator to automatically generate UI mockups from natural language descriptions.
## Overview
The UI Mockup Generator uses a standardized JSON schema to define mockups. This schema is designed to be both human-readable and machine-friendly, making it ideal for LLM integration. By providing appropriate prompts and examples, LLMs can generate valid JSON configurations that can be directly imported into the UI Mockup Generator.
## Prompting Strategy
### Best Practices for Prompting LLMs
1. **Be Specific About Requirements**
- Specify the exact type of UI mockup needed (e.g., "a funnel visualization showing conversion rates")
- Include details about colors, layout, and component types
- Mention any specific data that should be visualized
2. **Use Structured Prompts**
- Start with a clear request for a UI mockup in JSON format
- Specify that the output should follow the UI Mockup Generator schema
- Request specific components where appropriate
- Include any size/dimension constraints
3. **Provide Schema Context**
- Include a brief explanation of the schema structure
- Mention that the output should be valid JSON
- Reference specific component types available in the schema
### Example Prompts
**Basic Prompt Structure:**
```
Generate a UI mockup in JSON format for the UI Mockup Generator that shows [description of desired mockup].
The mockup should include [specific component requirements].
Follow the UI Mockup Generator schema with version 1.0 format.
```
**Specific Example (Funnel Visualization):**
```
Generate a UI mockup in JSON format for the UI Mockup Generator that shows an e-commerce funnel visualization.
The funnel should show the conversion path from product view to purchase with the following steps:
1. Product View (1000 visitors)
2. Add to Cart (650 visitors)
3. Checkout Started (350 visitors)
4. Purchase Completed (200 visitors)
Use a clean, modern design with blue as the primary color.
Include a title at the top and show percentage dropoffs between steps.
Follow the UI Mockup Generator schema with version 1.0 format.
```
## Schema Guidelines for LLMs
When prompting an LLM to generate a mockup, it's helpful to include these guidelines to improve the quality of the output:
1. **Required Structure**
- All mockups must include `version`, `canvas`, and `components` properties
- Each component must have a unique `id`, `type`, and `position`
- Follow the type-specific schema for each component type
2. **Component Types**
- Use appropriate component types (funnel, chart, text, card, etc.)
- Include all required properties for each component type
- Use consistent naming for component properties
3. **Styling Guidelines**
- Use hex color codes (e.g., "#1a2b47") or named colors
- Follow CSS-like conventions for styling properties
- Keep font choices consistent throughout the mockup
4. **Data Representation**
- Use realistic data values for visualizations
- Ensure chart data is properly formatted as arrays
- Match labels and values appropriately
## Example LLM Interaction
**User Prompt:**
```
Generate a UI mockup in JSON format for the UI Mockup Generator that shows a product analytics dashboard.
It should include:
1. A header with the title "Product Analytics Dashboard"
2. A funnel showing the conversion flow from view to purchase
3. A bar chart showing top 5 products by revenue
4. A card showing key metrics (total revenue, conversion rate, average order value)
Use a professional color scheme with blue as the primary color.
```
**Expected LLM Response:**
```json
{
"version": "1.0",
"canvas": {
"width": 1200,
"height": 800,
"background": "#f5f5f5",
"gridSize": 8,
"showGrid": false
},
"components": [
{
"id": "header",
"type": "text",
"position": { "x": 40, "y": 30, "z": 1 },
"data": {
"content": "Product Analytics Dashboard",
"variant": "h1"
},
"style": {
"fontFamily": "Inter",
"fontSize": 32,
"fontWeight": "bold",
"color": "#1a2b47"
}
},
{
"id": "conversion-funnel",
"type": "funnel",
"position": { "x": 40, "y": 100, "z": 1 },
"size": { "width": 500, "height": 300 },
"data": {
"title": "Conversion Funnel",
"steps": [
{ "label": "Product View", "value": 5000, "dropoff": 30 },
{ "label": "Add to Cart", "value": 3500, "dropoff": 40 },
{ "label": "Checkout", "value": 2100, "dropoff": 25 },
{ "label": "Purchase", "value": 1575, "conversion": true }
],
"options": {
"showPercentages": true,
"showLabels": true,
"orientation": "vertical"
},
"colors": {
"primary": "#2563eb",
"secondary": "#ef4444",
"tertiary": "#e5e7eb",
"success": "#10b981"
}
},
"style": {
"borderRadius": 8,
"backgroundColor": "#ffffff",
"padding": "16px",
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)"
}
},
{
"id": "top-products-chart",
"type": "chart",
"position": { "x": 580, "y": 100, "z": 1 },
"size": { "width": 580, "height": 300 },
"chartType": "bar",
"data": {
"title": "Top 5 Products by Revenue",
"labels": ["Product A", "Product B", "Product C", "Product D", "Product E"],
"datasets": [
{
"label": "Revenue",
"data": [12000, 9500, 7200, 6800, 5400],
"backgroundColor": "#2563eb"
}
]
},
"options": {
"showLegend": false,
"showGrid": true,
"showTooltip": true,
"axisLabels": {
"x": "Products",
"y": "Revenue ($)"
}
},
"style": {
"borderRadius": 8,
"backgroundColor": "#ffffff",
"padding": "16px",
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)"
}
},
{
"id": "key-metrics-card",
"type": "card",
"position": { "x": 40, "y": 420, "z": 1 },
"size": { "width": 1120, "height": 160 },
"data": {
"header": {
"title": "Key Metrics"
},
"body": {
"content": ""
}
},
"style": {
"borderRadius": 8,
"backgroundColor": "#ffffff",
"padding": "16px",
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)"
}
},
{
"id": "revenue-metric",
"type": "text",
"position": { "x": 80, "y": 470, "z": 2 },
"data": {
"content": "Total Revenue",
"variant": "body1"
},
"style": {
"fontFamily": "Inter",
"fontSize": 16,
"color": "#6b7280"
}
},
{
"id": "revenue-value",
"type": "text",
"position": { "x": 80, "y": 500, "z": 2 },
"data": {
"content": "$157,500",
"variant": "h3"
},
"style": {
"fontFamily": "Inter",
"fontSize": 28,
"fontWeight": "bold",
"color": "#2563eb"
}
},
{
"id": "conversion-metric",
"type": "text",
"position": { "x": 440, "y": 470, "z": 2 },
"data": {
"content": "Conversion Rate",
"variant": "body1"
},
"style": {
"fontFamily": "Inter",
"fontSize": 16,
"color": "#6b7280"
}
},
{
"id": "conversion-value",
"type": "text",
"position": { "x": 440, "y": 500, "z": 2 },
"data": {
"content": "31.5%",
"variant": "h3"
},
"style": {
"fontFamily": "Inter",
"fontSize": 28,
"fontWeight": "bold",
"color": "#2563eb"
}
},
{
"id": "aov-metric",
"type": "text",
"position": { "x": 800, "y": 470, "z": 2 },
"data": {
"content": "Average Order Value",
"variant": "body1"
},
"style": {
"fontFamily": "Inter",
"fontSize": 16,
"color": "#6b7280"
}
},
{
"id": "aov-value",
"type": "text",
"position": { "x": 800, "y": 500, "z": 2 },
"data": {
"content": "$100.00",
"variant": "h3"
},
"style": {
"fontFamily": "Inter",
"fontSize": 28,
"fontWeight": "bold",
"color": "#2563eb"
}
}
]
}
```
## Common Issues and Troubleshooting
1. **Invalid JSON Structure**
- Ensure the LLM outputs valid JSON format
- Check for missing commas, brackets, or quotation marks
- Validate the JSON structure before importing
2. **Missing Required Properties**
- Verify that all components have the required properties (id, type, position)
- Check that the canvas has width and height properties
- Ensure nested objects have their required properties
3. **Incorrect Component Types**
- Verify that only supported component types are used
- Check that component-specific properties follow the schema
4. **Inconsistent Styling**
- Look for inconsistent color formats or values
- Check for missing or invalid style properties
- Ensure font properties are consistent
## Advanced LLM Integration
### Incremental Mockup Generation
For complex mockups, consider using an incremental approach:
1. Generate the basic structure and layout first
2. Add detailed components one by one
3. Refine styling and positioning iteratively
### Using Component Templates
Create templates for common components that the LLM can reference:
```json
{
"funnel_template": {
"type": "funnel",
"position": { "x": 0, "y": 0 },
"size": { "width": 500, "height": 300 },
"data": {
"steps": [
{ "label": "Step 1", "value": 100, "dropoff": 20 },
{ "label": "Step 2", "value": 80, "dropoff": 25 },
{ "label": "Step 3", "value": 60, "conversion": true }
]
}
}
}
```
### Specialized Prompting
For specialized mockups, use prompts that focus on specific aspects:
1. **Layout-focused prompts**: Emphasize positioning and overall structure
2. **Data-focused prompts**: Emphasize data visualization aspects
3. **Style-focused prompts**: Emphasize colors, typography, and visual design
## Conclusion
By following these guidelines, LLMs can effectively generate UI mockups that are compatible with the UI Mockup Generator. The structured JSON schema provides a clear framework for the LLM to follow, while the flexibility of the schema allows for creative design variations. As LLM capabilities continue to improve, the quality and complexity of generated mockups will increase, making this integration increasingly valuable for rapid prototyping and design exploration.
+204
View File
@@ -0,0 +1,204 @@
# UI Mockup Generator - Software Specifications
## Overview
The UI Mockup Generator is a web application that allows users to create, customize, and export mockup UI elements for various interfaces. Similar to the image provided (showing a Funnel Viewer with analytics), the tool will allow users to quickly create professional-looking UI mockups through a simple interface. The configuration for these mockups will be stored in JSON format, making it compatible with LLM-based generation.
## Core Features
1. **Component Library**
- Funnel diagrams with drop-off percentages
- Charts and graphs (bar, line, pie, area, scatter)
- Data tables and lists
- Form elements (inputs, dropdowns, checkboxes)
- Navigation components (tabs, breadcrumbs, menus)
- Cards and containers
- Modal windows and dialogs
- Alerts and notifications
- User avatars and profiles
2. **Styling Options**
- Color picker with standard palette and custom color support
- Typography settings (font family, size, weight, style)
- Spacing controls (margin, padding)
- Border customization (width, style, radius)
- Shadow and elevation effects
- Background patterns and gradients
- Responsive sizing options
3. **Layout Management**
- Drag-and-drop interface
- Grid layout system
- Alignment and distribution tools
- Layers panel for managing element stacking
- Grouping and ungrouping elements
4. **Data and Interactions**
- Sample data generation for charts and tables
- Custom data input options
- Basic interaction annotations
- User flow connections between components
- State variations (hover, selected, disabled)
5. **Export and Sharing**
- Export as PNG, SVG, or PDF
- Export JSON configuration
- Share via URL
- Project saving and loading
## Technical Architecture
### Frontend
- **Framework**: React.js with TypeScript
- **State Management**: Redux or Context API
- **UI Component Library**: Chakra UI
- **Styling**: Styled Components / Emotion
- **Canvas Rendering**: html-to-image or dom-to-image for exports
- **Charts**: recharts or Chart.js
### Backend
- **Runtime**: Node.js
- **Framework**: Express.js
- **Database**: MongoDB (for saving user projects)
- **Authentication**: JWT-based auth (optional)
- **File Storage**: Local or AWS S3 (for exports)
### Deployment
- **Containerization**: Docker
- **Hosting**: Vercel, Netlify, or AWS
## JSON Schema
The UI components will be configured using a standardized JSON format that includes:
```json
{
"version": "1.0",
"canvas": {
"width": 1200,
"height": 800,
"background": "#f5f5f5"
},
"components": [
{
"id": "unique-id-1",
"type": "funnel",
"position": { "x": 50, "y": 100 },
"size": { "width": 500, "height": 400 },
"data": {
"steps": [
{ "label": "Product Details - Viewed", "value": 100, "dropoff": 36 },
{ "label": "Add to Cart - Clicked", "value": 64, "dropoff": 27 },
{ "label": "Details - Entered", "value": 37, "dropoff": 33 },
{ "label": "Place Order - Clicked", "value": 16, "conversion": true }
],
"colors": {
"primary": "#1a2b47",
"secondary": "#e0756b",
"tertiary": "#d3d3d3",
"success": "#4caf50"
}
},
"style": {
"borderRadius": 5,
"fontFamily": "Arial",
"fontSize": 14
}
}
]
}
```
## LLM Integration Guidelines
To ensure compatibility with LLM-based generation, the following guidelines will be followed:
1. **Component Types**: All components will have standardized types and properties
2. **Naming Conventions**: Consistent property naming across components
3. **Color References**: Support for both hex codes and named colors
4. **Documentation**: Comprehensive schema documentation
5. **Validation**: JSON schema validation to ensure correctness
## Development Roadmap
### Phase 1: Core Framework (Weeks 1-2)
- Set up project structure and build pipeline
- Implement basic canvas and component rendering
- Create essential UI components (rectangles, text, basic charts)
- Implement JSON export/import functionality
### Phase 2: Component Library (Weeks 3-4)
- Develop funnel diagram component
- Add various chart types
- Create form elements and UI controls
- Implement styling options and property panel
### Phase 3: Advanced Features (Weeks 5-6)
- Build layer management and z-index control
- Add drag-and-drop functionality
- Implement export options
- Create preset templates and example mockups
### Phase 4: Refinement and Documentation (Weeks 7-8)
- Optimize performance
- Improve user experience
- Write comprehensive documentation
- Create example mockups and templates
## Detailed Component Specifications
### Funnel Component
```json
{
"type": "funnel",
"data": {
"steps": [
{
"label": "Step Label",
"value": 100,
"dropoff": 25,
"color": "#1a2b47"
}
],
"showPercentages": true,
"showLabels": true,
"orientation": "vertical" // or "horizontal"
},
"style": {
// common style properties
}
}
```
### Chart Component
```json
{
"type": "chart",
"chartType": "bar", // "line", "pie", "area", "scatter"
"data": {
"labels": ["Label 1", "Label 2", "Label 3"],
"datasets": [
{
"label": "Dataset 1",
"data": [10, 20, 30],
"backgroundColor": "#4287f5"
}
]
},
"options": {
"showLegend": true,
"showGrid": true,
"showTooltip": true
},
"style": {
// common style properties
}
}
```
Additional components will follow similar structured patterns.
## Conclusion
The UI Mockup Generator will provide a powerful yet intuitive platform for creating high-quality UI mockups with standardized JSON configuration. This will enable both manual creation through the web interface and automated generation through LLM integration, meeting the needs of designers, developers, and AI enthusiasts alike.