├── .gitignore ├── LICENSE ├── README-features.md ├── README.md ├── biome.json ├── components.json ├── index.html ├── javascript:test └── schemaInference.test.js ├── metaschema.schema.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.svg ├── placeholder.svg └── robots.txt ├── rsbuild.config.ts ├── scripts └── build-test.js ├── src ├── App.css ├── App.tsx ├── components │ ├── SchemaEditor │ │ ├── AddFieldButton.tsx │ │ ├── JsonSchemaEditor.tsx │ │ ├── JsonSchemaVisualizer.tsx │ │ ├── SchemaField.tsx │ │ ├── SchemaFieldList.tsx │ │ ├── SchemaPropertyEditor.tsx │ │ ├── SchemaTypeSelector.tsx │ │ ├── SchemaVisualEditor.tsx │ │ ├── TypeDropdown.tsx │ │ ├── TypeEditor.tsx │ │ └── types │ │ │ ├── ArrayEditor.tsx │ │ │ ├── BooleanEditor.tsx │ │ │ ├── NumberEditor.tsx │ │ │ ├── ObjectEditor.tsx │ │ │ └── StringEditor.tsx │ ├── features │ │ ├── JsonValidator.tsx │ │ └── SchemaInferencer.tsx │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── env.d.ts ├── hooks │ ├── use-mobile.tsx │ ├── use-monaco-theme.tsx │ └── use-toast.ts ├── index.css ├── lib │ ├── schema-inference.ts │ ├── schemaEditor.ts │ └── utils.ts ├── main.tsx ├── pages │ ├── Index.tsx │ └── NotFound.tsx ├── types │ └── jsonSchema.ts └── utils │ ├── jsonValidator.ts │ └── schemaExample.ts ├── tailwind.config.js ├── tailwind.config.ts ├── temp_rsbuild.config.ts ├── test ├── jsonSchema.test.js ├── jsonValidator.test.js └── schemaInference.test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | dist-tests 15 | dist-test 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ophir LOJKINE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-features.md: -------------------------------------------------------------------------------- 1 | # JSON Schema Features 2 | 3 | This project includes two key features for working with JSON Schemas: 4 | 5 | 1. **JSON Schema Inference**: Automatically generates a JSON Schema from a JSON document 6 | 2. **JSON Validation**: Validates JSON documents against the current schema 7 | 8 | ## Implementation Details 9 | 10 | ### JSON Schema Inference 11 | 12 | The schema inference feature is implemented using a custom algorithm that analyzes JSON data and generates an appropriate schema. It's designed to: 13 | 14 | - Detect data types (string, number, boolean, object, array) 15 | - Identify common string formats (date, date-time, email, uuid, uri) 16 | - Handle nested objects and arrays 17 | - Support mixed-type arrays using `oneOf` 18 | - Mark all non-null properties as required 19 | 20 | The implementation is based on approaches used in libraries like: 21 | - [json-schema-generator](https://github.com/krg7880/json-schema-generator) 22 | - [GenSON](https://github.com/wolverdude/GenSON) 23 | 24 | ### JSON Validation 25 | 26 | The validation feature uses [Ajv](https://ajv.js.org/) (Another JSON Schema Validator), which is one of the fastest and most complete JSON Schema validators. 27 | 28 | Key features: 29 | - Complete support for JSON Schema draft-07 30 | - Format validation with ajv-formats 31 | - Detailed error reporting 32 | - High performance validation 33 | 34 | ## UI Features 35 | 36 | Both components leverage the Monaco Editor (the same editor used in Visual Studio Code) for an enhanced user experience: 37 | 38 | - Syntax highlighting for JSON 39 | - JSON formatting 40 | - Validation-as-you-type 41 | - Bracket matching and auto-closing 42 | - Line numbers and folding 43 | 44 | The JSON Validator component also displays the current schema alongside the input JSON document, making it easier to understand the validation requirements. 45 | 46 | ## Project Structure 47 | 48 | The features are organized as follows: 49 | 50 | ``` 51 | src/ 52 | components/ 53 | features/ 54 | SchemaInferencer.tsx # Component for inferring schemas from JSON 55 | JsonValidator.tsx # Component for validating JSON against schema 56 | lib/ 57 | schema-inference.ts # Core schema inference logic as a service 58 | pages/ 59 | Index.tsx # Main page with both features integrated 60 | ``` 61 | 62 | ## Dependencies 63 | 64 | - `monaco-editor`: Advanced code editor used in VS Code 65 | - `ajv`: JSON Schema validator 66 | - `ajv-formats`: Format validation for ajv 67 | 68 | ## Usage 69 | 70 | 1. **Schema Inference**: 71 | - Click "Infer from JSON" button 72 | - Paste or type a JSON document in the editor 73 | - Click "Generate Schema" 74 | 75 | 2. **JSON Validation**: 76 | - Click "Validate JSON" button 77 | - Paste or type a JSON document in the left editor 78 | - Review the current schema in the right editor 79 | - Click "Validate" to check against the current schema 80 | 81 | ## References 82 | 83 | - [JSON Schema website](https://json-schema.org/) 84 | - [Ajv Documentation](https://ajv.js.org/) 85 | - [Monaco Editor](https://microsoft.github.io/monaco-editor/) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema Builder 2 | 3 | [![image](https://github.com/user-attachments/assets/6be1cecf-e0d9-4597-ab04-7124e37e332d)](https://json.ophir.dev) 4 | 5 | A modern, React-based visual JSON Schema editor for creating and manipulating JSON Schema definitions with an intuitive interface. 6 | 7 | **Try online**: https://json.ophir.dev 8 | 9 | ## Features 10 | 11 | - **Visual Schema Editor**: Design your JSON Schema through an intuitive interface without writing raw JSON 12 | - **Real-time JSON Preview**: See your schema in JSON format as you build it visually 13 | - **Schema Inference**: Generate schemas automatically from existing JSON data 14 | - **JSON Validation**: Test JSON data against your schema with detailed validation feedback 15 | - **Responsive Design**: Fully responsive interface that works on desktop and mobile devices 16 | 17 | ## Getting Started 18 | 19 | ### Installation 20 | 21 | ```bash 22 | git clone https://github.com/lovasoa/jsonjoy-builder.git 23 | cd jsonjoy-builder 24 | npm install 25 | ``` 26 | 27 | ### Development 28 | 29 | Start the development server: 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 35 | The application will be available at http://localhost:5173 36 | 37 | ### Building for Production 38 | 39 | Build the application for production: 40 | 41 | ```bash 42 | npm run build 43 | ``` 44 | 45 | The built files will be available in the `dist` directory. 46 | 47 | ## Project Architecture 48 | 49 | ### Core Components 50 | 51 | - **JsonSchemaEditor**: The main component that provides tabs for switching between visual and JSON views 52 | - **SchemaVisualEditor**: Handles the visual representation and editing of schemas 53 | - **JsonSchemaVisualizer**: Provides JSON view with Monaco editor for direct schema editing 54 | - **SchemaInferencer**: Dialog component for generating schemas from JSON data 55 | - **JsonValidator**: Dialog component for validating JSON against the current schema 56 | 57 | ### Key Features 58 | 59 | #### Schema Inference 60 | 61 | The application can automatically generate JSON Schema definitions from existing JSON data. This feature uses a recursion-based inference system to detect: 62 | 63 | - Object structures and properties 64 | - Array types and their item schemas 65 | - String formats (dates, emails, URIs) 66 | - Numeric types (integers vs. floats) 67 | - Required fields 68 | 69 | #### JSON Validation 70 | 71 | Validate any JSON document against your schema with: 72 | - Real-time feedback 73 | - Detailed error reporting 74 | - Format validation for emails, dates, and other special formats 75 | 76 | ## Technology Stack 77 | 78 | - **React**: UI framework 79 | - **TypeScript**: Type-safe development 80 | - **Rsbuild**: Build tool and development server 81 | - **ShadCN UI**: Component library 82 | - **Monaco Editor**: Code editor for JSON viewing/editing 83 | - **Ajv**: JSON Schema validation 84 | - **Zod**: Type-safe json parsing in ts 85 | - **Lucide Icons**: Icon library 86 | - **Node.js Test Runner**: Simple built-in testing 87 | 88 | ## Development Scripts 89 | 90 | | Command | Description | 91 | |---------|-------------| 92 | | `npm run dev` | Start development server | 93 | | `npm run build` | Build for production | 94 | | `npm run build:dev` | Build with development settings | 95 | | `npm run lint` | Run linter | 96 | | `npm run format` | Format code | 97 | | `npm run check` | Type check the project | 98 | | `npm run fix` | Fix linting issues | 99 | | `npm run typecheck` | Type check with TypeScript | 100 | | `npm run preview` | Preview production build | 101 | | `npm run test` | Run tests | 102 | 103 | ## License 104 | 105 | This project is licensed under the MIT License - see the LICENSE file for details. 106 | 107 | ## Author 108 | 109 | [@ophir.dev](https://ophir.dev) 110 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 2, 8 | "lineEnding": "lf", 9 | "lineWidth": 80 10 | }, 11 | "javascript": { 12 | "formatter": { 13 | "quoteStyle": "double", 14 | "trailingCommas": "all", 15 | "semicolons": "always" 16 | } 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "suspicious": { 26 | "all": true, 27 | "noConsole": "off", 28 | "noReactSpecificProps": "off" 29 | } 30 | } 31 | }, 32 | "files": { 33 | "ignore": ["node_modules", "dist", "build"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | JSONJoy Builder - Visual JSON Schema Editor 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonjoy-builder", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lovasoa/jsonjoy-builder" 9 | }, 10 | "scripts": { 11 | "dev": "rsbuild dev", 12 | "build": "rsbuild build", 13 | "build:dev": "rsbuild build --mode development", 14 | "lint": "biome lint .", 15 | "format": "biome format . --write", 16 | "check": "biome check .", 17 | "fix": "biome check --fix --unsafe", 18 | "typecheck": "tsc", 19 | "preview": "rsbuild preview", 20 | "build:test": "node scripts/build-test.js", 21 | "pretest": "npm run build:test", 22 | "test": "node --test test/*.test.js" 23 | }, 24 | "dependencies": { 25 | "@monaco-editor/react": "^4.6.0", 26 | "@radix-ui/react-checkbox": "^1.1.1", 27 | "@radix-ui/react-dialog": "^1.1.2", 28 | "@radix-ui/react-label": "^2.1.0", 29 | "@radix-ui/react-popover": "^1.1.1", 30 | "@radix-ui/react-select": "^2.1.1", 31 | "@radix-ui/react-separator": "^1.1.0", 32 | "@radix-ui/react-slot": "^1.1.0", 33 | "@radix-ui/react-switch": "^1.1.0", 34 | "@radix-ui/react-tabs": "^1.1.0", 35 | "@radix-ui/react-toast": "^1.2.1", 36 | "@radix-ui/react-tooltip": "^1.1.4", 37 | "@tanstack/react-query": "^5.56.2", 38 | "ajv": "^8.17.1", 39 | "ajv-formats": "^3.0.1", 40 | "class-variance-authority": "^0.7.1", 41 | "clsx": "^2.1.1", 42 | "lucide-react": "^0.462.0", 43 | "next-themes": "^0.3.0", 44 | "react": "^18.3.1", 45 | "react-dom": "^18.3.1", 46 | "react-router-dom": "^6.26.2", 47 | "sonner": "^1.5.0", 48 | "tailwind-merge": "^2.5.2", 49 | "tailwindcss-animate": "^1.0.7", 50 | "zod": "^3.23.8" 51 | }, 52 | "devDependencies": { 53 | "@biomejs/biome": "^1.9.4", 54 | "@rsbuild/core": "^1.3.0", 55 | "@rsbuild/plugin-react": "^1.1.1", 56 | "@types/node": "^22.5.5", 57 | "@types/react": "^18.3.3", 58 | "@types/react-dom": "^18.3.0", 59 | "autoprefixer": "^10.4.20", 60 | "esbuild": "^0.25.1", 61 | "postcss": "^8.4.47", 62 | "tailwindcss": "^3.4.11", 63 | "typescript": "^5.5.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovasoa/jsonjoy-builder/d28d7807a425a0f9569937f76a0047449f764e3e/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: Googlebot 2 | Allow: / 3 | 4 | User-agent: Bingbot 5 | Allow: / 6 | 7 | User-agent: Twitterbot 8 | Allow: / 9 | 10 | User-agent: facebookexternalhit 11 | Allow: / 12 | 13 | User-agent: * 14 | Allow: / 15 | -------------------------------------------------------------------------------- /rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { defineConfig, loadEnv } from "@rsbuild/core"; 3 | import { pluginReact } from "@rsbuild/plugin-react"; 4 | 5 | const { publicVars } = loadEnv({ prefixes: ["PUBLIC_", "VITE_"] }); 6 | 7 | export default defineConfig({ 8 | plugins: [pluginReact()], 9 | server: { 10 | host: "::", 11 | port: 8080, 12 | }, 13 | source: { 14 | entry: { 15 | index: "./src/main.tsx", 16 | }, 17 | define: { 18 | ...publicVars, 19 | "import.meta.env.SSR": JSON.stringify(false), 20 | }, 21 | }, 22 | html: { 23 | template: "./index.html", 24 | }, 25 | resolve: { 26 | alias: { 27 | "@": path.resolve(__dirname, "./src"), 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/build-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { dirname, resolve } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import { build } from "esbuild"; 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | const root = resolve(__dirname, ".."); 9 | 10 | async function buildForTest() { 11 | try { 12 | await build({ 13 | entryPoints: [ 14 | "src/types/jsonSchema.ts", 15 | "src/utils/jsonValidator.ts", 16 | "src/utils/schemaExample.ts", 17 | "src/lib/schema-inference.ts", 18 | ], 19 | outdir: "dist-test", 20 | bundle: true, 21 | platform: "neutral", 22 | format: "esm", 23 | sourcemap: true, 24 | target: "node18", 25 | external: ["zod", "ajv", "ajv-formats"], 26 | }); 27 | } catch (e) { 28 | console.error("Build failed:", e); 29 | process.exit(1); 30 | } 31 | } 32 | 33 | buildForTest(); 34 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster as Sonner } from "@/components/ui/sonner"; 2 | import { Toaster } from "@/components/ui/toaster"; 3 | import { TooltipProvider } from "@/components/ui/tooltip"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 6 | import Index from "./pages/Index"; 7 | import NotFound from "./pages/NotFound"; 8 | 9 | const queryClient = new QueryClient(); 10 | 11 | const App = () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | } /> 19 | {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} 20 | } /> 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/AddFieldButton.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | } from "@/components/ui/dialog"; 11 | import { Input } from "@/components/ui/input"; 12 | import { 13 | Tooltip, 14 | TooltipContent, 15 | TooltipProvider, 16 | TooltipTrigger, 17 | } from "@/components/ui/tooltip"; 18 | import type { NewField, SchemaType } from "@/types/jsonSchema"; 19 | import { CirclePlus, HelpCircle, Info } from "lucide-react"; 20 | import type React from "react"; 21 | import { useState } from "react"; 22 | import SchemaTypeSelector from "./SchemaTypeSelector"; 23 | 24 | interface AddFieldButtonProps { 25 | onAddField: (field: NewField) => void; 26 | variant?: "primary" | "secondary"; 27 | } 28 | 29 | const AddFieldButton: React.FC = ({ 30 | onAddField, 31 | variant = "primary", 32 | }) => { 33 | const [dialogOpen, setDialogOpen] = useState(false); 34 | const [fieldName, setFieldName] = useState(""); 35 | const [fieldType, setFieldType] = useState("string"); 36 | const [fieldDesc, setFieldDesc] = useState(""); 37 | const [fieldRequired, setFieldRequired] = useState(false); 38 | 39 | const handleSubmit = (e: React.FormEvent) => { 40 | e.preventDefault(); 41 | if (!fieldName.trim()) return; 42 | 43 | onAddField({ 44 | name: fieldName, 45 | type: fieldType, 46 | description: fieldDesc, 47 | required: fieldRequired, 48 | }); 49 | 50 | setFieldName(""); 51 | setFieldType("string"); 52 | setFieldDesc(""); 53 | setFieldRequired(false); 54 | setDialogOpen(false); 55 | }; 56 | 57 | return ( 58 | <> 59 | 71 | 72 | 73 | 74 | 75 | 76 | Add New Field 77 | 78 | Schema Builder 79 | 80 | 81 | 82 | Create a new field for your JSON schema 83 | 84 | 85 | 86 |
87 |
88 |
89 |
90 |
91 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |

101 | Use camelCase for better readability (e.g., 102 | firstName) 103 |

104 |
105 |
106 |
107 |
108 | setFieldName(e.target.value)} 112 | placeholder="e.g. firstName, age, isActive" 113 | className="font-mono text-sm w-full" 114 | required 115 | /> 116 |
117 | 118 |
119 |
120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |

Add context about what this field represents

130 |
131 |
132 |
133 |
134 | setFieldDesc(e.target.value)} 138 | placeholder="Describe the purpose of this field" 139 | className="text-sm w-full" 140 | /> 141 |
142 | 143 |
144 | setFieldRequired(e.target.checked)} 149 | className="rounded border-gray-300 shrink-0" 150 | /> 151 | 154 |
155 |
156 | 157 |
158 |
159 |
160 | 163 | 164 | 165 | 166 | 167 | 168 | 172 |
173 |
• string: Text
174 |
• number: Numeric
175 |
• boolean: True/false
176 |
• object: Nested JSON
177 |
178 | • array: Lists of values 179 |
180 |
181 |
182 |
183 |
184 |
185 | 190 |
191 | 192 |
193 |

Example:

194 | 195 | {fieldType === "string" && '"example"'} 196 | {fieldType === "number" && "42"} 197 | {fieldType === "boolean" && "true"} 198 | {fieldType === "object" && '{ "key": "value" }'} 199 | {fieldType === "array" && '["item1", "item2"]'} 200 | 201 |
202 |
203 |
204 | 205 | 206 | 214 | 217 | 218 |
219 |
220 |
221 | 222 | ); 223 | }; 224 | 225 | export default AddFieldButton; 226 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/JsonSchemaEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 2 | import { cn } from "@/lib/utils"; 3 | import type { JSONSchema } from "@/types/jsonSchema"; 4 | import { Maximize2 } from "lucide-react"; 5 | import type React from "react"; 6 | import { useRef, useState } from "react"; 7 | import JsonSchemaVisualizer from "./JsonSchemaVisualizer"; 8 | import SchemaVisualEditor from "./SchemaVisualEditor"; 9 | 10 | interface JsonSchemaEditorProps { 11 | schema?: JSONSchema; 12 | setSchema?: (schema: JSONSchema) => void; 13 | className?: string; 14 | } 15 | 16 | const JsonSchemaEditor: React.FC = ({ 17 | schema = { type: "object" }, 18 | setSchema, 19 | className, 20 | }) => { 21 | // Handle schema changes and propagate to parent if needed 22 | const handleSchemaChange = (newSchema: JSONSchema) => { 23 | setSchema(newSchema); 24 | }; 25 | 26 | const [isFullscreen, setIsFullscreen] = useState(false); 27 | const [leftPanelWidth, setLeftPanelWidth] = useState(50); // percentage 28 | const resizeRef = useRef(null); 29 | const containerRef = useRef(null); 30 | const isDraggingRef = useRef(false); 31 | 32 | const toggleFullscreen = () => { 33 | setIsFullscreen(!isFullscreen); 34 | }; 35 | 36 | const fullscreenClass = isFullscreen 37 | ? "fixed inset-0 z-50 bg-background" 38 | : ""; 39 | 40 | const handleMouseDown = (e: React.MouseEvent) => { 41 | e.preventDefault(); 42 | isDraggingRef.current = true; 43 | document.addEventListener("mousemove", handleMouseMove); 44 | document.addEventListener("mouseup", handleMouseUp); 45 | }; 46 | 47 | const handleMouseMove = (e: MouseEvent) => { 48 | if (!isDraggingRef.current || !containerRef.current) return; 49 | 50 | const containerRect = containerRef.current.getBoundingClientRect(); 51 | const newWidth = 52 | ((e.clientX - containerRect.left) / containerRect.width) * 100; 53 | 54 | // Limit the minimum and maximum width 55 | if (newWidth >= 20 && newWidth <= 80) { 56 | setLeftPanelWidth(newWidth); 57 | } 58 | }; 59 | 60 | const handleMouseUp = () => { 61 | isDraggingRef.current = false; 62 | document.removeEventListener("mousemove", handleMouseMove); 63 | document.removeEventListener("mouseup", handleMouseUp); 64 | }; 65 | 66 | return ( 67 |
70 | {/* For mobile screens - show as tabs */} 71 |
72 | 73 |
74 |

JSON Schema Editor

75 |
76 | 84 | 85 | Visual 86 | JSON 87 | 88 |
89 |
90 | 91 | 98 | 99 | 100 | 101 | 108 | 112 | 113 |
114 |
115 | 116 | {/* For large screens - show side by side */} 117 |
124 |
125 |

JSON Schema Editor

126 | 134 |
135 |
136 |
140 | 141 |
142 |
147 |
151 | 155 |
156 |
157 |
158 |
159 | ); 160 | }; 161 | 162 | export default JsonSchemaEditor; 163 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/JsonSchemaVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import { useMonacoTheme } from "@/hooks/use-monaco-theme"; 2 | import { cn } from "@/lib/utils"; 3 | import type { JSONSchema } from "@/types/jsonSchema"; 4 | import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react"; 5 | import { Download, FileJson, Loader2 } from "lucide-react"; 6 | import type React from "react"; 7 | import { useRef } from "react"; 8 | 9 | interface JsonSchemaVisualizerProps { 10 | schema: JSONSchema; 11 | className?: string; 12 | onChange?: (schema: JSONSchema) => void; 13 | } 14 | 15 | const JsonSchemaVisualizer: React.FC = ({ 16 | schema, 17 | className, 18 | onChange, 19 | }) => { 20 | const editorRef = useRef[0] | null>(null); 21 | const { 22 | currentTheme, 23 | defineMonacoThemes, 24 | configureJsonDefaults, 25 | defaultEditorOptions, 26 | } = useMonacoTheme(); 27 | 28 | const handleBeforeMount: BeforeMount = (monaco) => { 29 | defineMonacoThemes(monaco); 30 | configureJsonDefaults(monaco); 31 | }; 32 | 33 | const handleEditorDidMount: OnMount = (editor) => { 34 | editorRef.current = editor; 35 | editor.focus(); 36 | }; 37 | 38 | const handleEditorChange = (value: string | undefined) => { 39 | if (!value) return; 40 | 41 | try { 42 | const parsedJson = JSON.parse(value); 43 | if (onChange) { 44 | onChange(parsedJson); 45 | } 46 | } catch (error) { 47 | // Monaco will show the error inline, no need for additional error handling 48 | } 49 | }; 50 | 51 | const handleDownload = () => { 52 | const content = JSON.stringify(schema, null, 2); 53 | const blob = new Blob([content], { type: "application/json" }); 54 | const url = URL.createObjectURL(blob); 55 | const a = document.createElement("a"); 56 | a.href = url; 57 | a.download = "schema.json"; 58 | document.body.appendChild(a); 59 | a.click(); 60 | document.body.removeChild(a); 61 | URL.revokeObjectURL(url); 62 | }; 63 | 64 | return ( 65 |
68 |
69 |
70 | 71 | JSON Schema Source 72 |
73 | 81 |
82 |
83 | 93 | 94 |
95 | } 96 | options={defaultEditorOptions} 97 | theme={currentTheme} 98 | /> 99 |
100 |
101 | ); 102 | }; 103 | 104 | export default JsonSchemaVisualizer; 105 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/SchemaField.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | JSONSchema as JSONSchemaType, 3 | NewField, 4 | ObjectJSONSchema, 5 | SchemaType, 6 | } from "@/types/jsonSchema"; 7 | import { 8 | asObjectSchema, 9 | getSchemaDescription, 10 | withObjectSchema, 11 | } from "@/types/jsonSchema"; 12 | import React, { Suspense, lazy } from "react"; 13 | import SchemaPropertyEditor from "./SchemaPropertyEditor"; 14 | 15 | // This component is now just a simple wrapper around SchemaPropertyEditor 16 | // to maintain backward compatibility during migration 17 | interface SchemaFieldProps { 18 | name: string; 19 | schema: JSONSchemaType; 20 | required?: boolean; 21 | onDelete: () => void; 22 | onEdit: (updatedField: NewField) => void; 23 | onAddField?: (newField: NewField) => void; 24 | isNested?: boolean; 25 | depth?: number; 26 | } 27 | 28 | const SchemaField: React.FC = (props) => { 29 | const { name, schema, required = false, onDelete, onEdit, depth = 0 } = props; 30 | 31 | // Handle name change 32 | const handleNameChange = (newName: string) => { 33 | if (newName === name) return; 34 | 35 | // Get type in a safe way 36 | const type = withObjectSchema( 37 | schema, 38 | (s) => s.type || "object", 39 | "object", 40 | ) as SchemaType; 41 | 42 | // Get description in a safe way 43 | const description = getSchemaDescription(schema); 44 | 45 | onEdit({ 46 | name: newName, 47 | type: Array.isArray(type) ? type[0] : type, 48 | description, 49 | required, 50 | validation: asObjectSchema(schema), 51 | }); 52 | }; 53 | 54 | // Handle required status change 55 | const handleRequiredChange = (isRequired: boolean) => { 56 | if (isRequired === required) return; 57 | 58 | // Get type in a safe way 59 | const type = withObjectSchema( 60 | schema, 61 | (s) => s.type || "object", 62 | "object", 63 | ) as SchemaType; 64 | 65 | // Get description in a safe way 66 | const description = getSchemaDescription(schema); 67 | 68 | onEdit({ 69 | name, 70 | type: Array.isArray(type) ? type[0] : type, 71 | description, 72 | required: isRequired, 73 | validation: asObjectSchema(schema), 74 | }); 75 | }; 76 | 77 | // Handle description change 78 | const handleDescriptionChange = (newDescription: string) => { 79 | // Get type in a safe way 80 | const type = withObjectSchema( 81 | schema, 82 | (s) => s.type || "object", 83 | "object", 84 | ) as SchemaType; 85 | 86 | onEdit({ 87 | name, 88 | type: Array.isArray(type) ? type[0] : type, 89 | description: newDescription, 90 | required, 91 | validation: asObjectSchema(schema), 92 | }); 93 | }; 94 | 95 | // Handle schema change 96 | const handleSchemaChange = (newSchema: ObjectJSONSchema) => { 97 | // Type will be defined in the schema 98 | const type = newSchema.type || "object"; 99 | 100 | // Description will be defined in the schema 101 | const description = newSchema.description || ""; 102 | 103 | onEdit({ 104 | name, 105 | type: Array.isArray(type) ? type[0] : type, 106 | description, 107 | required, 108 | validation: newSchema, 109 | }); 110 | }; 111 | 112 | return ( 113 | 123 | ); 124 | }; 125 | 126 | export default SchemaField; 127 | 128 | // ExpandButton - extract for reuse 129 | export interface ExpandButtonProps { 130 | expanded: boolean; 131 | onClick: () => void; 132 | } 133 | 134 | export const ExpandButton: React.FC = ({ 135 | expanded, 136 | onClick, 137 | }) => { 138 | const ChevronDown = React.lazy(() => 139 | import("lucide-react").then((mod) => ({ default: mod.ChevronDown })), 140 | ); 141 | const ChevronRight = React.lazy(() => 142 | import("lucide-react").then((mod) => ({ default: mod.ChevronRight })), 143 | ); 144 | 145 | return ( 146 | 156 | ); 157 | }; 158 | 159 | // FieldActions - extract for reuse 160 | export interface FieldActionsProps { 161 | onDelete: () => void; 162 | } 163 | 164 | export const FieldActions: React.FC = ({ onDelete }) => { 165 | const X = React.lazy(() => 166 | import("lucide-react").then((mod) => ({ default: mod.X })), 167 | ); 168 | 169 | return ( 170 |
171 | 181 |
182 | ); 183 | }; 184 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/SchemaFieldList.tsx: -------------------------------------------------------------------------------- 1 | import { getSchemaProperties } from "@/lib/schemaEditor"; 2 | import type { 3 | JSONSchema as JSONSchemaType, 4 | NewField, 5 | ObjectJSONSchema, 6 | SchemaType, 7 | } from "@/types/jsonSchema"; 8 | import type React from "react"; 9 | import SchemaPropertyEditor from "./SchemaPropertyEditor"; 10 | 11 | interface SchemaFieldListProps { 12 | schema: JSONSchemaType; 13 | onAddField: (newField: NewField) => void; 14 | onEditField: (name: string, updatedField: NewField) => void; 15 | onDeleteField: (name: string) => void; 16 | } 17 | 18 | const SchemaFieldList: React.FC = ({ 19 | schema, 20 | onAddField, 21 | onEditField, 22 | onDeleteField, 23 | }) => { 24 | // Get the properties from the schema 25 | const properties = getSchemaProperties(schema); 26 | 27 | // Get schema type as a valid SchemaType 28 | const getValidSchemaType = (propSchema: JSONSchemaType): SchemaType => { 29 | if (typeof propSchema === "boolean") return "object"; 30 | 31 | // Handle array of types by picking the first one 32 | const type = propSchema.type; 33 | if (Array.isArray(type)) { 34 | return type[0] || "object"; 35 | } 36 | 37 | return type || "object"; 38 | }; 39 | 40 | // Handle field name change (generates an edit event) 41 | const handleNameChange = (oldName: string, newName: string) => { 42 | const property = properties.find((prop) => prop.name === oldName); 43 | if (!property) return; 44 | 45 | onEditField(oldName, { 46 | name: newName, 47 | type: getValidSchemaType(property.schema), 48 | description: 49 | typeof property.schema === "boolean" 50 | ? "" 51 | : property.schema.description || "", 52 | required: property.required, 53 | validation: 54 | typeof property.schema === "boolean" 55 | ? { type: "object" } 56 | : property.schema, 57 | }); 58 | }; 59 | 60 | // Handle required status change 61 | const handleRequiredChange = (name: string, required: boolean) => { 62 | const property = properties.find((prop) => prop.name === name); 63 | if (!property) return; 64 | 65 | onEditField(name, { 66 | name, 67 | type: getValidSchemaType(property.schema), 68 | description: 69 | typeof property.schema === "boolean" 70 | ? "" 71 | : property.schema.description || "", 72 | required, 73 | validation: 74 | typeof property.schema === "boolean" 75 | ? { type: "object" } 76 | : property.schema, 77 | }); 78 | }; 79 | 80 | // Handle description change 81 | const handleDescriptionChange = (name: string, description: string) => { 82 | const property = properties.find((prop) => prop.name === name); 83 | if (!property) return; 84 | 85 | onEditField(name, { 86 | name, 87 | type: getValidSchemaType(property.schema), 88 | description, 89 | required: property.required, 90 | validation: 91 | typeof property.schema === "boolean" 92 | ? { type: "object" } 93 | : property.schema, 94 | }); 95 | }; 96 | 97 | // Handle schema change 98 | const handleSchemaChange = ( 99 | name: string, 100 | updatedSchema: ObjectJSONSchema, 101 | ) => { 102 | const property = properties.find((prop) => prop.name === name); 103 | if (!property) return; 104 | 105 | const type = updatedSchema.type || "object"; 106 | // Ensure we're using a single type, not an array of types 107 | const validType = Array.isArray(type) ? type[0] || "object" : type; 108 | 109 | onEditField(name, { 110 | name, 111 | type: validType, 112 | description: updatedSchema.description || "", 113 | required: property.required, 114 | validation: updatedSchema, 115 | }); 116 | }; 117 | 118 | return ( 119 |
120 | {properties.map((property) => ( 121 | onDeleteField(property.name)} 127 | onNameChange={(newName) => handleNameChange(property.name, newName)} 128 | onRequiredChange={(required) => 129 | handleRequiredChange(property.name, required) 130 | } 131 | onSchemaChange={(schema) => handleSchemaChange(property.name, schema)} 132 | /> 133 | ))} 134 |
135 | ); 136 | }; 137 | 138 | export default SchemaFieldList; 139 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/SchemaPropertyEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Input } from "@/components/ui/input"; 3 | import { cn, getTypeColor, getTypeLabel } from "@/lib/utils"; 4 | import type { 5 | JSONSchema, 6 | NewField, 7 | ObjectJSONSchema, 8 | SchemaType, 9 | } from "@/types/jsonSchema"; 10 | import { 11 | asObjectSchema, 12 | getSchemaDescription, 13 | isBooleanSchema, 14 | withObjectSchema, 15 | } from "@/types/jsonSchema"; 16 | import { ChevronDown, ChevronRight, X } from "lucide-react"; 17 | import { useEffect, useRef, useState } from "react"; 18 | import TypeDropdown from "./TypeDropdown"; 19 | import TypeEditor from "./TypeEditor"; 20 | 21 | export interface SchemaPropertyEditorProps { 22 | name: string; 23 | schema: JSONSchema; 24 | required: boolean; 25 | onDelete: () => void; 26 | onNameChange: (newName: string) => void; 27 | onRequiredChange: (required: boolean) => void; 28 | onSchemaChange: (schema: ObjectJSONSchema) => void; 29 | depth?: number; 30 | } 31 | 32 | export const SchemaPropertyEditor: React.FC = ({ 33 | name, 34 | schema, 35 | required, 36 | onDelete, 37 | onNameChange, 38 | onRequiredChange, 39 | onSchemaChange, 40 | depth = 0, 41 | }) => { 42 | const [expanded, setExpanded] = useState(false); 43 | const [isEditingName, setIsEditingName] = useState(false); 44 | const [isEditingDesc, setIsEditingDesc] = useState(false); 45 | const [tempName, setTempName] = useState(name); 46 | const [tempDesc, setTempDesc] = useState(getSchemaDescription(schema)); 47 | const type = withObjectSchema( 48 | schema, 49 | (s) => (s.type || "object") as SchemaType, 50 | "object" as SchemaType, 51 | ); 52 | 53 | // Update temp values when props change 54 | useEffect(() => { 55 | setTempName(name); 56 | setTempDesc(getSchemaDescription(schema)); 57 | }, [name, schema]); 58 | 59 | const handleNameSubmit = () => { 60 | const trimmedName = tempName.trim(); 61 | if (trimmedName && trimmedName !== name) { 62 | onNameChange(trimmedName); 63 | } else { 64 | setTempName(name); 65 | } 66 | setIsEditingName(false); 67 | }; 68 | 69 | const handleDescSubmit = () => { 70 | const trimmedDesc = tempDesc.trim(); 71 | if (trimmedDesc !== getSchemaDescription(schema)) { 72 | onSchemaChange({ 73 | ...asObjectSchema(schema), 74 | description: trimmedDesc || undefined, 75 | }); 76 | } else { 77 | setTempDesc(getSchemaDescription(schema)); 78 | } 79 | setIsEditingDesc(false); 80 | }; 81 | 82 | // Handle schema changes, preserving description 83 | const handleSchemaUpdate = (updatedSchema: ObjectJSONSchema) => { 84 | const description = getSchemaDescription(schema); 85 | onSchemaChange({ 86 | ...updatedSchema, 87 | description: description || undefined, 88 | }); 89 | }; 90 | 91 | return ( 92 |
0 && "ml-0 sm:ml-4 border-l border-l-border/40", 96 | )} 97 | > 98 |
99 |
100 | {/* Expand/collapse button */} 101 | 109 | 110 | {/* Property name */} 111 |
112 |
113 | {isEditingName ? ( 114 | setTempName(e.target.value)} 117 | onBlur={handleNameSubmit} 118 | onKeyDown={(e) => e.key === "Enter" && handleNameSubmit()} 119 | className="h-8 text-sm font-medium min-w-[120px] max-w-full z-10" 120 | autoFocus 121 | onFocus={(e) => e.target.select()} 122 | /> 123 | ) : ( 124 | 132 | )} 133 | 134 | {/* Description */} 135 | {isEditingDesc ? ( 136 | setTempDesc(e.target.value)} 139 | onBlur={handleDescSubmit} 140 | onKeyDown={(e) => e.key === "Enter" && handleDescSubmit()} 141 | placeholder="Add description..." 142 | className="h-8 text-xs text-muted-foreground italic flex-1 min-w-[150px] z-10" 143 | autoFocus 144 | onFocus={(e) => e.target.select()} 145 | /> 146 | ) : tempDesc ? ( 147 | 155 | ) : ( 156 | 164 | )} 165 |
166 | 167 | {/* Type display */} 168 |
169 | { 172 | onSchemaChange({ 173 | ...asObjectSchema(schema), 174 | type: newType, 175 | }); 176 | }} 177 | /> 178 | 179 | {/* Required toggle */} 180 | 192 |
193 |
194 |
195 | 196 | {/* Delete button */} 197 |
198 | 206 |
207 |
208 | 209 | {/* Type-specific editor */} 210 | {expanded && ( 211 |
212 | 217 |
218 | )} 219 |
220 | ); 221 | }; 222 | 223 | export default SchemaPropertyEditor; 224 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/SchemaTypeSelector.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { SchemaType } from "@/types/jsonSchema"; 3 | import type React from "react"; 4 | 5 | interface SchemaTypeSelectorProps { 6 | id?: string; 7 | value: SchemaType; 8 | onChange: (value: SchemaType) => void; 9 | } 10 | 11 | interface TypeOption { 12 | id: SchemaType; 13 | label: string; 14 | description: string; 15 | } 16 | 17 | const typeOptions: { id: SchemaType; label: string; description: string }[] = [ 18 | { 19 | id: "string", 20 | label: "Text", 21 | description: "For text values like names, descriptions, etc.", 22 | }, 23 | { 24 | id: "number", 25 | label: "Number", 26 | description: "For decimal or whole numbers", 27 | }, 28 | { 29 | id: "boolean", 30 | label: "Yes/No", 31 | description: "For true/false values", 32 | }, 33 | { 34 | id: "object", 35 | label: "Group", 36 | description: "For grouping related fields together", 37 | }, 38 | { 39 | id: "array", 40 | label: "List", 41 | description: "For collections of items", 42 | }, 43 | ]; 44 | 45 | const SchemaTypeSelector: React.FC = ({ 46 | id, 47 | value, 48 | onChange, 49 | }) => { 50 | return ( 51 |
55 | {typeOptions.map((type) => ( 56 | 73 | ))} 74 |
75 | ); 76 | }; 77 | 78 | export default SchemaTypeSelector; 79 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/SchemaVisualEditor.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createFieldSchema, 3 | updateObjectProperty, 4 | updatePropertyRequired, 5 | } from "@/lib/schemaEditor"; 6 | import type { JSONSchema, NewField } from "@/types/jsonSchema"; 7 | import { asObjectSchema, isBooleanSchema } from "@/types/jsonSchema"; 8 | import type React from "react"; 9 | import AddFieldButton from "./AddFieldButton"; 10 | import SchemaFieldList from "./SchemaFieldList"; 11 | 12 | interface SchemaVisualEditorProps { 13 | schema: JSONSchema; 14 | onChange: (schema: JSONSchema) => void; 15 | } 16 | 17 | const SchemaVisualEditor: React.FC = ({ 18 | schema, 19 | onChange, 20 | }) => { 21 | // Handle adding a top-level field 22 | const handleAddField = (newField: NewField) => { 23 | // Create a field schema based on the new field data 24 | const fieldSchema = createFieldSchema(newField); 25 | 26 | // Add the field to the schema 27 | let newSchema = updateObjectProperty( 28 | asObjectSchema(schema), 29 | newField.name, 30 | fieldSchema, 31 | ); 32 | 33 | // Update required status if needed 34 | if (newField.required) { 35 | newSchema = updatePropertyRequired(newSchema, newField.name, true); 36 | } 37 | 38 | // Update the schema 39 | onChange(newSchema); 40 | }; 41 | 42 | // Handle editing a top-level field 43 | const handleEditField = (name: string, updatedField: NewField) => { 44 | // Create a field schema based on the updated field data 45 | const fieldSchema = createFieldSchema(updatedField); 46 | 47 | // Update the field in the schema 48 | let newSchema = updateObjectProperty( 49 | asObjectSchema(schema), 50 | updatedField.name, 51 | fieldSchema, 52 | ); 53 | 54 | // Update required status 55 | newSchema = updatePropertyRequired( 56 | newSchema, 57 | updatedField.name, 58 | updatedField.required || false, 59 | ); 60 | 61 | // If name changed, we need to remove the old field 62 | if (name !== updatedField.name) { 63 | const { properties, ...rest } = newSchema; 64 | const { [name]: _, ...remainingProps } = properties || {}; 65 | 66 | newSchema = { 67 | ...rest, 68 | properties: remainingProps, 69 | }; 70 | 71 | // Re-add the field with the new name 72 | newSchema = updateObjectProperty( 73 | newSchema, 74 | updatedField.name, 75 | fieldSchema, 76 | ); 77 | 78 | // Re-update required status if needed 79 | if (updatedField.required) { 80 | newSchema = updatePropertyRequired(newSchema, updatedField.name, true); 81 | } 82 | } 83 | 84 | // Update the schema 85 | onChange(newSchema); 86 | }; 87 | 88 | // Handle deleting a top-level field 89 | const handleDeleteField = (name: string) => { 90 | // Check if the schema is valid first 91 | if (isBooleanSchema(schema) || !schema.properties) { 92 | return; 93 | } 94 | 95 | // Create a new schema without the field 96 | const { [name]: _, ...remainingProps } = schema.properties; 97 | 98 | const newSchema = { 99 | ...schema, 100 | properties: remainingProps, 101 | }; 102 | 103 | // Remove from required array if present 104 | if (newSchema.required) { 105 | newSchema.required = newSchema.required.filter((field) => field !== name); 106 | } 107 | 108 | // Update the schema 109 | onChange(newSchema); 110 | }; 111 | 112 | const hasFields = 113 | !isBooleanSchema(schema) && 114 | schema.properties && 115 | Object.keys(schema.properties).length > 0; 116 | 117 | return ( 118 |
119 |
120 | 121 |
122 | 123 |
124 | {!hasFields ? ( 125 |
126 |

No fields defined yet

127 |

Add your first field to get started

128 |
129 | ) : ( 130 | 136 | )} 137 |
138 |
139 | ); 140 | }; 141 | 142 | export default SchemaVisualEditor; 143 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/TypeDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { cn, getTypeColor, getTypeLabel } from "@/lib/utils"; 2 | import type { SchemaType } from "@/types/jsonSchema"; 3 | import { Check, ChevronDown } from "lucide-react"; 4 | import { useEffect, useRef, useState } from "react"; 5 | 6 | export interface TypeDropdownProps { 7 | value: SchemaType; 8 | onChange: (value: SchemaType) => void; 9 | className?: string; 10 | } 11 | 12 | const typeOptions: SchemaType[] = [ 13 | "string", 14 | "number", 15 | "boolean", 16 | "object", 17 | "array", 18 | "null", 19 | ]; 20 | 21 | export const TypeDropdown: React.FC = ({ 22 | value, 23 | onChange, 24 | className, 25 | }) => { 26 | const [isOpen, setIsOpen] = useState(false); 27 | const dropdownRef = useRef(null); 28 | 29 | // Close dropdown when clicking outside 30 | useEffect(() => { 31 | const handleClickOutside = (event: MouseEvent) => { 32 | if ( 33 | dropdownRef.current && 34 | !dropdownRef.current.contains(event.target as Node) 35 | ) { 36 | setIsOpen(false); 37 | } 38 | }; 39 | 40 | document.addEventListener("mousedown", handleClickOutside); 41 | return () => { 42 | document.removeEventListener("mousedown", handleClickOutside); 43 | }; 44 | }, []); 45 | 46 | return ( 47 |
48 | 61 | 62 | {isOpen && ( 63 |
64 |
65 | {typeOptions.map((type) => ( 66 | 84 | ))} 85 |
86 |
87 | )} 88 |
89 | ); 90 | }; 91 | 92 | export default TypeDropdown; 93 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/TypeEditor.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | JSONSchema, 3 | ObjectJSONSchema, 4 | SchemaType, 5 | } from "@/types/jsonSchema"; 6 | import { withObjectSchema } from "@/types/jsonSchema"; 7 | import { Suspense, lazy } from "react"; 8 | 9 | // Lazy load specific type editors to avoid circular dependencies 10 | const StringEditor = lazy(() => import("./types/StringEditor.tsx")); 11 | const NumberEditor = lazy(() => import("./types/NumberEditor.tsx")); 12 | const BooleanEditor = lazy(() => import("./types/BooleanEditor.tsx")); 13 | const ObjectEditor = lazy(() => import("./types/ObjectEditor.tsx")); 14 | const ArrayEditor = lazy(() => import("./types/ArrayEditor.tsx")); 15 | 16 | export interface TypeEditorProps { 17 | schema: JSONSchema; 18 | onChange: (schema: ObjectJSONSchema) => void; 19 | depth?: number; 20 | } 21 | 22 | const TypeEditor: React.FC = ({ 23 | schema, 24 | onChange, 25 | depth = 0, 26 | }) => { 27 | const type = withObjectSchema( 28 | schema, 29 | (s) => (s.type || "object") as SchemaType, 30 | "string" as SchemaType, 31 | ); 32 | 33 | return ( 34 | Loading editor...}> 35 | {type === "string" && ( 36 | 37 | )} 38 | {type === "number" && ( 39 | 40 | )} 41 | {type === "integer" && ( 42 | 48 | )} 49 | {type === "boolean" && ( 50 | 51 | )} 52 | {type === "object" && ( 53 | 54 | )} 55 | {type === "array" && ( 56 | 57 | )} 58 | 59 | ); 60 | }; 61 | 62 | export default TypeEditor; 63 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/types/ArrayEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input"; 2 | import { Label } from "@/components/ui/label"; 3 | import { Switch } from "@/components/ui/switch"; 4 | import { getArrayItemsSchema } from "@/lib/schemaEditor"; 5 | import { cn, getTypeColor, getTypeLabel } from "@/lib/utils"; 6 | import type { 7 | JSONSchema, 8 | ObjectJSONSchema, 9 | SchemaType, 10 | } from "@/types/jsonSchema"; 11 | import { isBooleanSchema, withObjectSchema } from "@/types/jsonSchema"; 12 | import { useState } from "react"; 13 | import TypeDropdown from "../TypeDropdown"; 14 | import type { TypeEditorProps } from "../TypeEditor"; 15 | import TypeEditor from "../TypeEditor"; 16 | 17 | const ArrayEditor: React.FC = ({ 18 | schema, 19 | onChange, 20 | depth = 0, 21 | }) => { 22 | const [minItems, setMinItems] = useState( 23 | withObjectSchema(schema, (s) => s.minItems, undefined), 24 | ); 25 | const [maxItems, setMaxItems] = useState( 26 | withObjectSchema(schema, (s) => s.maxItems, undefined), 27 | ); 28 | const [uniqueItems, setUniqueItems] = useState( 29 | withObjectSchema(schema, (s) => s.uniqueItems || false, false), 30 | ); 31 | 32 | // Get the array's item schema 33 | const itemsSchema = getArrayItemsSchema(schema) || { type: "string" }; 34 | 35 | // Get the type of the array items 36 | const itemType = withObjectSchema( 37 | itemsSchema, 38 | (s) => (s.type || "string") as SchemaType, 39 | "string" as SchemaType, 40 | ); 41 | 42 | // Handle validation settings change 43 | const handleValidationChange = () => { 44 | const validationProps: ObjectJSONSchema = { 45 | type: "array", 46 | ...(isBooleanSchema(schema) ? {} : schema), 47 | minItems: minItems, 48 | maxItems: maxItems, 49 | uniqueItems: uniqueItems || undefined, 50 | }; 51 | 52 | // Keep the items schema 53 | if (validationProps.items === undefined && itemsSchema) { 54 | validationProps.items = itemsSchema; 55 | } 56 | 57 | // Clean up undefined values 58 | const propsToKeep: Record = {}; 59 | for (const [key, value] of Object.entries(validationProps)) { 60 | if (value !== undefined) { 61 | propsToKeep[key] = value; 62 | } 63 | } 64 | 65 | onChange(propsToKeep as ObjectJSONSchema); 66 | }; 67 | 68 | // Handle item schema changes 69 | const handleItemSchemaChange = (updatedItemSchema: ObjectJSONSchema) => { 70 | const updatedSchema: ObjectJSONSchema = { 71 | type: "array", 72 | ...(isBooleanSchema(schema) ? {} : schema), 73 | items: updatedItemSchema, 74 | }; 75 | 76 | onChange(updatedSchema); 77 | }; 78 | 79 | return ( 80 |
81 | {/* Array validation settings */} 82 |
83 |
84 | 85 | { 91 | const value = e.target.value ? Number(e.target.value) : undefined; 92 | setMinItems(value); 93 | // Don't update immediately to avoid too many rerenders 94 | }} 95 | onBlur={handleValidationChange} 96 | placeholder="No minimum" 97 | className="h-8" 98 | /> 99 |
100 | 101 |
102 | 103 | { 109 | const value = e.target.value ? Number(e.target.value) : undefined; 110 | setMaxItems(value); 111 | // Don't update immediately to avoid too many rerenders 112 | }} 113 | onBlur={handleValidationChange} 114 | placeholder="No maximum" 115 | className="h-8" 116 | /> 117 |
118 |
119 | 120 |
121 | { 125 | setUniqueItems(checked); 126 | setTimeout(handleValidationChange, 0); 127 | }} 128 | /> 129 | 132 |
133 | 134 | {/* Array item type editor */} 135 |
136 |
137 | 138 | { 141 | handleItemSchemaChange({ 142 | ...withObjectSchema(itemsSchema, (s) => s, {}), 143 | type: newType, 144 | }); 145 | }} 146 | /> 147 |
148 | 149 | {/* Item schema editor */} 150 | 155 |
156 |
157 | ); 158 | }; 159 | 160 | export default ArrayEditor; 161 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/types/BooleanEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label"; 2 | import { Switch } from "@/components/ui/switch"; 3 | import type { ObjectJSONSchema } from "@/types/jsonSchema"; 4 | import { isBooleanSchema, withObjectSchema } from "@/types/jsonSchema"; 5 | import { X } from "lucide-react"; 6 | import { useState } from "react"; 7 | import type { TypeEditorProps } from "../TypeEditor"; 8 | 9 | const BooleanEditor: React.FC = ({ schema, onChange }) => { 10 | const [showTrue, setShowTrue] = useState(true); 11 | const [showFalse, setShowFalse] = useState(true); 12 | 13 | // Extract boolean-specific validation 14 | const enumValues = withObjectSchema( 15 | schema, 16 | (s) => s.enum as boolean[] | undefined, 17 | null, 18 | ); 19 | 20 | // Determine if we have enum restrictions 21 | const hasRestrictions = Array.isArray(enumValues); 22 | const allowsTrue = !hasRestrictions || enumValues?.includes(true) || false; 23 | const allowsFalse = !hasRestrictions || enumValues?.includes(false) || false; 24 | 25 | // Handle changing the allowed values 26 | const handleAllowedChange = (value: boolean, allowed: boolean) => { 27 | let newEnum: boolean[] | undefined; 28 | 29 | if (allowed) { 30 | // If allowing this value 31 | if (!hasRestrictions) { 32 | // No current restrictions, nothing to do 33 | return; 34 | } 35 | 36 | if (enumValues?.includes(value)) { 37 | // Already allowed, nothing to do 38 | return; 39 | } 40 | 41 | // Add this value to enum 42 | newEnum = enumValues ? [...enumValues, value] : [value]; 43 | 44 | // If both are now allowed, we can remove the enum constraint 45 | if (newEnum.includes(true) && newEnum.includes(false)) { 46 | newEnum = undefined; 47 | } 48 | } else { 49 | // If disallowing this value 50 | if (hasRestrictions && !enumValues?.includes(value)) { 51 | // Already disallowed, nothing to do 52 | return; 53 | } 54 | 55 | // Create a new enum with just the opposite value 56 | newEnum = [!value]; 57 | } 58 | 59 | // Update the schema 60 | const baseSchema = isBooleanSchema(schema) 61 | ? { type: "boolean" as const } 62 | : { ...schema }; 63 | 64 | // Create a new validation object with just the type and enum 65 | const updatedValidation: ObjectJSONSchema = { 66 | type: "boolean", 67 | }; 68 | 69 | if (newEnum) { 70 | updatedValidation.enum = newEnum; 71 | } else { 72 | // Remove enum property if no restrictions 73 | onChange({ type: "boolean" }); 74 | return; 75 | } 76 | 77 | onChange(updatedValidation); 78 | }; 79 | 80 | return ( 81 |
82 |
83 | 84 | 85 |
86 |
87 | handleAllowedChange(true, checked)} 91 | /> 92 | 95 |
96 | 97 |
98 | handleAllowedChange(false, checked)} 102 | /> 103 | 106 |
107 |
108 | 109 | {!allowsTrue && !allowsFalse && ( 110 |

111 | Warning: You must allow at least one value. 112 |

113 | )} 114 |
115 |
116 | ); 117 | }; 118 | 119 | export default BooleanEditor; 120 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/types/NumberEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input"; 2 | import { Label } from "@/components/ui/label"; 3 | import { Switch } from "@/components/ui/switch"; 4 | import type { JSONSchema, ObjectJSONSchema } from "@/types/jsonSchema"; 5 | import { isBooleanSchema, withObjectSchema } from "@/types/jsonSchema"; 6 | import { X } from "lucide-react"; 7 | import { useState } from "react"; 8 | import type { TypeEditorProps } from "../TypeEditor"; 9 | 10 | interface NumberEditorProps extends TypeEditorProps { 11 | integer?: boolean; 12 | } 13 | 14 | // Helper function to set properties without TypeScript errors 15 | function setSchemaProperty( 16 | schema: ObjectJSONSchema, 17 | key: string, 18 | value: unknown, 19 | ): ObjectJSONSchema { 20 | return { 21 | ...schema, 22 | [key]: value, 23 | }; 24 | } 25 | 26 | const NumberEditor: React.FC = ({ 27 | schema, 28 | onChange, 29 | integer = false, 30 | }) => { 31 | const [enumValue, setEnumValue] = useState(""); 32 | 33 | // Extract number-specific validations 34 | const minimum = withObjectSchema(schema, (s) => s.minimum, undefined); 35 | const maximum = withObjectSchema(schema, (s) => s.maximum, undefined); 36 | const exclusiveMinimum = withObjectSchema( 37 | schema, 38 | (s) => s.exclusiveMinimum, 39 | undefined, 40 | ); 41 | const exclusiveMaximum = withObjectSchema( 42 | schema, 43 | (s) => s.exclusiveMaximum, 44 | undefined, 45 | ); 46 | const multipleOf = withObjectSchema(schema, (s) => s.multipleOf, undefined); 47 | const enumValues = withObjectSchema( 48 | schema, 49 | (s) => (s.enum as number[]) || [], 50 | [], 51 | ); 52 | 53 | // Handle validation change 54 | const handleValidationChange = (property: string, value: unknown) => { 55 | // Create a safe base schema with necessary properties 56 | const baseProperties: Partial = { 57 | type: integer ? "integer" : "number", 58 | }; 59 | 60 | // Copy existing validation properties (except type and description) if schema is an object 61 | if (!isBooleanSchema(schema)) { 62 | if (schema.minimum !== undefined) baseProperties.minimum = schema.minimum; 63 | if (schema.maximum !== undefined) baseProperties.maximum = schema.maximum; 64 | if (schema.exclusiveMinimum !== undefined) 65 | baseProperties.exclusiveMinimum = schema.exclusiveMinimum; 66 | if (schema.exclusiveMaximum !== undefined) 67 | baseProperties.exclusiveMaximum = schema.exclusiveMaximum; 68 | if (schema.multipleOf !== undefined) 69 | baseProperties.multipleOf = schema.multipleOf; 70 | if (schema.enum !== undefined) baseProperties.enum = schema.enum; 71 | } 72 | 73 | // Only add the property if the value is defined, otherwise remove it 74 | if (value !== undefined) { 75 | // Create updated object with modified property 76 | const updatedProperties: Partial = { 77 | ...baseProperties, 78 | }; 79 | 80 | if (property === "minimum") updatedProperties.minimum = value as number; 81 | else if (property === "maximum") 82 | updatedProperties.maximum = value as number; 83 | else if (property === "exclusiveMinimum") 84 | updatedProperties.exclusiveMinimum = value as number; 85 | else if (property === "exclusiveMaximum") 86 | updatedProperties.exclusiveMaximum = value as number; 87 | else if (property === "multipleOf") 88 | updatedProperties.multipleOf = value as number; 89 | else if (property === "enum") updatedProperties.enum = value as unknown[]; 90 | 91 | onChange(updatedProperties as ObjectJSONSchema); 92 | return; 93 | } 94 | 95 | // Handle removing a property (value is undefined) 96 | if (property === "minimum") { 97 | const { minimum: _, ...rest } = baseProperties; 98 | onChange(rest as ObjectJSONSchema); 99 | return; 100 | } 101 | 102 | if (property === "maximum") { 103 | const { maximum: _, ...rest } = baseProperties; 104 | onChange(rest as ObjectJSONSchema); 105 | return; 106 | } 107 | 108 | if (property === "exclusiveMinimum") { 109 | const { exclusiveMinimum: _, ...rest } = baseProperties; 110 | onChange(rest as ObjectJSONSchema); 111 | return; 112 | } 113 | 114 | if (property === "exclusiveMaximum") { 115 | const { exclusiveMaximum: _, ...rest } = baseProperties; 116 | onChange(rest as ObjectJSONSchema); 117 | return; 118 | } 119 | 120 | if (property === "multipleOf") { 121 | const { multipleOf: _, ...rest } = baseProperties; 122 | onChange(rest as ObjectJSONSchema); 123 | return; 124 | } 125 | 126 | if (property === "enum") { 127 | const { enum: _, ...rest } = baseProperties; 128 | onChange(rest as ObjectJSONSchema); 129 | return; 130 | } 131 | 132 | // Fallback case - just use the base properties 133 | onChange(baseProperties as ObjectJSONSchema); 134 | }; 135 | 136 | // Handle adding enum value 137 | const handleAddEnumValue = () => { 138 | if (!enumValue.trim()) return; 139 | 140 | const numValue = Number(enumValue); 141 | if (Number.isNaN(numValue)) return; 142 | 143 | // For integer type, ensure the value is an integer 144 | const validValue = integer ? Math.floor(numValue) : numValue; 145 | 146 | if (!enumValues.includes(validValue)) { 147 | handleValidationChange("enum", [...enumValues, validValue]); 148 | } 149 | 150 | setEnumValue(""); 151 | }; 152 | 153 | // Handle removing enum value 154 | const handleRemoveEnumValue = (index: number) => { 155 | const newEnumValues = [...enumValues]; 156 | newEnumValues.splice(index, 1); 157 | 158 | if (newEnumValues.length === 0) { 159 | // If empty, remove the enum property entirely by setting it to undefined 160 | handleValidationChange("enum", undefined); 161 | } else { 162 | handleValidationChange("enum", newEnumValues); 163 | } 164 | }; 165 | 166 | return ( 167 |
168 |
169 |
170 | 171 | { 176 | const value = e.target.value ? Number(e.target.value) : undefined; 177 | handleValidationChange("minimum", value); 178 | }} 179 | placeholder="No minimum" 180 | className="h-8" 181 | step={integer ? 1 : "any"} 182 | /> 183 |
184 | 185 |
186 | 187 | { 192 | const value = e.target.value ? Number(e.target.value) : undefined; 193 | handleValidationChange("maximum", value); 194 | }} 195 | placeholder="No maximum" 196 | className="h-8" 197 | step={integer ? 1 : "any"} 198 | /> 199 |
200 |
201 | 202 |
203 |
204 | 205 | { 210 | const value = e.target.value ? Number(e.target.value) : undefined; 211 | handleValidationChange("exclusiveMinimum", value); 212 | }} 213 | placeholder="No exclusive min" 214 | className="h-8" 215 | step={integer ? 1 : "any"} 216 | /> 217 |
218 | 219 |
220 | 221 | { 226 | const value = e.target.value ? Number(e.target.value) : undefined; 227 | handleValidationChange("exclusiveMaximum", value); 228 | }} 229 | placeholder="No exclusive max" 230 | className="h-8" 231 | step={integer ? 1 : "any"} 232 | /> 233 |
234 |
235 | 236 |
237 | 238 | { 243 | const value = e.target.value ? Number(e.target.value) : undefined; 244 | handleValidationChange("multipleOf", value); 245 | }} 246 | placeholder="Any" 247 | className="h-8" 248 | min={0} 249 | step={integer ? 1 : "any"} 250 | /> 251 |
252 | 253 |
254 | 255 | 256 |
257 | {enumValues.length > 0 ? ( 258 | enumValues.map((value, index) => ( 259 |
263 | {value} 264 | 271 |
272 | )) 273 | ) : ( 274 |

275 | No restricted values set 276 |

277 | )} 278 |
279 | 280 |
281 | setEnumValue(e.target.value)} 285 | placeholder="Add allowed value..." 286 | className="h-8 text-xs flex-1" 287 | onKeyDown={(e) => e.key === "Enter" && handleAddEnumValue()} 288 | step={integer ? 1 : "any"} 289 | /> 290 | 297 |
298 |
299 |
300 | ); 301 | }; 302 | 303 | export default NumberEditor; 304 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/types/ObjectEditor.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getSchemaProperties, 3 | removeObjectProperty, 4 | updateObjectProperty, 5 | updatePropertyRequired, 6 | } from "@/lib/schemaEditor"; 7 | import type { NewField, ObjectJSONSchema } from "@/types/jsonSchema"; 8 | import { asObjectSchema, isBooleanSchema } from "@/types/jsonSchema"; 9 | import { useState } from "react"; 10 | import AddFieldButton from "../AddFieldButton"; 11 | import SchemaPropertyEditor from "../SchemaPropertyEditor"; 12 | import type { TypeEditorProps } from "../TypeEditor"; 13 | 14 | const ObjectEditor: React.FC = ({ 15 | schema, 16 | onChange, 17 | depth = 0, 18 | }) => { 19 | // Get object properties 20 | const properties = getSchemaProperties(schema); 21 | 22 | // Create a normalized schema object 23 | const normalizedSchema: ObjectJSONSchema = isBooleanSchema(schema) 24 | ? { type: "object", properties: {} } 25 | : { ...schema, type: "object", properties: schema.properties || {} }; 26 | 27 | // Handle adding a new property 28 | const handleAddProperty = (newField: NewField) => { 29 | // Create field schema from the new field data 30 | const fieldSchema = { 31 | type: newField.type, 32 | description: newField.description || undefined, 33 | ...(newField.validation || {}), 34 | } as ObjectJSONSchema; 35 | 36 | // Add the property to the schema 37 | let newSchema = updateObjectProperty( 38 | normalizedSchema, 39 | newField.name, 40 | fieldSchema, 41 | ); 42 | 43 | // Update required status if needed 44 | if (newField.required) { 45 | newSchema = updatePropertyRequired(newSchema, newField.name, true); 46 | } 47 | 48 | // Update the schema 49 | onChange(newSchema); 50 | }; 51 | 52 | // Handle deleting a property 53 | const handleDeleteProperty = (propertyName: string) => { 54 | const newSchema = removeObjectProperty(normalizedSchema, propertyName); 55 | onChange(newSchema); 56 | }; 57 | 58 | // Handle property name change 59 | const handlePropertyNameChange = (oldName: string, newName: string) => { 60 | if (oldName === newName) return; 61 | 62 | const property = properties.find((p) => p.name === oldName); 63 | if (!property) return; 64 | 65 | const propertySchemaObj = asObjectSchema(property.schema); 66 | 67 | // Add property with new name 68 | let newSchema = updateObjectProperty( 69 | normalizedSchema, 70 | newName, 71 | propertySchemaObj, 72 | ); 73 | 74 | if (property.required) { 75 | newSchema = updatePropertyRequired(newSchema, newName, true); 76 | } 77 | 78 | newSchema = removeObjectProperty(newSchema, oldName); 79 | 80 | onChange(newSchema); 81 | }; 82 | 83 | // Handle property required status change 84 | const handlePropertyRequiredChange = ( 85 | propertyName: string, 86 | required: boolean, 87 | ) => { 88 | const newSchema = updatePropertyRequired( 89 | normalizedSchema, 90 | propertyName, 91 | required, 92 | ); 93 | onChange(newSchema); 94 | }; 95 | 96 | const handlePropertySchemaChange = ( 97 | propertyName: string, 98 | propertySchema: ObjectJSONSchema, 99 | ) => { 100 | const newSchema = updateObjectProperty( 101 | normalizedSchema, 102 | propertyName, 103 | propertySchema, 104 | ); 105 | onChange(newSchema); 106 | }; 107 | 108 | return ( 109 |
110 | {properties.length > 0 ? ( 111 |
112 | {properties.map((property) => ( 113 | handleDeleteProperty(property.name)} 119 | onNameChange={(newName) => 120 | handlePropertyNameChange(property.name, newName) 121 | } 122 | onRequiredChange={(required) => 123 | handlePropertyRequiredChange(property.name, required) 124 | } 125 | onSchemaChange={(schema) => 126 | handlePropertySchemaChange(property.name, schema) 127 | } 128 | depth={depth} 129 | /> 130 | ))} 131 |
132 | ) : ( 133 |
134 | No properties defined 135 |
136 | )} 137 | 138 |
139 | 140 |
141 |
142 | ); 143 | }; 144 | 145 | export default ObjectEditor; 146 | -------------------------------------------------------------------------------- /src/components/SchemaEditor/types/StringEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input"; 2 | import { Label } from "@/components/ui/label"; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectItem, 7 | SelectTrigger, 8 | SelectValue, 9 | } from "@/components/ui/select"; 10 | import type { JSONSchema, ObjectJSONSchema } from "@/types/jsonSchema"; 11 | import { 12 | asObjectSchema, 13 | isBooleanSchema, 14 | withObjectSchema, 15 | } from "@/types/jsonSchema"; 16 | import { X } from "lucide-react"; 17 | import { useState } from "react"; 18 | import type { TypeEditorProps } from "../TypeEditor"; 19 | 20 | const StringEditor: React.FC = ({ schema, onChange }) => { 21 | const [enumValue, setEnumValue] = useState(""); 22 | 23 | // Extract string-specific validations 24 | const minLength = withObjectSchema(schema, (s) => s.minLength, undefined); 25 | const maxLength = withObjectSchema(schema, (s) => s.maxLength, undefined); 26 | const pattern = withObjectSchema(schema, (s) => s.pattern, undefined); 27 | const format = withObjectSchema(schema, (s) => s.format, undefined); 28 | const enumValues = withObjectSchema( 29 | schema, 30 | (s) => (s.enum as string[]) || [], 31 | [], 32 | ); 33 | 34 | // Handle validation change 35 | const handleValidationChange = (property: string, value: unknown) => { 36 | // Create a safe base schema 37 | const baseSchema = isBooleanSchema(schema) 38 | ? { type: "string" as const } 39 | : { ...schema }; 40 | 41 | // Extract reusable properties while safely handling potential undefined description 42 | const type = withObjectSchema(schema, (s) => s.type || "string", "string"); 43 | const description = withObjectSchema( 44 | schema, 45 | (s) => s.description, 46 | undefined, 47 | ); 48 | 49 | // Get all validation props except type and description 50 | const { type: _, description: __, ...validationProps } = baseSchema; 51 | 52 | // Create the updated validation schema 53 | const updatedValidation: ObjectJSONSchema = { 54 | ...validationProps, 55 | type: "string", 56 | [property]: value, 57 | }; 58 | 59 | onChange(updatedValidation); 60 | }; 61 | 62 | // Handle adding enum value 63 | const handleAddEnumValue = () => { 64 | if (!enumValue.trim()) return; 65 | 66 | if (!enumValues.includes(enumValue)) { 67 | handleValidationChange("enum", [...enumValues, enumValue]); 68 | } 69 | 70 | setEnumValue(""); 71 | }; 72 | 73 | // Handle removing enum value 74 | const handleRemoveEnumValue = (index: number) => { 75 | const newEnumValues = [...enumValues]; 76 | newEnumValues.splice(index, 1); 77 | 78 | if (newEnumValues.length === 0) { 79 | // If empty, remove the enum property entirely 80 | const baseSchema = isBooleanSchema(schema) 81 | ? { type: "string" as const } 82 | : { ...schema }; 83 | 84 | // Use a type safe approach 85 | if (!isBooleanSchema(baseSchema) && "enum" in baseSchema) { 86 | const { enum: _, ...rest } = baseSchema; 87 | onChange(rest as ObjectJSONSchema); 88 | } else { 89 | onChange(baseSchema as ObjectJSONSchema); 90 | } 91 | } else { 92 | handleValidationChange("enum", newEnumValues); 93 | } 94 | }; 95 | 96 | return ( 97 |
98 |
99 |
100 | 101 | { 107 | const value = e.target.value ? Number(e.target.value) : undefined; 108 | handleValidationChange("minLength", value); 109 | }} 110 | placeholder="No minimum" 111 | className="h-8" 112 | /> 113 |
114 | 115 |
116 | 117 | { 123 | const value = e.target.value ? Number(e.target.value) : undefined; 124 | handleValidationChange("maxLength", value); 125 | }} 126 | placeholder="No maximum" 127 | className="h-8" 128 | /> 129 |
130 |
131 | 132 |
133 | 134 | { 139 | const value = e.target.value || undefined; 140 | handleValidationChange("pattern", value); 141 | }} 142 | placeholder="^[a-zA-Z]+$" 143 | className="h-8" 144 | /> 145 |
146 | 147 |
148 | 149 | 174 |
175 | 176 |
177 | 178 | 179 |
180 | {enumValues.length > 0 ? ( 181 | enumValues.map((value) => ( 182 |
186 | {value} 187 | 196 |
197 | )) 198 | ) : ( 199 |

200 | No restricted values set 201 |

202 | )} 203 |
204 | 205 |
206 | setEnumValue(e.target.value)} 210 | placeholder="Add allowed value..." 211 | className="h-8 text-xs flex-1" 212 | onKeyDown={(e) => e.key === "Enter" && handleAddEnumValue()} 213 | /> 214 | 221 |
222 |
223 |
224 | ); 225 | }; 226 | 227 | export default StringEditor; 228 | -------------------------------------------------------------------------------- /src/components/features/JsonValidator.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog"; 10 | import { useMonacoTheme } from "@/hooks/use-monaco-theme"; 11 | import type { JSONSchema } from "@/types/jsonSchema"; 12 | import { type ValidationResult, validateJson } from "@/utils/jsonValidator"; 13 | import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react"; 14 | import { AlertCircle, Check, Loader2 } from "lucide-react"; 15 | import type * as Monaco from "monaco-editor"; 16 | import { useCallback, useEffect, useRef, useState } from "react"; 17 | 18 | interface JsonValidatorProps { 19 | open: boolean; 20 | onOpenChange: (open: boolean) => void; 21 | schema: JSONSchema; 22 | } 23 | 24 | export function JsonValidator({ 25 | open, 26 | onOpenChange, 27 | schema, 28 | }: JsonValidatorProps) { 29 | const [jsonInput, setJsonInput] = useState(""); 30 | const [validationResult, setValidationResult] = 31 | useState(null); 32 | const editorRef = useRef[0] | null>(null); 33 | const debounceTimerRef = useRef(null); 34 | const monacoRef = useRef(null); 35 | const schemaMonacoRef = useRef(null); 36 | const { 37 | currentTheme, 38 | defineMonacoThemes, 39 | configureJsonDefaults, 40 | defaultEditorOptions, 41 | } = useMonacoTheme(); 42 | 43 | const validateJsonAgainstSchema = useCallback(() => { 44 | if (!jsonInput.trim()) { 45 | setValidationResult(null); 46 | return; 47 | } 48 | 49 | const result = validateJson(jsonInput, schema); 50 | setValidationResult(result); 51 | }, [jsonInput, schema]); 52 | 53 | useEffect(() => { 54 | if (debounceTimerRef.current) { 55 | clearTimeout(debounceTimerRef.current); 56 | } 57 | 58 | debounceTimerRef.current = setTimeout(() => { 59 | validateJsonAgainstSchema(); 60 | }, 500); 61 | 62 | return () => { 63 | if (debounceTimerRef.current) { 64 | clearTimeout(debounceTimerRef.current); 65 | } 66 | }; 67 | }, [validateJsonAgainstSchema]); 68 | 69 | const handleJsonEditorBeforeMount: BeforeMount = (monaco) => { 70 | monacoRef.current = monaco; 71 | defineMonacoThemes(monaco); 72 | configureJsonDefaults(monaco, schema); 73 | }; 74 | 75 | const handleSchemaEditorBeforeMount: BeforeMount = (monaco) => { 76 | schemaMonacoRef.current = monaco; 77 | defineMonacoThemes(monaco); 78 | }; 79 | 80 | const handleEditorDidMount: OnMount = (editor) => { 81 | editorRef.current = editor; 82 | editor.focus(); 83 | }; 84 | 85 | const handleEditorChange = (value: string | undefined) => { 86 | setJsonInput(value || ""); 87 | }; 88 | 89 | const goToError = (line: number, column: number) => { 90 | if (editorRef.current) { 91 | editorRef.current.revealLineInCenter(line); 92 | editorRef.current.setPosition({ lineNumber: line, column: column }); 93 | editorRef.current.focus(); 94 | } 95 | }; 96 | 97 | // Create a modified version of defaultEditorOptions for the editor 98 | const editorOptions = { 99 | ...defaultEditorOptions, 100 | readOnly: false, 101 | }; 102 | 103 | // Create read-only options for the schema viewer 104 | const schemaViewerOptions = { 105 | ...defaultEditorOptions, 106 | readOnly: true, 107 | }; 108 | 109 | return ( 110 | 111 | 112 | 113 | Validate JSON 114 | 115 | Paste your JSON document to validate against the current schema. 116 | Validation occurs automatically as you type. 117 | 118 | 119 |
120 |
121 |
Your JSON:
122 |
123 | 132 | 133 |
134 | } 135 | options={editorOptions} 136 | theme={currentTheme} 137 | /> 138 |
139 |
140 | 141 |
142 |
Current Schema:
143 |
144 | 151 | 152 |
153 | } 154 | options={schemaViewerOptions} 155 | theme={currentTheme} 156 | /> 157 |
158 | 159 | 160 | 161 | {validationResult && ( 162 |
165 |
166 | {validationResult.valid ? ( 167 | <> 168 | 169 |

170 | JSON is valid according to the schema! 171 |

172 | 173 | ) : ( 174 | <> 175 | 176 |

177 | {validationResult.errors.length === 1 178 | ? validationResult.errors[0].path === "/" 179 | ? "Invalid JSON syntax" 180 | : "Schema validation error" 181 | : `${validationResult.errors.length} validation errors detected`} 182 |

183 | 184 | )} 185 |
186 | 187 | {!validationResult.valid && 188 | validationResult.errors && 189 | validationResult.errors.length > 0 && ( 190 |
191 | {validationResult.errors[0] && ( 192 |
193 | 194 | {validationResult.errors[0].path === "/" 195 | ? "Root" 196 | : validationResult.errors[0].path} 197 | 198 | {validationResult.errors[0].line && ( 199 | 200 | Line {validationResult.errors[0].line} 201 | {validationResult.errors[0].column 202 | ? `, Col ${validationResult.errors[0].column}` 203 | : ""} 204 | 205 | )} 206 |
207 | )} 208 |
    209 | {validationResult.errors.map((error, index) => ( 210 | 237 | ))} 238 |
239 |
240 | )} 241 |
242 | )} 243 |
244 |
245 | ); 246 | } 247 | -------------------------------------------------------------------------------- /src/components/features/SchemaInferencer.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog"; 10 | import { useMonacoTheme } from "@/hooks/use-monaco-theme"; 11 | import { createSchemaFromJson } from "@/lib/schema-inference"; 12 | import type { JSONSchema } from "@/types/jsonSchema"; 13 | import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react"; 14 | import { Loader2 } from "lucide-react"; 15 | import { useRef, useState } from "react"; 16 | 17 | interface SchemaInferencerProps { 18 | open: boolean; 19 | onOpenChange: (open: boolean) => void; 20 | onSchemaInferred: (schema: JSONSchema) => void; 21 | } 22 | 23 | export function SchemaInferencer({ 24 | open, 25 | onOpenChange, 26 | onSchemaInferred, 27 | }: SchemaInferencerProps) { 28 | const [jsonInput, setJsonInput] = useState(""); 29 | const [error, setError] = useState(null); 30 | const editorRef = useRef[0] | null>(null); 31 | const { 32 | currentTheme, 33 | defineMonacoThemes, 34 | configureJsonDefaults, 35 | defaultEditorOptions, 36 | } = useMonacoTheme(); 37 | 38 | const handleBeforeMount: BeforeMount = (monaco) => { 39 | defineMonacoThemes(monaco); 40 | configureJsonDefaults(monaco); 41 | }; 42 | 43 | const handleEditorDidMount: OnMount = (editor) => { 44 | editorRef.current = editor; 45 | editor.focus(); 46 | }; 47 | 48 | const handleEditorChange = (value: string | undefined) => { 49 | setJsonInput(value || ""); 50 | }; 51 | 52 | const inferSchemaFromJson = () => { 53 | try { 54 | const jsonObject = JSON.parse(jsonInput); 55 | setError(null); 56 | 57 | // Use the schema inference service to create a schema 58 | const inferredSchema = createSchemaFromJson(jsonObject); 59 | 60 | onSchemaInferred(inferredSchema); 61 | onOpenChange(false); 62 | } catch (error) { 63 | console.error("Invalid JSON input:", error); 64 | setError("Invalid JSON format. Please check your input."); 65 | } 66 | }; 67 | 68 | const handleClose = () => { 69 | setJsonInput(""); 70 | setError(null); 71 | onOpenChange(false); 72 | }; 73 | 74 | return ( 75 | 76 | 77 | 78 | Infer JSON Schema 79 | 80 | Paste your JSON document below to generate a schema from it. 81 | 82 | 83 |
84 |
85 | 96 | 97 |
98 | } 99 | /> 100 |
101 | {error &&

{error}

} 102 | 103 | 104 | 107 | 108 | 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from "class-variance-authority"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 2 | import { Check } from "lucide-react"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 2 | import { X } from "lucide-react"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 45 | {children} 46 | 47 | Dialog content 48 | 49 | 50 | 51 | Close 52 | 53 | 54 | 55 | )); 56 | DialogContent.displayName = DialogPrimitive.Content.displayName; 57 | 58 | const DialogHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
69 | ); 70 | DialogHeader.displayName = "DialogHeader"; 71 | 72 | const DialogFooter = ({ 73 | className, 74 | ...props 75 | }: React.HTMLAttributes) => ( 76 |
83 | ); 84 | DialogFooter.displayName = "DialogFooter"; 85 | 86 | const DialogTitle = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 98 | )); 99 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 100 | 101 | const DialogDescription = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 112 | 113 | export { 114 | Dialog, 115 | DialogPortal, 116 | DialogOverlay, 117 | DialogClose, 118 | DialogTrigger, 119 | DialogContent, 120 | DialogHeader, 121 | DialogFooter, 122 | DialogTitle, 123 | DialogDescription, 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverTrigger, PopoverContent }; 30 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as SelectPrimitive from "@radix-ui/react-select"; 2 | import { Check, ChevronDown, ChevronUp } from "lucide-react"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Select = SelectPrimitive.Root; 8 | 9 | const SelectGroup = SelectPrimitive.Group; 10 | 11 | const SelectValue = SelectPrimitive.Value; 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | span]:line-clamp-1", 21 | className, 22 | )} 23 | {...props} 24 | > 25 | {children} 26 | 27 | 28 | 29 | 30 | )); 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | 46 | 47 | )); 48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 49 | 50 | const SelectScrollDownButton = React.forwardRef< 51 | React.ElementRef, 52 | React.ComponentPropsWithoutRef 53 | >(({ className, ...props }, ref) => ( 54 | 62 | 63 | 64 | )); 65 | SelectScrollDownButton.displayName = 66 | SelectPrimitive.ScrollDownButton.displayName; 67 | 68 | const SelectContent = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, children, position = "popper", ...props }, ref) => ( 72 | 73 | 84 | 85 | 92 | {children} 93 | 94 | 95 | 96 | 97 | )); 98 | SelectContent.displayName = SelectPrimitive.Content.displayName; 99 | 100 | const SelectLabel = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )); 110 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 111 | 112 | const SelectItem = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, children, ...props }, ref) => ( 116 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | {children} 131 | 132 | )); 133 | SelectItem.displayName = SelectPrimitive.Item.displayName; 134 | 135 | const SelectSeparator = React.forwardRef< 136 | React.ElementRef, 137 | React.ComponentPropsWithoutRef 138 | >(({ className, ...props }, ref) => ( 139 | 144 | )); 145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 146 | 147 | export { 148 | Select, 149 | SelectGroup, 150 | SelectValue, 151 | SelectTrigger, 152 | SelectContent, 153 | SelectLabel, 154 | SelectItem, 155 | SelectSeparator, 156 | SelectScrollUpButton, 157 | SelectScrollDownButton, 158 | }; 159 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | import * as SheetPrimitive from "@radix-ui/react-dialog"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import { X } from "lucide-react"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Sheet = SheetPrimitive.Root; 9 | 10 | const SheetTrigger = SheetPrimitive.Trigger; 11 | 12 | const SheetClose = SheetPrimitive.Close; 13 | 14 | const SheetPortal = SheetPrimitive.Portal; 15 | 16 | const SheetOverlay = React.forwardRef< 17 | React.ElementRef, 18 | React.ComponentPropsWithoutRef 19 | >(({ className, ...props }, ref) => ( 20 | 28 | )); 29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; 30 | 31 | const sheetVariants = cva( 32 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 33 | { 34 | variants: { 35 | side: { 36 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 37 | bottom: 38 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 39 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 40 | right: 41 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 42 | }, 43 | }, 44 | defaultVariants: { 45 | side: "right", 46 | }, 47 | }, 48 | ); 49 | 50 | interface SheetContentProps 51 | extends React.ComponentPropsWithoutRef, 52 | VariantProps {} 53 | 54 | const SheetContent = React.forwardRef< 55 | React.ElementRef, 56 | SheetContentProps 57 | >(({ side = "right", className, children, ...props }, ref) => ( 58 | 59 | 60 | 65 | {children} 66 | 67 | 68 | Close 69 | 70 | 71 | 72 | )); 73 | SheetContent.displayName = SheetPrimitive.Content.displayName; 74 | 75 | const SheetHeader = ({ 76 | className, 77 | ...props 78 | }: React.HTMLAttributes) => ( 79 |
86 | ); 87 | SheetHeader.displayName = "SheetHeader"; 88 | 89 | const SheetFooter = ({ 90 | className, 91 | ...props 92 | }: React.HTMLAttributes) => ( 93 |
100 | ); 101 | SheetFooter.displayName = "SheetFooter"; 102 | 103 | const SheetTitle = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )); 113 | SheetTitle.displayName = SheetPrimitive.Title.displayName; 114 | 115 | const SheetDescription = React.forwardRef< 116 | React.ElementRef, 117 | React.ComponentPropsWithoutRef 118 | >(({ className, ...props }, ref) => ( 119 | 124 | )); 125 | SheetDescription.displayName = SheetPrimitive.Description.displayName; 126 | 127 | export { 128 | Sheet, 129 | SheetClose, 130 | SheetContent, 131 | SheetDescription, 132 | SheetFooter, 133 | SheetHeader, 134 | SheetOverlay, 135 | SheetPortal, 136 | SheetTitle, 137 | SheetTrigger, 138 | }; 139 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { Toaster as Sonner } from "sonner"; 3 | 4 | type ToasterProps = React.ComponentProps; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | 26 | ); 27 | }; 28 | 29 | export { Toaster }; 30 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Tabs = TabsPrimitive.Root; 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | TabsList.displayName = TabsPrimitive.List.displayName; 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )); 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )); 51 | TabsContent.displayName = TabsPrimitive.Content.displayName; 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 54 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |