First push
This commit is contained in:
1 parent
db25d6dbda
commit
7299578599
48 files changed
+17513
-1
No files matched your search
+33
@@ -130,3 +130,36 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Generated
+12832
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { Express, Request } from 'express';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userId?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user