89 | {ZONE_COLORS.map((color) => (
90 |
field.onChange(color.value)}
98 | title={color.name}
99 | />
100 | ))}
101 |
102 |
103 |
104 | )}
105 | />
106 |
107 | Create Zone
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
--------------------------------------------------------------------------------
/src/components/icons/DjangoIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export function DjangoIcon(props: ComponentProps<"svg">) {
4 | return (
5 |
13 |
14 |
18 |
22 |
26 |
30 |
34 |
38 |
42 |
43 |
44 | );
45 | }
--------------------------------------------------------------------------------
/src/lib/codegen/laravel/laravel-helpers.ts:
--------------------------------------------------------------------------------
1 | // Laravel-specific helper functions for migration generation
2 |
3 | export function toLaravelTableName(name: string): string {
4 | return name
5 | .toLowerCase()
6 | .replace(/[^a-z0-9_]/g, "_")
7 | .replace(/_+/g, "_")
8 | .replace(/^_|_$/g, "");
9 | }
10 |
11 | export function toLaravelClassName(tableName: string): string {
12 | return `Create${toPascalCase(tableName)}Table`;
13 | }
14 |
15 | export function toPascalCase(str: string): string {
16 | return str
17 | .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
18 | .replace(/^(.)/, (_, c) => c.toUpperCase());
19 | }
20 |
21 | export function toSnakeCase(str: string): string {
22 | return str
23 | .replace(/([A-Z])/g, "_$1")
24 | .toLowerCase()
25 | .replace(/^_/, "");
26 | }
27 |
28 | export function generateTimestamp(): string {
29 | const now = new Date();
30 | const year = now.getFullYear();
31 | const month = String(now.getMonth() + 1).padStart(2, "0");
32 | const day = String(now.getDate()).padStart(2, "0");
33 | const hour = String(now.getHours()).padStart(2, "0");
34 | const minute = String(now.getMinutes()).padStart(2, "0");
35 | const second = String(now.getSeconds()).padStart(2, "0");
36 |
37 | return `${year}_${month}_${day}_${hour}${minute}${second}`;
38 | }
39 |
40 | export function escapeString(str: string): string {
41 | return str.replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
42 | }
43 |
44 | export function isLaravelTimestampColumn(columnName: string): boolean {
45 | const name = columnName.toLowerCase();
46 | return ["created_at", "updated_at", "deleted_at"].includes(name);
47 | }
48 |
49 | export function isLaravelIdColumn(columnName: string): boolean {
50 | return (
51 | columnName.toLowerCase().endsWith("_id") ||
52 | columnName.toLowerCase() === "id"
53 | );
54 | }
55 |
56 | export function getLaravelColumnType(type: string, columnName: string): string {
57 | const upperType = type.toUpperCase();
58 | const lowerName = columnName.toLowerCase();
59 |
60 | // Handle Laravel conventions
61 | if (
62 | lowerName.endsWith("_id") &&
63 | ["INT", "INTEGER", "BIGINT"].includes(upperType)
64 | ) {
65 | return "unsignedBigInteger";
66 | }
67 |
68 | if (lowerName === "email") {
69 | return "string";
70 | }
71 |
72 | if (lowerName === "password") {
73 | return "string";
74 | }
75 |
76 | if (lowerName.includes("phone")) {
77 | return "string";
78 | }
79 |
80 | // Standard type mappings
81 | switch (upperType) {
82 | case "VARCHAR":
83 | return "string";
84 | case "CHAR":
85 | return "char";
86 | case "TEXT":
87 | return "text";
88 | case "LONGTEXT":
89 | return "longText";
90 | case "MEDIUMTEXT":
91 | return "mediumText";
92 | case "TINYTEXT":
93 | return "text";
94 | case "INT":
95 | case "INTEGER":
96 | return "integer";
97 | case "BIGINT":
98 | return "bigInteger";
99 | case "SMALLINT":
100 | return "smallInteger";
101 | case "TINYINT":
102 | return "boolean";
103 | case "DECIMAL":
104 | case "NUMERIC":
105 | return "decimal";
106 | case "FLOAT":
107 | return "float";
108 | case "DOUBLE":
109 | return "double";
110 | case "BOOLEAN":
111 | case "BOOL":
112 | return "boolean";
113 | case "DATE":
114 | return "date";
115 | case "DATETIME":
116 | return "dateTime";
117 | case "TIMESTAMP":
118 | return "timestamp";
119 | case "TIME":
120 | return "time";
121 | case "JSON":
122 | return "json";
123 | case "ENUM":
124 | return "enum";
125 | case "SET":
126 | return "set";
127 | case "BINARY":
128 | case "LONGBLOB":
129 | case "BLOB":
130 | return "binary";
131 | case "UUID":
132 | return "uuid";
133 | case "YEAR":
134 | return "year";
135 | default:
136 | return "string";
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/icons/TypeOrmIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export function TypeOrmIcon(props: ComponentProps<"svg">) {
4 | return (
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { type Edge, type Node } from "@xyflow/react";
2 |
3 | export type DatabaseType = "mysql" | "postgres";
4 | export type CombinedNode = AppNode | AppNoteNode | AppZoneNode;
5 | export type ElementType = "table" | "note" | "zone" | "relationship";
6 | export type ProcessedNode = (AppNode | AppNoteNode | AppZoneNode) & {
7 | draggable: boolean;
8 | };
9 | export type ColumnGeneratedType = "VIRTUAL" | "STORED";
10 | export type IndexType = "INDEX" | "UNIQUE" | "FULLTEXT" | "SPATIAL";
11 | export type ProcessedEdge = Omit
& {
12 | type: string;
13 | selectable: boolean;
14 | data: {
15 | relationship: string;
16 | isHighlighted: boolean;
17 | isPositionLocked?: boolean;
18 | };
19 | };
20 |
21 | export interface Diagram {
22 | id?: number;
23 | name: string;
24 | dbType: DatabaseType;
25 | data: {
26 | nodes: AppNode[]; //tables
27 | edges: AppEdge[]; //relations
28 | notes?: AppNoteNode[];
29 | zones?: AppZoneNode[];
30 | viewport: { x: number; y: number; zoom: number };
31 | isLocked?: boolean;
32 | };
33 | createdAt: Date;
34 | updatedAt: Date;
35 | deletedAt?: Date | null;
36 | }
37 |
38 | export interface AppState {
39 | key: string;
40 | value: string | number;
41 | }
42 | export interface Column {
43 | id: string;
44 | name: string;
45 | type: string;
46 | pk?: boolean;
47 | nullable?: boolean;
48 | defaultValue?: string | number | boolean | null | undefined;
49 | isUnique?: boolean;
50 | isAutoIncrement?: boolean;
51 | comment?: string;
52 | enumValues?: string;
53 | length?: number;
54 | precision?: number;
55 | scale?: number;
56 | isUnsigned?: boolean;
57 | charset?: string;
58 | collation?: string;
59 | isGenerated?: boolean;
60 | generatedExpression?: string;
61 | generatedType?: ColumnGeneratedType;
62 | }
63 |
64 | export interface Index {
65 | id: string;
66 | name: string;
67 | columns: string[];
68 | isUnique?: boolean;
69 | type?: IndexType
70 | }
71 |
72 | export interface CheckConstraint {
73 | name: string;
74 | expression: string;
75 | }
76 |
77 | export interface PartitionInfo {
78 | type: string;
79 | expression: string;
80 | partitions?: number;
81 | }
82 |
83 | export interface TableNodeData extends Record {
84 | label: string;
85 | columns: Column[];
86 | indices?: Index[];
87 | comment?: string;
88 | color?: string;
89 | isDeleted?: boolean;
90 | deletedAt?: Date;
91 | order?: number;
92 | isPositionLocked?: boolean;
93 | onDelete?: (ids: string[]) => void;
94 | checkConstraints?: CheckConstraint[];
95 | partitionInfo?: PartitionInfo;
96 | }
97 |
98 | export interface NoteNodeData extends Record {
99 | text: string;
100 | color?: string;
101 | onUpdate?: (id: string, data: Partial) => void;
102 | onDelete?: (ids: string[]) => void;
103 | isPositionLocked?: boolean;
104 | }
105 |
106 | export interface ZoneNodeData extends Record {
107 | name: string;
108 | color?: string;
109 | onUpdate?: (id: string, data: Partial) => void;
110 | onDelete?: (ids: string[]) => void;
111 | onCreateTableAtPosition?: (position: { x: number; y: number }) => void;
112 | onCreateNoteAtPosition?: (position: { x: number; y: number }) => void;
113 | isLocked?: boolean;
114 | }
115 |
116 | export interface EdgeData extends Record {
117 | relationship: string;
118 | isHighlighted?: boolean;
119 | isPositionLocked?: boolean;
120 | constraintName?: string;
121 | sourceColumns?: string[];
122 | targetColumns?: string[];
123 | onDelete?: string;
124 | onUpdate?: string;
125 | isComposite?: boolean;
126 | }
127 |
128 | export interface Settings {
129 | rememberLastPosition: boolean;
130 | snapToGrid: boolean;
131 | focusTableDuringSelection: boolean;
132 | focusRelDuringSelection: boolean;
133 | allowTableOverlapDuringCreation: boolean;
134 | enableFreePanning: boolean;
135 | exportForeignKeyConstraint: boolean;
136 | }
137 |
138 | export type AppNode = Node;
139 | export type AppNoteNode = Node;
140 | export type AppZoneNode = Node;
141 | export type AppEdge = Edge;
142 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ThothBlueprint
2 |
3 | Thank you for considering contributing to ThothBlueprint! This document provides guidelines and information for contributors.
4 |
5 | ## Code of Conduct
6 |
7 | This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
8 |
9 | ## How Can I Contribute?
10 |
11 | ### Reporting Bugs
12 |
13 | Before creating bug reports, please check the existing issues to avoid duplicates. When creating a bug report, include:
14 |
15 | - **Clear title and description**
16 | - **Steps to reproduce** the issue
17 | - **Expected vs actual behavior**
18 | - **Environment details** (browser, operating system)
19 | - **Screenshots** if applicable
20 |
21 | ### Suggesting Enhancements
22 |
23 | Enhancement suggestions are welcome! Please provide:
24 |
25 | - **Clear title and detailed description**
26 | - **Use case** explaining why this enhancement would be useful
27 | - **Possible implementation** details if you have ideas
28 |
29 | ### Pull Requests
30 |
31 | 1. **Fork** the repository
32 | 2. **Create a feature branch** from `main`
33 | 3. **Make your changes** following our coding standards
34 | 4. **Add tests** for new functionality
35 | 5. **Ensure all tests pass**
36 | 6. **Update documentation** if needed
37 | 7. **Submit a pull request**
38 |
39 | ## Development Setup
40 |
41 | ```bash
42 | # Clone your fork
43 | git clone https://github.com/AHS12/thoth-blueprint.git
44 |
45 | cd thothblueprint
46 |
47 | # Install dependencies
48 | pnpm install
49 |
50 | # Start development server
51 | pnpm dev
52 |
53 | # Run linter
54 | pnpm lint
55 |
56 | # Run type checking
57 | pnpm type-check
58 | ```
59 |
60 | ## Coding Standards
61 |
62 | - Follow **TypeScript and React best practices**
63 | - Use **meaningful variable and function names**
64 | - Add **type declarations** where possible
65 | - Write **comprehensive tests** for new features
66 | - Keep **backward compatibility** in mind
67 |
68 | ### Code Style
69 |
70 | We use ESLint and Prettier for code formatting:
71 |
72 | ```bash
73 | # Format code
74 | pnpm lint:fix
75 | ```
76 |
77 | ## Testing
78 |
79 | All contributions must include appropriate tests:
80 |
81 | ```bash
82 | # Run all tests
83 | pnpm test
84 |
85 | # Run type checking
86 | pnpm type-check
87 | ```
88 |
89 | ### Writing Tests
90 |
91 | - Use Jest and React Testing Library
92 | - Test both **happy path and edge cases**
93 | - Mock external dependencies when appropriate
94 |
95 | ## Project Structure
96 |
97 | This project follows a standard React/Vite project structure:
98 |
99 | - **`src/`** - Main source code
100 | - **`components/`** - React components
101 | - **`lib/`** - Utility functions and business logic
102 | - **`store/`** - Zustand store implementation
103 | - **`hooks/`** - Custom React hooks
104 | - **`pages/`** - Page components
105 | - **`public/`** - Static assets
106 | - **`docs/`** - Documentation files
107 |
108 | ## Commit Messages
109 |
110 | Use clear, descriptive commit messages:
111 |
112 | ```
113 | feat: add support for new export format
114 | fix: resolve relationship rendering issue
115 | docs: update contributing guidelines
116 | test: add tests for migration generator
117 | refactor: improve diagram rendering performance
118 | ```
119 |
120 | Prefix types:
121 | - `feat:` New features
122 | - `fix:` Bug fixes
123 | - `docs:` Documentation changes
124 | - `test:` Test additions/changes
125 | - `refactor:` Code refactoring
126 | - `style:` Code style changes
127 | - `chore:` Maintenance tasks
128 |
129 | ## Review Process
130 |
131 | 1. **Automated checks** must pass (tests, code style)
132 | 2. **Manual review** by maintainers
133 | 3. **Discussion** if changes are needed
134 | 4. **Approval** and merge
135 |
136 | ## Getting Help
137 |
138 | - **GitHub Issues**: For bugs and feature requests
139 | - **GitHub Discussions**: For questions and general discussion
140 | - **Email**: Contact maintainers directly for sensitive issues
141 |
142 | ## Recognition
143 |
144 | Contributors will be acknowledged in:
145 | - **README.md** contributors section
146 | - **GitHub contributors** page
147 |
148 | Thank you for helping make ThothBlueprint better! 🚀
--------------------------------------------------------------------------------
/src/components/icons/Django.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
13 |
16 |
20 |
24 |
28 |
32 |
36 |
40 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "thothblueprint",
3 | "description": "Free visual database design tool with drag-and-move editing, multi-format export, and migration generation for popular frameworks.",
4 | "private": true,
5 | "version": "0.0.7",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build",
10 | "build:dev": "vite build --mode development",
11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 5",
12 | "lint:fix": "eslint . --ext ts,tsx --fix",
13 | "preview": "vite preview",
14 | "type-check": "tsc --noEmit",
15 | "type-check:strict": "tsc --noEmit --strict",
16 | "prebuild": "pnpm run type-check && pnpm run lint",
17 | "check-unused": "pnpm dlx unimported",
18 | "analyze": "pnpm dlx depcheck",
19 | "prepare": "husky"
20 | },
21 | "dependencies": {
22 | "@dbml/core": "^3.13.9",
23 | "@dnd-kit/core": "^6.3.1",
24 | "@dnd-kit/sortable": "^10.0.0",
25 | "@dnd-kit/utilities": "^3.2.2",
26 | "@hookform/resolvers": "^3.9.0",
27 | "@radix-ui/react-accordion": "^1.2.0",
28 | "@radix-ui/react-alert-dialog": "^1.1.1",
29 | "@radix-ui/react-aspect-ratio": "^1.1.0",
30 | "@radix-ui/react-avatar": "^1.1.0",
31 | "@radix-ui/react-checkbox": "^1.1.1",
32 | "@radix-ui/react-collapsible": "^1.1.0",
33 | "@radix-ui/react-context-menu": "^2.2.1",
34 | "@radix-ui/react-dialog": "^1.1.2",
35 | "@radix-ui/react-dropdown-menu": "^2.1.1",
36 | "@radix-ui/react-hover-card": "^1.1.1",
37 | "@radix-ui/react-label": "^2.1.0",
38 | "@radix-ui/react-menubar": "^1.1.1",
39 | "@radix-ui/react-navigation-menu": "^1.2.0",
40 | "@radix-ui/react-popover": "^1.1.1",
41 | "@radix-ui/react-progress": "^1.1.0",
42 | "@radix-ui/react-radio-group": "^1.2.0",
43 | "@radix-ui/react-scroll-area": "^1.1.0",
44 | "@radix-ui/react-select": "^2.1.1",
45 | "@radix-ui/react-separator": "^1.1.0",
46 | "@radix-ui/react-slider": "^1.2.0",
47 | "@radix-ui/react-slot": "^1.1.0",
48 | "@radix-ui/react-switch": "^1.1.0",
49 | "@radix-ui/react-tabs": "^1.1.0",
50 | "@radix-ui/react-toast": "^1.2.1",
51 | "@radix-ui/react-toggle": "^1.1.0",
52 | "@radix-ui/react-toggle-group": "^1.1.0",
53 | "@radix-ui/react-tooltip": "^1.1.4",
54 | "@tanstack/react-query": "^5.56.2",
55 | "@types/file-saver": "^2.0.7",
56 | "@types/lodash": "^4.17.20",
57 | "@vercel/analytics": "^1.5.0",
58 | "@xyflow/react": "^12.8.4",
59 | "class-variance-authority": "^0.7.1",
60 | "clsx": "^2.1.1",
61 | "cmdk": "^1.0.0",
62 | "date-fns": "^3.6.0",
63 | "dexie": "^4.2.0",
64 | "dexie-react-hooks": "^4.2.0",
65 | "embla-carousel-react": "^8.3.0",
66 | "file-saver": "^2.0.5",
67 | "html-to-image": "^1.11.13",
68 | "input-otp": "^1.2.4",
69 | "jszip": "^3.10.1",
70 | "lodash": "^4.17.21",
71 | "lucide-react": "^0.462.0",
72 | "next-themes": "^0.3.0",
73 | "react": "^18.3.1",
74 | "react-day-picker": "^8.10.1",
75 | "react-dom": "^18.3.1",
76 | "react-dropzone": "^14.3.8",
77 | "react-hook-form": "^7.53.0",
78 | "react-icons": "^5.5.0",
79 | "react-markdown": "^9.0.1",
80 | "remark-gfm": "^4.0.0",
81 | "react-pick-color": "^2.0.0",
82 | "react-resizable-panels": "^2.1.3",
83 | "react-router-dom": "^6.26.2",
84 | "recharts": "^2.12.7",
85 | "sonner": "^1.5.0",
86 | "tailwind-merge": "^2.5.2",
87 | "tailwindcss-animate": "^1.0.7",
88 | "use-debounce": "^10.0.6",
89 | "vaul": "^0.9.3",
90 | "vite-plugin-pwa": "^1.0.3",
91 | "workbox-window": "^7.3.0",
92 | "zod": "^3.23.8",
93 | "zustand": "^5.0.8"
94 | },
95 | "devDependencies": {
96 | "@dyad-sh/react-vite-component-tagger": "^0.8.0",
97 | "@eslint/js": "^9.9.0",
98 | "@tailwindcss/typography": "^0.5.15",
99 | "@types/node": "^22.5.5",
100 | "@types/react": "^18.3.3",
101 | "@types/react-dom": "^18.3.0",
102 | "@vitejs/plugin-react-swc": "^3.9.0",
103 | "autoprefixer": "^10.4.20",
104 | "eslint": "^9.9.0",
105 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
106 | "eslint-plugin-react-refresh": "^0.4.9",
107 | "globals": "^15.9.0",
108 | "husky": "^9.1.7",
109 | "postcss": "^8.4.47",
110 | "tailwindcss": "^3.4.11",
111 | "typescript": "^5.5.3",
112 | "typescript-eslint": "^8.0.1",
113 | "vite": "^6.3.4"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------